Files
odysseus/docs/runtime-decomposition/wave-3-browser-authority.md
T

16 KiB

Wave 3 browser authority: observations with page execution disabled

Starting Checkpoint A: bc5e1ee6922000a290371f8c2aa18802a03ffcad, tree 8e09cc2560f50a3472e06ec614d6ada028b7eb18. Branch, cleanliness, both A commits and canonical Wave 5B ancestry were verified before edits. Existing 145-file Checkpoint A baseline passed 3369 tests, with 3 platform skips and 2 existing xfails.

Producer decision and live evidence

The actual release Docker image was available locally: sha256:cc2d47e2327d573af01c6b027f23d2ab0f2ee9b85d658e9eb8065bd02b9c3515 (Linux amd64). Its native binary reports exactly agent-browser 0.35.0.

The isolated local-launch probe performed:

  1. Fresh local browser launch with the first --pin-tab request.
  2. Create a sibling tab; capture and select an exact producer targetId.
  3. session info --no-pin-tab, then session info --pin-tab.
  4. Destroy the captured target using an external test fixture.
  5. snapshot --pin-tab.

Both re-arm calls succeeded. The snapshot also succeeded, a replacement target became active, and there was no tab_gone. Lifecycle metadata reported relaunchedBrowser=false, restartedBackground=false, launched=false. The CLI's special session info path does not attach the pin fields to its daemon request. Successful flags therefore cannot establish pin_armed_for. The producer audit's proposed re-arm sequence is not valid in this mode.

tests/test_browser_producer_live_contract.py reproduces this defect against the actual binary, rather than treating the defect as a passing pin contract. The four live tests also validate target/loader stability, reload/navigation, same-document history change, distinct same-URL pages, and exact target switch responses. Four passed in the actual release image. Raw GUIDs/CDP capability URLs are neither printed nor saved by the tests or production adapter.

Page/document reads and effects are unconditionally disabled before producer dispatch. Observations, matching preconditions, matching postconditions, successful pin flags, exact approval and child scope never override this gate.

Identity architecture

src/browser_identity.py owns producer validation, private configuration, registration, observations, metadata execution, resource binding and CDP observation. src/agent_runtime/resources.py supplies immutable types:

  • BrowserSessionObservation: trusted namespace, version, platform, binary digest, configuration digest, selector-only session key, one nested Wave 5B ProcessIdentity, domain-separated browser GUID digest, and deterministic session-incarnation digest. No duplicated start-token abstraction.
  • BrowserSessionResource: the observation plus mandatory owner/thread binding.
  • BrowserPageResource: exact parent session, producer targetId, opaque loaderId, explicit page/document scope, and alias/URL audit metadata. Page authority is session + target; document authority additionally includes loader. Metadata does not participate in the authority key.

Registration is server-only, checks the installed producer and creates private owned configuration. It does not spawn or adopt a daemon/browser. Model-facing lookup never creates a session. Legacy lifecycle records are not authority. There is currently no model-facing launch/enrolment operation; default/legacy sessions without a registered observation fail closed.

An explicit trusted observation checks active producer state, captures the daemon incarnation around exact executable observation, obtains the local CDP capability, rejects lifecycle launch/replacement, validates tab schema and the absence of labels, cross-checks CDP target type, captures main-frame loaderId, detaches and rechecks daemon/browser identity. A changed session invalidates every earlier page/document observation. A changed loader invalidates document scope; a same-URL or same-alias replacement never inherits target scope.

The proposed pin re-arm is not implemented as an authority-establishing action. pin_armed_for stays unset; even modifying this field cannot enable page execution. No alternate pin workaround or producer fork is introduced.

Trusted producer and observation transport

Only explicit glibc Linux release binaries are allowlisted:

Platform Version Native binary SHA-256
linux-x64 0.35.0 b7a28c3a43a7008dd02585e2e60c391c08983f7a099149caed63c9f13f57b752
linux-arm64 0.35.0 92cd7d0897837ac648b9a6ab1965c69c5920e0f54df57e4295cdb1143b0541c8

These digests were observed from the release image's installed package. x64 was executed live; arm64 execution remains a separate architecture gate. Selection uses /usr/local/lib/node_modules/agent-browser/bin/agent-browser-<platform>. Version, hash, ownership, permissions and schema are checked. No PATH search, npx execution/download, cache glob, mtime selection or replacement download. 0.27.0, unknown versions, platforms and hashes fail closed.

The CDP sidecar accepts only loopback browser websocket capability URLs and only Target.getTargets, Target.getTargetInfo, Target.attachToTarget, Page.getFrameTree, Target.detachFromTarget. It does not enable domains, evaluate, navigate, close targets or expose arbitrary CDP to tools. Frame identity must equal the captured target and loaderId must be nonempty. Requests have 3-second bounds and bounded frame/message sizes. This is producer identity observation, not semantic evidence or trust elevation.

The capability URL stays in a non-serializable, non-repr memory field. Metadata revalidation connects to that captured browser endpoint, rather than calling get cdp-url again: that getter can auto-launch a replacement. Failed or changed daemon/CDP observations invalidate the registered session; no rediscovery/retry.

Configuration is exactly {} in an owned private cwd, with observed inode and permissions checked. Client environment is constructed from an explicit fixed allowlist: owned HOME/TMPDIR/socket directory, system PATH, Chromium path and idle timeout. Ambient AGENT_BROWSER/CDP/provider/profile/state/config/proxy/XDG settings and model subprocess environment are not inherited. Configuration is part of the incarnation digest; credentials are not serialized.

Operation and approval boundaries

Operation Binding Current execution
session_info Exact registered session + caller/request Supported metadata only; no URL/title/content, target selection or launch
New page, initial open, tab list, whole-session close Session/creation producer guarantee Disabled; no trustworthy atomic creation/control contract admitted
Select/close page, navigate/reload/back/forward, time wait, viewport scroll, page network/console Exact session + target Disabled before dispatch
Click/fill/press/evaluate, selector/ref interactions and waits Exact session + target + loader Disabled before dispatch
Snapshot/read/find/screenshot Exact page, loader sandwich for any future read Disabled before dispatch; no replacement-page read

Failure is structured: failure_kind=browser_page_authority_unavailable, executed=false, retryable=false, producer_capability_unavailable=true. Missing session authority produces a separate session-unavailable failure. No timeout or post-check can authorize execution against a replacement.

RequestAuthority version 5 carries explicit session/page ceilings. Old snapshots restore empty browser scopes. Exact proposal capture binds normalized operation, request/owner/thread and the exact session/page/document observation. Metadata execution revalidates before one-use claim and at producer entry. Restoration adds no general scope. Unsupported page approvals are never claimed/executed.

Child scopes validate parent observations before intersection. Session ceilings require exact incarnation; page ceilings require exact parent + target; document ceilings also require loader. A page child cannot acquire session control, and a document child cannot renew a replaced document. Discovery adds no authority.

Model batches, raw tab/window/frame/connect commands, labels, raw targetIds, configuration/session/CDP/provider/profile/state flags and flag-like positional values are rejected. page: tN is strictly validated. The preview's automatic open/snapshot batch rewrite and native read/post-click batches/recovery engine are removed. Raw global Playwright browser control calls fail closed as well; remote backend/stdio identity is not page authority. Other remote/MCP transport mechanics remain unchanged and external.

Client invocations are bounded at 20 seconds, below the source-verified 30-second read/resend floor, with held-handle kill/wait on timeout/cancellation and no Odysseus retries. Immediate producer EOF/reset retries cannot be eliminated by this wrapper. No exactly-once claim is made; all effects remain disabled.

Control state and prior unsupported paths

Private browser runtime/configuration is protected by central control-plane resolution and native launch workspace guards, including actual configured directories. Direct, symlink and hardlink tests cover it. These are pathname/ inode observations, not race-freedom claims or a new containment policy. Service-owned Wave 5B cleanup remains independent of model authority; shutdown does not discover/download/run an untrusted producer binary.

Re-audit of Checkpoint A seams found:

Path Remaining enforcement
PTY/native manager routes routes/shell_routes.py:setup_shell_routes.shell_exec/shell_stream call _require_admin before _exec_shell/_generate_pty/_generate_tmux; internal/anonymous controls denied, authenticated human administration separate
Additional process producers resources.ProcessResource.__post_init__ admits only frozen native producer/role combinations; process_resources.resolve_process_operation requires sealed observations
Raw scheduled SSH TaskScheduler._execute_action → builtin_actions.action_ssh_command → _run_subprocess refuses SSH without an external workload adapter
Local Cookbook scheduled auto-stop routes/cookbook_routes.py:setup_cookbook_routes.protect_native_control applies shell admin boundary to local mutation; tools/cookbook._cookbook_kill_session refuses registry-less local control; legacy internal shell route cannot gain administration
Legacy/unscoped tasks authority.restore_task_authority → process_resources.resolve_process_operation admits no missing creation scope
Anonymous administration / generic app_api owned_resources.needs_owned_binding rejects shell/model/Cookbook namespaces; _require_admin also rejects unlabelled loopback when anonymous or unauthenticated

No model-reachable page producer entry remains in the native/research wrapper. Trusted observation/setup methods are not tools or routes. Native arbitrary program/network effects and remote workload effects retain their existing explicit launch/backend boundaries; this checkpoint adds no general network egress/provenance policy (Wave 4).

Validation and remaining release gates

wave-3-final-tests.txt contains 149 files, retaining all 145 Checkpoint A files and the exact prior 88-file selection. Legacy positive page/batch/recovery tests are replaced by explicit unsupported-before-dispatch tests; formatting, filesystem, YouTube, Wave 5B ownership/cleanup and research fallback tests remain.

Final resource/authority/approval focused run: 1,425 passed. Final 149-file integrated gate: 3,776 passed, 7 skipped, 2 xfailed. The exact old 88-file selection and all 145 Checkpoint A files were verified as subsets of this gate. The 7 skips are /tmp not being a symlink, applicable RLIMIT_AS already available, the Windows Ollama startup guard, and four explicit Docker-only producer probes. Those four probes ran separately: 4 passed on the actual release x64 image. Index/schema/configuration checks separately passed 40 tests.

Full-suite failure classification was performed against an isolated archive of the frozen Checkpoint A (no checkout/rewrite): replay of the initial 82 failing cases reproduced 79. Two browser/schema regressions were corrected. The third case, test_dispatcher_rejects_approved_document_action_without_target, passed alone but failed identically on the frozen archive when preceded by test_scheduler_restart_doublefire.py. That fixture permanently replaces core.database.SessionLocal/engine with a task-only database. This is an existing suite-order issue, not a browser authority regression. Missing Node Playwright dependencies and legacy fixtures that expect unscoped execution also remain explicit full-suite limitations; they are not skipped or counted as passes. New browser test environment documentation also records the existing memory backend owner settings required to regenerate the configuration page.

Final full repository run: 12,310 passed, 76 failed, 65 skipped, 2 xfailed, 6 subtests passed (403.66 seconds). Every final failed node was reproduced on frozen Checkpoint A, using the scheduler-order reproduction for the document case. This is not a green full-suite gate. Exact failed node IDs and totals are in validation/wave-3-browser-final-results.json.

Full-suite skips include smoke/live endpoints without an instance or opt-in, the four separately executed release producer probes, the three platform cases, missing caldav/chromadb/fitz/openpyxl/markitdown/libmagic/Node Playwright, ffmpeg format limitations and missing rsvg-convert. Nothing was silently converted into a pass. The two existing strict xfails remain the inferred single-file deletion and inferred CSV overwrite path cases in test_runtime_behavior_regressions.py.

Compileall, whitespace, conflict-marker and unmerged-index checks pass. The coherent fail-closed implementation is available for independent review; full-suite cleanup remains outstanding and page enabling is not merge-ready.

Exact production changes since Checkpoint A

src/browser_identity.py
src/agent_runtime/resources.py
src/agent_runtime/authority.py
src/agent_runtime/process_resources.py
src/agent_tools/web_tools.py
src/tool_execution.py
src/tool_approvals.py
src/tool_schemas.py
src/tool_index.py
src/clean_agent_preview.py
src/agent_loop.py
src/constants.py
scripts/generate_env_reference.py

website/configuration-reference.md is regenerated documentation. Runtime instructions/schema/index no longer advertise executable page interactions. The agent loop change is only the browser prompt snippet; it is not decomposed. Wave 5B lifecycle mechanics and MCP transport are not modified.

python3 -m pytest -q -rs $(cat docs/runtime-decomposition/wave-3-final-tests.txt)
python3 -m pytest -q -rs
python3 -m compileall -q app.py core routes services src tests scripts
git diff --check
git grep -n -E '^(<<<<<<< |=======$|>>>>>>> )' || true
git ls-files -u

Live release probe (source checkout mounted read-only, isolated container state):

docker run --rm --network none \
  -e ODYSSEUS_BROWSER_LIVE_CONTRACT=1 -e ODYSSEUS_DATA_DIR=/tmp/w3-data \
  -e DATABASE_URL=sqlite:///:memory: -v "$PWD:/app:ro" \
  --entrypoint python odysseus-maintainer-preview-odysseus:latest \
  -m pytest -q -rs -o cache_dir=/tmp/w3-pytest-cache \
  tests/test_browser_producer_live_contract.py

The x64 probes pass by proving observation contracts and the known defect. They are not a positive merge gate for enabling page effects. Re-enabling needs a separately audited/allowlisted producer that executes only while expected browser incarnation, targetId and optional loaderId still match, rejects stale state atomically before reading/effect, and does not resend an indeterminate effect. No producer changes are implemented here.

The original positive 18-case Docker gate remains mandatory before re-enabling: stable/repeated targets; reload; cross-/same-document navigation; identical URLs; close/recreate; browser and daemon replacement; popup races; destroyed targets; local-launch pin/atomic binding; exact target switch; A-F label collision; lifecycle metadata; timeout/duplicate effects; bfcache; prerender/frame invariant; strict schema. It must run per supported release architecture. Pin success and pre/post checking alone can never substitute for atomic binding.

P1: producer page/document capability unavailable; unregistered sessions and Checkpoint A compatibility paths intentionally denied. P2: private-runtime scan cost/retention, filesystem observation races and architecture-specific live coverage. Wave 4 remains responsible for effects/provenance/egress and truthful completion evidence; no Wave 4 journal or lifecycle redesign is introduced.