Ambient system companions over one privacy-preserving signal daemon (aggregate-only, no keystroke content): a git-driven terminal garden and IOKit hardware collectors. ambient daemon macos privacy terminal

macos-collector/Sources/CollectorCore/Wire.swift

105 lines · 3812 bytes

  1import Foundation
  2
  3/// The wire schema version. **Must equal** `signal_schema::SCHEMA_VERSION` on the
  4/// Rust side (v3 as of 0.3.0). A daemon rejects frames whose version it does not
  5/// understand, so this is a hard cross-language contract.
  6public let SCHEMA_VERSION: UInt16 = 3
  7
  8/// The collector domain. Discriminants **must match** Rust `Source::to_u8`.
  9public enum Source: UInt8 {
 10    case terminal = 0
 11    case git = 1
 12    case macos = 2
 13    case hardware = 3
 14}
 15
 16/// The enum-constrained metric name. Only the variants this collector emits are
 17/// declared here; every discriminant **must match** Rust `SignalName::to_u8`.
 18///
 19/// The privacy boundary is unchanged for hardware: each of these is an aggregate
 20/// `f64` scalar, and none of them carries a tag (all are emitted with
 21/// `tag == nil`), exactly as `SignalName::allows_tag` is `false` for them.
 22public enum SignalName: UInt8 {
 23    case batteryPct = 17    // macos:    battery charge percentage [0, 100]
 24    case charging = 18      // macos:    1.0 charging, 0.0 not
 25    case thermalState = 19  // macos:    ProcessInfo thermal state 0..3
 26    case batteryDrawW = 27  // hardware: instantaneous battery draw, watts
 27    case cpuLoad = 28       // hardware: aggregate CPU busy fraction [0, 1]
 28
 29    /// Human-readable name, matching the Rust metric names (for `--once`).
 30    public var label: String {
 31        switch self {
 32        case .batteryPct: return "battery_pct"
 33        case .charging: return "charging"
 34        case .thermalState: return "thermal_state"
 35        case .batteryDrawW: return "battery_draw_w"
 36        case .cpuLoad: return "cpu_load"
 37        }
 38    }
 39}
 40
 41/// One flat signal record  the Swift mirror of Rust `Signal`. The only payload
 42/// channel is `value: Double` (`f64`); there is deliberately no content field.
 43public struct Signal {
 44    public let schemaVersion: UInt16
 45    public let ts: UInt64
 46    public let source: Source
 47    public let name: SignalName
 48    public let value: Double
 49    public let tag: String?
 50
 51    public init(ts: UInt64, source: Source, name: SignalName, value: Double, tag: String? = nil) {
 52        self.schemaVersion = SCHEMA_VERSION
 53        self.ts = ts
 54        self.source = source
 55        self.name = name
 56        self.value = value
 57        self.tag = tag
 58    }
 59}
 60
 61private extension Array where Element == UInt8 {
 62    /// Append a fixed-width integer in little-endian order (matches Rust
 63    /// `to_le_bytes`).
 64    mutating func appendLE<T: FixedWidthInteger>(_ v: T) {
 65        var le = v.littleEndian
 66        Swift.withUnsafeBytes(of: &le) { append(contentsOf: $0) }
 67    }
 68}
 69
 70/// Encode a signal to its length-prefixed wire frame, byte-for-byte identical to
 71/// Rust `signal_schema::wire::encode`.
 72///
 73/// ```text
 74/// [u32 body_len]
 75/// body = [u16 schema_version][u64 ts][u8 source][u8 name][f64 value]
 76///        [u8 tag_present] ( [u16 tag_len][tag_len bytes utf8] )?
 77/// ```
 78/// All multi-byte integers and the `f64` (via its IEEE-754 bit pattern) are
 79/// little-endian.
 80public func encode(_ s: Signal) -> [UInt8] {
 81    var body: [UInt8] = []
 82    body.appendLE(s.schemaVersion)      // u16
 83    body.appendLE(s.ts)                 // u64
 84    body.append(s.source.rawValue)      // u8
 85    body.append(s.name.rawValue)        // u8
 86    body.appendLE(s.value.bitPattern)   // f64 as its u64 bit pattern, LE
 87    if let tag = s.tag {
 88        body.append(1)
 89        let bytes = Array(tag.utf8)
 90        body.appendLE(UInt16(bytes.count))
 91        body.append(contentsOf: bytes)
 92    } else {
 93        body.append(0)
 94    }
 95
 96    var frame: [UInt8] = []
 97    frame.appendLE(UInt32(body.count))
 98    frame.append(contentsOf: body)
 99    return frame
100}
101
102/// Unix milliseconds, best-effort (matches Rust `now_millis`).
103public func nowMillis() -> UInt64 {
104    UInt64(Date().timeIntervalSince1970 * 1000.0)
105}