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

218 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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.