Skip to main content

rig_resources/
trace.rs

1//! Local resource trace envelopes for evidence metadata.
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Value, json};
5
6/// Machine-readable trace envelope for resource-side decisions.
7///
8/// This is intentionally local to `rig-resources`. It proves a stable shape
9/// for graph, security, baseline, and memory-resource metadata before any
10/// trace API is promoted into the `rig-compose` kernel.
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct ResourceTraceEnvelope {
13    /// Trace shape version.
14    pub version: u32,
15    /// Category such as `graph`, `security`, `baseline`, or `memory`.
16    pub resource: String,
17    /// Specific operation performed by the resource.
18    pub operation: String,
19    /// Machine-readable trace kind such as `graph_expansion`.
20    pub trace_kind: String,
21    /// Compact, non-secret input summary.
22    pub input_summary: Value,
23    /// Compact output summary.
24    pub output_summary: Value,
25    /// Optional reason code for skip, suppress, deny, or not-applicable paths.
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub reason: Option<String>,
28    /// Additional resource-specific metadata.
29    #[serde(default, skip_serializing_if = "Value::is_null")]
30    pub metadata: Value,
31}
32
33impl ResourceTraceEnvelope {
34    /// Current envelope version.
35    pub const VERSION: u32 = 1;
36
37    /// Create a trace envelope with empty summaries.
38    #[must_use]
39    pub fn new(
40        resource: impl Into<String>,
41        operation: impl Into<String>,
42        trace_kind: impl Into<String>,
43    ) -> Self {
44        Self {
45            version: Self::VERSION,
46            resource: resource.into(),
47            operation: operation.into(),
48            trace_kind: trace_kind.into(),
49            input_summary: Value::Null,
50            output_summary: Value::Null,
51            reason: None,
52            metadata: Value::Null,
53        }
54    }
55
56    /// Attach the input summary.
57    #[must_use]
58    pub fn with_input_summary(mut self, input_summary: Value) -> Self {
59        self.input_summary = input_summary;
60        self
61    }
62
63    /// Attach the output summary.
64    #[must_use]
65    pub fn with_output_summary(mut self, output_summary: Value) -> Self {
66        self.output_summary = output_summary;
67        self
68    }
69
70    /// Attach a machine-readable reason code.
71    #[must_use]
72    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
73        self.reason = Some(reason.into());
74        self
75    }
76
77    /// Attach resource-specific metadata.
78    #[must_use]
79    pub fn with_metadata(mut self, metadata: Value) -> Self {
80        self.metadata = metadata;
81        self
82    }
83
84    /// Convert the trace envelope into JSON metadata.
85    #[must_use]
86    pub fn to_value(&self) -> Value {
87        json!(self)
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn trace_envelope_round_trips_as_json() {
97        let trace = ResourceTraceEnvelope::new("graph", "expand", "graph_expansion")
98            .with_input_summary(json!({"entity": "host-1"}))
99            .with_output_summary(json!({"distinct_neighbours": 4}))
100            .with_reason("threshold_exceeded");
101
102        let value = trace.to_value();
103        let decoded: ResourceTraceEnvelope = serde_json::from_value(value).unwrap();
104
105        assert_eq!(decoded.version, ResourceTraceEnvelope::VERSION);
106        assert_eq!(decoded.resource, "graph");
107        assert_eq!(decoded.reason.as_deref(), Some("threshold_exceeded"));
108    }
109}