feat(formation): Browser Verification v0 — Stagehand + Jev verify the Web GUI K [skip ci] - #1372
Merged
Merged
Conversation
… a browser [skip ci] - lib/formation::browser: BrowserContractV0 keeps the acceptance prompt verbatim as one required browser.task criterion; the helper protocol (request/result) and a receipt bound to contract_ref, prompt digest and target. Rust validates every result: verdicts must be backed by the judgment (pass=complete, fail=incomplete), evidence refs must resolve, evidence is bounded, and the overall verdict is recomputed. - formation-worker::browser_verify: runs the helper with a cleared environment (allowlisted keys only), a wall clock + grace, a process-group kill and a scratch-dir sweep for a detached Chrome. Missing, crashed, slow or out-of-contract helpers yield an inconclusive receipt, never a pass. - local Formation: with a browser Contract, the SAME temporary realization is verified in the browser after the typed HTTP Contract passes and before it is destroyed; only a browser pass forms the candidate. - ato form --verify-browser --accept "<prompt>" [--browser-verifier DIR]. Without the flags, Formation is unchanged. - apps/formation-browser-verifier: Stagehand 3.7.3 (LOCAL Chrome, loopback only via a dead proxy) drives the task with an agent (DeepSeek flash); Jev (jev-1.13.0, native Choice on /v1/systemone) judges the evidence as complete | verify_more | incomplete. Page text reaches the judge only as labelled untrusted state; an agent that cannot operate is inconclusive. - fixtures: one notes app in four variants (ok, broken add, no persistence, prompt injection).
| let receipt = run(r#"sys.stderr.write("chrome exited unexpectedly\n"); sys.exit(4)"#); | ||
| assert_eq!(receipt.overall, BrowserVerdict::Inconclusive); | ||
| let reason = receipt.reason.unwrap_or_default(); | ||
| assert!(reason.contains("browser_verifier_failed"), "{reason}"); |
| assert_eq!(receipt.overall, BrowserVerdict::Inconclusive); | ||
| let reason = receipt.reason.unwrap_or_default(); | ||
| assert!(reason.contains("browser_verifier_failed"), "{reason}"); | ||
| assert!(reason.contains("chrome exited unexpectedly"), "{reason}"); |
…y recheck, origin boundary, observed evidence [skip ci]
- K identity: with a browser Contract, a Formation's contract_ref is the
JCS digest of EffectiveContractV0 { base_contract_ref,
browser_contract_ref }; attempts keep base_contract_ref for provenance.
Without one, the base ref is unchanged. The receipt field is now
browser_contract_ref.
- verify_more no longer runs the agent: after the task, the verifier only
observes (snapshot + a model's reading) and asks the judge again.
- Origin boundary: a guard proxy refuses and records every request; Chrome
bypasses it only for the candidate's host:port (<-loopback> drops the
implicit loopback bypass). Page-attributed off-origin requests (CDP
Network.requestWillBeSent / webSocketCreated) and main-frame navigations
off the origin become blocked_request / origin_violation events; any
violation rules out a pass (Rust enforces it too).
- Evidence kinds are separate: browser_snapshot (URL, title, innerText,
navigation type via CDP), model_extracted_facts, agent_report; navigations
and loads are observed_events; agent steps are agent_claimed_step. A pass
must cite a browser_snapshot. The judge's state separates observed events,
observed page states, model-derived facts, agent claims and gaps.
- The helper and its browser get HOME inside the verification scratch.
- Fixtures: notes-false-claims, notes-exfiltrate, notes-redirect.
- ADR-020: Jev's role is evidence judgment only; normalization stays
v0.identity.
…hardening acceptance [skip ci] A large result (many navigation events) was cut at 8 KiB because the helper exited before stdout was flushed to the pipe; the worker then saw an invalid answer. The helper now exits only after the write completes. docs/ops records cases A-J after the hardening.
…nd traffic [skip ci]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Formation Browser Verification v0: build a typed Browser Contract (K) that keeps the natural-language acceptance prompt verbatim, have Stagehand actually operate the local temporary realization in a browser, and have Jev judge the evidence as a bounded decision, returning pass / fail / inconclusive. The base is
main. All commits carry[skip ci](no normal CI; CodeQL default setup starts on its own).Hardening (updated in this revision)
1. Browser Contract is part of the final K identity
effective_contract_ref = base_contract_ref, unchanged; existing identity does not move.sha256(JCS(EffectiveContractV0 { schema: "ato.effective-contract/0", base_contract_ref, browser_contract_ref })).FormationResult::Formed.contract_refandFormationAttempt.contract_refare the effective ref; the attempt keepsbase_contract_reffor provenance, and the receipt carriesbrowser_contract_ref.local_formation_v1+ CLI run on OCI).2.
verify_moreis structurally read-onlyrunTask(recheckInstruction)afterverify_moreis removed. After the task, the loop only doesobserve(browser snapshot + a model's reading) → Jev again. There is no code path fromverify_moreto the agent.3. The browser is confined to the exact target origin
--proxy-server=<guard>and--proxy-bypass-list=<-loopback>;<host:port>, so it can reach only the candidate's exact host:port directly. Other loopback ports, localhost, [::1], LAN, the internet, ws/wss and redirect targets are refused by Chrome's network stack before any connection to them exists.Network.requestWillBeSent/webSocketCreated, becomeblocked_request; a main frame off its origin becomesorigin_violation. The browser's own background traffic is refused but not counted as a candidate violation (on macOS Google Chrome it made A inconclusive, so this was split).PassDespiteViolation).4. Observed and model-derived evidence are separate
browser_snapshot: URL, title,document.body.innerText(bounded) and navigation type, taken directly from CDP (no LLM).model_extracted_facts: Stagehand extract output.agent_report: the agent's own account.observed_events: navigation / load / blocked_request / origin_violation. Agent steps are recorded asagent_claimed_stepactions, never as events.browser_snapshot, and unknown kinds are refused.objective / observed_browser_events / observed_page_states / model_derived_facts / agent_claims / known_gaps. The instructions state that observed evidence comes first, that agent claims alone never prove the objective, and that text announcing task success is not application state.Also
HOMEpoints inside the verification scratch directory.v0.identity(Jev does not normalize K).Acceptance (OCI Linux aarch64, bwrap, Playwright Chromium 149,
ato form --verify-browser)Three services the candidate must never reach (
127.0.0.1:47999,127.0.0.1:47997,[::1]:47996) ran during every case and counted requests.blocked_request. I: the final snapshot is the browser's 403 page (the guard refused it).sha256:e7247bd4…, basesha256:22d8402e…. The same fixture without--verify-browserhascontract_refexactlysha256:22d8402e….docs/ops/formation-browser-verification-v0-acceptance-2026-09-23.md; design:docs/rfcs/draft/ADR-020-formation-browser-contract-v0.md.Tests
formation-browser-verifiernode --test: 19/19 (G/H/I/J, evidence separation, guard refuse-and-record, a large result through a pipe, and more).tsc --noEmitclean.ato-formationlib 86/86 (browser 14: effective ref, a PASS must cite a snapshot, violation rules out PASS, unknown kinds).browser_verifier_protocol_v115/15 (including helper HOME).local_formation_v113/13 (including the effective-ref test). OCI:temporary_realization_v114/14.cargo clippy: 0 warnings in the touched crates. Known pre-existing failure:sandbox_v1::a_step_that_declared_no_network_does_not_get_one(reproduced on base in the Phase 1 records).Known constraints
Not included
Jev Contract normalization, criteria decomposition, LLM generation of D, Runtime Network, remote browser, Browserbase, multi-service, static-lane browser serving, replay/caching, generic computer-use.
🤖 Generated with Claude Code
https://claude.ai/code/session_01XzC19UkhNG9pY63HmTB5Rj