import type { BrowserGrant } from "./browserGrantVerify.js"; import { type BrowserSessionRecord, type SpaceSpec } from "./sessionManager.js"; import { type BrowserEngineId } from "./browserProvisioner.js"; export interface BrowserRunSpec { runId: string; grant: BrowserGrant; mode: "launch" | "attach"; engine?: BrowserEngineId; cdpWsEndpoint?: string; space?: SpaceSpec; codeMode?: boolean; headless?: boolean; serverBase?: string; runAuthHeader?: string; hitlMode?: string; } export interface BrowserBridge { url: string; port: number; /** Register a verified run. Returns the per-run bearer token to inject * as MEL_BROWSER_TOKEN. The grant itself NEVER leaves this process. */ registerRun(spec: BrowserRunSpec): { token: string; }; /** Idempotent full teardown for one run: cancels in-flight ops, * releases leases, closes ONLY owned contexts, forgets the grant. */ teardownRun(runId: string, reason: string): Promise; teardownAll(reason: string): Promise; /** Tear down every per-turn bridge run belonging to an assistant session * (runId form `${sessionId}:${turnId}`). Called when a host process exits or * crashes so a turn that never sent its `done` frame does not leak its lease * and its enforceOnPage route handlers onto the user's still-open tab. */ teardownRunsForSession(sessionId: string, reason: string): Promise; hasRun(runId: string): boolean; /** * Launch a headed browser as an interactive (grant-free, read-only-watch) * session tracked in sessionManager under `sessionId`. * * The session is governed by browserProvisioner for engine resolution * and by sessionManager for lifecycle. It is discoverable by * setWatchLease(sessionId, ...) and by getInteractiveSession(sessionId), * which is the attach seam for the grant/run path. * * State transitions are reported via `onState(sessionId, state, engine)`: * "active" - browser is open and ready to watch * "closed" - browser was closed by the user or system * "failed" - launch failed (engine not found, Playwright error, etc.) * * Errors during launch are caught internally; the caller receives * "failed" via onState rather than a thrown exception. */ launchInteractive(sessionId: string, engine: string, profile: string, onState: (sessionId: string, state: "active" | "closed" | "failed", engine: string) => void): Promise; /** * Look up an active interactive session by the sessionId that was passed * to launchInteractive. Returns the BrowserSessionRecord if the session * is live, or undefined if it was never launched, has closed, or crashed. * * ATTACH SEAM: the grant/run path in ensureSession should call this * before launching a new browser. If a record is returned the run can * reuse rec.context and the existing page rather than opening a second * browser for the same sessionId. Concretely, in ensureSession the * "LAUNCH mode" block should check: * * const interactive = bridge.getInteractiveSession(spec.sessionId); * if (interactive) { ... reuse interactive.context ... } * * This seam is left as a documented hook; full grant-attach wiring is * a follow-up task once the grant schema carries a sessionId. */ getInteractiveSession(sessionId: string): BrowserSessionRecord | undefined; /** * Set the watch-lease state for a session (plan Section 10.f). * * Called by the connection layer when the server emits a * `browser:watch` socket event `{ sessionId, active }`. When * active=true, the bridge begins capturing frames at an adaptive FPS * (change-detection, capped at 2-4 fps) and POSTing each JPEG frame * to `framePostUrl` as `{ sessionId, runId, jpegB64 }` authenticated * with the runner's relay nonce. When active=false, capturing stops. * * The producer runs only while a viewer is watching and stops * automatically when the run is torn down. * * This method now finds both grant-scoped run sessions (looked up by * runId) AND interactive sessions launched via launchInteractive * (looked up by sessionId directly). */ setWatchLease(sessionId: string, active: boolean, framePostUrl: string, runAuthHeader: string): void; /** * Close one or all interactive sessions (emergency stop / per-session close). * * When sessionId is provided, only that interactive session is closed. * When sessionId is omitted (or undefined), ALL active interactive sessions * on this runner are closed. * * Returns the array of sessionIds that were actually closed. Idempotent; * per-session errors are swallowed (best-effort). Does NOT touch * grant-scoped run sessions (byRunId). */ killInteractive(sessionId?: string): Promise; /** * Set or clear the takeover pause flag (Fix 3). * * Called by the connection layer when the server emits a * `browser:takeover { sessionId?, paused }` socket event. * * While paused, `performAct` blocks all consequential act kinds * (navigate, click, tap, input_text, press_key, scroll, * select_option, upload_file, submit, back, forward, batch, * ask_user) and returns a `BridgeError("paused_by_user", ...)` so * the agent stops trying and waits. Read-only ops (get_screen_tree, * screenshot, get_text, current_target, wait, * wait_for_network_idle) are not blocked. * * If sessionId is provided, only that interactive session is paused. * If sessionId is omitted, ALL sessions on this runner are paused. * The flag is cleared on teardownRun / session teardown. */ setPaused(paused: boolean, sessionId?: string): void; /** * Switch the active tab for an interactive session to the page identified * by `ref` (the opaque string returned by getTabRef). The frame producer * follows the new active page on its next tick. * * Returns true if the tab was found and switched, false otherwise. */ switchTabForSession(sessionId: string, ref: string): boolean; shutdown(): Promise; } export declare function startBrowserBridge(opts: { log: (msg: string) => void; verbose?: boolean; }): Promise; export declare class BridgeError extends Error { readonly code: string; constructor(code: string, message: string); }