use std::collections::HashMap;
use std::path::Path;
use cellos_core::types::{Correlation, EgressRule, ExecutionCellSpec, RunSpec, SecretDeliveryMode};
use serde::{Deserialize, Serialize};
use crate::context::ContextPack;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DoctrineAuthorityRule {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_ttl_seconds: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub require_secret_delivery: Option<SecretDeliveryMode>,
#[serde(default, skip_serializing_if = "is_false")]
pub require_egress_justification: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub forbid_egress: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub correlation_label: Option<(String, String)>,
}
fn is_false(b: &bool) -> bool {
!*b
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DoctrineAuthorityPolicy {
pub rules: HashMap<String, DoctrineAuthorityRule>,
}
impl DoctrineAuthorityPolicy {
pub fn empty() -> Self {
Self {
rules: HashMap::new(),
}
}
pub fn built_in() -> Self {
let mut rules = HashMap::new();
rules.insert(
"D1".to_string(),
DoctrineAuthorityRule {
require_secret_delivery: Some(SecretDeliveryMode::RuntimeLeasedBroker),
correlation_label: Some((
"doctrine.D1".to_string(),
"no-ambient-authority".to_string(),
)),
..Default::default()
},
);
rules.insert(
"D2".to_string(),
DoctrineAuthorityRule {
forbid_egress: true,
correlation_label: Some((
"doctrine.D2".to_string(),
"isolation-default".to_string(),
)),
..Default::default()
},
);
rules.insert(
"D3".to_string(),
DoctrineAuthorityRule {
correlation_label: Some((
"doctrine.D3".to_string(),
"authority-on-execution".to_string(),
)),
..Default::default()
},
);
rules.insert(
"D4".to_string(),
DoctrineAuthorityRule {
correlation_label: Some((
"doctrine.D4".to_string(),
"explicit-persistence".to_string(),
)),
..Default::default()
},
);
rules.insert(
"D5".to_string(),
DoctrineAuthorityRule {
max_ttl_seconds: Some(300),
correlation_label: Some(("doctrine.D5".to_string(), "ttl-bound".to_string())),
..Default::default()
},
);
rules.insert(
"D6".to_string(),
DoctrineAuthorityRule {
correlation_label: Some(("doctrine.D6".to_string(), "attributable".to_string())),
..Default::default()
},
);
rules.insert(
"D7".to_string(),
DoctrineAuthorityRule {
correlation_label: Some(("doctrine.D7".to_string(), "residue-free".to_string())),
..Default::default()
},
);
rules.insert(
"requires-internet".to_string(),
DoctrineAuthorityRule {
require_egress_justification: true,
correlation_label: Some((
"doctrine.tag".to_string(),
"requires-internet".to_string(),
)),
..Default::default()
},
);
rules.insert(
"short-lived".to_string(),
DoctrineAuthorityRule {
max_ttl_seconds: Some(60),
correlation_label: Some(("doctrine.tag".to_string(), "short-lived".to_string())),
..Default::default()
},
);
Self { rules }
}
pub fn load_from_env() -> Result<Self, anyhow::Error> {
let Some(path) = std::env::var_os("CELLOS_CORTEX_POLICY_PATH") else {
return Ok(Self::built_in());
};
Self::load_from_path(Path::new(&path))
}
pub fn load_from_path(path: &Path) -> Result<Self, anyhow::Error> {
let body = std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("read policy file {}: {}", path.display(), e))?;
let override_policy: DoctrineAuthorityPolicy = serde_json::from_str(&body)
.map_err(|e| anyhow::anyhow!("parse policy file {}: {}", path.display(), e))?;
let mut merged = Self::built_in();
for (id, rule) in override_policy.rules {
merged.rules.insert(id, rule);
}
Ok(merged)
}
}
pub fn apply_policy(
pack: &ContextPack,
spec: &mut ExecutionCellSpec,
policy: &DoctrineAuthorityPolicy,
) {
for doctrine_ref in &pack.doctrine_refs {
let Some(rule) = policy.rules.get(doctrine_ref) else {
tracing::warn!(
target: "cellos.cortex.policy",
doctrine_ref = %doctrine_ref,
cell_id = %spec.id,
"apply_policy: doctrine ref has no matching rule (silently no-op'd per ADR-0009)"
);
continue;
};
apply_rule(rule, spec);
}
}
fn apply_rule(rule: &DoctrineAuthorityRule, spec: &mut ExecutionCellSpec) {
if let Some(max_ttl) = rule.max_ttl_seconds {
if spec.lifetime.ttl_seconds == 0 || spec.lifetime.ttl_seconds > max_ttl {
spec.lifetime.ttl_seconds = max_ttl;
}
if let Some(run) = spec.run.as_mut() {
let new_timeout_ms = spec.lifetime.ttl_seconds.saturating_mul(1000);
match run.timeout_ms {
Some(existing) if existing <= new_timeout_ms => {} _ => run.timeout_ms = Some(new_timeout_ms),
}
}
}
if let Some(required) = rule.require_secret_delivery.as_ref() {
let run = spec.run.get_or_insert_with(default_run_spec);
if is_stricter_delivery(&run.secret_delivery, required) {
run.secret_delivery = required.clone();
}
}
if rule.forbid_egress {
spec.authority.egress_rules = Some(Vec::new());
}
if rule.require_egress_justification {
if let Some(rules) = spec.authority.egress_rules.as_mut() {
rules.retain(|r: &EgressRule| {
r.dns_egress_justification
.as_deref()
.map(|s| !s.trim().is_empty())
.unwrap_or(false)
});
}
}
if let Some((key, value)) = rule.correlation_label.as_ref() {
let correlation = spec.correlation.get_or_insert_with(Correlation::default);
let labels = correlation.labels.get_or_insert_with(HashMap::new);
labels.insert(key.clone(), value.clone());
}
}
fn default_run_spec() -> RunSpec {
RunSpec {
argv: Vec::new(),
working_directory: None,
timeout_ms: None,
limits: None,
secret_delivery: SecretDeliveryMode::default(),
}
}
fn is_stricter_delivery(current: &SecretDeliveryMode, required: &SecretDeliveryMode) -> bool {
matches!(
(current, required),
(
SecretDeliveryMode::Env,
SecretDeliveryMode::RuntimeBroker | SecretDeliveryMode::RuntimeLeasedBroker,
)
)
}
#[cfg(test)]
mod tests {
use super::*;
use cellos_core::types::{AuthorityBundle, EgressRule, Lifetime, RunSpec};
fn fresh_spec() -> ExecutionCellSpec {
ExecutionCellSpec {
id: "test-cell".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: Some(RunSpec {
argv: vec!["agent".into()],
working_directory: None,
timeout_ms: Some(1_800_000),
limits: None,
secret_delivery: SecretDeliveryMode::Env,
}),
authority: AuthorityBundle::default(),
lifetime: Lifetime { ttl_seconds: 1800 },
export: None,
telemetry: None,
}
}
fn pack_with(refs: &[&str]) -> ContextPack {
ContextPack {
memory_digest: String::new(),
doctrine_refs: refs.iter().map(|s| s.to_string()).collect(),
task: "t".into(),
expires_at: None,
}
}
#[test]
fn d5_clamps_ttl_to_300_seconds() {
let policy = DoctrineAuthorityPolicy::built_in();
let mut spec = fresh_spec();
let pack = pack_with(&["D5"]);
apply_policy(&pack, &mut spec, &policy);
assert_eq!(spec.lifetime.ttl_seconds, 300);
let run = spec.run.expect("runspec");
assert_eq!(run.timeout_ms, Some(300_000));
}
#[test]
fn d5_does_not_raise_ttl_when_spec_is_already_tighter() {
let policy = DoctrineAuthorityPolicy::built_in();
let mut spec = fresh_spec();
spec.lifetime.ttl_seconds = 60;
spec.run.as_mut().unwrap().timeout_ms = Some(60_000);
let pack = pack_with(&["D5"]);
apply_policy(&pack, &mut spec, &policy);
assert_eq!(spec.lifetime.ttl_seconds, 60);
assert_eq!(spec.run.unwrap().timeout_ms, Some(60_000));
}
#[test]
fn d1_overrides_secret_delivery_to_runtime_leased_broker() {
let policy = DoctrineAuthorityPolicy::built_in();
let mut spec = fresh_spec();
assert_eq!(
spec.run.as_ref().unwrap().secret_delivery,
SecretDeliveryMode::Env
);
let pack = pack_with(&["D1"]);
apply_policy(&pack, &mut spec, &policy);
assert_eq!(
spec.run.unwrap().secret_delivery,
SecretDeliveryMode::RuntimeLeasedBroker
);
}
#[test]
fn d1_does_not_downgrade_runtime_leased_broker() {
let policy = DoctrineAuthorityPolicy::built_in();
let mut spec = fresh_spec();
spec.run.as_mut().unwrap().secret_delivery = SecretDeliveryMode::RuntimeLeasedBroker;
let pack = pack_with(&["D1"]);
apply_policy(&pack, &mut spec, &policy);
assert_eq!(
spec.run.unwrap().secret_delivery,
SecretDeliveryMode::RuntimeLeasedBroker
);
}
#[test]
fn requires_internet_strips_unjustified_egress_rules() {
let policy = DoctrineAuthorityPolicy::built_in();
let mut spec = fresh_spec();
spec.authority.egress_rules = Some(vec![
EgressRule {
host: "api.example.com".into(),
port: 443,
protocol: Some("https".into()),
dns_egress_justification: None,
},
EgressRule {
host: "registry.example.net".into(),
port: 443,
protocol: Some("https".into()),
dns_egress_justification: Some("upstream container registry".into()),
},
]);
let pack = pack_with(&["requires-internet"]);
apply_policy(&pack, &mut spec, &policy);
let rules = spec.authority.egress_rules.expect("egress_rules retained");
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].host, "registry.example.net");
}
#[test]
fn d2_forbids_egress_replaces_rules_with_empty_vec() {
let policy = DoctrineAuthorityPolicy::built_in();
let mut spec = fresh_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "leaky.example.com".into(),
port: 80,
protocol: None,
dns_egress_justification: Some("operator override".into()),
}]);
let pack = pack_with(&["D2"]);
apply_policy(&pack, &mut spec, &policy);
let rules = spec.authority.egress_rules.expect("Some(empty)");
assert!(rules.is_empty());
}
#[test]
fn correlation_labels_are_recorded_for_every_matching_doctrine() {
let policy = DoctrineAuthorityPolicy::built_in();
let mut spec = fresh_spec();
let pack = pack_with(&["D1", "D5", "short-lived"]);
apply_policy(&pack, &mut spec, &policy);
let labels = spec
.correlation
.expect("correlation populated")
.labels
.expect("labels populated");
assert_eq!(
labels.get("doctrine.D1").map(String::as_str),
Some("no-ambient-authority")
);
assert_eq!(
labels.get("doctrine.D5").map(String::as_str),
Some("ttl-bound")
);
assert_eq!(
labels.get("doctrine.tag").map(String::as_str),
Some("short-lived")
);
}
#[test]
fn unknown_doctrine_refs_are_ignored() {
let policy = DoctrineAuthorityPolicy::built_in();
let mut spec = fresh_spec();
let before = format!("{:?}", spec);
let pack = pack_with(&["NOT-A-REAL-DOCTRINE-ID-42"]);
apply_policy(&pack, &mut spec, &policy);
let after = format!("{:?}", spec);
assert_eq!(before, after, "spec must be unchanged for unknown ids");
}
#[test]
fn empty_policy_is_a_noop() {
let policy = DoctrineAuthorityPolicy::empty();
let mut spec = fresh_spec();
let before = format!("{:?}", spec);
let pack = pack_with(&["D1", "D5"]);
apply_policy(&pack, &mut spec, &policy);
let after = format!("{:?}", spec);
assert_eq!(before, after);
}
#[test]
fn load_from_path_merges_overrides_on_top_of_built_in() {
let body = r#"{
"rules": {
"D5": {
"maxTtlSeconds": 30,
"correlationLabel": ["doctrine.D5", "operator-override"]
},
"tighter-than-built-in": {
"maxTtlSeconds": 10
}
}
}"#;
let tmp = std::env::temp_dir().join(format!(
"cellos-cortex-policy-test-{}.json",
std::process::id()
));
std::fs::write(&tmp, body).expect("write tmp policy");
let merged = DoctrineAuthorityPolicy::load_from_path(&tmp).expect("load ok");
std::fs::remove_file(&tmp).ok();
let d5 = merged.rules.get("D5").expect("D5 present");
assert_eq!(d5.max_ttl_seconds, Some(30));
let d1 = merged
.rules
.get("D1")
.expect("D1 fell through from built_in");
assert_eq!(
d1.require_secret_delivery.as_ref(),
Some(&SecretDeliveryMode::RuntimeLeasedBroker)
);
assert!(merged.rules.contains_key("tighter-than-built-in"));
}
#[test]
fn is_stricter_delivery_only_promotes_env_to_broker() {
assert!(is_stricter_delivery(
&SecretDeliveryMode::Env,
&SecretDeliveryMode::RuntimeBroker
));
assert!(is_stricter_delivery(
&SecretDeliveryMode::Env,
&SecretDeliveryMode::RuntimeLeasedBroker
));
assert!(!is_stricter_delivery(
&SecretDeliveryMode::RuntimeBroker,
&SecretDeliveryMode::RuntimeLeasedBroker
));
assert!(!is_stricter_delivery(
&SecretDeliveryMode::RuntimeLeasedBroker,
&SecretDeliveryMode::RuntimeBroker
));
assert!(!is_stricter_delivery(
&SecretDeliveryMode::RuntimeLeasedBroker,
&SecretDeliveryMode::Env
));
}
}