cellos_host_telemetry/lib.rs
1//! Host-side telemetry receiver for the in-VM observability path (Phase F).
2//!
3//! **Status: F3b — vsock listener + host-stamping + agent-silenced detection
4//! shipped.** The remaining F-phase work is F4b (supervisor signing of the
5//! outbound CloudEvents) and F1b (additive event constructor for
6//! `cell.observability.guest.agent_silenced`). ADR-0006 is the doctrine
7//! reference.
8//!
9//! Role: bind a per-cell UDS at `<vsock_uds_base>_9001` BEFORE the workload
10//! runs, receive CBOR-framed `cell.observability.guest.*` events from the
11//! in-guest [`cellos-telemetry`] agent, host-stamp the non-negotiable
12//! attribution fields (`cell_id`, `run_id`, `host_received_at`,
13//! `spec_signature_hash`, ADG `output`), and produce internal
14//! [`StampedDeclaration`] values the F4b signer projects to `CloudEventV1`
15//! via the [`cellos_core::events`] builders.
16//!
17//! Channel-authenticity model (ADR-0006 §5): the host trusts WHICH UDS path
18//! the bytes arrived on (Firecracker proxies the guest's vsock connection
19//! to a per-cell UDS at `<vsock_uds_base>_<port>`), not a payload signature.
20//! The guest agent must NOT hold a signing key. This crate must NEVER take
21//! a dependency on signing primitives that would let it accept guest-signed
22//! envelopes; the supervisor signs outbound, period.
23//!
24//! Module layout:
25//!
26//! - [`listener`] — per-cell UDS bind + CBOR frame decode + `content_version`
27//! major-version gate.
28//! - [`host_stamp`] — projects [`GuestDeclaration`] + [`HostStamp`] into
29//! the internal [`StampedDeclaration`] value type.
30//! - [`keepalive`] — `KeepAlive` tracker, `AgentSilencedTrigger` (fire-once),
31//! and `watch_for_silence` watcher loop.
32//!
33//! See [docs/adr/0006-in-vm-observability-runner-evidence.md] for the
34//! complete decision record.
35
36#![deny(unsafe_code)]
37#![warn(missing_docs)]
38
39use std::time::SystemTime;
40
41use thiserror::Error;
42
43pub mod host_stamp;
44pub mod keepalive;
45pub mod listener;
46pub mod probes;
47pub mod sign_outbound;
48
49#[doc(inline)]
50pub use probes::{
51 build_host_probe_envelope, emit_reading, HostProbe, ProbeContext, ProbeError, ProbeReading,
52 HOST_PROBE_EVENT_SOURCE, HOST_PROBE_EVENT_TYPE_PREFIX,
53};
54
55/// Vsock port reserved for guest telemetry events.
56///
57/// The supervisor binds the per-cell UDS at `<vsock_uds_base>_9001` BEFORE
58/// the workload's first instruction so the channel-authenticity primitive
59/// holds (ADR-0006 §5). Mirrors the `_9000` exit-code UDS in
60/// `cellos-host-firecracker`.
61pub const VSOCK_TELEMETRY_PORT: u32 = 9001;
62
63/// CBOR wire-format major version. Host rejects unknown majors per
64/// ADR-0006 §12 wire-schema versioning.
65///
66/// Value layout: low byte = minor, high byte = major. The major-version
67/// check in [`listener::decode_frame`] reads `(content_version >> 8) as u8`.
68pub const WIRE_CONTENT_VERSION_MAJOR: u16 = 1;
69
70/// Errors surfaced by the telemetry receiver.
71#[derive(Debug, Error)]
72pub enum TelemetryError {
73 /// Listener could not bind the requested per-cell UDS.
74 #[error("vsock UDS bind failed: {0}")]
75 Bind(String),
76
77 /// Wire payload could not be decoded or violated framing rules.
78 #[error("malformed wire payload: {0}")]
79 Wire(String),
80
81 /// `content_version` in the CBOR header is unknown.
82 #[error("unsupported wire major version: {0}")]
83 UnsupportedVersion(u16),
84}
85
86/// Fields a guest-side agent fills. Anything else is host-stamped on
87/// receipt and overrides what the guest sent (ADR-0006 §6).
88///
89/// **Type-level non-forgeability.** The struct has no `cell_id`/`run_id`/
90/// `spec_signature_hash` fields — the wire decoder (`listener::decode_frame`)
91/// drops any such keys the guest tries to stuff into the CBOR map, and the
92/// stamping layer (`host_stamp::stamp`) reads attribution exclusively from
93/// [`HostStamp`]. This is the structural enforcement of ADR-0006 §6: a
94/// compromised guest cannot forge cross-cell attribution because the
95/// attribution fields don't exist on this type.
96#[derive(Debug, Clone)]
97pub struct GuestDeclaration {
98 /// Probe source identifier (e.g. `"process.spawned"`,
99 /// `"net.connect_attempted"`). The set of valid values is locked in
100 /// `cellos_telemetry::probe_source`.
101 pub probe_source: String,
102 /// Guest-side process id observed by the probe.
103 pub guest_pid: u32,
104 /// Guest-side process command name.
105 pub guest_comm: String,
106 /// Guest-side monotonic timestamp at probe-fire (ns).
107 pub guest_monotonic_ns: u64,
108}
109
110/// Attribution fields stamped supervisor-side on every guest declaration.
111/// Overrides whatever the guest sent — non-negotiable per ADR-0006 §6.
112#[derive(Debug, Clone)]
113pub struct HostStamp {
114 /// Per-cell identifier.
115 pub cell_id: String,
116 /// Per-run identifier.
117 pub run_id: String,
118 /// Wallclock at host receive.
119 pub host_received_at: SystemTime,
120 /// SHA-256 of the spec the cell was admitted under.
121 pub spec_signature_hash: String,
122}
123
124/// Forward-looking F3 host-probe reading shape (ADR-0006 acceptance prep,
125/// 2026-05-16). The richer `probes::HostProbe` / `probes::ProbeReading` API
126/// in this crate is the F3a implementation; this simpler envelope is the
127/// minimal contract documented for future host-side probes that emit
128/// `cellos.events.host.probe.v1` CloudEvents without needing the full
129/// `ProbeContext` / `EventSink` plumbing.
130///
131/// Implementations of `probes::HostProbe` are the canonical path today;
132/// this struct is the additive forward declaration.
133#[derive(Debug, Clone)]
134pub struct HostProbeReading {
135 /// Probe identifier (matches `probes::HostProbe::probe_name`).
136 pub probe: &'static str,
137 /// Probe output as free-form JSON.
138 pub value_json: serde_json::Value,
139 /// Wallclock at probe-fire, milliseconds since Unix epoch.
140 pub timestamp_ms: u64,
141}
142
143/// Internal value type: a guest declaration with host-stamped attribution.
144///
145/// **Internal — not a CloudEvent.** F4b owns signing and projects this to
146/// [`cellos_core::CloudEventV1`] via the existing
147/// `cellos_core::events::observability_*_data_v1` builders. This crate
148/// produces unsigned values; the supervisor signs them on the way out.
149#[derive(Debug, Clone)]
150pub struct StampedDeclaration {
151 // ── Host-stamped (overrides anything the guest sent) ──
152 /// Per-cell identifier (host-stamped).
153 pub cell_id: String,
154 /// Per-run identifier (host-stamped).
155 pub run_id: String,
156 /// Wallclock at host receive (host-stamped, per-frame).
157 pub host_received_at: SystemTime,
158 /// SHA-256 of the spec the cell was admitted under (host-stamped).
159 pub spec_signature_hash: String,
160 // ── Guest-fillable ──
161 /// Probe source identifier.
162 pub probe_source: String,
163 /// Guest-side process id observed by the probe.
164 pub guest_pid: u32,
165 /// Guest-side process command name.
166 pub guest_comm: String,
167 /// Guest-side monotonic timestamp at probe-fire (ns).
168 pub guest_monotonic_ns: u64,
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn vsock_port_is_locked() {
177 assert_eq!(VSOCK_TELEMETRY_PORT, 9001);
178 }
179
180 #[test]
181 fn wire_major_starts_at_one() {
182 assert_eq!(WIRE_CONTENT_VERSION_MAJOR, 1);
183 }
184
185 #[test]
186 fn guest_declaration_has_no_attribution_fields() {
187 // Type-level non-forgeability check: GuestDeclaration must NOT carry
188 // any of the host-stamped attribution fields. If a future commit
189 // adds `cell_id` (or similar) to GuestDeclaration, this test should
190 // be deleted only with a corresponding ADR amendment — the
191 // attribution-fields-not-in-guest-declaration property is a
192 // structural enforcement of ADR-0006 §6.
193 let g = GuestDeclaration {
194 probe_source: "process.spawned".into(),
195 guest_pid: 1,
196 guest_comm: "x".into(),
197 guest_monotonic_ns: 0,
198 };
199 // Compile-time witness: the only fields you can read are these.
200 let _ = (
201 &g.probe_source,
202 &g.guest_pid,
203 &g.guest_comm,
204 &g.guest_monotonic_ns,
205 );
206 }
207}