Files
harness-tools/docs/fast-browser-development-contracts.md
T
jb fd63b0ccec
fast-browser scenarios / Offline scenario contract tests (push) Failing after 44s
fast-browser scenarios / Local Chromium headless scenarios (push) Failing after 23s
fast-browser-with-jev
2026-09-20 12:24:08 -04:00

12 KiB
Raw Blame History

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 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:

{
  "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 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.