You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Handoff: drive local agents over ACP in the termchart bridge (unlock browser/tools/approvals); logged-in browsing blocked by Chrome default-profile automation #331
Handoff: drive local agents over ACP in the termchart bridge (unlock browser / tools / approvals)
Status: spike proven; blocked on logged-in browser auth by a Chrome security restriction. Another agent can pick this up cold from this issue.
Author of work: prior agent session (Ivan). Written for a fresh agent with zero prior context.
0. TL;DR
termchart's local bridge (packages/cli → termchart bridge) drives locally-installed AI CLIs. Today it shells out one-shot (claude -p, codex exec -, gemini -p). That strips MCP servers, tools, approvals, and persistent sessions — e.g. Claude reports NO_BROWSER_TOOL, so "open Amazon and check my orders" is impossible through the bridge.
T3 Code (reference architecture) instead speaks each agent's native bidirectional protocol: ACP (Agent Client Protocol) for Claude, and Codex's app-server JSON-RPC. That gives full tool use, per-session MCP injection, streaming, and interactive permissions.
Decision (user): adopt ACP for the termchart bridge; surface permission requests to the user as approvals.
A standalone ACP spike (in /tmp/acp-spike, ephemeral — full code embedded below) proved ACP gives Claude a real browser: injected the Playwright MCP into session/new, Claude ran mcp__playwright__browser_navigate/find/snapshot and read live pages.
Amazon dry-run: isolated (logged-out) browser reached Amazon with no captcha (good feasibility signal), reported NOT_LOGGED_IN. Using the real logged-in session is the open problem (see §5).
Blocker: modern Chrome refuses --remote-debugging automation when --user-data-dir is the Default profile (anti-cookie-theft). So Playwright can't drive the real profile directly. Viable paths: copy the profile to a non-default dir, or Playwright MCP --extension (attach to live Chrome). Claude Desktop's "import cookies" is not reachable via ACP (different product).
1. Goal
Let the termchart bridge drive local agents with their full capabilities (tools, MCP servers, approvals, persistent sessions), so lifeboard can ask an agent to do real auth-gated web tasks (the motivating example: "open Amazon and check recent orders") without us writing any auth/cookie code — the browser owns the session.
2. Repos & where things live
termchart — github.com/ivanmkc/termchart, branch lifeboard/19-review-fixes. This is where the bridge + integration live.
Bridge CLI command: packages/cli/src/bridge.ts (dispatched from packages/cli/src/cli.ts:366).
Bridge server + drivers: packages/cli/src/bridge/server.ts, drivers.ts, jobs.ts, security.ts, schedule.ts, notify.ts.
⚠️ The built packages/cli/dist/cli.js is stale (missing the bridge command). Run from source: npx tsx packages/cli/src/cli.ts bridge --port 8787, or rebuild (npm run build:cli).
T3 Code — reference architecture (pingdotgg/t3code, cloned at /tmp/t3code). Key packages: packages/effect-acp (ACP client, downloads the agentclientprotocol/agent-client-protocol schema), packages/effect-codex-app-server (Codex app-server), apps/server/src/provider/Drivers/ClaudeExecutable.ts, apps/server/src/cli/triage.ts.
3. The termchart bridge today (verified capability matrix)
HTTP API (loopback only; auth = Authorization: Bearer <token> + trusted Origin: http://127.0.0.1 for writes):
GET /health → {ok, version, capabilities:[agents,jobs,notify]}
GET /agents → detected drivers + auth + models
POST /agents/:id/run {prompt, model?} → sync one-shot turn → {output} (prompt on stdin)
POST /jobs {driverId, prompt} → 201 {id}; GET /jobs/:id; GET /events (SSE, event: job); jobs are a bridge-side multi-step loop (≤20 steps, 5min/step) and return a canvas component ({"type":"component",...}) for lifeboard.
/schedule, /notify.
Live results (this machine): claude 2.1.259 (Claude Pro) — sync run ✅ and ran Bash unattended (no approval gate). codex 0.153.0 (ChatGPT Plus) — invocation OK but usage-limited (resets ~Sep 7). gemini 0.17.1 (Google AI) — broken (see §4).
4. Bugs / findings in the current bridge (worth fixing regardless of ACP)
Gemini driver mis-wired (packages/cli/src/bridge/drivers.ts): runs gemini -p with the prompt on stdin, but gemini -p wants the prompt as an argument → "Not enough arguments following: p". Gemini is currently undrivable. (Note: gemini --experimental-acp speaks ACP natively — could be the fix + ACP proof for gemini.)
Codex error truncation (runDriver): slices errors to 300 chars = codex's startup banner, hiding the real reason (the "usage limit" message was cut off).
Codex model mismatch: /agents advertises gpt-5; codex actually launches gpt-5.6-luna.
No MCP/tools/approvals via claude -p: the driver passes ["-p", ...model] with no --mcp-config/permission flags, so no browser/MCP tools load and there's no approval interaction. This is the core capability gap ACP fixes.
5. What was proven with ACP (the spike)
Setup: mkdir /tmp/acp-spike && cd /tmp/acp-spike && npm init -y (set "type":"module"), then npm i @zed-industries/agent-client-protocol@0.4.5. Claude ACP agent runs via npx -y @zed-industries/claude-code-acp (v0.16.2). Browser MCP via npx -y @playwright/mcp@latest.
spike.mjs (example.com proof): ACP initialize → session/new with Playwright MCP in mcpServers → session/prompt → Claude issued mcp__playwright__browser_navigate/find/snapshot and returned the real <h1>"Example Domain". stopReason: end_turn. ✅ ACP + injected MCP gives Claude a browser tool claude -p lacks.
amazon-profile.mjs (isolated variant reached Amazon logged-out with no captcha at the homepage → strong feasibility signal; profile variant hit the blocker in §6).
Gotchas discovered:
Env sanitization required: launching the ACP agent from inside another Claude session trips a nested-session guard. Strip CLAUDECODE* env vars from the child's env (for k: if /^CLAUDECODE$|^CLAUDE_CODE/ delete).
ACP schema-version mismatch (non-fatal):@zed-industries/agent-client-protocol@0.4.5 rejects some tool_call_update notifications (rawOutput field) from claude-code-acp@0.16.2 with -32602 Invalid params. Turn still completes. Fix by bumping the lib, matching versions, or a tolerant validator.
To allow a session/request_permission, respond { outcome: { outcome: "selected", optionId: <the allow_once option's optionId> } }.
6. The current blocker — logged-in browser auth
Motivating task needs the user's logged-in Amazon session. Findings:
Claude Desktop's "import cookies" is NOT reachable via ACP. ACP drives Claude Code (CLI), which has no built-in browser/cookie capability. The cookie importer belongs to the Claude Desktop app (/Applications/Claude.app, its own Electron cookie store) — different product/runtime. Dead end for the bridge.
Direct --user-data-dir=<Default profile> is blocked by Chrome. Playwright launches Chrome but launchPersistentContexttimes out on --remote-debugging-pipe: recent Chrome refuses remote-debugging/automation on the Default profile (anti-cookie-theft). Not a lock and not our bug (confirmed with playwright-corechromium.launchPersistentContext(defaultProfile, {channel:'chrome'}) → timeout after Chrome launches pid).
Operational notes: Chrome must be fully quit first (it locks the profile); a hard kill leaves stale ~/Library/Application Support/Google/Chrome/Singleton* symlinks that must be rm'd; failed Playwright launches can leave zombie Google Chrome processes — clean them.
Default profile is 3.7 GB (~1.8 GB minus caches).
Viable "browser owns cookies" options (pick one):
(A) Copy the profile to a non-default dir (exclude Cache, Code Cache, GPUCache, Service Worker), launch Playwright/Chrome with --user-data-dir=<copy>. Chrome's restriction targets the default path, so a copy is automatable and carries cookies (decryptable on same machine/Keychain — may trigger a one-time macOS Keychain prompt). Fully autonomous otherwise.
(B) Playwright MCP --extension: install the Playwright MCP browser extension in the user's Chrome once, start MCP with --extension, connect → Claude drives the live logged-in Chrome. Cleanest match to "use my logged-in browser"; requires a one-time manual extension install and reconnect.
(C) One-time login in the Playwright MCP's own persistent (non-isolated) profile — user logs into Amazon once; stays logged in thereafter.
✅ Prove the logged-in Amazon dry-run first, then integrate ACP into the bridge.
⚠️ User initially chose "use my Chrome profile" — now known blocked (§6). Needs a re-pick between (A)/(B)/(C).
8. Next steps (ordered, concrete)
Unblock logged-in browsing — implement option (A) profile-copy (most autonomous) or (B) extension. Re-run the Amazon dry-run (login-state only, STOP before orders — see prompt in the embedded code) to confirm LOGGED_IN. Then, with user OK, the real "recent orders" read.
Fix the gemini driver and codex error truncation in packages/cli/src/bridge/drivers.ts (quick wins, independent of ACP).
Integrate an ACP driver into the termchart bridge:
Add an acp-backed runner alongside runDriver in drivers.ts (or a new bridge/acp.ts). Spawn the ACP agent (claude-code-acp; gemini via --experimental-acp), connect with @zed-industries/agent-client-protocolClientSideConnection + ndJsonStream over the child's stdio.
T3 Code reference: pingdotgg/t3code — packages/effect-acp, packages/effect-codex-app-server.
Appendix A — spike.mjs (example.com ACP+browser proof)
// ACP spike: drive real Claude Code over ACP with a browser MCP injected.import{spawn}from"node:child_process";import{Writable,Readable}from"node:stream";import{ClientSideConnection,ndJsonStream,PROTOCOL_VERSION}from"@zed-industries/agent-client-protocol";constlog=(...a)=>console.log("[spike]", ...a);constagentText=[],toolCalls=[];constcleanEnv={ ...process.env};for(constkofObject.keys(cleanEnv))if(/^CLAUDECODE$|^CLAUDE_CODE/.test(k))deletecleanEnv[k];constchild=spawn("npx",["-y","@zed-industries/claude-code-acp"],{stdio: ["pipe","pipe","pipe"],env: cleanEnv});child.stderr.on("data",(d)=>process.stderr.write(`[acp-agent] ${d}`));constclient={asyncsessionUpdate({ update }){constk=update.sessionUpdate;if(k==="agent_message_chunk")agentText.push(update.content?.text??"");elseif(k==="tool_call"){toolCalls.push(update.title||update.toolCallId);log("TOOL_CALL:",update.title||update.kind);}},asyncrequestPermission({ options }){constallow=options.find(o=>o.kind==="allow_once")||options.find(o=>o.kind==="allow_always")||options[0];return{outcome: {outcome: "selected",optionId: allow.optionId}};},asyncreadTextFile({ path }){const{ readFile }=awaitimport("node:fs/promises");return{content: awaitreadFile(path,"utf8")};},asyncwriteTextFile({ path, content }){const{ writeFile }=awaitimport("node:fs/promises");awaitwriteFile(path,content);return{};},};constconn=newClientSideConnection(()=>client,ndJsonStream(Writable.toWeb(child.stdin),Readable.toWeb(child.stdout)));constdone=(async()=>{awaitconn.initialize({protocolVersion: PROTOCOL_VERSION,clientCapabilities: {fs: {readTextFile: true,writeTextFile: true},terminal: false}});constsess=awaitconn.newSession({cwd: "/tmp/acp-spike",mcpServers: [{name: "playwright",command: "npx",args: ["-y","@playwright/mcp@latest","--headless","--browser","chrome"],env: []}]});constres=awaitconn.prompt({sessionId: sess.sessionId,prompt: [{type: "text",text: "Use your browser tool (Playwright MCP) to navigate to https://example.com and tell me the EXACT text of the page's <h1>. Reply with only that heading."}]});log("stopReason:",res.stopReason);})();try{awaitPromise.race([done,newPromise((_,r)=>setTimeout(()=>r(newError("timeout")),220000))]);log("TOOL CALLS:",toolCalls.join(" | "));log("AGENT SAID:",agentText.join("").trim().slice(0,300));}catch(e){log("FAILED:",e.message);}finally{child.kill("SIGKILL");setTimeout(()=>process.exit(0),500);}
Appendix B — amazon-profile.mjs (logged-in dry-run; uses real Chrome profile — currently blocked, see §6)
Same client/connection scaffold as Appendix A, plus: a guard that aborts if pgrep -x "Google Chrome" is non-empty; mcpServers uses the real profile:
You have a browser tool (Playwright MCP) running with the user's real Chrome profile. This is a DRY RUN —
do NOT read/list/open any orders or personal data, and do NOT type credentials.
(1) navigate to https://www.amazon.com ; (2) determine only whether the session is signed in — a greeting
like 'Hello, <FirstName>' / account menu vs a 'Sign in' link; (3) if a captcha/robot check/OTP appears, note it.
Then STOP — do NOT click Orders, do NOT reveal the account name. Reply with ONLY one token on the first line:
LOGGED_IN | NOT_LOGGED_IN | CHALLENGE — then one short sentence of evidence (no personal data).
Pre-req before running the profile variant: fully quit Chrome, then rm -f ~/Library/Application\ Support/Google/Chrome/Singleton*. (This variant is the one blocked by Chrome's default-profile automation restriction — switch to option (A)/(B)/(C) in §6.)
Appendix C — reproduce the bridge capability tests
# start bridge (dist is stale; run from source)cd~/code/termchart && npx tsx packages/cli/src/cli.ts bridge --port 8787 # prints token
TOK=<token>
curl -s http://127.0.0.1:8787/health
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:8787/agents
curl -s -X POST -H "Authorization: Bearer $TOK" -H "Origin: http://127.0.0.1" -H "content-type: application/json" \
-d '{"prompt":"Reply with one word: PONG"}' http://127.0.0.1:8787/agents/claude/run
Handoff: drive local agents over ACP in the termchart bridge (unlock browser / tools / approvals)
Status: spike proven; blocked on logged-in browser auth by a Chrome security restriction. Another agent can pick this up cold from this issue.
Author of work: prior agent session (Ivan). Written for a fresh agent with zero prior context.
0. TL;DR
packages/cli→termchart bridge) drives locally-installed AI CLIs. Today it shells out one-shot (claude -p,codex exec -,gemini -p). That strips MCP servers, tools, approvals, and persistent sessions — e.g. Claude reportsNO_BROWSER_TOOL, so "open Amazon and check my orders" is impossible through the bridge./tmp/acp-spike, ephemeral — full code embedded below) proved ACP gives Claude a real browser: injected the Playwright MCP intosession/new, Claude ranmcp__playwright__browser_navigate/find/snapshotand read live pages.NOT_LOGGED_IN. Using the real logged-in session is the open problem (see §5).--remote-debuggingautomation when--user-data-diris the Default profile (anti-cookie-theft). So Playwright can't drive the real profile directly. Viable paths: copy the profile to a non-default dir, or Playwright MCP--extension(attach to live Chrome). Claude Desktop's "import cookies" is not reachable via ACP (different product).1. Goal
Let the termchart bridge drive local agents with their full capabilities (tools, MCP servers, approvals, persistent sessions), so lifeboard can ask an agent to do real auth-gated web tasks (the motivating example: "open Amazon and check recent orders") without us writing any auth/cookie code — the browser owns the session.
2. Repos & where things live
github.com/ivanmkc/termchart, branchlifeboard/19-review-fixes. This is where the bridge + integration live.packages/cli/src/bridge.ts(dispatched frompackages/cli/src/cli.ts:366).packages/cli/src/bridge/server.ts,drivers.ts,jobs.ts,security.ts,schedule.ts,notify.ts.packages/cli/dist/cli.jsis stale (missing thebridgecommand). Run from source:npx tsx packages/cli/src/cli.ts bridge --port 8787, or rebuild (npm run build:cli).github.com/ivanmkc/agent-bridge(sibling prototype). PR feat(render): fail-fast guard for oversized graphs + edge-case verifications #3 (uca-phase1) shipped R40 = interactive stream-json (control_request → approval.requested), the proprietary analog of ACP's permission flow. Issues fix(er): render full ER relationship labels (patch + bundle beautiful-mermaid) #1/feat(cli): --rounded (rounded corners/elbows) #2 are its control-plane/board spec. Useful precedent for the approvals model.pingdotgg/t3code, cloned at/tmp/t3code). Key packages:packages/effect-acp(ACP client, downloads theagentclientprotocol/agent-client-protocolschema),packages/effect-codex-app-server(Codex app-server),apps/server/src/provider/Drivers/ClaudeExecutable.ts,apps/server/src/cli/triage.ts.3. The termchart bridge today (verified capability matrix)
HTTP API (loopback only; auth =
Authorization: Bearer <token>+ trustedOrigin: http://127.0.0.1for writes):GET /health→{ok, version, capabilities:[agents,jobs,notify]}GET /agents→ detected drivers + auth + modelsPOST /agents/:id/run {prompt, model?}→ sync one-shot turn →{output}(prompt on stdin)POST /jobs {driverId, prompt}→201 {id};GET /jobs/:id;GET /events(SSE,event: job); jobs are a bridge-side multi-step loop (≤20 steps, 5min/step) and return a canvas component ({"type":"component",...}) for lifeboard./schedule,/notify.Live results (this machine):
claude2.1.259 (Claude Pro) — sync run ✅ and ran Bash unattended (no approval gate).codex0.153.0 (ChatGPT Plus) — invocation OK but usage-limited (resets ~Sep 7).gemini0.17.1 (Google AI) — broken (see §4).4. Bugs / findings in the current bridge (worth fixing regardless of ACP)
packages/cli/src/bridge/drivers.ts): runsgemini -pwith the prompt on stdin, butgemini -pwants the prompt as an argument →"Not enough arguments following: p". Gemini is currently undrivable. (Note:gemini --experimental-acpspeaks ACP natively — could be the fix + ACP proof for gemini.)runDriver): slices errors to 300 chars = codex's startup banner, hiding the real reason (the "usage limit" message was cut off)./agentsadvertisesgpt-5; codex actually launchesgpt-5.6-luna.claude -p: the driver passes["-p", ...model]with no--mcp-config/permission flags, so no browser/MCP tools load and there's no approval interaction. This is the core capability gap ACP fixes.5. What was proven with ACP (the spike)
Setup:
mkdir /tmp/acp-spike && cd /tmp/acp-spike && npm init -y(set"type":"module"), thennpm i @zed-industries/agent-client-protocol@0.4.5. Claude ACP agent runs vianpx -y @zed-industries/claude-code-acp(v0.16.2). Browser MCP vianpx -y @playwright/mcp@latest.spike.mjs(example.com proof): ACPinitialize→session/newwith Playwright MCP inmcpServers→session/prompt→ Claude issuedmcp__playwright__browser_navigate/find/snapshotand returned the real<h1>"Example Domain".stopReason: end_turn. ✅ ACP + injected MCP gives Claude a browser toolclaude -placks.amazon-profile.mjs(isolated variant reached Amazon logged-out with no captcha at the homepage → strong feasibility signal; profile variant hit the blocker in §6).Gotchas discovered:
CLAUDECODE*env vars from the child's env (for k: if /^CLAUDECODE$|^CLAUDE_CODE/ delete).@zed-industries/agent-client-protocol@0.4.5rejects sometool_call_updatenotifications (rawOutputfield) fromclaude-code-acp@0.16.2with-32602 Invalid params. Turn still completes. Fix by bumping the lib, matching versions, or a tolerant validator.session/request_permission, respond{ outcome: { outcome: "selected", optionId: <the allow_once option's optionId> } }.6. The current blocker — logged-in browser auth
Motivating task needs the user's logged-in Amazon session. Findings:
/Applications/Claude.app, its own Electron cookie store) — different product/runtime. Dead end for the bridge.--user-data-dir=<Default profile>is blocked by Chrome. Playwright launches Chrome butlaunchPersistentContexttimes out on--remote-debugging-pipe: recent Chrome refuses remote-debugging/automation on the Default profile (anti-cookie-theft). Not a lock and not our bug (confirmed withplaywright-corechromium.launchPersistentContext(defaultProfile, {channel:'chrome'})→ timeout after Chrome launches pid).killleaves stale~/Library/Application Support/Google/Chrome/Singleton*symlinks that must berm'd; failed Playwright launches can leave zombieGoogle Chromeprocesses — clean them.Viable "browser owns cookies" options (pick one):
Cache,Code Cache,GPUCache,Service Worker), launch Playwright/Chrome with--user-data-dir=<copy>. Chrome's restriction targets the default path, so a copy is automatable and carries cookies (decryptable on same machine/Keychain — may trigger a one-time macOS Keychain prompt). Fully autonomous otherwise.--extension: install the Playwright MCP browser extension in the user's Chrome once, start MCP with--extension, connect → Claude drives the live logged-in Chrome. Cleanest match to "use my logged-in browser"; requires a one-time manual extension install and reconnect.7. Decisions already locked by the user
-p/exec).session/request_permissionto the user as approvals (not blanket auto-approve). Mirror agent-bridge PR feat(render): fail-fast guard for oversized graphs + edge-case verifications #3's R40 approval model / lifeboard's approval UX. (The spike auto-approves only because it's a controlled test.)8. Next steps (ordered, concrete)
LOGGED_IN. Then, with user OK, the real "recent orders" read.packages/cli/src/bridge/drivers.ts(quick wins, independent of ACP).acp-backed runner alongsiderunDriverindrivers.ts(or a newbridge/acp.ts). Spawn the ACP agent (claude-code-acp; gemini via--experimental-acp), connect with@zed-industries/agent-client-protocolClientSideConnection+ndJsonStreamover the child's stdio./jobs+/events(ACP is streaming/long) — map ACPsession/update(tool_call, message chunks, plan) onto job progress + lifeboard canvas components.session/request_permissionto the user (surface via/events; add a respond endpoint) per the locked decision.mcpServersper session (browser MCP configurable). SanitizeCLAUDECODE*env. Handle the schema-version mismatch (§5).packages/effect-acp/test/fixtures/acp-mock-peer.ts).9. Reference links
github.com/agentclientprotocol/agent-client-protocol; npm@zed-industries/agent-client-protocol(client),@zed-industries/claude-code-acp(Claude ACP agent).@playwright/mcp.pingdotgg/t3code—packages/effect-acp,packages/effect-codex-app-server.Appendix A —
spike.mjs(example.com ACP+browser proof)Appendix B —
amazon-profile.mjs(logged-in dry-run; uses real Chrome profile — currently blocked, see §6)Same client/connection scaffold as Appendix A, plus: a guard that aborts if
pgrep -x "Google Chrome"is non-empty;mcpServersuses the real profile:and the safe dry-run prompt (login-state only):
Pre-req before running the profile variant: fully quit Chrome, then
rm -f ~/Library/Application\ Support/Google/Chrome/Singleton*. (This variant is the one blocked by Chrome's default-profile automation restriction — switch to option (A)/(B)/(C) in §6.)Appendix C — reproduce the bridge capability tests