fix(macos): unify exec allowlist validation pipeline

This commit is contained in:
Peter Steinberger
2026-02-21 23:09:03 +01:00
parent 61dc7ac679
commit 2028ca4428
6 changed files with 322 additions and 93 deletions

View File

@@ -108,10 +108,9 @@ struct SystemRunSettingsView: View {
TextField("Add allowlist path pattern (case-insensitive globs)", text: self.$newPattern)
.textFieldStyle(.roundedBorder)
Button("Add") {
let pattern = self.newPattern.trimmingCharacters(in: .whitespacesAndNewlines)
guard self.model.isPathPattern(pattern) else { return }
self.model.addEntry(pattern)
self.newPattern = ""
if self.model.addEntry(self.newPattern) == nil {
self.newPattern = ""
}
}
.buttonStyle(.bordered)
.disabled(!self.model.isPathPattern(self.newPattern))
@@ -120,6 +119,11 @@ struct SystemRunSettingsView: View {
Text("Path patterns only. Basename entries like \"echo\" are ignored.")
.font(.footnote)
.foregroundStyle(.secondary)
if let validationMessage = self.model.allowlistValidationMessage {
Text(validationMessage)
.font(.footnote)
.foregroundStyle(.orange)
}
if self.model.entries.isEmpty {
Text("No allowlisted commands yet.")
@@ -238,6 +242,7 @@ final class ExecApprovalsSettingsModel {
var autoAllowSkills = false
var entries: [ExecAllowlistEntry] = []
var skillBins: [String] = []
var allowlistValidationMessage: String?
var agentPickerIds: [String] {
[Self.defaultsScopeId] + self.agentIds
@@ -293,6 +298,7 @@ final class ExecApprovalsSettingsModel {
func selectAgent(_ id: String) {
self.selectedAgentId = id
self.allowlistValidationMessage = nil
self.loadSettings(for: id)
Task { await self.refreshSkillBins() }
}
@@ -305,6 +311,7 @@ final class ExecApprovalsSettingsModel {
self.askFallback = defaults.askFallback
self.autoAllowSkills = defaults.autoAllowSkills
self.entries = []
self.allowlistValidationMessage = nil
return
}
let resolved = ExecApprovalsStore.resolve(agentId: agentId)
@@ -314,6 +321,7 @@ final class ExecApprovalsSettingsModel {
self.autoAllowSkills = resolved.agent.autoAllowSkills
self.entries = resolved.allowlist
.sorted { $0.pattern.localizedCaseInsensitiveCompare($1.pattern) == .orderedAscending }
self.allowlistValidationMessage = nil
}
func setSecurity(_ security: ExecSecurity) {
@@ -371,30 +379,45 @@ final class ExecApprovalsSettingsModel {
Task { await self.refreshSkillBins(force: enabled) }
}
func addEntry(_ pattern: String) {
guard !self.isDefaultsScope else { return }
let trimmed = pattern.trimmingCharacters(in: .whitespacesAndNewlines)
guard self.isPathPattern(trimmed) else { return }
self.entries.append(ExecAllowlistEntry(pattern: trimmed, lastUsedAt: nil))
ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
@discardableResult
func addEntry(_ pattern: String) -> ExecAllowlistPatternValidationReason? {
guard !self.isDefaultsScope else { return nil }
switch ExecApprovalHelpers.validateAllowlistPattern(pattern) {
case .valid(let normalizedPattern):
self.entries.append(ExecAllowlistEntry(pattern: normalizedPattern, lastUsedAt: nil))
let rejected = ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
self.allowlistValidationMessage = rejected.first?.reason.message
return rejected.first?.reason
case .invalid(let reason):
self.allowlistValidationMessage = reason.message
return reason
}
}
func updateEntry(_ entry: ExecAllowlistEntry, id: UUID) {
guard !self.isDefaultsScope else { return }
guard let index = self.entries.firstIndex(where: { $0.id == id }) else { return }
@discardableResult
func updateEntry(_ entry: ExecAllowlistEntry, id: UUID) -> ExecAllowlistPatternValidationReason? {
guard !self.isDefaultsScope else { return nil }
guard let index = self.entries.firstIndex(where: { $0.id == id }) else { return nil }
var next = entry
let trimmed = next.pattern.trimmingCharacters(in: .whitespacesAndNewlines)
guard self.isPathPattern(trimmed) else { return }
next.pattern = trimmed
switch ExecApprovalHelpers.validateAllowlistPattern(next.pattern) {
case .valid(let normalizedPattern):
next.pattern = normalizedPattern
case .invalid(let reason):
self.allowlistValidationMessage = reason.message
return reason
}
self.entries[index] = next
ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
let rejected = ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
self.allowlistValidationMessage = rejected.first?.reason.message
return rejected.first?.reason
}
func removeEntry(id: UUID) {
guard !self.isDefaultsScope else { return }
guard let index = self.entries.firstIndex(where: { $0.id == id }) else { return }
self.entries.remove(at: index)
ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
let rejected = ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
self.allowlistValidationMessage = rejected.first?.reason.message
}
func entry(for id: UUID) -> ExecAllowlistEntry? {
@@ -402,9 +425,7 @@ final class ExecApprovalsSettingsModel {
}
func isPathPattern(_ pattern: String) -> Bool {
let trimmed = pattern.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return false }
return trimmed.contains("/") || trimmed.contains("~") || trimmed.contains("\\")
ExecApprovalHelpers.isPathPattern(pattern)
}
func refreshSkillBins(force: Bool = false) async {