218 lines
12 KiB
Markdown
218 lines
12 KiB
Markdown
# 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.
|