Skip to content

feat: let the agent show and drive the guest browser - #222

Merged
yuga-hashimoto merged 2 commits into
mainfrom
feat/webview-debugging
Aug 5, 2026
Merged

feat: let the agent show and drive the guest browser#222
yuga-hashimoto merged 2 commits into
mainfrom
feat/webview-debugging

Conversation

@yuga-hashimoto

Copy link
Copy Markdown
Owner

Makes the in-app Guest Browser a shared surface for human and agent:

  • WebView.setWebContentsDebuggingEnabled(true) so the in-guest agent can attach over CDP (devtools abstract socket; reachable from the guest, which shares the app UID).
  • Guest browser route takes an optional url argument.
  • GuestBrowserCommandWatcher polls the active workspace for .and-code/browser-command.json and opens the guest browser at the requested URL.
  • guest-tools/andcode-browser-mcp: MCP server with browser_show/status/navigate/click/type/screenshot/info; CDP client implemented over AF_UNIX sockets (no extra deps).

Flow: agent calls browser_show(url) → app opens the screen for the user → agent drives the same WebView via CDP while the user watches/operates.

- Enable WebView CDP debugging so the in-guest agent can attach to the
  Guest Browser WebView through the devtools abstract socket.
- Guest browser route accepts an optional url argument.
- GuestBrowserCommandWatcher polls the active workspace for
  .and-code/browser-command.json and opens the guest browser at the
  requested URL, so the agent can put the browser in front of the user.
- Add guest-tools/andcode-browser-mcp: an MCP server exposing
  browser_show/status/navigate/click/type/screenshot/info to the agent
  (CDP client implemented over AF_UNIX, no extra dependencies).
@yuga-hashimoto
yuga-hashimoto merged commit 6d73450 into main Aug 5, 2026
5 checks passed
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 10 issue(s) in this PR.

  • ✅ Successfully posted inline: 10 comment(s)

Comment on lines +39 to +41
if (!commandFile.exists()) {
return null
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[maintainability · low]
runCatching は inline 関数のため、この return null は非ローカル return となり consumeOpenCommand 全体から抜けます(getOrNull() もスキップ)。動作はしますが意図が分かりにくく、将来このブロックを inline でない関数に抽出した際にコンパイルエラーになる等、壊れやすい構造です。return null ではなく return@runCatching null を使うか、明示的な return を持つ関数本体に書き換えると読みやすくなります。

Comment on lines +42 to +44
val text = commandFile.readText()
commandFile.delete()
JSONObject(text).optString("url").takeIf { it.isNotBlank() }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
commandFile.delete() の戻り値が無視されています。削除に失敗した場合(権限不足・ファイルロック等)は同じコマンドファイルが残るため、ポーリングのたびに同じ URL が再処理され onOpenUrl(および navController.navigate)が毎秒繰り返し呼ばれます。また、readText → delete → パースの順序のため、エージェントの書き込み途中(JSON 未完成)を読み込んだケースではパース前にファイルが削除され、コマンドが静かに失われます(エージェントには「要求を受け付けた」と返っているのにブラウザが開かない)。パースを先に行い、パース成功かつ delete() が true を返した場合のみ消費する形にすると、両方の問題を回避できます。

Suggestion:

Suggested change
val text = commandFile.readText()
commandFile.delete()
JSONObject(text).optString("url").takeIf { it.isNotBlank() }
private fun consumeOpenCommand(commandFile: File): String? {
if (!commandFile.exists()) return null
return runCatching {
val text = commandFile.readText()
val url = JSONObject(text).optString("url").takeIf { it.isNotBlank() }
if (url != null && commandFile.delete()) url else null
}.getOrNull()
}

// Lets the in-guest agent drive this WebView over CDP (the devtools abstract
// socket is reachable from the guest, which shares the app's UID), so a page
// can be shown to the user and operated by human and agent at the same time.
WebView.setWebContentsDebuggingEnabled(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security · medium]
WebView.setWebContentsDebuggingEnabled(true) はプロセス全体に効くグローバル設定です。有効にすると、webview_devtools_remote_<pid> 抽象ソケットへ接続できるすべてのプロセスが、アプリ内の全 WebView の内容を CDP 経由で読み取り・書き換え・JS インジェクション・フォーム入力/認証情報の傍受ができるようになり、Android 公式ガイダンスでも本番ビルドでの有効化は推奨されていません。ゲストエージェントからの CDP 接続が本機能の意図だとしても、BuildConfig のビルドタイプや設定フラグで有効/無効を切り替えられるようにし、リリース配布時に意図せずデバッグ機能が有効にならないよう制御することを推奨します。

// Lets the in-guest agent drive this WebView over CDP (the devtools abstract
// socket is reachable from the guest, which shares the app's UID), so a page
// can be shown to the user and operated by human and agent at the same time.
WebView.setWebContentsDebuggingEnabled(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[maintainability · low]
この設定はプロセスグローバルな静的設定であり、WebView インスタンスごとに呼ぶ必要はありません。AndroidView の factory は再コンポジションや画面再生成のたびに再実行され得るため、この場所に置くと冗長で、グローバル設定が UI の詳細実装に埋もれてしまいます。Application.onCreate() などで一度だけ呼び出す方が、設定の意図と適用タイミングが明確になります。

Comment on lines +570 to +575
GuestBrowserCommandWatcher(
workspacePath = chatState.selectedWorkspacePath,
onOpenUrl = { url ->
navController.navigate(guestBrowserRoute(url)) { launchSingleTop = true }
},
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[performance · low]
このウォッチャーはアプリのルート AndCodeApp に常時マウントされ、BrowserCommandWatcher 内の while(true) ループがワークスペース選択中は 1 秒間隔のファイルポーリングをアプリのライフサイクル全体にわたって実行し続けます。ゲストブラウザを実際に使っていない時やアプリがバックグラウンドにある間も継続するため、定常的なディスクI/O(バッテリー・CPU消費)になります。

機能上はエージェントがバックグラウンドで動作している前提なので常時監視自体は意図的と思われますが、ローカルランタイムが起動中の時だけ監視する、コマンドファイルを置くディレクトリの変更を FileObserver 等でイベント駆動に切り替える、といった形でポーリングを限定することを検討してください。


composable(ROUTE_GUEST_BROWSER) {
composable(GUEST_BROWSER_ROUTE_PATTERN) { backStack ->
val requestedUrl = backStack.arguments?.getString(GUEST_BROWSER_ARG_URL)?.let { decodeRouteArg(it) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
decodeRouteArg は内部で Base64.getUrlDecoder().decode() を実行するため、base64url として不正な文字列が渡されると IllegalArgumentException を投げてコンポーズ時にクラッシュします。同じファイルの CODE_VIEWER ルート(runCatching で包んでいる)や他サイトと異なり、この新規コードは防御なしで呼び出しています。url 引数が不正な場合(例: ディープリンクや非エンコード経路からの遷移)にクラッシュし得るため、runCatching { ... }.getOrNull() で包んで不正引数を吸収することを推奨します。

Suggestion:

Suggested change
val requestedUrl = backStack.arguments?.getString(GUEST_BROWSER_ARG_URL)?.let { decodeRouteArg(it) }
val requestedUrl =
backStack.arguments?.getString(GUEST_BROWSER_ARG_URL)?.let { arg ->
runCatching { decodeRouteArg(arg) }.getOrNull()
}

mcp = FastMCP("and-code-browser")

COMMAND_FILE = Path(".and-code") / "browser-command.json"
CDP_BRIDGE_PORT_NOTE = "abstract socket only; no TCP bridge needed"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[maintainability · low]
定数 CDP_BRIDGE_PORT_NOTE は定義されているだけでコード内のどこからも参照されておらず、未使用のデッドコードです。削除するか、実コメントとして活用してください。

Comment on lines +45 to +52
hexname = parts[-1]
try:
name = bytes.fromhex(hexname).decode("ascii")
except (ValueError, UnicodeDecodeError):
continue
if name.startswith("webview_devtools_remote_"):
return name
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · high]
/proc/net/unix の Path 列は abstract socket 名を @webview_devtools_remote_<pid> のようなリテラル文字列で表示します(16進数エンコードではありません)。そのため bytes.fromhex(hexname) は必ず ValueError を投げ、except で握りつぶされて continue するため、この関数は常に None を返します。結果として browser_navigate / browser_click / browser_type / browser_screenshot / browser_info はすべて「socket not found」で失敗し、browser_status も常に未接続を返すことになります。さらに、先頭の @ を除去せずに connect("\0" + name) へ渡すと誤った abstract アドレスへ接続するため、@ を除いた名前を返す必要があります。

Suggestion:

Suggested change
hexname = parts[-1]
try:
name = bytes.fromhex(hexname).decode("ascii")
except (ValueError, UnicodeDecodeError):
continue
if name.startswith("webview_devtools_remote_"):
return name
return None
path = parts[-1]
# /proc/net/unix では abstract socket 名は "@webview_devtools_remote_<pid>" のように
# リテラル文字列で表示される(16進数ではない)。先頭の "@" を取り除いて返す。
if path.startswith("@") and path[1:].startswith("webview_devtools_remote_"):
return path[1:]
return None

Comment on lines +74 to +79
resp = b""
while b"\r\n\r\n" not in resp:
chunk = sock.recv(4096)
if not chunk:
raise CdpError("socket closed during ws handshake")
resp += chunk

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
ハンドシェイクで \r\n\r\n が見つかった時点で読み込みを終了し、resp に受信済みの余剰バイト(同じ TCP セグメントに同梱された最初の WebSocket フレームなど)がすべて破棄されます。フレームの途中で切れていた場合は以降のフレーム境界がずれ、recv_text() が不正なデータを返して CDP セッションが壊れる可能性があります。受信バッファを保持して次の読み込みで使い回すバッファリングに変更してください。

"""開いているゲストブラウザで URL へ遷移します。"""
page = _session().open_page_session()
try:
page.call("Page.navigate", url=url)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security · medium]
browser_navigate は引数 URL を無検証で Page.navigate に渡し、browser_show も同様にコマンドファイルへそのまま書き込みます。WebView は javaScriptEnabled = true で、アプリ側の normalizeUrl は http/https に正規化しますが、このツールはそれを迂回します。エージェントがページ内コンテンツ(プロンプトインジェクション等)に誘導された場合、file://javascript: URL によってローカルリソースの読み込みや WebView 内でのコード実行につながる可能性があります。http/https のみ許可するスキーム検証を追加してください。

yuga-hashimoto added a commit that referenced this pull request Aug 5, 2026
The runtime installer now seeds /usr/local/bin/andcode-browser-mcp.py
(stdlib-only MCP server, no third-party deps) into both the Alpine and
Antigravity rootfses and registers it in each agent's config:

- OpenCode:  ~/.config/opencode/opencode.json (mcp.and-code-browser)
- Claude:    ~/.claude.json (mcpServers.and-code-browser)
- Antigravity: ~/.gemini/config/mcp_config.json

Provisioning is idempotent, preserves user-added servers, and also runs
on startup for runtimes installed before this change, so OpenCode,
Claude Code and Antigravity all expose the same browser_* tools.

Supersedes the guest-tools script from #222.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant