diff --git a/apps/headless/Host/AgentBridge.swift b/apps/headless/Host/AgentBridge.swift index debf6e4..2f4d6e5 100644 --- a/apps/headless/Host/AgentBridge.swift +++ b/apps/headless/Host/AgentBridge.swift @@ -519,6 +519,16 @@ final class WebKitBrowserEngine: BrowserEngine { extension BrowserWindowController: BrowserEngineSession { var hostIsolated: Bool { isIsolatedSession } + func hostSessionMetadata() throws -> BrowserSessionPageMetadata { + onMain { + BrowserSessionPageMetadata( + url: self.webView.url?.absoluteString, + title: self.webView.title, + lifecycle: self.webView.isLoading ? .navigating : .available + ) + } + } + func hostEnableAgentControl() { onMain { self.enableAgentControl() } } func hostVisit(_ url: URL) throws -> JSONValue { try agentVisit(url) } func hostInspect(parameters: [String: JSONValue]) throws -> JSONValue { diff --git a/apps/headless/LinuxHost/BrowserProcess.swift b/apps/headless/LinuxHost/BrowserProcess.swift index 72b0f99..ad5e299 100644 --- a/apps/headless/LinuxHost/BrowserProcess.swift +++ b/apps/headless/LinuxHost/BrowserProcess.swift @@ -616,6 +616,7 @@ final class LinuxBrowserSession: @unchecked Sendable { private var mainFrameID: String? private var lastSafeURL: String? private var navigationRecoveryPending = false + private var navigationInProgress = false private var isolatedContextID: Int? private let mockLock = NSLock() private var networkMocks: [NetworkMock] = [] @@ -671,12 +672,30 @@ final class LinuxBrowserSession: @unchecked Sendable { } func visit(_ url: URL) throws -> JSONValue { - navigationLock.lock(); lastSafeURL = url.absoluteString; navigationLock.unlock() + navigationLock.lock() + lastSafeURL = url.absoluteString + navigationInProgress = true + navigationLock.unlock() pauseRecordingCapture() _ = try command("Page.navigate", parameters: ["url": url.absoluteString]) return try wait(parameters: ["settled": .bool(true), "timeoutMs": .number(20_000)]) } + func sessionMetadata() throws -> BrowserSessionPageMetadata { + let history = try command("Page.getNavigationHistory", timeoutMilliseconds: 2_000) + let currentIndex = (history["currentIndex"] as? NSNumber)?.intValue ?? -1 + let entries = history["entries"] as? [[String: Any]] ?? [] + let entry = entries.indices.contains(currentIndex) ? entries[currentIndex] : nil + navigationLock.lock() + let navigating = navigationInProgress + navigationLock.unlock() + return BrowserSessionPageMetadata( + url: entry?["url"] as? String, + title: entry?["title"] as? String, + lifecycle: navigating ? .navigating : .available + ) + } + func inspect(parameters: [String: JSONValue]) throws -> JSONValue { var options: [String: Any] = [ "context": parameters["context"]?.stringValue ?? "full", @@ -1106,6 +1125,22 @@ final class LinuxBrowserSession: @unchecked Sendable { } case "Page.frameStartedLoading": pauseRecordingCapture() + if let frameID = parameters["frameId"] as? String { + navigationLock.lock() + if mainFrameID == nil { mainFrameID = frameID } + if mainFrameID == frameID { navigationInProgress = true } + navigationLock.unlock() + } + case "Page.frameStoppedLoading": + if let frameID = parameters["frameId"] as? String { + navigationLock.lock() + if mainFrameID == frameID { navigationInProgress = false } + navigationLock.unlock() + } + case "Page.loadEventFired": + navigationLock.lock() + navigationInProgress = false + navigationLock.unlock() case "Page.frameRequestedNavigation": pauseRecordingCapture() if let frameID = parameters["frameId"] as? String, diff --git a/apps/headless/LinuxHost/main.swift b/apps/headless/LinuxHost/main.swift index 1847564..c1d47cf 100644 --- a/apps/headless/LinuxHost/main.swift +++ b/apps/headless/LinuxHost/main.swift @@ -77,6 +77,10 @@ final class ChromiumBrowserEngineSession: BrowserEngineSession { var hostIsolated: Bool { browserSession.isIsolated } + func hostSessionMetadata() throws -> BrowserSessionPageMetadata { + try browserSession.sessionMetadata() + } + func hostVisit(_ url: URL) throws -> JSONValue { try browserSession.visit(url) } func hostInspect(parameters: [String: JSONValue]) throws -> JSONValue { try browserSession.inspect(parameters: parameters) diff --git a/apps/headless/Sources/HeadlessProtocol/HostCore.swift b/apps/headless/Sources/HeadlessProtocol/HostCore.swift index c2a304e..8cafa80 100644 --- a/apps/headless/Sources/HeadlessProtocol/HostCore.swift +++ b/apps/headless/Sources/HeadlessProtocol/HostCore.swift @@ -10,10 +10,28 @@ public struct BrowserScreenshot: Sendable { } } +public enum BrowserSessionLifecycle: String, Sendable { + case available + case navigating +} + +public struct BrowserSessionPageMetadata: Sendable { + public let url: String? + public let title: String? + public let lifecycle: BrowserSessionLifecycle + + public init(url: String?, title: String?, lifecycle: BrowserSessionLifecycle) { + self.url = url + self.title = title + self.lifecycle = lifecycle + } +} + /// The portable browser surface used by `HostCore`. Platform adapters keep /// WKWebView and CDP details out of the command dispatcher. public protocol BrowserEngineSession: AnyObject { var hostIsolated: Bool { get } + func hostSessionMetadata() throws -> BrowserSessionPageMetadata func hostEnableAgentControl() func hostVisit(_ url: URL) throws -> JSONValue func hostInspect(parameters: [String: JSONValue]) throws -> JSONValue @@ -143,14 +161,19 @@ public extension BrowserEngine { /// Shared command dispatcher and lifecycle state for every browser engine. /// New portable commands belong here exactly once. public final class HostCore: @unchecked Sendable { + private static var sessionURLMaximumBytes: Int { 8_192 } + private static var sessionTitleMaximumBytes: Int { 1_000 } + private let engine: Engine private let artifacts: ArtifactStore private let authenticationBroker: AuthenticationBroker private let authenticationChallenges: AuthenticationChallengeStore private let navigationAllowlist: NavigationAllowlist + private let monotonicNow: @Sendable () -> TimeInterval private let shutdownHandler: @Sendable () -> Void private let lock = NSLock() private var sessions: [String: Engine.Session] + private var sessionCreatedAt: [String: TimeInterval] private var trace: [String: [JSONValue]] = ["default": []] private var activeFlows: [String: [RecordedFlowStep]] = [:] private var recordings: [String: BrowserRecording] = [:] @@ -165,6 +188,9 @@ public final class HostCore: @unchecked Sendable { authenticationBroker: AuthenticationBroker = UnavailableAuthenticationBroker(), authenticationChallenges: AuthenticationChallengeStore = AuthenticationChallengeStore(), navigationAllowlist: NavigationAllowlist = processNavigationAllowlist, + monotonicNow: @escaping @Sendable () -> TimeInterval = { + ProcessInfo.processInfo.systemUptime + }, shutdownHandler: @escaping @Sendable () -> Void ) { self.engine = engine @@ -172,7 +198,9 @@ public final class HostCore: @unchecked Sendable { self.authenticationBroker = authenticationBroker self.authenticationChallenges = authenticationChallenges self.navigationAllowlist = navigationAllowlist + self.monotonicNow = monotonicNow self.sessions = ["default": defaultSession] + self.sessionCreatedAt = ["default": monotonicNow()] self.privateAuthenticationBrokers = defaultSession.hostIsolated ? ["default": EphemeralAuthenticationBroker()] : [:] self.shutdownHandler = shutdownHandler @@ -190,6 +218,7 @@ public final class HostCore: @unchecked Sendable { var stopped: [BrowserRecording] = [] for name in names { sessions.removeValue(forKey: name) + sessionCreatedAt.removeValue(forKey: name) trace.removeValue(forKey: name) activeFlows.removeValue(forKey: name) if let recording = recordings.removeValue(forKey: name) { stopped.append(recording) } @@ -211,6 +240,7 @@ public final class HostCore: @unchecked Sendable { let openSessions = Array(sessions.values) recordings.removeAll() sessions.removeAll() + sessionCreatedAt.removeAll() trace.removeAll() activeFlows.removeAll() privateAuthenticationBrokers.values.forEach { $0.removeAll() } @@ -241,17 +271,7 @@ public final class HostCore: @unchecked Sendable { case .sessionCreate: return try createSession(request) case .sessionList: - let listing = withState { () -> ([JSONValue], [JSONValue]) in - let ordered = sessions.sorted(by: { $0.key < $1.key }) - return ( - ordered.map { .string($0.key) }, - ordered.map { name, session in - .object([ - "name": .string(name), "isolated": .bool(session.hostIsolated), - ]) - } - ) - } + let listing = sessionListing() return .success( id: request.id, result: .object([ @@ -326,6 +346,7 @@ public final class HostCore: @unchecked Sendable { let openSessions = Array(sessions.values) recordings.removeAll() sessions.removeAll() + sessionCreatedAt.removeAll() trace.removeAll() activeFlows.removeAll() privateAuthenticationBrokers.values.forEach { $0.removeAll() } @@ -340,6 +361,7 @@ public final class HostCore: @unchecked Sendable { let replacement = try engine.createSession() withState { sessions["default"] = replacement + sessionCreatedAt["default"] = monotonicNow() trace["default"] = [] } return .success(id: request.id, result: .object([ @@ -350,6 +372,7 @@ public final class HostCore: @unchecked Sendable { if let replacement = try? engine.createSession() { withState { sessions["default"] = replacement + sessionCreatedAt["default"] = monotonicNow() trace["default"] = [] } } @@ -398,6 +421,7 @@ public final class HostCore: @unchecked Sendable { if stopping { return "HOST_UNAVAILABLE" } if sessions[name] != nil { return "SESSION_EXISTS" } sessions[name] = created + sessionCreatedAt[name] = monotonicNow() trace[name] = [] if created.hostIsolated { privateAuthenticationBrokers[name] = EphemeralAuthenticationBroker() @@ -423,6 +447,7 @@ public final class HostCore: @unchecked Sendable { let closing = withState { () -> (Engine.Session?, BrowserRecording?) in let session = sessions.removeValue(forKey: name) guard session != nil else { return (nil, nil) } + sessionCreatedAt.removeValue(forKey: name) let recording = recordings.removeValue(forKey: name) trace.removeValue(forKey: name) activeFlows.removeValue(forKey: name) @@ -436,6 +461,80 @@ public final class HostCore: @unchecked Sendable { return .success(id: request.id, result: .object(["closed": .string(name)])) } + private func sessionListing() -> ([JSONValue], [JSONValue]) { + let listedAt = monotonicNow() + let snapshot = withState { + sessions.map { name, session in + (name, session, sessionCreatedAt[name] ?? listedAt) + }.sorted { $0.0 < $1.0 } + } + let names = snapshot.map { JSONValue.string($0.0) } + let details = snapshot.map { name, session, createdAt in + sessionDetail( + name: name, session: session, + ageMilliseconds: max(0, (listedAt - createdAt) * 1_000) + ) + } + return (names, details) + } + + private func sessionDetail( + name: String, session: Engine.Session, ageMilliseconds: TimeInterval + ) -> JSONValue { + let base: [String: JSONValue] = [ + "name": .string(name), + "isolated": .bool(session.hostIsolated), + "ageMs": .number(ageMilliseconds), + "untrustedContent": .bool(true), + ] + do { + let metadata = try session.hostSessionMetadata() + let boundedURL = Self.boundedSessionURL(metadata.url) + let boundedTitle = Self.boundedSessionString( + metadata.title, maximumBytes: Self.sessionTitleMaximumBytes + ) + return .object(base.merging([ + "status": .string(metadata.lifecycle.rawValue), + "url": boundedURL.value.map(JSONValue.string) ?? .null, + "title": boundedTitle.value.map(JSONValue.string) ?? .null, + "urlTruncated": .bool(boundedURL.truncated), + "titleTruncated": .bool(boundedTitle.truncated), + ]) { _, value in value }) + } catch { + return .object(base.merging([ + "status": .string("unavailable"), + "url": .null, + "title": .null, + "urlTruncated": .bool(false), + "titleTruncated": .bool(false), + ]) { _, value in value }) + } + } + + private static func boundedSessionURL(_ value: String?) -> (value: String?, truncated: Bool) { + guard let value, + var components = URLComponents(string: value), + let scheme = components.scheme?.lowercased(), + scheme == "http" || scheme == "https", + components.host != nil else { return (nil, false) } + components.user = nil + components.password = nil + return boundedSessionString( + components.string, maximumBytes: sessionURLMaximumBytes + ) + } + + private static func boundedSessionString( + _ value: String?, maximumBytes: Int + ) -> (value: String?, truncated: Bool) { + guard let value else { return (nil, false) } + let bytes = Array(value.utf8) + guard bytes.count > maximumBytes else { return (value, false) } + var end = maximumBytes + while end > 0, String(bytes: bytes[.. JSONValue { diff --git a/apps/headless/Sources/HeadlessProtocol/ProtocolSchema.swift b/apps/headless/Sources/HeadlessProtocol/ProtocolSchema.swift index 4c48715..17d6c77 100644 --- a/apps/headless/Sources/HeadlessProtocol/ProtocolSchema.swift +++ b/apps/headless/Sources/HeadlessProtocol/ProtocolSchema.swift @@ -134,13 +134,34 @@ public struct ProtocolResultField: Sendable { public let name: String public let kind: ProtocolResultValueKind public let required: Bool + public let items: ProtocolResultArrayItems? + public let values: [String]? fileprivate var document: JSONValue { - .object([ + var fields: [String: JSONValue] = [ "name": .string(name), "type": .string(kind.rawValue), "required": .bool(required), - ]) + ] + if let items { fields["items"] = items.document } + if let values { fields["values"] = .array(values.map(JSONValue.string)) } + return .object(fields) + } +} + +public struct ProtocolResultArrayItems: Sendable { + public let kind: ProtocolResultValueKind + public let name: String? + public let fields: [ProtocolResultField] + + fileprivate var document: JSONValue { + var value: [String: JSONValue] = ["type": .string(kind.rawValue)] + if let name { value["name"] = .string(name) } + if kind == .object { + value["additionalProperties"] = .bool(true) + value["fields"] = .array(fields.map(\.document)) + } + return .object(value) } } @@ -175,12 +196,16 @@ public struct ProtocolResultDefinition: Sendable { "Invalid \(name) result: \(field.name) must be \(field.kind.rawValue)" ) } + try field.validateItems(value) } } } private extension ProtocolResultField { func accepts(_ value: JSONValue) -> Bool { + if let values, case .string(let string) = value { + return values.contains(string) + } switch (kind, value) { case (.string, .string), (.number, .number), (.boolean, .bool), (.object, .object), (.array, .array), (.json, _): @@ -191,12 +216,50 @@ private extension ProtocolResultField { return false } } + + func validateItems(_ value: JSONValue) throws { + guard let items, case .array(let values) = value else { return } + for value in values { + guard items.accepts(value) else { + throw ProtocolValidationError.invalidParameter( + "Invalid \(name) result: array item must be \(items.kind.rawValue)" + ) + } + if items.kind == .object { + try ProtocolResultDefinition( + name: items.name ?? "ArrayItem", fields: items.fields + ).validate(value) + } + } + } +} + +private extension ProtocolResultArrayItems { + func accepts(_ value: JSONValue) -> Bool { + ProtocolResultField( + name: "item", kind: kind, required: true, items: nil, values: nil + ).accepts(value) + } } private func resultField( - _ name: String, _ kind: ProtocolResultValueKind, required: Bool = true + _ name: String, _ kind: ProtocolResultValueKind, required: Bool = true, + values: [String]? = nil +) -> ProtocolResultField { + ProtocolResultField( + name: name, kind: kind, required: required, items: nil, values: values + ) +} + +private func resultArrayField( + _ name: String, itemKind: ProtocolResultValueKind, + itemName: String? = nil, itemFields: [ProtocolResultField] = [] ) -> ProtocolResultField { - ProtocolResultField(name: name, kind: kind, required: required) + ProtocolResultField( + name: name, kind: .array, required: true, + items: ProtocolResultArrayItems(kind: itemKind, name: itemName, fields: itemFields), + values: nil + ) } private func result( @@ -221,7 +284,17 @@ public func protocolResultDefinition(for command: CommandName) -> ProtocolResult case .sessionCreate: return result("SessionCreate", [resultField("session", .string), resultField("isolated", .boolean)]) case .sessionList: - return result("SessionList", [resultField("sessions", .array), resultField("details", .array)]) + return result("SessionList", [ + resultArrayField("sessions", itemKind: .string), + resultArrayField("details", itemKind: .object, itemName: "SessionDetail", itemFields: [ + resultField("name", .string), resultField("isolated", .boolean), + resultField("ageMs", .number), + resultField("status", .string, values: ["available", "navigating", "unavailable"]), + resultField("url", .stringOrNull), resultField("title", .stringOrNull), + resultField("urlTruncated", .boolean), resultField("titleTruncated", .boolean), + resultField("untrustedContent", .boolean), + ]), + ]) case .sessionClose: return result("SessionClose", [resultField("closed", .string)]) case .visit, .back, .reload, .wait: return result("PageState", [ @@ -597,6 +670,8 @@ public let protocolCommandDefinitions: [CommandName: ProtocolCommandDefinition] command(.shutdown), command(.profileClear), command(.sessionCreate, [string("name", required: true, maximumBytes: 64), boolean("isolated")]), + // Keep the compatibility name array directly accessible. Each detail + // record carries its own untrustedContent marker for page-derived data. command(.sessionList), command(.sessionClose), command(.visit, [string("url", required: true)], untrusted: true), diff --git a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift index 99cfddd..ba6c0e7 100644 --- a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift +++ b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift @@ -288,6 +288,11 @@ private func readRawSocketLine(descriptor: Int32) throws -> Data { private final class TestBrowserSession: BrowserEngineSession { let hostIsolated: Bool private(set) var agentControlEnableCount = 0 + private(set) var sessionMetadataReadCount = 0 + var sessionMetadata = BrowserSessionPageMetadata( + url: nil, title: nil, lifecycle: .available + ) + var sessionMetadataHook: (() throws -> Void)? var authenticationState: JSONValue = .object([ "origin": .string("http://localhost"), "detection": .string("none"), ]) @@ -303,6 +308,12 @@ private final class TestBrowserSession: BrowserEngineSession { hostIsolated = isolated } + func hostSessionMetadata() throws -> BrowserSessionPageMetadata { + sessionMetadataReadCount += 1 + try sessionMetadataHook?() + return sessionMetadata + } + func hostEnableAgentControl() { agentControlEnableCount += 1 } func hostVisit(_ url: URL) throws -> JSONValue { .object(["url": .string(url.absoluteString)]) } func hostInspect(parameters: [String: JSONValue]) throws -> JSONValue { @@ -2875,8 +2886,30 @@ struct ProtocolTests { try expect( protocolCommandDefinition(for: .visit).resultContainsUntrustedContent && protocolCommandDefinition(for: .captureInfo).resultContainsUntrustedContent, - "page state and capture metadata must remain marked as untrusted" + "page-derived result contracts must remain marked as untrusted" ) + try expect( + !protocolCommandDefinition(for: .sessionList).resultContainsUntrustedContent, + "session list must preserve its directly accessible compatibility fields" + ) + let sessionDetail: [String: JSONValue] = [ + "name": .string("default"), "isolated": .bool(false), "ageMs": .number(1), + "status": .string("available"), "url": .string("https://example.test/"), + "title": .string("Example"), "urlTruncated": .bool(false), + "titleTruncated": .bool(false), "untrustedContent": .bool(true), + ] + try protocolResultDefinition(for: .sessionList).validate(.object([ + "sessions": .array([.string("default")]), + "details": .array([.object(sessionDetail)]), + ])) + try expectThrows("session metadata should reject unknown lifecycle states") { + var invalid = sessionDetail + invalid["status"] = .string("closed") + try protocolResultDefinition(for: .sessionList).validate(.object([ + "sessions": .array([.string("default")]), + "details": .array([.object(invalid)]), + ])) + } let schemaInvocation = try CLIParser().parse(["schema"]) try expect(schemaInvocation.local == .schema, "schema must remain a local CLI command") @@ -4243,10 +4276,12 @@ struct ProtocolTests { defer { try? FileManager.default.removeItem(atPath: root) } let defaultSession = TestBrowserSession() let engine = TestBrowserEngine() + let clock = TestMonotonicClock(100) let core = HostCore( engine: engine, artifacts: try ArtifactStore(environment: ["HEADLESS_ARTIFACT_DIR": root]), defaultSession: defaultSession, + monotonicNow: { clock.now() }, shutdownHandler: {} ) defer { core.stop() } @@ -4287,15 +4322,76 @@ struct ProtocolTests { try expect(isolatedResult["isolated"] == .bool(true), "session result should report isolation") try expect(engine.createdSessions.count == 3, "isolated creation should delegate to the engine") try expect(engine.createdSessions[2].hostIsolated, "engine should create an isolated session") + + engine.createdSessions[1].sessionMetadata = BrowserSessionPageMetadata( + url: "https://user:secret@example.test/path?value=\(String(repeating: "x", count: 8_300))", + title: String(repeating: "é", count: 600), + lifecycle: .available + ) + engine.createdSessions[2].sessionMetadata = BrowserSessionPageMetadata( + url: "https://private.example.test/loading", + title: "Loading", + lifecycle: .navigating + ) + let broken = core.handle(CommandRequest( + command: .sessionCreate, parameters: ["name": .string("broken")] + )) + try expect(broken.ok, "metadata failure session creation should succeed") + let brokenSession = engine.createdSessions[3] + brokenSession.sessionMetadataHook = { + core.sessionDidClose(brokenSession) + throw TestFailure(description: "session closed during metadata read") + } + let controlCounts = engine.createdSessions.map(\.agentControlEnableCount) + clock.advance(by: 1.25) let listed = core.handle(CommandRequest(command: .sessionList)) guard listed.ok, case .object(let listedResult) = listed.result, case .array(let details)? = listedResult["details"] else { throw TestFailure(description: "session list should include typed details") } try expect( - details.contains(.object(["name": .string("private"), "isolated": .bool(true)])), + listedResult["sessions"] == .array([ + .string("broken"), .string("default"), .string("private"), .string("secondary"), + ]), + "session list should retain a deterministic compatibility array" + ) + let objects = details.compactMap { value -> [String: JSONValue]? in + guard case .object(let object) = value else { return nil } + return object + } + try expect(objects.count == 4, "every listed session should have one detail") + let privateDetail = objects.first { $0["name"] == .string("private") } + try expect( + privateDetail?["isolated"] == .bool(true) + && privateDetail?["status"] == .string("navigating"), "session list should identify isolated sessions" ) + let secondaryDetail = objects.first { $0["name"] == .string("secondary") } + let listedURL = secondaryDetail?["url"]?.stringValue ?? "" + let listedTitle = secondaryDetail?["title"]?.stringValue ?? "" + try expect(!listedURL.contains("user") && !listedURL.contains("secret"), "URL userinfo must be redacted") + try expect(listedURL.utf8.count == 8_192, "session URLs should be byte bounded") + try expect(secondaryDetail?["urlTruncated"] == .bool(true), "URL truncation should be reported") + try expect(listedTitle.utf8.count == 1_000, "session titles should be UTF-8 byte bounded") + try expect(secondaryDetail?["titleTruncated"] == .bool(true), "title truncation should be reported") + try expect(secondaryDetail?["untrustedContent"] == .bool(true), "page metadata should be untrusted") + try expect(secondaryDetail?["ageMs"] == .number(1_250), "session age should use monotonic time") + let brokenDetail = objects.first { $0["name"] == .string("broken") } + try expect(brokenDetail?["status"] == .string("unavailable"), "one metadata failure should be isolated") + try expect( + engine.createdSessions.map(\.agentControlEnableCount) == controlCounts, + "session listing must not enable agent control" + ) + let afterRace = core.handle(CommandRequest(command: .sessionList)) + guard case .object(let afterRaceResult) = afterRace.result else { + throw TestFailure(description: "session list should survive a close race") + } + try expect( + afterRaceResult["sessions"] == .array([ + .string("default"), .string("private"), .string("secondary"), + ]), + "a session closed during listing should disappear from the next snapshot" + ) let inspected = core.handle(CommandRequest( command: .inspect, session: "secondary", parameters: ["interactive": .bool(true)] diff --git a/apps/headless/Tests/linux-e2e.sh b/apps/headless/Tests/linux-e2e.sh index 3edf357..f38b461 100755 --- a/apps/headless/Tests/linux-e2e.sh +++ b/apps/headless/Tests/linux-e2e.sh @@ -299,6 +299,13 @@ HEADLESS_CONFORMANCE_BASE_URL=http://127.0.0.1:41739 \ headless session create qa | grep -q '"session":"qa"' headless session list | grep -q '"qa"' headless --session qa visit http://127.0.0.1:41739/designers/dashboard/ | grep -q 'Designers Dashboard' +SESSION_METADATA="$(headless session list)" +echo "$SESSION_METADATA" | grep -q '"name":"qa"' +echo "$SESSION_METADATA" | grep -q '"status":"available"' +echo "$SESSION_METADATA" | grep -q '"url":"http://127.0.0.1:41739/designers/dashboard/"' +echo "$SESSION_METADATA" | grep -q '"title":"Designers Dashboard"' +echo "$SESSION_METADATA" | grep -q '"untrustedContent":true' +echo "$SESSION_METADATA" | grep -Eq '"ageMs":[0-9]' # Exercise the control-channel regression directly: the same session must # survive a second document, reload that document, go back, and reload again. headless --session qa visit http://127.0.0.1:41739/next/ | grep -q 'Designer Details' diff --git a/apps/headless/Tests/macos-e2e.sh b/apps/headless/Tests/macos-e2e.sh index 2895dcb..3fcd7ad 100755 --- a/apps/headless/Tests/macos-e2e.sh +++ b/apps/headless/Tests/macos-e2e.sh @@ -1309,6 +1309,13 @@ if [[ "$(frontmost_pid)" == "$HOST_PID" ]]; then fi "$CLI" session list | grep -q '"qa"' "$CLI" --session qa visit "http://127.0.0.1:$PORT/designers/dashboard" | grep -q 'Designers Dashboard' +SESSION_METADATA="$("$CLI" session list)" +echo "$SESSION_METADATA" | grep -q '"name":"qa"' +echo "$SESSION_METADATA" | grep -q '"status":"available"' +echo "$SESSION_METADATA" | grep -q '"url":"http://127.0.0.1:'"$PORT"'/designers/dashboard"' +echo "$SESSION_METADATA" | grep -q '"title":"Designers Dashboard"' +echo "$SESSION_METADATA" | grep -q '"untrustedContent":true' +echo "$SESSION_METADATA" | grep -Eq '"ageMs":[0-9]' STEP="inspect-diagnostics" SNAPSHOT="$("$CLI" --session qa inspect --interactive --text)" echo "$SNAPSHOT" | grep -q '"name":"Continue"' diff --git a/apps/headless/docs/COMMANDS.md b/apps/headless/docs/COMMANDS.md index 92989e1..ca13268 100644 --- a/apps/headless/docs/COMMANDS.md +++ b/apps/headless/docs/COMMANDS.md @@ -45,6 +45,13 @@ schema facilities; its exit status is nonzero only when a failed check blocks a supported operation. It never starts a browser, repairs storage, removes a stale socket, or prints environment values and file contents. +- `session list` preserves the ordered `sessions` name array and includes a + typed `details` entry for each session. Details report isolation, monotonic + age, `available`, `navigating`, or `unavailable` status, and nullable current + HTTP(S) URL and title. URL userinfo is removed, URL and title lengths are + bounded with explicit truncation flags, and page-derived fields are marked + `untrustedContent`. Listing does not focus windows, enable agent control, or + execute page JavaScript; one unavailable session does not fail the list. - `start --supervised` is for SDK-owned lifecycle management. It refuses to attach to an existing host, verifies the launched host PID, and shuts the host down when the launcher input closes or the launcher exits. Normal starts diff --git a/docs/roadmap/architecture-decisions.md b/docs/roadmap/architecture-decisions.md index edd3563..c13f4e4 100644 --- a/docs/roadmap/architecture-decisions.md +++ b/docs/roadmap/architecture-decisions.md @@ -937,6 +937,40 @@ behavior rather than trusting doctor's earlier result. --- +## 31. Session metadata is a bounded read-only host snapshot + +**Decision:** `session.list` preserves its ordered `sessions` name array and +adds one fixed-shape detail per session. Details contain the name, isolation, +monotonic age in milliseconds, lifecycle status, nullable current HTTP(S) URL +and title, independent truncation flags, and `untrustedContent: true`. The +mixed-trust result is not wrapped as wholly untrusted: the ordered `sessions` +compatibility array stays directly accessible, while each detail record marks +its page-derived URL and title as untrusted. URLs are +limited to 8,192 UTF-8 bytes after userinfo removal; titles are limited to +1,000 UTF-8 bytes. Non-web URLs are omitted. + +The core snapshots session references and creation times under its state lock, +then releases that lock before asking either engine for metadata. WebKit reads +`WKWebView` URL, title, and loading state on the main thread. Chromium uses the +read-only `Page.getNavigationHistory` CDP command plus host-owned navigation +state. Neither adapter activates a window, enables agent control, evaluates +page JavaScript, takes a semantic snapshot, or changes navigation. A session +that closes or fails while being queried remains in that response with status +`unavailable`; it does not fail metadata for other sessions and disappears +from the next snapshot. + +**Status:** implemented for +[#196](https://github.com/LockInTime/headless/issues/196). + +**Consequences:** agents can choose an existing session without inspecting or +focusing every page. Page-controlled titles and URLs remain explicitly +untrusted and bounded. No cookies, storage, credential aliases, authentication +state, native window identifiers, process identifiers, or filesystem paths are +exposed. The fields are additive at protocol version `0.5`; the generated SDK +schema defines `SessionDetail` so clients do not need untyped casts. + +--- + ## Decision log | # | Decision | Status | Date | @@ -964,5 +998,6 @@ behavior rather than trusting doctor's earlier result. | 28 | SDKs derive from one Swift-owned protocol contract | Decided | 2026-09-12 | | 29 | Detached hosts use a private bounded log writer | Implemented | 2026-09-19 | | 30 | Doctor is a read-only local readiness report | Implemented | 2026-09-19 | +| 31 | Session metadata is a bounded read-only host snapshot | Implemented | 2026-09-19 | New decisions append here with the same format. diff --git a/docs/roadmap/improvements-backlog.md b/docs/roadmap/improvements-backlog.md index 2ede644..e025e39 100644 --- a/docs/roadmap/improvements-backlog.md +++ b/docs/roadmap/improvements-backlog.md @@ -476,8 +476,11 @@ get an architecture-decision entry: include a redacted tail in startup failures. - **G3. Response pagination primitives** (with A3) — cursor pattern reusable by `console list`, `network list`, `artifacts list`. -- **G4. Session metadata** — `session list` returning current URL/title/age; - cheap, big agent QoL. +- **G4. Session metadata** ([#196](https://github.com/LockInTime/headless/issues/196)) — + ~~`session list` returning current URL/title/age; cheap, big agent QoL.~~ + **Done:** the compatible ordered name array now includes typed, bounded + details for age, isolation, lifecycle, sanitized URL, and title. Engine + failures and close races are isolated to one unavailable detail. - **G5. `wait --network-idle`** — Chromium engine has the events; declared capability on WKWebView. - **G6. Element screenshot on Linux `--by-region @rN`** — region-scoped series diff --git a/packages/headless-npm/scripts/generate-sdk.mjs b/packages/headless-npm/scripts/generate-sdk.mjs index 848ebbb..c148394 100644 --- a/packages/headless-npm/scripts/generate-sdk.mjs +++ b/packages/headless-npm/scripts/generate-sdk.mjs @@ -62,8 +62,13 @@ function parameterType(parameter) { } } -function resultType(type) { - switch (type) { +function resultType(field) { + if (Array.isArray(field.values)) return field.values.map(literal).join(" | "); + if (field.type === "array" && field.items) { + if (field.items.type === "object") return `readonly ${field.items.name}[]`; + return `readonly ${resultType({ type: field.items.type })}[]`; + } + switch (field.type) { case "array": return "readonly JsonValue[]"; case "boolean": return "boolean"; case "json": return "JsonValue"; @@ -79,6 +84,11 @@ const resultSchemas = new Map(); function registerResultSchema(result, label) { const name = result?.name; if (!name || !Array.isArray(result.fields)) throw new Error(`invalid result schema: ${label}`); + for (const field of result.fields) { + if (field.items?.type === "object") { + registerResultSchema(field.items, `${label}.${field.name}`); + } + } const encoded = JSON.stringify(result); const previous = resultSchemas.get(name); if (previous && previous !== encoded) throw new Error(`conflicting result schema: ${name}`); @@ -144,7 +154,7 @@ for (const encoded of resultSchemas.values()) { const result = JSON.parse(encoded); lines.push(`export interface ${result.name} {`); for (const field of result.fields) { - lines.push(` readonly ${JSON.stringify(field.name)}${field.required ? "" : "?"}: ${resultType(field.type)};`); + lines.push(` readonly ${JSON.stringify(field.name)}${field.required ? "" : "?"}: ${resultType(field)};`); } if (result.additionalProperties) lines.push(" readonly [key: string]: JsonValue;"); lines.push("}", ""); diff --git a/packages/headless-npm/src/generated.ts b/packages/headless-npm/src/generated.ts index 13a3531..7f1e5fc 100644 --- a/packages/headless-npm/src/generated.ts +++ b/packages/headless-npm/src/generated.ts @@ -7,7 +7,7 @@ export type Untrusted = Readonly<{ readonly untrustedContent: true; readonly export const PROTOCOL_VERSION = "0.5" as const; export const PROTOCOL_SCHEMA_VERSION = 1 as const; export const MAXIMUM_MESSAGE_BYTES = 1048576 as const; -export const PROTOCOL_SCHEMA_SHA256 = "882634187c7ef02ec4ed51fff0e747114eadeff308c10bb9b3274b3630fad11d" as const; +export const PROTOCOL_SCHEMA_SHA256 = "b42ada9e237195df8ac612528759969ababae12ef6c4c72c56799764f2ac5608" as const; export const PROTOCOL_FIXTURES_SHA256 = "0b51ffaa2d3e3aaf0c32adcfeb02c180dcbe44face0d49e1c332b69f403ae062" as const; export const RESPONSE_ADDITIONAL_PROPERTIES = true as const; export const MAXIMUM_COMMAND_TIMEOUT_MS = 125000 as const; @@ -355,9 +355,22 @@ export interface SessionCreate { readonly [key: string]: JsonValue; } +export interface SessionDetail { + readonly "name": string; + readonly "isolated": boolean; + readonly "ageMs": number; + readonly "status": "available" | "navigating" | "unavailable"; + readonly "url": string | null; + readonly "title": string | null; + readonly "urlTruncated": boolean; + readonly "titleTruncated": boolean; + readonly "untrustedContent": boolean; + readonly [key: string]: JsonValue; +} + export interface SessionList { - readonly "sessions": readonly JsonValue[]; - readonly "details": readonly JsonValue[]; + readonly "sessions": readonly string[]; + readonly "details": readonly SessionDetail[]; readonly [key: string]: JsonValue; } @@ -964,11 +977,71 @@ export const COMMAND_METADATA = { "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" diff --git a/packages/headless-python/scripts/generate.py b/packages/headless-python/scripts/generate.py index af36d14..c6e6af1 100644 --- a/packages/headless-python/scripts/generate.py +++ b/packages/headless-python/scripts/generate.py @@ -45,7 +45,16 @@ def parameter_type(parameter: dict[str, Any]) -> str: }[parameter["type"]] -def result_type(field_type: str) -> str: +def result_type(field: dict[str, Any]) -> str: + values = field.get("values") + if isinstance(values, list): + return literal_union(values) + field_type = field["type"] + items = field.get("items") + if field_type == "array" and isinstance(items, dict): + if items.get("type") == "object": + return f"list[{items['name']}]" + return f"list[{result_type({'type': items['type']})}]" return { "array": "list[JsonValue]", "boolean": "bool", @@ -72,6 +81,10 @@ def register_result_schema( name = result.get("name") if not isinstance(name, str) or not name: raise ValueError(f"invalid result schema name: {source}") + for field in result["fields"]: + items = field.get("items") + if isinstance(items, dict) and items.get("type") == "object": + register_result_schema(result_schemas, items, f"{source}.{field['name']}") previous = result_schemas.get(name) if previous is not None and previous != result: raise ValueError(f"conflicting result schema: {name}") @@ -95,7 +108,7 @@ def emit_result(lines: list[str], schema: dict[str, Any]) -> None: lines.append(f"class {schema['name']}(TypedDict, total=False):") for field in schema["fields"]: wrapper = "Required" if field["required"] else "NotRequired" - lines.append(f" {field['name']}: {wrapper}[{result_type(field['type'])}]") + lines.append(f" {field['name']}: {wrapper}[{result_type(field)}]") if not schema["fields"]: lines.append(" pass") lines.append("") diff --git a/packages/headless-python/src/headless_sdk/generated.py b/packages/headless-python/src/headless_sdk/generated.py index c18387e..8872561 100644 --- a/packages/headless-python/src/headless_sdk/generated.py +++ b/packages/headless-python/src/headless_sdk/generated.py @@ -13,7 +13,7 @@ PROTOCOL_SCHEMA_VERSION = 1 MAXIMUM_MESSAGE_BYTES = 1048576 MAXIMUM_COMMAND_TIMEOUT_SECONDS = 125.0 -PROTOCOL_SCHEMA_SHA256 = '882634187c7ef02ec4ed51fff0e747114eadeff308c10bb9b3274b3630fad11d' +PROTOCOL_SCHEMA_SHA256 = 'b42ada9e237195df8ac612528759969ababae12ef6c4c72c56799764f2ac5608' PROTOCOL_FIXTURES_SHA256 = '0b51ffaa2d3e3aaf0c32adcfeb02c180dcbe44face0d49e1c332b69f403ae062' 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', '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') @@ -222,9 +222,20 @@ class SessionCreate(TypedDict, total=False): session: Required[str] isolated: Required[bool] +class SessionDetail(TypedDict, total=False): + name: Required[str] + isolated: Required[bool] + ageMs: Required[int | float] + status: Required[Literal['available', 'navigating', 'unavailable']] + url: Required[str | None] + title: Required[str | None] + urlTruncated: Required[bool] + titleTruncated: Required[bool] + untrustedContent: Required[bool] + class SessionList(TypedDict, total=False): - sessions: Required[list[JsonValue]] - details: Required[list[JsonValue]] + sessions: Required[list[str]] + details: Required[list[SessionDetail]] class SessionClose(TypedDict, total=False): closed: Required[str] @@ -1369,10 +1380,44 @@ class AuthenticationRequired(TypedDict, total=False): 'parameters': [], 'result': {'mayContainUntrustedContent': False, 'schema': {'additionalProperties': True, - 'fields': [{'name': 'sessions', + 'fields': [{'items': {'type': 'string'}, + 'name': 'sessions', 'required': True, 'type': 'array'}, - {'name': 'details', + {'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', diff --git a/sdk/protocol-schema.json b/sdk/protocol-schema.json index 45731a4..3f2231e 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":[{"name":"sessions","required":true,"type":"array"},{"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":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"},{"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","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"]},{"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":"truncated","required":false,"type":"boolean"}],"name":"Screenshot","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":30000,"parameterPresentOverrides":{"series":125000}}},{"capabilityNegotiated":false,"constraints":[],"name":"artifact.list","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"directory","required":true,"type":"string"},{"name":"artifacts","required":true,"type":"array"},{"name":"total","required":true,"type":"number"},{"name":"omitted","required":true,"type":"number"},{"name":"truncated","required":true,"type":"boolean"}],"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":200,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"number"}],"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":"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":200,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"number"}],"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":"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","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":"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":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"},{"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","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"]},{"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":"truncated","required":false,"type":"boolean"}],"name":"Screenshot","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":30000,"parameterPresentOverrides":{"series":125000}}},{"capabilityNegotiated":false,"constraints":[],"name":"artifact.list","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"directory","required":true,"type":"string"},{"name":"artifacts","required":true,"type":"array"},{"name":"total","required":true,"type":"number"},{"name":"omitted","required":true,"type":"number"},{"name":"truncated","required":true,"type":"boolean"}],"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":200,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"number"}],"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":"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":200,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"number"}],"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":"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","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}}