From 1092b30531416ad3550af6a8dc5e31f858b6c9bc Mon Sep 17 00:00:00 2001 From: Nimrod Gutman Date: Wed, 21 Jan 2026 20:31:12 +0200 Subject: [PATCH] fix(node): handle invoke approvals and errors --- .../Clawdbot/NodeMode/MacNodeRuntime.swift | 58 +++++++++++++++++-- .../Sources/ClawdbotKit/GatewayChannel.swift | 9 ++- .../ClawdbotKit/GatewayNodeSession.swift | 4 +- .../Sources/ClawdbotKit/SystemCommands.swift | 5 +- 4 files changed, 68 insertions(+), 8 deletions(-) diff --git a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift index 16f340415..ae8d87136 100644 --- a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift +++ b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift @@ -486,8 +486,10 @@ actor MacNodeRuntime { return false }() - let approvedByAsk = params.approved == true - if requiresAsk, !approvedByAsk { + let decisionFromParams = Self.parseApprovalDecision(params.approvalDecision) + var approvedByAsk = params.approved == true || decisionFromParams != nil + var persistAllowlist = decisionFromParams == .allowAlways + if decisionFromParams == .deny { await self.emitExecEvent( "exec.denied", payload: ExecEventPayload( @@ -495,11 +497,53 @@ actor MacNodeRuntime { runId: runId, host: "node", command: displayCommand, - reason: "approval-required")) + reason: "user-denied")) return Self.errorResponse( req, code: .unavailable, - message: "SYSTEM_RUN_DENIED: approval required") + message: "SYSTEM_RUN_DENIED: user denied") + } + if requiresAsk, !approvedByAsk { + let decision = await MainActor.run { + ExecApprovalsPromptPresenter.prompt( + ExecApprovalPromptRequest( + command: displayCommand, + cwd: params.cwd, + host: "node", + security: security.rawValue, + ask: ask.rawValue, + agentId: agentId, + resolvedPath: resolution?.resolvedPath)) + } + switch decision { + case .deny: + await self.emitExecEvent( + "exec.denied", + payload: ExecEventPayload( + sessionKey: sessionKey, + runId: runId, + host: "node", + command: displayCommand, + reason: "user-denied")) + return Self.errorResponse( + req, + code: .unavailable, + message: "SYSTEM_RUN_DENIED: user denied") + case .allowAlways: + approvedByAsk = true + persistAllowlist = true + case .allowOnce: + approvedByAsk = true + } + } + if persistAllowlist, security == .allowlist { + let pattern = resolution?.resolvedPath + ?? resolution?.rawExecutable + ?? command.first + ?? "" + if !pattern.isEmpty { + ExecApprovalsStore.addAllowlistEntry(agentId: agentId, pattern: pattern) + } } if security == .allowlist, allowlistMatch == nil, !skillAllow, !approvedByAsk { @@ -763,6 +807,12 @@ extension MacNodeRuntime { UserDefaults.standard.object(forKey: cameraEnabledKey) as? Bool ?? false } + private static func parseApprovalDecision(_ raw: String?) -> ExecApprovalDecision? { + let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + guard !trimmed.isEmpty else { return nil } + return ExecApprovalDecision(rawValue: trimmed) + } + private static let blockedEnvKeys: Set = [ "PATH", "NODE_OPTIONS", diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift index 813af5c0a..90a19a64b 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift @@ -571,7 +571,14 @@ public actor GatewayChannelActor { id: id, method: method, params: paramsObject) - let data = try self.encoder.encode(frame) + let data: Data + do { + data = try self.encoder.encode(frame) + } catch { + self.logger.error( + "gateway request encode failed \(method, privacy: .public) error=\(error.localizedDescription, privacy: .public)") + throw error + } let response = try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in self.pending[id] = cont Task { [weak self] in diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayNodeSession.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayNodeSession.swift index 2cc26a51d..a2ac2ad6d 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayNodeSession.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayNodeSession.swift @@ -219,8 +219,8 @@ public actor GatewayNodeSession { } if let error = response.error { params["error"] = AnyCodable([ - "code": AnyCodable(error.code.rawValue), - "message": AnyCodable(error.message), + "code": error.code.rawValue, + "message": error.message, ]) } do { diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/SystemCommands.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/SystemCommands.swift index 89857da9d..bfe980f41 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/SystemCommands.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/SystemCommands.swift @@ -30,6 +30,7 @@ public struct ClawdbotSystemRunParams: Codable, Sendable, Equatable { public var agentId: String? public var sessionKey: String? public var approved: Bool? + public var approvalDecision: String? public init( command: [String], @@ -40,7 +41,8 @@ public struct ClawdbotSystemRunParams: Codable, Sendable, Equatable { needsScreenRecording: Bool? = nil, agentId: String? = nil, sessionKey: String? = nil, - approved: Bool? = nil) + approved: Bool? = nil, + approvalDecision: String? = nil) { self.command = command self.rawCommand = rawCommand @@ -51,6 +53,7 @@ public struct ClawdbotSystemRunParams: Codable, Sendable, Equatable { self.agentId = agentId self.sessionKey = sessionKey self.approved = approved + self.approvalDecision = approvalDecision } }