spb/zyquo-agent Public MIT
The autonomous agent that actually operates your Mac — plans, runs real commands, verifies its own work.
Swift 94.7%
Shell 4.1%
Python 0.7%
Makefile 0.5%
1//2// SafetySettingsTab.swift3// Zyquo Agent4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Settings › Safety — default safety mode, the editable allow/deny rule9// lists, the read-only built-in destructive-pattern list, the AppleScript10// approval toggle and the workspace-escape policy.11//12// The rule editor reads/writes the same policy-rules.json the PolicyEngine13// uses (through PersistenceService). Each run constructs a fresh14// PolicyEngine at task start, which loads the rules then — edits here apply15// to the NEXT run, not to a run already in flight.16//1718import SwiftUI1920struct SafetySettingsTab: View {21 @EnvironmentObject private var settings: AgentSettingsStore2223 var body: some View {24 Form {25 Section("Defaults") {26 Picker("Default safety mode for new tasks", selection: $settings.settings.defaultSafetyMode) {27 ForEach(SafetyMode.allCases) { mode in28 Text(mode.displayName).tag(mode)29 }30 }31 .pickerStyle(.segmented)32 Text(modeDescription)33 .font(ZyquoFont.caption)34 .foregroundStyle(ZyquoColor.textTertiary)35 }3637 Section("AppleScript & workspace boundaries") {38 Toggle("Require approval for AppleScript", isOn: $settings.settings.requireApprovalForAppleScript)39 Text("Manual and Guarded modes always ask before AppleScript runs. This setting is stored for the Autonomous-mode engine hook (not yet enforced there — Autonomous currently auto-runs only scripts with no risky patterns).")40 .font(ZyquoFont.caption)41 .foregroundStyle(ZyquoColor.textTertiary)42 Picker("File access outside the workspace", selection: $settings.settings.workspaceEscapePolicy) {43 ForEach(WorkspaceEscapePolicy.allCases) { policy in44 Text(policy.displayName).tag(policy)45 }46 }47 Text("The engine currently always asks on any read or write outside the task workspace, in every mode. “Deny without asking” is stored for a future engine hook.")48 .font(ZyquoFont.caption)49 .foregroundStyle(ZyquoColor.textTertiary)50 }5152 PolicyRulesSection()5354 Section("Built-in destructive patterns (always ask — every mode)") {55 DisclosureGroup("Never run (hard deny)") {56 ForEach(ShellCommandAnalyzer.hardDenyDescriptions, id: \.self) { item in57 patternRow(item, color: ZyquoColor.danger)58 }59 }60 DisclosureGroup("Always require approval") {61 ForEach(ShellCommandAnalyzer.alwaysAskDescriptions, id: \.self) { item in62 patternRow(item, color: ZyquoColor.warning)63 }64 }65 Text("These circuit breakers are built in and cannot be disabled — not even by Autonomous mode or a remembered allow rule.")66 .font(ZyquoFont.caption)67 .foregroundStyle(ZyquoColor.textTertiary)68 }69 }70 .formStyle(.grouped)71 }7273 private var modeDescription: String {74 switch settings.settings.defaultSafetyMode {75 case .manual: return "Manual — approve every action."76 case .guarded: return "Guarded — safe/read-only actions run automatically, anything mutating asks."77 case .autonomous: return "Autonomous — runs freely within budget; destructive and elevated actions still ask."78 }79 }8081 private func patternRow(_ text: String, color: Color) -> some View {82 HStack(alignment: .firstTextBaseline, spacing: ZyquoSpacing.xxs) {83 Circle()84 .fill(color)85 .frame(width: 5, height: 5)86 .padding(.top, 4)87 Text(text)88 .font(ZyquoFont.body(size: 12))89 .foregroundStyle(ZyquoColor.textSecondary)90 .fixedSize(horizontal: false, vertical: true)91 }92 }93}9495// MARK: - Allow/deny rule editor9697/// Edits the PolicyEngine's stored rules through the same policy-rules.json98/// document (PersistenceService). Live engines load rules at task start.99private struct PolicyRulesSection: View {100 @State private var rules = StoredPolicyRules()101 @State private var newPattern = ""102 @State private var newKind = "bash"103 @State private var newList: RuleList = .allow104105 private enum RuleList: String, CaseIterable, Identifiable {106 case allow = "Allow"107 case deny = "Deny"108 var id: String { rawValue }109 }110111 private static let rulesFileName = "policy-rules.json"112113 var body: some View {114 Section("Allow / deny rules") {115 Text("Token-prefix rules evaluated per subcommand: “brew list” matches `brew list --versions` but not `brew install`. Deny rules always win; allow rules can never cover a built-in destructive pattern. Changes apply to the next run.")116 .font(ZyquoFont.caption)117 .foregroundStyle(ZyquoColor.textTertiary)118119 if rules.allow.isEmpty && rules.deny.isEmpty {120 Text("No rules yet — “Approve & remember” on an approval card adds allow rules here.")121 .font(ZyquoFont.body(size: 12))122 .foregroundStyle(ZyquoColor.textTertiary)123 }124 ForEach(rules.deny, id: \.self) { rule in125 ruleRow(rule, isDeny: true)126 }127 ForEach(rules.allow, id: \.self) { rule in128 ruleRow(rule, isDeny: false)129 }130131 HStack(spacing: ZyquoSpacing.xs) {132 Picker("", selection: $newList) {133 ForEach(RuleList.allCases) { list in Text(list.rawValue).tag(list) }134 }135 .labelsHidden()136 .frame(width: 90)137 Picker("", selection: $newKind) {138 Text("bash").tag("bash")139 Text("osascript").tag("osascript")140 }141 .labelsHidden()142 .frame(width: 110)143 TextField("Pattern (e.g. brew list)", text: $newPattern)144 .textFieldStyle(.roundedBorder)145 .font(ZyquoFont.code(size: 11))146 .onSubmit(addRule)147 Button("Add") { addRule() }148 .controlSize(.small)149 .disabled(newPattern.trimmingCharacters(in: .whitespaces).isEmpty)150 }151 }152 .onAppear(perform: load)153 }154155 private func ruleRow(_ rule: StoredPolicyRule, isDeny: Bool) -> some View {156 HStack(spacing: ZyquoSpacing.xs) {157 ZyquoBadge(158 text: isDeny ? "deny" : "allow",159 color: isDeny ? ZyquoColor.danger : ZyquoColor.success160 )161 Text(rule.kind)162 .font(ZyquoFont.code(size: 11))163 .foregroundStyle(ZyquoColor.textTertiary)164 .frame(width: 70, alignment: .leading)165 Text(rule.pattern)166 .font(ZyquoFont.code(size: 11.5))167 .foregroundStyle(ZyquoColor.textPrimary)168 Spacer()169 Button {170 remove(rule, fromDeny: isDeny)171 } label: {172 Image(systemName: "trash")173 .font(.system(size: 10))174 .foregroundStyle(ZyquoColor.danger)175 }176 .buttonStyle(.plain)177 .help("Remove rule")178 }179 }180181 // MARK: Persistence (same document the PolicyEngine loads per task start)182183 private func load() {184 rules = PersistenceService.shared.load(StoredPolicyRules.self, from: Self.rulesFileName)185 ?? StoredPolicyRules()186 }187188 private func save() {189 PersistenceService.shared.save(rules, to: Self.rulesFileName)190 }191192 private func addRule() {193 let pattern = newPattern.trimmingCharacters(in: .whitespaces)194 guard !pattern.isEmpty else { return }195 let rule = StoredPolicyRule(kind: newKind, pattern: pattern)196 switch newList {197 case .allow:198 if !rules.allow.contains(rule) { rules.allow.append(rule) }199 case .deny:200 if !rules.deny.contains(rule) { rules.deny.append(rule) }201 }202 newPattern = ""203 save()204 }205206 private func remove(_ rule: StoredPolicyRule, fromDeny: Bool) {207 if fromDeny {208 rules.deny.removeAll { $0 == rule }209 } else {210 rules.allow.removeAll { $0 == rule }211 }212 save()213 }214}215