From 7678c80b41e6f3b6a0d41e6b837af0bb451c0fdb Mon Sep 17 00:00:00 2001 From: SarthakWade Date: Tue, 22 Sep 2026 16:02:27 +0530 Subject: [PATCH 1/3] feat: add semantic hover command --- .agents/skills/headless-computer-use/SKILL.md | 3 + .../references/commands.md | 6 +- CHANGELOG.md | 7 +- README.md | 6 +- apps/headless-rs/src/protocol.rs | 4 +- apps/headless-rs/tests/protocol_tests.rs | 16 ++ apps/headless/Host/AgentBridge.swift | 8 + apps/headless/LinuxHost/BrowserProcess.swift | 12 + apps/headless/LinuxHost/main.swift | 3 + .../Sources/HeadlessProtocol/CLI.swift | 3 + .../HeadlessProtocol/Capabilities.swift | 4 + .../Sources/HeadlessProtocol/HostCore.swift | 6 +- .../Sources/HeadlessProtocol/Protocol.swift | 3 +- .../HeadlessProtocol/ProtocolSchema.swift | 9 + .../Resources/AgentRuntime.js | 65 +++++- .../Tests/Fixtures/trusted-input.html | 14 ++ .../HeadlessProtocolTests/ProtocolTests.swift | 66 ++++++ apps/headless/Tests/agent-runtime.test.mjs | 52 +++++ apps/headless/Tests/conformance.sh | 1 + apps/headless/Tests/fixture-server.mjs | 1 + apps/headless/Tests/linux-e2e.sh | 10 + apps/headless/Tests/macos-e2e.sh | 12 + apps/headless/docs/COMMANDS.md | 12 +- apps/headless/docs/P2.md | 10 + docs/roadmap/architecture-decisions.md | 37 +++ docs/roadmap/improvements-backlog.md | 5 +- .../headless-npm/scripts/generate-sdk.mjs | 51 +++- packages/headless-npm/src/generated.ts | 218 +++++++++++++++--- packages/headless-npm/src/protocol.ts | 29 ++- packages/headless-npm/test/protocol.test.mjs | 10 +- packages/headless-python/scripts/generate.py | 42 +++- .../src/headless_sdk/_protocol.py | 22 ++ .../src/headless_sdk/generated.py | 211 ++++++++++++++++- .../headless-python/tests/test_protocol.py | 10 +- .../headless-python/tests/test_transport.py | 4 +- .../headless-python/tests/type_contract.py | 6 + sdk/protocol-fixtures.json | 16 ++ sdk/protocol-schema.json | 2 +- 38 files changed, 934 insertions(+), 62 deletions(-) diff --git a/.agents/skills/headless-computer-use/SKILL.md b/.agents/skills/headless-computer-use/SKILL.md index f6935a7..4ad7117 100644 --- a/.agents/skills/headless-computer-use/SKILL.md +++ b/.agents/skills/headless-computer-use/SKILL.md @@ -32,6 +32,8 @@ Chromium capabilities; macOS WebKit reports them as unsupported. 4. Start with `inspect --context summary --task "..."`, then use an outline, scoped text, or scoped actions only when the task needs them. 5. Prefer role/name targeting; otherwise use a ref from the latest inspection. + Use `hover` when the task requires a tooltip or other hover-only state; it + does not click or focus the target. Inspect again after the state changes. For native dropdowns, use `select` with exactly one exact `--label` or `--value`; do not treat custom ARIA widgets as native selects. 6. After navigation or a substantial rerender, wait for the expected URL, text, @@ -49,6 +51,7 @@ headless session create agent-qa headless --session agent-qa visit http://localhost:3000 headless --session agent-qa inspect --context summary --task "click Continue" headless --session agent-qa inspect --context actions --task "click Continue" --limit 10 +headless --session agent-qa hover --role button --name Continue headless --session agent-qa click --role button --name Continue headless --session agent-qa wait --url /next --settled --timeout 10000 headless --session agent-qa inspect --context actions --task "verify next page" diff --git a/.agents/skills/headless-computer-use/references/commands.md b/.agents/skills/headless-computer-use/references/commands.md index 7bb75a8..2bffb03 100644 --- a/.agents/skills/headless-computer-use/references/commands.md +++ b/.agents/skills/headless-computer-use/references/commands.md @@ -28,6 +28,8 @@ headless --session NAME inspect --context actions --task "TASK" headless --session NAME inspect --context full --text headless --session NAME click REF headless --session NAME click --role ROLE --name NAME +headless --session NAME hover REF +headless --session NAME hover --role ROLE --name NAME headless --session NAME fill REF "TEXT" headless --session NAME fill REF -- "--json stays literal" headless --session NAME select REF --label LABEL @@ -48,7 +50,9 @@ large pages, request `outline`, select a returned `@rN` region, then use bound the result; check `omitted` before assuming it describes the whole page. Use `click --role ... --name ...` for unique accessible controls. Use an `@eN` ref from the latest inspection when role/name is ambiguous. Inspect again after -navigation or a large rerender. Native single-select controls advertise +navigation or a large rerender. Use `hover` only for hover-dependent state such +as tooltips; it does not focus or click, and its output never includes internal +coordinates. Native single-select controls advertise `select`; use exactly one exact `--label` or `--value`. File inputs advertise `upload` for an existing private artifact-store basename. Upload never accepts or imports a filesystem path. Ask before uploading, as in [safety.md](safety.md). diff --git a/CHANGELOG.md b/CHANGELOG.md index eaf9922..7e53d10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,8 @@ Two versions travel independently, on purpose: `HEADLESS_VERSION`, reported by the CLI, host, and MCP adapter, and used for release assets. - **Protocol version**: `headlessProtocolVersion` in `Protocol.swift`, - currently `0.5`. It changes only when the wire contract changes, and always - with an entry in + currently `0.5`. It changes only for incompatible wire-contract changes, + always with an entry in [`docs/roadmap/architecture-decisions.md`](docs/roadmap/architecture-decisions.md). ## [Unreleased] @@ -24,6 +24,9 @@ Two versions travel independently, on purpose: ### Added +- Semantic `hover` targets a fresh element ref or an exact role/name without + exposing coordinates. Chromium dispatches a trusted CDP mouse move, while + WebKit reports its fixed synthetic hover path in capabilities. - macOS Command-, opens one Settings window built from the typed registry and writing through the same backend as `headless config`. Linux has no Settings GUI. diff --git a/README.md b/README.md index 8b1eebb..81eac44 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ headless --session qa inspect --context outline --limit 20 headless --session qa inspect --context actions --task "click Continue" headless --session qa record start --fps 10 headless --session qa tour --full-page +headless --session qa hover --role button --name Continue headless --session qa click --role button --name Continue headless --session qa wait --url /next --settled headless --session qa record stop --output dashboard-flow.mp4 @@ -128,7 +129,10 @@ structural region references such as `@r4`, then inspect only that region with explicit broad-page escape hatch. Each control's `actions` list contains only protocol verbs that can run (`click`, -`fill`, or `upload`); unsupported controls never advertise nonexistent commands. Full +`fill`, `select`, or `upload`); unsupported controls never advertise nonexistent +commands. Hover is available for any visible semantic target but is not +advertised because a page does not expose whether hovering has meaningful +behavior. Full inspection retains roles, names, rendered media metadata, safety markers, bounds, and element references such as `@e1`. `capture-info` returns the browser surface, page state, action trace, and diff --git a/apps/headless-rs/src/protocol.rs b/apps/headless-rs/src/protocol.rs index 9f83d6a..18cd9c8 100644 --- a/apps/headless-rs/src/protocol.rs +++ b/apps/headless-rs/src/protocol.rs @@ -26,6 +26,7 @@ pub enum CommandName { Visit, Inspect, Click, + Hover, Fill, Press, Scroll, @@ -93,6 +94,7 @@ impl CommandName { CommandName::Visit => "visit", CommandName::Inspect => "inspect", CommandName::Click => "click", + CommandName::Hover => "hover", CommandName::Fill => "fill", CommandName::Press => "press", CommandName::Scroll => "scroll", @@ -350,7 +352,7 @@ impl CommandRequest { } Ok(()) } - Click => self.target(false, true, true), + Click | Hover => self.target(false, true, true), Fill => self.target(true, true, true), Press => { self.allow(&["key"])?; diff --git a/apps/headless-rs/tests/protocol_tests.rs b/apps/headless-rs/tests/protocol_tests.rs index 2b1e93c..1cf8707 100644 --- a/apps/headless-rs/tests/protocol_tests.rs +++ b/apps/headless-rs/tests/protocol_tests.rs @@ -165,6 +165,22 @@ fn command_parameter_validation() { let request = valid_request(CommandName::Click, params(&[("target", json!("@food"))])); assert!(request.validate().is_err()); + // hover follows the same exclusive target contract as click + let request = valid_request(CommandName::Hover, params(&[("target", json!("@e8"))])); + request.validate().unwrap(); + let request = valid_request( + CommandName::Hover, + params(&[("role", json!("button")), ("name", json!("Account"))]), + ); + request.validate().unwrap(); + let request = valid_request(CommandName::Hover, params(&[])); + assert!(request.validate().is_err()); + let request = valid_request( + CommandName::Hover, + params(&[("target", json!("@e8")), ("name", json!("Account"))]), + ); + assert!(request.validate().is_err()); + // fill requires a value let request = valid_request(CommandName::Fill, params(&[("target", json!("@e12"))])); assert!(request.validate().is_err()); diff --git a/apps/headless/Host/AgentBridge.swift b/apps/headless/Host/AgentBridge.swift index bbbde02..1e6f4e0 100644 --- a/apps/headless/Host/AgentBridge.swift +++ b/apps/headless/Host/AgentBridge.swift @@ -57,6 +57,11 @@ extension BrowserWindowController { return try callAgent("return globalThis.__headlessAgent.click(args);", arguments: ["args": args]) } + func agentHover(parameters: [String: JSONValue]) throws -> JSONValue { + let args = try browserTargetArguments(parameters) + return try callAgent("return globalThis.__headlessAgent.hover(args);", arguments: ["args": args]) + } + func agentFill(parameters: [String: JSONValue]) throws -> JSONValue { var args = try browserTargetArguments(parameters) guard let value = parameters["value"]?.stringValue else { @@ -644,6 +649,9 @@ extension BrowserWindowController: BrowserEngineSession { func hostClick(parameters: [String: JSONValue]) throws -> JSONValue { try agentClick(parameters: parameters) } + func hostHover(parameters: [String: JSONValue]) throws -> JSONValue { + try agentHover(parameters: parameters) + } func hostFill(parameters: [String: JSONValue]) throws -> JSONValue { try agentFill(parameters: parameters) } diff --git a/apps/headless/LinuxHost/BrowserProcess.swift b/apps/headless/LinuxHost/BrowserProcess.swift index 8888489..b719409 100644 --- a/apps/headless/LinuxHost/BrowserProcess.swift +++ b/apps/headless/LinuxHost/BrowserProcess.swift @@ -773,6 +773,18 @@ final class LinuxBrowserSession: @unchecked Sendable { ]) } + func hover(parameters: [String: JSONValue]) throws -> JSONValue { + let target = try trustedInputTarget(parameters: parameters, action: "hover") + _ = try command("Input.dispatchMouseEvent", parameters: [ + "type": "mouseMoved", "x": target.x, "y": target.y, + ]) + return .object([ + "hovered": .string(target.reference), + "role": .string(target.role), + "name": .string(target.name), + ]) + } + func upload(parameters: [String: JSONValue], artifactURL: URL) throws -> JSONValue { let args = try browserTargetArguments(parameters) let objectId = try evaluateNode( diff --git a/apps/headless/LinuxHost/main.swift b/apps/headless/LinuxHost/main.swift index 3168c83..ce12d9d 100644 --- a/apps/headless/LinuxHost/main.swift +++ b/apps/headless/LinuxHost/main.swift @@ -88,6 +88,9 @@ final class ChromiumBrowserEngineSession: BrowserEngineSession { func hostClick(parameters: [String: JSONValue]) throws -> JSONValue { try browserSession.click(parameters: parameters) } + func hostHover(parameters: [String: JSONValue]) throws -> JSONValue { + try browserSession.hover(parameters: parameters) + } func hostFill(parameters: [String: JSONValue]) throws -> JSONValue { try browserSession.fill(parameters: parameters) } diff --git a/apps/headless/Sources/HeadlessProtocol/CLI.swift b/apps/headless/Sources/HeadlessProtocol/CLI.swift index 9bd481d..cb9849b 100644 --- a/apps/headless/Sources/HeadlessProtocol/CLI.swift +++ b/apps/headless/Sources/HeadlessProtocol/CLI.swift @@ -151,6 +151,8 @@ public struct CLIParser { return try parseInspect(arguments, session: session, jsonOutput: jsonOutput) case "click": return try parseTargeted(.click, arguments: arguments, session: session, jsonOutput: jsonOutput) + case "hover": + return try parseTargeted(.hover, arguments: arguments, session: session, jsonOutput: jsonOutput) case "upload": return try parseUpload(arguments, session: session, jsonOutput: jsonOutput) case "fill": @@ -884,6 +886,7 @@ Commands: inspect [--context summary|outline|text|actions|full] [--task TEXT] [--within @rN] [--limit N] [--budget TOKENS] [--depth N] [--text] click REF | click --role ROLE [--name NAME] + hover REF | hover --role ROLE [--name NAME] fill REF TEXT | fill REF -- TEXT_WITH_LITERAL_FLAGS | press KEY select REF --label LABEL | select REF --value VALUE select --role ROLE [--name NAME] (--label LABEL | --value VALUE) diff --git a/apps/headless/Sources/HeadlessProtocol/Capabilities.swift b/apps/headless/Sources/HeadlessProtocol/Capabilities.swift index c1a8169..6616e51 100644 --- a/apps/headless/Sources/HeadlessProtocol/Capabilities.swift +++ b/apps/headless/Sources/HeadlessProtocol/Capabilities.swift @@ -24,6 +24,7 @@ public struct BrowserEngineCapabilities: Sendable { public let qaDiagnosticSynchronization: String public let screenshotClipboard: Bool public let inputDispatch: String + public let hoverDispatch: String public let selectDispatch: String public let networkIdleWait: Bool public let normalProfileStorage: String @@ -70,6 +71,7 @@ public struct BrowserEngineCapabilities: Sendable { "screenshotClipboard": .bool(screenshotClipboard), "tourTimeoutMs": .number(65_000), "inputDispatch": .string(inputDispatch), + "hoverDispatch": .string(hoverDispatch), "selectDispatch": .string(selectDispatch), "networkIdleWait": .bool(networkIdleWait), "fileUpload": .bool(fileUpload), @@ -117,6 +119,7 @@ public struct BrowserEngineCapabilities: Sendable { qaDiagnosticSynchronization: "best-effort-page-world-observer", screenshotClipboard: true, inputDispatch: "synthetic-dom", + hoverDispatch: "synthetic-dom", selectDispatch: "synthetic-dom", networkIdleWait: false, normalProfileStorage: "persistent-wkwebsite-data-store", @@ -142,6 +145,7 @@ public struct BrowserEngineCapabilities: Sendable { qaDiagnosticSynchronization: "runtime-round-trip-flush", screenshotClipboard: false, inputDispatch: "trusted-cdp", + hoverDispatch: "trusted-cdp", selectDispatch: "synthetic-dom", networkIdleWait: true, normalProfileStorage: "private-xdg-data-directory", diff --git a/apps/headless/Sources/HeadlessProtocol/HostCore.swift b/apps/headless/Sources/HeadlessProtocol/HostCore.swift index cb45172..3f33dc1 100644 --- a/apps/headless/Sources/HeadlessProtocol/HostCore.swift +++ b/apps/headless/Sources/HeadlessProtocol/HostCore.swift @@ -36,6 +36,7 @@ public protocol BrowserEngineSession: AnyObject { func hostVisit(_ url: URL) throws -> JSONValue func hostInspect(parameters: [String: JSONValue]) throws -> JSONValue func hostClick(parameters: [String: JSONValue]) throws -> JSONValue + func hostHover(parameters: [String: JSONValue]) throws -> JSONValue func hostFill(parameters: [String: JSONValue]) throws -> JSONValue func hostSelect(parameters: [String: JSONValue]) throws -> JSONValue func hostPress(parameters: [String: JSONValue]) throws -> JSONValue @@ -558,6 +559,7 @@ public final class HostCore: @unchecked Sendable { return try session.hostVisit(url) case .inspect: return try session.hostInspect(parameters: request.parameters) case .click: return try session.hostClick(parameters: request.parameters) + case .hover: return try session.hostHover(parameters: request.parameters) case .fill: return try session.hostFill(parameters: request.parameters) case .select: return try session.hostSelect(parameters: request.parameters) case .upload: @@ -675,7 +677,9 @@ public final class HostCore: @unchecked Sendable { after command: CommandName, request: CommandRequest, sessionName: String, session: Engine.Session, result: JSONValue ) throws -> CommandResponse? { - guard [.visit, .inspect, .click, .wait, .back, .reload].contains(command) else { return nil } + guard [.visit, .inspect, .click, .hover, .wait, .back, .reload].contains(command) else { + return nil + } let form = try AuthenticationForm(session.hostAuthenticationState()) guard form.detection != .none else { authenticationChallenges.invalidate(session: sessionName) diff --git a/apps/headless/Sources/HeadlessProtocol/Protocol.swift b/apps/headless/Sources/HeadlessProtocol/Protocol.swift index ef08ce0..ea6c316 100644 --- a/apps/headless/Sources/HeadlessProtocol/Protocol.swift +++ b/apps/headless/Sources/HeadlessProtocol/Protocol.swift @@ -63,6 +63,7 @@ public enum CommandName: String, Codable, CaseIterable, Sendable { case visit case inspect case click + case hover case fill case select case upload @@ -262,7 +263,7 @@ public struct CommandRequest: Codable, Equatable, Sendable { throw ProtocolValidationError.invalidParameter("Invalid integer parameter: \(key)") } } - case .click: + case .click, .hover: try target(allowValue: false) case .fill: try target(allowValue: true) diff --git a/apps/headless/Sources/HeadlessProtocol/ProtocolSchema.swift b/apps/headless/Sources/HeadlessProtocol/ProtocolSchema.swift index 4901531..adee982 100644 --- a/apps/headless/Sources/HeadlessProtocol/ProtocolSchema.swift +++ b/apps/headless/Sources/HeadlessProtocol/ProtocolSchema.swift @@ -318,6 +318,11 @@ public func protocolResultDefinition(for command: CommandName) -> ProtocolResult resultField("clicked", .string), resultField("role", .string), resultField("name", .string), ]) + case .hover: + return result("Hover", [ + resultField("hovered", .string), resultField("role", .string), + resultField("name", .string), + ]) case .fill: return result("Fill", [resultField("filled", .string), resultField("valueLength", .number)]) case .select: @@ -705,6 +710,10 @@ public let protocolCommandDefinitions: [CommandName: ProtocolCommandDefinition] .click, targetParameters, untrusted: true, constraints: ["exactly one target reference or semantic role/name target"] ), + command( + .hover, targetParameters, untrusted: true, + constraints: ["exactly one target reference or semantic role/name target"] + ), command( .fill, targetParameters + [ diff --git a/apps/headless/Sources/HeadlessProtocol/Resources/AgentRuntime.js b/apps/headless/Sources/HeadlessProtocol/Resources/AgentRuntime.js index 1b33eb4..a7b72b4 100644 --- a/apps/headless/Sources/HeadlessProtocol/Resources/AgentRuntime.js +++ b/apps/headless/Sources/HeadlessProtocol/Resources/AgentRuntime.js @@ -8,6 +8,7 @@ if (!globalThis.__headlessAgent) { const regionRefs = new WeakMap(); let current = new Map(); let currentRegions = new Map(); + let hoveredElement = null; // Every reference ever handed out, so a failed lookup can say whether the // reference expired or was never issued at all. const issuedRefs = new Set(); @@ -706,6 +707,53 @@ if (!globalThis.__headlessAgent) { element.click(); return {clicked: refFor(element), role: role(element), name: name(element)}; }; + const hover = args => { + const element = target(args); + element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); + if (!visible(element)) fail('ELEMENT_NOT_FOUND', 'ELEMENT_NOT_VISIBLE'); + const rect = element.getBoundingClientRect(); + const left = Math.max(0, rect.left); + const right = Math.min(innerWidth, rect.right); + const top = Math.max(0, rect.top); + const bottom = Math.min(innerHeight, rect.bottom); + if (right <= left || bottom <= top) fail('ELEMENT_NOT_FOUND', 'ELEMENT_NOT_VISIBLE'); + const x = left + (right - left) / 2; + const y = top + (bottom - top) / 2; + const hit = document.elementFromPoint(x, y); + if (!hit || (hit !== element && !element.contains(hit))) { + fail('ELEMENT_NOT_FOUND', 'ELEMENT_OBSCURED'); + } + const Pointer = typeof PointerEvent === 'function' ? PointerEvent : MouseEvent; + const pointer = {bubbles: true, clientX: x, clientY: y, pointerId: 1, + pointerType: 'mouse', isPrimary: true}; + const mouse = {bubbles: true, clientX: x, clientY: y}; + const previous = hoveredElement instanceof Element && hoveredElement.isConnected + ? hoveredElement : null; + hoveredElement = element; + if (previous !== element) { + if (previous) { + previous.dispatchEvent(new Pointer('pointerout', {...pointer, relatedTarget: element})); + previous.dispatchEvent(new Pointer('pointerleave', { + ...pointer, bubbles: false, relatedTarget: element + })); + previous.dispatchEvent(new MouseEvent('mouseout', {...mouse, relatedTarget: element})); + previous.dispatchEvent(new MouseEvent('mouseleave', { + ...mouse, bubbles: false, relatedTarget: element + })); + } + element.dispatchEvent(new Pointer('pointerover', {...pointer, relatedTarget: previous})); + element.dispatchEvent(new Pointer('pointerenter', { + ...pointer, bubbles: false, relatedTarget: previous + })); + element.dispatchEvent(new MouseEvent('mouseover', {...mouse, relatedTarget: previous})); + element.dispatchEvent(new MouseEvent('mouseenter', { + ...mouse, bubbles: false, relatedTarget: previous + })); + } + element.dispatchEvent(new Pointer('pointermove', pointer)); + element.dispatchEvent(new MouseEvent('mousemove', mouse)); + return {hovered: refFor(element), role: role(element), name: name(element)}; + }; // Chromium uses this isolated-world resolver only to select and validate a // target. The host performs the action through CDP's trusted input domain. // Page-derived coordinates remain bounded to the visible viewport. @@ -717,17 +765,26 @@ if (!globalThis.__headlessAgent) { if (!editable || element.disabled || element.readOnly) throw new Error('NOT_EDITABLE'); } element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); - element.focus({preventScroll: true}); + if (action !== 'hover') element.focus({preventScroll: true}); + if (action === 'hover' && !visible(element)) { + fail('ELEMENT_NOT_FOUND', 'ELEMENT_NOT_VISIBLE'); + } const rect = element.getBoundingClientRect(); const left = Math.max(0, rect.left); const right = Math.min(innerWidth, rect.right); const top = Math.max(0, rect.top); const bottom = Math.min(innerHeight, rect.bottom); - if (right <= left || bottom <= top) throw new Error('ELEMENT_NOT_VISIBLE'); + if (right <= left || bottom <= top) { + if (action === 'hover') fail('ELEMENT_NOT_FOUND', 'ELEMENT_NOT_VISIBLE'); + throw new Error('ELEMENT_NOT_VISIBLE'); + } const x = left + (right - left) / 2; const y = top + (bottom - top) / 2; const hit = document.elementFromPoint(x, y); - if (!hit || (hit !== element && !element.contains(hit))) throw new Error('ELEMENT_OBSCURED'); + if (!hit || (hit !== element && !element.contains(hit))) { + if (action === 'hover') fail('ELEMENT_NOT_FOUND', 'ELEMENT_OBSCURED'); + throw new Error('ELEMENT_OBSCURED'); + } return {ref: refFor(element), role: role(element), name: name(element), x, y}; }; const checkedFileInput = element => { @@ -1199,7 +1256,7 @@ if (!globalThis.__headlessAgent) { return {count: document.getAnimations().length, animations: all, truncated: document.getAnimations().length > all.length}; }; return { - snapshot, click, fill, select, credentialFill, finishCredentialFill, press, inputTarget, fileInput, fileInputMetadata, + snapshot, click, hover, fill, select, credentialFill, finishCredentialFill, press, inputTarget, fileInput, fileInputMetadata, authentication, scroll, state, tour, screenshotPlan, regionSlice, scrollToCapturePoint, rectangle, styles, storage, performance: performanceSummary, animations }; diff --git a/apps/headless/Tests/Fixtures/trusted-input.html b/apps/headless/Tests/Fixtures/trusted-input.html index 2485eb9..25504bd 100644 --- a/apps/headless/Tests/Fixtures/trusted-input.html +++ b/apps/headless/Tests/Fixtures/trusted-input.html @@ -10,6 +10,13 @@

Trusted input fixture

+ + + waiting diff --git a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift index a529837..ac135a6 100644 --- a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift +++ b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift @@ -338,6 +338,11 @@ private final class TestBrowserSession: BrowserEngineSession { .object(["engineResult": .bool(true), "parameters": .object(parameters)]) } func hostClick(parameters: [String: JSONValue]) throws -> JSONValue { .object(["clicked": .bool(true)]) } + private(set) var lastHoverParameters: [String: JSONValue]? + func hostHover(parameters: [String: JSONValue]) throws -> JSONValue { + lastHoverParameters = parameters + return .object(["hovered": .bool(true), "parameters": .object(parameters)]) + } func hostFill(parameters: [String: JSONValue]) throws -> JSONValue { .object(["filled": .bool(true)]) } private(set) var lastSelectParameters: [String: JSONValue]? func hostSelect(parameters: [String: JSONValue]) throws -> JSONValue { @@ -994,6 +999,19 @@ struct ProtocolTests { parameters: ["target": .string("@e../1")] ).validate() } + try CommandRequest( + id: "valid-hover", command: .hover, + parameters: ["role": .string("button"), "name": .string("Details")] + ).validate() + try expectThrows("hover should require one semantic target") { + try CommandRequest(id: "missing-hover-target", command: .hover).validate() + } + try expectThrows("hover should reject region references") { + try CommandRequest( + id: "hover-region", command: .hover, + parameters: ["target": .string("@r1")] + ).validate() + } try CommandRequest( id: "valid-select", command: .select, parameters: ["target": .string("@e1"), "label": .string("Canada")] @@ -1238,6 +1256,29 @@ struct ProtocolTests { ) } + static func cliSemanticHover() throws { + let semantic = try CLIParser().parse([ + "hover", "--role", "button", "--name", "Continue", + ]) + try expect(semantic.request?.command == .hover, "hover command should parse") + try expect( + semantic.request?.parameters == [ + "role": .string("button"), "name": .string("Continue"), + ], + "semantic hover target should parse" + ) + let reference = try CLIParser().parse(["hover", "@e4"]) + try expect( + reference.request?.parameters == ["target": .string("@e4")], + "ref hover target should parse" + ) + try expectThrows("hover should reject conflicting target forms") { + _ = try CLIParser().parse([ + "hover", "@e4", "--role", "button", "--name", "Continue", + ]) + } + } + static func cliSemanticSelect() throws { let byLabel = try CLIParser().parse([ "select", "--role", "combobox", "--name", "Country", @@ -2682,6 +2723,10 @@ struct ProtocolTests { parameters: ["target": .string("@e1"), "value": .string(secret)] ) try expect(fill == nil, "fill values must never become replayable flow steps") + try expect( + flowStepIfSafe(command: .hover, parameters: safeParameters) == nil, + "hover must not become a replayable flow step" + ) let select = flowStepIfSafe( command: .select, parameters: ["target": .string("@e1"), "value": .string(secret)] @@ -3041,6 +3086,14 @@ struct ProtocolTests { chromiumFeatures["inputDispatch"] == .string("trusted-cdp"), "Chromium should declare trusted CDP input" ) + try expect( + webkitFeatures["hoverDispatch"] == .string("synthetic-dom"), + "WebKit should declare synthetic hover dispatch" + ) + try expect( + chromiumFeatures["hoverDispatch"] == .string("trusted-cdp"), + "Chromium should declare trusted CDP hover dispatch" + ) try expect( webkitFeatures["selectDispatch"] == .string("synthetic-dom") && chromiumFeatures["selectDispatch"] == .string("synthetic-dom"), @@ -5066,6 +5119,18 @@ struct ProtocolTests { "agent control should be enabled at creation and before command execution" ) + let hoverParameters: [String: JSONValue] = [ + "role": .string("button"), "name": .string("Details"), + ] + let hovered = core.handle(CommandRequest( + command: .hover, session: "secondary", parameters: hoverParameters + )) + try expect(hovered.ok, "shared host hover dispatch should succeed") + try expect( + engine.createdSessions[1].lastHoverParameters == hoverParameters, + "HostCore should pass validated hover parameters to the engine" + ) + let selectParameters: [String: JSONValue] = [ "role": .string("combobox"), "name": .string("Country"), "label": .string("Canada"), @@ -5281,6 +5346,7 @@ struct ProtocolTests { ("CLI visit", cliVisit), ("CLI fill literal value", cliFillPreservesLiteralValue), ("CLI semantic click", cliSemanticClick), + ("CLI semantic hover", cliSemanticHover), ("CLI semantic select", cliSemanticSelect), ("CLI inspect context and task", cliInspectContextAndTask), ("CLI conflicting target", cliRejectsConflictingClickTarget), diff --git a/apps/headless/Tests/agent-runtime.test.mjs b/apps/headless/Tests/agent-runtime.test.mjs index 04ecc6b..fe87d4d 100644 --- a/apps/headless/Tests/agent-runtime.test.mjs +++ b/apps/headless/Tests/agent-runtime.test.mjs @@ -180,6 +180,7 @@ assert.equal(agent.click({target: fresh.elements[0].ref}).clicked, fresh.element const controls = window.document.createElement('section'); controls.innerHTML = ` + ; readonly "upload": Untrusted; @@ -843,6 +865,7 @@ export type CommandResult = CommandResults[C]; export const COMMAND_METADATA = { "ping": { "capabilityNegotiated": false, + "constraints": [], "parameters": [], "result": { "mayContainUntrustedContent": false, @@ -912,6 +935,7 @@ export const COMMAND_METADATA = { }, "shutdown": { "capabilityNegotiated": false, + "constraints": [], "parameters": [], "result": { "mayContainUntrustedContent": false, @@ -936,6 +960,7 @@ export const COMMAND_METADATA = { }, "profile.clear": { "capabilityNegotiated": false, + "constraints": [], "parameters": [], "result": { "mayContainUntrustedContent": false, @@ -965,6 +990,7 @@ export const COMMAND_METADATA = { }, "session.create": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "maximumBytes": 64, @@ -1008,6 +1034,7 @@ export const COMMAND_METADATA = { }, "session.list": { "capabilityNegotiated": false, + "constraints": [], "parameters": [], "result": { "mayContainUntrustedContent": false, @@ -1097,6 +1124,7 @@ export const COMMAND_METADATA = { }, "session.close": { "capabilityNegotiated": false, + "constraints": [], "parameters": [], "result": { "mayContainUntrustedContent": false, @@ -1121,6 +1149,7 @@ export const COMMAND_METADATA = { }, "visit": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "maximumBytes": 8192, @@ -1188,6 +1217,7 @@ export const COMMAND_METADATA = { }, "inspect": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "name": "interactive", @@ -1317,6 +1347,9 @@ export const COMMAND_METADATA = { }, "click": { "capabilityNegotiated": false, + "constraints": [ + "exactly one target reference or semantic role/name target" + ], "parameters": [ { "maximumBytes": 16, @@ -1371,8 +1404,70 @@ export const COMMAND_METADATA = { "parameterPresentOverrides": {} } }, + "hover": { + "capabilityNegotiated": false, + "constraints": [ + "exactly one target reference or semantic role/name target" + ], + "parameters": [ + { + "maximumBytes": 16, + "name": "target", + "required": false, + "sensitive": false, + "type": "string" + }, + { + "maximumBytes": 128, + "name": "role", + "required": false, + "sensitive": false, + "type": "string" + }, + { + "maximumBytes": 1000, + "name": "name", + "required": false, + "sensitive": false, + "type": "string" + } + ], + "result": { + "mayContainUntrustedContent": true, + "schema": { + "additionalProperties": true, + "fields": [ + { + "name": "hovered", + "required": true, + "type": "string" + }, + { + "name": "role", + "required": true, + "type": "string" + }, + { + "name": "name", + "required": true, + "type": "string" + } + ], + "name": "Hover", + "type": "object" + } + }, + "scope": "session", + "timeout": { + "defaultMilliseconds": 15000, + "parameterPresentOverrides": {} + } + }, "fill": { "capabilityNegotiated": false, + "constraints": [ + "exactly one target reference or semantic role/name target" + ], "parameters": [ { "maximumBytes": 16, @@ -1431,6 +1526,11 @@ export const COMMAND_METADATA = { }, "select": { "capabilityNegotiated": false, + "constraints": [ + "exactly one target reference or semantic role/name target", + "exactly one option label or value", + "native single-selection HTML select controls only" + ], "parameters": [ { "maximumBytes": 16, @@ -1506,6 +1606,10 @@ export const COMMAND_METADATA = { }, "upload": { "capabilityNegotiated": true, + "constraints": [ + "artifact must be an existing private-store basename", + "exactly one target reference or semantic role/name target" + ], "parameters": [ { "maximumBytes": 16, @@ -1574,6 +1678,7 @@ export const COMMAND_METADATA = { }, "press": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "maximumBytes": 32, @@ -1606,6 +1711,7 @@ export const COMMAND_METADATA = { }, "scroll": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "maximumBytes": 8192, @@ -1657,6 +1763,7 @@ export const COMMAND_METADATA = { }, "back": { "capabilityNegotiated": false, + "constraints": [], "parameters": [], "result": { "mayContainUntrustedContent": true, @@ -1716,6 +1823,7 @@ export const COMMAND_METADATA = { }, "reload": { "capabilityNegotiated": false, + "constraints": [], "parameters": [], "result": { "mayContainUntrustedContent": true, @@ -1775,6 +1883,7 @@ export const COMMAND_METADATA = { }, "wait": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "name": "settled", @@ -1873,6 +1982,7 @@ export const COMMAND_METADATA = { }, "tour": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "name": "fullPage", @@ -1922,6 +2032,7 @@ export const COMMAND_METADATA = { }, "capture.info": { "capabilityNegotiated": false, + "constraints": [], "parameters": [], "result": { "mayContainUntrustedContent": true, @@ -1961,6 +2072,11 @@ export const COMMAND_METADATA = { }, "screenshot": { "capabilityNegotiated": true, + "constraints": [ + "target, full-page, and series modes are mutually constrained", + "region is required only for region series and must be an @rN reference", + "PDF requires full-page mode and no clipboard" + ], "parameters": [ { "maximumBytes": 16, @@ -2128,6 +2244,7 @@ export const COMMAND_METADATA = { }, "artifact.list": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "maximum": 250, @@ -2206,6 +2323,7 @@ export const COMMAND_METADATA = { }, "record.start": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "maximumBytes": 128, @@ -2288,6 +2406,7 @@ export const COMMAND_METADATA = { }, "record.status": { "capabilityNegotiated": false, + "constraints": [], "parameters": [], "result": { "mayContainUntrustedContent": false, @@ -2327,6 +2446,7 @@ export const COMMAND_METADATA = { }, "record.stop": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "maximumBytes": 128, @@ -2374,6 +2494,7 @@ export const COMMAND_METADATA = { }, "qa.report": { "capabilityNegotiated": false, + "constraints": [], "parameters": [], "result": { "mayContainUntrustedContent": true, @@ -2423,6 +2544,7 @@ export const COMMAND_METADATA = { }, "qa.clear": { "capabilityNegotiated": false, + "constraints": [], "parameters": [], "result": { "mayContainUntrustedContent": false, @@ -2447,6 +2569,7 @@ export const COMMAND_METADATA = { }, "console.list": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "maximumBytes": 16, @@ -2536,6 +2659,7 @@ export const COMMAND_METADATA = { }, "network.list": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "name": "failed", @@ -2623,6 +2747,7 @@ export const COMMAND_METADATA = { }, "network.get": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "maximumBytes": 128, @@ -2670,6 +2795,9 @@ export const COMMAND_METADATA = { }, "styles.get": { "capabilityNegotiated": false, + "constraints": [ + "exactly one target reference or semantic role/name target" + ], "parameters": [ { "maximumBytes": 16, @@ -2744,6 +2872,9 @@ export const COMMAND_METADATA = { }, "cookies.list": { "capabilityNegotiated": false, + "constraints": [ + "values require the sensitive diagnostics environment gate" + ], "parameters": [ { "name": "includeValues", @@ -2790,6 +2921,9 @@ export const COMMAND_METADATA = { }, "storage.list": { "capabilityNegotiated": false, + "constraints": [ + "values require the sensitive diagnostics environment gate" + ], "parameters": [ { "maximumBytes": 16, @@ -2838,6 +2972,7 @@ export const COMMAND_METADATA = { }, "visual.compare": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "maximumBytes": 128, @@ -2894,6 +3029,7 @@ export const COMMAND_METADATA = { }, "performance.get": { "capabilityNegotiated": false, + "constraints": [], "parameters": [], "result": { "mayContainUntrustedContent": true, @@ -2933,6 +3069,7 @@ export const COMMAND_METADATA = { }, "animation.list": { "capabilityNegotiated": false, + "constraints": [], "parameters": [], "result": { "mayContainUntrustedContent": true, @@ -2967,6 +3104,7 @@ export const COMMAND_METADATA = { }, "report.create": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "maximumBytes": 128, @@ -3019,6 +3157,7 @@ export const COMMAND_METADATA = { }, "flow.start": { "capabilityNegotiated": false, + "constraints": [], "parameters": [], "result": { "mayContainUntrustedContent": false, @@ -3048,6 +3187,7 @@ export const COMMAND_METADATA = { }, "flow.stop": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "maximumBytes": 128, @@ -3100,6 +3240,7 @@ export const COMMAND_METADATA = { }, "flow.run": { "capabilityNegotiated": false, + "constraints": [], "parameters": [ { "maximumBytes": 128, @@ -3137,6 +3278,7 @@ export const COMMAND_METADATA = { }, "network.emulate": { "capabilityNegotiated": true, + "constraints": [], "parameters": [ { "name": "offline", @@ -3212,6 +3354,7 @@ export const COMMAND_METADATA = { }, "network.mock.set": { "capabilityNegotiated": true, + "constraints": [], "parameters": [ { "maximumBytes": 8192, @@ -3276,6 +3419,7 @@ export const COMMAND_METADATA = { }, "network.mock.clear": { "capabilityNegotiated": true, + "constraints": [], "parameters": [], "result": { "mayContainUntrustedContent": false, @@ -3300,6 +3444,10 @@ export const COMMAND_METADATA = { }, "auth.login": { "capabilityNegotiated": true, + "constraints": [ + "choose interactive login or account alias", + "saved login requires a valid single-use challenge" + ], "parameters": [ { "maximumBytes": 64, @@ -3429,15 +3577,19 @@ export abstract class GeneratedCommandClient { return this.invoke("inspect", parameters, options); } - click(parameters: ClickParameters = {}, options?: CommandOptions): Promise> { + click(parameters: ClickParameters, options?: CommandOptions): Promise> { return this.invoke("click", parameters, options); } + hover(parameters: HoverParameters, options?: CommandOptions): Promise> { + return this.invoke("hover", parameters, options); + } + fill(parameters: FillParameters, options?: CommandOptions): Promise> { return this.invoke("fill", parameters, options); } - select(parameters: SelectParameters = {}, options?: CommandOptions): Promise> { + select(parameters: SelectParameters, options?: CommandOptions): Promise> { return this.invoke("select", parameters, options); } @@ -3513,7 +3665,7 @@ export abstract class GeneratedCommandClient { return this.invoke("network.get", parameters, options); } - stylesGet(parameters: StylesGetParameters = {}, options?: CommandOptions): Promise> { + stylesGet(parameters: StylesGetParameters, options?: CommandOptions): Promise> { return this.invoke("styles.get", parameters, options); } @@ -3590,15 +3742,19 @@ export abstract class GeneratedSessionCommandClient { return this.invoke("inspect", parameters, options); } - click(parameters: ClickParameters = {}, options?: CommandOptions): Promise> { + click(parameters: ClickParameters, options?: CommandOptions): Promise> { return this.invoke("click", parameters, options); } + hover(parameters: HoverParameters, options?: CommandOptions): Promise> { + return this.invoke("hover", parameters, options); + } + fill(parameters: FillParameters, options?: CommandOptions): Promise> { return this.invoke("fill", parameters, options); } - select(parameters: SelectParameters = {}, options?: CommandOptions): Promise> { + select(parameters: SelectParameters, options?: CommandOptions): Promise> { return this.invoke("select", parameters, options); } @@ -3670,7 +3826,7 @@ export abstract class GeneratedSessionCommandClient { return this.invoke("network.get", parameters, options); } - stylesGet(parameters: StylesGetParameters = {}, options?: CommandOptions): Promise> { + stylesGet(parameters: StylesGetParameters, options?: CommandOptions): Promise> { return this.invoke("styles.get", parameters, options); } diff --git a/packages/headless-npm/src/protocol.ts b/packages/headless-npm/src/protocol.ts index 845b1d1..cc07f32 100644 --- a/packages/headless-npm/src/protocol.ts +++ b/packages/headless-npm/src/protocol.ts @@ -139,6 +139,7 @@ export function validateParameters( if (!isPlainParameterRecord(parameters)) { throw new ValidationError(`${command} parameters must be a plain data object`); } + const values = parameters as Readonly>; const definitions = COMMAND_METADATA[command].parameters as readonly UnknownRecord[]; const known = new Map(definitions.map((definition) => [String(definition.name), definition])); for (const key of Object.keys(parameters)) { @@ -146,15 +147,39 @@ export function validateParameters( } for (const definition of definitions) { const name = String(definition.name); - const value = parameters[name]; + const value = values[name]; if (value === undefined) { if (definition.required === true) throw new ValidationError(`${command} requires ${name}`); } else { validateParameter(command, definition, value); } } + const constraints = COMMAND_METADATA[command].constraints as readonly string[]; + if (constraints.includes("exactly one target reference or semantic role/name target")) { + const targetPresent = values.target !== undefined; + const rolePresent = values.role !== undefined; + const namePresent = values.name !== undefined; + if (targetPresent === (rolePresent || namePresent)) { + throw new ValidationError(`${command} requires exactly one reference or semantic target`); + } + if (targetPresent && !/^@e\d+$/.test(String(values.target))) { + throw new ValidationError(`${command}.target must be an element reference`); + } + if ((rolePresent && values.role === "") || (namePresent && values.name === "")) { + throw new ValidationError(`${command} semantic target values must not be empty`); + } + } + if (constraints.includes("exactly one option label or value")) { + const labelPresent = values.label !== undefined; + const valuePresent = values.value !== undefined; + if (labelPresent === valuePresent) { + throw new ValidationError(`${command} requires exactly one option label or value`); + } + if ((labelPresent && values.label === "") || (valuePresent && values.value === "")) { + throw new ValidationError(`${command} option matchers must not be empty`); + } + } if (command === "screenshot") { - const values = parameters as Readonly>; const targetPresent = values.target !== undefined; const semanticTargetPresent = values.role !== undefined || values.name !== undefined; if (targetPresent && semanticTargetPresent) { diff --git a/packages/headless-npm/test/protocol.test.mjs b/packages/headless-npm/test/protocol.test.mjs index 01e565b..9aafce6 100644 --- a/packages/headless-npm/test/protocol.test.mjs +++ b/packages/headless-npm/test/protocol.test.mjs @@ -104,6 +104,14 @@ test("schema-driven validation mirrors portable Swift bounds", () => { () => validateParameters("screenshot", { fullPage: true, target: "@e1" }), /mutually exclusive/, ); + assert.throws(() => validateParameters("hover", {}), /exactly one reference or semantic target/); + assert.throws( + () => validateParameters("hover", { target: "@e1", role: "button" }), + /exactly one reference or semantic target/, + ); + assert.throws(() => validateParameters("hover", { target: "@r1" }), /element reference/); + assert.doesNotThrow(() => validateParameters("hover", { target: "@e1" })); + assert.doesNotThrow(() => validateParameters("hover", { role: "button", name: "Continue" })); }); test("generated scopes and timeout policies drive the SDK", () => { @@ -120,7 +128,7 @@ test("generated scopes and timeout policies drive the SDK", () => { }); test("request framing contains exactly one terminal newline", () => { - const frame = encodeRequest(createRequest("fill", { target: "@1", value: "line one\nline two" }, undefined, "frame")); + const frame = encodeRequest(createRequest("fill", { target: "@e1", value: "line one\nline two" }, undefined, "frame")); assert.equal(frame.at(-1), 0x0a); assert.equal(frame.subarray(0, -1).includes(0x0a), false); assert.equal(JSON.parse(frame.subarray(0, -1)).parameters.value, "line one\nline two"); diff --git a/packages/headless-python/scripts/generate.py b/packages/headless-python/scripts/generate.py index c6e6af1..5738fa9 100644 --- a/packages/headless-python/scripts/generate.py +++ b/packages/headless-python/scripts/generate.py @@ -114,7 +114,38 @@ def emit_result(lines: list[str], schema: dict[str, Any]) -> None: lines.append("") +def has_target_only_constraint(command: dict[str, Any]) -> bool: + return command["constraints"] == [ + "exactly one target reference or semantic role/name target" + ] and [parameter["name"] for parameter in command["parameters"]] == ["target", "role", "name"] + + +def emit_target_overloads(lines: list[str], command: dict[str, Any], asynchronous: bool) -> None: + prefix = "async def" if asynchronous else "def" + method = snake_name(command["name"]) + result = command_result_type(command) + cancel_type = "AsyncCancellation" if asynchronous else "SyncCancellation" + variants = [ + ("target: str", "role: None = None", "name: None = None"), + ("target: None = None", "role: str", "name: str | None = None"), + ("target: None = None", "role: None = None", "name: str"), + ] + for target, role, name in variants: + lines.append(" @overload") + lines.append(f" {prefix} {method}(") + lines.append(" self,") + lines.append(" *,") + lines.append(f" {target},") + lines.append(f" {role},") + lines.append(f" {name},") + lines.append(" timeout: float | None = None,") + lines.append(f" cancel: {cancel_type} | None = None,") + lines.append(f" ) -> {result}: ...") + + def emit_method(lines: list[str], command: dict[str, Any], asynchronous: bool) -> None: + if has_target_only_constraint(command): + emit_target_overloads(lines, command, asynchronous) prefix = "async def" if asynchronous else "def" method = snake_name(command["name"]) result = command_result_type(command) @@ -254,7 +285,16 @@ def generate() -> str: "from __future__ import annotations", "", "from collections.abc import Sequence", - "from typing import Any, Literal, NotRequired, Required, TypeAlias, TypedDict, cast", + "from typing import (", + " Any,", + " Literal,", + " NotRequired,", + " Required,", + " TypeAlias,", + " TypedDict,", + " cast,", + " overload,", + ")", "", "from ._types import AsyncCancellation, SyncCancellation, Untrusted", "", diff --git a/packages/headless-python/src/headless_sdk/_protocol.py b/packages/headless-python/src/headless_sdk/_protocol.py index 0900b00..41a6f3c 100644 --- a/packages/headless-python/src/headless_sdk/_protocol.py +++ b/packages/headless-python/src/headless_sdk/_protocol.py @@ -153,6 +153,28 @@ def validate_parameters(command: CommandName, parameters: Mapping[str, object]) raise ValidationError(f"{command} requires {name}") else: _validate_parameter(command, definition, parameters[name]) + constraints = cast(list[str], metadata["constraints"]) + if "exactly one target reference or semantic role/name target" in constraints: + target_present = "target" in parameters + role_present = "role" in parameters + name_present = "name" in parameters + if target_present == (role_present or name_present): + raise ValidationError(f"{command} requires exactly one reference or semantic target") + if target_present and re.fullmatch(r"@e\d+", cast(str, parameters["target"])) is None: + raise ValidationError(f"{command}.target must be an element reference") + if (role_present and parameters["role"] == "") or ( + name_present and parameters["name"] == "" + ): + raise ValidationError(f"{command} semantic target values must not be empty") + if "exactly one option label or value" in constraints: + label_present = "label" in parameters + value_present = "value" in parameters + if label_present == value_present: + raise ValidationError(f"{command} requires exactly one option label or value") + if (label_present and parameters["label"] == "") or ( + value_present and parameters["value"] == "" + ): + raise ValidationError(f"{command} option matchers must not be empty") if command == "screenshot": target_present = "target" in parameters semantic_target_present = "role" in parameters or "name" in parameters diff --git a/packages/headless-python/src/headless_sdk/generated.py b/packages/headless-python/src/headless_sdk/generated.py index d455b4f..e6affaa 100644 --- a/packages/headless-python/src/headless_sdk/generated.py +++ b/packages/headless-python/src/headless_sdk/generated.py @@ -2,7 +2,16 @@ from __future__ import annotations from collections.abc import Sequence -from typing import Any, Literal, NotRequired, Required, TypeAlias, TypedDict, cast +from typing import ( + Any, + Literal, + NotRequired, + Required, + TypeAlias, + TypedDict, + cast, + overload, +) from ._types import AsyncCancellation, SyncCancellation, Untrusted @@ -13,8 +22,8 @@ PROTOCOL_SCHEMA_VERSION = 1 MAXIMUM_MESSAGE_BYTES = 1048576 MAXIMUM_COMMAND_TIMEOUT_SECONDS = 125.0 -PROTOCOL_SCHEMA_SHA256 = 'db70dc1b07b034624e85ab8f07ac420d5af5b948edf999cd11e5445ed07d7a17' -PROTOCOL_FIXTURES_SHA256 = '76bbee05cacd7ed16452176ad2973bf4604f4b864f8de1277664880c079a5435' +PROTOCOL_SCHEMA_SHA256 = 'b7058c5aa8035496a5a21fdbe274f4db9ff7dacd9ad2aac908349a55f3079e39' +PROTOCOL_FIXTURES_SHA256 = '397f001d3930c79db7da27f4b885276c71fbcd5c56756a5236ff298f10636567' RESPONSE_ADDITIONAL_PROPERTIES = True COMMAND_ERROR_CODES = ('ARTIFACT_ERROR', 'AUTH_ACCOUNT_NOT_FOUND', 'AUTH_CHALLENGE_CONSUMED', 'AUTH_CHALLENGE_EXPIRED', 'AUTH_CHALLENGE_NOT_FOUND', 'AUTH_FORM_CHANGED', 'AUTH_ORIGIN_CHANGED', 'AUTH_REQUIRED', 'CREDENTIAL_ALIAS_EXISTS', 'ELEMENT_NOT_FOUND', 'FLOW_FAILED', 'HOST_STOPPING', 'HOST_UNAVAILABLE', 'INTERNAL_ERROR', 'INVALID_CAPTURE_FORMAT', 'INVALID_COMMAND', 'INVALID_FLOW', 'INVALID_INPUT', 'INVALID_REQUEST', 'INVALID_SESSION', 'MISSING_PARAMETER', 'OPERATION_FAILED', 'PAGINATION_CURSOR_EXPIRED', 'PAGINATION_CURSOR_INVALID', 'PAGINATION_CURSOR_SCOPE_MISMATCH', 'PAGINATION_CURSOR_STALE', 'PEER_DENIED', 'RECORDER_UNAVAILABLE', 'RECORDING_ACTIVE', 'RECORDING_FAILED', 'RECORDING_NOT_ACTIVE', 'REGION_NOT_FOUND', 'RESPONSE_TOO_LARGE', 'SENSITIVE_DIAGNOSTICS_DISABLED', 'SESSION_EXISTS', 'SESSION_NOT_FOUND', 'TIMEOUT', 'UNSAFE_NAVIGATION', 'UNSAFE_RESOURCE_TYPE', 'UNSUPPORTED_CAPABILITY', 'USER_PRESENCE_DENIED', 'USER_PRESENCE_UNAVAILABLE', 'VAULT_LOCKED', 'VAULT_OPERATION_FAILED', 'VAULT_RESPONSE_INVALID', 'VAULT_UNAVAILABLE') CommandErrorCode = Literal['ARTIFACT_ERROR', 'AUTH_ACCOUNT_NOT_FOUND', 'AUTH_CHALLENGE_CONSUMED', 'AUTH_CHALLENGE_EXPIRED', 'AUTH_CHALLENGE_NOT_FOUND', 'AUTH_FORM_CHANGED', 'AUTH_ORIGIN_CHANGED', 'AUTH_REQUIRED', 'CREDENTIAL_ALIAS_EXISTS', 'ELEMENT_NOT_FOUND', 'FLOW_FAILED', 'HOST_STOPPING', 'HOST_UNAVAILABLE', 'INTERNAL_ERROR', 'INVALID_CAPTURE_FORMAT', 'INVALID_COMMAND', 'INVALID_FLOW', 'INVALID_INPUT', 'INVALID_REQUEST', 'INVALID_SESSION', 'MISSING_PARAMETER', 'OPERATION_FAILED', 'PAGINATION_CURSOR_EXPIRED', 'PAGINATION_CURSOR_INVALID', 'PAGINATION_CURSOR_SCOPE_MISMATCH', 'PAGINATION_CURSOR_STALE', 'PEER_DENIED', 'RECORDER_UNAVAILABLE', 'RECORDING_ACTIVE', 'RECORDING_FAILED', 'RECORDING_NOT_ACTIVE', 'REGION_NOT_FOUND', 'RESPONSE_TOO_LARGE', 'SENSITIVE_DIAGNOSTICS_DISABLED', 'SESSION_EXISTS', 'SESSION_NOT_FOUND', 'TIMEOUT', 'UNSAFE_NAVIGATION', 'UNSAFE_RESOURCE_TYPE', 'UNSUPPORTED_CAPABILITY', 'USER_PRESENCE_DENIED', 'USER_PRESENCE_UNAVAILABLE', 'VAULT_LOCKED', 'VAULT_OPERATION_FAILED', 'VAULT_RESPONSE_INVALID', 'VAULT_UNAVAILABLE'] @@ -22,7 +31,7 @@ LifecycleErrorCode = Literal['HOST_START_FAILED', 'NAVIGATION_ALLOWLIST_CONFLICT', 'UNSUPPORTED_BROWSER_RUNTIME', 'UNSUPPORTED_CAPABILITY'] LAUNCH_PRESENTATIONS = ('background', 'foreground') LaunchPresentation = Literal['background', 'foreground'] -CommandName = Literal['ping', 'shutdown', 'profile.clear', 'session.create', 'session.list', 'session.close', 'visit', 'inspect', 'click', 'fill', 'select', 'upload', 'press', 'scroll', 'back', 'reload', 'wait', 'tour', 'capture.info', 'screenshot', 'artifact.list', 'record.start', 'record.status', 'record.stop', 'qa.report', 'qa.clear', 'console.list', 'network.list', 'network.get', 'styles.get', 'cookies.list', 'storage.list', 'visual.compare', 'performance.get', 'animation.list', 'report.create', 'flow.start', 'flow.stop', 'flow.run', 'network.emulate', 'network.mock.set', 'network.mock.clear', 'auth.login'] +CommandName = Literal['ping', 'shutdown', 'profile.clear', 'session.create', 'session.list', 'session.close', 'visit', 'inspect', 'click', 'hover', 'fill', 'select', 'upload', 'press', 'scroll', 'back', 'reload', 'wait', 'tour', 'capture.info', 'screenshot', 'artifact.list', 'record.start', 'record.status', 'record.stop', 'qa.report', 'qa.clear', 'console.list', 'network.list', 'network.get', 'styles.get', 'cookies.list', 'storage.list', 'visual.compare', 'performance.get', 'animation.list', 'report.create', 'flow.start', 'flow.stop', 'flow.run', 'network.emulate', 'network.mock.set', 'network.mock.clear', 'auth.login'] class PingParameters(TypedDict): pass @@ -61,6 +70,11 @@ class ClickParameters(TypedDict): role: NotRequired[str] name: NotRequired[str] +class HoverParameters(TypedDict): + target: NotRequired[str] + role: NotRequired[str] + name: NotRequired[str] + class FillParameters(TypedDict): target: NotRequired[str] role: NotRequired[str] @@ -278,6 +292,11 @@ class Click(TypedDict, total=False): role: Required[str] name: Required[str] +class Hover(TypedDict, total=False): + hovered: Required[str] + role: Required[str] + name: Required[str] + class Fill(TypedDict, total=False): filled: Required[str] valueLength: Required[int | float] @@ -807,6 +826,32 @@ class AuthenticationRequired(TypedDict, total=False): 'type': 'object'}}, 'scope': 'session', 'timeout': {'defaultMilliseconds': 15000, 'parameterPresentOverrides': {}}}, + 'hover': {'capabilityNegotiated': False, + 'constraints': ['exactly one target reference or semantic role/name target'], + 'parameters': [{'maximumBytes': 16, + 'name': 'target', + 'required': False, + 'sensitive': False, + 'type': 'string'}, + {'maximumBytes': 128, + 'name': 'role', + 'required': False, + 'sensitive': False, + 'type': 'string'}, + {'maximumBytes': 1000, + 'name': 'name', + 'required': False, + 'sensitive': False, + 'type': 'string'}], + 'result': {'mayContainUntrustedContent': True, + 'schema': {'additionalProperties': True, + 'fields': [{'name': 'hovered', 'required': True, 'type': 'string'}, + {'name': 'role', 'required': True, 'type': 'string'}, + {'name': 'name', 'required': True, 'type': 'string'}], + 'name': 'Hover', + 'type': 'object'}}, + 'scope': 'session', + 'timeout': {'defaultMilliseconds': 15000, 'parameterPresentOverrides': {}}}, 'inspect': {'capabilityNegotiated': False, 'constraints': [], 'parameters': [{'name': 'interactive', @@ -2047,6 +2092,36 @@ def inspect( parameters['depth'] = cast(JsonValue, depth) return cast(Untrusted[Inspection], self._invoke_sync('inspect', parameters, timeout, cancel)) + @overload + def click( + self, + *, + target: str, + role: None = None, + name: None = None, + timeout: float | None = None, + cancel: SyncCancellation | None = None, + ) -> Untrusted[Click]: ... + @overload + def click( + self, + *, + target: None = None, + role: str, + name: str | None = None, + timeout: float | None = None, + cancel: SyncCancellation | None = None, + ) -> Untrusted[Click]: ... + @overload + def click( + self, + *, + target: None = None, + role: None = None, + name: str, + timeout: float | None = None, + cancel: SyncCancellation | None = None, + ) -> Untrusted[Click]: ... def click( self, *, @@ -2066,6 +2141,55 @@ def click( parameters['name'] = cast(JsonValue, name) return cast(Untrusted[Click], self._invoke_sync('click', parameters, timeout, cancel)) + @overload + def hover( + self, + *, + target: str, + role: None = None, + name: None = None, + timeout: float | None = None, + cancel: SyncCancellation | None = None, + ) -> Untrusted[Hover]: ... + @overload + def hover( + self, + *, + target: None = None, + role: str, + name: str | None = None, + timeout: float | None = None, + cancel: SyncCancellation | None = None, + ) -> Untrusted[Hover]: ... + @overload + def hover( + self, + *, + target: None = None, + role: None = None, + name: str, + timeout: float | None = None, + cancel: SyncCancellation | None = None, + ) -> Untrusted[Hover]: ... + def hover( + self, + *, + target: str | None = None, + role: str | None = None, + name: str | None = None, + timeout: float | None = None, + cancel: SyncCancellation | None = None, + ) -> Untrusted[Hover]: + parameters: dict[str, JsonValue] = { + } + if target is not None: + parameters['target'] = cast(JsonValue, target) + if role is not None: + parameters['role'] = cast(JsonValue, role) + if name is not None: + parameters['name'] = cast(JsonValue, name) + return cast(Untrusted[Hover], self._invoke_sync('hover', parameters, timeout, cancel)) + def fill( self, *, @@ -2723,6 +2847,36 @@ async def inspect( parameters['depth'] = cast(JsonValue, depth) return cast(Untrusted[Inspection], await self._invoke_async('inspect', parameters, timeout, cancel)) + @overload + async def click( + self, + *, + target: str, + role: None = None, + name: None = None, + timeout: float | None = None, + cancel: AsyncCancellation | None = None, + ) -> Untrusted[Click]: ... + @overload + async def click( + self, + *, + target: None = None, + role: str, + name: str | None = None, + timeout: float | None = None, + cancel: AsyncCancellation | None = None, + ) -> Untrusted[Click]: ... + @overload + async def click( + self, + *, + target: None = None, + role: None = None, + name: str, + timeout: float | None = None, + cancel: AsyncCancellation | None = None, + ) -> Untrusted[Click]: ... async def click( self, *, @@ -2742,6 +2896,55 @@ async def click( parameters['name'] = cast(JsonValue, name) return cast(Untrusted[Click], await self._invoke_async('click', parameters, timeout, cancel)) + @overload + async def hover( + self, + *, + target: str, + role: None = None, + name: None = None, + timeout: float | None = None, + cancel: AsyncCancellation | None = None, + ) -> Untrusted[Hover]: ... + @overload + async def hover( + self, + *, + target: None = None, + role: str, + name: str | None = None, + timeout: float | None = None, + cancel: AsyncCancellation | None = None, + ) -> Untrusted[Hover]: ... + @overload + async def hover( + self, + *, + target: None = None, + role: None = None, + name: str, + timeout: float | None = None, + cancel: AsyncCancellation | None = None, + ) -> Untrusted[Hover]: ... + async def hover( + self, + *, + target: str | None = None, + role: str | None = None, + name: str | None = None, + timeout: float | None = None, + cancel: AsyncCancellation | None = None, + ) -> Untrusted[Hover]: + parameters: dict[str, JsonValue] = { + } + if target is not None: + parameters['target'] = cast(JsonValue, target) + if role is not None: + parameters['role'] = cast(JsonValue, role) + if name is not None: + parameters['name'] = cast(JsonValue, name) + return cast(Untrusted[Hover], await self._invoke_async('hover', parameters, timeout, cancel)) + async def fill( self, *, diff --git a/packages/headless-python/tests/test_protocol.py b/packages/headless-python/tests/test_protocol.py index 7614396..5a61c6b 100644 --- a/packages/headless-python/tests/test_protocol.py +++ b/packages/headless-python/tests/test_protocol.py @@ -107,6 +107,14 @@ def test_schema_driven_validation_matches_swift_bounds() -> None: create_request("screenshot", {"series": "region", "region": "@r4", "clipboard": True}) with pytest.raises(ValidationError, match="mutually exclusive"): create_request("screenshot", {"fullPage": True, "target": "@e1"}) + with pytest.raises(ValidationError, match="exactly one reference or semantic target"): + create_request("hover", {}) + with pytest.raises(ValidationError, match="exactly one reference or semantic target"): + create_request("hover", {"target": "@e1", "role": "button"}) + with pytest.raises(ValidationError, match="element reference"): + create_request("hover", {"target": "@r1"}) + create_request("hover", {"target": "@e1"}) + create_request("hover", {"role": "button", "name": "Continue"}) with pytest.raises(ValidationError, match="finite number"): create_request("wait", {"timeoutMs": 10**1000}) with pytest.raises(ValidationError, match="request id is invalid"): @@ -130,7 +138,7 @@ def test_generated_timeout_policies_are_used() -> None: def test_request_has_exactly_one_terminal_newline() -> None: frame = encode_request( - create_request("fill", {"target": "@1", "value": "line one\nline two"}, request_id="one") + create_request("fill", {"target": "@e1", "value": "line one\nline two"}, request_id="one") ) assert frame.endswith(b"\n") assert b"\n" not in frame[:-1] diff --git a/packages/headless-python/tests/test_transport.py b/packages/headless-python/tests/test_transport.py index 874dfb5..14526ad 100644 --- a/packages/headless-python/tests/test_transport.py +++ b/packages/headless-python/tests/test_transport.py @@ -60,7 +60,9 @@ def response(request: dict[str, Any], _: object) -> bytes: assert isinstance(result, Untrusted) assert result.value["title"] == "Untrusted" assert server.requests[1]["session"] == "work.one" - assert "visit" in client.capabilities["commands"] + commands = client.capabilities["commands"] + assert isinstance(commands, list) + assert "visit" in commands with pytest.raises(ValidationError, match="timeout"): client.ping(timeout=10**1000) diff --git a/packages/headless-python/tests/type_contract.py b/packages/headless-python/tests/type_contract.py index dd9b04b..51e8ef7 100644 --- a/packages/headless-python/tests/type_contract.py +++ b/packages/headless-python/tests/type_contract.py @@ -11,6 +11,9 @@ def sync_contract(client: Client, error: AuthenticationRequiredError) -> None: assert_type(page, Untrusted[PageState]) login = client.session("work").auth_login(challenge="id", account="work") assert_type(login, Untrusted[AuthenticationLogin]) + client.session("work").hover(target="@e1") + client.session("work").hover(role="button", name="Account") + client.session("work").hover(name="Account") assert_type(error.details.value["challenge"], str) @@ -19,3 +22,6 @@ async def async_contract(client: AsyncClient) -> None: assert_type(page, Untrusted[PageState]) login = await client.session("work").auth_login(interactive=True) assert_type(login, Untrusted[AuthenticationLogin]) + await client.session("work").hover(target="@e1") + await client.session("work").hover(role="button", name="Account") + await client.session("work").hover(name="Account") diff --git a/sdk/protocol-fixtures.json b/sdk/protocol-fixtures.json index 2769b74..3f88bb5 100644 --- a/sdk/protocol-fixtures.json +++ b/sdk/protocol-fixtures.json @@ -54,6 +54,22 @@ } } }, + { + "name": "semantic hover", + "argv": ["hover", "--role", "button", "--name", "Continue"], + "request": { + "id": "fixture-hover", + "version": "0.5", + "command": "hover", + "parameters": {"role": "button", "name": "Continue"} + }, + "response": { + "id": "fixture-hover", + "version": "0.5", + "ok": true, + "result": {"hovered": "@e1", "role": "button", "name": "Continue"} + } + }, { "name": "credential alias login", "argv": ["auth", "login", "--challenge", "11111111-1111-4111-8111-111111111111", "--account", "work"], diff --git a/sdk/protocol-schema.json b/sdk/protocol-schema.json index 0ad10b6..b195901 100644 --- a/sdk/protocol-schema.json +++ b/sdk/protocol-schema.json @@ -1 +1 @@ -{"cancellation":{"afterSend":"close the client connection; command outcome is unknown until state is inspected","beforeSend":"request is not written","ownedHost":"terminate, await, and reap only a host process launched by that SDK instance","sharedHost":"never stop the host implicitly"},"commands":[{"capabilityNegotiated":false,"constraints":[],"name":"ping","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"ready","required":true,"type":"boolean"},{"name":"pid","required":true,"type":"number"},{"name":"engine","required":true,"type":"string"},{"name":"platform","required":true,"type":"string"},{"name":"productVersion","required":true,"type":"string"},{"name":"protocolVersion","required":true,"type":"string"},{"name":"capabilities","required":true,"type":"object"},{"name":"recordingAvailable","required":true,"type":"boolean"},{"name":"artifactDirectory","required":true,"type":"string"},{"name":"navigationAllowlist","required":true,"type":"array"}],"name":"HostStatus","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"shutdown","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"stopping","required":true,"type":"boolean"}],"name":"Shutdown","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"profile.clear","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"cleared","required":true,"type":"boolean"},{"name":"session","required":true,"type":"string"}],"name":"ProfileClear","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"session.create","parameters":[{"maximumBytes":64,"name":"name","required":true,"sensitive":false,"type":"string"},{"name":"isolated","required":false,"sensitive":false,"type":"boolean"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"session","required":true,"type":"string"},{"name":"isolated","required":true,"type":"boolean"}],"name":"SessionCreate","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"session.list","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"items":{"type":"string"},"name":"sessions","required":true,"type":"array"},{"items":{"additionalProperties":true,"fields":[{"name":"name","required":true,"type":"string"},{"name":"isolated","required":true,"type":"boolean"},{"name":"ageMs","required":true,"type":"number"},{"name":"status","required":true,"type":"string","values":["available","navigating","unavailable"]},{"name":"url","required":true,"type":"string-or-null"},{"name":"title","required":true,"type":"string-or-null"},{"name":"urlTruncated","required":true,"type":"boolean"},{"name":"titleTruncated","required":true,"type":"boolean"},{"name":"untrustedContent","required":true,"type":"boolean"}],"name":"SessionDetail","type":"object"},"name":"details","required":true,"type":"array"}],"name":"SessionList","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"session.close","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"closed","required":true,"type":"string"}],"name":"SessionClose","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"visit","parameters":[{"maximumBytes":8192,"name":"url","required":true,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"title","required":true,"type":"string"},{"name":"readyState","required":true,"type":"string"},{"name":"text","required":true,"type":"string"},{"name":"runningAnimations","required":true,"type":"number"},{"name":"mutationQuietMs","required":true,"type":"number"},{"name":"scrollY","required":true,"type":"number"},{"name":"contentHeight","required":true,"type":"number"}],"name":"PageState","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"inspect","parameters":[{"name":"interactive","required":false,"sensitive":false,"type":"boolean"},{"name":"text","required":false,"sensitive":false,"type":"boolean"},{"maximumBytes":16,"name":"context","required":false,"sensitive":false,"type":"string","values":["summary","outline","text","actions","full"]},{"maximumBytes":512,"name":"task","required":false,"sensitive":false,"type":"string"},{"maximumBytes":16,"name":"within","required":false,"sensitive":false,"type":"string"},{"maximum":250,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"integer"},{"maximum":16000,"minimum":256,"name":"budget","required":false,"sensitive":false,"type":"integer"},{"maximum":8,"minimum":0,"name":"depth","required":false,"sensitive":false,"type":"integer"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"title","required":true,"type":"string"},{"name":"contextMode","required":true,"type":"string"},{"name":"viewport","required":true,"type":"object"},{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"elements","required":false,"type":"array"},{"name":"regions","required":false,"type":"array"},{"name":"snippets","required":false,"type":"array"},{"name":"text","required":false,"type":"string"}],"name":"Inspection","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["exactly one target reference or semantic role/name target"],"name":"click","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"clicked","required":true,"type":"string"},{"name":"role","required":true,"type":"string"},{"name":"name","required":true,"type":"string"}],"name":"Click","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["exactly one target reference or semantic role/name target"],"name":"fill","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"},{"maximumBytes":900000,"name":"value","required":true,"sensitive":true,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"filled","required":true,"type":"string"},{"name":"valueLength","required":true,"type":"number"}],"name":"Fill","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["exactly one target reference or semantic role/name target","exactly one option label or value","native single-selection HTML select controls only"],"name":"select","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"label","required":false,"sensitive":true,"type":"string"},{"maximumBytes":1000,"name":"value","required":false,"sensitive":true,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"selected","required":true,"type":"string"},{"name":"role","required":true,"type":"string"},{"name":"name","required":true,"type":"string"},{"name":"optionIndex","required":true,"type":"number"}],"name":"Select","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":["artifact must be an existing private-store basename","exactly one target reference or semantic role/name target"],"name":"upload","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"artifact","required":true,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"uploaded","required":true,"type":"string"},{"name":"role","required":true,"type":"string"},{"name":"name","required":true,"type":"string"},{"name":"artifact","required":true,"type":"string"}],"name":"Upload","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"press","parameters":[{"maximumBytes":32,"name":"key","required":true,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"pressed","required":true,"type":"string"}],"name":"Press","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"scroll","parameters":[{"maximumBytes":8192,"name":"direction","required":false,"sensitive":false,"type":"string","values":["up","down","top","bottom"]},{"maximum":100000,"minimum":0.1,"name":"amount","required":false,"sensitive":false,"type":"number"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"direction","required":true,"type":"string"},{"name":"amount","required":true,"type":"number"}],"name":"Scroll","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"back","parameters":[],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"title","required":true,"type":"string"},{"name":"readyState","required":true,"type":"string"},{"name":"text","required":true,"type":"string"},{"name":"runningAnimations","required":true,"type":"number"},{"name":"mutationQuietMs","required":true,"type":"number"},{"name":"scrollY","required":true,"type":"number"},{"name":"contentHeight","required":true,"type":"number"}],"name":"PageState","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"reload","parameters":[],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"title","required":true,"type":"string"},{"name":"readyState","required":true,"type":"string"},{"name":"text","required":true,"type":"string"},{"name":"runningAnimations","required":true,"type":"number"},{"name":"mutationQuietMs","required":true,"type":"number"},{"name":"scrollY","required":true,"type":"number"},{"name":"contentHeight","required":true,"type":"number"}],"name":"PageState","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"wait","parameters":[{"name":"settled","required":false,"sensitive":false,"type":"boolean"},{"name":"networkIdle","required":false,"sensitive":false,"type":"boolean"},{"maximumBytes":8192,"name":"url","required":false,"sensitive":false,"type":"string"},{"maximumBytes":30000,"name":"text","required":false,"sensitive":false,"type":"string"},{"maximum":120000,"minimum":100,"name":"timeoutMs","required":false,"sensitive":false,"type":"number"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"title","required":true,"type":"string"},{"name":"readyState","required":true,"type":"string"},{"name":"text","required":true,"type":"string"},{"name":"runningAnimations","required":true,"type":"number"},{"name":"mutationQuietMs","required":true,"type":"number"},{"name":"scrollY","required":true,"type":"number"},{"name":"contentHeight","required":true,"type":"number"}],"name":"PageState","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"maximumMilliseconds":125000,"minimumMilliseconds":10000,"parameterGraceMilliseconds":5000,"parameterName":"timeoutMs","parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"tour","parameters":[{"name":"fullPage","required":false,"sensitive":false,"type":"boolean"},{"maximum":5000,"minimum":100,"name":"pace","required":false,"sensitive":false,"type":"number"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"start","required":true,"type":"number"},{"name":"end","required":true,"type":"number"},{"name":"durationMs","required":true,"type":"number"}],"name":"Tour","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":125000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"capture.info","parameters":[],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"engine","required":true,"type":"string"},{"name":"page","required":true,"type":"object"},{"name":"trace","required":true,"type":"array"},{"name":"recording","required":true,"type":"object"}],"name":"CaptureInfo","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":["target, full-page, and series modes are mutually constrained","region is required only for region series and must be an @rN reference","PDF requires full-page mode and no clipboard"],"name":"screenshot","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"},{"name":"fullPage","required":false,"sensitive":false,"type":"boolean"},{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"},{"maximumBytes":32,"name":"series","required":false,"sensitive":false,"type":"string","values":["viewport","section","region"]},{"maximumBytes":16,"name":"region","required":false,"sensitive":false,"type":"string"},{"maximumBytes":80,"name":"outputPrefix","required":false,"sensitive":false,"type":"string"},{"caseInsensitiveValues":true,"maximumBytes":16,"name":"format","required":false,"sensitive":false,"type":"string","values":["png","jpg","jpeg","pdf"]},{"name":"clipboard","required":false,"sensitive":false,"type":"boolean"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"name","required":false,"type":"string"},{"name":"path","required":false,"type":"string"},{"name":"kind","required":false,"type":"string"},{"name":"bytes","required":false,"type":"number"},{"name":"createdAt","required":false,"type":"number"},{"name":"artifacts","required":false,"type":"array"},{"name":"series","required":false,"type":"string","values":["viewport","section","region"]},{"name":"count","required":false,"type":"number"},{"name":"positions","required":false,"type":"array"},{"name":"truncated","required":false,"type":"boolean"},{"name":"totalPoints","required":false,"type":"number"},{"name":"untrustedContent","required":false,"type":"boolean"}],"name":"Screenshot","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":30000,"parameterPresentOverrides":{"series":125000}}},{"capabilityNegotiated":false,"constraints":[],"name":"artifact.list","parameters":[{"maximum":250,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"integer"},{"maximumBytes":64,"name":"cursor","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"directory","required":true,"type":"string"},{"name":"artifacts","required":true,"type":"array"},{"name":"returned","required":true,"type":"number"},{"name":"total","required":true,"type":"number"},{"name":"omitted","required":true,"type":"number"},{"name":"truncated","required":true,"type":"boolean"},{"name":"nextCursor","required":true,"type":"string-or-null"},{"name":"mutation","required":true,"type":"string","values":["none"]}],"name":"ArtifactList","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"record.start","parameters":[{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"},{"maximum":30,"minimum":1,"name":"fps","required":false,"sensitive":false,"type":"number"},{"caseInsensitiveValues":true,"maximumBytes":16,"name":"format","required":false,"sensitive":false,"type":"string","values":["mp4","mov","webm","gif"]},{"caseInsensitiveValues":true,"maximumBytes":16,"name":"quality","required":false,"sensitive":false,"type":"string","values":["fast","balanced","high"]}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"active","required":true,"type":"boolean"},{"name":"format","required":false,"type":"string"},{"name":"quality","required":false,"type":"string"},{"name":"name","required":false,"type":"string"}],"name":"Recording","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"record.status","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"active","required":true,"type":"boolean"},{"name":"format","required":false,"type":"string"},{"name":"quality","required":false,"type":"string"},{"name":"name","required":false,"type":"string"}],"name":"Recording","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"record.stop","parameters":[{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"active","required":true,"type":"boolean"},{"name":"format","required":false,"type":"string"},{"name":"quality","required":false,"type":"string"},{"name":"name","required":false,"type":"string"}],"name":"Recording","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":30000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"qa.report","parameters":[],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"summary","required":true,"type":"object"},{"name":"issues","required":true,"type":"array"},{"name":"events","required":true,"type":"array"},{"name":"omitted","required":true,"type":"object"},{"name":"truncated","required":true,"type":"boolean"}],"name":"QAReport","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"qa.clear","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"cleared","required":true,"type":"number"}],"name":"QAClear","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"console.list","parameters":[{"maximumBytes":16,"name":"level","required":false,"sensitive":false,"type":"string","values":["all","log","info","debug","warn","error","assert"]},{"maximum":250,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"integer"},{"maximumBytes":64,"name":"cursor","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"messages","required":true,"type":"array"},{"name":"returned","required":true,"type":"number"},{"name":"available","required":true,"type":"number"},{"name":"truncated","required":true,"type":"boolean"},{"name":"nextCursor","required":true,"type":"string-or-null"},{"name":"mutation","required":true,"type":"string","values":["none"]}],"name":"ConsoleList","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"network.list","parameters":[{"name":"failed","required":false,"sensitive":false,"type":"boolean"},{"maximum":599,"minimum":100,"name":"status","required":false,"sensitive":false,"type":"number"},{"maximum":250,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"integer"},{"maximumBytes":64,"name":"cursor","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"requests","required":true,"type":"array"},{"name":"returned","required":true,"type":"number"},{"name":"available","required":true,"type":"number"},{"name":"truncated","required":true,"type":"boolean"},{"name":"nextCursor","required":true,"type":"string-or-null"},{"name":"mutation","required":true,"type":"string","values":["none"]}],"name":"NetworkList","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"network.get","parameters":[{"maximumBytes":128,"name":"requestId","required":true,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"found","required":true,"type":"boolean"},{"name":"requestId","required":false,"type":"string"},{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"request","required":false,"type":"object"}],"name":"NetworkDetail","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["exactly one target reference or semantic role/name target"],"name":"styles.get","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"},{"itemMaximumBytes":128,"maximumItems":64,"name":"properties","required":false,"sensitive":false,"type":"string-array"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"ref","required":true,"type":"string"},{"name":"role","required":true,"type":"string"},{"name":"name","required":true,"type":"string"},{"name":"box","required":true,"type":"object"},{"name":"styles","required":true,"type":"object"}],"name":"Styles","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["values require the sensitive diagnostics environment gate"],"name":"cookies.list","parameters":[{"name":"includeValues","required":false,"sensitive":false,"type":"boolean"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"cookies","required":true,"type":"array"},{"name":"returned","required":true,"type":"number"},{"name":"available","required":true,"type":"number"},{"name":"truncated","required":true,"type":"boolean"}],"name":"CookieList","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["values require the sensitive diagnostics environment gate"],"name":"storage.list","parameters":[{"maximumBytes":16,"name":"scope","required":false,"sensitive":false,"type":"string","values":["local","session","all"]},{"name":"includeValues","required":false,"sensitive":false,"type":"boolean"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"origin","required":true,"type":"string"},{"name":"stores","required":true,"type":"array"}],"name":"StorageList","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"visual.compare","parameters":[{"maximumBytes":128,"name":"before","required":true,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"after","required":true,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"name","required":true,"type":"string"},{"name":"changedPixels","required":false,"type":"number"},{"name":"differenceRatio","required":false,"type":"number"}],"name":"VisualComparison","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"performance.get","parameters":[],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"timing","required":true,"type":"json"},{"name":"webVitals","required":true,"type":"object"},{"name":"resources","required":true,"type":"object"}],"name":"Performance","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"animation.list","parameters":[],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"count","required":true,"type":"number"},{"name":"animations","required":true,"type":"array"},{"name":"truncated","required":true,"type":"boolean"}],"name":"AnimationList","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"report.create","parameters":[{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"name","required":true,"type":"string"},{"name":"path","required":true,"type":"string"},{"name":"kind","required":true,"type":"string"},{"name":"bytes","required":true,"type":"number"},{"name":"createdAt","required":true,"type":"number"}],"name":"Artifact","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"flow.start","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"recording","required":true,"type":"boolean"},{"name":"note","required":true,"type":"string"}],"name":"FlowStart","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"flow.stop","parameters":[{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"name","required":true,"type":"string"},{"name":"path","required":true,"type":"string"},{"name":"kind","required":true,"type":"string"},{"name":"bytes","required":true,"type":"number"},{"name":"createdAt","required":true,"type":"number"}],"name":"Artifact","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"flow.run","parameters":[{"maximumBytes":128,"name":"input","required":true,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"completed","required":true,"type":"number"},{"name":"input","required":true,"type":"string"}],"name":"FlowRun","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":125000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":[],"name":"network.emulate","parameters":[{"name":"offline","required":false,"sensitive":false,"type":"boolean"},{"maximum":120000,"minimum":0,"name":"latencyMs","required":false,"sensitive":false,"type":"number"},{"maximum":1000000,"minimum":-1,"name":"downloadKbps","required":false,"sensitive":false,"type":"number"},{"maximum":1000000,"minimum":-1,"name":"uploadKbps","required":false,"sensitive":false,"type":"number"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"offline","required":true,"type":"boolean"},{"name":"latencyMs","required":true,"type":"number"},{"name":"downloadKbps","required":true,"type":"number"},{"name":"uploadKbps","required":true,"type":"number"},{"name":"engine","required":true,"type":"string"}],"name":"NetworkEmulation","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":[],"name":"network.mock.set","parameters":[{"maximumBytes":8192,"name":"url","required":true,"sensitive":false,"type":"string"},{"maximum":599,"minimum":100,"name":"status","required":false,"sensitive":false,"type":"number"},{"maximumBytes":65536,"name":"body","required":true,"sensitive":false,"type":"string"},{"maximumBytes":256,"name":"contentType","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"status","required":true,"type":"number"},{"name":"activeMocks","required":true,"type":"number"}],"name":"NetworkMock","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":[],"name":"network.mock.clear","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"cleared","required":true,"type":"number"}],"name":"NetworkMockClear","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":["choose interactive login or account alias","saved login requires a valid single-use challenge"],"name":"auth.login","parameters":[{"maximumBytes":64,"name":"challenge","required":false,"sensitive":false,"type":"string"},{"maximumBytes":64,"name":"account","required":false,"sensitive":false,"type":"string"},{"name":"interactive","required":false,"sensitive":false,"type":"boolean"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"origin","required":true,"type":"string"},{"name":"account","required":true,"type":"string-or-null"},{"name":"saved","required":true,"type":"boolean"},{"name":"continuation","required":true,"type":"string"},{"name":"passwordExposed","required":true,"type":"boolean"},{"name":"originalActionReplayed","required":true,"type":"boolean"}],"name":"AuthenticationLogin","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}}],"compatibility":{"mismatch":"host rejects request or client rejects response before decoding result","olderClient":"may call only commands and parameters declared by its bundled schema","olderHost":"missing commands or capabilities fail explicitly; clients must not emulate them","policy":"exact-wire-version","productVersionIndependent":true},"errorDetails":{"AUTH_REQUIRED":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"challenge","required":true,"type":"string"},{"name":"origin","required":true,"type":"string"},{"name":"detection","required":true,"type":"string"},{"name":"accounts","required":true,"type":"array"},{"name":"expiresInSeconds","required":true,"type":"number"},{"name":"userPresenceRequired","required":true,"type":"boolean"},{"name":"credentialUseAvailable","required":true,"type":"boolean"},{"name":"vaultAvailable","required":true,"type":"boolean"},{"name":"vaultStatus","required":true,"type":"string"},{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"originalActionReplayed","required":true,"type":"boolean"}],"name":"AuthenticationRequired","type":"object"}}},"format":"headless-sdk-contract","hostLifecycle":{"connect":"attach to an existing host and never assume ownership","existingHost":"supervised start fails rather than attaching","ownerExit":"host shuts down when the owner pipe closes","sharedStart":"headless start","shutdown":"only explicit user requests may stop a shared host","supervisedOwnerChannel":"open standard-input pipe","supervisedStart":"headless start --supervised"},"jsonSchemaDialect":"https://json-schema.org/draft/2020-12/schema","localCommands":["capabilities","config.describe","config.get","config.list","config.reset","config.set","credentials.add","credentials.list","credentials.remove","credentials.rename","help","runtime","schema","start","version"],"localErrorCodes":["CONFIGURATION_FAILED","HOST_START_FAILED","HOST_UNAVAILABLE","INVALID_CONFIGURATION","NAVIGATION_ALLOWLIST_CONFLICT","UNSUPPORTED_BROWSER_RUNTIME","UNSUPPORTED_CAPABILITY","VAULT_UNAVAILABLE"],"localLifecycle":{"connect":{"errors":["HOST_UNAVAILABLE"],"ownership":"shared","transport":"local-unix-socket"},"launch":{"argv":["start","--supervised"],"command":"start","errors":["HOST_START_FAILED","NAVIGATION_ALLOWLIST_CONFLICT","UNSUPPORTED_BROWSER_RUNTIME","UNSUPPORTED_CAPABILITY"],"options":[{"name":"presentation","required":false,"type":"string","values":["background","foreground"]},{"itemMaximumBytes":300,"maximumItems":32,"name":"allow","required":false,"type":"string-array"},{"const":true,"name":"supervised","required":true,"type":"boolean"}],"ownership":"owned-only-after-response-pid-matches-launched-child","result":{"additionalProperties":true,"fields":[{"name":"ready","required":true,"type":"boolean"},{"name":"pid","required":true,"type":"number"},{"name":"engine","required":true,"type":"string"},{"name":"platform","required":true,"type":"string"},{"name":"productVersion","required":true,"type":"string"},{"name":"protocolVersion","required":true,"type":"string"},{"name":"capabilities","required":true,"type":"object"},{"name":"recordingAvailable","required":true,"type":"boolean"},{"name":"artifactDirectory","required":true,"type":"string"},{"name":"navigationAllowlist","required":true,"type":"array"}],"name":"HostStatus","type":"object"}}},"maximumMessageBytes":1048576,"protocolVersion":"0.5","releasePolicy":{"preOneDeprecation":"at least one minor SDK release","provenance":"publish from reviewed tags with package attestations","schemaVersioning":"integer; generators reject unknown versions","sdkVersioning":"semantic versions independent of the wire version; combined product distributors track product tags","securityReporting":"SECURITY.md; never include secrets or private artifacts","stableRemoval":"major SDK release and migration notes"},"request":{"additionalProperties":false,"id":{"maximumBytes":128,"minimumBytes":1,"type":"string"},"parameters":{"additionalProperties":false,"type":"object"},"required":["id","version","command","parameters"],"session":{"maximumBytes":64,"type":["string","null"]},"type":"object","version":{"const":"0.5","type":"string"}},"response":{"additionalProperties":true,"failure":{"error":{"codes":["ARTIFACT_ERROR","AUTH_ACCOUNT_NOT_FOUND","AUTH_CHALLENGE_CONSUMED","AUTH_CHALLENGE_EXPIRED","AUTH_CHALLENGE_NOT_FOUND","AUTH_FORM_CHANGED","AUTH_ORIGIN_CHANGED","AUTH_REQUIRED","CREDENTIAL_ALIAS_EXISTS","ELEMENT_NOT_FOUND","FLOW_FAILED","HOST_STOPPING","HOST_UNAVAILABLE","INTERNAL_ERROR","INVALID_CAPTURE_FORMAT","INVALID_COMMAND","INVALID_FLOW","INVALID_INPUT","INVALID_REQUEST","INVALID_SESSION","MISSING_PARAMETER","OPERATION_FAILED","PAGINATION_CURSOR_EXPIRED","PAGINATION_CURSOR_INVALID","PAGINATION_CURSOR_SCOPE_MISMATCH","PAGINATION_CURSOR_STALE","PEER_DENIED","RECORDER_UNAVAILABLE","RECORDING_ACTIVE","RECORDING_FAILED","RECORDING_NOT_ACTIVE","REGION_NOT_FOUND","RESPONSE_TOO_LARGE","SENSITIVE_DIAGNOSTICS_DISABLED","SESSION_EXISTS","SESSION_NOT_FOUND","TIMEOUT","UNSAFE_NAVIGATION","UNSAFE_RESOURCE_TYPE","UNSUPPORTED_CAPABILITY","USER_PRESENCE_DENIED","USER_PRESENCE_UNAVAILABLE","VAULT_LOCKED","VAULT_OPERATION_FAILED","VAULT_RESPONSE_INVALID","VAULT_UNAVAILABLE"],"optional":["suggestion","details"],"required":["code","message"]},"ok":false,"result":null},"required":["id","version","ok"],"success":{"error":null,"ok":true,"result":"json"},"type":"object"},"schemaVersion":1,"security":{"arbitraryJavaScript":false,"credentialParameters":"aliases-and-challenge-identifiers-only","pageData":"untrusted","sensitiveDiagnostics":"requires-command-flag-and-host-environment-gate","tcpListener":false,"unknownRequestFields":"rejected","unknownResponseFields":"accepted for compatible additive evolution"},"supportWindow":{"hostPlatforms":["macOS 13 or newer","Linux amd64","Linux arm64"],"maintenance":"latest two minor SDK lines and at least 12 months after supersession, whichever is longer","pythonRuntimes":["CPython 3.11 through 3.14"],"schemaVersions":[1],"securityFixes":"supported SDK lines receive applicable security fixes","typescriptRuntimes":["Node.js 22 or newer"],"wireVersions":["0.5"]},"timeouts":{"connect":"fails before a request is sent","requestAfterWrite":"outcome unknown; never retry automatically","requestBeforeWrite":"safe to retry with a new request identifier","startup":"terminate and reap only the supervised launcher and host"},"transport":{"framing":"one-newline-terminated-json-document-per-connection","kind":"local-unix-socket","peerAuthorization":"same-os-user","runtimeDirectoryMode":"0700","socketMode":"0600","tcp":false}} +{"cancellation":{"afterSend":"close the client connection; command outcome is unknown until state is inspected","beforeSend":"request is not written","ownedHost":"terminate, await, and reap only a host process launched by that SDK instance","sharedHost":"never stop the host implicitly"},"commands":[{"capabilityNegotiated":false,"constraints":[],"name":"ping","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"ready","required":true,"type":"boolean"},{"name":"pid","required":true,"type":"number"},{"name":"engine","required":true,"type":"string"},{"name":"platform","required":true,"type":"string"},{"name":"productVersion","required":true,"type":"string"},{"name":"protocolVersion","required":true,"type":"string"},{"name":"capabilities","required":true,"type":"object"},{"name":"recordingAvailable","required":true,"type":"boolean"},{"name":"artifactDirectory","required":true,"type":"string"},{"name":"navigationAllowlist","required":true,"type":"array"}],"name":"HostStatus","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"shutdown","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"stopping","required":true,"type":"boolean"}],"name":"Shutdown","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"profile.clear","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"cleared","required":true,"type":"boolean"},{"name":"session","required":true,"type":"string"}],"name":"ProfileClear","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"session.create","parameters":[{"maximumBytes":64,"name":"name","required":true,"sensitive":false,"type":"string"},{"name":"isolated","required":false,"sensitive":false,"type":"boolean"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"session","required":true,"type":"string"},{"name":"isolated","required":true,"type":"boolean"}],"name":"SessionCreate","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"session.list","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"items":{"type":"string"},"name":"sessions","required":true,"type":"array"},{"items":{"additionalProperties":true,"fields":[{"name":"name","required":true,"type":"string"},{"name":"isolated","required":true,"type":"boolean"},{"name":"ageMs","required":true,"type":"number"},{"name":"status","required":true,"type":"string","values":["available","navigating","unavailable"]},{"name":"url","required":true,"type":"string-or-null"},{"name":"title","required":true,"type":"string-or-null"},{"name":"urlTruncated","required":true,"type":"boolean"},{"name":"titleTruncated","required":true,"type":"boolean"},{"name":"untrustedContent","required":true,"type":"boolean"}],"name":"SessionDetail","type":"object"},"name":"details","required":true,"type":"array"}],"name":"SessionList","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"session.close","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"closed","required":true,"type":"string"}],"name":"SessionClose","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"visit","parameters":[{"maximumBytes":8192,"name":"url","required":true,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"title","required":true,"type":"string"},{"name":"readyState","required":true,"type":"string"},{"name":"text","required":true,"type":"string"},{"name":"runningAnimations","required":true,"type":"number"},{"name":"mutationQuietMs","required":true,"type":"number"},{"name":"scrollY","required":true,"type":"number"},{"name":"contentHeight","required":true,"type":"number"}],"name":"PageState","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"inspect","parameters":[{"name":"interactive","required":false,"sensitive":false,"type":"boolean"},{"name":"text","required":false,"sensitive":false,"type":"boolean"},{"maximumBytes":16,"name":"context","required":false,"sensitive":false,"type":"string","values":["summary","outline","text","actions","full"]},{"maximumBytes":512,"name":"task","required":false,"sensitive":false,"type":"string"},{"maximumBytes":16,"name":"within","required":false,"sensitive":false,"type":"string"},{"maximum":250,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"integer"},{"maximum":16000,"minimum":256,"name":"budget","required":false,"sensitive":false,"type":"integer"},{"maximum":8,"minimum":0,"name":"depth","required":false,"sensitive":false,"type":"integer"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"title","required":true,"type":"string"},{"name":"contextMode","required":true,"type":"string"},{"name":"viewport","required":true,"type":"object"},{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"elements","required":false,"type":"array"},{"name":"regions","required":false,"type":"array"},{"name":"snippets","required":false,"type":"array"},{"name":"text","required":false,"type":"string"}],"name":"Inspection","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["exactly one target reference or semantic role/name target"],"name":"click","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"clicked","required":true,"type":"string"},{"name":"role","required":true,"type":"string"},{"name":"name","required":true,"type":"string"}],"name":"Click","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["exactly one target reference or semantic role/name target"],"name":"hover","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"hovered","required":true,"type":"string"},{"name":"role","required":true,"type":"string"},{"name":"name","required":true,"type":"string"}],"name":"Hover","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["exactly one target reference or semantic role/name target"],"name":"fill","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"},{"maximumBytes":900000,"name":"value","required":true,"sensitive":true,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"filled","required":true,"type":"string"},{"name":"valueLength","required":true,"type":"number"}],"name":"Fill","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["exactly one target reference or semantic role/name target","exactly one option label or value","native single-selection HTML select controls only"],"name":"select","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"label","required":false,"sensitive":true,"type":"string"},{"maximumBytes":1000,"name":"value","required":false,"sensitive":true,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"selected","required":true,"type":"string"},{"name":"role","required":true,"type":"string"},{"name":"name","required":true,"type":"string"},{"name":"optionIndex","required":true,"type":"number"}],"name":"Select","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":["artifact must be an existing private-store basename","exactly one target reference or semantic role/name target"],"name":"upload","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"artifact","required":true,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"uploaded","required":true,"type":"string"},{"name":"role","required":true,"type":"string"},{"name":"name","required":true,"type":"string"},{"name":"artifact","required":true,"type":"string"}],"name":"Upload","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"press","parameters":[{"maximumBytes":32,"name":"key","required":true,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"pressed","required":true,"type":"string"}],"name":"Press","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"scroll","parameters":[{"maximumBytes":8192,"name":"direction","required":false,"sensitive":false,"type":"string","values":["up","down","top","bottom"]},{"maximum":100000,"minimum":0.1,"name":"amount","required":false,"sensitive":false,"type":"number"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"direction","required":true,"type":"string"},{"name":"amount","required":true,"type":"number"}],"name":"Scroll","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"back","parameters":[],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"title","required":true,"type":"string"},{"name":"readyState","required":true,"type":"string"},{"name":"text","required":true,"type":"string"},{"name":"runningAnimations","required":true,"type":"number"},{"name":"mutationQuietMs","required":true,"type":"number"},{"name":"scrollY","required":true,"type":"number"},{"name":"contentHeight","required":true,"type":"number"}],"name":"PageState","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"reload","parameters":[],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"title","required":true,"type":"string"},{"name":"readyState","required":true,"type":"string"},{"name":"text","required":true,"type":"string"},{"name":"runningAnimations","required":true,"type":"number"},{"name":"mutationQuietMs","required":true,"type":"number"},{"name":"scrollY","required":true,"type":"number"},{"name":"contentHeight","required":true,"type":"number"}],"name":"PageState","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"wait","parameters":[{"name":"settled","required":false,"sensitive":false,"type":"boolean"},{"name":"networkIdle","required":false,"sensitive":false,"type":"boolean"},{"maximumBytes":8192,"name":"url","required":false,"sensitive":false,"type":"string"},{"maximumBytes":30000,"name":"text","required":false,"sensitive":false,"type":"string"},{"maximum":120000,"minimum":100,"name":"timeoutMs","required":false,"sensitive":false,"type":"number"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"title","required":true,"type":"string"},{"name":"readyState","required":true,"type":"string"},{"name":"text","required":true,"type":"string"},{"name":"runningAnimations","required":true,"type":"number"},{"name":"mutationQuietMs","required":true,"type":"number"},{"name":"scrollY","required":true,"type":"number"},{"name":"contentHeight","required":true,"type":"number"}],"name":"PageState","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"maximumMilliseconds":125000,"minimumMilliseconds":10000,"parameterGraceMilliseconds":5000,"parameterName":"timeoutMs","parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"tour","parameters":[{"name":"fullPage","required":false,"sensitive":false,"type":"boolean"},{"maximum":5000,"minimum":100,"name":"pace","required":false,"sensitive":false,"type":"number"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"start","required":true,"type":"number"},{"name":"end","required":true,"type":"number"},{"name":"durationMs","required":true,"type":"number"}],"name":"Tour","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":125000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"capture.info","parameters":[],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"engine","required":true,"type":"string"},{"name":"page","required":true,"type":"object"},{"name":"trace","required":true,"type":"array"},{"name":"recording","required":true,"type":"object"}],"name":"CaptureInfo","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":["target, full-page, and series modes are mutually constrained","region is required only for region series and must be an @rN reference","PDF requires full-page mode and no clipboard"],"name":"screenshot","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"},{"name":"fullPage","required":false,"sensitive":false,"type":"boolean"},{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"},{"maximumBytes":32,"name":"series","required":false,"sensitive":false,"type":"string","values":["viewport","section","region"]},{"maximumBytes":16,"name":"region","required":false,"sensitive":false,"type":"string"},{"maximumBytes":80,"name":"outputPrefix","required":false,"sensitive":false,"type":"string"},{"caseInsensitiveValues":true,"maximumBytes":16,"name":"format","required":false,"sensitive":false,"type":"string","values":["png","jpg","jpeg","pdf"]},{"name":"clipboard","required":false,"sensitive":false,"type":"boolean"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"name","required":false,"type":"string"},{"name":"path","required":false,"type":"string"},{"name":"kind","required":false,"type":"string"},{"name":"bytes","required":false,"type":"number"},{"name":"createdAt","required":false,"type":"number"},{"name":"artifacts","required":false,"type":"array"},{"name":"series","required":false,"type":"string","values":["viewport","section","region"]},{"name":"count","required":false,"type":"number"},{"name":"positions","required":false,"type":"array"},{"name":"truncated","required":false,"type":"boolean"},{"name":"totalPoints","required":false,"type":"number"},{"name":"untrustedContent","required":false,"type":"boolean"}],"name":"Screenshot","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":30000,"parameterPresentOverrides":{"series":125000}}},{"capabilityNegotiated":false,"constraints":[],"name":"artifact.list","parameters":[{"maximum":250,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"integer"},{"maximumBytes":64,"name":"cursor","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"directory","required":true,"type":"string"},{"name":"artifacts","required":true,"type":"array"},{"name":"returned","required":true,"type":"number"},{"name":"total","required":true,"type":"number"},{"name":"omitted","required":true,"type":"number"},{"name":"truncated","required":true,"type":"boolean"},{"name":"nextCursor","required":true,"type":"string-or-null"},{"name":"mutation","required":true,"type":"string","values":["none"]}],"name":"ArtifactList","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"record.start","parameters":[{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"},{"maximum":30,"minimum":1,"name":"fps","required":false,"sensitive":false,"type":"number"},{"caseInsensitiveValues":true,"maximumBytes":16,"name":"format","required":false,"sensitive":false,"type":"string","values":["mp4","mov","webm","gif"]},{"caseInsensitiveValues":true,"maximumBytes":16,"name":"quality","required":false,"sensitive":false,"type":"string","values":["fast","balanced","high"]}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"active","required":true,"type":"boolean"},{"name":"format","required":false,"type":"string"},{"name":"quality","required":false,"type":"string"},{"name":"name","required":false,"type":"string"}],"name":"Recording","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"record.status","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"active","required":true,"type":"boolean"},{"name":"format","required":false,"type":"string"},{"name":"quality","required":false,"type":"string"},{"name":"name","required":false,"type":"string"}],"name":"Recording","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"record.stop","parameters":[{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"active","required":true,"type":"boolean"},{"name":"format","required":false,"type":"string"},{"name":"quality","required":false,"type":"string"},{"name":"name","required":false,"type":"string"}],"name":"Recording","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":30000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"qa.report","parameters":[],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"summary","required":true,"type":"object"},{"name":"issues","required":true,"type":"array"},{"name":"events","required":true,"type":"array"},{"name":"omitted","required":true,"type":"object"},{"name":"truncated","required":true,"type":"boolean"}],"name":"QAReport","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"qa.clear","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"cleared","required":true,"type":"number"}],"name":"QAClear","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"console.list","parameters":[{"maximumBytes":16,"name":"level","required":false,"sensitive":false,"type":"string","values":["all","log","info","debug","warn","error","assert"]},{"maximum":250,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"integer"},{"maximumBytes":64,"name":"cursor","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"messages","required":true,"type":"array"},{"name":"returned","required":true,"type":"number"},{"name":"available","required":true,"type":"number"},{"name":"truncated","required":true,"type":"boolean"},{"name":"nextCursor","required":true,"type":"string-or-null"},{"name":"mutation","required":true,"type":"string","values":["none"]}],"name":"ConsoleList","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"network.list","parameters":[{"name":"failed","required":false,"sensitive":false,"type":"boolean"},{"maximum":599,"minimum":100,"name":"status","required":false,"sensitive":false,"type":"number"},{"maximum":250,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"integer"},{"maximumBytes":64,"name":"cursor","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"requests","required":true,"type":"array"},{"name":"returned","required":true,"type":"number"},{"name":"available","required":true,"type":"number"},{"name":"truncated","required":true,"type":"boolean"},{"name":"nextCursor","required":true,"type":"string-or-null"},{"name":"mutation","required":true,"type":"string","values":["none"]}],"name":"NetworkList","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"network.get","parameters":[{"maximumBytes":128,"name":"requestId","required":true,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"found","required":true,"type":"boolean"},{"name":"requestId","required":false,"type":"string"},{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"request","required":false,"type":"object"}],"name":"NetworkDetail","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["exactly one target reference or semantic role/name target"],"name":"styles.get","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"},{"itemMaximumBytes":128,"maximumItems":64,"name":"properties","required":false,"sensitive":false,"type":"string-array"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"ref","required":true,"type":"string"},{"name":"role","required":true,"type":"string"},{"name":"name","required":true,"type":"string"},{"name":"box","required":true,"type":"object"},{"name":"styles","required":true,"type":"object"}],"name":"Styles","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["values require the sensitive diagnostics environment gate"],"name":"cookies.list","parameters":[{"name":"includeValues","required":false,"sensitive":false,"type":"boolean"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"cookies","required":true,"type":"array"},{"name":"returned","required":true,"type":"number"},{"name":"available","required":true,"type":"number"},{"name":"truncated","required":true,"type":"boolean"}],"name":"CookieList","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["values require the sensitive diagnostics environment gate"],"name":"storage.list","parameters":[{"maximumBytes":16,"name":"scope","required":false,"sensitive":false,"type":"string","values":["local","session","all"]},{"name":"includeValues","required":false,"sensitive":false,"type":"boolean"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"origin","required":true,"type":"string"},{"name":"stores","required":true,"type":"array"}],"name":"StorageList","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"visual.compare","parameters":[{"maximumBytes":128,"name":"before","required":true,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"after","required":true,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"name","required":true,"type":"string"},{"name":"changedPixels","required":false,"type":"number"},{"name":"differenceRatio","required":false,"type":"number"}],"name":"VisualComparison","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"performance.get","parameters":[],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"timing","required":true,"type":"json"},{"name":"webVitals","required":true,"type":"object"},{"name":"resources","required":true,"type":"object"}],"name":"Performance","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"animation.list","parameters":[],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"count","required":true,"type":"number"},{"name":"animations","required":true,"type":"array"},{"name":"truncated","required":true,"type":"boolean"}],"name":"AnimationList","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"report.create","parameters":[{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"name","required":true,"type":"string"},{"name":"path","required":true,"type":"string"},{"name":"kind","required":true,"type":"string"},{"name":"bytes","required":true,"type":"number"},{"name":"createdAt","required":true,"type":"number"}],"name":"Artifact","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"flow.start","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"recording","required":true,"type":"boolean"},{"name":"note","required":true,"type":"string"}],"name":"FlowStart","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"flow.stop","parameters":[{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"name","required":true,"type":"string"},{"name":"path","required":true,"type":"string"},{"name":"kind","required":true,"type":"string"},{"name":"bytes","required":true,"type":"number"},{"name":"createdAt","required":true,"type":"number"}],"name":"Artifact","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"flow.run","parameters":[{"maximumBytes":128,"name":"input","required":true,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"completed","required":true,"type":"number"},{"name":"input","required":true,"type":"string"}],"name":"FlowRun","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":125000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":[],"name":"network.emulate","parameters":[{"name":"offline","required":false,"sensitive":false,"type":"boolean"},{"maximum":120000,"minimum":0,"name":"latencyMs","required":false,"sensitive":false,"type":"number"},{"maximum":1000000,"minimum":-1,"name":"downloadKbps","required":false,"sensitive":false,"type":"number"},{"maximum":1000000,"minimum":-1,"name":"uploadKbps","required":false,"sensitive":false,"type":"number"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"offline","required":true,"type":"boolean"},{"name":"latencyMs","required":true,"type":"number"},{"name":"downloadKbps","required":true,"type":"number"},{"name":"uploadKbps","required":true,"type":"number"},{"name":"engine","required":true,"type":"string"}],"name":"NetworkEmulation","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":[],"name":"network.mock.set","parameters":[{"maximumBytes":8192,"name":"url","required":true,"sensitive":false,"type":"string"},{"maximum":599,"minimum":100,"name":"status","required":false,"sensitive":false,"type":"number"},{"maximumBytes":65536,"name":"body","required":true,"sensitive":false,"type":"string"},{"maximumBytes":256,"name":"contentType","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"status","required":true,"type":"number"},{"name":"activeMocks","required":true,"type":"number"}],"name":"NetworkMock","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":[],"name":"network.mock.clear","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"cleared","required":true,"type":"number"}],"name":"NetworkMockClear","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":["choose interactive login or account alias","saved login requires a valid single-use challenge"],"name":"auth.login","parameters":[{"maximumBytes":64,"name":"challenge","required":false,"sensitive":false,"type":"string"},{"maximumBytes":64,"name":"account","required":false,"sensitive":false,"type":"string"},{"name":"interactive","required":false,"sensitive":false,"type":"boolean"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"origin","required":true,"type":"string"},{"name":"account","required":true,"type":"string-or-null"},{"name":"saved","required":true,"type":"boolean"},{"name":"continuation","required":true,"type":"string"},{"name":"passwordExposed","required":true,"type":"boolean"},{"name":"originalActionReplayed","required":true,"type":"boolean"}],"name":"AuthenticationLogin","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}}],"compatibility":{"mismatch":"host rejects request or client rejects response before decoding result","olderClient":"may call only commands and parameters declared by its bundled schema","olderHost":"missing commands or capabilities fail explicitly; clients must not emulate them","policy":"exact-wire-version","productVersionIndependent":true},"errorDetails":{"AUTH_REQUIRED":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"challenge","required":true,"type":"string"},{"name":"origin","required":true,"type":"string"},{"name":"detection","required":true,"type":"string"},{"name":"accounts","required":true,"type":"array"},{"name":"expiresInSeconds","required":true,"type":"number"},{"name":"userPresenceRequired","required":true,"type":"boolean"},{"name":"credentialUseAvailable","required":true,"type":"boolean"},{"name":"vaultAvailable","required":true,"type":"boolean"},{"name":"vaultStatus","required":true,"type":"string"},{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"originalActionReplayed","required":true,"type":"boolean"}],"name":"AuthenticationRequired","type":"object"}}},"format":"headless-sdk-contract","hostLifecycle":{"connect":"attach to an existing host and never assume ownership","existingHost":"supervised start fails rather than attaching","ownerExit":"host shuts down when the owner pipe closes","sharedStart":"headless start","shutdown":"only explicit user requests may stop a shared host","supervisedOwnerChannel":"open standard-input pipe","supervisedStart":"headless start --supervised"},"jsonSchemaDialect":"https://json-schema.org/draft/2020-12/schema","localCommands":["capabilities","config.describe","config.get","config.list","config.reset","config.set","credentials.add","credentials.list","credentials.remove","credentials.rename","help","runtime","schema","start","version"],"localErrorCodes":["CONFIGURATION_FAILED","HOST_START_FAILED","HOST_UNAVAILABLE","INVALID_CONFIGURATION","NAVIGATION_ALLOWLIST_CONFLICT","UNSUPPORTED_BROWSER_RUNTIME","UNSUPPORTED_CAPABILITY","VAULT_UNAVAILABLE"],"localLifecycle":{"connect":{"errors":["HOST_UNAVAILABLE"],"ownership":"shared","transport":"local-unix-socket"},"launch":{"argv":["start","--supervised"],"command":"start","errors":["HOST_START_FAILED","NAVIGATION_ALLOWLIST_CONFLICT","UNSUPPORTED_BROWSER_RUNTIME","UNSUPPORTED_CAPABILITY"],"options":[{"name":"presentation","required":false,"type":"string","values":["background","foreground"]},{"itemMaximumBytes":300,"maximumItems":32,"name":"allow","required":false,"type":"string-array"},{"const":true,"name":"supervised","required":true,"type":"boolean"}],"ownership":"owned-only-after-response-pid-matches-launched-child","result":{"additionalProperties":true,"fields":[{"name":"ready","required":true,"type":"boolean"},{"name":"pid","required":true,"type":"number"},{"name":"engine","required":true,"type":"string"},{"name":"platform","required":true,"type":"string"},{"name":"productVersion","required":true,"type":"string"},{"name":"protocolVersion","required":true,"type":"string"},{"name":"capabilities","required":true,"type":"object"},{"name":"recordingAvailable","required":true,"type":"boolean"},{"name":"artifactDirectory","required":true,"type":"string"},{"name":"navigationAllowlist","required":true,"type":"array"}],"name":"HostStatus","type":"object"}}},"maximumMessageBytes":1048576,"protocolVersion":"0.5","releasePolicy":{"preOneDeprecation":"at least one minor SDK release","provenance":"publish from reviewed tags with package attestations","schemaVersioning":"integer; generators reject unknown versions","sdkVersioning":"semantic versions independent of the wire version; combined product distributors track product tags","securityReporting":"SECURITY.md; never include secrets or private artifacts","stableRemoval":"major SDK release and migration notes"},"request":{"additionalProperties":false,"id":{"maximumBytes":128,"minimumBytes":1,"type":"string"},"parameters":{"additionalProperties":false,"type":"object"},"required":["id","version","command","parameters"],"session":{"maximumBytes":64,"type":["string","null"]},"type":"object","version":{"const":"0.5","type":"string"}},"response":{"additionalProperties":true,"failure":{"error":{"codes":["ARTIFACT_ERROR","AUTH_ACCOUNT_NOT_FOUND","AUTH_CHALLENGE_CONSUMED","AUTH_CHALLENGE_EXPIRED","AUTH_CHALLENGE_NOT_FOUND","AUTH_FORM_CHANGED","AUTH_ORIGIN_CHANGED","AUTH_REQUIRED","CREDENTIAL_ALIAS_EXISTS","ELEMENT_NOT_FOUND","FLOW_FAILED","HOST_STOPPING","HOST_UNAVAILABLE","INTERNAL_ERROR","INVALID_CAPTURE_FORMAT","INVALID_COMMAND","INVALID_FLOW","INVALID_INPUT","INVALID_REQUEST","INVALID_SESSION","MISSING_PARAMETER","OPERATION_FAILED","PAGINATION_CURSOR_EXPIRED","PAGINATION_CURSOR_INVALID","PAGINATION_CURSOR_SCOPE_MISMATCH","PAGINATION_CURSOR_STALE","PEER_DENIED","RECORDER_UNAVAILABLE","RECORDING_ACTIVE","RECORDING_FAILED","RECORDING_NOT_ACTIVE","REGION_NOT_FOUND","RESPONSE_TOO_LARGE","SENSITIVE_DIAGNOSTICS_DISABLED","SESSION_EXISTS","SESSION_NOT_FOUND","TIMEOUT","UNSAFE_NAVIGATION","UNSAFE_RESOURCE_TYPE","UNSUPPORTED_CAPABILITY","USER_PRESENCE_DENIED","USER_PRESENCE_UNAVAILABLE","VAULT_LOCKED","VAULT_OPERATION_FAILED","VAULT_RESPONSE_INVALID","VAULT_UNAVAILABLE"],"optional":["suggestion","details"],"required":["code","message"]},"ok":false,"result":null},"required":["id","version","ok"],"success":{"error":null,"ok":true,"result":"json"},"type":"object"},"schemaVersion":1,"security":{"arbitraryJavaScript":false,"credentialParameters":"aliases-and-challenge-identifiers-only","pageData":"untrusted","sensitiveDiagnostics":"requires-command-flag-and-host-environment-gate","tcpListener":false,"unknownRequestFields":"rejected","unknownResponseFields":"accepted for compatible additive evolution"},"supportWindow":{"hostPlatforms":["macOS 13 or newer","Linux amd64","Linux arm64"],"maintenance":"latest two minor SDK lines and at least 12 months after supersession, whichever is longer","pythonRuntimes":["CPython 3.11 through 3.14"],"schemaVersions":[1],"securityFixes":"supported SDK lines receive applicable security fixes","typescriptRuntimes":["Node.js 22 or newer"],"wireVersions":["0.5"]},"timeouts":{"connect":"fails before a request is sent","requestAfterWrite":"outcome unknown; never retry automatically","requestBeforeWrite":"safe to retry with a new request identifier","startup":"terminate and reap only the supervised launcher and host"},"transport":{"framing":"one-newline-terminated-json-document-per-connection","kind":"local-unix-socket","peerAuthorization":"same-os-user","runtimeDirectoryMode":"0700","socketMode":"0600","tcp":false}} From c64c2ff005fe73664fb142ea0bed64e461c30451 Mon Sep 17 00:00:00 2001 From: Aditya Garud <153842990+yashranaway@users.noreply.github.com> Date: Tue, 22 Sep 2026 18:53:32 +0000 Subject: [PATCH 2/3] fix: revalidate hover before dispatch and keep it out of flows Share one post-scroll hit test for the synthetic and CDP paths, reject a covered target before any pointer event, and pause recording when a hover handler navigates. Record why hover is not replayed in a flow. --- apps/headless/LinuxHost/BrowserProcess.swift | 3 ++ .../Resources/AgentRuntime.js | 35 +++++++++-------- apps/headless/Tests/agent-runtime.test.mjs | 23 +++++++++++ apps/headless/docs/COMMANDS.md | 7 +++- apps/headless/docs/P2.md | 24 +++++++----- docs/roadmap/architecture-decisions.md | 38 ++++++++++++------- 6 files changed, 90 insertions(+), 40 deletions(-) diff --git a/apps/headless/LinuxHost/BrowserProcess.swift b/apps/headless/LinuxHost/BrowserProcess.swift index b719409..42690ef 100644 --- a/apps/headless/LinuxHost/BrowserProcess.swift +++ b/apps/headless/LinuxHost/BrowserProcess.swift @@ -778,6 +778,9 @@ final class LinuxBrowserSession: @unchecked Sendable { _ = try command("Input.dispatchMouseEvent", parameters: [ "type": "mouseMoved", "x": target.x, "y": target.y, ]) + // A hover handler can start a cross-document navigation before the next + // command. Pause capture so a recording does not composite that frame. + pauseRecordingCapture() return .object([ "hovered": .string(target.reference), "role": .string(target.role), diff --git a/apps/headless/Sources/HeadlessProtocol/Resources/AgentRuntime.js b/apps/headless/Sources/HeadlessProtocol/Resources/AgentRuntime.js index a7b72b4..258ccca 100644 --- a/apps/headless/Sources/HeadlessProtocol/Resources/AgentRuntime.js +++ b/apps/headless/Sources/HeadlessProtocol/Resources/AgentRuntime.js @@ -707,9 +707,8 @@ if (!globalThis.__headlessAgent) { element.click(); return {clicked: refFor(element), role: role(element), name: name(element)}; }; - const hover = args => { - const element = target(args); - element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); + // Hit-test the layout after scroll, before any pointer event. + const pointerHit = element => { if (!visible(element)) fail('ELEMENT_NOT_FOUND', 'ELEMENT_NOT_VISIBLE'); const rect = element.getBoundingClientRect(); const left = Math.max(0, rect.left); @@ -723,10 +722,16 @@ if (!globalThis.__headlessAgent) { if (!hit || (hit !== element && !element.contains(hit))) { fail('ELEMENT_NOT_FOUND', 'ELEMENT_OBSCURED'); } + return {x, y}; + }; + const hover = args => { + const element = target(args); + element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); + const point = pointerHit(element); const Pointer = typeof PointerEvent === 'function' ? PointerEvent : MouseEvent; - const pointer = {bubbles: true, clientX: x, clientY: y, pointerId: 1, + const pointer = {bubbles: true, clientX: point.x, clientY: point.y, pointerId: 1, pointerType: 'mouse', isPrimary: true}; - const mouse = {bubbles: true, clientX: x, clientY: y}; + const mouse = {bubbles: true, clientX: point.x, clientY: point.y}; const previous = hoveredElement instanceof Element && hoveredElement.isConnected ? hoveredElement : null; hoveredElement = element; @@ -765,26 +770,24 @@ if (!globalThis.__headlessAgent) { if (!editable || element.disabled || element.readOnly) throw new Error('NOT_EDITABLE'); } element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); - if (action !== 'hover') element.focus({preventScroll: true}); - if (action === 'hover' && !visible(element)) { - fail('ELEMENT_NOT_FOUND', 'ELEMENT_NOT_VISIBLE'); + if (action === 'hover') { + const point = pointerHit(element); + return { + ref: refFor(element), role: role(element), name: name(element), + x: point.x, y: point.y, + }; } + element.focus({preventScroll: true}); const rect = element.getBoundingClientRect(); const left = Math.max(0, rect.left); const right = Math.min(innerWidth, rect.right); const top = Math.max(0, rect.top); const bottom = Math.min(innerHeight, rect.bottom); - if (right <= left || bottom <= top) { - if (action === 'hover') fail('ELEMENT_NOT_FOUND', 'ELEMENT_NOT_VISIBLE'); - throw new Error('ELEMENT_NOT_VISIBLE'); - } + if (right <= left || bottom <= top) throw new Error('ELEMENT_NOT_VISIBLE'); const x = left + (right - left) / 2; const y = top + (bottom - top) / 2; const hit = document.elementFromPoint(x, y); - if (!hit || (hit !== element && !element.contains(hit))) { - if (action === 'hover') fail('ELEMENT_NOT_FOUND', 'ELEMENT_OBSCURED'); - throw new Error('ELEMENT_OBSCURED'); - } + if (!hit || (hit !== element && !element.contains(hit))) throw new Error('ELEMENT_OBSCURED'); return {ref: refFor(element), role: role(element), name: name(element), x, y}; }; const checkedFileInput = element => { diff --git a/apps/headless/Tests/agent-runtime.test.mjs b/apps/headless/Tests/agent-runtime.test.mjs index fe87d4d..f44a265 100644 --- a/apps/headless/Tests/agent-runtime.test.mjs +++ b/apps/headless/Tests/agent-runtime.test.mjs @@ -289,6 +289,29 @@ assert.throws( error => error.headlessCode === 'ELEMENT_NOT_FOUND' && error.message === 'ELEMENT_NOT_VISIBLE', ); secondButton.style.opacity = '1'; +window.document.elementFromPoint = () => window.document.body; +const coveredEvents = secondHoverEvents.length; +assert.throws( + () => agent.hover({target: hiddenHoverRef}), + error => error.headlessCode === 'ELEMENT_NOT_FOUND' && error.message === 'ELEMENT_OBSCURED', +); +assert.throws( + () => agent.inputTarget({target: hiddenHoverRef}, 'hover'), + error => error.headlessCode === 'ELEMENT_NOT_FOUND' && error.message === 'ELEMENT_OBSCURED', +); +assert.equal(secondHoverEvents.length, coveredEvents, 'obscured hover must not dispatch'); +secondButton.remove(); +assert.throws( + () => agent.hover({target: hiddenHoverRef}), + error => error.headlessCode === 'ELEMENT_NOT_FOUND' && /detached/.test(error.message), +); +const staleHoverRef = agent.snapshot(false, false, {context: 'actions', limit: 20}) + .elements.find(element => element.name === 'Runtime action').ref; +agent.snapshot(false, false, {context: 'text', limit: 1}); +assert.throws( + () => agent.hover({target: staleHoverRef}), + error => error.headlessCode === 'ELEMENT_NOT_FOUND' && /expired/.test(error.message), +); window.document.elementFromPoint = () => button; const clicked = agent.click({role: 'button', name: 'Runtime action'}); diff --git a/apps/headless/docs/COMMANDS.md b/apps/headless/docs/COMMANDS.md index 6525d90..0572876 100644 --- a/apps/headless/docs/COMMANDS.md +++ b/apps/headless/docs/COMMANDS.md @@ -244,8 +244,11 @@ back | reload semantic target (`--role`/`--name`). `fill` accepts a reference; `press` acts on the focused control. On Linux click, fill, and key input dispatch trusted CDP events; WebKit uses synthetic input. Hover does not focus or click its - target and returns no coordinates. Native select and hover dispatch are - declared separately by capabilities. + target and returns no coordinates. Before dispatch it resolves a ref from the + latest inspection only, then rejects a stale, detached, hidden, or + center-covered target. Hover is not a flow step: a flow replays commands, and + a later replay would not reproduce a transient pointer state. Native select + and hover dispatch are declared separately by capabilities. - `fill REF -- value` keeps leading dashes in the value. Flow recordings never record fill values. - `select` supports native single-selection HTML controls only. Choose exactly diff --git a/apps/headless/docs/P2.md b/apps/headless/docs/P2.md index 6ac2b67..263af3d 100644 --- a/apps/headless/docs/P2.md +++ b/apps/headless/docs/P2.md @@ -91,15 +91,21 @@ operation on both engines. The capability matrix reports this separately as `selectDispatch: synthetic-dom`, so Chromium's `inputDispatch: trusted-cdp` claim remains limited to click, fill, and key input. -`hover` accepts the same ref or role/name target grammar as `click`. Chromium -resolves and hit-tests the target in the isolated world, then dispatches one -trusted CDP mouse move. WebKit emits one fixed synthetic pointer/mouse hover -transition sequence but cannot activate CSS `:hover`. `hoverDispatch` declares -the difference. Neither path focuses, clicks, returns coordinates, accepts -selectors, or exposes caller JavaScript. Page hover handlers may navigate or -cause other page side effects under the normal browser safety policies. Hover -is not replayed in flows and is not advertised in action hints because -meaningful hover behavior cannot be inferred from markup. +`hover` accepts the same ref or role/name target grammar as `click`. A ref +resolves only from the latest inspection, so a later inspect or document +refresh expires it. After scrolling the target into view, both engines reject +a detached, hidden, or center-covered target before any pointer event. +Chromium then dispatches one trusted CDP mouse move. WebKit emits one fixed +synthetic pointer/mouse hover transition sequence but cannot activate CSS +`:hover`. `hoverDispatch` declares the difference. Neither path focuses, +clicks, returns coordinates, accepts selectors, or exposes caller JavaScript. +Page hover handlers may navigate or cause other page side effects under the +normal browser safety policies. Hover is not a flow step. A flow replays +commands later, and a replayed hover would not restore the pointer or, on +WebKit, CSS `:hover`. Screenshot series store images from the capture that +just ran. Those images are not commands, so a series is not replayed either. +Inspection does not advertise hover because markup cannot prove that hovering +has meaningful behavior. The Linux host accepts only a qualified non-Snap runtime for this CDP pipe. Automatic selection prefers an installed bundled runtime and native browser diff --git a/docs/roadmap/architecture-decisions.md b/docs/roadmap/architecture-decisions.md index dc1bc96..70cd9d5 100644 --- a/docs/roadmap/architecture-decisions.md +++ b/docs/roadmap/architecture-decisions.md @@ -1171,21 +1171,33 @@ a narrower region instead of yielding partial or misleading evidence. ## 37. Hover is semantic, bounded, and dispatch-accurate **Decision:** protocol `0.5` gains one compatible portable `hover` command with -the same fresh element-ref or exact role/name target grammar as `click`. The -isolated runtime scrolls the target to the viewport and rejects missing, stale, hidden, -detached, or center-point-obscured targets before dispatch. The public response -contains only the bounded target ref, role, and name. It never returns the -internal hit-test coordinates. - -Chromium performs one trusted `Input.dispatchMouseEvent` mouse move after the -isolated-world target check. WebKit tracks the previous synthetic target and -emits fixed pointer/mouse leave, enter, and move transitions. The capability -matrix reports `hoverDispatch` as `trusted-cdp` or `synthetic-dom`. The command -never focuses or clicks directly, accepts selectors or coordinates, or exposes +the same fresh element-ref or exact role/name target grammar as `click`. A ref +resolves only against the latest inspection. A later inspect or a document +refresh expires it, and a detached node fails closed. After scrolling the +target into view, both engines reject a hidden target and a target whose +viewport center is covered by another element, including a dropdown or other +overlay, before any pointer event. The public response contains only the +bounded target ref, role, and name. It never returns the internal hit-test +coordinates. + +Chromium performs one trusted `Input.dispatchMouseEvent` mouse move after that +check. WebKit tracks the previous synthetic target and emits fixed +pointer/mouse leave, enter, and move transitions. The capability matrix +reports `hoverDispatch` as `trusted-cdp` or `synthetic-dom`. The command never +focuses or clicks directly, accepts selectors or coordinates, or exposes caller JavaScript. Page-controlled hover handlers can still navigate or cause other page side effects, subject to the existing navigation and download -policies. Hover is not a replayable flow step. Inspection does not advertise -hover because markup cannot prove that hovering has meaningful behavior. +policies. + +Hover is not a flow step. A flow replays commands later. Hover only describes +where the pointer is now, and WebKit's synthetic sequence does not activate +CSS `:hover`, so a replay would not reproduce the revealed surface. Screenshot +series are saved images from one capture. They are not replayed commands, so +they are not flow steps either. Hover follows that rule. A hover that reveals +a login form still returns `AUTH_REQUIRED` through the existing challenge +path, and the hover itself is not stored for replay. Inspection does not +advertise hover because markup cannot prove that hovering has meaningful +behavior. Generic drag remains deferred to [#208](https://github.com/LockInTime/headless/issues/208). That contract must From ca431afead4e2dc8de4553b8d3f89981ff253bd7 Mon Sep 17 00:00:00 2001 From: Aditya Garud <153842990+yashranaway@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:10:14 +0000 Subject: [PATCH 3/3] fix(python): keep protocol validation under the statement limit The hover target checks and the region screenshot checks overflowed one function once both landed. Split them into helpers. --- .../src/headless_sdk/_protocol.py | 119 +++++++++--------- 1 file changed, 63 insertions(+), 56 deletions(-) diff --git a/packages/headless-python/src/headless_sdk/_protocol.py b/packages/headless-python/src/headless_sdk/_protocol.py index 41a6f3c..dc0cccd 100644 --- a/packages/headless-python/src/headless_sdk/_protocol.py +++ b/packages/headless-python/src/headless_sdk/_protocol.py @@ -136,24 +136,9 @@ def _validate_parameter(command: str, definition: Mapping[str, Any], value: obje raise ValidationError(f"unsupported generated parameter type for {label}") -def validate_parameters(command: CommandName, parameters: Mapping[str, object]) -> None: - metadata = COMMAND_METADATA.get(command) - if metadata is None: - raise ValidationError(f"unknown Headless command: {command}") - if not isinstance(parameters, dict): - raise ValidationError(f"{command} parameters must be a plain dictionary") - definitions = cast(list[dict[str, Any]], metadata["parameters"]) - known = {cast(str, definition["name"]): definition for definition in definitions} - unknown = next((key for key in parameters if key not in known), None) - if unknown is not None: - raise ValidationError(f"{command} received unknown parameter {unknown}") - for name, definition in known.items(): - if name not in parameters: - if definition["required"] is True: - raise ValidationError(f"{command} requires {name}") - else: - _validate_parameter(command, definition, parameters[name]) - constraints = cast(list[str], metadata["constraints"]) +def _validate_constraint_unions( + command: str, parameters: Mapping[str, object], constraints: list[str] +) -> None: if "exactly one target reference or semantic role/name target" in constraints: target_present = "target" in parameters role_present = "role" in parameters @@ -175,45 +160,67 @@ def validate_parameters(command: CommandName, parameters: Mapping[str, object]) value_present and parameters["value"] == "" ): raise ValidationError(f"{command} option matchers must not be empty") + + +def _validate_screenshot(parameters: Mapping[str, object]) -> None: + target_present = "target" in parameters + semantic_target_present = "role" in parameters or "name" in parameters + if target_present and semantic_target_present: + raise ValidationError("screenshot element targets are mutually exclusive") + if target_present and re.fullmatch(r"@e\d+", cast(str, parameters["target"])) is None: + raise ValidationError("screenshot.target must be an element reference") + if parameters.get("role") == "" or parameters.get("name") == "": + raise ValidationError("screenshot semantic target values must not be empty") + has_target = target_present or semantic_target_present + series = parameters.get("series") + region_present = "region" in parameters + if region_present and re.fullmatch(r"@r\d+", cast(str, parameters["region"])) is None: + raise ValidationError("screenshot.region must be a region reference") + if (series == "region") != region_present: + raise ValidationError("region screenshot series requires exactly one region reference") + if series is not None and ( + parameters.get("fullPage") is True or has_target or "output" in parameters + ): + raise ValidationError( + "screenshot series conflicts with full-page, output, or element target" + ) + if parameters.get("fullPage") is True and has_target: + raise ValidationError("full-page and element screenshots are mutually exclusive") + if "outputPrefix" in parameters and series is None: + raise ValidationError("screenshot.outputPrefix requires a screenshot series") + explicit_format = cast(str, parameters.get("format", "")).lower() + output = cast(str, parameters.get("output", "")).lower() + pdf = explicit_format == "pdf" or (not explicit_format and output.endswith(".pdf")) + if series is not None and (pdf or parameters.get("clipboard") is True): + raise ValidationError("screenshot series does not support PDF or clipboard output") + if pdf and ( + parameters.get("fullPage") is not True or has_target or parameters.get("clipboard") is True + ): + raise ValidationError( + "PDF screenshots require full-page mode without a target or clipboard" + ) + + +def validate_parameters(command: CommandName, parameters: Mapping[str, object]) -> None: + metadata = COMMAND_METADATA.get(command) + if metadata is None: + raise ValidationError(f"unknown Headless command: {command}") + if not isinstance(parameters, dict): + raise ValidationError(f"{command} parameters must be a plain dictionary") + definitions = cast(list[dict[str, Any]], metadata["parameters"]) + known = {cast(str, definition["name"]): definition for definition in definitions} + unknown = next((key for key in parameters if key not in known), None) + if unknown is not None: + raise ValidationError(f"{command} received unknown parameter {unknown}") + for name, definition in known.items(): + if name not in parameters: + if definition["required"] is True: + raise ValidationError(f"{command} requires {name}") + else: + _validate_parameter(command, definition, parameters[name]) + _validate_constraint_unions(command, parameters, cast(list[str], metadata["constraints"])) if command == "screenshot": - target_present = "target" in parameters - semantic_target_present = "role" in parameters or "name" in parameters - if target_present and semantic_target_present: - raise ValidationError("screenshot element targets are mutually exclusive") - if target_present and re.fullmatch(r"@e\d+", cast(str, parameters["target"])) is None: - raise ValidationError("screenshot.target must be an element reference") - if parameters.get("role") == "" or parameters.get("name") == "": - raise ValidationError("screenshot semantic target values must not be empty") - has_target = target_present or semantic_target_present - series = parameters.get("series") - region_present = "region" in parameters - if region_present and re.fullmatch(r"@r\d+", cast(str, parameters["region"])) is None: - raise ValidationError("screenshot.region must be a region reference") - if (series == "region") != region_present: - raise ValidationError("region screenshot series requires exactly one region reference") - if series is not None and ( - parameters.get("fullPage") is True or has_target or "output" in parameters - ): - raise ValidationError( - "screenshot series conflicts with full-page, output, or element target" - ) - if parameters.get("fullPage") is True and has_target: - raise ValidationError("full-page and element screenshots are mutually exclusive") - if "outputPrefix" in parameters and series is None: - raise ValidationError("screenshot.outputPrefix requires a screenshot series") - explicit_format = cast(str, parameters.get("format", "")).lower() - output = cast(str, parameters.get("output", "")).lower() - pdf = explicit_format == "pdf" or (not explicit_format and output.endswith(".pdf")) - if series is not None and (pdf or parameters.get("clipboard") is True): - raise ValidationError("screenshot series does not support PDF or clipboard output") - if pdf and ( - parameters.get("fullPage") is not True - or has_target - or parameters.get("clipboard") is True - ): - raise ValidationError( - "PDF screenshots require full-page mode without a target or clipboard" - ) + _validate_screenshot(parameters) if command == "session.create": validate_session(cast(str, parameters["name"]))