feat: add exec approvals allowlists

This commit is contained in:
Peter Steinberger
2026-01-18 01:33:52 +00:00
parent 3a0fd6be3c
commit 0674f1fa3c
21 changed files with 1019 additions and 101 deletions

View File

@@ -428,8 +428,32 @@ actor MacNodeRuntime {
return Self.errorResponse(req, code: .invalidRequest, message: "INVALID_REQUEST: command required")
}
let wasAllowlisted = SystemRunAllowlist.contains(command)
switch Self.systemRunPolicy() {
let trimmedAgent = params.agentId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let agentId = trimmedAgent.isEmpty ? nil : trimmedAgent
let policy = SystemRunPolicy.load(agentId: agentId)
let allowlistEntries = SystemRunAllowlistStore.load(agentId: agentId)
let resolution = SystemRunCommandResolution.resolve(command: command, cwd: params.cwd)
let allowlistMatch = SystemRunAllowlistStore.match(
command: command,
resolution: resolution,
entries: allowlistEntries)
let autoAllowSkills = MacNodeConfigFile.systemRunAutoAllowSkills(agentId: agentId) ?? false
let skillAllow: Bool
if autoAllowSkills, let name = resolution?.executableName {
let bins = await SkillBinsCache.shared.currentBins()
skillAllow = bins.contains(name)
} else {
skillAllow = false
}
let shouldPrompt: Bool = {
if policy == .never { return false }
if allowlistMatch != nil { return false }
if skillAllow { return false }
return policy == .ask
}()
switch policy {
case .never:
return Self.errorResponse(
req,
@@ -438,16 +462,24 @@ actor MacNodeRuntime {
case .always:
break
case .ask:
if !wasAllowlisted {
if shouldPrompt {
let services = await self.mainActorServices()
let decision = await services.confirmSystemRun(
let decision = await services.confirmSystemRun(context: SystemRunPromptContext(
command: SystemRunAllowlist.displayString(for: command),
cwd: params.cwd)
cwd: params.cwd,
agentId: agentId,
executablePath: resolution?.resolvedPath))
switch decision {
case .allowOnce:
break
case .allowAlways:
SystemRunAllowlist.add(command)
if let resolvedPath = resolution?.resolvedPath, !resolvedPath.isEmpty {
_ = SystemRunAllowlistStore.add(pattern: resolvedPath, agentId: agentId)
} else if let raw = command.first?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty
{
_ = SystemRunAllowlistStore.add(pattern: raw, agentId: agentId)
}
case .deny:
return Self.errorResponse(
req,
@@ -457,6 +489,14 @@ actor MacNodeRuntime {
}
}
if let match = allowlistMatch {
SystemRunAllowlistStore.markUsed(
entryId: match.id,
command: command,
resolvedPath: resolution?.resolvedPath,
agentId: agentId)
}
let env = Self.sanitizedEnv(params.env)
if params.needsScreenRecording == true {

View File

@@ -9,6 +9,13 @@ enum SystemRunDecision: Sendable {
case deny
}
struct SystemRunPromptContext: Sendable {
let command: String
let cwd: String?
let agentId: String?
let executablePath: String?
}
@MainActor
protocol MacNodeRuntimeMainActorServices: Sendable {
func recordScreen(
@@ -25,7 +32,7 @@ protocol MacNodeRuntimeMainActorServices: Sendable {
maxAgeMs: Int?,
timeoutMs: Int?) async throws -> CLLocation
func confirmSystemRun(command: String, cwd: String?) async -> SystemRunDecision
func confirmSystemRun(context: SystemRunPromptContext) async -> SystemRunDecision
}
@MainActor
@@ -67,16 +74,24 @@ final class LiveMacNodeRuntimeMainActorServices: MacNodeRuntimeMainActorServices
timeoutMs: timeoutMs)
}
func confirmSystemRun(command: String, cwd: String?) async -> SystemRunDecision {
func confirmSystemRun(context: SystemRunPromptContext) async -> SystemRunDecision {
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = "Allow this command?"
var details = "Clawdbot wants to run:\n\n\(command)"
let trimmedCwd = cwd?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
var details = "Clawdbot wants to run:\n\n\(context.command)"
let trimmedCwd = context.cwd?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmedCwd.isEmpty {
details += "\n\nWorking directory:\n\(trimmedCwd)"
}
let trimmedAgent = context.agentId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmedAgent.isEmpty {
details += "\n\nAgent:\n\(trimmedAgent)"
}
let trimmedPath = context.executablePath?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmedPath.isEmpty {
details += "\n\nExecutable:\n\(trimmedPath)"
}
details += "\n\nThis runs on this Mac via node mode."
alert.informativeText = details