fix(macos): preserve native command ownership and consent (#118914)

This commit is contained in:
Peter Steinberger
2026-08-03 12:50:11 -07:00
committed by GitHub
parent 3c858f7a92
commit 7ae6a950f6
2 changed files with 106 additions and 12 deletions
@@ -186,9 +186,6 @@ actor MacNodeRuntime {
/// One branch per advertised native command keeps command ownership explicit.
func handleInvoke(_ req: BridgeInvokeRequest) async -> BridgeInvokeResponse {
let command = req.command
if let nodeHostWorker, await nodeHostWorker.supports(command) {
return await nodeHostWorker.invoke(req)
}
if self.isCanvasCommand(command), !Self.canvasEnabled() {
return BridgeInvokeResponse(
id: req.id,
@@ -230,6 +227,9 @@ actor MacNodeRuntime {
MacNodeClaudeSessionCatalogContract.readCommand:
return try await self.handleClaudeSessionInvoke(req)
default:
if let nodeHostWorker, await nodeHostWorker.supports(command) {
return await nodeHostWorker.invoke(req)
}
return Self.errorResponse(req, code: .invalidRequest, message: "INVALID_REQUEST: unknown command")
}
} catch let error as MacNodeCodexThreadCatalog.CatalogError {
@@ -6,13 +6,17 @@ import Testing
private struct WorkerBackpressureTimeout: Error {}
private actor StubMacNodeHostWorker: MacNodeHostWorking {
let manifest = MacNodeHostManifest(
version: "test",
caps: ["system", "mcp"],
commands: ["system.run", "mcp.tools.call.v1"],
pathEnv: "/usr/bin:/bin")
let manifest: MacNodeHostManifest
private var requests: [BridgeInvokeRequest] = []
init(commands: [String] = ["system.run", "mcp.tools.call.v1"]) {
self.manifest = MacNodeHostManifest(
version: "test",
caps: ["system", "mcp"],
commands: commands,
pathEnv: "/usr/bin:/bin")
}
func start(command _: [String]) async throws -> MacNodeHostManifest { self.manifest }
func supports(_ command: String) async -> Bool { self.manifest.commands.contains(command) }
@@ -90,18 +94,108 @@ struct MacNodeHostWorkerTests {
await worker.stop()
}
@Test func `Mac runtime forwards CLI node commands to the shared worker`() async {
let worker = StubMacNodeHostWorker()
@Test(arguments: [
OpenClawSystemCommand.run.rawValue,
"mcp.tools.call.v1",
"codex.terminal.resume.v1",
])
func `Mac runtime forwards worker-owned commands to the shared worker`(command: String) async {
let worker = StubMacNodeHostWorker(commands: [command])
let runtime = MacNodeRuntime(nodeHostWorker: worker)
let response = await runtime.handleInvoke(BridgeInvokeRequest(
id: "worker-run",
command: OpenClawSystemCommand.run.rawValue,
command: command,
paramsJSON: #"{"command":["/usr/bin/true"]}"#))
#expect(response.ok)
#expect(response.payloadJSON == #"{"owner":"cli"}"#)
#expect(await worker.invokedCommands() == [OpenClawSystemCommand.run.rawValue])
#expect(await worker.invokedCommands() == [command])
}
@Test(arguments: [
MacNodeCodexThreadCatalogContract.listCommand,
MacNodeCodexThreadCatalogContract.turnsCommand,
MacNodeClaudeSessionCatalogContract.listCommand,
MacNodeClaudeSessionCatalogContract.readCommand,
])
func `native session catalogs own commands shared with the worker`(command: String) async {
let worker = StubMacNodeHostWorker(commands: [command])
let payload = #"{"owner":"native"}"#
let runtime = MacNodeRuntime(
nodeHostWorker: worker,
codexThreadCatalogEnabled: { true },
codexThreadListRequest: { _ in payload },
codexThreadTurnsRequest: { _ in payload },
claudeSessionCatalogEnabled: { true },
claudeSessionListRequest: { _ in payload },
claudeSessionReadRequest: { _ in payload })
let response = await runtime.handleInvoke(BridgeInvokeRequest(
id: "native-catalog",
command: command,
paramsJSON: #"{"limit":1}"#))
#expect(response.ok)
#expect(response.payloadJSON == payload)
#expect(await worker.invokedCommands().isEmpty)
}
@Test(arguments: [
(
MacNodeCodexThreadCatalogContract.listCommand,
"UNAVAILABLE: Codex session catalog is disabled"
),
(
MacNodeCodexThreadCatalogContract.turnsCommand,
"UNAVAILABLE: Codex session catalog is disabled"
),
(
MacNodeClaudeSessionCatalogContract.listCommand,
"UNAVAILABLE: Claude session catalog is disabled"
),
(
MacNodeClaudeSessionCatalogContract.readCommand,
"UNAVAILABLE: Claude session catalog is disabled"
),
(
OpenClawComputerCommand.act.rawValue,
"COMPUTER_DISABLED: enable Computer Control in Settings"
),
])
func `worker cannot bypass native capability consent`(command: String, expectedMessage: String) async {
let worker = StubMacNodeHostWorker(commands: [command])
let runtime = MacNodeRuntime(
nodeHostWorker: worker,
computerControlEnabled: { false },
codexThreadCatalogEnabled: { false },
claudeSessionCatalogEnabled: { false })
let response = await runtime.handleInvoke(BridgeInvokeRequest(
id: "native-disabled",
command: command))
#expect(!response.ok)
#expect(response.error?.code == .unavailable)
#expect(response.error?.message == expectedMessage)
#expect(await worker.invokedCommands().isEmpty)
}
@Test(arguments: [OpenClawCanvasCommand.present.rawValue, "canvas.plugin.render"])
func `worker cannot bypass the canvas namespace consent gate`(command: String) async {
await TestIsolation.withUserDefaultsValues([canvasEnabledKey: false]) {
let worker = StubMacNodeHostWorker(commands: [command])
let runtime = MacNodeRuntime(nodeHostWorker: worker)
let response = await runtime.handleInvoke(BridgeInvokeRequest(
id: "canvas-disabled",
command: command))
#expect(!response.ok)
#expect(response.error?.code == .unavailable)
#expect(response.error?.message == "CANVAS_DISABLED: enable Canvas in Settings")
#expect(await worker.invokedCommands().isEmpty)
}
}
@Test func `capability union preserves native order and adds worker commands once`() {