fix(macos): harden exec allowlist shell-chain checks

This commit is contained in:
Peter Steinberger
2026-02-21 16:27:05 +01:00
parent 8178ea472d
commit 5da03e6221
9 changed files with 507 additions and 133 deletions

View File

@@ -360,7 +360,9 @@ private enum ExecHostExecutor {
let autoAllowSkills: Bool
let env: [String: String]?
let resolution: ExecCommandResolution?
let allowlistMatch: ExecAllowlistEntry?
let allowlistResolutions: [ExecCommandResolution]
let allowlistMatches: [ExecAllowlistEntry]
let allowlistSatisfied: Bool
let skillAllow: Bool
}
@@ -393,7 +395,7 @@ private enum ExecHostExecutor {
if ExecApprovalHelpers.requiresAsk(
ask: context.ask,
security: context.security,
allowlistMatch: context.allowlistMatch,
allowlistMatch: context.allowlistSatisfied ? context.allowlistMatches.first : nil,
skillAllow: context.skillAllow),
approvalDecision == nil
{
@@ -425,7 +427,7 @@ private enum ExecHostExecutor {
self.persistAllowlistEntry(decision: approvalDecision, context: context)
if context.security == .allowlist,
context.allowlistMatch == nil,
!context.allowlistSatisfied,
!context.skillAllow,
!approvedByAsk
{
@@ -435,12 +437,21 @@ private enum ExecHostExecutor {
reason: "allowlist-miss")
}
if let match = context.allowlistMatch {
ExecApprovalsStore.recordAllowlistUse(
agentId: context.trimmedAgent,
pattern: match.pattern,
command: context.displayCommand,
resolvedPath: context.resolution?.resolvedPath)
if context.allowlistSatisfied {
var seenPatterns = Set<String>()
for (idx, match) in context.allowlistMatches.enumerated() {
if !seenPatterns.insert(match.pattern).inserted {
continue
}
let resolvedPath = idx < context.allowlistResolutions.count
? context.allowlistResolutions[idx].resolvedPath
: nil
ExecApprovalsStore.recordAllowlistUse(
agentId: context.trimmedAgent,
pattern: match.pattern,
command: context.displayCommand,
resolvedPath: resolvedPath)
}
}
if let errorResponse = await self.ensureScreenRecordingAccess(request.needsScreenRecording) {
@@ -465,18 +476,22 @@ private enum ExecHostExecutor {
let ask = approvals.agent.ask
let autoAllowSkills = approvals.agent.autoAllowSkills
let env = self.sanitizedEnv(request.env)
let resolution = ExecCommandResolution.resolve(
let allowlistResolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: request.rawCommand,
cwd: request.cwd,
env: env)
let allowlistMatch = security == .allowlist
? ExecAllowlistMatcher.match(entries: approvals.allowlist, resolution: resolution)
: nil
let resolution = allowlistResolutions.first
let allowlistMatches = security == .allowlist
? ExecAllowlistMatcher.matchAll(entries: approvals.allowlist, resolutions: allowlistResolutions)
: []
let allowlistSatisfied = security == .allowlist &&
!allowlistResolutions.isEmpty &&
allowlistMatches.count == allowlistResolutions.count
let skillAllow: Bool
if autoAllowSkills, let name = resolution?.executableName {
if autoAllowSkills, !allowlistResolutions.isEmpty {
let bins = await SkillBinsCache.shared.currentBins()
skillAllow = bins.contains(name)
skillAllow = allowlistResolutions.allSatisfy { bins.contains($0.executableName) }
} else {
skillAllow = false
}
@@ -490,7 +505,9 @@ private enum ExecHostExecutor {
autoAllowSkills: autoAllowSkills,
env: env,
resolution: resolution,
allowlistMatch: allowlistMatch,
allowlistResolutions: allowlistResolutions,
allowlistMatches: allowlistMatches,
allowlistSatisfied: allowlistSatisfied,
skillAllow: skillAllow)
}
@@ -499,13 +516,18 @@ private enum ExecHostExecutor {
context: ExecApprovalContext)
{
guard decision == .allowAlways, context.security == .allowlist else { return }
guard let pattern = ExecApprovalHelpers.allowlistPattern(
command: context.command,
resolution: context.resolution)
else {
return
var seenPatterns = Set<String>()
for candidate in context.allowlistResolutions {
guard let pattern = ExecApprovalHelpers.allowlistPattern(
command: context.command,
resolution: candidate)
else {
continue
}
if seenPatterns.insert(pattern).inserted {
ExecApprovalsStore.addAllowlistEntry(agentId: context.trimmedAgent, pattern: pattern)
}
}
ExecApprovalsStore.addAllowlistEntry(agentId: context.trimmedAgent, pattern: pattern)
}
private static func ensureScreenRecordingAccess(_ needsScreenRecording: Bool?) async -> ExecHostResponse? {