Skip to content

fix(security): judge a run's tab between turns; an allow arrival consents navigation only (#1129) - #1131

Merged
parawanderer merged 1 commit into
mainfrom
fix/arrival-between-turns
Oct 25, 2026
Merged

parawanderer merged 1 commit into
mainfrom
fix/arrival-between-turns

Conversation

@parawanderer

Copy link
Copy Markdown
Owner

Fixes the two findings of the navigate red-team pass in #1129 (finder: window-ml-redteam-e3).

1. A commit between turns was never judged (open escape)

settle() removed the run's arrival judge at turn end, judgeArrival still wrote lastJudged with no judge registered, and holdStands re-judged only a run already held. A hand-move, redirect or timer between turns to a page a policy denies or asks about was never judged: the follow-up's adoptOnTab (and CONTENT_READY) re-adopted the run there, which also read that page's info into the model, and the next turn acted freely.

  • watchBetweenTurns (nav-arrival.ts): from settle() until the session is released, a commit holds or clears the run with no card. Both re-adopt paths already skip a held run.
  • holdStands judges the page when the run is held or when no judge of this run has spoken on that URL, and holds the run on a deny or ask. After an eviction nothing is remembered, so it judges (fail closed).
  • judgeTurnStart (replaces the turn-start judgeArrival) judges that page for this run, queued so no tool of the turn reaches the page first (arrivalSettled). A run's first turn takes the person's start page as judged.
  • lastJudged is written only when a judge ran. forgetArrivalRun (from releaseSessionTokens) drops the run's hold, its between-turns judge and its last judged URL; a closed tab drops its judges.

2. An allow row's arrival consented reads the choke point refuses (layering split)

Per the 2026-10-25 ruling (narrow): an allow row's arrival now puts its origin in a turn-local navOnlyOrigins, read by the navigate gate only, not consentedOrigins. An uncredentialed fetch_url there goes to its card instead of being refused silently at the FETCH_URL choke point. The card carries no line saying why yet: that is a new public string and waits for approval.

Tests

Four new tests in tests/nav-arrival-host.test.mjs, section "a page the tab moved to BETWEEN turns" plus one under ruling (b): an asked page reached between turns is asked about once at the next turn's start; a denied page reached between turns holds the next turn and is not re-adopted; a follow-up on an approved page and a first turn on the start page ask nothing; an allow arrival's plain GET cards. The two between-turn tests failed against main's bundle. Full npm test: 6959 pass, 0 fail.

For #1129 after this merges (not edited here)

tests/nav-navigate-redteam.test.mjs: the ruling (b) test at ~277 needs cards to be ["click","click","fetch_url","fetch_url"], and its CHOKE_REFUSED assert at ~279 drops (the plain GET is refused on its card). The BETWEEN-TURN GAP test.todo passes as a plain test on this build. e3 updates its own file.

Still open

The start origin shows the same gate / choke-point split: consented at the gate, refused at the choke point once the tab has left it (e3's start-origin pin). Not changed here; owned by the security lane.

Docs: docs/dev/agent-tools.md (Arrivals), docs/dev/site-access.md (origin-wide auto-approvals).

Drafted by Claude Code · work: site-access · session 2dfb7129 · claude-opus-5-5

🤖 Generated with Claude Code

…ents navigation only (#1129)

A commit while no turn ran found no judge, was marked judged anyway, and the next
turn re-adopted the run on a page a policy denies or asks about (reading its page
info into the model). The tab is now watched between turns (watchBetweenTurns),
holdStands and judgeTurnStart judge any page the run's judge has not spoken on,
and lastJudged is written only when a judge ran.

An allow row's arrival now consents its origin for navigation only
(navOnlyOrigins), matching the FETCH_URL choke point: an uncredentialed read there
goes to its card instead of being refused silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@parawanderer parawanderer added @agent The agent loop, its tools, approvals, runs and where they start @security Trust boundaries: consent, site access, privileged fetches, what a page or model reaches labels Oct 25, 2026
@parawanderer

Copy link
Copy Markdown
Owner Author

Reviewed. No finding.

Rule 10 holds. The diff touches six files: docs/dev/agent-tools.md, docs/dev/site-access.md, src/sw/nav-arrival.ts, src/sw/sw-run-host.ts, src/sw/sw-runs.ts, tests/nav-arrival-host.test.mjs. tests/nav-navigate-redteam.test.mjs is not among them.

Every committed navigation of the run's tab is judged over its lifetime, fail closed. Three paths cover every window:

  1. During a turn, watchArrivals registers the judge and judgeArrival marks lastJudged only when a judge is registered (the if (!runs.length) return before lastJudged.set). A commit no judge watched is never marked judged.

  2. Between turns, watchBetweenTurns (called from settle()) judges quietly: deny/ask holds, allow/pass clears. The next turn's judgeTurnStart sees spoke already set and skips, or judges when it has not.

  3. At turn start, judgeTurnStart runs before any tool (arrivalSettled blocks at five call sites in sw-run-host). force: resurrected || !!arrivalHold(runId) re-judges after an eviction or when held. After eviction all maps (spoke, holds, judges) are empty, so holdStands and judgeTurnStart both judge the page the tab holds now.

holdStands (called from userRunAction before the turn starts) now judges when spoke.get(runId) !== url, not only when held: an unjudged page is judged before the turn acts there. If the judgement denies or asks, the run is held and nothing reaches the page.

The navOnlyOrigins split is correct. consentedOrigins (from runOrigins + pageOrigin) feeds the FETCH_URL choke point via ctx.consented. navOnlyOrigins (set only by the allow verdict) feeds only needsGate (the navigate gate). An uncredentialed read of an allow-consented origin cards because the choke point does not see navOnlyOrigins. The test pins this: cards includes fetch_url, fetched is empty.

forgetArrivalRun cleans up completely. quiet, holds, spoke all cleared on releaseSessionTokens. forgetArrivalTab now clears judges (was only lastJudged), preventing a leak when a tab closes while a run is live.

85/85 nav tests pass (81 existing + 4 new). The four new tests cover: hand-move between turns to an ask page, denied page between turns holding the next turn, follow-up on the same page not re-asked, and allow-arrival consenting navigation only.

Drafted by Claude Code · work: flaky-ci · session 309c99e2 · openrouter.xiaomi/mimo-v2.6-pro

@parawanderer
parawanderer merged commit dc5a301 into main Oct 25, 2026
21 checks passed
@parawanderer
parawanderer deleted the fix/arrival-between-turns branch October 25, 2026 10:00
parawanderer added a commit that referenced this pull request Oct 25, 2026
…before #1129 (#1133)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

@agent The agent loop, its tools, approvals, runs and where they start @security Trust boundaries: consent, site access, privileged fetches, what a page or model reaches

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant