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-side telemetry receiver for the in-VM observability path (Phase F).
//!
//! **Status: F3b — vsock listener + host-stamping + agent-silenced detection
//! shipped.** The remaining F-phase work is F4b (supervisor signing of the
//! outbound CloudEvents) and F1b (additive event constructor for
//! `cell.observability.guest.agent_silenced`). ADR-0006 is the doctrine
//! reference.
//!
//! Role: bind a per-cell UDS at `<vsock_uds_base>_9001` BEFORE the workload
//! runs, receive CBOR-framed `cell.observability.guest.*` events from the
//! in-guest [`cellos-telemetry`] agent, host-stamp the non-negotiable
//! attribution fields (`cell_id`, `run_id`, `host_received_at`,
//! `spec_signature_hash`, ADG `output`), and produce internal
//! [`StampedDeclaration`] values the F4b signer projects to `CloudEventV1`
//! via the [`cellos_core::events`] builders.
//!
//! Channel-authenticity model (ADR-0006 §5): the host trusts WHICH UDS path
//! the bytes arrived on (Firecracker proxies the guest's vsock connection
//! to a per-cell UDS at `<vsock_uds_base>_<port>`), not a payload signature.
//! The guest agent must NOT hold a signing key. This crate must NEVER take
//! a dependency on signing primitives that would let it accept guest-signed
//! envelopes; the supervisor signs outbound, period.
//!
//! Module layout:
//!
//! - [`listener`] — per-cell UDS bind + CBOR frame decode + `content_version`
//!   major-version gate.
//! - [`host_stamp`] — projects [`GuestDeclaration`] + [`HostStamp`] into
//!   the internal [`StampedDeclaration`] value type.
//! - [`keepalive`] — `KeepAlive` tracker, `AgentSilencedTrigger` (fire-once),
//!   and `watch_for_silence` watcher loop.
//!
//! See [docs/adr/0006-in-vm-observability-runner-evidence.md] for the
//! complete decision record.

#![deny(unsafe_code)]
#![warn(missing_docs)]

use std::time::SystemTime;

use thiserror::Error;

pub mod host_stamp;
pub mod keepalive;
pub mod listener;
pub mod probes;
pub mod sign_outbound;

#[doc(inline)]
pub use probes::{
    build_host_probe_envelope, emit_reading, HostProbe, ProbeContext, ProbeError, ProbeReading,
    HOST_PROBE_EVENT_SOURCE, HOST_PROBE_EVENT_TYPE_PREFIX,
};

/// Vsock port reserved for guest telemetry events.
///
/// The supervisor binds the per-cell UDS at `<vsock_uds_base>_9001` BEFORE
/// the workload's first instruction so the channel-authenticity primitive
/// holds (ADR-0006 §5). Mirrors the `_9000` exit-code UDS in
/// `cellos-host-firecracker`.
pub const VSOCK_TELEMETRY_PORT: u32 = 9001;

/// CBOR wire-format major version. Host rejects unknown majors per
/// ADR-0006 §12 wire-schema versioning.
///
/// Value layout: low byte = minor, high byte = major. The major-version
/// check in [`listener::decode_frame`] reads `(content_version >> 8) as u8`.
pub const WIRE_CONTENT_VERSION_MAJOR: u16 = 1;

/// Errors surfaced by the telemetry receiver.
#[derive(Debug, Error)]
pub enum TelemetryError {
    /// Listener could not bind the requested per-cell UDS.
    #[error("vsock UDS bind failed: {0}")]
    Bind(String),

    /// Wire payload could not be decoded or violated framing rules.
    #[error("malformed wire payload: {0}")]
    Wire(String),

    /// `content_version` in the CBOR header is unknown.
    #[error("unsupported wire major version: {0}")]
    UnsupportedVersion(u16),
}

/// Fields a guest-side agent fills. Anything else is host-stamped on
/// receipt and overrides what the guest sent (ADR-0006 §6).
///
/// **Type-level non-forgeability.** The struct has no `cell_id`/`run_id`/
/// `spec_signature_hash` fields — the wire decoder (`listener::decode_frame`)
/// drops any such keys the guest tries to stuff into the CBOR map, and the
/// stamping layer (`host_stamp::stamp`) reads attribution exclusively from
/// [`HostStamp`]. This is the structural enforcement of ADR-0006 §6: a
/// compromised guest cannot forge cross-cell attribution because the
/// attribution fields don't exist on this type.
#[derive(Debug, Clone)]
pub struct GuestDeclaration {
    /// Probe source identifier (e.g. `"process.spawned"`,
    /// `"net.connect_attempted"`). The set of valid values is locked in
    /// `cellos_telemetry::probe_source`.
    pub probe_source: String,
    /// Guest-side process id observed by the probe.
    pub guest_pid: u32,
    /// Guest-side process command name.
    pub guest_comm: String,
    /// Guest-side monotonic timestamp at probe-fire (ns).
    pub guest_monotonic_ns: u64,
}

/// Attribution fields stamped supervisor-side on every guest declaration.
/// Overrides whatever the guest sent — non-negotiable per ADR-0006 §6.
#[derive(Debug, Clone)]
pub struct HostStamp {
    /// Per-cell identifier.
    pub cell_id: String,
    /// Per-run identifier.
    pub run_id: String,
    /// Wallclock at host receive.
    pub host_received_at: SystemTime,
    /// SHA-256 of the spec the cell was admitted under.
    pub spec_signature_hash: String,
}

/// Forward-looking F3 host-probe reading shape (ADR-0006 acceptance prep,
/// 2026-05-16). The richer `probes::HostProbe` / `probes::ProbeReading` API
/// in this crate is the F3a implementation; this simpler envelope is the
/// minimal contract documented for future host-side probes that emit
/// `cellos.events.host.probe.v1` CloudEvents without needing the full
/// `ProbeContext` / `EventSink` plumbing.
///
/// Implementations of `probes::HostProbe` are the canonical path today;
/// this struct is the additive forward declaration.
#[derive(Debug, Clone)]
pub struct HostProbeReading {
    /// Probe identifier (matches `probes::HostProbe::probe_name`).
    pub probe: &'static str,
    /// Probe output as free-form JSON.
    pub value_json: serde_json::Value,
    /// Wallclock at probe-fire, milliseconds since Unix epoch.
    pub timestamp_ms: u64,
}

/// Internal value type: a guest declaration with host-stamped attribution.
///
/// **Internal — not a CloudEvent.** F4b owns signing and projects this to
/// [`cellos_core::CloudEventV1`] via the existing
/// `cellos_core::events::observability_*_data_v1` builders. This crate
/// produces unsigned values; the supervisor signs them on the way out.
#[derive(Debug, Clone)]
pub struct StampedDeclaration {
    // ── Host-stamped (overrides anything the guest sent) ──
    /// Per-cell identifier (host-stamped).
    pub cell_id: String,
    /// Per-run identifier (host-stamped).
    pub run_id: String,
    /// Wallclock at host receive (host-stamped, per-frame).
    pub host_received_at: SystemTime,
    /// SHA-256 of the spec the cell was admitted under (host-stamped).
    pub spec_signature_hash: String,
    // ── Guest-fillable ──
    /// Probe source identifier.
    pub probe_source: String,
    /// Guest-side process id observed by the probe.
    pub guest_pid: u32,
    /// Guest-side process command name.
    pub guest_comm: String,
    /// Guest-side monotonic timestamp at probe-fire (ns).
    pub guest_monotonic_ns: u64,
}

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

    #[test]
    fn vsock_port_is_locked() {
        assert_eq!(VSOCK_TELEMETRY_PORT, 9001);
    }

    #[test]
    fn wire_major_starts_at_one() {
        assert_eq!(WIRE_CONTENT_VERSION_MAJOR, 1);
    }

    #[test]
    fn guest_declaration_has_no_attribution_fields() {
        // Type-level non-forgeability check: GuestDeclaration must NOT carry
        // any of the host-stamped attribution fields. If a future commit
        // adds `cell_id` (or similar) to GuestDeclaration, this test should
        // be deleted only with a corresponding ADR amendment — the
        // attribution-fields-not-in-guest-declaration property is a
        // structural enforcement of ADR-0006 §6.
        let g = GuestDeclaration {
            probe_source: "process.spawned".into(),
            guest_pid: 1,
            guest_comm: "x".into(),
            guest_monotonic_ns: 0,
        };
        // Compile-time witness: the only fields you can read are these.
        let _ = (
            &g.probe_source,
            &g.guest_pid,
            &g.guest_comm,
            &g.guest_monotonic_ns,
        );
    }
}