SPB Git

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%
3.1 KB · 86 lines swift
Raw Blame History
1//2//  AuditLog.swift3//  Zyquo Agent4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Append-only record of every executed action: timestamp, kind, exact9//  payload, cwd, policy ruling, exit code, truncated output. Stored as JSONL10//  next to the task so nothing the agent does is invisible; viewable in the11//  Audit tab and exportable.12//1314import Foundation1516/// One audited action.17struct AuditEntry: Codable, Identifiable, Sendable {18    var id: UUID = UUID()19    var timestamp: Date = Date()20    var taskID: UUID?21    /// "bash", "osascript", "write_file", …22    var actionKind: String23    /// The exact command/script/path that ran.24    var payload: String25    var cwd: String26    /// How the gate cleared it (autoAllowed / approvedByUser / …).27    var ruling: String28    var exitCode: Int32?29    /// Output truncated to `AuditLog.outputLimit` characters.30    var outputExcerpt: String?31}3233/// Append-only JSONL writer. Actor: serializes file appends.34actor AuditLog {35    static let outputLimit = 20003637    private let fileURL: URL38    private let encoder: JSONEncoder3940    /// Default log lives in the app's data folder; tasks may pass a41    /// workspace-local URL instead.42    init(fileURL: URL) {43        self.fileURL = fileURL44        let encoder = JSONEncoder()45        encoder.dateEncodingStrategy = .iso860146        self.encoder = encoder47    }4849    /// Appends one entry; creates the file on first write. Failures are50    /// reported to stderr but never crash the agent — losing an audit line is51    /// bad, killing the run is worse.52    func append(_ entry: AuditEntry) {53        var entry = entry54        if let excerpt = entry.outputExcerpt, excerpt.count > Self.outputLimit {55            entry.outputExcerpt = String(excerpt.prefix(Self.outputLimit)) + "… [truncated]"56        }57        do {58            let data = try encoder.encode(entry)59            guard var line = String(data: data, encoding: .utf8) else { return }60            line += "\n"61            let dir = fileURL.deletingLastPathComponent()62            try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)63            if !FileManager.default.fileExists(atPath: fileURL.path) {64                try line.write(to: fileURL, atomically: true, encoding: .utf8)65            } else {66                let handle = try FileHandle(forWritingTo: fileURL)67                defer { try? handle.close() }68                try handle.seekToEnd()69                try handle.write(contentsOf: Data(line.utf8))70            }71        } catch {72            FileHandle.standardError.write(Data("AuditLog append failed: \(error)\n".utf8))73        }74    }7576    /// All entries, oldest first (for the Audit tab and export).77    func entries() -> [AuditEntry] {78        guard let content = try? String(contentsOf: fileURL, encoding: .utf8) else { return [] }79        let decoder = JSONDecoder()80        decoder.dateDecodingStrategy = .iso860181        return content.split(separator: "\n").compactMap { line in82            try? decoder.decode(AuditEntry.self, from: Data(line.utf8))83        }84    }85}86