🌉

Chrome拡張だけでVercel AI SDKからブラウザを操作する browser-agent-bridge

に公開

LayerX バクラク事業部 エンジニアの @ypresto です。

この記事は LayerX AI エージェントブログリレー 39日目の記事です。

昨日はshibutaniさんの「大TypeScript時代を支えるAsyncLocalStorageによるリクエストスコープなメタデータ管理」でした。AsyncLocalStorageを型安全にするためのテクニックが紹介されていて興味深いのでこちらもぜひ..!


わたしの1本目の記事で、Agentにつながる情報の量が大切と触れていました。一方で、Agentからアクセスしたい情報すべてにAPIがあるわけではありません。特にユーザー認証が必要なページはログイン、2FA、CAPTCHAなどに阻まれて、スクレイピングでの自動化も困難です。

Vercel AI SDKを用いてWebアプリとしてAgentを開発している方は、いっそのことユーザーのブラウザを操作できてしまえば・・と感じたことはないでしょうか。その夢を実現するべく、AI SDKのtoolsインタフェースとChrome拡張をセットにしたライブラリを作成してみました。

https://github.com/ypresto/browser-agent-bridge

※ npm publish・Chromeストア公開はまだなので、まずはお手元でお試しください。公開時に更新して通知します!

動作例

念願の学童保育の申請自動化をチャットUIから実現してみました。

お試し方法

git clone https://github.com/ypresto/browser-agent-bridge.git
cd browser-agent-bridge/pacakges/examples
pnpm i
pnpm run dev

cd ../extension-chrome
pnpm run build

# Chromeで chrome://extensions を開き、「パッケージ化されていない拡張機能を読み込む」を押す
# extension-chromeのディレクトリを選択する

open http://localhost:30001

どういうこと?

ざっくり言うと、Playwright MCPをChrome拡張だけで実現したものになります。開発者はサーバ側のVercel AI SDKにtoolを渡し、クライアント側で橋渡し用のコードを起動し、エンドユーザーはChrome拡張を入れるだけで、AgentをブラウザRPAに対応することができます。

  const browserTools = createBrowserTools(...);

  const result = streamText({
    model: openai('gpt-5-mini'),
    providerOptions: {
      openai: {
        reasoningEffort: 'low'
      },
    },
    system: "...",
    messages,
    tools: {
      ...browserTools,
      ...yourTools,
      }),
    },
    stopWhen: stepCountIs(40),
  });

これに対応するため、 postMessage() やChromeの sendMessage() 、WebSocketを駆使して、Agentから操作対象のタブまでを下記のように接続しています。

Chrome拡張で実装する理由

わたしの前回の記事でも紹介しましたが、Agentによるブラウザ操作をChrome拡張で実現することには下記のメリット・デメリットがあります。

  • メリット
    • いつものブラウザを利用することで、ユーザーのセッションを再利用できる
    • ユーザーにとってインストールが簡単、同じ拡張を複数アプリで使うことも可能
    • ブラウザ操作のためのバックエンド整備が不要
  • デメリット
    • 拡張に任意のページを操作する権限を与える必要がある
    • ブラウザ経由でAgentにアクセスする前提で、バックグラウンドで実行することはできない

今回、拡張機能から任意のページを操作できることについては、拡張機能内に独自のパーミッション管理を実装することでリスクを回避しました。

Chrome拡張にAgentを搭載する vs ブラウザRPAだけ提供する

browser-agent-bridgeはWebアプリ上のAgentから使えるブラウザRPAを提供します。一方でChrome拡張にAgentを埋め込んでリリースする方法も考えられますが、その場合下記の課題が発生します。

  • 独自の拡張機能のリリース管理や、バックエンドの互換性管理の手間
  • 既存のWebアプリと体験が異なってしまう、エンドユーザーが拡張機能を使い分けることになる

1本目の記事にも書きましたが、わたしのブラウザ操作の動機は「学童保育の出欠申請の自動化」でした。このような草の根的な、「身近な」Agentの開発を促進するためには、複雑な部分の外部化が必要だと考えます。AgentフレームワークがAgentの開発を加速するように、ブラウザRPAという「道具」部分を提供することで、ブラウザ対応のAgentの開発のハードルを下げて加速したいと考えています。

(なお @browser-agent-bridge/dom-core は、独自のChrome拡張から使うこともできます)

実装内容

  • Service Worker:拡張機能の司令塔となるスクリプト
    • Content Script間をつなぎ、セッションやパーミッションを管理します
  • Content Script:ユーザーが表示しているページを間接的に読み書きするためのスクリプト
    • Agentが実行されているタブから postMessage() を受け付けて、ServiceWorkerに引き渡します
    • Service Workerの指示を受けて、操作対象タブに対してDOM操作行います
  • Popup:拡張機能のアイコンを押したときに表示されるUI、chrome.action.openPoup() で強制表示できます
    • ユーザーにパーミッション確認します

Service Worker、Content Script、実際のWebページのそれぞれは、セキュリティを高めるために空間が分離 (isolated) されており、 postMessage() / sendMessage() の関数でMessage Passing方式で通信します。

ここからは設計面で工夫した点を紹介します。

セキュリティモデル

ユーザーのブラウザを直接操作できるということは、あらゆる情報のアクセス・変更ができるということなので、セキュリティについて考慮が必要になります。

パーミッション管理の実装

予期しないWebアプリや、予期しないタイミングで操作を開始されるリスクがあります。Agentがブラウザ操作のtoolを呼び出すと、ユーザーに許可を求める形としました。

AgentのWebページ上にボタンを表示すると、Agent側のJSから自動で「許可」ボタンをクリックしたり、クリックジャッキングができてしまいます。そこで、Chrome拡張のUI (Popup) として表示することで、これらの問題を回避しています。

window.postMessage() と originの確認

postMessage() を使うと任意のWebサイトから拡張機能へメッセージを送信できます。MDNにも書かれていますが、送信元を検証することがセキュリティ上必要です。コマンドの送信元が、RPAのセッションを作ったときのoriginと一致している確認することで、別のWebサイトからの乗っ取りを防止しています。

// Content Script

window.addEventListener('message', (event) => {
  ...
	const messageToSend = {
	  type: 'executeCommand',
	  command,
	  origin: event.origin,
	  ...  
	}
	...
})

// Service Worker

if (session.origin !== message.origin) {
  sendResponse({
    error: 'Session token origin mismatch',
    success: false,
  });
  return;
}

あわせて、操作対象のタブが外部リンクを踏むなどして、ユーザーが許可したoriginの外に移動していた場合も、操作に対してエラーを返す対策をしています。

任意のスクリプトの実行を実装しない

Playwright MCPには、任意のスクリプトを実行する browser_evaluate ツールが実装されています。このツールを実装すると、Chrome拡張経由であればSame Origin Policyを無視できることになってしまい、あまり健全とは言えません。今回はリスクが高いと判断し、意図的に実装していません。

Playwright MCPとAccessibility Tree

Claude CodeからPlaywright MCPを使っていた方は、その精度の高さに驚いたのではないかと思います。PlaywrightはgetByRole()getByLabel() といったLocatorで、Accessibility tree (あるいはAccessibility Object Model: AOM) という、スクリーンリーダー (音声読み上げ) などが使うセマンティックな構造情報を利用して稼働しています。Playwright MCPでは、操作中のWebページの “内容” を取得するために使う browser_snapshot ツールから、Accessibility treeをyamlで表現して返却しています。

下記はGoogleのトップページにおけるPlaywright MCPの browser_snapshot のレスポンス例です。

- generic [ref=e2]:
  - navigation [ref=e3]:
    - link "Googleについて" [ref=e4] [cursor=pointer]:
      - /url: https://about.google/?fg=1&utm_source=google-JP&utm_medium=referral&utm_campaign=hp-header
    - link "ストア" [ref=e5] [cursor=pointer]:
      - /url: https://store.google.com/JP?utm_source=hp_header&utm_medium=google_ooo&utm_campaign=GS100042&hl=ja-JP
    - generic [ref=e8]:
      - generic [ref=e9]:
        - link "Gmail" [ref=e11] [cursor=pointer]:
          - /url: https://mail.google.com/mail/?authuser=0&ogbl
        - link "画像を検索する" [ref=e13] [cursor=pointer]:
          - /url: https://www.google.com/imghp?hl=ja&authuser=0&ogbl
          - text: 画像
      - button "Google アプリ" [ref=e16] [cursor=pointer]:
        - img [ref=e17]
      - 'button "Google アカウント: Yuya Tanaka (foo@example.com)" [ref=e21] [cursor=pointer]':
        - img [ref=e22]
  - img "Google" [ref=e25]
  - search [ref=e33]:
    - generic [ref=e35]:
      - generic [ref=e37]:
        - img [ref=e41]
        - combobox "検索" [active] [ref=e44]
        - generic [ref=e45]:
          - generic [ref=e47]:
            - button "音声で検索" [ref=e48] [cursor=pointer]:
              - img [ref=e49]
            - button "画像で検索" [ref=e51] [cursor=pointer]:
              - img [ref=e52]
          - link "AI モード" [ref=e54] [cursor=pointer]:
            - generic [ref=e56]:
              - img [ref=e58]
              - generic [ref=e65]: AI モード
      - generic [ref=e67]:
        - button "Google 検索" [ref=e68] [cursor=pointer]
        - button "I'm Feeling Lucky" [ref=e69] [cursor=pointer]
  ...(省略)...

browser-agent-bridgeでは、Playwrightが操作対象のページに挿入するAccessibility Tree構築のためのスクリプトを切り出して、 snapshot ツールからPlaywright MCPと同じyamlを返却しています (冒頭の構成図では @browser-agent-bridge/dom-core に実装されています)。

これにより、HTMLを直接読み込むよりもトークンを大幅に削減し、精度と速度に寄与していると考えられます。

実際に使う際の工夫

  • 十分な速度で操作できるよう、ある程度賢くて速いモデルを選んでください。今回は "gpt-5-mini"reasoningEffort: "low" で使ってみました。
  • プロンプトでユーザーにあまり質問や確認しないように指示しておいたほうが、スムーズに操作してくれるかもしれません。
  • LLMあるあるですが、現在日時を確認できるツールを用意せずに「11月」のように曖昧な指示を与えると、過年度のページを操作しに行こうとしたので、気をつけてください。

まとめ

Agentからアクセスできる情報が拡大することで、Agentの「可能性」は拡大していきます。browser-agent-bridgeは、MCP化されておらずログイン画面の後ろにある「情報」へのアクセスを拡大する一助にしていくべく、開発を続けていこうと思います!

npmへのpublish時に通知するので、ぜひ チャンネル登録 ・いいねボタンをよろしくお願いします!

LayerX

Discussion