12 KiB
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:
{
"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 scenariofinallypath;{"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:
{
"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<Operation extends BrowserOperation>(request: BrowserTransportRequest<Operation>, signal): Promise<BrowserTransportResultFor<Operation>> and
close(): Promise<void>. BrowserTransportRequest contains one
BrowserOperation plus an optional scoped RuntimeOperationContext;
BrowserTransportResultFor<Operation> 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 <root>/<sessionId>/<runId>/<artifactId>.(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 updatingcontracts.tsfirst.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.