# Fast-browser development contracts Status: frozen implementation boundary, 2026-09-19. The TypeScript source of truth is `extensions/fast-browser/contracts.ts`; this document explains ownership and safety rules, not a second protocol. ## Published exports `BrowserBridge` remains the prototype observe/act/close contract. New work must implement `BrowserRuntime`, a persistent, session-owned extension of that contract. The published operation names are: `observe`, `act`, `read`, `assert`, `navigate`, `reload`, `listTabs`, `selectTab`, `waitPopup`, `closeTab`, `captureEvidence`, `narrowOriginPolicy`, and `close`. `EvidenceRequest.operation` uses the exported `BROWSER_EVIDENCE_OPERATIONS`/`BrowserEvidenceOperationName` allowlist. It contains every supported capture label except session `close`; `closeTab` remains a supported distinct label. `BrowserOperation` is the explicit JSONL/CLI request union. `ScenarioOperation` wraps the existing bounded `Step` as `{op:"step",step}` and reuses the fixed read/assert/control request arms. It has no JavaScript, selector, raw CDP, or arbitrary command arm. `FAST_BROWSER_SCENARIO_FILE_VERSION` and `FAST_BROWSER_REPORT_VERSION` are both `1`; scenario owners may add their validated setup/reset/cleanup details around this union. Scenario input is an explicit caller-provided file path; it is not restricted to fixture IDs. Assertions are finite and read-only: URL `exact` or `origin`, title `exact` or `contains`, and visible-text `contains`. `wait:"bounded"` uses the fixed 3-second read/assertion budget; `wait:"none"` performs one read. A failed or stale read is an operation error, never a mutation or retry. `read` returns only bounded rendered visible text, title, and URL metadata; it excludes input/value properties, password values, hidden text, HTML, frames, and script output. Tab IDs are opaque runtime-issued handles. A handle remains valid across calls for the same target; `documentId` changes on navigation/reload. `listTabs` is origin-filtered and deterministic. `waitPopup` returns an owned popup handle with `autoSwitched:false`; only `selectTab` changes the selected target. No operation infers a target from page text. ## Scenario-only tab references Runtime and transport calls still use opaque `BrowserTabId` values. JSON scenario files use the bounded, data-only `ScenarioTabReference` instead; the CLI resolves it to an opaque ID at execution time. References are either the initial selected tab, an earlier operation's `selectedTab`/`popup` result, or an earlier `listTabs` result at `ScenarioTabIndex` 0–31. Operation references may use an explicit prior numeric `index` (0–31) or prior unique `id`; forward, missing, duplicate, and out-of-range references are rejected before execution. A list operation may include an exact-origin or bounded-index `select` predicate. An origin predicate must match exactly one listed tab. Listing never changes the runtime selection; a later explicit `selectTab` operation does that. Popup waits also never auto-switch. Example: ```json { "version": 1, "id": "popup-check", "operations": [ {"id":"start","op":"read","request":{"tab":{"source":"initial","target":"selected"}}}, {"id":"tabs","op":"listTabs","request":{"select":{"kind":"origin","origin":"http://127.0.0.1:4310"}}}, {"id":"selected","op":"selectTab","request":{"tab":{"source":"operation","operation":{"id":"tabs"},"result":"listedTab","index":0}}}, {"id":"popup","op":"waitPopup","request":{"opener":{"source":"operation","operation":{"id":"selected"},"result":"selectedTab"}}} ] } ``` `selectedTab` is produced by `selectTab`, `popup` by `waitPopup`, and `listedTab` by `listTabs`; there is no interpolation, templating, selector, or script arm. ## Scenario fixture/server lifecycle `ScenarioDefinition.fixture` is optional. When present, `baseUrl` is an explicit credential-free HTTP(S) URL and `server` is either: - `{"mode":"managed","start":...,"readiness":...}`: the CLI starts one long-lived child with trusted argv (`shell:false`), optional cwd, and existing env-reference shape, then owns that process until the scenario `finally` path; - `{"mode":"external","readiness":...}`: no child is started. The caller supplies the explicit already-running readiness URL. `ScenarioHttpReadiness` is GET-only and contains only `url`, optional expected status, and a bounded timeout. It has no headers, body, shell, or page-derived command fields. Setup/reset/cleanup hooks remain one-shot hooks and are distinct from the managed server child. The CLI's existing scenario-wide `budgets.timeoutMs` starts before any hook or server startup; no second global deadline knob is added. The runner polls readiness within the published `FAST_BROWSER_SCENARIO_SERVER_BOUNDS`, keeps the managed child owned through all operations, and performs bounded shutdown in `finally`. Existing definitions without a managed server remain valid when the CLI/user config provides an explicit external readiness URL. Example execution order: ```json { "version": 1, "id": "fixture-login", "fixture": { "baseUrl": "http://127.0.0.1:4310", "server": { "mode": "managed", "start": {"command":"npm","args":["run","fixture"]}, "readiness": {"url":"http://127.0.0.1:4310/health","expectedStatus":200} } }, "operations": [{"op":"step","step":{"instruction":"Continue","action":"click","postcondition":{"kind":"text","value":"Done"}}}] } ``` Order is: start the existing global scenario budget → setup hook → managed server start and bounded GET readiness (or external readiness) → reset hook → browser operations → finally close browser/runtime, run cleanup while the owned server is still available, bounded server stop, and close uncommitted artifact resources while retaining committed artifacts. A readiness failure stops before browser operations and never becomes a page-derived command. ## Policy, budgets, and errors `RuntimeOriginPolicy` preserves the current `"*"` default or a non-empty exact origin list. `narrowOriginPolicy` computes a `RuntimeOperationContext` for an effective request, constrained by the immutable session base; it never irreversibly tightens later calls. Pass that context to the scoped calls, or omit it on a later call to restore the session base. A restricted base never widens. Navigation accepts only bounded HTTP(S) URLs without credentials whose origin is approved. A runtime owns one session's CDP/worker state and serializes its calls; distinct sessions may run in parallel. Its lifetime is the browser-session lifetime, not the first tool call. Cancellation must stop the worker/daemon and release no resource until teardown is confirmed. Close is idempotent and best effort. `FAST_BROWSER_BUDGETS` publishes defaults and hard ceilings for startup (10s), worker readiness (5s), bridge request (10s/120s ceiling for the existing compatibility seam), action verification (3s), read/assertion wait (3s), navigation/reload (10s), popup wait (5s), screenshot (10s/60s ceiling), failure evidence (2s), and a run (60s/120s). Do not add a generic unbounded timeout options object. Failure capture is bounded and best effort; it must never mask the original operation result or retry a mutation. All safe failures use `BrowserErrorDetails`/`BrowserOperationError`, with an operation, phase, safe message, error code, retry classification, and mandatory `mutationState` (`not_attempted`, `verified`, or `unknown`). Raw exceptions are not reportable. ## Transport boundary `BrowserTransport` is the typed JSONL boundary. Its exact API is `dispatch(request: BrowserTransportRequest, signal): Promise>` and `close(): Promise`. `BrowserTransportRequest` contains one `BrowserOperation` plus an optional scoped `RuntimeOperationContext`; `BrowserTransportResultFor` is the exact result mapping for every `BrowserOperation` variant, and `BrowserTransportResult` is its complete union. Evidence transport uses `BrowserTransportArtifactPayload`: JSONL `encoding:"base64"`, `dataBase64`, declared `byteLength`, content type, kind, and truncation flag. The runtime decodes those bytes and writes them to the parent `PrivateArtifactStore`; no caller path crosses into the worker. `FAST_BROWSER_ARTIFACT_LIMITS` caps screenshots at 8 MiB and diagnostics at 256 KiB. `BrowserTransportFactory.create({cdpUrl, allowedOrigins, python?}, parentSignal?)` owns worker framing, validation, deadlines, and teardown. `BrowserRuntime` owns persistent tab/policy state and delegates to this transport; these layers must not both implement the JSONL protocol. `BrowserRuntimeOptions.python` and `BrowserTransportOptions.python` are the explicit interpreter-selection seam for the pinned environment. If omitted, the existing configured/default bridge resolution remains responsible for choosing Python; no worker may silently drop to a user-global environment when an explicit interpreter/factory is supplied. ## Evidence and artifact ownership `PrivateArtifactStore.open(owner, kind)` is the only write API. Evidence requests carry the caller/scenario `runId`; the runtime creates the operation identity. There is no caller-selected path. The session-owned store creates private directories and atomically commits `0600` files under its private root, conceptually `///.(png|json)`. The returned `PrivateArtifactMetadata` records owner, path, byte count, content type, and `localOnly:true/modelForwarded:false`. Runtime close closes the store. Screenshots are local failure/request artifacts and are never automatically sent to an LLM or provider. `captureEvidence` may return a partial/unavailable bundle. Console summaries are bounded level/message/count records. The pinned browser-harness 0.1.13 source supports `drain_events()` and enables Page/DOM/Runtime/Network on tab attach; implementers may consume buffered `Runtime.consoleAPICalled` and `Network.loadingFailed` events only if present. Do not invent a helper method or claim complete console history. Network summaries contain method, origin-only URL, status/category—never query/fragment tokens, headers, cookies, or bodies. Console text can still contain arbitrary page content; local/private and bounded is not the same as secret-free, so public shaping must redact it. ## File ownership / update order - `contracts.ts` — shared API owner (this change); additions must remain typed. - `bridge.ts` — later JSONL transport validation and request deadlines; no new public operation names without updating `contracts.ts` first. - `browser_worker.py` — later pinned browser-harness adapter and persistent target/diagnostic state; use only observed harness capabilities. - `browser-session.ts` — browser process/profile/CDP ownership and parallel session isolation; it does not own tab protocol state. - `controller.ts` — later bounded action orchestration and automatic failure-evidence call; preserve stale/no-blind-retry mutation semantics. - `index.ts` — later public redaction, tool/CLI wiring, and origin narrowing; never expose private artifact contents or raw URL paths. - `fb-scenarios-ci` — scenario file parsing, trusted argv/config hooks, CLI, reports, and CI examples. Composition requires an explicitly supplied model adapter and returns a capability error when absent; never silently selects a provider. Existing test mocks that implement only `BrowserBridge` need no compatibility aliases; runtime mocks must implement every `BrowserRuntime` method. The scenario worker should import the exports above rather than define alternatives.