refactor(macos): split exec approvals handler

This commit is contained in:
Peter Steinberger
2026-01-20 16:24:41 +00:00
parent 0b0d8b2406
commit 4fda10c508

View File

@@ -259,6 +259,20 @@ enum ExecApprovalsPromptPresenter {
@MainActor @MainActor
private enum ExecHostExecutor { private enum ExecHostExecutor {
private struct ExecApprovalContext {
let command: [String]
let displayCommand: String
let trimmedAgent: String?
let approvals: ExecApprovalsResolved
let security: ExecSecurity
let ask: ExecAsk
let autoAllowSkills: Bool
let env: [String: String]?
let resolution: ExecCommandResolution?
let allowlistMatch: ExecAllowlistEntry?
let skillAllow: Bool
}
private static let blockedEnvKeys: Set<String> = [ private static let blockedEnvKeys: Set<String> = [
"PATH", "PATH",
"NODE_OPTIONS", "NODE_OPTIONS",
@@ -277,14 +291,93 @@ private enum ExecHostExecutor {
static func handle(_ request: ExecHostRequest) async -> ExecHostResponse { static func handle(_ request: ExecHostRequest) async -> ExecHostResponse {
let command = request.command.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } let command = request.command.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
guard !command.isEmpty else { guard !command.isEmpty else {
return ExecHostResponse( return self.errorResponse(
type: "exec-res", code: "INVALID_REQUEST",
id: UUID().uuidString, message: "command required",
ok: false, reason: "invalid")
payload: nil,
error: ExecHostError(code: "INVALID_REQUEST", message: "command required", reason: "invalid"))
} }
let context = await self.buildContext(request: request, command: command)
if context.security == .deny {
return self.errorResponse(
code: "UNAVAILABLE",
message: "SYSTEM_RUN_DISABLED: security=deny",
reason: "security=deny")
}
let approvalDecision = request.approvalDecision
if approvalDecision == .deny {
return self.errorResponse(
code: "UNAVAILABLE",
message: "SYSTEM_RUN_DENIED: user denied",
reason: "user-denied")
}
var approvedByAsk = approvalDecision != nil
if self.requiresAsk(
ask: context.ask,
security: context.security,
allowlistMatch: context.allowlistMatch,
skillAllow: context.skillAllow),
approvalDecision == nil
{
let decision = ExecApprovalsPromptPresenter.prompt(
ExecApprovalPromptRequest(
command: context.displayCommand,
cwd: request.cwd,
host: "node",
security: context.security.rawValue,
ask: context.ask.rawValue,
agentId: context.trimmedAgent,
resolvedPath: context.resolution?.resolvedPath))
switch decision {
case .deny:
return self.errorResponse(
code: "UNAVAILABLE",
message: "SYSTEM_RUN_DENIED: user denied",
reason: "user-denied")
case .allowAlways:
approvedByAsk = true
self.persistAllowlistEntry(decision: decision, context: context)
case .allowOnce:
approvedByAsk = true
}
}
self.persistAllowlistEntry(decision: approvalDecision, context: context)
if context.security == .allowlist,
context.allowlistMatch == nil,
!context.skillAllow,
!approvedByAsk
{
return self.errorResponse(
code: "UNAVAILABLE",
message: "SYSTEM_RUN_DENIED: allowlist miss",
reason: "allowlist-miss")
}
if let match = context.allowlistMatch {
ExecApprovalsStore.recordAllowlistUse(
agentId: context.trimmedAgent,
pattern: match.pattern,
command: context.displayCommand,
resolvedPath: context.resolution?.resolvedPath)
}
if let errorResponse = await self.ensureScreenRecordingAccess(request.needsScreenRecording) {
return errorResponse
}
return await self.runCommand(
command: command,
cwd: request.cwd,
env: context.env,
timeoutMs: request.timeoutMs)
}
private static func buildContext(request: ExecHostRequest, command: [String]) async -> ExecApprovalContext {
let displayCommand = ExecCommandFormatter.displayString( let displayCommand = ExecCommandFormatter.displayString(
for: command, for: command,
rawCommand: request.rawCommand) rawCommand: request.rawCommand)
@@ -310,122 +403,72 @@ private enum ExecHostExecutor {
} else { } else {
skillAllow = false skillAllow = false
} }
return ExecApprovalContext(
command: command,
displayCommand: displayCommand,
trimmedAgent: trimmedAgent,
approvals: approvals,
security: security,
ask: ask,
autoAllowSkills: autoAllowSkills,
env: env,
resolution: resolution,
allowlistMatch: allowlistMatch,
skillAllow: skillAllow)
}
if security == .deny { private static func requiresAsk(
return ExecHostResponse( ask: ExecAsk,
type: "exec-res", security: ExecSecurity,
id: UUID().uuidString, allowlistMatch: ExecAllowlistEntry?,
ok: false, skillAllow: Bool) -> Bool
payload: nil, {
error: ExecHostError( if ask == .always { return true }
code: "UNAVAILABLE", if ask == .onMiss, security == .allowlist, allowlistMatch == nil, !skillAllow { return true }
message: "SYSTEM_RUN_DISABLED: security=deny", return false
reason: "security=deny")) }
private static func persistAllowlistEntry(
decision: ExecApprovalDecision?,
context: ExecApprovalContext)
{
guard decision == .allowAlways, context.security == .allowlist else { return }
guard let pattern = self.allowlistPattern(command: context.command, resolution: context.resolution) else {
return
} }
ExecApprovalsStore.addAllowlistEntry(agentId: context.trimmedAgent, pattern: pattern)
}
let requiresAsk: Bool = { private static func allowlistPattern(
if ask == .always { return true } command: [String],
if ask == .onMiss, security == .allowlist, allowlistMatch == nil, !skillAllow { return true } resolution: ExecCommandResolution?) -> String?
return false {
}() let pattern = resolution?.resolvedPath ?? resolution?.rawExecutable ?? command.first ?? ""
return pattern.isEmpty ? nil : pattern
}
let approvalDecision = request.approvalDecision private static func ensureScreenRecordingAccess(_ needsScreenRecording: Bool?) async -> ExecHostResponse? {
if approvalDecision == .deny { guard needsScreenRecording == true else { return nil }
return ExecHostResponse( let authorized = await PermissionManager
type: "exec-res", .status([.screenRecording])[.screenRecording] ?? false
id: UUID().uuidString, if authorized { return nil }
ok: false, return self.errorResponse(
payload: nil, code: "UNAVAILABLE",
error: ExecHostError( message: "PERMISSION_MISSING: screenRecording",
code: "UNAVAILABLE", reason: "permission:screenRecording")
message: "SYSTEM_RUN_DENIED: user denied", }
reason: "user-denied"))
}
var approvedByAsk = approvalDecision != nil private static func runCommand(
if requiresAsk, approvalDecision == nil { command: [String],
let decision = ExecApprovalsPromptPresenter.prompt( cwd: String?,
ExecApprovalPromptRequest( env: [String: String]?,
command: displayCommand, timeoutMs: Int?) async -> ExecHostResponse
cwd: request.cwd, {
host: "node", let timeoutSec = timeoutMs.flatMap { Double($0) / 1000.0 }
security: security.rawValue,
ask: ask.rawValue,
agentId: trimmedAgent,
resolvedPath: resolution?.resolvedPath))
switch decision {
case .deny:
return ExecHostResponse(
type: "exec-res",
id: UUID().uuidString,
ok: false,
payload: nil,
error: ExecHostError(
code: "UNAVAILABLE",
message: "SYSTEM_RUN_DENIED: user denied",
reason: "user-denied"))
case .allowAlways:
approvedByAsk = true
if security == .allowlist {
let pattern = resolution?.resolvedPath ?? resolution?.rawExecutable ?? command.first ?? ""
if !pattern.isEmpty {
ExecApprovalsStore.addAllowlistEntry(agentId: trimmedAgent, pattern: pattern)
}
}
case .allowOnce:
approvedByAsk = true
}
}
if approvalDecision == .allowAlways, security == .allowlist {
let pattern = resolution?.resolvedPath ?? resolution?.rawExecutable ?? command.first ?? ""
if !pattern.isEmpty {
ExecApprovalsStore.addAllowlistEntry(agentId: trimmedAgent, pattern: pattern)
}
}
if security == .allowlist, allowlistMatch == nil, !skillAllow, !approvedByAsk {
return ExecHostResponse(
type: "exec-res",
id: UUID().uuidString,
ok: false,
payload: nil,
error: ExecHostError(
code: "UNAVAILABLE",
message: "SYSTEM_RUN_DENIED: allowlist miss",
reason: "allowlist-miss"))
}
if let match = allowlistMatch {
ExecApprovalsStore.recordAllowlistUse(
agentId: trimmedAgent,
pattern: match.pattern,
command: displayCommand,
resolvedPath: resolution?.resolvedPath)
}
if request.needsScreenRecording == true {
let authorized = await PermissionManager
.status([.screenRecording])[.screenRecording] ?? false
if !authorized {
return ExecHostResponse(
type: "exec-res",
id: UUID().uuidString,
ok: false,
payload: nil,
error: ExecHostError(
code: "UNAVAILABLE",
message: "PERMISSION_MISSING: screenRecording",
reason: "permission:screenRecording"))
}
}
let timeoutSec = request.timeoutMs.flatMap { Double($0) / 1000.0 }
let result = await Task.detached { () -> ShellExecutor.ShellResult in let result = await Task.detached { () -> ShellExecutor.ShellResult in
await ShellExecutor.runDetailed( await ShellExecutor.runDetailed(
command: command, command: command,
cwd: request.cwd, cwd: cwd,
env: env, env: env,
timeout: timeoutSec) timeout: timeoutSec)
}.value }.value
@@ -436,7 +479,24 @@ private enum ExecHostExecutor {
stdout: result.stdout, stdout: result.stdout,
stderr: result.stderr, stderr: result.stderr,
error: result.errorMessage) error: result.errorMessage)
return ExecHostResponse( return self.successResponse(payload)
}
private static func errorResponse(
code: String,
message: String,
reason: String?) -> ExecHostResponse
{
ExecHostResponse(
type: "exec-res",
id: UUID().uuidString,
ok: false,
payload: nil,
error: ExecHostError(code: code, message: message, reason: reason))
}
private static func successResponse(_ payload: ExecHostRunResult) -> ExecHostResponse {
ExecHostResponse(
type: "exec-res", type: "exec-res",
id: UUID().uuidString, id: UUID().uuidString,
ok: true, ok: true,