cellos-host-telemetry 0.5.1

Host-side telemetry receiver for CellOS — vsock listener that host-stamps and signs CloudEvents emitted by the in-guest cellos-telemetry agent.
Documentation
//! Host-stamping of guest declarations (ADR-0006 §6).
//!
//! Doctrine: the guest fills only `probe_source`, `guest_pid`, `guest_comm`,
//! `guest_monotonic_ns`. Everything else — `cell_id`, `run_id`,
//! `host_received_at`, `spec_signature_hash`, and the ADG `output` block —
//! is stamped supervisor-side and **overwrites** anything the guest sent. A
//! compromised guest cannot forge cross-cell attribution because attribution
//! does not live in the wire payload — it lives in WHICH vsock CID:port the
//! bytes arrived on (the channel-authenticity primitive, ADR-0006 §5).
//!
//! This module is deliberately small: a single [`stamp`] function that takes
//! a [`GuestDeclaration`] and a [`HostStamp`] and produces a
//! [`StampedDeclaration`] — the internal value type the F4b signer projects
//! into a `CloudEventV1` via `cellos_core::events::*` builders.

use std::time::SystemTime;

use crate::{GuestDeclaration, HostStamp, StampedDeclaration};

/// Project a guest-declared probe event into a host-attributed
/// [`StampedDeclaration`].
///
/// The returned value carries the host-stamped attribution fields verbatim
/// from `stamp` and the guest-fillable fields from `guest`. Any attribution
/// field the guest may have placed in its own payload is invisible to this
/// projection — by construction, only [`GuestDeclaration`]'s four fields
/// reach this function.
pub fn stamp(guest: GuestDeclaration, stamp: HostStamp) -> StampedDeclaration {
    StampedDeclaration {
        // Host-stamped (non-negotiable, ADR-0006 §6). Order chosen so the
        // attribution block sits at the head of the struct — the eventual
        // ADG `output` block reads from these.
        cell_id: stamp.cell_id,
        run_id: stamp.run_id,
        host_received_at: stamp.host_received_at,
        spec_signature_hash: stamp.spec_signature_hash,
        // Guest-fillable (everything else the guest sent has already been
        // dropped at decode-time — see [`crate::listener::decode_frame`]).
        probe_source: guest.probe_source,
        guest_pid: guest.guest_pid,
        guest_comm: guest.guest_comm,
        guest_monotonic_ns: guest.guest_monotonic_ns,
    }
}

/// Convenience: stamp using `SystemTime::now()` for `host_received_at`.
///
/// The receiver path uses [`stamp`] directly with a captured timestamp so
/// retries / replays preserve the original receive instant; this helper is
/// for tests and ad-hoc construction sites.
pub fn stamp_now(
    guest: GuestDeclaration,
    cell_id: impl Into<String>,
    run_id: impl Into<String>,
    spec_signature_hash: impl Into<String>,
) -> StampedDeclaration {
    stamp(
        guest,
        HostStamp {
            cell_id: cell_id.into(),
            run_id: run_id.into(),
            host_received_at: SystemTime::now(),
            spec_signature_hash: spec_signature_hash.into(),
        },
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    fn fixture_guest() -> GuestDeclaration {
        GuestDeclaration {
            probe_source: "process.spawned".to_string(),
            guest_pid: 1234,
            guest_comm: "workload".to_string(),
            guest_monotonic_ns: 42_000_000,
        }
    }

    #[test]
    fn host_stamp_overrides_attribution() {
        // The guest CANNOT provide `cell_id`/`run_id`/`spec_signature_hash`
        // because they are not fields on `GuestDeclaration`. This test
        // documents the type-level guarantee: `stamp()` only reads from the
        // `HostStamp` for attribution.
        let stamped = stamp_now(fixture_guest(), "cell-A", "run-1", "sha256:deadbeef");
        assert_eq!(stamped.cell_id, "cell-A");
        assert_eq!(stamped.run_id, "run-1");
        assert_eq!(stamped.spec_signature_hash, "sha256:deadbeef");
        // Guest-fillable preserved.
        assert_eq!(stamped.probe_source, "process.spawned");
        assert_eq!(stamped.guest_pid, 1234);
        assert_eq!(stamped.guest_comm, "workload");
        assert_eq!(stamped.guest_monotonic_ns, 42_000_000);
    }

    #[test]
    fn explicit_stamp_preserves_received_at() {
        let t = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_700_000_000);
        let stamped = stamp(
            fixture_guest(),
            HostStamp {
                cell_id: "c".into(),
                run_id: "r".into(),
                host_received_at: t,
                spec_signature_hash: "sha256:00".into(),
            },
        );
        assert_eq!(stamped.host_received_at, t);
    }
}