
hermes(別の機械で動いている、ぼくの agent)に、自分でアカウントへログインしてほしかった。先にぼくがログインして cookie を渡すのではなく、最初から自分でログインする形だ。
ログイン方法はだいたい 2 種類しかない。1 つはアカウントとパスワード。agent がぼくの Bitwarden 金庫からユーザー名、パスワード、2FA 認証コードを取り出して、入力すれば終わり——この経路は bitwarden-use で通せた。人の手をまったく使わずに github にログインできたし、passkey までいける(秘密鍵を金庫から取り出してローカルで WebAuthn に署名するので、スマホを取り出して指紋を触る必要はない)。もう 1 つは「Sign in with Google」のような第三者 OAuth。こっちで半日以上つまずいた。
以下では、なぜ難しいのか、そして chrome-use が最終的にどう通したのかを説明する。
1 回のクリックの裏で、3 つのウィンドウが会話している
x のログインページで「Google アカウントでログイン」をクリックすると、見た目は 1 回の操作だけだが、裏ではこうなっている。
- その Google ボタンは x が自分で描いているものではなく、
accounts.google.comのクロスオリジン iframe が差し込まれている。 - クリックすると popup ウィンドウ が開き、その中でアカウントを選ぶ。
- 選び終えると、Google は token を URL に書くのではなく、この popup を中継ページ
accounts.google.com/gsi/transformにリダイレクトする。 gsi/transformがやることは 1 つだけ。window.openerを通じて id_token を最初のログインページ(opener)へ postMessage で返し、その後自分を閉じる。- opener が token を受け取って、x 側のログインが成功し、
/homeに移動する。
重要なのは 4 番目だ。token は popup から opener へ、ウィンドウ越しに呼びかける形で返される。その土台が window.opener の関係だ。この関係が切れると、token は空気に向かって叫ぶだけになる。popup は gsi/transform の上で止まったまま閉じず、x 側はいつまでも受け取れず、数秒後にログイン選択ページへ戻ってしまう。
自動化すると、なぜ触っただけで切れるのか
ぼくは chrome-use で本物の Chrome を操作している(ブラウザ拡張を通じてタブを引き受ける仕組みで、relay モードと呼んでいる)。問題は 3 か所にあった。
1 つ目、opener を自分たちで誤って消していた。 relay は各タブのデバッグセッションに tabId をキーとして使っている。x のログインページが一度遷移すると(クロスプロセス遷移なので)、Chrome はこのタブに新しい tabId を割り当て、古いキーは無効になる。さらに悪いことに、その時点の「復旧」ロジックは、無効なセッションに当たると、そのタブを破棄し、新しい空白タブを開いて置き換えていた。破棄されていたのは、まさに token を待っている opener だった。ログを半日ほど見続けたが、毎回 token が戻る瞬間で止まっていた。opener が自分たちの復旧ロジックに消されていたのだ。
2 つ目、relay から Google ボタンが見えなかった。 クロスオリジン iframe は Chrome 内では独立したレンダラープロセス(OOPIF)になる。chrome-use が CDP に直接つなぐ場合は自動的に中へ入り、iframe 内の本物の「Continue as 立」ボタンが見える。だが relay モードでは、拡張がタブの最上位にしか attach しておらず、iframe はブラックボックスだった。座標を頼りに当てずっぽうでクリックするしかなく、開けたのも「別のアカウントを使う」という汎用フローで、個人化されたワンクリックログインではなかった。
3 つ目、relay が popup に attach できなかった。 Google のアカウント選択 popup が開いた瞬間に切り替えてアカウントをクリックしようとすると、拡張が「タブが応答しない」と返した。popup のロード完了を待ってからゆっくり attach しようとしていたが、一瞬で通り過ぎる OAuth ウィンドウには遅すぎた。
さらに隠れた落とし穴もあった。長く調べてようやく分かったのだが、拡張が使う native-messaging host は、インストール済みの古いバイナリを指していて、ローカルで新しくビルドしたものではなかった。つまり、それまでの「パッチを当てたテスト」は、全部古いロジックで走っていた。その wrapper スクリプトを新しくビルドしたバイナリに向けて、ようやくパッチが本当に有効な経路に入った。
3 つの修正
上の 3 か所に対して、それぞれ 1 つずつ手を入れた。
opener を守る。 セッションが無効になってもタブを破棄しないようにし、非破壊的な再接続に変えた。同じ targetId はまだ存在している(sessionId が入れ替わっただけ)ので、再 attach して新しい sessionId を取ればよい。読み取りやクリックのようなコマンドで無効セッションに当たっても、拡張側がセッションを復旧するまで数回リトライを許し、最初の 1 回で強くエラーにしないようにした。
iframe を貫通する。 attach 時に拡張が自分から Target.setAutoAttach({flatten:true}) を開くようにし、Google iframe という子セッションのイベントをデーモンへ転送させた。コマンド側も子 sessionId に合わせて、その iframe 内へルーティングする。これでスナップショットが中まで入れるようになり、「Continue as 立」にクリック可能な ref が付く——CDP 直接接続と同じになった。
popup についていく。 拡張は chrome.tabs.onCreated を使い、popup が作られた瞬間に attach するようにした(opener が自分の管理下にあるタブのものだけを対象にし、別のウィンドウを誤って巻き込まないようにしている)。デーモンはそれを副次タブとして保持するが、メインタブは奪わない。opener はそのまま固定され、token を待ち続ける。
通った
パッチを入れて、拡張をリロードしてから、もう一度走らせた。
- スナップショットで、GSI iframe 内の「Continue as 立, leeguooooo@gmail.com」が見えるようになり、それをクリックする。
- アカウント選択 popup が出るので切り替える(今回は「応答しない」とは出ない)。そこでアカウントを選ぶ。
- 選んだ後は何も触らず、gsi/transform に token を opener へ postMessage で返させる。
- opener は破棄されず、token を受け取る——x は
/homeに移動し、ログイン状態になる。
最初から最後まで、人のクリックは 1 回もない。この硬い骨は、どうにか噛み砕けた。
仕上げ
agent が自分でアカウントにログインする経路は、これで 2 本とも通った。OAuth のような第三者遷移は chrome-use が自分でクリックし切る。アカウントとパスワード、2FA、passkey のようなものは、bitwarden-use があなたの金庫から取り出して入力する。
- chrome-use:github.com/leeguooooo/chrome-use
- bitwarden-use:github.com/leeguooooo/bitwarden-use

微信
支付宝
コメント
コメントは即時公開されますが、ポリシー違反時は非表示になる場合があります。