Repository navigation
browse: keep tunnel-paired agents on web URLs and away from the daemon's disk - #4116
Conversation
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
|
😎 This pull request was merged. |
Tunnel instruction block screenshot example uses --base64; newtab gates the URL its handler opens (skipping --json); Security Model bullet says 25 commands. Review-Finding: p1/specialist-batched#1 reader=specialist-batched Review-Finding: p1/specialist-batched#2 reader=specialist-batched Review-Finding: p1/red-team#1 reader=red-team Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
Tunnel gate refuses null args and non-string elements with the logged 403; the screenshot 403 hint names --selector <css>; docs and changelog say tunnel agents cannot read local files or choose where anything is written, and that fixed-name captures still go to the temp dir. Review-Finding: v1/adversarial-native#2 reader=adversarial-native Review-Finding: v1/adversarial-native#3 reader=adversarial-native Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…e tunnel Over the pair-agent tunnel nothing is written to the daemon's disk: screenshot must include --base64, snapshot refuses -a/--annotate and -H/--heatmap as well as -o, and the 403 hint names these limits. Docs and changelog state the claim again. Approved under B1 (a-d2bdd5d079e4). Review-Finding: v1/adversarial-native#1 reader=adversarial-native Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…l claim The /command route passes [] to canDispatchOverTunnel when the body has no args field, matching the handler's default, so a tunnel screenshot without --base64 is refused in either form. Docs and changelog now say a paired agent can't read local files, can't choose a path on the daemon's disk, and can't make the daemon save screenshot or snapshot images. Approved under C1 (a-f778cd1e7945). Review-Finding: p2/specialist-batched#1 reader=specialist-batched Review-Finding: p2/red-team#1 reader=red-team Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
E2E Evals: ✅ PASS96 executed, 0 reused rule/judge records | 0 manual accepted (unscored; no score-cache credit) | $31.75 rule/judge cost | reconcile exit: 0 Profile: pr / mode Full gate restored by:
Rule and judge shards
Fail-closed reconciliationSliced lane: planner → duration-packed executors → fail-closed report. Behavior cases run a pre-registered 3-trial panel (PASS at 2/3 with no contract violation); a PASS 2/3 is shown with its failed trial, never as a clean pass. Reused results retain their original provenance and expiry. Dispatched narrow screen (gstack-ship-queue ready --screen) of merge commit |
What changed
pair-agent now tells remote agents what the tunnel refuses: non-web URLs, screenshots without --base64, image-writing snapshot flags, and eval.
Before/after as text: pair-agent now tells remote agents what the tunnel refuses: non-web URLs, screenshots without --base64, image-writing snapshot flags, and eval
The tunnel dispatch gate (
canDispatchOverTunnelinbrowse/src/routes/commands.ts) now reads command args on the pair-agent tunnel surface:gotoandnewtabaccept only http(s) URLs over the tunnel.file:,data:,javascript:,view-source:,chrome:andabout:targets get a 403.newtabchecks the URL its handler opens, sonewtab --json <url>works.screenshotmust include--base64and cannot name an output path.snapshotrefuses-a/--annotate,-H/--heatmapand-o.evalleavesTUNNEL_COMMANDS(now 25 commands), because it always runs a file from the daemon's disk./commandroute gates a body with noargsfield as[], the same default the handler uses.null, non-array args and arrays with a non-string element get the logged 403.screenshot --base64 --selector <css>for targeting an element.screenshot --base64as its screenshot example.parseScreenshotArgsmoves out of the screenshot handler inmeta-commands.tsso the handler and the gate share one parser. The handler's behavior is unchanged.Docs match: BROWSER.md, docs/BROWSER_INTERNALS.md, docs/REMOTE_BROWSER_ACCESS.md and pair-agent/SKILL.md.tmpl plus the regenerated SKILL.md. Over the tunnel a paired agent can't read local files, can't choose a path on the daemon's disk, and can't make the daemon save screenshot or snapshot images.
Why
The tunnel allowlist comment said paired remote agents never get disk access, but navigation and screenshot args let a tunnel token open local files and have images saved under the daemon's working directory and /tmp. The gate now checks the args, so tunnel agents stay on web pages and get screenshots back in the response.
Blast radius
Only requests on the tunnel surface change. Local CLI, skill-spawned, root and extension clients go through the same code as before;
local surface dispatches the same commands unchangedanda screenshot with no args field is gated like args: [] (tunnel refuses, local dispatches)in browse/test/dual-listener.test.ts drive the real/commandhandler on both surfaces (script ran).canDispatchOverTunnelwith omitted args keeps its command-only answer for its other callers (server.ts re-export and tests; read).Approvals
Approved: yes (record r-9084c1af180d). The full approval record stays private: this repository is public or its visibility is unknown.
Summary
Tunnel-paired browse agents get web navigation and in-response screenshots, and nothing more from the daemon's disk. Review found the remaining ways a tunnel request could save a temp image or skip the args check; all are closed here with tests.
Test Coverage
Step 7 audit: 13/13 changed code paths tested (100%), 0 gaps. Since then, review fixes added 7 tests across browse/test/tunnel-gate-unit.test.ts, browse/test/tab-isolation.test.ts and browse/test/dual-listener.test.ts; each failed on the previous head and passes now.
Tests ran: focused 696 (local lane at fc0da49)
Tests ran: bun run test 1627 (files; carried from CI run 42348464341/1 at tree 681cd61eebd5)
Pre-Landing Review
Pre-Landing Review: 8 issues — 8 fixed (pass 1 auto-fixes plus the owner-granted rounds), 0 asked, 0 deferred, 2 noted
REVIEW_LOOP: passes=2 blocking_found=4 deferred=0 noted=2 fixed=8 scope=frozen ledgers_read=6 fix_checks=2 dispatches=10
Gate edits: none detected (6 listed, 3 read; patterns: RH-1,RH-3,RH-4,RH-12,RH-13,RH-14,RH-15,RH-16; unlisted idioms: none)
Outside review: unavailable (Codex CLI not installed on this machine); missing coverage, not a pass. The in-host adversarial pass ran on both rounds.
Deferred findings
Blocking findings in code this PR did not change (review scope: frozen); filed as issues and not closed by this PR.
browse/src/routes/commands.ts:172screenshot parses as an output path, so the tunnel 403 hint says it cannot name an output path; hint could mention --selector. Fails closed. Reader: FIXABLE, Low, Blocks: no.Review notes
Below the blocking floor (advisory, informational, test-gap or wording items); recorded here and not fixed in this PR.
browse/src/browser-manager.tsBrowser downloads triggered by a tunnel click use Playwright defaults; unverified where they land. Pre-existing. Reader: INVESTIGATE, Low, Blocks: no.changelog.d/capy-gst-1747-tunnel-web-urls.md:4The restored claim was broader than what the gate enforces; narrowed in the five places.browse/src/routes/commands.ts:56newtab with --json before the URL is refused for domain-scoped tokens.The deferred item #4120 was filed by the defer step for the 403-hint wording, which dd6c3d2 already changed (the hint now names
screenshot --base64 --selector <css>); it can be closed.Exploratory QA
Functional probes of the gate (qa/functional captures 001-006, revalidated as qa/functional2 001-004 on the final tree): verdict pass. Two defects found by probes were fixed (newtab --json refused; the instruction block's screenshot example refused). Limit: gate-function probes; the live tunnel listener is covered by browse/test/dual-listener.test.ts, not a running ngrok tunnel.
Design Review
No frontend files changed — design review skipped.
Eval Results
No paid evals ran. The selector fell back to full coverage (BROWSER.md is an unknown dependency) and labels those 153 behaviors scheduled/release coverage, not PR passes. The directly touched rule case, safety-pair-agent-block, was not run on this machine (no Claude CLI).
Plan Completion
Plan completion audit: not run (no plan is bound to this branch and no docs/designs/ file matches). Fix: add "Plan: " to the PR body, or run /autoplan.
TODOS
No TODO items completed in this PR.
Documentation
Status: current. The branch's documentation already reflects the tunnel-gate change; no edits are needed.
Scope: 12 committed files at aaff474..6574c9d on capy/keep-tunnel-paired-agents (no staged, unstaged or new files). Nested docs and authored templates were searched for tunnel allowlist claims.
Documentation health:
--base64screenshot example, eval removed from tunnel)--outdescribed as never allowed over the tunnel)Coverage debt: the new tunnel argument limits (http(s)-only goto/newtab,
--base64-only screenshot, snapshot -a/-H/-o refused) have reference coverage in BROWSER.md, REMOTE_BROWSER_ACCESS.md and pair-agent; there is no how-to for capturing screenshots from a remote agent (a common gap). No critical gaps.Diagram drift: none found.
Test plan
(GST-1747)