From 0b40b90bfcf3df48c4f3546442641708c192a961 Mon Sep 17 00:00:00 2001 From: SarthakWade Date: Sat, 19 Sep 2026 14:42:11 +0530 Subject: [PATCH 1/3] feat(protocol): add bounded list pagination --- apps/headless/Host/AgentBridge.swift | 20 +- apps/headless/LinuxHost/BrowserProcess.swift | 12 +- apps/headless/LinuxHost/main.swift | 8 +- .../Sources/HeadlessProtocol/Artifacts.swift | 33 ++- .../Sources/HeadlessProtocol/CLI.swift | 34 ++- .../HeadlessProtocol/Diagnostics.swift | 33 ++- .../Sources/HeadlessProtocol/HostCore.swift | 15 +- .../Sources/HeadlessProtocol/HostError.swift | 7 + .../Sources/HeadlessProtocol/Pagination.swift | 194 +++++++++++++++ .../Sources/HeadlessProtocol/Protocol.swift | 17 +- .../HeadlessProtocol/ProtocolSchema.swift | 21 +- .../HeadlessProtocolTests/ProtocolTests.swift | 231 +++++++++++++++++- apps/headless/Tests/linux-e2e.sh | 14 ++ apps/headless/Tests/macos-e2e.sh | 14 ++ apps/headless/build.sh | 1 + apps/headless/docs/COMMANDS.md | 14 +- apps/headless/test.sh | 1 + docs/roadmap/architecture-decisions.md | 39 +++ docs/roadmap/improvements-backlog.md | 7 +- packages/headless-npm/src/generated.ts | 115 ++++++++- .../src/headless_sdk/generated.py | 128 ++++++++-- sdk/protocol-schema.json | 2 +- 22 files changed, 861 insertions(+), 99 deletions(-) create mode 100644 apps/headless/Sources/HeadlessProtocol/Pagination.swift diff --git a/apps/headless/Host/AgentBridge.swift b/apps/headless/Host/AgentBridge.swift index 2f4d6e5..610b4f5 100644 --- a/apps/headless/Host/AgentBridge.swift +++ b/apps/headless/Host/AgentBridge.swift @@ -321,12 +321,16 @@ extension BrowserWindowController { func agentQAReport() -> JSONValue { qaBridge.store.report() } func agentQAClear() -> JSONValue { qaBridge.clear() } - func agentConsole(level: String, limit: Int) -> JSONValue { - qaBridge.store.console(level: level, limit: limit) + func agentConsole(level: String, limit: Int, cursor: String?) throws -> JSONValue { + try qaBridge.store.console(level: level, limit: limit, cursor: cursor) } - func agentNetwork(failedOnly: Bool, status: Int?, limit: Int) -> JSONValue { - qaBridge.store.network(failedOnly: failedOnly, status: status, limit: limit) + func agentNetwork( + failedOnly: Bool, status: Int?, limit: Int, cursor: String? + ) throws -> JSONValue { + try qaBridge.store.network( + failedOnly: failedOnly, status: status, limit: limit, cursor: cursor + ) } func agentNetworkDetail(requestID: String) -> JSONValue { @@ -582,11 +586,11 @@ extension BrowserWindowController: BrowserEngineSession { } func hostQAReport() throws -> JSONValue { agentQAReport() } func hostQAClear() throws -> JSONValue { agentQAClear() } - func hostConsole(level: String, limit: Int) throws -> JSONValue { - agentConsole(level: level, limit: limit) + func hostConsole(level: String, limit: Int, cursor: String?) throws -> JSONValue { + try agentConsole(level: level, limit: limit, cursor: cursor) } - func hostNetwork(failedOnly: Bool, status: Int?, limit: Int) throws -> JSONValue { - agentNetwork(failedOnly: failedOnly, status: status, limit: limit) + func hostNetwork(failedOnly: Bool, status: Int?, limit: Int, cursor: String?) throws -> JSONValue { + try agentNetwork(failedOnly: failedOnly, status: status, limit: limit, cursor: cursor) } func hostNetworkDetail(requestID: String) throws -> JSONValue { agentNetworkDetail(requestID: requestID) diff --git a/apps/headless/LinuxHost/BrowserProcess.swift b/apps/headless/LinuxHost/BrowserProcess.swift index ad5e299..364084b 100644 --- a/apps/headless/LinuxHost/BrowserProcess.swift +++ b/apps/headless/LinuxHost/BrowserProcess.swift @@ -984,12 +984,16 @@ final class LinuxBrowserSession: @unchecked Sendable { return diagnostics.report() } - func console(level: String, limit: Int) -> JSONValue { - diagnostics.console(level: level, limit: limit) + func console(level: String, limit: Int, cursor: String?) throws -> JSONValue { + try diagnostics.console(level: level, limit: limit, cursor: cursor) } - func network(failedOnly: Bool, status: Int?, limit: Int) -> JSONValue { - diagnostics.network(failedOnly: failedOnly, status: status, limit: limit) + func network( + failedOnly: Bool, status: Int?, limit: Int, cursor: String? + ) throws -> JSONValue { + try diagnostics.network( + failedOnly: failedOnly, status: status, limit: limit, cursor: cursor + ) } func networkDetail(requestID: String) -> JSONValue { diff --git a/apps/headless/LinuxHost/main.swift b/apps/headless/LinuxHost/main.swift index c1d47cf..0dc2c2d 100644 --- a/apps/headless/LinuxHost/main.swift +++ b/apps/headless/LinuxHost/main.swift @@ -140,11 +140,11 @@ final class ChromiumBrowserEngineSession: BrowserEngineSession { } func hostQAReport() throws -> JSONValue { try browserSession.qaReport() } func hostQAClear() throws -> JSONValue { browserSession.diagnostics.clear() } - func hostConsole(level: String, limit: Int) throws -> JSONValue { - browserSession.console(level: level, limit: limit) + func hostConsole(level: String, limit: Int, cursor: String?) throws -> JSONValue { + try browserSession.console(level: level, limit: limit, cursor: cursor) } - func hostNetwork(failedOnly: Bool, status: Int?, limit: Int) throws -> JSONValue { - browserSession.network(failedOnly: failedOnly, status: status, limit: limit) + func hostNetwork(failedOnly: Bool, status: Int?, limit: Int, cursor: String?) throws -> JSONValue { + try browserSession.network(failedOnly: failedOnly, status: status, limit: limit, cursor: cursor) } func hostNetworkDetail(requestID: String) throws -> JSONValue { browserSession.networkDetail(requestID: requestID) diff --git a/apps/headless/Sources/HeadlessProtocol/Artifacts.swift b/apps/headless/Sources/HeadlessProtocol/Artifacts.swift index c1cb852..528856a 100644 --- a/apps/headless/Sources/HeadlessProtocol/Artifacts.swift +++ b/apps/headless/Sources/HeadlessProtocol/Artifacts.swift @@ -26,6 +26,7 @@ public enum ArtifactError: Error, CustomStringConvertible { public final class ArtifactStore: @unchecked Sendable { public let rootURL: URL private let lock = NSLock() + private let pagination = PaginationCursorStore() public init(environment: [String: String] = ProcessInfo.processInfo.environment) throws { rootURL = try Self.resolvedRootURL(environment: environment, platform: .current) @@ -179,7 +180,7 @@ public final class ArtifactStore: @unchecked Sendable { return try metadata(for: finalURL) } - public func list() throws -> JSONValue { + public func list(limit: Int = 250, cursor: String? = nil) throws -> JSONValue { lock.lock(); defer { lock.unlock() } let keys: [URLResourceKey] = [.isRegularFileKey, .isSymbolicLinkKey, .fileSizeKey, .creationDateKey] let urls = try FileManager.default.contentsOfDirectory( @@ -192,23 +193,27 @@ public final class ArtifactStore: @unchecked Sendable { return try metadata(for: url) }.sorted { left, right in guard case .object(let lhs) = left, case .object(let rhs) = right else { return false } - return (lhs["createdAt"]?.numberValue ?? 0) > (rhs["createdAt"]?.numberValue ?? 0) + let leftCreated = lhs["createdAt"]?.numberValue ?? 0 + let rightCreated = rhs["createdAt"]?.numberValue ?? 0 + if leftCreated != rightCreated { return leftCreated > rightCreated } + return (lhs["name"]?.stringValue ?? "") < (rhs["name"]?.stringValue ?? "") } - // The store grows without bound across a long session, and the listing - // has to survive the 1 MiB protocol frame. Newest first, bounded, and - // explicit about what was left out. - let listed = Array(artifacts.prefix(Self.maximumListedArtifacts)) + let page = try pagination.page( + values: artifacts, context: "artifact.list", limit: limit, + cursor: cursor, direction: .fromStart + ) return .object([ "directory": .string(rootURL.path), - "artifacts": .array(listed), + "artifacts": .array(page.values), + "returned": .number(Double(page.values.count)), "total": .number(Double(artifacts.count)), - "omitted": .number(Double(artifacts.count - listed.count)), - "truncated": .bool(listed.count < artifacts.count), + "omitted": .number(Double(artifacts.count - page.consumed)), + "truncated": .bool(page.truncated), + "nextCursor": page.nextCursor.map(JSONValue.string) ?? .null, + "mutation": .string("none"), ]) } - private static let maximumListedArtifacts = 250 - private static let listedExtensions: Set = ScreenshotFormat.artifactExtensions .union(RecordingFormat.artifactExtensions) @@ -217,7 +222,11 @@ public final class ArtifactStore: @unchecked Sendable { private func metadata(for url: URL) throws -> JSONValue { let attributes = try FileManager.default.attributesOfItem(atPath: url.path) let bytes = (attributes[.size] as? NSNumber)?.doubleValue ?? 0 - let created = (attributes[.creationDate] as? Date ?? Date()).timeIntervalSince1970 + let created = ( + (attributes[.creationDate] as? Date) + ?? (attributes[.modificationDate] as? Date) + ?? Date(timeIntervalSince1970: 0) + ).timeIntervalSince1970 return .object([ "name": .string(url.lastPathComponent), "path": .string(url.path), diff --git a/apps/headless/Sources/HeadlessProtocol/CLI.swift b/apps/headless/Sources/HeadlessProtocol/CLI.swift index d919687..d253137 100644 --- a/apps/headless/Sources/HeadlessProtocol/CLI.swift +++ b/apps/headless/Sources/HeadlessProtocol/CLI.swift @@ -179,8 +179,17 @@ public struct CLIParser { case "screenshot": return try parseScreenshot(arguments, session: session, jsonOutput: jsonOutput) case "artifacts": - guard arguments == ["list"] else { throw CLIParseError.missingArgument("artifacts list") } - return remote(.artifactList, session: session, jsonOutput: jsonOutput) + guard arguments.first == "list" else { throw CLIParseError.missingArgument("artifacts list") } + var args = Array(arguments.dropFirst()) + let limit = try removeOption("--limit", from: &args) + let cursor = try removeOption("--cursor", from: &args) + try requireEmpty(args) + var parameters: [String: JSONValue] = [:] + if let limit { parameters["limit"] = .number(try paginationLimit(limit)) } + if let cursor { parameters["cursor"] = .string(cursor) } + return remote( + .artifactList, session: session, parameters: parameters, jsonOutput: jsonOutput + ) case "record": return try parseRecord(arguments, session: session, jsonOutput: jsonOutput) case "qa": @@ -617,12 +626,14 @@ public struct CLIParser { var args = Array(arguments.dropFirst()) let level = try removeOption("--level", from: &args) ?? "all" let limit = try removeOption("--limit", from: &args) + let cursor = try removeOption("--cursor", from: &args) try requireEmpty(args) guard ["all", "log", "info", "debug", "warn", "error", "assert"].contains(level) else { throw CLIParseError.invalidOption(level) } var parameters: [String: JSONValue] = ["level": .string(level)] - if let limit { parameters["limit"] = .number(try diagnosticLimit(limit)) } + if let limit { parameters["limit"] = .number(try paginationLimit(limit)) } + if let cursor { parameters["cursor"] = .string(cursor) } return remote(.consoleList, session: session, parameters: parameters, jsonOutput: jsonOutput) } @@ -634,13 +645,15 @@ public struct CLIParser { let failed = removeFlag("--failed", from: &args) let status = try removeOption("--status", from: &args) let limit = try removeOption("--limit", from: &args) + let cursor = try removeOption("--cursor", from: &args) try requireEmpty(args) var parameters: [String: JSONValue] = ["failed": .bool(failed)] if let status { guard let code = Double(status), code >= 100, code <= 599 else { throw CLIParseError.invalidNumber(status) } parameters["status"] = .number(code) } - if let limit { parameters["limit"] = .number(try diagnosticLimit(limit)) } + if let limit { parameters["limit"] = .number(try paginationLimit(limit)) } + if let cursor { parameters["cursor"] = .string(cursor) } return remote(.networkList, session: session, parameters: parameters, jsonOutput: jsonOutput) case "get": guard args.count == 1 else { throw CLIParseError.missingArgument("network get REQUEST_ID") } @@ -760,8 +773,11 @@ public struct CLIParser { } } - private func diagnosticLimit(_ value: String) throws -> Double { - guard let number = Double(value), number >= 1, number <= 200 else { throw CLIParseError.invalidNumber(value) } + private func paginationLimit(_ value: String) throws -> Double { + guard let number = Double(value), number.isFinite, number.rounded() == number, + number >= 1, number <= Double(PaginationCursorStore.maximumLimit) else { + throw CLIParseError.invalidNumber(value) + } return number } @@ -845,12 +861,12 @@ Commands: screenshot [REF | --role ROLE --name NAME | --full-page] [--format png|jpg|jpeg] [--output FILE] [--clipboard] screenshot --full-page --format pdf [--output FILE.pdf] screenshot --every-viewport|--by-section [--format png|jpg|jpeg] [--output PREFIX] - artifacts list + artifacts list [--limit N] [--cursor CURSOR] record start [--fps N] [--format mp4|mov|webm|gif] [--quality fast|balanced|high] [--output FILE] record status | record stop [--output FILE] qa report | qa clear - console list [--level LEVEL] [--limit N] - network list [--failed] [--status CODE] [--limit N] + console list [--level LEVEL] [--limit N] [--cursor CURSOR] + network list [--failed] [--status CODE] [--limit N] [--cursor CURSOR] network get REQUEST_ID network emulate [--offline] [--latency MS] [--download-kbps N] [--upload-kbps N] network mock set URL --body BODY [--status CODE] [--content-type MIME] diff --git a/apps/headless/Sources/HeadlessProtocol/Diagnostics.swift b/apps/headless/Sources/HeadlessProtocol/Diagnostics.swift index a2d869e..3ed9cf3 100644 --- a/apps/headless/Sources/HeadlessProtocol/Diagnostics.swift +++ b/apps/headless/Sources/HeadlessProtocol/Diagnostics.swift @@ -10,6 +10,7 @@ public func diagnosticStringHeaders(_ headers: [String: Any]?) -> [String: Strin public final class QADiagnosticStore: @unchecked Sendable { private let lock = NSLock() + private let pagination = PaginationCursorStore() private var events: [JSONValue] = [] private var didTruncate = false private let maximumEvents = 500 @@ -142,23 +143,30 @@ public final class QADiagnosticStore: @unchecked Sendable { return data.count } - public func console(level: String, limit: Int) -> JSONValue { + public func console(level: String, limit: Int, cursor: String? = nil) throws -> JSONValue { lock.lock(); let snapshot = events; lock.unlock() let items = snapshot.filter { event in guard case .object(let object) = event, object["kind"] == .string("console") else { return false } return level == "all" || object["level"] == .string(level) } - let boundedLimit = max(1, min(limit, 200)) - let boundedItems = Array(items.suffix(boundedLimit)) + let page = try pagination.page( + values: items, context: "console.list|level=\(level)", limit: limit, + cursor: cursor, direction: .newestBatchFirst + ) return .object([ "untrustedContent": .bool(true), - "messages": .array(boundedItems), - "returned": .number(Double(boundedItems.count)), + "messages": .array(page.values), + "returned": .number(Double(page.values.count)), "available": .number(Double(items.count)), + "truncated": .bool(page.truncated), + "nextCursor": page.nextCursor.map(JSONValue.string) ?? .null, + "mutation": .string("none"), ]) } - public func network(failedOnly: Bool, status: Int?, limit: Int) -> JSONValue { + public func network( + failedOnly: Bool, status: Int?, limit: Int, cursor: String? = nil + ) throws -> JSONValue { lock.lock(); let snapshot = events; lock.unlock() let matching = snapshot.filter { event in guard case .object(let object) = event, @@ -169,12 +177,19 @@ public final class QADiagnosticStore: @unchecked Sendable { if let status, Int(object["status"]?.numberValue ?? 0) != status { return false } return true }.map(networkSummary(_:)) - let bounded = Array(matching.suffix(max(1, min(limit, 200)))) + let context = "network.list|failed=\(failedOnly)|status=\(status.map(String.init) ?? "any")" + let page = try pagination.page( + values: matching, context: context, limit: limit, + cursor: cursor, direction: .newestBatchFirst + ) return .object([ "untrustedContent": .bool(true), - "requests": .array(bounded), - "returned": .number(Double(bounded.count)), + "requests": .array(page.values), + "returned": .number(Double(page.values.count)), "available": .number(Double(matching.count)), + "truncated": .bool(page.truncated), + "nextCursor": page.nextCursor.map(JSONValue.string) ?? .null, + "mutation": .string("none"), ]) } diff --git a/apps/headless/Sources/HeadlessProtocol/HostCore.swift b/apps/headless/Sources/HeadlessProtocol/HostCore.swift index 8cafa80..2654348 100644 --- a/apps/headless/Sources/HeadlessProtocol/HostCore.swift +++ b/apps/headless/Sources/HeadlessProtocol/HostCore.swift @@ -52,8 +52,8 @@ public protocol BrowserEngineSession: AnyObject { func hostScrollToCapturePoint(y: Double) throws -> JSONValue func hostQAReport() throws -> JSONValue func hostQAClear() throws -> JSONValue - func hostConsole(level: String, limit: Int) throws -> JSONValue - func hostNetwork(failedOnly: Bool, status: Int?, limit: Int) throws -> JSONValue + func hostConsole(level: String, limit: Int, cursor: String?) throws -> JSONValue + func hostNetwork(failedOnly: Bool, status: Int?, limit: Int, cursor: String?) throws -> JSONValue func hostNetworkDetail(requestID: String) throws -> JSONValue func hostStyles(parameters: [String: JSONValue]) throws -> JSONValue func hostCookies(includeValues: Bool) throws -> JSONValue @@ -265,7 +265,10 @@ public final class HostCore: @unchecked Sendable { return try clearProfile(request) } if request.command == .artifactList { - return .success(id: request.id, result: try artifacts.list()) + return .success(id: request.id, result: try artifacts.list( + limit: Int(request.parameters["limit"]?.numberValue ?? 250), + cursor: request.parameters["cursor"]?.stringValue + )) } switch request.command { case .sessionCreate: @@ -606,13 +609,15 @@ public final class HostCore: @unchecked Sendable { case .consoleList: return try session.hostConsole( level: request.parameters["level"]?.stringValue ?? "all", - limit: Int(request.parameters["limit"]?.numberValue ?? 100) + limit: Int(request.parameters["limit"]?.numberValue ?? 100), + cursor: request.parameters["cursor"]?.stringValue ) case .networkList: return try session.hostNetwork( failedOnly: request.parameters["failed"]?.boolValue ?? false, status: request.parameters["status"]?.numberValue.map(Int.init), - limit: Int(request.parameters["limit"]?.numberValue ?? 100) + limit: Int(request.parameters["limit"]?.numberValue ?? 100), + cursor: request.parameters["cursor"]?.stringValue ) case .networkGet: guard let requestID = request.parameters["requestId"]?.stringValue else { diff --git a/apps/headless/Sources/HeadlessProtocol/HostError.swift b/apps/headless/Sources/HeadlessProtocol/HostError.swift index a64d2a5..da61f9c 100644 --- a/apps/headless/Sources/HeadlessProtocol/HostError.swift +++ b/apps/headless/Sources/HeadlessProtocol/HostError.swift @@ -12,6 +12,10 @@ public enum HostErrorCode: String, CaseIterable, Sendable { case invalidFlow = "INVALID_FLOW" case flowFailed = "FLOW_FAILED" case invalidCommand = "INVALID_COMMAND" + case paginationCursorInvalid = "PAGINATION_CURSOR_INVALID" + case paginationCursorExpired = "PAGINATION_CURSOR_EXPIRED" + case paginationCursorScopeMismatch = "PAGINATION_CURSOR_SCOPE_MISMATCH" + case paginationCursorStale = "PAGINATION_CURSOR_STALE" case operationFailed = "OPERATION_FAILED" } @@ -46,6 +50,9 @@ public struct HostError: Error, CustomStringConvertible, Sendable { return "Use an engine that declares support for this capability." case .missingParameter, .invalidFlow, .flowFailed, .invalidCommand: return nil + case .paginationCursorInvalid, .paginationCursorExpired, + .paginationCursorScopeMismatch, .paginationCursorStale: + return "Restart pagination without --cursor." case .operationFailed: return nil } diff --git a/apps/headless/Sources/HeadlessProtocol/Pagination.swift b/apps/headless/Sources/HeadlessProtocol/Pagination.swift new file mode 100644 index 0000000..c7cc167 --- /dev/null +++ b/apps/headless/Sources/HeadlessProtocol/Pagination.swift @@ -0,0 +1,194 @@ +import Foundation + +public enum PaginationDirection: Sendable { + case fromStart + case newestBatchFirst +} + +public struct PaginationPage: Sendable { + public let values: [JSONValue] + public let nextCursor: String? + public let consumed: Int + public let total: Int + + public var truncated: Bool { nextCursor != nil } +} + +/// Keeps opaque cursors in the owning host process. Tokens carry no collection +/// state, and the bounded registry disappears on restart. +public final class PaginationCursorStore: @unchecked Sendable { + private struct Record { + let context: String + let fingerprint: String + let position: Int + let direction: PaginationDirection + let issuedAt: TimeInterval + let expiresAt: TimeInterval + } + + public static let maximumLimit = 250 + public static let cursorMaximumBytes = 64 + public static let lifetimeSeconds: TimeInterval = 300 + private static let maximumRecords = 512 + private static let maximumPageValueBytes = 768 * 1_024 + + private let lock = NSLock() + private let now: @Sendable () -> TimeInterval + private var records: [String: Record] = [:] + + public init(now: @escaping @Sendable () -> TimeInterval = { + ProcessInfo.processInfo.systemUptime + }) { + self.now = now + } + + public func page( + values: [JSONValue], context: String, limit: Int, cursor: String?, + direction: PaginationDirection + ) throws -> PaginationPage { + let boundedLimit = max(1, min(limit, Self.maximumLimit)) + let fingerprint = Self.fingerprint(values) + let currentTime = now() + let position: Int + if let cursor { + position = try resolve( + cursor, context: context, fingerprint: fingerprint, + direction: direction, currentTime: currentTime + ) + } else { + position = direction == .fromStart ? 0 : values.count + } + + let pageValues: [JSONValue] + let nextPosition: Int? + switch direction { + case .fromStart: + let start = min(position, values.count) + let requestedEnd = min(values.count, start + boundedLimit) + pageValues = try Self.valuesWithinBudget( + Array(values[start.. 0 ? start : nil + } + + let nextCursor = nextPosition.map { + issue( + context: context, fingerprint: fingerprint, position: $0, + direction: direction, currentTime: currentTime + ) + } + return PaginationPage( + values: pageValues, nextCursor: nextCursor, + consumed: nextPosition ?? (direction == .fromStart ? values.count : 0), + total: values.count + ) + } + + private func resolve( + _ cursor: String, context: String, fingerprint: String, + direction: PaginationDirection, currentTime: TimeInterval + ) throws -> Int { + guard cursor.utf8.count <= Self.cursorMaximumBytes, + let uuid = UUID(uuidString: cursor), + uuid.uuidString.caseInsensitiveCompare(cursor) == .orderedSame else { + throw HostError(code: .paginationCursorInvalid, message: "Pagination cursor is invalid.") + } + let key = uuid.uuidString.lowercased() + lock.lock() + defer { lock.unlock() } + guard let record = records[key] else { + throw HostError(code: .paginationCursorInvalid, message: "Pagination cursor is unknown.") + } + guard record.expiresAt > currentTime else { + records.removeValue(forKey: key) + throw HostError(code: .paginationCursorExpired, message: "Pagination cursor has expired.") + } + guard record.context == context, record.direction == direction else { + throw HostError( + code: .paginationCursorScopeMismatch, + message: "Pagination cursor does not belong to this command and filter set." + ) + } + guard record.fingerprint == fingerprint else { + throw HostError( + code: .paginationCursorStale, + message: "The paginated collection changed after the cursor was issued." + ) + } + return record.position + } + + private func issue( + context: String, fingerprint: String, position: Int, + direction: PaginationDirection, currentTime: TimeInterval + ) -> String { + let token = UUID().uuidString.lowercased() + lock.lock() + records = records.filter { $0.value.expiresAt > currentTime } + if records.count >= Self.maximumRecords, + let oldest = records.min(by: { $0.value.issuedAt < $1.value.issuedAt })?.key { + records.removeValue(forKey: oldest) + } + records[token] = Record( + context: context, fingerprint: fingerprint, position: position, + direction: direction, issuedAt: currentTime, + expiresAt: currentTime + Self.lifetimeSeconds + ) + lock.unlock() + return token + } + + private static func fingerprint(_ values: [JSONValue]) -> String { + var first: UInt64 = 0xcbf29ce484222325 + var second: UInt64 = 0x9e3779b185ebca87 + for value in values { + let bytes = (try? ProtocolCodec.encoder.encode(value)) ?? Data() + var length = UInt64(bytes.count).littleEndian + withUnsafeBytes(of: &length) { update($0, first: &first, second: &second) } + bytes.withUnsafeBytes { update($0, first: &first, second: &second) } + } + return String(format: "%016llx%016llx-%d", first, second, values.count) + } + + private static func valuesWithinBudget( + _ values: [JSONValue], keepingNewest: Bool + ) throws -> [JSONValue] { + var kept: [JSONValue] = [] + var encodedBytes = 2 + let candidates = keepingNewest ? Array(values.reversed()) : values + for value in candidates { + let size = try ProtocolCodec.encoder.encode(value).count + (kept.isEmpty ? 0 : 1) + guard size + 2 <= maximumPageValueBytes else { + if kept.isEmpty { + throw HostError( + code: .operationFailed, + message: "A pagination item exceeds the response budget." + ) + } + break + } + guard encodedBytes + size <= maximumPageValueBytes else { break } + kept.append(value) + encodedBytes += size + } + return keepingNewest ? Array(kept.reversed()) : kept + } + + private static func update( + _ bytes: UnsafeRawBufferPointer, first: inout UInt64, second: inout UInt64 + ) { + for byte in bytes { + first = (first ^ UInt64(byte)) &* 0x100000001b3 + second = (second ^ UInt64(byte)) &* 0x9e3779b185ebca87 + } + } +} diff --git a/apps/headless/Sources/HeadlessProtocol/Protocol.swift b/apps/headless/Sources/HeadlessProtocol/Protocol.swift index 6e30453..5274520 100644 --- a/apps/headless/Sources/HeadlessProtocol/Protocol.swift +++ b/apps/headless/Sources/HeadlessProtocol/Protocol.swift @@ -208,8 +208,13 @@ public struct CommandRequest: Codable, Equatable, Sendable { switch command { case .ping, .shutdown, .profileClear, .sessionList, .sessionClose, .back, .reload, - .captureInfo, .artifactList, .recordStatus, .qaReport, .qaClear: + .captureInfo, .recordStatus, .qaReport, .qaClear: break + case .artifactList: + _ = try number( + "limit", minimum: 1, maximum: Double(PaginationCursorStore.maximumLimit) + ) + _ = try string("cursor", maximumBytes: PaginationCursorStore.cursorMaximumBytes) case .authLogin: if let challenge = try string("challenge", maximumBytes: 64), UUID(uuidString: challenge) == nil { @@ -349,11 +354,17 @@ public struct CommandRequest: Codable, Equatable, Sendable { !["all", "log", "info", "debug", "warn", "error", "assert"].contains(level) { throw ProtocolValidationError.invalidParameter("Invalid console level") } - _ = try number("limit", minimum: 1, maximum: 200) + _ = try number( + "limit", minimum: 1, maximum: Double(PaginationCursorStore.maximumLimit) + ) + _ = try string("cursor", maximumBytes: PaginationCursorStore.cursorMaximumBytes) case .networkList: try boolean("failed") _ = try number("status", minimum: 100, maximum: 599) - _ = try number("limit", minimum: 1, maximum: 200) + _ = try number( + "limit", minimum: 1, maximum: Double(PaginationCursorStore.maximumLimit) + ) + _ = try string("cursor", maximumBytes: PaginationCursorStore.cursorMaximumBytes) case .networkGet: _ = try string("requestId", required: true, maximumBytes: 128) case .stylesGet: diff --git a/apps/headless/Sources/HeadlessProtocol/ProtocolSchema.swift b/apps/headless/Sources/HeadlessProtocol/ProtocolSchema.swift index 17d6c77..95554ec 100644 --- a/apps/headless/Sources/HeadlessProtocol/ProtocolSchema.swift +++ b/apps/headless/Sources/HeadlessProtocol/ProtocolSchema.swift @@ -351,8 +351,10 @@ public func protocolResultDefinition(for command: CommandName) -> ProtocolResult case .artifactList: return result("ArtifactList", [ resultField("directory", .string), resultField("artifacts", .array), - resultField("total", .number), resultField("omitted", .number), - resultField("truncated", .boolean), + resultField("returned", .number), resultField("total", .number), + resultField("omitted", .number), resultField("truncated", .boolean), + resultField("nextCursor", .stringOrNull), + resultField("mutation", .string, values: ["none"]), ]) case .recordStart, .recordStatus, .recordStop: return result("Recording", [ @@ -371,11 +373,15 @@ public func protocolResultDefinition(for command: CommandName) -> ProtocolResult return result("ConsoleList", [ resultField("untrustedContent", .boolean), resultField("messages", .array), resultField("returned", .number), resultField("available", .number), + resultField("truncated", .boolean), resultField("nextCursor", .stringOrNull), + resultField("mutation", .string, values: ["none"]), ]) case .networkList: return result("NetworkList", [ resultField("untrustedContent", .boolean), resultField("requests", .array), resultField("returned", .number), resultField("available", .number), + resultField("truncated", .boolean), resultField("nextCursor", .stringOrNull), + resultField("mutation", .string, values: ["none"]), ]) case .networkGet: return result("NetworkDetail", [ @@ -733,7 +739,10 @@ public let protocolCommandDefinitions: [CommandName: ProtocolCommandDefinition] "target, full-page, and series modes are mutually constrained", "PDF requires full-page mode and no clipboard", ]), - command(.artifactList), + command(.artifactList, [ + integer("limit", minimum: 1, maximum: Double(PaginationCursorStore.maximumLimit)), + string("cursor", maximumBytes: PaginationCursorStore.cursorMaximumBytes), + ]), command(.recordStart, [ string("output", maximumBytes: 128), number("fps", minimum: 1, maximum: 30), string( @@ -751,11 +760,13 @@ public let protocolCommandDefinitions: [CommandName: ProtocolCommandDefinition] command(.qaClear), command(.consoleList, [ string("level", maximumBytes: 16, values: ["all", "log", "info", "debug", "warn", "error", "assert"]), - number("limit", minimum: 1, maximum: 200), + integer("limit", minimum: 1, maximum: Double(PaginationCursorStore.maximumLimit)), + string("cursor", maximumBytes: PaginationCursorStore.cursorMaximumBytes), ], untrusted: true), command(.networkList, [ boolean("failed"), number("status", minimum: 100, maximum: 599), - number("limit", minimum: 1, maximum: 200), + integer("limit", minimum: 1, maximum: Double(PaginationCursorStore.maximumLimit)), + string("cursor", maximumBytes: PaginationCursorStore.cursorMaximumBytes), ], untrusted: true), command(.networkGet, [string("requestId", required: true, maximumBytes: 128)], untrusted: true), command( diff --git a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift index ba6c0e7..a755b17 100644 --- a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift +++ b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift @@ -36,6 +36,21 @@ private func expectThrows(_ message: String, _ body: () throws -> Void) throws { } } +private func expectHostError( + _ expected: HostErrorCode, _ message: String, _ body: () throws -> Void +) throws { + do { + try body() + throw TestFailure(description: message) + } catch let error as HostError { + try expect(error.code == expected, "\(message): received \(error.code.rawValue)") + } catch is TestFailure { + throw TestFailure(description: message) + } catch { + throw TestFailure(description: "\(message): received \(error)") + } +} + private func repositoryFile(_ relativePath: String) -> URL? { var directory = URL(fileURLWithPath: #filePath).deletingLastPathComponent() while directory.path != "/" { @@ -351,8 +366,10 @@ private final class TestBrowserSession: BrowserEngineSession { func hostScrollToCapturePoint(y: Double) throws -> JSONValue { .object(["y": .number(y)]) } func hostQAReport() throws -> JSONValue { .object(["issues": .array([])]) } func hostQAClear() throws -> JSONValue { .object(["cleared": .bool(true)]) } - func hostConsole(level: String, limit: Int) throws -> JSONValue { .object(["entries": .array([])]) } - func hostNetwork(failedOnly: Bool, status: Int?, limit: Int) throws -> JSONValue { + func hostConsole(level: String, limit: Int, cursor: String?) throws -> JSONValue { + .object(["entries": .array([])]) + } + func hostNetwork(failedOnly: Bool, status: Int?, limit: Int, cursor: String?) throws -> JSONValue { .object(["requests": .array([])]) } func hostNetworkDetail(requestID: String) throws -> JSONValue { @@ -2911,6 +2928,31 @@ struct ProtocolTests { ])) } + for command in [CommandName.artifactList, .consoleList, .networkList] { + let parameters = protocolCommandDefinition(for: command).parameters + let limit = parameters.first { $0.name == "limit" } + let cursor = parameters.first { $0.name == "cursor" } + try expect( + limit?.kind == .integer && limit?.minimum == 1 + && limit?.maximum == Double(PaginationCursorStore.maximumLimit), + "\(command.rawValue) must expose the shared bounded integer limit" + ) + try expect( + cursor?.kind == .string + && cursor?.maximumBytes == PaginationCursorStore.cursorMaximumBytes, + "\(command.rawValue) must expose the shared bounded cursor" + ) + } + try expect( + Set([ + HostErrorCode.paginationCursorInvalid.rawValue, + HostErrorCode.paginationCursorExpired.rawValue, + HostErrorCode.paginationCursorScopeMismatch.rawValue, + HostErrorCode.paginationCursorStale.rawValue, + ]).isSubset(of: Set(protocolErrorCodes)), + "the schema must publish every typed pagination recovery error" + ) + let schemaInvocation = try CLIParser().parse(["schema"]) try expect(schemaInvocation.local == .schema, "schema must remain a local CLI command") try expect(schemaInvocation.request == nil, "schema must not enter the browser protocol") @@ -3327,6 +3369,19 @@ struct ProtocolTests { try expect(listing["total"] == .number(260), "artifact listing should report the true total") try expect(listing["omitted"] == .number(10), "artifact listing should report what it left out") try expect(listing["truncated"] == .bool(true), "a bounded artifact listing is truncated") + guard let cursor = listing["nextCursor"]?.stringValue, + case .object(let secondPage) = try store.list(limit: 250, cursor: cursor), + case .array(let remaining)? = secondPage["artifacts"] else { + throw TestFailure(description: "artifact pagination should return a second page") + } + try expect(remaining.count == 10, "artifact pagination should return every omitted item") + let names = (artifacts + remaining).compactMap { value -> String? in + guard case .object(let object) = value else { return nil } + return object["name"]?.stringValue + } + try expect(Set(names).count == 260, "artifact traversal should not duplicate or skip entries") + try expect(secondPage["nextCursor"] == .null, "the final artifact page should end traversal") + try expect(secondPage["mutation"] == .string("none"), "stable traversal should report no mutation") let encoded = try ProtocolCodec.encodeLine( CommandResponse.success(id: "artifacts", result: try store.list()) ) @@ -3334,6 +3389,86 @@ struct ProtocolTests { encoded.count <= headlessMaximumMessageBytes, "an artifact listing must fit the protocol frame" ) + guard case .object(let mutationStart) = try store.list(limit: 1), + let mutationCursor = mutationStart["nextCursor"]?.stringValue else { + throw TestFailure(description: "artifact mutation cursor") + } + _ = try store.write( + Data("x".utf8), requestedName: "bound-new.json", extension: "json", prefix: "bound" + ) + try expectHostError(.paginationCursorStale, "artifact mutations must invalidate cursors") { + _ = try store.list(limit: 1, cursor: mutationCursor) + } + } + + static func paginationCursorSecurityAndLifecycle() throws { + let clock = TestMonotonicClock(10) + let cursors = PaginationCursorStore(now: { clock.now() }) + let values = (0..<5).map { JSONValue.number(Double($0)) } + let first = try cursors.page( + values: values, context: "console.list|level=all", limit: 2, + cursor: nil, direction: .newestBatchFirst + ) + guard let cursor = first.nextCursor else { + throw TestFailure(description: "bounded pagination should issue a cursor") + } + try expect(cursor.utf8.count == 36, "cursor tokens should be fixed-size opaque UUIDs") + try expect(!cursor.contains("console") && !cursor.contains("level"), "cursor tokens must not encode scope") + + try expectHostError(.paginationCursorScopeMismatch, "cross-filter cursors must fail closed") { + _ = try cursors.page( + values: values, context: "console.list|level=error", limit: 2, + cursor: cursor, direction: .newestBatchFirst + ) + } + try expectHostError(.paginationCursorInvalid, "tampered cursors must fail closed") { + let replacement = cursor.first == "0" ? "1" : "0" + _ = try cursors.page( + values: values, context: "console.list|level=all", limit: 2, + cursor: replacement + cursor.dropFirst(), direction: .newestBatchFirst + ) + } + try expectHostError(.paginationCursorStale, "mutated collections must reject old cursors") { + _ = try cursors.page( + values: values + [.number(5)], context: "console.list|level=all", limit: 2, + cursor: cursor, direction: .newestBatchFirst + ) + } + clock.advance(by: PaginationCursorStore.lifetimeSeconds + 1) + try expectHostError(.paginationCursorExpired, "expired cursors must have a typed error") { + _ = try cursors.page( + values: values, context: "console.list|level=all", limit: 2, + cursor: cursor, direction: .newestBatchFirst + ) + } + + let otherSession = PaginationCursorStore() + try expectHostError(.paginationCursorInvalid, "cross-session cursors must fail closed") { + _ = try otherSession.page( + values: values, context: "console.list|level=all", limit: 2, + cursor: cursor, direction: .newestBatchFirst + ) + } + + let largeValues = (0..<12).map { index in + JSONValue.object([ + "index": .number(Double(index)), + "payload": .string(String(repeating: "x", count: 100_000)), + ]) + } + let bounded = try PaginationCursorStore().page( + values: largeValues, context: "bounded", limit: 250, + cursor: nil, direction: .fromStart + ) + try expect(bounded.values.count < largeValues.count, "pagination should enforce a byte budget") + try expect(bounded.nextCursor != nil, "byte-bounded pages should remain traversable") + let boundedResponse = try ProtocolCodec.encodeLine(CommandResponse.success( + id: "bounded", result: .object(["items": .array(bounded.values)]) + )) + try expect( + boundedResponse.count < headlessMaximumMessageBytes, + "pagination byte bounds should keep responses below the frame limit" + ) } static func hostLoggingIsPrivateBoundedAndRedacted() throws { @@ -3451,13 +3586,13 @@ struct ProtocolTests { requestID: "request-1", requestHeaders: ["Authorization": "Bearer secret", "X-Visible": "yes"], responseHeaders: ["Set-Cookie": "session=secret", "Content-Type": "application/json"], source: "test" ) - guard case .object(let console) = store.console(level: "error", limit: 10), + guard case .object(let console) = try store.console(level: "error", limit: 10), case .array(let messages)? = console["messages"] else { throw TestFailure(description: "console service") } try expect(console["untrustedContent"] == .bool(true), "console output should mark page evidence untrusted") try expect(messages.count == 1, "console service should filter by level") - guard case .object(let network) = store.network(failedOnly: true, status: nil, limit: 10), + guard case .object(let network) = try store.network(failedOnly: true, status: nil, limit: 10), case .array(let requests)? = network["requests"] else { throw TestFailure(description: "network service") } @@ -3486,17 +3621,102 @@ struct ProtocolTests { try expect(boundedHeaders["X-000"] == .string("value-0"), "header selection should use sorted keys") try expect(boundedHeaders["X-063"] == .string("value-63"), "the deterministic header boundary changed") try expect(boundedHeaders["X-064"] == nil, "headers beyond the sorted cap should be omitted") + + let pagingStore = QADiagnosticStore() + for index in 0..<5 { + pagingStore.append(kind: "console", level: "error", message: "console-\(index)") + pagingStore.append( + kind: "response", url: "https://example.test/\(index)", status: 500, + requestID: "page-\(index)" + ) + } + var consoleCursor: String? + var consoleMessages: [String] = [] + repeat { + guard case .object(let page) = try pagingStore.console( + level: "error", limit: 2, cursor: consoleCursor + ), case .array(let pageMessages)? = page["messages"] else { + throw TestFailure(description: "console pagination shape") + } + consoleMessages += pageMessages.compactMap { value in + guard case .object(let object) = value else { return nil } + return object["message"]?.stringValue + } + consoleCursor = page["nextCursor"]?.stringValue + } while consoleCursor != nil + try expect( + consoleMessages == ["console-3", "console-4", "console-1", "console-2", "console-0"], + "console traversal should return newest chronological batches without gaps" + ) + + var networkCursor: String? + var requestIDs: [String] = [] + repeat { + guard case .object(let page) = try pagingStore.network( + failedOnly: true, status: 500, limit: 2, cursor: networkCursor + ), case .array(let requests)? = page["requests"] else { + throw TestFailure(description: "network pagination shape") + } + requestIDs += requests.compactMap { value in + guard case .object(let object) = value else { return nil } + return object["requestId"]?.stringValue + } + networkCursor = page["nextCursor"]?.stringValue + } while networkCursor != nil + try expect( + requestIDs.count == 5 && Set(requestIDs) == Set((0..<5).map { "page-\($0)" }), + "network traversal should be complete and duplicate-free" + ) + + guard case .object(let scopedPage) = try pagingStore.console(level: "error", limit: 1), + let scopedCursor = scopedPage["nextCursor"]?.stringValue else { + throw TestFailure(description: "console scope cursor") + } + try expectHostError(.paginationCursorScopeMismatch, "console filters must bind cursors") { + _ = try pagingStore.console(level: "warn", limit: 1, cursor: scopedCursor) + } + try expectHostError(.paginationCursorScopeMismatch, "commands must bind cursors") { + _ = try pagingStore.network( + failedOnly: false, status: nil, limit: 1, cursor: scopedCursor + ) + } + pagingStore.append(kind: "console", level: "error", message: "mutation") + try expectHostError(.paginationCursorStale, "diagnostic mutation must be explicit") { + _ = try pagingStore.console(level: "error", limit: 1, cursor: scopedCursor) + } } static func diagnosticCLI() throws { - let console = try CLIParser().parse(["console", "list", "--level", "error", "--limit", "25"]) + let cursor = "123e4567-e89b-12d3-a456-426614174000" + let console = try CLIParser().parse([ + "console", "list", "--level", "error", "--limit", "25", "--cursor", cursor, + ]) try expect(console.request?.command == .consoleList, "console command should parse") + try expect(console.request?.parameters["cursor"] == .string(cursor), "console cursor should parse") + let networkList = try CLIParser().parse([ + "network", "list", "--failed", "--limit", "2", "--cursor", cursor, + ]) + try expect(networkList.request?.parameters["cursor"] == .string(cursor), "network cursor should parse") + let artifacts = try CLIParser().parse(["artifacts", "list", "--limit", "4", "--cursor", cursor]) + try expect(artifacts.request?.parameters["limit"] == .number(4), "artifact limit should parse") + try expect(artifacts.request?.parameters["cursor"] == .string(cursor), "artifact cursor should parse") let network = try CLIParser().parse(["network", "get", "request-1"]) try expect(network.request?.command == .networkGet, "network detail command should parse") let styles = try CLIParser().parse(["styles", "get", "--role", "button", "--name", "Continue", "--property", "display"]) try expect(styles.request?.parameters["properties"] == .array([.string("display")]), "style property should parse") let storage = try CLIParser().parse(["storage", "list", "--scope", "local"]) try expect(storage.request?.command == .storageList, "storage command should parse") + try expectThrows("pagination limits must be integers") { + _ = try CLIParser().parse(["console", "list", "--limit", "1.5"]) + } + try expectThrows("pagination limits must stay bounded") { + _ = try CLIParser().parse(["artifacts", "list", "--limit", "251"]) + } + try expectThrows("protocol pagination limits must be integers") { + try CommandRequest( + command: .networkList, parameters: ["limit": .number(1.5)] + ).validate() + } do { _ = try CLIParser().parse(["qa", "bogus", "--x"]) throw TestFailure(description: "unknown QA subcommands should fail") @@ -4642,6 +4862,7 @@ struct ProtocolTests { ("diagnostic bounds and URL redaction", diagnosticsBoundAndRedacted), ("responses fit the protocol frame", responsesFitTheProtocolFrame), ("artifact listing stays bounded", artifactListingStaysBounded), + ("pagination cursor security and lifecycle", paginationCursorSecurityAndLifecycle), ("host logging security and bounds", hostLoggingIsPrivateBoundedAndRedacted), ("diagnostic services", diagnosticServices), ("diagnostic CLI", diagnosticCLI), diff --git a/apps/headless/Tests/linux-e2e.sh b/apps/headless/Tests/linux-e2e.sh index f38b461..992c2d8 100755 --- a/apps/headless/Tests/linux-e2e.sh +++ b/apps/headless/Tests/linux-e2e.sh @@ -322,10 +322,20 @@ echo "$ACTION_SNAPSHOT" | grep -q '"task":"click Continue"' echo "$ACTION_SNAPSHOT" | grep -q '"name":"Continue"' echo "$ACTION_SNAPSHOT" | grep -q '"actions":\["click"\]' echo "$ACTION_SNAPSHOT" | grep -q '"relevance"' +headless --session qa reload >/dev/null +headless --session qa reload >/dev/null +CONSOLE_PAGE_ONE="$(headless --session qa console list --level error --limit 1)" +CONSOLE_CURSOR="$(echo "$CONSOLE_PAGE_ONE" | sed -n 's/.*"nextCursor":"\([^"]*\)".*/\1/p')" +test -n "$CONSOLE_CURSOR" +headless --session qa console list --level error --limit 1 --cursor "$CONSOLE_CURSOR" | grep -q 'Next.js runtime error' CONSOLE="$(headless --session qa console list --level error)" echo "$CONSOLE" | grep -q 'Next.js runtime error' NETWORK="$(headless --session qa network list)" echo "$NETWORK" | grep -q '"requestId"' +NETWORK_PAGE_ONE="$(headless --session qa network list --limit 1)" +NETWORK_CURSOR="$(echo "$NETWORK_PAGE_ONE" | sed -n 's/.*"nextCursor":"\([^"]*\)".*/\1/p')" +test -n "$NETWORK_CURSOR" +headless --session qa network list --limit 1 --cursor "$NETWORK_CURSOR" | grep -q '"requestId"' NETWORK_ID="$(echo "$NETWORK" | sed -n 's/.*"requestId":"\([^"]*\)"[^}]*"url":"[^"]*\/api\/diagnostic".*/\1/p')" test -n "$NETWORK_ID" NETWORK_DETAIL="$(headless --session qa network get "$NETWORK_ID")" @@ -570,6 +580,10 @@ head -c 6 "$HEADLESS_ARTIFACT_DIR/dashboard-flow.gif" | grep -Eq 'GIF8[79]a' headless artifacts list | grep -q '"name":"dashboard-flow.mp4"' headless artifacts list | grep -q '"name":"dashboard-flow.webm"' headless artifacts list | grep -q '"name":"dashboard-flow.gif"' +ARTIFACT_PAGE_ONE="$(headless artifacts list --limit 1)" +ARTIFACT_CURSOR="$(echo "$ARTIFACT_PAGE_ONE" | sed -n 's/.*"nextCursor":"\([^"]*\)".*/\1/p')" +test -n "$ARTIFACT_CURSOR" +headless artifacts list --limit 1 --cursor "$ARTIFACT_CURSOR" | grep -q '"returned":1' headless --session qa back | grep -q 'Designers Dashboard' headless --session qa reload | grep -q 'Designers Dashboard' headless --session qa capture-info | grep -q '"engine":"chromium"' diff --git a/apps/headless/Tests/macos-e2e.sh b/apps/headless/Tests/macos-e2e.sh index 3fcd7ad..c88a00e 100755 --- a/apps/headless/Tests/macos-e2e.sh +++ b/apps/headless/Tests/macos-e2e.sh @@ -1327,10 +1327,20 @@ echo "$ACTION_SNAPSHOT" | grep -q '"task":"click Continue"' echo "$ACTION_SNAPSHOT" | grep -q '"name":"Continue"' echo "$ACTION_SNAPSHOT" | grep -q '"actions":\["click"\]' echo "$ACTION_SNAPSHOT" | grep -q '"relevance"' +"$CLI" --session qa reload >/dev/null +"$CLI" --session qa reload >/dev/null +CONSOLE_PAGE_ONE="$("$CLI" --session qa console list --level error --limit 1)" +CONSOLE_CURSOR="$(echo "$CONSOLE_PAGE_ONE" | sed -n 's/.*"nextCursor":"\([^"]*\)".*/\1/p')" +test -n "$CONSOLE_CURSOR" +"$CLI" --session qa console list --level error --limit 1 --cursor "$CONSOLE_CURSOR" | grep -q 'Next.js runtime error' CONSOLE="$("$CLI" --session qa console list --level error)" echo "$CONSOLE" | grep -q 'Next.js runtime error' NETWORK="$("$CLI" --session qa network list)" echo "$NETWORK" | grep -q '"requestId"' +NETWORK_PAGE_ONE="$("$CLI" --session qa network list --limit 1)" +NETWORK_CURSOR="$(echo "$NETWORK_PAGE_ONE" | sed -n 's/.*"nextCursor":"\([^"]*\)".*/\1/p')" +test -n "$NETWORK_CURSOR" +"$CLI" --session qa network list --limit 1 --cursor "$NETWORK_CURSOR" | grep -q '"requestId"' NETWORK_ID="$(echo "$NETWORK" | sed -n 's/.*"requestId":"\([^"]*\)"[^}]*"url":"[^"]*\/api\/diagnostic".*/\1/p')" test -n "$NETWORK_ID" NETWORK_DETAIL="$("$CLI" --session qa network get "$NETWORK_ID")" @@ -1493,6 +1503,10 @@ test -s "$HEADLESS_ARTIFACT_DIR/dashboard-flow.mov" file "$HEADLESS_ARTIFACT_DIR/dashboard-flow.mov" | grep -Eq 'ISO Media|QuickTime' "$CLI" artifacts list | grep -q '"name":"dashboard-flow.mp4"' "$CLI" artifacts list | grep -q '"name":"dashboard-flow.mov"' +ARTIFACT_PAGE_ONE="$("$CLI" artifacts list --limit 1)" +ARTIFACT_CURSOR="$(echo "$ARTIFACT_PAGE_ONE" | sed -n 's/.*"nextCursor":"\([^"]*\)".*/\1/p')" +test -n "$ARTIFACT_CURSOR" +"$CLI" artifacts list --limit 1 --cursor "$ARTIFACT_CURSOR" | grep -q '"returned":1' "$CLI" --session qa back | grep -q 'Designers Dashboard' "$CLI" --session qa reload | grep -q 'Designers Dashboard' STEP="capture-hostile" diff --git a/apps/headless/build.sh b/apps/headless/build.sh index 14a2395..f6f1702 100755 --- a/apps/headless/build.sh +++ b/apps/headless/build.sh @@ -61,6 +61,7 @@ if [[ -z "${SDKROOT:-}" ]]; then -target "$NATIVE_ARCH-apple-macos13.0" -typecheck \ Sources/HeadlessProtocol/Protocol.swift \ Sources/HeadlessProtocol/ProtocolSchema.swift \ + Sources/HeadlessProtocol/Pagination.swift \ Sources/HeadlessProtocol/SupervisedHost.swift \ Sources/HeadlessProtocol/CredentialCommands.swift \ Sources/HeadlessProtocol/HostError.swift \ diff --git a/apps/headless/docs/COMMANDS.md b/apps/headless/docs/COMMANDS.md index ca13268..725eb36 100644 --- a/apps/headless/docs/COMMANDS.md +++ b/apps/headless/docs/COMMANDS.md @@ -52,6 +52,14 @@ schema 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. +- `artifacts list`, `console list`, and `network list` accept `--limit N` and + `--cursor CURSOR`. Limits are integers from 1 through 250. Read + `nextCursor` until it is `null`; every page reports its returned and + available or total counts, `truncated`, and mutation state. Cursors are + opaque, expire after five minutes, are bound to the command, filters, and + owning store, and do not survive a host restart. If the collection changes, + or a cursor is invalid, expired, or used with another scope, restart the + listing without `--cursor`. Cursors contain no paths, page data, or secrets. - `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 @@ -251,7 +259,7 @@ capture-info screenshot [REF | --role ROLE --name NAME | --full-page] [--format png|jpg|jpeg] [--output FILE] [--clipboard] screenshot --full-page --format pdf [--output FILE.pdf] screenshot --every-viewport|--by-section [--format png|jpg|jpeg] [--output PREFIX] -artifacts list +artifacts list [--limit N] [--cursor CURSOR] record start [--fps N] [--format mp4|mov|webm|gif] [--quality fast|balanced|high] [--output FILE] record status | record stop [--output FILE] qa report | qa clear @@ -274,8 +282,8 @@ report create [--output REPORT.json] ## Diagnostics ```sh -console list [--level LEVEL] [--limit N] -network list [--failed] [--status CODE] [--limit N] +console list [--level LEVEL] [--limit N] [--cursor CURSOR] +network list [--failed] [--status CODE] [--limit N] [--cursor CURSOR] network get REQUEST_ID network emulate [--offline] [--latency MS] [--download-kbps N] [--upload-kbps N] network mock set URL --body BODY [--status CODE] [--content-type MIME] diff --git a/apps/headless/test.sh b/apps/headless/test.sh index 8435699..d33cb32 100755 --- a/apps/headless/test.sh +++ b/apps/headless/test.sh @@ -26,6 +26,7 @@ if [[ "$(uname -s)" == "Darwin" ]]; then -target "$(uname -m)-apple-macos13.0" -typecheck \ Sources/HeadlessProtocol/Protocol.swift \ Sources/HeadlessProtocol/ProtocolSchema.swift \ + Sources/HeadlessProtocol/Pagination.swift \ Sources/HeadlessProtocol/SupervisedHost.swift \ Sources/HeadlessProtocol/CredentialCommands.swift \ Sources/HeadlessProtocol/HostError.swift \ diff --git a/docs/roadmap/architecture-decisions.md b/docs/roadmap/architecture-decisions.md index c13f4e4..4b6e1fa 100644 --- a/docs/roadmap/architecture-decisions.md +++ b/docs/roadmap/architecture-decisions.md @@ -971,6 +971,44 @@ schema defines `SessionDetail` so clients do not need untyped casts. --- +## 32. List pagination uses bounded server-side opaque cursors + +**Decision:** `artifact.list`, `console.list`, and `network.list` share one +pagination implementation. Cursors are random UUID tokens backed by state in +the owning host process. Tokens encode no offsets, paths, filters, session +names, page data, or secrets. Records bind a token to its owning store, +command, filter set, collection fingerprint, direction, and next position. +They expire after five minutes, the registry retains at most 512 records, and +all records disappear on host restart. + +Limits are integers from 1 through 250. Artifact pages are ordered newest +first, then by name when creation times match. Diagnostic output keeps its +existing behavior: the newest batch is returned first and entries within each +batch remain chronological; subsequent pages walk older batches. Every page +reserves at most 768 KiB for encoded list values so response metadata remains +inside the 1 MiB frame. Pages report `returned`, truthful `total` or +`available`, nullable `nextCursor`, `truncated`, and `mutation: "none"`. +Existing fields and default limits remain. + +Collection fingerprints are computed from canonical bounded list values and +kept only in private process memory. Mutation between pages fails with +`PAGINATION_CURSOR_STALE`; expired, unknown or malformed, and wrong-command or +wrong-filter tokens fail with distinct typed cursor errors. A cursor presented +to a different session or artifact store is unknown and fails closed. Recovery +for every cursor error is to restart without `--cursor`; data is never resumed +against a changed snapshot. + +**Status:** implemented for +[#195](https://github.com/LockInTime/headless/issues/195). + +**Consequences:** stable bounded collections can be traversed completely +without increasing the 1 MiB frame limit. Callers that omit pagination options +retain the prior first-page behavior. Cursor replay is safe until expiry, but +cursors are intentionally not durable across host restarts and do not promise +snapshot retention after source mutation. + +--- + ## Decision log | # | Decision | Status | Date | @@ -999,5 +1037,6 @@ schema defines `SessionDetail` so clients do not need untyped casts. | 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 | +| 32 | List pagination uses bounded server-side opaque cursors | 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 e025e39..4510a39 100644 --- a/docs/roadmap/improvements-backlog.md +++ b/docs/roadmap/improvements-backlog.md @@ -474,8 +474,11 @@ get an architecture-decision entry: a bounded writer into private JSON-line logs in the runtime directory, keep one rotated generation, preserve the absolute-path operator override, and include a redacted tail in startup failures. -- **G3. Response pagination primitives** (with A3) — cursor pattern reusable - by `console list`, `network list`, `artifacts list`. +- **G3. Response pagination primitives** ([#195](https://github.com/LockInTime/headless/issues/195)) — + ~~cursor pattern reusable by `console list`, `network list`, `artifacts + list`.~~ **Done:** all three commands use bounded opaque host-side cursors, + deterministic ordering, filter and store binding, expiry, mutation + detection, and typed recovery errors. - **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 diff --git a/packages/headless-npm/src/generated.ts b/packages/headless-npm/src/generated.ts index 7f1e5fc..875a0fb 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 = "b42ada9e237195df8ac612528759969ababae12ef6c4c72c56799764f2ac5608" as const; +export const PROTOCOL_SCHEMA_SHA256 = "0eda66ca7a4ee067e6289ff70f96fd51e55361ca4941f26b03944294cd4dcdf3" 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; @@ -221,6 +221,8 @@ export interface ScreenshotParameters { } export interface ArtifactListParameters { + readonly "limit"?: number; + readonly "cursor"?: string; } export interface RecordStartParameters { @@ -246,12 +248,14 @@ export interface QaClearParameters { export interface ConsoleListParameters { readonly "level"?: "all" | "log" | "info" | "debug" | "warn" | "error" | "assert"; readonly "limit"?: number; + readonly "cursor"?: string; } export interface NetworkListParameters { readonly "failed"?: boolean; readonly "status"?: number; readonly "limit"?: number; + readonly "cursor"?: string; } export interface NetworkGetParameters { @@ -465,9 +469,12 @@ export interface Screenshot { export interface ArtifactList { readonly "directory": string; readonly "artifacts": readonly JsonValue[]; + readonly "returned": number; readonly "total": number; readonly "omitted": number; readonly "truncated": boolean; + readonly "nextCursor": string | null; + readonly "mutation": "none"; readonly [key: string]: JsonValue; } @@ -499,6 +506,9 @@ export interface ConsoleList { readonly "messages": readonly JsonValue[]; readonly "returned": number; readonly "available": number; + readonly "truncated": boolean; + readonly "nextCursor": string | null; + readonly "mutation": "none"; readonly [key: string]: JsonValue; } @@ -507,6 +517,9 @@ export interface NetworkList { readonly "requests": readonly JsonValue[]; readonly "returned": number; readonly "available": number; + readonly "truncated": boolean; + readonly "nextCursor": string | null; + readonly "mutation": "none"; readonly [key: string]: JsonValue; } @@ -630,7 +643,7 @@ export interface AuthenticationRequired { readonly [key: string]: JsonValue; } -export type CommandErrorCode = "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"; +export type CommandErrorCode = "ARTIFACT_ERROR" | "AUTH_ACCOUNT_NOT_FOUND" | "AUTH_CHALLENGE_CONSUMED" | "AUTH_CHALLENGE_EXPIRED" | "AUTH_CHALLENGE_NOT_FOUND" | "AUTH_FORM_CHANGED" | "AUTH_ORIGIN_CHANGED" | "AUTH_REQUIRED" | "CREDENTIAL_ALIAS_EXISTS" | "ELEMENT_NOT_FOUND" | "FLOW_FAILED" | "HOST_STOPPING" | "HOST_UNAVAILABLE" | "INTERNAL_ERROR" | "INVALID_CAPTURE_FORMAT" | "INVALID_COMMAND" | "INVALID_FLOW" | "INVALID_INPUT" | "INVALID_REQUEST" | "INVALID_SESSION" | "MISSING_PARAMETER" | "OPERATION_FAILED" | "PAGINATION_CURSOR_EXPIRED" | "PAGINATION_CURSOR_INVALID" | "PAGINATION_CURSOR_SCOPE_MISMATCH" | "PAGINATION_CURSOR_STALE" | "PEER_DENIED" | "RECORDER_UNAVAILABLE" | "RECORDING_ACTIVE" | "RECORDING_FAILED" | "RECORDING_NOT_ACTIVE" | "REGION_NOT_FOUND" | "RESPONSE_TOO_LARGE" | "SENSITIVE_DIAGNOSTICS_DISABLED" | "SESSION_EXISTS" | "SESSION_NOT_FOUND" | "TIMEOUT" | "UNSAFE_NAVIGATION" | "UNSAFE_RESOURCE_TYPE" | "UNSUPPORTED_CAPABILITY" | "USER_PRESENCE_DENIED" | "USER_PRESENCE_UNAVAILABLE" | "VAULT_LOCKED" | "VAULT_OPERATION_FAILED" | "VAULT_RESPONSE_INVALID" | "VAULT_UNAVAILABLE"; export type LifecycleErrorCode = "HOST_START_FAILED" | "NAVIGATION_ALLOWLIST_CONFLICT" | "UNSUPPORTED_BROWSER_RUNTIME" | "UNSUPPORTED_CAPABILITY"; @@ -1971,7 +1984,23 @@ export const COMMAND_METADATA = { }, "artifact.list": { "capabilityNegotiated": false, - "parameters": [], + "parameters": [ + { + "maximum": 250, + "minimum": 1, + "name": "limit", + "required": false, + "sensitive": false, + "type": "integer" + }, + { + "maximumBytes": 64, + "name": "cursor", + "required": false, + "sensitive": false, + "type": "string" + } + ], "result": { "mayContainUntrustedContent": false, "schema": { @@ -1987,6 +2016,11 @@ export const COMMAND_METADATA = { "required": true, "type": "array" }, + { + "name": "returned", + "required": true, + "type": "number" + }, { "name": "total", "required": true, @@ -2001,6 +2035,19 @@ export const COMMAND_METADATA = { "name": "truncated", "required": true, "type": "boolean" + }, + { + "name": "nextCursor", + "required": true, + "type": "string-or-null" + }, + { + "name": "mutation", + "required": true, + "type": "string", + "values": [ + "none" + ] } ], "name": "ArtifactList", @@ -2274,12 +2321,19 @@ export const COMMAND_METADATA = { ] }, { - "maximum": 200, + "maximum": 250, "minimum": 1, "name": "limit", "required": false, "sensitive": false, - "type": "number" + "type": "integer" + }, + { + "maximumBytes": 64, + "name": "cursor", + "required": false, + "sensitive": false, + "type": "string" } ], "result": { @@ -2306,6 +2360,24 @@ export const COMMAND_METADATA = { "name": "available", "required": true, "type": "number" + }, + { + "name": "truncated", + "required": true, + "type": "boolean" + }, + { + "name": "nextCursor", + "required": true, + "type": "string-or-null" + }, + { + "name": "mutation", + "required": true, + "type": "string", + "values": [ + "none" + ] } ], "name": "ConsoleList", @@ -2336,12 +2408,19 @@ export const COMMAND_METADATA = { "type": "number" }, { - "maximum": 200, + "maximum": 250, "minimum": 1, "name": "limit", "required": false, "sensitive": false, - "type": "number" + "type": "integer" + }, + { + "maximumBytes": 64, + "name": "cursor", + "required": false, + "sensitive": false, + "type": "string" } ], "result": { @@ -2368,6 +2447,24 @@ export const COMMAND_METADATA = { "name": "available", "required": true, "type": "number" + }, + { + "name": "truncated", + "required": true, + "type": "boolean" + }, + { + "name": "nextCursor", + "required": true, + "type": "string-or-null" + }, + { + "name": "mutation", + "required": true, + "type": "string", + "values": [ + "none" + ] } ], "name": "NetworkList", @@ -3232,8 +3329,8 @@ export abstract class GeneratedCommandClient { return this.invoke("screenshot", parameters, options); } - artifactList(options?: CommandOptions): Promise> { - return this.invoke("artifact.list", {}, options); + artifactList(parameters: ArtifactListParameters = {}, options?: CommandOptions): Promise> { + return this.invoke("artifact.list", parameters, options); } recordStart(parameters: RecordStartParameters = {}, options?: CommandOptions): Promise> { diff --git a/packages/headless-python/src/headless_sdk/generated.py b/packages/headless-python/src/headless_sdk/generated.py index 8872561..6baf630 100644 --- a/packages/headless-python/src/headless_sdk/generated.py +++ b/packages/headless-python/src/headless_sdk/generated.py @@ -13,11 +13,11 @@ PROTOCOL_SCHEMA_VERSION = 1 MAXIMUM_MESSAGE_BYTES = 1048576 MAXIMUM_COMMAND_TIMEOUT_SECONDS = 125.0 -PROTOCOL_SCHEMA_SHA256 = 'b42ada9e237195df8ac612528759969ababae12ef6c4c72c56799764f2ac5608' +PROTOCOL_SCHEMA_SHA256 = '0eda66ca7a4ee067e6289ff70f96fd51e55361ca4941f26b03944294cd4dcdf3' 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') -CommandErrorCode = Literal['ARTIFACT_ERROR', 'AUTH_ACCOUNT_NOT_FOUND', 'AUTH_CHALLENGE_CONSUMED', 'AUTH_CHALLENGE_EXPIRED', 'AUTH_CHALLENGE_NOT_FOUND', 'AUTH_FORM_CHANGED', 'AUTH_ORIGIN_CHANGED', 'AUTH_REQUIRED', 'CREDENTIAL_ALIAS_EXISTS', 'ELEMENT_NOT_FOUND', 'FLOW_FAILED', 'HOST_STOPPING', 'HOST_UNAVAILABLE', 'INTERNAL_ERROR', 'INVALID_CAPTURE_FORMAT', 'INVALID_COMMAND', 'INVALID_FLOW', 'INVALID_INPUT', 'INVALID_REQUEST', 'INVALID_SESSION', 'MISSING_PARAMETER', 'OPERATION_FAILED', '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'] +COMMAND_ERROR_CODES = ('ARTIFACT_ERROR', 'AUTH_ACCOUNT_NOT_FOUND', 'AUTH_CHALLENGE_CONSUMED', 'AUTH_CHALLENGE_EXPIRED', 'AUTH_CHALLENGE_NOT_FOUND', 'AUTH_FORM_CHANGED', 'AUTH_ORIGIN_CHANGED', 'AUTH_REQUIRED', 'CREDENTIAL_ALIAS_EXISTS', 'ELEMENT_NOT_FOUND', 'FLOW_FAILED', 'HOST_STOPPING', 'HOST_UNAVAILABLE', 'INTERNAL_ERROR', 'INVALID_CAPTURE_FORMAT', 'INVALID_COMMAND', 'INVALID_FLOW', 'INVALID_INPUT', 'INVALID_REQUEST', 'INVALID_SESSION', 'MISSING_PARAMETER', 'OPERATION_FAILED', 'PAGINATION_CURSOR_EXPIRED', 'PAGINATION_CURSOR_INVALID', 'PAGINATION_CURSOR_SCOPE_MISMATCH', 'PAGINATION_CURSOR_STALE', 'PEER_DENIED', 'RECORDER_UNAVAILABLE', 'RECORDING_ACTIVE', 'RECORDING_FAILED', 'RECORDING_NOT_ACTIVE', 'REGION_NOT_FOUND', 'RESPONSE_TOO_LARGE', 'SENSITIVE_DIAGNOSTICS_DISABLED', 'SESSION_EXISTS', 'SESSION_NOT_FOUND', 'TIMEOUT', 'UNSAFE_NAVIGATION', 'UNSAFE_RESOURCE_TYPE', 'UNSUPPORTED_CAPABILITY', 'USER_PRESENCE_DENIED', 'USER_PRESENCE_UNAVAILABLE', 'VAULT_LOCKED', 'VAULT_OPERATION_FAILED', 'VAULT_RESPONSE_INVALID', 'VAULT_UNAVAILABLE') +CommandErrorCode = Literal['ARTIFACT_ERROR', 'AUTH_ACCOUNT_NOT_FOUND', 'AUTH_CHALLENGE_CONSUMED', 'AUTH_CHALLENGE_EXPIRED', 'AUTH_CHALLENGE_NOT_FOUND', 'AUTH_FORM_CHANGED', 'AUTH_ORIGIN_CHANGED', 'AUTH_REQUIRED', 'CREDENTIAL_ALIAS_EXISTS', 'ELEMENT_NOT_FOUND', 'FLOW_FAILED', 'HOST_STOPPING', 'HOST_UNAVAILABLE', 'INTERNAL_ERROR', 'INVALID_CAPTURE_FORMAT', 'INVALID_COMMAND', 'INVALID_FLOW', 'INVALID_INPUT', 'INVALID_REQUEST', 'INVALID_SESSION', 'MISSING_PARAMETER', 'OPERATION_FAILED', 'PAGINATION_CURSOR_EXPIRED', 'PAGINATION_CURSOR_INVALID', 'PAGINATION_CURSOR_SCOPE_MISMATCH', 'PAGINATION_CURSOR_STALE', 'PEER_DENIED', 'RECORDER_UNAVAILABLE', 'RECORDING_ACTIVE', 'RECORDING_FAILED', 'RECORDING_NOT_ACTIVE', 'REGION_NOT_FOUND', 'RESPONSE_TOO_LARGE', 'SENSITIVE_DIAGNOSTICS_DISABLED', 'SESSION_EXISTS', 'SESSION_NOT_FOUND', 'TIMEOUT', 'UNSAFE_NAVIGATION', 'UNSAFE_RESOURCE_TYPE', 'UNSUPPORTED_CAPABILITY', 'USER_PRESENCE_DENIED', 'USER_PRESENCE_UNAVAILABLE', 'VAULT_LOCKED', 'VAULT_OPERATION_FAILED', 'VAULT_RESPONSE_INVALID', 'VAULT_UNAVAILABLE'] LIFECYCLE_ERROR_CODES = ('HOST_START_FAILED', 'NAVIGATION_ALLOWLIST_CONFLICT', 'UNSUPPORTED_BROWSER_RUNTIME', 'UNSUPPORTED_CAPABILITY') LifecycleErrorCode = Literal['HOST_START_FAILED', 'NAVIGATION_ALLOWLIST_CONFLICT', 'UNSUPPORTED_BROWSER_RUNTIME', 'UNSUPPORTED_CAPABILITY'] LAUNCH_PRESENTATIONS = ('background', 'foreground') @@ -111,7 +111,8 @@ class ScreenshotParameters(TypedDict): clipboard: NotRequired[bool] class ArtifactListParameters(TypedDict): - pass + limit: NotRequired[int] + cursor: NotRequired[str] class RecordStartParameters(TypedDict): output: NotRequired[str] @@ -133,12 +134,14 @@ class QaClearParameters(TypedDict): class ConsoleListParameters(TypedDict): level: NotRequired[Literal['all', 'log', 'info', 'debug', 'warn', 'error', 'assert']] - limit: NotRequired[int | float] + limit: NotRequired[int] + cursor: NotRequired[str] class NetworkListParameters(TypedDict): failed: NotRequired[bool] status: NotRequired[int | float] - limit: NotRequired[int | float] + limit: NotRequired[int] + cursor: NotRequired[str] class NetworkGetParameters(TypedDict): requestId: str @@ -306,9 +309,12 @@ class Screenshot(TypedDict, total=False): class ArtifactList(TypedDict, total=False): directory: Required[str] artifacts: Required[list[JsonValue]] + returned: Required[int | float] total: Required[int | float] omitted: Required[int | float] truncated: Required[bool] + nextCursor: Required[str | None] + mutation: Required[Literal['none']] class Recording(TypedDict, total=False): active: Required[bool] @@ -332,12 +338,18 @@ class ConsoleList(TypedDict, total=False): messages: Required[list[JsonValue]] returned: Required[int | float] available: Required[int | float] + truncated: Required[bool] + nextCursor: Required[str | None] + mutation: Required[Literal['none']] class NetworkList(TypedDict, total=False): untrustedContent: Required[bool] requests: Required[list[JsonValue]] returned: Required[int | float] available: Required[int | float] + truncated: Required[bool] + nextCursor: Required[str | None] + mutation: Required[Literal['none']] class NetworkDetail(TypedDict, total=False): found: Required[bool] @@ -449,7 +461,17 @@ class AuthenticationRequired(TypedDict, total=False): 'timeout': {'defaultMilliseconds': 15000, 'parameterPresentOverrides': {}}}, 'artifact.list': {'capabilityNegotiated': False, 'constraints': [], - 'parameters': [], + 'parameters': [{'maximum': 250, + 'minimum': 1, + 'name': 'limit', + 'required': False, + 'sensitive': False, + 'type': 'integer'}, + {'maximumBytes': 64, + 'name': 'cursor', + 'required': False, + 'sensitive': False, + 'type': 'string'}], 'result': {'mayContainUntrustedContent': False, 'schema': {'additionalProperties': True, 'fields': [{'name': 'directory', @@ -458,6 +480,9 @@ class AuthenticationRequired(TypedDict, total=False): {'name': 'artifacts', 'required': True, 'type': 'array'}, + {'name': 'returned', + 'required': True, + 'type': 'number'}, {'name': 'total', 'required': True, 'type': 'number'}, @@ -466,7 +491,14 @@ class AuthenticationRequired(TypedDict, total=False): 'type': 'number'}, {'name': 'truncated', 'required': True, - 'type': 'boolean'}], + 'type': 'boolean'}, + {'name': 'nextCursor', + 'required': True, + 'type': 'string-or-null'}, + {'name': 'mutation', + 'required': True, + 'type': 'string', + 'values': ['none']}], 'name': 'ArtifactList', 'type': 'object'}}, 'scope': 'host', @@ -598,12 +630,17 @@ class AuthenticationRequired(TypedDict, total=False): 'warn', 'error', 'assert']}, - {'maximum': 200, + {'maximum': 250, 'minimum': 1, 'name': 'limit', 'required': False, 'sensitive': False, - 'type': 'number'}], + 'type': 'integer'}, + {'maximumBytes': 64, + 'name': 'cursor', + 'required': False, + 'sensitive': False, + 'type': 'string'}], 'result': {'mayContainUntrustedContent': True, 'schema': {'additionalProperties': True, 'fields': [{'name': 'untrustedContent', @@ -617,7 +654,17 @@ class AuthenticationRequired(TypedDict, total=False): 'type': 'number'}, {'name': 'available', 'required': True, - 'type': 'number'}], + 'type': 'number'}, + {'name': 'truncated', + 'required': True, + 'type': 'boolean'}, + {'name': 'nextCursor', + 'required': True, + 'type': 'string-or-null'}, + {'name': 'mutation', + 'required': True, + 'type': 'string', + 'values': ['none']}], 'name': 'ConsoleList', 'type': 'object'}}, 'scope': 'session', @@ -895,12 +942,17 @@ class AuthenticationRequired(TypedDict, total=False): 'required': False, 'sensitive': False, 'type': 'number'}, - {'maximum': 200, + {'maximum': 250, 'minimum': 1, 'name': 'limit', 'required': False, 'sensitive': False, - 'type': 'number'}], + 'type': 'integer'}, + {'maximumBytes': 64, + 'name': 'cursor', + 'required': False, + 'sensitive': False, + 'type': 'string'}], 'result': {'mayContainUntrustedContent': True, 'schema': {'additionalProperties': True, 'fields': [{'name': 'untrustedContent', @@ -914,7 +966,17 @@ class AuthenticationRequired(TypedDict, total=False): 'type': 'number'}, {'name': 'available', 'required': True, - 'type': 'number'}], + 'type': 'number'}, + {'name': 'truncated', + 'required': True, + 'type': 'boolean'}, + {'name': 'nextCursor', + 'required': True, + 'type': 'string-or-null'}, + {'name': 'mutation', + 'required': True, + 'type': 'string', + 'values': ['none']}], 'name': 'NetworkList', 'type': 'object'}}, 'scope': 'session', @@ -1820,10 +1882,17 @@ def session_list( def artifact_list( self, *, + limit: int | None = None, + cursor: str | None = None, timeout: float | None = None, cancel: SyncCancellation | None = None, ) -> ArtifactList: - parameters: dict[str, JsonValue] = {} + parameters: dict[str, JsonValue] = { + } + if limit is not None: + parameters['limit'] = cast(JsonValue, limit) + if cursor is not None: + parameters['cursor'] = cast(JsonValue, cursor) return cast(ArtifactList, self._invoke_sync('artifact.list', parameters, timeout, cancel)) class SyncSessionCommands: @@ -2145,7 +2214,8 @@ def console_list( self, *, level: Literal['all', 'log', 'info', 'debug', 'warn', 'error', 'assert'] | None = None, - limit: int | float | None = None, + limit: int | None = None, + cursor: str | None = None, timeout: float | None = None, cancel: SyncCancellation | None = None, ) -> Untrusted[ConsoleList]: @@ -2155,6 +2225,8 @@ def console_list( parameters['level'] = cast(JsonValue, level) if limit is not None: parameters['limit'] = cast(JsonValue, limit) + if cursor is not None: + parameters['cursor'] = cast(JsonValue, cursor) return cast(Untrusted[ConsoleList], self._invoke_sync('console.list', parameters, timeout, cancel)) def network_list( @@ -2162,7 +2234,8 @@ def network_list( *, failed: bool | None = None, status: int | float | None = None, - limit: int | float | None = None, + limit: int | None = None, + cursor: str | None = None, timeout: float | None = None, cancel: SyncCancellation | None = None, ) -> Untrusted[NetworkList]: @@ -2174,6 +2247,8 @@ def network_list( parameters['status'] = cast(JsonValue, status) if limit is not None: parameters['limit'] = cast(JsonValue, limit) + if cursor is not None: + parameters['cursor'] = cast(JsonValue, cursor) return cast(Untrusted[NetworkList], self._invoke_sync('network.list', parameters, timeout, cancel)) def network_get( @@ -2452,10 +2527,17 @@ async def session_list( async def artifact_list( self, *, + limit: int | None = None, + cursor: str | None = None, timeout: float | None = None, cancel: AsyncCancellation | None = None, ) -> ArtifactList: - parameters: dict[str, JsonValue] = {} + parameters: dict[str, JsonValue] = { + } + if limit is not None: + parameters['limit'] = cast(JsonValue, limit) + if cursor is not None: + parameters['cursor'] = cast(JsonValue, cursor) return cast(ArtifactList, await self._invoke_async('artifact.list', parameters, timeout, cancel)) class AsyncSessionCommands: @@ -2777,7 +2859,8 @@ async def console_list( self, *, level: Literal['all', 'log', 'info', 'debug', 'warn', 'error', 'assert'] | None = None, - limit: int | float | None = None, + limit: int | None = None, + cursor: str | None = None, timeout: float | None = None, cancel: AsyncCancellation | None = None, ) -> Untrusted[ConsoleList]: @@ -2787,6 +2870,8 @@ async def console_list( parameters['level'] = cast(JsonValue, level) if limit is not None: parameters['limit'] = cast(JsonValue, limit) + if cursor is not None: + parameters['cursor'] = cast(JsonValue, cursor) return cast(Untrusted[ConsoleList], await self._invoke_async('console.list', parameters, timeout, cancel)) async def network_list( @@ -2794,7 +2879,8 @@ async def network_list( *, failed: bool | None = None, status: int | float | None = None, - limit: int | float | None = None, + limit: int | None = None, + cursor: str | None = None, timeout: float | None = None, cancel: AsyncCancellation | None = None, ) -> Untrusted[NetworkList]: @@ -2806,6 +2892,8 @@ async def network_list( parameters['status'] = cast(JsonValue, status) if limit is not None: parameters['limit'] = cast(JsonValue, limit) + if cursor is not None: + parameters['cursor'] = cast(JsonValue, cursor) return cast(Untrusted[NetworkList], await self._invoke_async('network.list', parameters, timeout, cancel)) async def network_get( diff --git a/sdk/protocol-schema.json b/sdk/protocol-schema.json index 3f2231e..3b0fa61 100644 --- a/sdk/protocol-schema.json +++ b/sdk/protocol-schema.json @@ -1 +1 @@ -{"cancellation":{"afterSend":"close the client connection; command outcome is unknown until state is inspected","beforeSend":"request is not written","ownedHost":"terminate, await, and reap only a host process launched by that SDK instance","sharedHost":"never stop the host implicitly"},"commands":[{"capabilityNegotiated":false,"constraints":[],"name":"ping","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"ready","required":true,"type":"boolean"},{"name":"pid","required":true,"type":"number"},{"name":"engine","required":true,"type":"string"},{"name":"platform","required":true,"type":"string"},{"name":"productVersion","required":true,"type":"string"},{"name":"protocolVersion","required":true,"type":"string"},{"name":"capabilities","required":true,"type":"object"},{"name":"recordingAvailable","required":true,"type":"boolean"},{"name":"artifactDirectory","required":true,"type":"string"},{"name":"navigationAllowlist","required":true,"type":"array"}],"name":"HostStatus","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"shutdown","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"stopping","required":true,"type":"boolean"}],"name":"Shutdown","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"profile.clear","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"cleared","required":true,"type":"boolean"},{"name":"session","required":true,"type":"string"}],"name":"ProfileClear","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"session.create","parameters":[{"maximumBytes":64,"name":"name","required":true,"sensitive":false,"type":"string"},{"name":"isolated","required":false,"sensitive":false,"type":"boolean"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"session","required":true,"type":"string"},{"name":"isolated","required":true,"type":"boolean"}],"name":"SessionCreate","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"session.list","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"items":{"type":"string"},"name":"sessions","required":true,"type":"array"},{"items":{"additionalProperties":true,"fields":[{"name":"name","required":true,"type":"string"},{"name":"isolated","required":true,"type":"boolean"},{"name":"ageMs","required":true,"type":"number"},{"name":"status","required":true,"type":"string","values":["available","navigating","unavailable"]},{"name":"url","required":true,"type":"string-or-null"},{"name":"title","required":true,"type":"string-or-null"},{"name":"urlTruncated","required":true,"type":"boolean"},{"name":"titleTruncated","required":true,"type":"boolean"},{"name":"untrustedContent","required":true,"type":"boolean"}],"name":"SessionDetail","type":"object"},"name":"details","required":true,"type":"array"}],"name":"SessionList","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"session.close","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"closed","required":true,"type":"string"}],"name":"SessionClose","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"visit","parameters":[{"maximumBytes":8192,"name":"url","required":true,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"title","required":true,"type":"string"},{"name":"readyState","required":true,"type":"string"},{"name":"text","required":true,"type":"string"},{"name":"runningAnimations","required":true,"type":"number"},{"name":"mutationQuietMs","required":true,"type":"number"},{"name":"scrollY","required":true,"type":"number"},{"name":"contentHeight","required":true,"type":"number"}],"name":"PageState","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"inspect","parameters":[{"name":"interactive","required":false,"sensitive":false,"type":"boolean"},{"name":"text","required":false,"sensitive":false,"type":"boolean"},{"maximumBytes":16,"name":"context","required":false,"sensitive":false,"type":"string","values":["summary","outline","text","actions","full"]},{"maximumBytes":512,"name":"task","required":false,"sensitive":false,"type":"string"},{"maximumBytes":16,"name":"within","required":false,"sensitive":false,"type":"string"},{"maximum":250,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"integer"},{"maximum":16000,"minimum":256,"name":"budget","required":false,"sensitive":false,"type":"integer"},{"maximum":8,"minimum":0,"name":"depth","required":false,"sensitive":false,"type":"integer"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"title","required":true,"type":"string"},{"name":"contextMode","required":true,"type":"string"},{"name":"viewport","required":true,"type":"object"},{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"elements","required":false,"type":"array"},{"name":"regions","required":false,"type":"array"},{"name":"snippets","required":false,"type":"array"},{"name":"text","required":false,"type":"string"}],"name":"Inspection","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["exactly one target reference or semantic role/name target"],"name":"click","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"clicked","required":true,"type":"string"},{"name":"role","required":true,"type":"string"},{"name":"name","required":true,"type":"string"}],"name":"Click","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["exactly one target reference or semantic role/name target"],"name":"fill","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"},{"maximumBytes":900000,"name":"value","required":true,"sensitive":true,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"filled","required":true,"type":"string"},{"name":"valueLength","required":true,"type":"number"}],"name":"Fill","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":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":[{"maximum":250,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"integer"},{"maximumBytes":64,"name":"cursor","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"directory","required":true,"type":"string"},{"name":"artifacts","required":true,"type":"array"},{"name":"returned","required":true,"type":"number"},{"name":"total","required":true,"type":"number"},{"name":"omitted","required":true,"type":"number"},{"name":"truncated","required":true,"type":"boolean"},{"name":"nextCursor","required":true,"type":"string-or-null"},{"name":"mutation","required":true,"type":"string","values":["none"]}],"name":"ArtifactList","type":"object"}},"scope":"host","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"record.start","parameters":[{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"},{"maximum":30,"minimum":1,"name":"fps","required":false,"sensitive":false,"type":"number"},{"caseInsensitiveValues":true,"maximumBytes":16,"name":"format","required":false,"sensitive":false,"type":"string","values":["mp4","mov","webm","gif"]},{"caseInsensitiveValues":true,"maximumBytes":16,"name":"quality","required":false,"sensitive":false,"type":"string","values":["fast","balanced","high"]}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"active","required":true,"type":"boolean"},{"name":"format","required":false,"type":"string"},{"name":"quality","required":false,"type":"string"},{"name":"name","required":false,"type":"string"}],"name":"Recording","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"record.status","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"active","required":true,"type":"boolean"},{"name":"format","required":false,"type":"string"},{"name":"quality","required":false,"type":"string"},{"name":"name","required":false,"type":"string"}],"name":"Recording","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"record.stop","parameters":[{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"active","required":true,"type":"boolean"},{"name":"format","required":false,"type":"string"},{"name":"quality","required":false,"type":"string"},{"name":"name","required":false,"type":"string"}],"name":"Recording","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":30000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"qa.report","parameters":[],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"summary","required":true,"type":"object"},{"name":"issues","required":true,"type":"array"},{"name":"events","required":true,"type":"array"},{"name":"omitted","required":true,"type":"object"},{"name":"truncated","required":true,"type":"boolean"}],"name":"QAReport","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"qa.clear","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"cleared","required":true,"type":"number"}],"name":"QAClear","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"console.list","parameters":[{"maximumBytes":16,"name":"level","required":false,"sensitive":false,"type":"string","values":["all","log","info","debug","warn","error","assert"]},{"maximum":250,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"integer"},{"maximumBytes":64,"name":"cursor","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"messages","required":true,"type":"array"},{"name":"returned","required":true,"type":"number"},{"name":"available","required":true,"type":"number"},{"name":"truncated","required":true,"type":"boolean"},{"name":"nextCursor","required":true,"type":"string-or-null"},{"name":"mutation","required":true,"type":"string","values":["none"]}],"name":"ConsoleList","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"network.list","parameters":[{"name":"failed","required":false,"sensitive":false,"type":"boolean"},{"maximum":599,"minimum":100,"name":"status","required":false,"sensitive":false,"type":"number"},{"maximum":250,"minimum":1,"name":"limit","required":false,"sensitive":false,"type":"integer"},{"maximumBytes":64,"name":"cursor","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"requests","required":true,"type":"array"},{"name":"returned","required":true,"type":"number"},{"name":"available","required":true,"type":"number"},{"name":"truncated","required":true,"type":"boolean"},{"name":"nextCursor","required":true,"type":"string-or-null"},{"name":"mutation","required":true,"type":"string","values":["none"]}],"name":"NetworkList","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"network.get","parameters":[{"maximumBytes":128,"name":"requestId","required":true,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"found","required":true,"type":"boolean"},{"name":"requestId","required":false,"type":"string"},{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"request","required":false,"type":"object"}],"name":"NetworkDetail","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["exactly one target reference or semantic role/name target"],"name":"styles.get","parameters":[{"maximumBytes":16,"name":"target","required":false,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"role","required":false,"sensitive":false,"type":"string"},{"maximumBytes":1000,"name":"name","required":false,"sensitive":false,"type":"string"},{"itemMaximumBytes":128,"maximumItems":64,"name":"properties","required":false,"sensitive":false,"type":"string-array"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"ref","required":true,"type":"string"},{"name":"role","required":true,"type":"string"},{"name":"name","required":true,"type":"string"},{"name":"box","required":true,"type":"object"},{"name":"styles","required":true,"type":"object"}],"name":"Styles","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["values require the sensitive diagnostics environment gate"],"name":"cookies.list","parameters":[{"name":"includeValues","required":false,"sensitive":false,"type":"boolean"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"cookies","required":true,"type":"array"},{"name":"returned","required":true,"type":"number"},{"name":"available","required":true,"type":"number"},{"name":"truncated","required":true,"type":"boolean"}],"name":"CookieList","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":["values require the sensitive diagnostics environment gate"],"name":"storage.list","parameters":[{"maximumBytes":16,"name":"scope","required":false,"sensitive":false,"type":"string","values":["local","session","all"]},{"name":"includeValues","required":false,"sensitive":false,"type":"boolean"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"origin","required":true,"type":"string"},{"name":"stores","required":true,"type":"array"}],"name":"StorageList","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"visual.compare","parameters":[{"maximumBytes":128,"name":"before","required":true,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"after","required":true,"sensitive":false,"type":"string"},{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"name","required":true,"type":"string"},{"name":"changedPixels","required":false,"type":"number"},{"name":"differenceRatio","required":false,"type":"number"}],"name":"VisualComparison","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"performance.get","parameters":[],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"timing","required":true,"type":"json"},{"name":"webVitals","required":true,"type":"object"},{"name":"resources","required":true,"type":"object"}],"name":"Performance","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"animation.list","parameters":[],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"count","required":true,"type":"number"},{"name":"animations","required":true,"type":"array"},{"name":"truncated","required":true,"type":"boolean"}],"name":"AnimationList","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"report.create","parameters":[{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"name","required":true,"type":"string"},{"name":"path","required":true,"type":"string"},{"name":"kind","required":true,"type":"string"},{"name":"bytes","required":true,"type":"number"},{"name":"createdAt","required":true,"type":"number"}],"name":"Artifact","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"flow.start","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"recording","required":true,"type":"boolean"},{"name":"note","required":true,"type":"string"}],"name":"FlowStart","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"flow.stop","parameters":[{"maximumBytes":128,"name":"output","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"name","required":true,"type":"string"},{"name":"path","required":true,"type":"string"},{"name":"kind","required":true,"type":"string"},{"name":"bytes","required":true,"type":"number"},{"name":"createdAt","required":true,"type":"number"}],"name":"Artifact","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":false,"constraints":[],"name":"flow.run","parameters":[{"maximumBytes":128,"name":"input","required":true,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"completed","required":true,"type":"number"},{"name":"input","required":true,"type":"string"}],"name":"FlowRun","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":125000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":[],"name":"network.emulate","parameters":[{"name":"offline","required":false,"sensitive":false,"type":"boolean"},{"maximum":120000,"minimum":0,"name":"latencyMs","required":false,"sensitive":false,"type":"number"},{"maximum":1000000,"minimum":-1,"name":"downloadKbps","required":false,"sensitive":false,"type":"number"},{"maximum":1000000,"minimum":-1,"name":"uploadKbps","required":false,"sensitive":false,"type":"number"}],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"offline","required":true,"type":"boolean"},{"name":"latencyMs","required":true,"type":"number"},{"name":"downloadKbps","required":true,"type":"number"},{"name":"uploadKbps","required":true,"type":"number"},{"name":"engine","required":true,"type":"string"}],"name":"NetworkEmulation","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":[],"name":"network.mock.set","parameters":[{"maximumBytes":8192,"name":"url","required":true,"sensitive":false,"type":"string"},{"maximum":599,"minimum":100,"name":"status","required":false,"sensitive":false,"type":"number"},{"maximumBytes":65536,"name":"body","required":true,"sensitive":false,"type":"string"},{"maximumBytes":256,"name":"contentType","required":false,"sensitive":false,"type":"string"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"url","required":true,"type":"string"},{"name":"status","required":true,"type":"number"},{"name":"activeMocks","required":true,"type":"number"}],"name":"NetworkMock","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":[],"name":"network.mock.clear","parameters":[],"result":{"mayContainUntrustedContent":false,"schema":{"additionalProperties":true,"fields":[{"name":"cleared","required":true,"type":"number"}],"name":"NetworkMockClear","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}},{"capabilityNegotiated":true,"constraints":["choose interactive login or account alias","saved login requires a valid single-use challenge"],"name":"auth.login","parameters":[{"maximumBytes":64,"name":"challenge","required":false,"sensitive":false,"type":"string"},{"maximumBytes":64,"name":"account","required":false,"sensitive":false,"type":"string"},{"name":"interactive","required":false,"sensitive":false,"type":"boolean"}],"result":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"origin","required":true,"type":"string"},{"name":"account","required":true,"type":"string-or-null"},{"name":"saved","required":true,"type":"boolean"},{"name":"continuation","required":true,"type":"string"},{"name":"passwordExposed","required":true,"type":"boolean"},{"name":"originalActionReplayed","required":true,"type":"boolean"}],"name":"AuthenticationLogin","type":"object"}},"scope":"session","timeout":{"defaultMilliseconds":15000,"parameterPresentOverrides":{}}}],"compatibility":{"mismatch":"host rejects request or client rejects response before decoding result","olderClient":"may call only commands and parameters declared by its bundled schema","olderHost":"missing commands or capabilities fail explicitly; clients must not emulate them","policy":"exact-wire-version","productVersionIndependent":true},"errorDetails":{"AUTH_REQUIRED":{"mayContainUntrustedContent":true,"schema":{"additionalProperties":true,"fields":[{"name":"challenge","required":true,"type":"string"},{"name":"origin","required":true,"type":"string"},{"name":"detection","required":true,"type":"string"},{"name":"accounts","required":true,"type":"array"},{"name":"expiresInSeconds","required":true,"type":"number"},{"name":"userPresenceRequired","required":true,"type":"boolean"},{"name":"credentialUseAvailable","required":true,"type":"boolean"},{"name":"vaultAvailable","required":true,"type":"boolean"},{"name":"vaultStatus","required":true,"type":"string"},{"name":"untrustedContent","required":true,"type":"boolean"},{"name":"originalActionReplayed","required":true,"type":"boolean"}],"name":"AuthenticationRequired","type":"object"}}},"format":"headless-sdk-contract","hostLifecycle":{"connect":"attach to an existing host and never assume ownership","existingHost":"supervised start fails rather than attaching","ownerExit":"host shuts down when the owner pipe closes","sharedStart":"headless start","shutdown":"only explicit user requests may stop a shared host","supervisedOwnerChannel":"open standard-input pipe","supervisedStart":"headless start --supervised"},"jsonSchemaDialect":"https://json-schema.org/draft/2020-12/schema","localCommands":["capabilities","config.describe","config.get","config.list","config.reset","config.set","credentials.add","credentials.list","credentials.remove","credentials.rename","help","runtime","schema","start","version"],"localErrorCodes":["CONFIGURATION_FAILED","HOST_START_FAILED","HOST_UNAVAILABLE","INVALID_CONFIGURATION","NAVIGATION_ALLOWLIST_CONFLICT","UNSUPPORTED_BROWSER_RUNTIME","UNSUPPORTED_CAPABILITY","VAULT_UNAVAILABLE"],"localLifecycle":{"connect":{"errors":["HOST_UNAVAILABLE"],"ownership":"shared","transport":"local-unix-socket"},"launch":{"argv":["start","--supervised"],"command":"start","errors":["HOST_START_FAILED","NAVIGATION_ALLOWLIST_CONFLICT","UNSUPPORTED_BROWSER_RUNTIME","UNSUPPORTED_CAPABILITY"],"options":[{"name":"presentation","required":false,"type":"string","values":["background","foreground"]},{"itemMaximumBytes":300,"maximumItems":32,"name":"allow","required":false,"type":"string-array"},{"const":true,"name":"supervised","required":true,"type":"boolean"}],"ownership":"owned-only-after-response-pid-matches-launched-child","result":{"additionalProperties":true,"fields":[{"name":"ready","required":true,"type":"boolean"},{"name":"pid","required":true,"type":"number"},{"name":"engine","required":true,"type":"string"},{"name":"platform","required":true,"type":"string"},{"name":"productVersion","required":true,"type":"string"},{"name":"protocolVersion","required":true,"type":"string"},{"name":"capabilities","required":true,"type":"object"},{"name":"recordingAvailable","required":true,"type":"boolean"},{"name":"artifactDirectory","required":true,"type":"string"},{"name":"navigationAllowlist","required":true,"type":"array"}],"name":"HostStatus","type":"object"}}},"maximumMessageBytes":1048576,"protocolVersion":"0.5","releasePolicy":{"preOneDeprecation":"at least one minor SDK release","provenance":"publish from reviewed tags with package attestations","schemaVersioning":"integer; generators reject unknown versions","sdkVersioning":"semantic versions independent of the wire version; combined product distributors track product tags","securityReporting":"SECURITY.md; never include secrets or private artifacts","stableRemoval":"major SDK release and migration notes"},"request":{"additionalProperties":false,"id":{"maximumBytes":128,"minimumBytes":1,"type":"string"},"parameters":{"additionalProperties":false,"type":"object"},"required":["id","version","command","parameters"],"session":{"maximumBytes":64,"type":["string","null"]},"type":"object","version":{"const":"0.5","type":"string"}},"response":{"additionalProperties":true,"failure":{"error":{"codes":["ARTIFACT_ERROR","AUTH_ACCOUNT_NOT_FOUND","AUTH_CHALLENGE_CONSUMED","AUTH_CHALLENGE_EXPIRED","AUTH_CHALLENGE_NOT_FOUND","AUTH_FORM_CHANGED","AUTH_ORIGIN_CHANGED","AUTH_REQUIRED","CREDENTIAL_ALIAS_EXISTS","ELEMENT_NOT_FOUND","FLOW_FAILED","HOST_STOPPING","HOST_UNAVAILABLE","INTERNAL_ERROR","INVALID_CAPTURE_FORMAT","INVALID_COMMAND","INVALID_FLOW","INVALID_INPUT","INVALID_REQUEST","INVALID_SESSION","MISSING_PARAMETER","OPERATION_FAILED","PAGINATION_CURSOR_EXPIRED","PAGINATION_CURSOR_INVALID","PAGINATION_CURSOR_SCOPE_MISMATCH","PAGINATION_CURSOR_STALE","PEER_DENIED","RECORDER_UNAVAILABLE","RECORDING_ACTIVE","RECORDING_FAILED","RECORDING_NOT_ACTIVE","REGION_NOT_FOUND","RESPONSE_TOO_LARGE","SENSITIVE_DIAGNOSTICS_DISABLED","SESSION_EXISTS","SESSION_NOT_FOUND","TIMEOUT","UNSAFE_NAVIGATION","UNSAFE_RESOURCE_TYPE","UNSUPPORTED_CAPABILITY","USER_PRESENCE_DENIED","USER_PRESENCE_UNAVAILABLE","VAULT_LOCKED","VAULT_OPERATION_FAILED","VAULT_RESPONSE_INVALID","VAULT_UNAVAILABLE"],"optional":["suggestion","details"],"required":["code","message"]},"ok":false,"result":null},"required":["id","version","ok"],"success":{"error":null,"ok":true,"result":"json"},"type":"object"},"schemaVersion":1,"security":{"arbitraryJavaScript":false,"credentialParameters":"aliases-and-challenge-identifiers-only","pageData":"untrusted","sensitiveDiagnostics":"requires-command-flag-and-host-environment-gate","tcpListener":false,"unknownRequestFields":"rejected","unknownResponseFields":"accepted for compatible additive evolution"},"supportWindow":{"hostPlatforms":["macOS 13 or newer","Linux amd64","Linux arm64"],"maintenance":"latest two minor SDK lines and at least 12 months after supersession, whichever is longer","pythonRuntimes":["CPython 3.11 through 3.14"],"schemaVersions":[1],"securityFixes":"supported SDK lines receive applicable security fixes","typescriptRuntimes":["Node.js 22 or newer"],"wireVersions":["0.5"]},"timeouts":{"connect":"fails before a request is sent","requestAfterWrite":"outcome unknown; never retry automatically","requestBeforeWrite":"safe to retry with a new request identifier","startup":"terminate and reap only the supervised launcher and host"},"transport":{"framing":"one-newline-terminated-json-document-per-connection","kind":"local-unix-socket","peerAuthorization":"same-os-user","runtimeDirectoryMode":"0700","socketMode":"0600","tcp":false}} From c81e8aa90ecf177886765af821ad16807d0958ff Mon Sep 17 00:00:00 2001 From: SarthakWade Date: Sat, 19 Sep 2026 14:52:53 +0530 Subject: [PATCH 2/3] test(pagination): preserve element reference lifecycle --- apps/headless/Tests/linux-e2e.sh | 4 ++-- apps/headless/Tests/macos-e2e.sh | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/headless/Tests/linux-e2e.sh b/apps/headless/Tests/linux-e2e.sh index 992c2d8..1e6b9d6 100755 --- a/apps/headless/Tests/linux-e2e.sh +++ b/apps/headless/Tests/linux-e2e.sh @@ -312,6 +312,8 @@ headless --session qa visit http://127.0.0.1:41739/next/ | grep -q 'Designer Det headless --session qa reload | grep -q 'Designer Details' headless --session qa back | grep -q 'Designers Dashboard' headless --session qa reload | grep -q 'Designers Dashboard' +headless --session qa reload >/dev/null +headless --session qa reload >/dev/null SNAPSHOT="$(headless --session qa inspect --interactive --text)" echo "$SNAPSHOT" | grep -q '"name":"Continue"' echo "$SNAPSHOT" | grep -q '"name":"Reviewer"' @@ -322,8 +324,6 @@ echo "$ACTION_SNAPSHOT" | grep -q '"task":"click Continue"' echo "$ACTION_SNAPSHOT" | grep -q '"name":"Continue"' echo "$ACTION_SNAPSHOT" | grep -q '"actions":\["click"\]' echo "$ACTION_SNAPSHOT" | grep -q '"relevance"' -headless --session qa reload >/dev/null -headless --session qa reload >/dev/null CONSOLE_PAGE_ONE="$(headless --session qa console list --level error --limit 1)" CONSOLE_CURSOR="$(echo "$CONSOLE_PAGE_ONE" | sed -n 's/.*"nextCursor":"\([^"]*\)".*/\1/p')" test -n "$CONSOLE_CURSOR" diff --git a/apps/headless/Tests/macos-e2e.sh b/apps/headless/Tests/macos-e2e.sh index c88a00e..e22dfe0 100755 --- a/apps/headless/Tests/macos-e2e.sh +++ b/apps/headless/Tests/macos-e2e.sh @@ -1313,23 +1313,23 @@ 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 "$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" + "$CLI" --session qa reload >/dev/null + "$CLI" --session qa reload >/dev/null + SNAPSHOT="$("$CLI" --session qa inspect --interactive --text)" echo "$SNAPSHOT" | grep -q '"name":"Continue"' echo "$SNAPSHOT" | grep -q '"name":"Reviewer"' ! echo "$SNAPSHOT" | grep -q '"pwned":true' ACTION_SNAPSHOT="$("$CLI" --session qa inspect --context actions --task 'click Continue')" echo "$ACTION_SNAPSHOT" | grep -q '"contextMode":"actions"' echo "$ACTION_SNAPSHOT" | grep -q '"task":"click Continue"' -echo "$ACTION_SNAPSHOT" | grep -q '"name":"Continue"' -echo "$ACTION_SNAPSHOT" | grep -q '"actions":\["click"\]' -echo "$ACTION_SNAPSHOT" | grep -q '"relevance"' -"$CLI" --session qa reload >/dev/null -"$CLI" --session qa reload >/dev/null -CONSOLE_PAGE_ONE="$("$CLI" --session qa console list --level error --limit 1)" + echo "$ACTION_SNAPSHOT" | grep -q '"name":"Continue"' + echo "$ACTION_SNAPSHOT" | grep -q '"actions":\["click"\]' + echo "$ACTION_SNAPSHOT" | grep -q '"relevance"' + CONSOLE_PAGE_ONE="$("$CLI" --session qa console list --level error --limit 1)" CONSOLE_CURSOR="$(echo "$CONSOLE_PAGE_ONE" | sed -n 's/.*"nextCursor":"\([^"]*\)".*/\1/p')" test -n "$CONSOLE_CURSOR" "$CLI" --session qa console list --level error --limit 1 --cursor "$CONSOLE_CURSOR" | grep -q 'Next.js runtime error' From 878cb8d0ffd092475f35e754dc93600af71c4cfa Mon Sep 17 00:00:00 2001 From: SarthakWade Date: Sat, 19 Sep 2026 14:53:15 +0530 Subject: [PATCH 3/3] style(macos): align pagination e2e setup --- apps/headless/Tests/macos-e2e.sh | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/headless/Tests/macos-e2e.sh b/apps/headless/Tests/macos-e2e.sh index e22dfe0..4a4281c 100755 --- a/apps/headless/Tests/macos-e2e.sh +++ b/apps/headless/Tests/macos-e2e.sh @@ -1313,23 +1313,23 @@ 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" - "$CLI" --session qa reload >/dev/null - "$CLI" --session qa reload >/dev/null - SNAPSHOT="$("$CLI" --session qa inspect --interactive --text)" +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" +"$CLI" --session qa reload >/dev/null +"$CLI" --session qa reload >/dev/null +SNAPSHOT="$("$CLI" --session qa inspect --interactive --text)" echo "$SNAPSHOT" | grep -q '"name":"Continue"' echo "$SNAPSHOT" | grep -q '"name":"Reviewer"' ! echo "$SNAPSHOT" | grep -q '"pwned":true' ACTION_SNAPSHOT="$("$CLI" --session qa inspect --context actions --task 'click Continue')" echo "$ACTION_SNAPSHOT" | grep -q '"contextMode":"actions"' echo "$ACTION_SNAPSHOT" | grep -q '"task":"click Continue"' - echo "$ACTION_SNAPSHOT" | grep -q '"name":"Continue"' - echo "$ACTION_SNAPSHOT" | grep -q '"actions":\["click"\]' - echo "$ACTION_SNAPSHOT" | grep -q '"relevance"' - CONSOLE_PAGE_ONE="$("$CLI" --session qa console list --level error --limit 1)" +echo "$ACTION_SNAPSHOT" | grep -q '"name":"Continue"' +echo "$ACTION_SNAPSHOT" | grep -q '"actions":\["click"\]' +echo "$ACTION_SNAPSHOT" | grep -q '"relevance"' +CONSOLE_PAGE_ONE="$("$CLI" --session qa console list --level error --limit 1)" CONSOLE_CURSOR="$(echo "$CONSOLE_PAGE_ONE" | sed -n 's/.*"nextCursor":"\([^"]*\)".*/\1/p')" test -n "$CONSOLE_CURSOR" "$CLI" --session qa console list --level error --limit 1 --cursor "$CONSOLE_CURSOR" | grep -q 'Next.js runtime error'