/** Shared prototype contracts. Browser handles never come from model output. */ export type BrowserAction = "click" | "fill"; export interface Candidate { id: string; role: string; name: string; actions: BrowserAction[]; } export interface Observation { observationId: string; url: string; title: string; candidates: Candidate[]; } export type Postcondition = | { kind: "url"; value: string } | { kind: "text"; value: string }; export interface ActRequest { observationId: string; candidateId: string; action: BrowserAction; text?: string; /** Mandatory for clicks. Filling always verifies the actual field value. */ postcondition?: Postcondition; } export interface ActResult { verified: boolean; evidence: string; } export interface BrowserBridge { observe(signal: AbortSignal): Promise; /** Must revalidate the snapshot's tab/document/target immediately before mutation, * consume all old observations, and verify the outcome. Never retry a mutation. */ act(request: ActRequest, signal: AbortSignal): Promise; /** Must terminate owned workers before releasing the caller's browser lane. */ close(): Promise; } export interface Step { instruction: string; action: BrowserAction; /** A literal is inserted exactly; compose asks the auxiliary model for new text. */ text?: string; compose?: string; /** Reference to a credential supplied by the initiating session; never sent to JEV. */ credential?: string; postcondition?: Postcondition; } export interface Decision { candidateId: string | null; confidence: number; model: string; } export interface DecisionInput { instruction: string; action: BrowserAction; observation: Observation; } export type CredentialSource = string | { env: string } | { file: string; key?: string }; export type CredentialBindings = Record; export interface StepTrace { step: number; status: "verified" | "abstained" | "unverified" | "error"; timingsMs: Record; /** Always emitted by the controller; unknown forbids automatic retry. */ mutationState?: "not_attempted" | "verified" | "unknown"; candidateId?: string; confidence?: number; model?: string; evidence?: string; } export interface RunResult { status: "completed" | "stopped"; reason: string; steps: StepTrace[]; elapsedMs: number; /** True when a dispatched action lacks a verified outcome. Never blindly retry. */ actionMayHaveExecuted?: boolean; } /** * Development contracts for the session-scoped runtime. The prototype * BrowserBridge above remains the compatibility boundary for observe/act/close; * new workers should implement BrowserRuntime rather than adding ad-hoc CDP * methods to BrowserBridge. */ export type MutationState = "not_attempted" | "verified" | "unknown"; export type BrowserErrorCode = | "aborted" | "timeout" | "stale" | "policy_denied" | "invalid_request" | "no_tab" | "ambiguous_tab" | "browser_closed" | "unsupported" | "worker_failure"; /** Safe to serialize to a report. Never include a URL query/fragment, headers, * bodies, field values, page text, provider response, or raw exception. */ export interface BrowserErrorDetails { code: BrowserErrorCode; operation: BrowserRuntimeOperationName; phase: string; message: string; mutationState: MutationState; retry: "never" | "safe"; } export class BrowserOperationError extends Error { readonly details: BrowserErrorDetails; override name = "BrowserOperationError"; constructor(details: BrowserErrorDetails) { super(details.message); this.details = details; } } /** Fixed budgets: implementations may choose a lower internal deadline, but * no caller-supplied generic timeout may exceed these ceilings. */ export const FAST_BROWSER_BUDGETS = { browserStartupMs: { default: 10_000, hardCeiling: 10_000 }, workerReadyMs: { default: 5_000, hardCeiling: 5_000 }, bridgeRequestMs: { default: 10_000, hardCeiling: 120_000 }, actionVerificationMs: { default: 3_000, hardCeiling: 3_000 }, readAssertionWaitMs: { default: 3_000, hardCeiling: 3_000 }, navigationMs: { default: 10_000, hardCeiling: 10_000 }, reloadMs: { default: 10_000, hardCeiling: 10_000 }, popupWaitMs: { default: 5_000, hardCeiling: 5_000 }, screenshotMs: { default: 10_000, hardCeiling: 60_000 }, failureEvidenceMs: { default: 2_000, hardCeiling: 2_000 }, runMs: { default: 60_000, hardCeiling: 120_000 }, } as const; /** The session base policy is the existing '*' default or a non-empty exact * origin list. A runtime policy request may only narrow that base. */ export type RuntimeOriginPolicy = "*" | readonly string[]; /** Per-operation policy context. It is derived from the immutable session base * and is not a persistent cross-call mutation. Omit it to use the base. */ export interface RuntimeOperationContext { allowedOrigins: RuntimeOriginPolicy; } /** Opaque and stable for the life of a target; never model-created. */ export type BrowserTabId = string & { readonly __fastBrowserTabId: unique symbol }; export interface BrowserTab { tabId: BrowserTabId; kind: "page" | "popup"; openerTabId?: BrowserTabId; selected: boolean; owned: true; /** Internal runtime value; public reports must reduce this to origin. */ url: string; origin: string; title: string; } export interface ReadRequest { tabId?: BrowserTabId; } /** Read returns rendered visible text only. It never returns form values, * password values, hidden text, HTML, frames, or arbitrary script output. */ export interface PageRead { tab: BrowserTab; documentId: string; url: string; title: string; visibleText: string; visibleTextTruncated: boolean; } export type PageAssertion = | { kind: "url"; match: "exact" | "origin"; value: string } | { kind: "title"; match: "exact" | "contains"; value: string } | { kind: "visibleText"; match: "contains"; value: string }; export interface AssertRequest { tabId?: BrowserTabId; assertion: PageAssertion; /** "bounded" uses the fixed readAssertionWaitMs budget and performs no * mutation; "none" checks one read immediately. */ wait: "none" | "bounded"; } export interface AssertResult { status: "satisfied" | "not_satisfied" | "timed_out"; readOnly: true; tab: BrowserTab; documentId: string; } export interface NavigateRequest { tabId?: BrowserTabId; /** HTTP(S), without credentials; destination origin must be approved. */ url: string; } export interface NavigationResult { status: "committed"; mutationState: "verified"; tab: BrowserTab; documentId: string; } export interface ReloadRequest { tabId?: BrowserTabId; } export interface ReloadResult { status: "committed"; mutationState: "verified"; tab: BrowserTab; documentId: string; } export interface SelectTabRequest { tabId: BrowserTabId; } export interface SelectTabResult { selected: true; tab: BrowserTab; } export interface WaitPopupRequest { openerTabId: BrowserTabId; } export interface WaitPopupResult { status: "found" | "timed_out"; /** A found popup is returned, but is never selected automatically. */ tab?: BrowserTab; autoSwitched: false; } export interface CloseTabRequest { tabId: BrowserTabId; } export interface CloseTabResult { closed: true; tabId: BrowserTabId; mutationState: "verified"; } export interface NarrowOriginPolicyRequest { allowedOrigins: RuntimeOriginPolicy; } export interface ConsoleFailureSummary { level: "error" | "warning"; message: string; count: number; truncated: boolean; } /** Network evidence is deliberately origin-only: no query/fragment, headers, * cookies, request/response bodies, or field values. */ export interface NetworkFailureSummary { method: string; origin: string; status?: number; category: "http" | "network" | "blocked"; } export type PrivateArtifactKind = "screenshot" | "diagnostics"; export interface PrivateArtifactOwner { sessionId: string; runId: string; operationId: string; } export interface PrivateArtifactMetadata { artifactId: string; kind: PrivateArtifactKind; contentType: "image/png" | "application/json"; path: string; bytes: number; owner: PrivateArtifactOwner; permissions: "0600"; localOnly: true; modelForwarded: false; } /** No caller-selected path: the store owns the path and commits atomically. */ export interface PrivateArtifactWriter { readonly owner: PrivateArtifactOwner; readonly kind: PrivateArtifactKind; write(chunk: Uint8Array): Promise; commit(): Promise; abort(): Promise; } export interface PrivateArtifactStore { open(owner: PrivateArtifactOwner, kind: PrivateArtifactKind): Promise; close(): Promise; } /** JSONL-safe worker-to-parent bytes. The parent decodes dataBase64 and writes * it through PrivateArtifactStore; worker requests never contain caller paths. */ export interface BrowserTransportArtifactPayload { kind: PrivateArtifactKind; contentType: "image/png" | "application/json"; encoding: "base64"; dataBase64: string; byteLength: number; truncated: boolean; } export interface BrowserTransportEvidence { status: "complete" | "partial" | "unavailable"; screenshot?: BrowserTransportArtifactPayload; diagnostics?: BrowserTransportArtifactPayload; console: readonly ConsoleFailureSummary[]; failedRequests: readonly NetworkFailureSummary[]; } export const FAST_BROWSER_ARTIFACT_LIMITS = { screenshotBytes: 8 * 1024 * 1024, diagnosticsBytes: 256 * 1024, } as const; export const BROWSER_EVIDENCE_OPERATIONS = [ "observe", "act", "read", "assert", "navigate", "reload", "listTabs", "selectTab", "waitPopup", "closeTab", "captureEvidence", "narrowOriginPolicy", ] as const; export type BrowserEvidenceOperationName = (typeof BROWSER_EVIDENCE_OPERATIONS)[number]; export interface EvidenceRequest { /** Caller/scenario run identity; the runtime creates the operation identity. */ runId: string; reason: "failure" | "requested"; operation?: BrowserEvidenceOperationName; } export interface FailureEvidence { status: "complete" | "partial" | "unavailable"; screenshot?: PrivateArtifactMetadata; diagnostics?: PrivateArtifactMetadata; console: readonly ConsoleFailureSummary[]; failedRequests: readonly NetworkFailureSummary[]; } /** A persistent per-session bridge. Different sessions may run in parallel; * all operations on one runtime are serialized. Tab handles/document identity * survive calls until the target/document is closed or navigated. */ export interface BrowserRuntime extends BrowserBridge { readonly sessionId: string; /** Computes one effective request policy; it never permanently tightens the * immutable session base. Pass the returned context to one or more calls in * the same explicitly scoped operation, or omit context to restore base. */ narrowOriginPolicy(request: NarrowOriginPolicyRequest, signal: AbortSignal): Promise; observe(signal: AbortSignal, context?: RuntimeOperationContext): Promise; act(request: ActRequest, signal: AbortSignal, context?: RuntimeOperationContext): Promise; read(request: ReadRequest, signal: AbortSignal, context?: RuntimeOperationContext): Promise; assert(request: AssertRequest, signal: AbortSignal, context?: RuntimeOperationContext): Promise; navigate(request: NavigateRequest, signal: AbortSignal, context?: RuntimeOperationContext): Promise; reload(request: ReloadRequest, signal: AbortSignal, context?: RuntimeOperationContext): Promise; listTabs(signal: AbortSignal, context?: RuntimeOperationContext): Promise; selectTab(request: SelectTabRequest, signal: AbortSignal, context?: RuntimeOperationContext): Promise; waitPopup(request: WaitPopupRequest, signal: AbortSignal, context?: RuntimeOperationContext): Promise; closeTab(request: CloseTabRequest, signal: AbortSignal, context?: RuntimeOperationContext): Promise; captureEvidence(request: EvidenceRequest, signal: AbortSignal, context?: RuntimeOperationContext): Promise; } export interface BrowserRuntimeOptions { sessionId: string; cdpUrl: string; allowedOrigins: RuntimeOriginPolicy; /** Explicit interpreter selection for the pinned worker environment. */ python?: string; artifacts: PrivateArtifactStore; /** Test/embedding seam; production may use the default transport factory. */ transportFactory?: BrowserTransportFactory; } export interface BrowserRuntimeFactory { create(options: BrowserRuntimeOptions, parentSignal?: AbortSignal): BrowserRuntime; } /** The transport owns JSONL framing, validation, deadlines, and worker * teardown. BrowserRuntime owns persistent tab/policy state and delegates one * typed operation at a time; neither layer implements the other's boundary. */ export interface BrowserTransportRequest { operation: Operation; context?: RuntimeOperationContext; } export interface BrowserTransportCloseResult { closed: true; } /** Exact result mapping for every BrowserOperation variant. */ export type BrowserTransportResultFor = Operation extends { op: "observe" } ? { op: "observe"; result: Observation } : Operation extends { op: "act" } ? { op: "act"; result: ActResult } : Operation extends { op: "read" } ? { op: "read"; result: PageRead } : Operation extends { op: "assert" } ? { op: "assert"; result: AssertResult } : Operation extends { op: "navigate" } ? { op: "navigate"; result: NavigationResult } : Operation extends { op: "reload" } ? { op: "reload"; result: ReloadResult } : Operation extends { op: "listTabs" } ? { op: "listTabs"; result: readonly BrowserTab[] } : Operation extends { op: "selectTab" } ? { op: "selectTab"; result: SelectTabResult } : Operation extends { op: "waitPopup" } ? { op: "waitPopup"; result: WaitPopupResult } : Operation extends { op: "closeTab" } ? { op: "closeTab"; result: CloseTabResult } : Operation extends { op: "captureEvidence" } ? { op: "captureEvidence"; result: BrowserTransportEvidence } : Operation extends { op: "narrowOriginPolicy" } ? { op: "narrowOriginPolicy"; result: RuntimeOperationContext } : Operation extends { op: "close" } ? { op: "close"; result: BrowserTransportCloseResult } : never; export type BrowserTransportResult = BrowserTransportResultFor; export interface BrowserTransport { dispatch( request: BrowserTransportRequest, signal: AbortSignal, ): Promise>; close(): Promise; } export interface BrowserTransportOptions { cdpUrl: string; allowedOrigins: RuntimeOriginPolicy; python?: string; } export interface BrowserTransportFactory { create(options: BrowserTransportOptions, parentSignal?: AbortSignal): BrowserTransport; } export const BROWSER_RUNTIME_OPERATIONS = [ "observe", "act", "read", "assert", "navigate", "reload", "listTabs", "selectTab", "waitPopup", "closeTab", "captureEvidence", "narrowOriginPolicy", "close", ] as const; export type BrowserRuntimeOperationName = (typeof BROWSER_RUNTIME_OPERATIONS)[number]; /** Explicit JSONL/CLI operation shapes. There is no arbitrary CDP or JS arm. */ export type BrowserOperation = | { op: "observe" } | { op: "act"; request: ActRequest } | { op: "read"; request: ReadRequest } | { op: "assert"; request: AssertRequest } | { op: "navigate"; request: NavigateRequest } | { op: "reload"; request: ReloadRequest } | { op: "listTabs" } | { op: "selectTab"; request: SelectTabRequest } | { op: "waitPopup"; request: WaitPopupRequest } | { op: "closeTab"; request: CloseTabRequest } | { op: "captureEvidence"; request: EvidenceRequest } | { op: "narrowOriginPolicy"; request: NarrowOriginPolicyRequest } | { op: "close" }; /** Scenario references are data-only handles resolved by the CLI runner. * They cannot contain selectors, expressions, templates, CDP IDs, or JS. */ /** Scenario files are bounded to 32 operations by the CLI owner. */ export type ScenarioOperationIndex = | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31; export type ScenarioOperationRef = | { index: ScenarioOperationIndex } | { id: string }; /** Tab list indices are intentionally finite and bounded for JSON scenarios. */ export type ScenarioTabIndex = ScenarioOperationIndex; /** Initial means the runtime's selected tab at scenario start. Operation * references must point to an earlier operation; the runner rejects forward * references and duplicate/unknown IDs. */ export type ScenarioTabReference = | { source: "initial"; target: "selected" } | { source: "operation"; operation: ScenarioOperationRef; result: "selectedTab" | "popup"; } | { source: "operation"; operation: ScenarioOperationRef; result: "listedTab"; index: ScenarioTabIndex; }; /** An origin selector must resolve to exactly one bounded listed tab. It never * changes the selected runtime tab by itself. */ export type ScenarioTabSelection = | { kind: "index"; index: ScenarioTabIndex } | { kind: "origin"; origin: string }; export interface ScenarioReadRequest { tab?: ScenarioTabReference; } export interface ScenarioAssertRequest { tab?: ScenarioTabReference; assertion: PageAssertion; wait: "none" | "bounded"; } export interface ScenarioNavigateRequest { tab?: ScenarioTabReference; url: string; } export interface ScenarioReloadRequest { tab?: ScenarioTabReference; } export interface ScenarioListTabsRequest { /** Lists deterministically and optionally identifies one unique result; * selection still requires a later explicit selectTab operation. */ select?: ScenarioTabSelection; } export interface ScenarioSelectTabRequest { tab: ScenarioTabReference; } export interface ScenarioWaitPopupRequest { opener: ScenarioTabReference; } export interface ScenarioCloseTabRequest { tab: ScenarioTabReference; } export interface ScenarioOperationMetadata { /** Optional stable local reference for later ScenarioOperationRef{id}. */ id?: string; } /** Scenarios reuse the existing bounded Step instead of inventing browser code. */ export type ScenarioOperation = | ({ op: "step"; step: Step } & ScenarioOperationMetadata) | ({ op: "read"; request: ScenarioReadRequest } & ScenarioOperationMetadata) | ({ op: "assert"; request: ScenarioAssertRequest } & ScenarioOperationMetadata) | ({ op: "navigate"; request: ScenarioNavigateRequest } & ScenarioOperationMetadata) | ({ op: "reload"; request: ScenarioReloadRequest } & ScenarioOperationMetadata) | ({ op: "listTabs"; request?: ScenarioListTabsRequest } & ScenarioOperationMetadata) | ({ op: "selectTab"; request: ScenarioSelectTabRequest } & ScenarioOperationMetadata) | ({ op: "waitPopup"; request: ScenarioWaitPopupRequest } & ScenarioOperationMetadata) | ({ op: "closeTab"; request: ScenarioCloseTabRequest } & ScenarioOperationMetadata); export const FAST_BROWSER_SCENARIO_FILE_VERSION = 1 as const; export const FAST_BROWSER_REPORT_VERSION = 1 as const; export const FAST_BROWSER_SCENARIO_SERVER_BOUNDS = { readinessTimeoutMs: { default: 10_000, hardCeiling: 30_000 }, readinessPollMs: { default: 100, hardCeiling: 1_000 }, serverShutdownMs: { default: 5_000, hardCeiling: 10_000 }, } as const; /** Existing scenario hooks use this same trusted argv/cwd/env-reference * shape. `start` is long-lived and is supervised until scenario finally; it is * not a setup/reset/cleanup hook and has no shell mode. */ export interface ScenarioProcessHook { command: string; args: string[]; cwd?: string; env?: Record; timeoutMs?: number; } export interface ScenarioHttpReadiness { /** GET-only HTTP(S) URL supplied by scenario/user config, never page data. */ url: string; expectedStatus?: number; timeoutMs?: number; } export type ScenarioServerConfig = | { mode: "managed"; start: ScenarioProcessHook; readiness: ScenarioHttpReadiness; shutdownTimeoutMs?: number; } | { mode: "external"; readiness: ScenarioHttpReadiness; }; export interface ScenarioFixtureConfig { /** Explicit HTTP(S) fixture base URL; no credentials or page-derived URL. */ baseUrl: string; server?: ScenarioServerConfig; } export interface ScenarioDefinition { version: typeof FAST_BROWSER_SCENARIO_FILE_VERSION; id: string; operations: readonly ScenarioOperation[]; fixture?: ScenarioFixtureConfig; } export interface ScenarioOperationReport { index: number; op: ScenarioOperation["op"]; status: "passed" | "failed" | "skipped"; elapsedMs: number; mutationState: MutationState; error?: BrowserErrorDetails; } export interface ScenarioReport { version: typeof FAST_BROWSER_REPORT_VERSION; scenarioId: string; status: "passed" | "failed" | "skipped"; elapsedMs: number; operations: readonly ScenarioOperationReport[]; evidence: readonly PrivateArtifactMetadata[]; cleanup: "complete" | "failed"; }