fast-browser-with-jev
fast-browser scenarios / Offline scenario contract tests (push) Failing after 44s
fast-browser scenarios / Local Chromium headless scenarios (push) Failing after 23s

This commit is contained in:
jb
2026-09-20 12:24:08 -04:00
parent fd0edb1511
commit fd63b0ccec
73 changed files with 21212 additions and 2 deletions
+217
View File
@@ -0,0 +1,217 @@
# 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.