diff --git a/CHANGELOG.md b/CHANGELOG.md index 30677d7..7415e69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,10 @@ Cutting that release is tracked in ### Changed +- Machine-readable capabilities now derive their command and engine inventories + exhaustively from the protocol enums, preventing new commands or engines from + being silently omitted. + - macOS agent startup now opens browser windows without activating Headless or covering the user's current app. The startup presentation can be configured persistently or overridden for one launch. diff --git a/apps/headless/Sources/HeadlessProtocol/Capabilities.swift b/apps/headless/Sources/HeadlessProtocol/Capabilities.swift index 736f5ad..ada309e 100644 --- a/apps/headless/Sources/HeadlessProtocol/Capabilities.swift +++ b/apps/headless/Sources/HeadlessProtocol/Capabilities.swift @@ -106,7 +106,17 @@ public struct BrowserEngineCapabilities: Sendable { screenshotClipboard: false ) - public static let all: [BrowserEngineCapabilities] = [.webkit, .chromium] + public static func profile(for engine: BrowserEngineName) -> BrowserEngineCapabilities { + switch engine { + case .webkit: webkit + case .chromium: chromium + } + } + + /// Generated from the engine enum so adding an engine cannot silently + /// leave it out of the machine-readable capability document. + public static let all: [BrowserEngineCapabilities] = + BrowserEngineName.allCases.map { BrowserEngineCapabilities.profile(for: $0) } } public var currentBrowserEngineCapabilities: BrowserEngineCapabilities { diff --git a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift index 304b5cd..7af5aaa 100644 --- a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift +++ b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift @@ -1213,7 +1213,12 @@ struct ProtocolTests { engines.count == BrowserEngineName.allCases.count, "capabilities should contain exactly one profile for every engine" ) - for profile in BrowserEngineCapabilities.all { + try expect( + BrowserEngineCapabilities.all.map(\.engine) == BrowserEngineName.allCases, + "capability profiles should be generated in engine-enum order" + ) + for engineName in BrowserEngineName.allCases { + let profile = BrowserEngineCapabilities.profile(for: engineName) guard case .object(let engine)? = engines[profile.engine.rawValue], case .array(let rawSupported)? = engine["commands"], case .array(let rawUnsupported)? = engine["unsupportedCommands"], diff --git a/docs/roadmap/improvements-backlog.md b/docs/roadmap/improvements-backlog.md index 9686050..13a1494 100644 --- a/docs/roadmap/improvements-backlog.md +++ b/docs/roadmap/improvements-backlog.md @@ -269,8 +269,12 @@ oversized line / local-command rejection (`MCP/main.swift:64-66`).~~ **Done:** `HeadlessMCPTests` drives the built stdio process through every named case and uses a private local socket server to verify a real browser-command round trip. -**C4. Machine-accurate `capabilities`** ([#32](https://github.com/LockInTime/headless/issues/32)) — generate from `CommandName.allCases` -and the engine matrix (see B6) so agents can trust it. +**C4. Machine-accurate `capabilities`** ([#32](https://github.com/LockInTime/headless/issues/32)) — ~~generate from `CommandName.allCases` +and the engine matrix (see B6) so agents can trust it.~~ **Done:** the document +derives commands from `CommandName.allCases`, engine profiles from +`BrowserEngineName.allCases`, and capture formats and bounds from their runtime +definitions. The protocol suite proves both command partitions are exhaustive +and every declared engine has exactly one profile. **C5. Harness onboarding [exists: skill content].** ([#33](https://github.com/LockInTime/headless/issues/33)) ~~Root `AGENTS.md` + `CLAUDE.md`; mirror the skill into `.claude/skills/` so Claude Code