browserbase/skills の browser-to-api を読んだ ── ブラウザを踏ませて OpenAPI 3.1 を自動生成するスキル

開発未分類

きっかけ

朝、X で browserbase/skills というリポジトリが流れてきた。Claude Code 向けにブラウザ自動化と Browserbase 連携をひとまとめにした公式スキル集で、その中に browser-to-api という気になる名前のフォルダがあった。--skill browser-to-api でユーザーが指してきたので、何をするスキルなのかを SKILL.mdREFERENCE.md を読みながら整理した。

結論を先に書くと、これは「ブラウザを操作してサイトを踏み回らせると、その背後で動いている内部 API の OpenAPI 3.1 スペックが裏で勝手に組み上がる」というスキルだった。サイトが OpenAPI を公開していようがいまいが関係ない。観察できる HTTP トラフィックから帰納的に仕様を再構成する。

スキルの立ち位置 ── 「踏む人」と「読む人」の分業

browser-to-api 単体ではトラフィックをキャプチャしない。キャプチャは姉妹スキルの browser-trace の仕事で、こちらは Chrome DevTools Protocol(CDP)のイベントを全部 .jsonl に書き出すロガーになっている。browser-to-api はその出力ディレクトリを入力として読み、後処理で OpenAPI を吐く。

browser-trace    →  .o11y/<run>/cdp/network/{requests,responses}.jsonl
browser-to-api   →  .o11y/<run>/api-spec/index.html + openapi.yaml + client.mjs

CDP のリクエスト/レスポンスを requestId でペアリングして、URL をテンプレート化し、JSON ボディからスキーマを推論し、pathscomponents.schemas を組み立てる。browser-trace 側で browse network on を併用するとレスポンスボディも join できて、レスポンス側のスキーマまで埋まる。CDP の firehose 単体だとリクエストボディ(postData)は取れるが、レスポンスボディは別途 Network.getResponseBody で取りに行かないと埋まらない、という Chrome DevTools の仕様の隙間を回避する形になっている。

パイプラインは 5 段の関数合成

discover.mjs という top-level ディスパッチャがあって、その下で load → filter → normalize → infer → emit が順に走る。それぞれ 1 ファイル 1 ステージで、--stage <name> で部分的に再実行できる。Node 18+ の標準ライブラリだけで動き、npm install も要らない。

ステージやること
loadrequests.jsonl と responses.jsonl を requestId でペアリング。OPTIONS や redirect は捨てる。Image/CSS/Font 等の非API リソースも捨てる。body ディレクトリがあれば join
filter--include / --exclude / --origins を適用。組み込みの除外リスト(analytics、sourcemap、service worker、フォント)も適用
normalizeURL のパスをテンプレート化(/items/42/items/{id})。(origin, method, templatedPath) でグルーピング
inferリクエスト・レスポンスのボディから JSON スキーマを帰納推論。required / enum / format を判定
emitOpenAPI 3.1 を YAML/JSON で出力。同じ構造のスキーマは components.schemas にホイスト。report.mdclient.mjsindex.html も生成

最終成果物の index.html はサーバー不要の自己完結 HTML レポートで、browser-to-api のドキュメントが「常にこれを開け」と言っている。各オペレーションを展開可能なカードで並べ、変数表・curl 例・クライアント呼び出しコード・リクエスト/レスポンス例を全部入れてくれる。client.mjs は zero-dep の fetch ラッパで、各エンドポイントにつき関数が 1 個生える。

パステンプレートの判定ルールが意外と細かい

normalize ステージの中身を REFERENCE.md で確認すると、URL パスのテンプレート化は以下の優先順位で動く。

  1. UUID v1〜v5 → {id}string, format: uuid
  2. 純粋な整数 → {id}integer
  3. Hex / base62 で 8 文字以上 → {id}string
  4. 同位置の短い alpha トークンが複数サンプルで揺れる → {slug}string
  5. それ以外は静的セグメントとして残す

複数の変数セグメントが同じパスにあると {id}, {id2}, {id3} と添字が付く。ここで気になったのは、テンプレート化前のパスは異なるのに正規化後に同じテンプレートに collapse する組が、レスポンスの形(status / content-type)が違う場合は 意図的に分けたまま残してフラグを立てることだ。divergent-response-shape という normalization フラグが付いて confidence.json に記録される。「とりあえず潰す」ではなく「分けるべきものは分かる範囲で分ける」という方針が、過正規化対策として効いている。

スキーマ推論は帰納で、契約じゃない

JSON ボディからの型推論は次のルールで動く。

  • required: すべてのサンプルに存在したフィールドだけ
  • type: 観察された型のユニオン
  • enum: 異なる値が 8 個以下、かつサンプル数 5 件以上のとき
  • format: date-time(ISO っぽい文字列)/ uri / email / uuid を検出

ドキュメント自身が「これは inductive であって contractual ではない」と明示している。「全サンプルに含まれていたから required にしたが、サーバー側では optional かもしれない」「観察した範囲では enum に見えるが、本当はオープンセットかもしれない」というのを正直に書いている。confidence.json でエンドポイントごとの信頼度を low / medium / high の 3 段で開示し、x-confidencex-sample-count を OpenAPI 拡張として埋め込む。下流のクライアントジェネレータや SDK ビルダーが「サンプル少ないからこの型は弱い」と判断できるように設計されている。

GraphQL や JSON-RPC のような多重化エンドポイントを自動分解

これは便利だなと思った機能。/graphql/dapi/fe/gql のように 1 つの URL で多種のオペレーションを多重化するエンドポイントを、リクエストボディの operationName / method / action などのフィールド、あるいはクエリパラメータの opname / op を見て、論理オペレーションごとに分解する。OpenAPI 上は /dapi/fe/gql [Autocomplete] のようにラベル付きの別パスとして並ぶ。GraphQL APQ、JSON-RPC、似た形のディスパッチパターン全般がカバーされる。

「1 エンドポイント = 1 オペレーション」の前提が崩れる現代的な API でも、観察ベースで分けてくれるのは現実的だ。

ノイズフィルタが強い

normalize 段で自動的に捨てられるトラフィックの分類が SKILL.md に書いてある。

  • トラッキング / アナリティクス: /track, /pixel, /beacon, /impression, /pageview
  • Bot 防御: Akamai(/akam/), fingerprint 系(sensor_data), 難読化された多段パス
  • セッション系: /session, /authenticate/start, cookie consent, A/B 実験
  • ページレンダリング自体: GETtext/html を返すもの

これだけで普通のサイトのトラフィックの 60〜80% が落ちる、と書いてあった。実感としては妥当な数字に見える。漁師の魚拓を取るときに、海水ごと展示しても仕方ないのと同じで、欲しいのは魚=API コール部分だけだ。--include で例外を救う設計も入っている。

レダクションも標準で入っている

authorization / cookie / x-api-key / *token* / *secret* のヘッダ、JWT 形式の文字列、メールアドレス、E.164 電話番号は "<redacted>" に置換される。型は保持されるのでスキーマ推論には影響しない。--redact で追加もできる。Auth スキーム自体は OpenAPI の security には書かず、x-observed-auth という拡張に観察値を記録するだけにとどめている。「観察したものは観察したと書く、推測はしない」というスタンスが徹底している。

制約も正直に書いてある

  • 覆える範囲はキャプチャで踏んだフローだけ。踏まなかったエンドポイントは存在しないことになる
  • スキーマは帰納的なので、すべてのサンプルに含まれていたフィールドでもサーバー側では optional かもしれない
  • Auth は観察するだけで、セキュリティスキームを宣言はしない
  • パステンプレートはヒューリスティック。曖昧な URL は confidence.json でフラグが立つ
  • レダクションは best-effort。アプリ固有のシークレットは漏れる余地が残る

このあたりを「完成度」ではなく「観察できた範囲の写し絵」として開示しているのが、ドキュメントとして信頼できる感触に繋がっていた。

読み終えての感想

OpenAPI を「先に書くもの」から「ブラウザを踏ませた後に勝手に出てくるもの」に位置付けを変えるスキルだなと感じた。手動で書いた仕様書よりも、実際のトラフィックから組み上がったスペックの方が、その瞬間の現実に対しては忠実だ。サーバー側のコードを書いた本人がドキュメントを書き忘れていても、クライアント側で踏めば勝手にそれが補える。

業務寄りで考えると、関与先で使っている SaaS の内部 API が変わったことを早期に検知したいときに、定期的に browser-trace で踏ませて browser-to-api で吐かせて、前回スペックとの diff を取る、というモニタリング用途がそのまま使える。サーバー側に Swagger が無くても自前で起こせる。

ただし、踏んだフローでしかカバーできない、というのはずっと残る制約で、これは A/B テストでホールドアウトされてしまった分岐や、エラーパスの観察漏れを意味する。「本物のスペック」ではなく「観察できた範囲のスペック」、という前提を忘れずに使いたい。

利用規約との折り合いについては、踏むサイトごとに個別に判断する話なので、スキル側で吸収できる類のリスクではない。ここは別途整理する必要があると感じた。

参考リンク