Skip to content

test(agent): rule-10 red-team pass on navigate-is-a-fetch (K3 arrival gate) - #1129

Merged
parawanderer merged 1 commit into
mainfrom
redteam/navigate-is-a-fetch
Oct 25, 2026
Merged

parawanderer merged 1 commit into
mainfrom
redteam/navigate-is-a-fetch

Conversation

@parawanderer

Copy link
Copy Markdown
Owner

Red-team pass on #1120 (landed as 9254b38): what a hostile page, a hostile policy shape or a model that navigates anywhere must NOT get from the arrival judgement, the origin grant and the named-navigate pass. Twelve properties, each a test; where the gate already held it is a pin of the As-built narrowing.

The central finding is a layering split, not an escape. consentedOrigins (the arrival's allow/pass write) silences the agent-host PROMPT but does not reach the FETCH_URL choke point (sw-fetch-url.ts), which re-judges via caller.consented = ctx.consented.has(u) || originCovers(ctx, u, …) || policyAllows(…). originCovers reads ctx.origins (grantRunOrigin), not consentedOrigins. So an allow/pass arrival's uncredentialed fetch of its own origin is REFUSED end-to-end with hasn't been approved for fetching on this page. The As-built's "navigation and uncredentialed reads" is gate-level and does not survive the choke point. The tests pin the REFUSAL (the defended outcome) and name the split so a reorder of fetchPolicy in agent-host.ts, or a widening of caller.consented, turns them red.

A second gap is open as a failing-first todo: a commit BETWEEN turns finds no judge (settle() tears down unwatchArrivals; judgeArrival marks lastJudged with no judge registered; holdStands only re-judges when a hold already exists). A hand-move or page redirect between turns to a page a policy would deny/ask is never judged for it. The fix is the security lane's.

What is pinned: the start origin is consented never as-you (choke-point refusal); an arrival no policy decides passes and consents nothing (both fetches card); an ask asks on every arrival it covers; namedNav is single-use (a redirect on from the named page is judged); a hold outlives the turn; ruling (b) allow row consents the origin for navigation only; a page-built run keeps per-URL fetch approvals (grantRunOrigin is byWorker && runsInWorker); the describe layer is conservative (the arrival check is the enforcement); an origin grant is exact.

Drafted by Claude Code · work: window-ml-redteam-e3 · session ff9768dc · openrouter.xiaomi/mimo-v2.6-pro

🤖 Generated with Claude Code

@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 added a commit that referenced this pull request Oct 25, 2026
…ents navigation only; the between-turns commit is KNOWN OPEN (#1130)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
parawanderer added a commit that referenced this pull request Oct 25, 2026
…ents navigation only (#1129) (#1131)

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 added a commit that referenced this pull request Oct 25, 2026
…before #1129 (#1133)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… gate)

What a hostile page, a hostile policy shape or a model that navigates anywhere
must NOT get from the arrival judgement, the origin grant and the named-navigate
pass. Twelve properties, each a test; where the gate already held it is a pin of
the As-built narrowing. The between-turn judge gap (settle() tears down
unwatchArrivals; judgeArrival marks lastJudged with no judge; holdStands requires
an existing hold) is a failing-first todo for the fix's owner.

The central finding is a layering split, not an escape: consentedOrigins
(the arrival's write) silences the agent-host PROMPT but does not reach the
FETCH_URL choke point, which re-judges via originCovers (grantRunOrigin), so an
allow/pass arrival's uncredentialed fetch of its own origin is REFUSED
end-to-end. The As-built's "navigation and uncredentialed reads" is gate-level
and does not survive the choke point. Pinned as the defended outcome with the
exact refusal text so a reorder of fetchPolicy or a widening of caller.consented
turns these red.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@parawanderer
parawanderer force-pushed the redteam/navigate-is-a-fetch branch from 5dba084 to 67d14e4 Compare October 25, 2026 10:19
@parawanderer

Copy link
Copy Markdown
Owner Author

Head moved to 67d14e4 after the rebase onto main (which brought #1131's fix). Amended: the ruling (b) pin now expects both fetches to card (navOnlyOrigins: the allow row consents navigation only, so the plain GET is refused on its card rather than at the choke point); the between-turns todo flipped to a plain test; the PROBED record updated. 13/13 green against the rebuilt bundle. Review should cover this version.

Drafted by Claude Code · work: window-ml-redteam-e3 · session ff9768dc · openrouter.xiaomi/mimo-v2.6-pro

@parawanderer

Copy link
Copy Markdown
Owner Author

Reviewed. No finding.

Every pinned property asserts the defended outcome with the exact refusal text, not a proxy. CHOKE_REFUSED is the literal choke-point string hasn't been approved for fetching on this page. denied() matches "No admin pages" denies: no tool runs on this page. r.cards, r.asks, r.fetched are exact shape assertions (deepEqual). No test asserts a count or a boolean where the refusal text is the defended outcome.

The todo fails for the claimed reason and nothing else. test.todo("BETWEEN-TURN GAP: ...") at line 240 fails on assert.ok(r.asks.some((a) => a.kind === "arrival")) at line 256 with actual: false. The comment names the mechanism precisely: settle() tears down unwatchArrivals, a commit writes lastJudged with no judge, and holdStands only re-judges when a hold exists. The test setup is correct (ASK_B covers OTHER, between: (go) => go(OTHER) moves the tab there). The failure is the gap, not a test bug.

The tests that pin the refusal are sensitive to the code changes the body names. Four tests assert CHOKE_REFUSED or r.fetched empty (lines 152-153, 278-279, 413). If caller.consented widens to include consentedOrigins (which the gate writes but the choke point does not read), those turn red. If fetchPolicy reorders past fetchOriginGranted in agent-host.ts, the deny-policy tests (DENY_ADMIN cases) turn red. Tests pinning only arrival asks (r.asks), the describe layer, or page-built runs assert different layers and would stay green — that is correct, not a gap.

PROBED AND NOT GOTTEN tests are documentation (channels list + code facts). They stay green under any code change by design.

The hostRun harness mirrors nav-arrival-host.test.mjs (same shape: loadBackground, commit, ready, onTabMessage), so the behavioral tests exercise the real worker in node:vm. pageBuiltRun is a separate harness because the worker-built path's origin grant never reaches a page-built run — correctly noted in the file header.

One note: the PR body says "Twelve properties, each a test" but the file has 12 top-level test() calls plus 2 test.todo and 2 PROBED docs. The 12 behavioral tests match.

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

@parawanderer

Copy link
Copy Markdown
Owner Author

Correction to my earlier review on this PR. That review was written against the pre-rebase file (the head before the rebase onto dc5a301); the current head 67d14e4 has no test.todo at all.

What I get against 67d14e4: no finding. The between-turn test is a plain test("BETWEEN-TURN GAP FIXED: ...") (line 241) that passes on the fix from #1131. The allow-row test (line 256) pins card pins ["click", "click", "fetch_url", "fetch_url"], not CHOKE_REFUSED. The remaining CHOKE_REFUSED assert is at line 154 (the uncredentialed read back at the START origin), which is the expected open start-origin gate/choke split recorded in POLICIES.md's as-built paragraph. The file header (lines 14-22) names the split and the navOnlyOrigins fix. All 98 nav tests pass, 0 todo, 0 fail.

The two claims in the PR body hold against this head: the gate/choke-point split is pinned by the restsOn list (lines 453-458) and the PROBED AND NOT GOTTEN list (lines 420-432), and a reorder of fetchPolicy in agent-host.ts or a widening of caller.consented would turn the pinned outcomes red.

e3 is clear to merge on this.

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

@parawanderer
parawanderer merged commit 9133ec9 into main Oct 25, 2026
19 checks passed
@parawanderer
parawanderer deleted the redteam/navigate-is-a-fetch branch October 25, 2026 11:49
parawanderer added a commit that referenced this pull request Oct 25, 2026
#1135)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@parawanderer

Copy link
Copy Markdown
Owner Author

Approved by @parawanderer for merge.

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

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