Commit Graph
3 Commits
Author SHA1 Message Date
Garry TanandClaude Fable 5 1ae9aef999 fix(browse): adversarial-review hardening — 6 findings fixed, regression-pinned
Pre-push adversarial review (4 lenses, refute-style verification: 13 raw
findings, 7 refuted, 6 confirmed) caught these; each fix carries a pin:

1. --restrict=read (equals form) sailed past validatePairAgentFlags —
   hasFlag/parseFlag are exact-token matches — so the user asked for a
   read-only sandbox and silently got FULL access: the exact failure mode
   this branch claims to close. The equals form is now a hard error before
   any server work.
2. handleTunnel trimmed the agent name but clientIds are stored verbatim,
   so a space-padded agent was unrevocable by the documented kill switch
   (trimmed DELETE 404'd while the grant stayed live). Names now pass
   through verbatim; the live-daemon test revokes ' padded'.
3. The sole pin for "CLI always sends explicit scopes" passed vacuously on
   a simulated revert: toContain('DEFAULT_PAIR_SCOPES') was satisfied by a
   comment. The tripwire now matches the code shape with a regex and bans
   the conditional spread formatting-insensitively.
4. The rewritten 403 scope hint was unpinned — new e2e asserts it names
   --restrict and --control and never --admin.
5. tunnelRevoke's verify-failure and HTTP-error branches and tunnelAgents'
   unreadable-list branch had no coverage — three stub-daemon pins added
   (an unreadable list must never render as "No paired agents").
6. CHANGELOG claimed "40+ new test cases"; the honest count is 35.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 04:29:07 +00:00
Garry TanandClaude Fable 5 44e0b6f7be fix(browse): CLI always sends explicit pair scopes via shared DEFAULT_PAIR_SCOPES
The effective pairing default lived in two places: the CLI omitted scopes
unless --restrict was passed, and the server filled in its own literal.
handlePairAgent now always sends an explicit scopes list and both sides
reference one exported constant, DEFAULT_PAIR_SCOPES, so the default cannot
silently drift again (pinned by a server-auth source tripwire).

Three input traps closed in the same surface:
- Bare --restrict (or --restrict swallowing the next flag) parsed as "no
  restriction" and silently granted FULL access, the opposite of the user's
  intent. validatePairAgentFlags rejects it pre-server, before any consent
  gate, so an arg error never boots a daemon.
- A scopes list could smuggle the control scope past the explicit flag:
  --restrict "read,control" minted a control-scoped session with no
  --control. /pair now 400s on control in a scopes list without the control
  flag, and the CLI points the user at --control.
- Option typos validated only at exchange time: createSetupKey stored any
  scope string and any rateLimit, so /pair returned 200 with a poisoned
  setup key whose failure surfaced to the REMOTE agent at /connect as a
  misleading "Invalid request body". Shared validation now runs in both
  creators and throws typed InvalidScopeError; /pair and /token 400 with the
  message, naming the bad scope or negative rateLimit. Also
  `opts.rateLimit || 10` became `?? 10` so the documented "0 = unlimited"
  survives the /pair path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 04:00:04 +00:00
Garry TanandClaude Fable 5 f28bfd0158 feat(browse): tunnel revoke/agents CLI with post-revoke verification
`$B tunnel revoke <name>` was documented in the instruction block,
pair-agent/SKILL.md, and REMOTE_BROWSER_ACCESS.md but implemented nowhere:
the CLI forwarded it to the daemon as Unknown command 'tunnel', and nothing
in the repo called DELETE /token/:clientId or GET /agents.

New pre-server short-circuit (#2254 pattern: tokens are memory-only, never
boot a daemon to revoke against it). `tunnel revoke <name>` DELETEs the
token, prints the deleted count ("(count unknown)" for old daemons that
answer {revoked} without tokens_deleted), then RE-READS GET /agents to prove
the agent is gone. The still-listed branch is the version-skew net: a new
CLI against a still-running old daemon with the first-match revoke bug exits
1 and says to re-run (each old-daemon call deletes the next match) or stop.
An alive pid with an unreachable port reports "Could not reach daemon"
(exit 1), never a false "no daemon". `tunnel agents` lists sessions plus
pending (unexchanged) setup keys, which GET /agents now exposes via
listTokens({includeSetup}) — without them the revocation view was blind to
a paired-but-never-connected agent. Setup-key tokens never leave the server.
DELETE /token/ now decodeURIComponents the clientId (400 on malformed
encoding) so CLI-encoded names round-trip.

Tests: subprocess CLI coverage (usage paths, no-daemon exit 0 without
spawning, live pair/connect/revoke loop, pending-key listing), stub-daemon
pins for the skew and unreachable branches, and e2e pins for revoke-all
semantics, percent-encoded ids, and the second-DELETE-is-404 regression.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 03:53:56 +00:00