cellos-cortex 0.1.0

Bridge between CellOS execution cells and the Cortex doctrine layer — DoctrineAuthorityPolicy, CortexCellRunner, CellosLedgerEmitter.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
//! Cortex doctrine → CellOS authority policy (ADR-0009).
//!
//! [`DoctrineAuthorityPolicy`] is a declarative table that maps a Cortex
//! doctrine id (a string carried in [`crate::ContextPack::doctrine_refs`]) to a
//! set of constraints applied to the [`ExecutionCellSpec`] the bridge produces.
//!
//! The policy is applied by [`apply_policy`] from inside
//! [`crate::runner::CortexCellRunner::translate`], immediately after the
//! structural conversion (`pack.task` → `argv`, `pack.expires_at` → TTL).
//! See ADR-0009 in `docs/adr/0009-cortex-doctrine-to-cellos-authority-mapping.md`
//! for the design rationale and the full doctrine → authority mapping table.
//!
//! All policy mutations are **monotonic toward least authority**:
//!
//! - `max_ttl_seconds` clamps the spec's `lifetime.ttlSeconds` *down*,
//!   never up;
//! - `require_secret_delivery` overrides `run.secretDelivery` only when the
//!   incoming mode is weaker (i.e. `Env` is replaced by a broker mode; a
//!   broker mode is never replaced by `Env`);
//! - `forbid_egress = true` forces `authority.egressRules` to an empty vec;
//! - `require_egress_justification = true` strips egress rules whose
//!   `dnsEgressJustification` is missing or empty;
//! - `correlation_label` adds a `(key, value)` pair to
//!   `spec.correlation.labels`, recording which doctrine rule fired.
//!
//! Rules that would *raise* authority (longer TTL, weaker secret delivery,
//! granting egress) are silently dropped — see ADR-0009 § Consequences.
//!
//! Operators override the [`DoctrineAuthorityPolicy::built_in`] defaults by
//! setting `CELLOS_CORTEX_POLICY_PATH` to a JSON file; the file is parsed
//! into a [`DoctrineAuthorityPolicy`] and **merged on top of** the built-in
//! policy (per-id keys from the operator file win, missing keys fall back
//! to built-ins).

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;

/// A single doctrine → spec-constraint rule. All fields are optional;
/// `None` means "this rule does not touch this dimension".
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DoctrineAuthorityRule {
    /// Maximum `lifetime.ttlSeconds` allowed. The spec's TTL is clamped
    /// *down* to this value; never raised above it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_ttl_seconds: Option<u64>,

    /// Force `run.secretDelivery` to this mode when the existing mode is
    /// weaker. `Env` is always considered weaker than the two broker modes;
    /// `RuntimeBroker` and `RuntimeLeasedBroker` are treated as equally
    /// strong for the purpose of override (an existing broker mode is
    /// never downgraded to `Env`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub require_secret_delivery: Option<SecretDeliveryMode>,

    /// When `true`, strip every `EgressRule` whose `dnsEgressJustification`
    /// is `None` or empty. Rules with a non-empty justification are kept.
    #[serde(default, skip_serializing_if = "is_false")]
    pub require_egress_justification: bool,

    /// When `true`, replace `authority.egressRules` with `Some(vec![])` —
    /// "isolation default", per `docs/DOCTRINE.md` invariant 5.
    #[serde(default, skip_serializing_if = "is_false")]
    pub forbid_egress: bool,

    /// Optional `(label_key, label_value)` to drop into
    /// `spec.correlation.labels`. Used by every built-in rule so operators
    /// can audit *which* doctrine ids shaped a given cell.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub correlation_label: Option<(String, String)>,
}

fn is_false(b: &bool) -> bool {
    !*b
}

/// Table of doctrine id → [`DoctrineAuthorityRule`].
///
/// See `built_in` for the default table; `load_from_env` for operator
/// overrides; `apply_policy` for the application function.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DoctrineAuthorityPolicy {
    /// Doctrine id (`"D1"`, `"D5"`, `"requires-internet"`, …) → rule.
    pub rules: HashMap<String, DoctrineAuthorityRule>,
}

impl DoctrineAuthorityPolicy {
    /// Build an empty policy. `apply_policy` is a no-op against this.
    pub fn empty() -> Self {
        Self {
            rules: HashMap::new(),
        }
    }

    /// Built-in mapping covering the seven invariants in `docs/DOCTRINE.md`
    /// plus the two operator-tag conventions Cortex already emits. See
    /// ADR-0009 § Doctrine → Authority mapping for the full table.
    ///
    /// # Enforcement kind per doctrine (red-team finding C-F3)
    ///
    /// Not every doctrine in this table mutates the spec. Some only stamp a
    /// `correlation_label` so the audit trail can prove the doctrine was
    /// honoured — the *enforcement* lives elsewhere in the stack.
    ///
    /// | Doctrine | Kind                | Enforced by              |
    /// |----------|---------------------|--------------------------|
    /// | D1       | spec mutation       | `require_secret_delivery` here |
    /// | D2       | spec mutation       | `forbid_egress` here     |
    /// | D3       | label only (audit)  | supervisor admission gate (verifies authority token before launch) |
    /// | D4       | label only (audit)  | `cellos-supervisor` persistence policy (no ambient writable mounts) |
    /// | D5       | spec mutation       | `max_ttl_seconds` here   |
    /// | D6       | label only (audit)  | `cellos-core::events` (every event carries `cellId`/`runId`) |
    /// | D7       | label only (audit)  | supervisor teardown path (`TeardownReport.peers_tracked_after == 0`) |
    /// | requires-internet | spec mutation | `require_egress_justification` here |
    /// | short-lived       | spec mutation | `max_ttl_seconds` here    |
    ///
    /// Label-only doctrines are intentional: D3/D4/D6/D7 ultimately bind to
    /// supervisor-layer guarantees CellOS already enforces unconditionally,
    /// so the policy table records *that the doctrine was declared* without
    /// duplicating enforcement in two places. A future ADR amendment may add
    /// a typed `DoctrineEnforcementKind { SpecMutation, AuditOnly }` field
    /// to make this distinction first-class.
    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 }
    }

    /// Load operator overrides from `CELLOS_CORTEX_POLICY_PATH`.
    ///
    /// - If the env var is unset, returns `built_in()` unchanged.
    /// - If the env var is set and the file exists, parses it as a JSON
    ///   `DoctrineAuthorityPolicy` and merges entries on top of the
    ///   built-in policy (operator keys win on collision).
    /// - If the env var is set but the file cannot be read or parsed,
    ///   returns an error rather than silently falling back to built-ins
    ///   (operators who set the variable expect their file to be honored).
    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))
    }

    /// Same as `load_from_env` but takes an explicit path. Useful for tests.
    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)
    }
}

/// Apply `policy` to `spec`, driven by the doctrine ids declared on `pack`.
///
/// All mutations are monotonic toward least authority — see the module
/// docs. Doctrine ids on `pack` that have no matching rule in `policy` are
/// silently ignored; rules in `policy` whose ids are not on `pack` are
/// silently ignored.
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 {
            // Red-team finding C-F2: silent fall-through is intentional per
            // ADR-0009 § Consequences (we never *raise* authority from an
            // unknown id) but the operator deserves an audit trail when a
            // declared doctrine id has no matching enforcement — otherwise a
            // typo like `"D5 "` (trailing space) silently degrades to the
            // default 300s TTL with no signal. Promote to `error!` + fail-
            // closed once the doctrine catalogue is locked (future ADR-0009
            // amendment).
            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) {
    // 1. TTL clamp (down only).
    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;
        }
        // Keep RunSpec.timeout_ms in sync with the clamped TTL, otherwise
        // a 300s TTL with a 1800s timeout_ms (inherited from a more
        // permissive prior spec) would silently keep the cell alive past
        // the doctrine bound.
        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 => {} // already tighter
                _ => run.timeout_ms = Some(new_timeout_ms),
            }
        }
    }

    // 2. Secret delivery override (only when stricter).
    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();
        }
    }

    // 3. Forbid egress (replace egress_rules with empty vec).
    if rule.forbid_egress {
        spec.authority.egress_rules = Some(Vec::new());
    }

    // 4. Require justification on each egress rule (strip un-justified).
    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)
            });
        }
    }

    // 5. Correlation label.
    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(),
    }
}

/// True when `required` is strictly stronger than `current`. Used to
/// guarantee monotonic-toward-least-authority semantics.
///
/// Strength ordering (ascending): `Env` < `RuntimeBroker` ≤ `RuntimeLeasedBroker`.
/// The two broker modes are not strictly ordered; we only override `Env` →
/// broker, never broker → broker (a deployment that picked
/// `RuntimeLeasedBroker` is already at the recommended A3-01 default and
/// is not weakened by a rule asking for `RuntimeBroker`).
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);
        // run.timeout_ms must be re-pinned to the clamped TTL — otherwise
        // a 1_800_000 ms timeout would silently outlive the watchdog.
        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);
        // D5 caps at 300; current is 60. Monotonic toward least authority:
        // we do NOT raise it to 300.
        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();
        // fresh_spec starts at Env (the explicit-opt-in weakest mode).
        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);
        // Still RuntimeLeasedBroker — no spurious "override" applied.
        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() {
        // Override D5 to a tighter 30s and add a brand-new id.
        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();

        // D5 was overridden.
        let d5 = merged.rules.get("D5").expect("D5 present");
        assert_eq!(d5.max_ttl_seconds, Some(30));
        // D1 fell through from the built-ins.
        let d1 = merged
            .rules
            .get("D1")
            .expect("D1 fell through from built_in");
        assert_eq!(
            d1.require_secret_delivery.as_ref(),
            Some(&SecretDeliveryMode::RuntimeLeasedBroker)
        );
        // New id from the override file is present.
        assert!(merged.rules.contains_key("tighter-than-built-in"));
    }

    #[test]
    fn is_stricter_delivery_only_promotes_env_to_broker() {
        // env → broker: yes
        assert!(is_stricter_delivery(
            &SecretDeliveryMode::Env,
            &SecretDeliveryMode::RuntimeBroker
        ));
        assert!(is_stricter_delivery(
            &SecretDeliveryMode::Env,
            &SecretDeliveryMode::RuntimeLeasedBroker
        ));
        // broker → broker: no (preserve current choice)
        assert!(!is_stricter_delivery(
            &SecretDeliveryMode::RuntimeBroker,
            &SecretDeliveryMode::RuntimeLeasedBroker
        ));
        assert!(!is_stricter_delivery(
            &SecretDeliveryMode::RuntimeLeasedBroker,
            &SecretDeliveryMode::RuntimeBroker
        ));
        // broker → env: never (this would downgrade)
        assert!(!is_stricter_delivery(
            &SecretDeliveryMode::RuntimeLeasedBroker,
            &SecretDeliveryMode::Env
        ));
    }
}