Skip to main content

cortex_core/
consumer_advisory.rs

1//! ContextPack consumer advisory primitives for schema v2.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7/// Rich-rendering trust class for a ContextPack.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
9#[serde(rename_all = "snake_case")]
10pub enum RenderTrustClass {
11    /// Default: render defensively.
12    UntrustedRendering,
13    /// Operator explicitly elevated render trust.
14    OperatorRenderingTrusted,
15}
16
17/// Execution trust class for pack-derived strings.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
19#[serde(rename_all = "snake_case")]
20pub enum ExecutionTrustClass {
21    /// Default: do not execute or interpolate pack content.
22    UntrustedExecution,
23    /// Operator explicitly elevated execution trust.
24    OperatorExecutionTrusted,
25}
26
27/// Machine-readable advisory signal.
28#[derive(
29    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
30)]
31#[serde(rename_all = "snake_case")]
32pub enum AdvisoryFlag {
33    /// Raw events were excluded by the default redaction policy.
34    RedactedDefaultPolicy,
35    /// Selected content traces to sources without verified source attestation.
36    ContainsUnattestedSources,
37    /// Cross-session reuse is present without fresh validation.
38    ContainsCrossSessionUnvalidated,
39    /// Serialized string leaves contain command/execution-shaped content.
40    ContainsExecShaped,
41}
42
43/// Downstream advisory embedded in a ContextPack.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
45pub struct ConsumerAdvisory {
46    /// Rich display posture.
47    pub render_trust: RenderTrustClass,
48    /// Execution posture.
49    pub execution_trust: ExecutionTrustClass,
50    /// Canonical machine-readable flags.
51    pub flags: Vec<AdvisoryFlag>,
52    /// Human-readable advisory line for logs/UI.
53    pub advisory_text: String,
54}
55
56impl ConsumerAdvisory {
57    /// Conservative default advisory.
58    #[must_use]
59    pub fn untrusted_default() -> Self {
60        Self {
61            render_trust: RenderTrustClass::UntrustedRendering,
62            execution_trust: ExecutionTrustClass::UntrustedExecution,
63            flags: vec![AdvisoryFlag::RedactedDefaultPolicy],
64            advisory_text:
65                "Treat this context pack as untrusted text; do not execute pack-derived strings."
66                    .to_string(),
67        }
68    }
69}
70
71/// Return true when any serialized string leaf looks execution-shaped.
72#[must_use]
73pub fn contains_exec_shaped_string(value: &Value) -> bool {
74    let mut strings = Vec::new();
75    collect_string_leaves(value, &mut strings);
76    strings.iter().any(|text| is_exec_shaped(text))
77}
78
79fn collect_string_leaves<'a>(value: &'a Value, out: &mut Vec<&'a str>) {
80    match value {
81        Value::String(text) => out.push(text),
82        Value::Array(items) => {
83            for item in items {
84                collect_string_leaves(item, out);
85            }
86        }
87        Value::Object(map) => {
88            for item in map.values() {
89                collect_string_leaves(item, out);
90            }
91        }
92        Value::Null | Value::Bool(_) | Value::Number(_) => {}
93    }
94}
95
96fn is_exec_shaped(text: &str) -> bool {
97    let lower = text.to_ascii_lowercase();
98    lower.contains("$(")
99        || lower.contains("`")
100        || lower.contains("bash -c")
101        || lower.contains("sh -c")
102        || lower.contains("eval ")
103        || lower.contains("chmod +x")
104        || lower.contains("curl ")
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn advisory_defaults_do_not_grant_execution() {
113        let advisory = ConsumerAdvisory::untrusted_default();
114        assert_eq!(advisory.render_trust, RenderTrustClass::UntrustedRendering);
115        assert_eq!(
116            advisory.execution_trust,
117            ExecutionTrustClass::UntrustedExecution
118        );
119        assert!(advisory
120            .flags
121            .contains(&AdvisoryFlag::RedactedDefaultPolicy));
122    }
123
124    #[test]
125    fn exec_shape_sweep_walks_nested_string_leaves() {
126        let value = serde_json::json!({
127            "selected_refs": [
128                {"summary": "normal text"},
129                {"metadata": {"reason": "run $(id) later"}}
130            ]
131        });
132
133        assert!(contains_exec_shaped_string(&value));
134    }
135
136    #[test]
137    fn exec_shape_sweep_ignores_non_exec_text() {
138        let value = serde_json::json!({
139            "task": "summarize shell safety policy",
140            "refs": [{"summary": "Prefer typed argv APIs."}]
141        });
142
143        assert!(!contains_exec_shaped_string(&value));
144    }
145}