import Foundation // MARK: - Approvals (BACKEND_PROTOCOL §4) public enum Risk: String, Sendable, Codable { case readOnly, write, execute, network, destructive, unknown /// Run a command on the host machine, outside the sandbox container (HOST_EXEC). /// The highest-trust gate: always surfaced, never auto-approved. case hostExec } public enum AlwaysScope: String, Sendable, Codable { /// This exact tool + input, this session. case session /// Any call to this tool, this session. case toolName /// Tool + argument pattern (e.g. Bash(git *)). case toolNameWithPattern } public struct ApprovalRequest: Sendable, Codable, Equatable, Identifiable { public let id: ApprovalID public let sessionID: SessionID public let toolCallID: String? public let toolName: String public let input: JSONValue public let title: String public let risk: Risk public let suggested: Decision? public let createdAt: Date public init( id: ApprovalID, sessionID: SessionID, toolCallID: String?, toolName: String, input: JSONValue, title: String, risk: Risk, suggested: Decision? = nil, createdAt: Date ) { self.id = id self.sessionID = sessionID self.toolCallID = toolCallID self.toolName = toolName self.input = input self.title = title self.risk = risk self.suggested = suggested self.createdAt = createdAt } } public enum Decision: Sendable, Equatable { case allow(updatedInput: JSONValue? = nil) case allowAlways(AlwaysScope) case deny(reason: String?) /// Deny + stop the whole run. case cancelRun } extension Decision: Codable { private enum CodingKeys: String, CodingKey { case type, updatedInput, scope, reason } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(String.self, forKey: .type) switch type { case "allow": self = .allow(updatedInput: try container.decodeIfPresent(JSONValue.self, forKey: .updatedInput)) case "allowAlways": self = .allowAlways(try container.decode(AlwaysScope.self, forKey: .scope)) case "deny": self = .deny(reason: try container.decodeIfPresent(String.self, forKey: .reason)) case "cancelRun": self = .cancelRun default: throw DecodingError.dataCorruptedError( forKey: .type, in: container, debugDescription: "Unknown decision type \(type)") } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case .allow(let updatedInput): try container.encode("allow", forKey: .type) try container.encodeIfPresent(updatedInput, forKey: .updatedInput) case .allowAlways(let scope): try container.encode("allowAlways", forKey: .type) try container.encode(scope, forKey: .scope) case .deny(let reason): try container.encode("deny", forKey: .type) try container.encodeIfPresent(reason, forKey: .reason) case .cancelRun: try container.encode("cancelRun", forKey: .type) } } } public struct ApprovalResolved: Sendable, Codable, Equatable { public let id: ApprovalID public let decision: Decision /// "mac-ui" | "iphone:" | "always-rule" — first responder wins. public let decidedBy: String public let decidedAt: Date public init(id: ApprovalID, decision: Decision, decidedBy: String, decidedAt: Date) { self.id = id self.decision = decision self.decidedBy = decidedBy self.decidedAt = decidedAt } }