// // AuditLog.swift // Zyquo Agent // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Append-only record of every executed action: timestamp, kind, exact // payload, cwd, policy ruling, exit code, truncated output. Stored as JSONL // next to the task so nothing the agent does is invisible; viewable in the // Audit tab and exportable. // import Foundation /// One audited action. struct AuditEntry: Codable, Identifiable, Sendable { var id: UUID = UUID() var timestamp: Date = Date() var taskID: UUID? /// "bash", "osascript", "write_file", … var actionKind: String /// The exact command/script/path that ran. var payload: String var cwd: String /// How the gate cleared it (autoAllowed / approvedByUser / …). var ruling: String var exitCode: Int32? /// Output truncated to `AuditLog.outputLimit` characters. var outputExcerpt: String? } /// Append-only JSONL writer. Actor: serializes file appends. actor AuditLog { static let outputLimit = 2000 private let fileURL: URL private let encoder: JSONEncoder /// Default log lives in the app's data folder; tasks may pass a /// workspace-local URL instead. init(fileURL: URL) { self.fileURL = fileURL let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 self.encoder = encoder } /// Appends one entry; creates the file on first write. Failures are /// reported to stderr but never crash the agent — losing an audit line is /// bad, killing the run is worse. func append(_ entry: AuditEntry) { var entry = entry if let excerpt = entry.outputExcerpt, excerpt.count > Self.outputLimit { entry.outputExcerpt = String(excerpt.prefix(Self.outputLimit)) + "… [truncated]" } do { let data = try encoder.encode(entry) guard var line = String(data: data, encoding: .utf8) else { return } line += "\n" let dir = fileURL.deletingLastPathComponent() try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) if !FileManager.default.fileExists(atPath: fileURL.path) { try line.write(to: fileURL, atomically: true, encoding: .utf8) } else { let handle = try FileHandle(forWritingTo: fileURL) defer { try? handle.close() } try handle.seekToEnd() try handle.write(contentsOf: Data(line.utf8)) } } catch { FileHandle.standardError.write(Data("AuditLog append failed: \(error)\n".utf8)) } } /// All entries, oldest first (for the Audit tab and export). func entries() -> [AuditEntry] { guard let content = try? String(contentsOf: fileURL, encoding: .utf8) else { return [] } let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 return content.split(separator: "\n").compactMap { line in try? decoder.decode(AuditEntry.self, from: Data(line.utf8)) } } }