From 3d3718f94a7f1f8d0c689b9463f2b24d0e1249bf Mon Sep 17 00:00:00 2001 From: v0 Date: Mon, 20 Apr 2026 00:54:15 +0000 Subject: [PATCH 1/3] feat: extend bridge with session lifecycle and observability tools Add new session lifecycle methods and wire tools into registry. Co-authored-by: Jeremy Schulze <197647907+Delqhi@users.noreply.github.com> --- docs/BRIDGE_CONTRACT_V1.md | 146 ++++++ docs/REWRITE_TRACKER.md | 67 +++ extension/src/contract/v1/index.js | 333 ++++++++++++ extension/src/drivers/evidence.js | 149 ++++++ extension/src/drivers/session-lifecycle.js | 243 +++++++++ extension/src/drivers/trace.js | 88 ++++ extension/src/tools/aliases.js | 11 + extension/src/tools/contract.js | 64 +++ extension/src/tools/evidence.js | 46 ++ extension/src/tools/index.js | 9 + extension/src/tools/session.js | 70 +++ extension/src/tools/stealth.js | 162 ++++++ package.json | 15 +- pnpm-lock.yaml | 582 ++++++++++++++++++++- scripts/validate-bridge-contract.mjs | 125 +++++ server.js | 19 + test/contract/v1.test.mjs | 106 ++++ test/evidence/bundle.test.mjs | 96 ++++ test/session/lifecycle.test.mjs | 102 ++++ 19 files changed, 2429 insertions(+), 4 deletions(-) create mode 100644 docs/BRIDGE_CONTRACT_V1.md create mode 100644 docs/REWRITE_TRACKER.md create mode 100644 extension/src/contract/v1/index.js create mode 100644 extension/src/drivers/evidence.js create mode 100644 extension/src/drivers/session-lifecycle.js create mode 100644 extension/src/drivers/trace.js create mode 100644 extension/src/tools/contract.js create mode 100644 extension/src/tools/evidence.js create mode 100644 extension/src/tools/stealth.js create mode 100644 scripts/validate-bridge-contract.mjs create mode 100644 test/contract/v1.test.mjs create mode 100644 test/evidence/bundle.test.mjs create mode 100644 test/session/lifecycle.test.mjs diff --git a/docs/BRIDGE_CONTRACT_V1.md b/docs/BRIDGE_CONTRACT_V1.md new file mode 100644 index 0000000..c408bf8 --- /dev/null +++ b/docs/BRIDGE_CONTRACT_V1.md @@ -0,0 +1,146 @@ +# OpenSIN-Bridge Contract v1 + +> Status: stable +> Schema: `opensin.bridge.contract/v1` +> Source of truth: `extension/src/contract/v1/index.js` +> Worker counterpart: `OpenSIN-AI/A2A-SIN-Worker-heypiggy/bridge_contract.py` + +## Why + +Worker issue [#69](https://github.com/OpenSIN-AI/A2A-SIN-Worker-heypiggy/issues/69) +demands a versioned contract for browser commands, events, retries, and failure +classes. Previously the bridge surface was the union of `extension/src/tools/*` +plus an organic alias layer in `aliases.js`. That worked for ad-hoc agents but +gave downstream workers no machine-readable promise about: + +- which methods are safe to retry, +- which methods mutate browser state, +- what failure shape the worker can branch on. + +This document is the human-readable view. The machine-readable view ships in +`extension/src/contract/v1/index.js` and is exposed at runtime via +`bridge.contract`. + +## Read it + +```js +const contract = await rpc("bridge.contract") +contract.version // "opensin.bridge.contract/v1" +contract.revision // 1 +contract.methods // [{ name, idempotent, mutates, raises, retryHint, ... }] +contract.errorCodes // ["transport_error", "target_gone", ...] +contract.retryHints // { transport_error: "safe_retry", ... } +contract.internalToContract // { TIMEOUT: "timeout", ... } +``` + +The contract is also pinned in CI via +`scripts/validate-bridge-contract.mjs` (`pnpm run validate:bridge-contract`). + +## Idempotency model + +Every method declares an `idempotent` boolean. The rule is: + +- `idempotent: true` → calling it twice with the same params produces the + same observable browser state and is therefore safe to retry on transient + failure. +- `idempotent: false` → the worker MUST NOT blindly retry. Either it has + postconditions to verify the action took effect, or it must escalate to + recovery. + +Examples: + +| Method | Idempotent | Mutates | Reasoning | +| ----------------- | ---------- | ------- | -------------------------------------------------------- | +| `tabs.list` | yes | no | Read-only. | +| `tabs.activate` | yes | yes | Same tabId twice converges to the same active tab. | +| `nav.goto` | no | yes | SPAs may build different state per navigation. | +| `dom.click` | no | yes | Two submits = double order. Worker must verify. | +| `dom.snapshot` | yes | no | Read-only. | +| `session.invalidate` | yes | yes | Already-invalid session stays invalid; converges. | +| `session.restore` | no | yes | Restoring twice can clobber fresh state. Worker decides. | + +## Error taxonomy + +Failures map onto a small, stable code set (full list: +`extension/src/contract/v1/index.js`, `ERROR_CODES`). The bridge never invents +new error codes at runtime — every internal `BridgeError` is translated via +`INTERNAL_TO_CONTRACT`. + +### Transport / RPC envelope + +| Code | Meaning | +| ----------------- | ------------------------------------------------------ | +| `transport_error` | WS / native host disconnected, request never reached. | +| `rpc_invalid` | Malformed RPC envelope or invalid params. | +| `unknown_method` | Method not registered in this contract revision. | +| `rate_limited` | Bridge-internal rate limit hit. Retry with backoff. | +| `timeout` | Tool exceeded its timeout budget. | + +### Browser surface + +| Code | Meaning | +| --------------------- | -------------------------------------------------------- | +| `target_gone` | Tab/frame closed during call. Recover then retry. | +| `navigation_aborted` | Navigation interrupted (programmatic or user). | +| `navigation_timeout` | Navigation didn't complete within timeoutMs. | +| `cdp_failed` | Chrome DevTools Protocol failure (debugger detached). | +| `frame_detached` | The selected frame is gone. | + +### DOM / interaction + +| Code | Meaning | +| -------------------------- | ---------------------------------------------------- | +| `element_not_found` | Selector / ref didn't resolve. | +| `element_not_actionable` | Resolved but covered, disabled, or zero-size. | +| `postcondition_failed` | Mutating action did not produce expected change. | +| `duplicate_action` | Submit-style guard prevented a second click. | + +### Session + +| Code | Meaning | +| ----------------------- | ------------------------------------------------------ | +| `session_invalid` | Manifest invalidated (logged out, cookie purged, ...). | +| `session_stale` | TTL expired. Worker can extend or reacquire. | +| `session_locked` | Another worker holds the session lock. | +| `origin_not_permitted` | Origin not in the bridge's allowed list. | + +### Adversarial environment + +| Code | Meaning | +| --------------------- | ------------------------------------------------------ | +| `anti_bot_challenge` | Cloudflare / DataDome / PerimeterX detected. | +| `captcha_required` | reCAPTCHA / hCaptcha / Turnstile interactive challenge.| +| `rate_limit_remote` | Remote site (not bridge) is throttling. | + +## Retry hints + +Every error code carries a hint (`safe_retry`, `recover_then_retry`, `abort`). +The worker's retry loop SHOULD use this to decide between: + +- `safe_retry` → exponential backoff in place +- `recover_then_retry` → re-acquire context (session, frame, tab) first +- `abort` → escalate to higher-level policy + +Hints are advisory. The worker is the policy authority — the bridge is the +classifier. + +## Stability guarantees + +Within v1: + +- No method is removed. +- No error code is repurposed. +- Adding a method or relaxing idempotency bumps `revision`. + +A breaking change forces a new file under `extension/src/contract/v2/index.js` +and a parallel exposure (`bridge.contract` always returns the active major). + +## Related tools + +- `bridge.contract` — get the full contract object +- `bridge.contract.method` — get one method's metadata +- `bridge.contract.translate` — translate an internal code to a contract code +- `bridge.contract.idempotent` — yes/no shortcut +- `bridge.contract.version` — version + revision only +- `bridge.evidenceBundle` — pair with the contract for failure triage (#70) +- `bridge.traces` — recent dispatches, optionally by `traceId` diff --git a/docs/REWRITE_TRACKER.md b/docs/REWRITE_TRACKER.md new file mode 100644 index 0000000..c6ac5eb --- /dev/null +++ b/docs/REWRITE_TRACKER.md @@ -0,0 +1,67 @@ +# Rewrite Tracker — HeyPiggy worker + OpenSIN-Bridge + +> Last updated: 2026-04-20 +> Master: [worker #67](https://github.com/OpenSIN-AI/A2A-SIN-Worker-heypiggy/issues/67) +> Base SHA: `bf1cd8c` on `main` (worker repo) + +## Why this file exists + +Worker repo `OpenSIN-AI/A2A-SIN-Worker-heypiggy` issue #67 froze the previous +implementation as broken and split the rewrite into nine sub-issues. Several of +them have a **bridge-side counterpart** that has to land in this repo so the +worker can be rewritten against a stable contract instead of the old organic +surface. + +This file is the index. Each row links the worker issue to the concrete +bridge artifacts that satisfy its bridge-side requirements. + +## Coverage matrix + +| Worker issue | Theme | Bridge artifacts | Status | +| ------------ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| #68 | Architecture RFC | (worker-only) — the bridge contract v1 (`docs/BRIDGE_CONTRACT_V1.md`) is the bridge-side input to the RFC. | done | +| #69 | Bridge contract | `extension/src/contract/v1/index.js`, `extension/src/tools/contract.js`, `docs/BRIDGE_CONTRACT_V1.md`, `scripts/validate-bridge-contract.mjs`, `bridge.contract` RPC | done | +| #70 | Observability / evidence | `extension/src/drivers/evidence.js`, `extension/src/drivers/trace.js`, `extension/src/tools/evidence.js`, `bridge.evidenceBundle` and `bridge.traces` RPCs | done | +| #71 | Session lifecycle | `extension/src/drivers/session-lifecycle.js`, `session.manifest`, `session.invalidate`, `session.lastKnownGood`, `session.health` RPCs | done | +| #72 | Runtime state machine | (worker-only) — bridge exposes the primitives the FSM consumes (snapshot, dom.click, session.health, evidenceBundle, contract). | n/a | +| #73 | Interaction engine | (worker-only) — bridge already exposes actionability via `dom.fullSnapshot` + `dom.click` postcondition fields. Contract documents the failure codes. | n/a | +| #74 | Stealth strategy | `extension/src/tools/stealth.js`, `stealth.assess`, `stealth.detectChallenge`. The existing `stealth-main.js` is unchanged; this adds the assessment surface. | done | +| #75 | Panel plugins | (worker-only) — bridge stays panel-agnostic by design (`BOUNDARIES.md`). | n/a | +| #76 | Validation harness | `scripts/validate-bridge-contract.mjs`, `test/contract/*.test.mjs`, `test/session/*.test.mjs`, `test/evidence/*.test.mjs`, package script `validate:bridge-contract` | done | + +## How to consume from the worker + +```python +# heypiggy/bridge_contract.py +import json +import urllib.request + +def fetch_contract(bridge_http_url: str) -> dict: + body = {"jsonrpc": "2.0", "id": 1, "method": "tools/call", + "params": {"name": "bridge.contract", "arguments": {}}} + req = urllib.request.Request( + f"{bridge_http_url}/mcp", + data=json.dumps(body).encode(), + headers={"Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=10) as resp: + envelope = json.loads(resp.read()) + text = envelope["result"]["content"][0]["text"] + return json.loads(text) +``` + +The worker pins the contract revision at boot, validates that every +method/error code it expects is present, and aborts startup if anything is +missing. That is the gate worker issue #69 demands. + +## Stop conditions + +Per worker issue #76, the rewrite can not enable wider rollout until: + +- `pnpm run validate:bridge-contract` passes in CI on this repo +- the worker-side replay harness clears the synthetic flow set +- a live canary run produces a complete `bridge.evidenceBundle` for every + failure (no unclassified errors) + +Until those gates pass the worker stays in the locked-down rewrite branch +established in worker issue #67. diff --git a/extension/src/contract/v1/index.js b/extension/src/contract/v1/index.js new file mode 100644 index 0000000..004cc82 --- /dev/null +++ b/extension/src/contract/v1/index.js @@ -0,0 +1,333 @@ +/** + * OpenSIN-Bridge Contract v1 + * ========================================================================== + * + * Versioned, machine-readable contract between the Bridge (Chrome extension + * + server) and any worker that drives it. Implements the deliverables of + * issue #69 (worker-repo) on the Bridge side: + * + * - command/event schema (namespaces, params, returns, idempotency) + * - idempotent vs non-idempotent action model + * - error taxonomy (`transport_error`, `target_gone`, `navigation_aborted`, + * `anti_bot_challenge`, ...) + * - retry-policy hints separated from business-recovery policy + * + * Design choices: + * + * - The contract is data, not code. It is consumed by: + * 1. `extension/src/tools/contract.js` (exposed as `bridge.contract`) + * 2. `scripts/validate-bridge-contract.mjs` (CI guard) + * 3. The worker-side counterpart in heypiggy `bridge_contract.py` + * - The contract DOES NOT replace the existing tool registry. It declares + * the public surface area the bridge guarantees and how callers should + * classify failures. + * - Adding a new contract version means: bump VERSION, copy this file to + * `extension/src/contract/v2/index.js`, and keep v1 frozen forever. + * + * Stability rules: + * - Removing a method or namespace is a MAJOR change → new version file. + * - Adding a method or making a non-idempotent method idempotent is a + * MINOR change → bump `revision`. + * - Editing wording / docs only is a PATCH change → no revision bump. + */ + +export const VERSION = "opensin.bridge.contract/v1" +export const REVISION = 1 + +/** + * Stable error taxonomy. The string codes are part of the public contract + * and must never be repurposed. Workers branch on these codes to decide + * between retry, recovery, or abort. + */ +export const ERROR_CODES = Object.freeze({ + // ----- transport / RPC envelope ------------------------------------------ + TRANSPORT_ERROR: "transport_error", + RPC_INVALID: "rpc_invalid", + UNKNOWN_METHOD: "unknown_method", + RATE_LIMITED: "rate_limited", + TIMEOUT: "timeout", + + // ----- browser surface ---------------------------------------------------- + TARGET_GONE: "target_gone", // tab closed / detached during call + NAVIGATION_ABORTED: "navigation_aborted", + NAVIGATION_TIMEOUT: "navigation_timeout", + CDP_FAILED: "cdp_failed", + FRAME_DETACHED: "frame_detached", + + // ----- DOM / interaction ------------------------------------------------- + ELEMENT_NOT_FOUND: "element_not_found", + ELEMENT_NOT_ACTIONABLE: "element_not_actionable", + POSTCONDITION_FAILED: "postcondition_failed", + DUPLICATE_ACTION: "duplicate_action", + + // ----- authentication / session ------------------------------------------ + SESSION_INVALID: "session_invalid", + SESSION_STALE: "session_stale", + SESSION_LOCKED: "session_locked", + ORIGIN_NOT_PERMITTED: "origin_not_permitted", + + // ----- adversarial environment ------------------------------------------- + ANTI_BOT_CHALLENGE: "anti_bot_challenge", + CAPTCHA_REQUIRED: "captcha_required", + RATE_LIMIT_REMOTE: "rate_limit_remote", + + // ----- bridge internals -------------------------------------------------- + PRECONDITION_FAILED: "precondition_failed", + UNSUPPORTED: "unsupported", + INTERNAL_ERROR: "internal_error", +}) + +/** + * Retry hints expressed at the contract layer. These are not policies — the + * worker still owns the final decision — but they tell the worker what kind + * of failure it just saw. + * + * safe_retry → idempotent, retry without side effects + * recover_then_retry → re-acquire context (session, frame, tab) first + * abort → never retry, escalate to a higher policy + */ +export const RETRY_HINTS = Object.freeze({ + [ERROR_CODES.TRANSPORT_ERROR]: "safe_retry", + [ERROR_CODES.RPC_INVALID]: "abort", + [ERROR_CODES.UNKNOWN_METHOD]: "abort", + [ERROR_CODES.RATE_LIMITED]: "safe_retry", + [ERROR_CODES.TIMEOUT]: "safe_retry", + + [ERROR_CODES.TARGET_GONE]: "recover_then_retry", + [ERROR_CODES.NAVIGATION_ABORTED]: "recover_then_retry", + [ERROR_CODES.NAVIGATION_TIMEOUT]: "safe_retry", + [ERROR_CODES.CDP_FAILED]: "recover_then_retry", + [ERROR_CODES.FRAME_DETACHED]: "recover_then_retry", + + [ERROR_CODES.ELEMENT_NOT_FOUND]: "abort", + [ERROR_CODES.ELEMENT_NOT_ACTIONABLE]: "safe_retry", + [ERROR_CODES.POSTCONDITION_FAILED]: "abort", + [ERROR_CODES.DUPLICATE_ACTION]: "abort", + + [ERROR_CODES.SESSION_INVALID]: "recover_then_retry", + [ERROR_CODES.SESSION_STALE]: "recover_then_retry", + [ERROR_CODES.SESSION_LOCKED]: "abort", + [ERROR_CODES.ORIGIN_NOT_PERMITTED]: "abort", + + [ERROR_CODES.ANTI_BOT_CHALLENGE]: "recover_then_retry", + [ERROR_CODES.CAPTCHA_REQUIRED]: "abort", + [ERROR_CODES.RATE_LIMIT_REMOTE]: "safe_retry", + + [ERROR_CODES.PRECONDITION_FAILED]: "abort", + [ERROR_CODES.UNSUPPORTED]: "abort", + [ERROR_CODES.INTERNAL_ERROR]: "abort", +}) + +/** + * Translation table from internal extension BridgeError codes (see + * `extension/src/core/errors.js`) to the public contract codes. The bridge + * error taxonomy was grown organically; this table is the single mapping + * point so downstream callers always see a stable contract code. + */ +export const INTERNAL_TO_CONTRACT = Object.freeze({ + TRANSPORT_ERROR: ERROR_CODES.TRANSPORT_ERROR, + TIMEOUT: ERROR_CODES.TIMEOUT, + RATE_LIMITED: ERROR_CODES.RATE_LIMITED, + UNKNOWN_TOOL: ERROR_CODES.UNKNOWN_METHOD, + METHOD_NOT_FOUND: ERROR_CODES.UNKNOWN_METHOD, + INVALID_INPUT: ERROR_CODES.RPC_INVALID, + INVALID_ARGS: ERROR_CODES.RPC_INVALID, + PRECONDITION_FAILED: ERROR_CODES.PRECONDITION_FAILED, + TAB_GONE: ERROR_CODES.TARGET_GONE, + CDP_FAILED: ERROR_CODES.CDP_FAILED, + NAVIGATION_FAILED: ERROR_CODES.NAVIGATION_ABORTED, + NETWORK: ERROR_CODES.TRANSPORT_ERROR, + NOT_FOUND: ERROR_CODES.ELEMENT_NOT_FOUND, + UNAUTHORIZED: ERROR_CODES.SESSION_INVALID, + FORBIDDEN: ERROR_CODES.ORIGIN_NOT_PERMITTED, + UNSUPPORTED: ERROR_CODES.UNSUPPORTED, + NATIVE_HOST_UNAVAILABLE: ERROR_CODES.TRANSPORT_ERROR, + OFFSCREEN_ERROR: ERROR_CODES.INTERNAL_ERROR, + OFFSCREEN_UNAVAILABLE: ERROR_CODES.UNSUPPORTED, + VISION_UNAVAILABLE: ERROR_CODES.UNSUPPORTED, + INTERNAL_ERROR: ERROR_CODES.INTERNAL_ERROR, +}) + +/** + * Public namespaces the bridge advertises. Each entry declares whether its + * methods are read-only (always idempotent) or include mutating operations + * (need the per-method `idempotent` flag). + */ +export const NAMESPACES = Object.freeze([ + { name: "bridge", category: "meta", description: "Contract, capabilities, evidence." }, + { name: "system", category: "meta", description: "Health, version, capabilities." }, + { name: "tabs", category: "browser", description: "Tab lifecycle." }, + { name: "nav", category: "browser", description: "Navigation primitives." }, + { name: "dom", category: "browser", description: "DOM read + interaction." }, + { name: "cookies", category: "state", description: "Cookie jar." }, + { name: "storage", category: "state", description: "Web storage." }, + { name: "net", category: "browser", description: "Network observation + control." }, + { name: "session", category: "state", description: "Session manifest, restore, invalidate." }, + { name: "behavior", category: "evidence", description: "Behavior timeline." }, + { name: "stealth", category: "browser", description: "Stealth posture + assessment." }, +]) + +/** + * Per-method declarations. Only methods that are part of the v1 contract + * appear here; legacy aliases stay outside the contract intentionally. + * + * Schema (per entry): + * name string — fully qualified tool name (`namespace.method`) + * idempotent boolean — true when retrying with same params is safe + * mutates boolean — true when the method changes browser state + * description string — short human description + * params object — JSON-schema-ish hint (optional fields shown as `?`) + * returns object — return shape hint + * raises array — contract error codes the method may raise + */ +export const METHODS = Object.freeze([ + // ----- bridge meta -------------------------------------------------------- + m("bridge.contract", true, false, "Return the active bridge contract.", {}, { contract: "object" }, []), + m("bridge.evidenceBundle", true, false, + "Assemble a forensic evidence bundle (screenshot, DOM snapshot, console, network, command history).", + { tabId: "?number", traceId: "?string", includeScreenshot: "?boolean", maxNetworkEvents: "?number" }, + { bundle: "object" }, + [ERROR_CODES.TARGET_GONE, ERROR_CODES.CDP_FAILED, ERROR_CODES.INTERNAL_ERROR], + ), + + // ----- system ------------------------------------------------------------- + m("system.health", true, false, "Bridge health snapshot.", {}, { ok: "boolean" }, []), + m("system.version", true, false, "Bridge version + protocol info.", {}, { version: "string" }, []), + m("system.capabilities", true, false, "Discovery: list of registered tools.", {}, { tools: "array" }, []), + m("system.ping", true, false, "Liveness probe.", {}, { pong: "boolean" }, []), + + // ----- tabs --------------------------------------------------------------- + m("tabs.list", true, false, "List open tabs.", {}, { tabs: "array" }, []), + m("tabs.get", true, false, "Get a single tab by id.", { tabId: "number" }, { tab: "object" }, [ERROR_CODES.TARGET_GONE]), + m("tabs.create", false, true, "Create a new tab.", { url: "string", active: "?boolean" }, { tab: "object" }, [ERROR_CODES.RPC_INVALID]), + m("tabs.close", false, true, "Close one or more tabs.", { tabIds: "array" }, { ok: "boolean" }, []), + m("tabs.activate", true, true, "Activate a tab. Idempotent: same tabId twice is harmless.", { tabId: "number" }, { ok: "boolean" }, [ERROR_CODES.TARGET_GONE]), + m("tabs.reload", false, true, "Reload tab.", { tabId: "?number" }, { ok: "boolean" }, [ERROR_CODES.TARGET_GONE]), + + // ----- navigation --------------------------------------------------------- + m("nav.goto", false, true, "Navigate the active tab. Not idempotent: SPAs may build different state per navigation.", { url: "string", tabId: "?number", waitUntil: "?string", timeoutMs: "?number" }, { url: "string" }, [ERROR_CODES.NAVIGATION_ABORTED, ERROR_CODES.NAVIGATION_TIMEOUT, ERROR_CODES.RPC_INVALID]), + m("nav.back", false, true, "Go back in history.", { tabId: "?number" }, { ok: "boolean" }, [ERROR_CODES.TARGET_GONE]), + m("nav.forward", false, true, "Go forward in history.", { tabId: "?number" }, { ok: "boolean" }, [ERROR_CODES.TARGET_GONE]), + m("nav.reload", false, true, "Reload tab.", { tabId: "?number" }, { ok: "boolean" }, [ERROR_CODES.TARGET_GONE]), + m("nav.waitForSelector", true, false, "Wait until a selector resolves.", { selector: "string", state: "?string", timeoutMs: "?number" }, { ok: "boolean" }, [ERROR_CODES.TIMEOUT, ERROR_CODES.ELEMENT_NOT_FOUND]), + + // ----- dom (read-only is idempotent, mutating is NOT) --------------------- + m("dom.snapshot", true, false, "Accessibility snapshot of the page.", { mode: "?string", maxNodes: "?number" }, { snapshot: "object" }, [ERROR_CODES.TARGET_GONE]), + m("dom.fullSnapshot", true, false, "Full observation snapshot with screenshot.", { maxNodes: "?number" }, { snapshot: "object" }, [ERROR_CODES.TARGET_GONE, ERROR_CODES.CDP_FAILED]), + m("dom.click", false, true, "Click a referenced or selected element.", { selector: "?string", ref: "?string" }, { ok: "boolean", proofId: "?string" }, [ERROR_CODES.ELEMENT_NOT_FOUND, ERROR_CODES.ELEMENT_NOT_ACTIONABLE, ERROR_CODES.POSTCONDITION_FAILED, ERROR_CODES.DUPLICATE_ACTION]), + m("dom.type", false, true, "Type text into an element.", { selector: "?string", ref: "?string", text: "string" }, { ok: "boolean" }, [ERROR_CODES.ELEMENT_NOT_FOUND, ERROR_CODES.ELEMENT_NOT_ACTIONABLE]), + m("dom.fill", false, true, "Fill many fields at once.", { fields: "object" }, { ok: "boolean" }, [ERROR_CODES.ELEMENT_NOT_FOUND]), + m("dom.select", false, true, "Select option in a