Skip to main content

lean_ctx/core/engine_interface/context_plan/
sources.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Ephemeral explicit-source provider; never persists into project/global stores.
4
5use std::{
6    collections::{BTreeMap, HashMap},
7    path::Path,
8};
9
10use chrono::{DateTime, SecondsFormat, Timelike, Utc};
11use lean_ctx_protocol::context_gateway::ClassificationV1;
12use lean_ctx_protocol::{
13    ContextDispositionV1, DataClassification, EngineContextSourceDescriptorV1,
14    EngineContextSourcePermissionV1, EngineContextSourcePlanRequestV1,
15    EngineContextSourcePlanResponseV1, EngineContextSourceV1, UtcTimestamp,
16};
17
18use crate::core::{
19    context_field::{ContextField, ContextItemId, FieldWeights, Provenance, ViewCosts, ViewKind},
20    context_kernel::{
21        autopilot::{AutopilotController, TaskAutopilotDecision},
22        orchestrator::ContextKernel,
23        policy::ContextPolicy,
24        types::{
25            CandidateProvider, ContextObjectKind, ContextObjectV1, Freshness, RetrievalContext,
26            SensitivityLevel, SideEffectPolicy,
27        },
28    },
29};
30
31#[derive(Clone)]
32struct ExplicitSourceProvider(Vec<ContextObjectV1>);
33
34impl CandidateProvider for ExplicitSourceProvider {
35    fn provider_id(&self) -> &'static str {
36        "engine.explicit-sources.v1"
37    }
38
39    fn candidates(&self, ctx: &RetrievalContext) -> Vec<ContextObjectV1> {
40        self.0.iter().take(ctx.max_candidates).cloned().collect()
41    }
42
43    fn side_effect_policy(&self) -> SideEffectPolicy {
44        SideEffectPolicy::ReadOnly
45    }
46}
47
48pub fn plan(
49    root: &Path,
50    request: EngineContextSourcePlanRequestV1,
51) -> Result<EngineContextSourcePlanResponseV1, &'static str> {
52    request
53        .validate_payload()
54        .map_err(|_| "invalid_source_manifest")?;
55    ensure_project_root_matches_scope(root)?;
56    let policy = current_policy(root)?;
57    let (evaluation_time, include_marker) = if policy.retention_days.is_some() {
58        (
59            Utc::now()
60                .with_nanosecond(0)
61                .ok_or("source_plan_epoch_invalid")?
62                .fixed_offset(),
63            true,
64        )
65    } else {
66        (Utc::now().fixed_offset(), false)
67    };
68    Ok(plan_at(root, request, evaluation_time, include_marker)?.response)
69}
70
71/// Private same-process handoff; no deserialization from a caller's wire plan.
72pub(super) struct SourcePlanningHandoff {
73    pub(super) decision: TaskAutopilotDecision,
74    pub(super) response: EngineContextSourcePlanResponseV1,
75    pub(super) admitted_sources: Vec<AdmittedSourceBody>,
76}
77
78/// Private, freshly admitted bodies for the selected source bindings. The
79/// descriptor still identifies the caller's original bytes; the retained
80/// candidate holds the current policy-approved representation for delivery.
81pub(super) struct AdmittedSourceBody {
82    pub(super) descriptor: EngineContextSourceDescriptorV1,
83    pub(super) candidate: ContextObjectV1,
84}
85
86/// Replan a materialization request at the plan's retained identity time.
87///
88/// The identity time is only used to reconstruct the original retention-aware
89/// digest. Fresh source and policy admission is performed independently below.
90pub(super) fn plan_for_materialization(
91    root: &Path,
92    request: EngineContextSourcePlanRequestV1,
93    evaluation_time: Option<DateTime<chrono::FixedOffset>>,
94) -> Result<SourcePlanningHandoff, &'static str> {
95    request
96        .validate_payload()
97        .map_err(|_| "invalid_source_manifest")?;
98    ensure_project_root_matches_scope(root)?;
99    let policy = current_policy(root)?;
100    let include_marker = policy.retention_days.is_some();
101    let evaluation_time = if include_marker {
102        let evaluation_time = evaluation_time.ok_or("source_plan_epoch_missing")?;
103        if evaluation_time > Utc::now().fixed_offset() {
104            return Err("source_plan_epoch_invalid");
105        }
106        evaluation_time
107    } else {
108        // Retention is the only policy that contributes the epoch to identity;
109        // ignore an optional echo for old no-retention plans.
110        Utc::now().fixed_offset()
111    };
112    plan_at(root, request, evaluation_time, include_marker)
113}
114
115fn plan_at(
116    root: &Path,
117    request: EngineContextSourcePlanRequestV1,
118    evaluation_time: DateTime<chrono::FixedOffset>,
119    include_marker: bool,
120) -> Result<SourcePlanningHandoff, &'static str> {
121    request
122        .validate_payload()
123        .map_err(|_| "invalid_source_manifest")?;
124    ensure_project_root_matches_scope(root)?;
125    let project_root = root.to_str().ok_or("unsafe_root")?;
126    crate::core::policy::runtime::with_project_source_view(project_root, || {
127        plan_at_in_view(root, request, evaluation_time, include_marker)
128    })
129    .map_err(|_| "source_policy_view_unavailable")?
130}
131
132fn plan_at_in_view(
133    root: &Path,
134    request: EngineContextSourcePlanRequestV1,
135    evaluation_time: DateTime<chrono::FixedOffset>,
136    include_marker: bool,
137) -> Result<SourcePlanningHandoff, &'static str> {
138    let now = Utc::now();
139    let max_sensitivity = current_policy(root)?.max_sensitivity;
140    let mut candidates = request
141        .sources
142        .iter()
143        .map(|source| candidate(source, now, &max_sensitivity))
144        .collect::<Result<Vec<_>, _>>()?
145        .into_iter()
146        .flatten()
147        .collect::<Vec<_>>();
148    // Bound enumeration deterministically, never by untrusted input order.
149    candidates.sort_by(|left, right| left.id.as_str().cmp(right.id.as_str()));
150    let admitted_candidate_by_ref = candidates
151        .iter()
152        .map(|candidate| (candidate.id.0.clone(), candidate.clone()))
153        .collect::<HashMap<_, _>>();
154    let provider = ExplicitSourceProvider(candidates);
155    let controller = AutopilotController::with_kernels(
156        ContextKernel::with_field(
157            vec![Box::new(provider.clone())],
158            ContextField::with_weights(FieldWeights::default()),
159        )
160        .with_relevance_floor(),
161        ContextKernel::with_field(
162            vec![Box::new(provider)],
163            ContextField::with_weights(FieldWeights::default()),
164        )
165        .with_relevance_floor(),
166    );
167    let decision = super::plan_with_controller_at(
168        root,
169        request.planning,
170        Some(controller),
171        Some(evaluation_time),
172    )?;
173    let initial_result = super::response(&decision)?;
174    let epoch = if include_marker {
175        Some(
176            UtcTimestamp::new(evaluation_time.to_rfc3339_opts(SecondsFormat::Secs, true))
177                .map_err(|_| "source_plan_epoch_invalid")?,
178        )
179    } else {
180        None
181    };
182    // Only kernel-admitted origins may contribute equivalent-source metadata.
183    // Hash equality alone cannot resurrect a denied or unenumerated source.
184    let mut by_digest = BTreeMap::<&str, Vec<&EngineContextSourceDescriptorV1>>::new();
185    for source in &request.sources {
186        let descriptor = &source.descriptor;
187        if let Some([origin]) = decision
188            .decision()
189            .context_plan
190            .origins
191            .get(descriptor.object_ref.as_str())
192            .map(Vec::as_slice)
193            && origin.admitted
194            && origin.source == descriptor.source_id.as_str()
195            && origin.content_ref == descriptor.content_digest.as_str()
196        {
197            by_digest
198                .entry(descriptor.content_digest.as_str())
199                .or_default()
200                .push(descriptor);
201        }
202    }
203    let mut groups = Vec::new();
204    for selection in &initial_result.plan.selections {
205        if selection.disposition != ContextDispositionV1::Selected {
206            continue;
207        }
208        if let Some(equivalent) = selection
209            .sha256_digest
210            .as_deref()
211            .and_then(|digest| by_digest.get_mut(digest))
212            && equivalent.len() > 1
213        {
214            equivalent.sort_by(|left, right| left.object_ref.cmp(&right.object_ref));
215            groups.push(serde_json::json!({
216                "selected_ref": selection.source_ref,
217                "equivalent_sources": equivalent,
218            }));
219        }
220    }
221    let lineage = (!groups.is_empty()).then(|| {
222        serde_json::json!({
223            "schema_version": 1, "groups": groups,
224        })
225    });
226    let source_bindings = request
227        .sources
228        .iter()
229        .filter(|source| {
230            initial_result.plan.selections.iter().any(|selection| {
231                selection.source_ref == source.descriptor.object_ref.as_str()
232                    && selection.disposition == ContextDispositionV1::Selected
233            })
234        })
235        .map(|source| source.descriptor.clone())
236        .collect::<Vec<_>>();
237    let admitted_sources = source_bindings
238        .iter()
239        .map(|descriptor| {
240            let candidate = admitted_candidate_by_ref
241                .get(descriptor.object_ref.as_str())
242                .ok_or("source_plan_changed")?;
243            Ok(AdmittedSourceBody {
244                descriptor: descriptor.clone(),
245                candidate: candidate.clone(),
246            })
247        })
248        .collect::<Result<Vec<_>, &'static str>>()?;
249    let admission_metadata = source_admission_metadata(&admitted_sources)?;
250    // Two binds in the single bind's key order, so each bound has its own
251    // typed failure: an oversized lineage keeps the contract's
252    // `source_lineage_too_large` (docs/contracts/engine-context-sources-v1.md),
253    // never the newer admission-metadata code.
254    let decision = decision
255        .bind_source_metadata(lineage, epoch, None)
256        .map_err(|_| "source_lineage_too_large")?
257        .bind_source_metadata(None, None, Some(admission_metadata))
258        .map_err(|_| "source_plan_metadata_too_large")?;
259    let result = super::response(&decision)?;
260    // Reload immediately before returning: the replay identity is not a
261    // concurrency lock, so a policy change must win over its older snapshot.
262    ensure_current_admission(
263        root,
264        &request.sources,
265        &source_bindings,
266        &admitted_sources,
267        result.plan.budget_tokens,
268    )?;
269    let response =
270        EngineContextSourcePlanResponseV1::new(result, source_bindings).map_err(|_| "internal")?;
271    // No presentation is recorded here: selection alone is not yet a
272    // delivery under the project's delivery policy. The revalidation that
273    // closes a materialization records it (`revalidate_admitted_sources`).
274    Ok(SourcePlanningHandoff {
275        decision,
276        response,
277        admitted_sources,
278    })
279}
280
281fn current_policy(root: &Path) -> Result<ContextPolicy, &'static str> {
282    let root = root.to_str().ok_or("unsafe_root")?;
283    ContextPolicy::from_config(root).map_err(|_| "context_policy_unavailable")
284}
285
286/// The kernel context policy (sensitivity ceiling, budget cap, retention) in
287/// a stable serialized form — one part of the policy identity a presented
288/// source is admitted under. `None`: the policy cannot be loaded.
289pub(crate) fn kernel_policy_identity(root: &Path) -> Option<String> {
290    serde_json::to_string(&current_policy(root).ok()?).ok()
291}
292
293/// The bindings of the selected, admitted caller-supplied sources as they
294/// hold now. `None` — no lineage at all — when one of them is restricted
295/// (owner decision E3: a restricted source is in no reusable lineage) or the
296/// policy identity cannot be resolved; an empty selection is `None` too.
297pub(super) fn presented_bindings(
298    root: &Path,
299    admitted_sources: &[AdmittedSourceBody],
300) -> Option<Vec<crate::core::archive::presented::PresentedBinding>> {
301    if admitted_sources.is_empty() {
302        return None;
303    }
304    admitted_sources
305        .iter()
306        .map(|source| {
307            if source.candidate.sensitivity == SensitivityLevel::Restricted {
308                return None;
309            }
310            crate::core::archive::presented::PresentedBinding::admitted_now(
311                root,
312                source.descriptor.object_ref.as_str(),
313                source.descriptor.source_id.as_str(),
314                source.descriptor.content_digest.as_str(),
315            )
316        })
317        .collect()
318}
319
320/// Record "presented and admitted now" for every admitted caller-supplied
321/// source that is not restricted. Best effort: a presentation that cannot be
322/// recorded leaves the binding without authority, never with a wrong one.
323fn record_presentations(root: &Path, admitted_sources: &[AdmittedSourceBody]) {
324    for source in admitted_sources {
325        if source.candidate.sensitivity == SensitivityLevel::Restricted {
326            continue;
327        }
328        if let Some(binding) = crate::core::archive::presented::PresentedBinding::admitted_now(
329            root,
330            source.descriptor.object_ref.as_str(),
331            source.descriptor.source_id.as_str(),
332            source.descriptor.content_digest.as_str(),
333        ) {
334            crate::core::archive::presented::record_presentation(&binding);
335        }
336    }
337}
338
339fn ensure_project_root_matches_scope(root: &Path) -> Result<(), &'static str> {
340    let request_root = crate::core::policy::runtime::detached_project_root(root)
341        .map_err(|_| "source_policy_view_unavailable")?;
342    if request_root != root {
343        let supplied = root
344            .canonicalize()
345            .map_err(|_| "source_policy_view_unavailable")?;
346        let requested = request_root
347            .canonicalize()
348            .map_err(|_| "source_policy_view_unavailable")?;
349        if supplied != requested {
350            return Err("source_policy_view_mismatch");
351        }
352    }
353    Ok(())
354}
355
356fn source_admission_metadata(
357    sources: &[AdmittedSourceBody],
358) -> Result<serde_json::Value, &'static str> {
359    let mut records = sources
360        .iter()
361        .map(|source| {
362            let content = source
363                .candidate
364                .content
365                .as_deref()
366                .ok_or("source_plan_changed")?;
367            let admitted_digest = super::super::sha256_digest(content.as_bytes())
368                .map_err(|_| "source_plan_digest_failed")?;
369            Ok(serde_json::json!({
370                "object_ref": source.descriptor.object_ref.as_str(),
371                "source_id": source.descriptor.source_id.as_str(),
372                "original_digest": source.descriptor.content_digest.as_str(),
373                "admitted_digest": admitted_digest.as_str(),
374                "effective_classification": classification_name(&source.candidate.sensitivity),
375            }))
376        })
377        .collect::<Result<Vec<_>, &'static str>>()?;
378    records.sort_by(|left, right| {
379        left["object_ref"]
380            .as_str()
381            .cmp(&right["object_ref"].as_str())
382    });
383    Ok(serde_json::json!({
384        "schema_version": 1,
385        "sources": records,
386    }))
387}
388
389fn classification_name(sensitivity: &SensitivityLevel) -> &'static str {
390    match sensitivity {
391        SensitivityLevel::Public => "public",
392        SensitivityLevel::Internal => "internal",
393        SensitivityLevel::Confidential => "confidential",
394        SensitivityLevel::Restricted => "restricted",
395    }
396}
397
398pub(super) fn ensure_current_admission(
399    root: &Path,
400    sources: &[EngineContextSourceV1],
401    bindings: &[EngineContextSourceDescriptorV1],
402    admitted_sources: &[AdmittedSourceBody],
403    budget_tokens: u64,
404) -> Result<(), &'static str> {
405    let policy = current_policy(root)?;
406    if let Some(cap) = policy.budget_cap_tokens
407        && budget_tokens > u64::try_from(cap).map_err(|_| "context_policy_unavailable")?
408    {
409        return Err("source_plan_changed");
410    }
411    let now = Utc::now();
412    let evaluation_time = now.fixed_offset();
413    for binding in bindings {
414        sources
415            .iter()
416            .find(|source| source.descriptor == *binding)
417            .ok_or("source_plan_changed")?;
418        let object = admitted_sources
419            .iter()
420            .find(|source| source.descriptor == *binding)
421            .map(|source| &source.candidate)
422            .ok_or("source_plan_changed")?;
423        if policy
424            .candidate_violation_at(&object, Some(&evaluation_time))
425            .is_some()
426        {
427            return Err("source_plan_changed");
428        }
429    }
430    Ok(())
431}
432
433pub(super) fn revalidate_admitted_sources(
434    root: &Path,
435    sources: &[EngineContextSourceV1],
436    bindings: &[EngineContextSourceDescriptorV1],
437    admitted_sources: &[AdmittedSourceBody],
438    budget_tokens: u64,
439) -> Result<(), &'static str> {
440    ensure_project_root_matches_scope(root)?;
441    let project_root = root.to_str().ok_or("unsafe_root")?;
442    crate::core::policy::runtime::with_project_source_view(project_root, || {
443        let policy = current_policy(root)?;
444        if let Some(cap) = policy.budget_cap_tokens
445            && budget_tokens > u64::try_from(cap).map_err(|_| "context_policy_unavailable")?
446        {
447            return Err("source_plan_changed");
448        }
449        let now = Utc::now();
450        let evaluation_time = now.fixed_offset();
451        let mut revalidated = Vec::with_capacity(bindings.len());
452        for binding in bindings {
453            let original = sources
454                .iter()
455                .find(|source| source.descriptor == *binding)
456                .ok_or("source_plan_changed")?;
457            let retained = admitted_sources
458                .iter()
459                .find(|source| source.descriptor == *binding)
460                .ok_or("source_plan_changed")?;
461            let current =
462                candidate(original, now, &policy.max_sensitivity)?.ok_or("source_plan_changed")?;
463            if current.content != retained.candidate.content
464                || current.sensitivity != retained.candidate.sensitivity
465                || policy
466                    .candidate_violation_at(&current, Some(&evaluation_time))
467                    .is_some()
468            {
469                return Err("source_plan_changed");
470            }
471            revalidated.push(AdmittedSourceBody {
472                descriptor: binding.clone(),
473                candidate: current,
474            });
475        }
476        // Every binding was presented again in this request and admitted now.
477        record_presentations(root, &revalidated);
478        Ok(())
479    })
480    .map_err(|_| "source_policy_view_unavailable")?
481}
482
483/// `Ok(None)`: the source is rejected BEFORE it becomes a candidate and leaves
484/// no selection entry (contract: "rejected pre-candidate inputs have no
485/// selection entry").
486///
487/// That includes a source whose body carries a classification marking the
488/// gateway DETECTS, above both what its descriptor declares and what the
489/// kernel policy admits (`max_sensitivity`). Its descriptor misstates the
490/// body, so the input is rejected like any other invalid manifest — and a
491/// canonical exclusion entry would project the SHA-256 of content that is
492/// more sensitive than this plan may carry. A source whose DECLARED
493/// classification exceeds the policy is unchanged: it stays a candidate and
494/// the kernel records its canonical policy exclusion.
495pub fn candidate(
496    source: &EngineContextSourceV1,
497    now: DateTime<Utc>,
498    max_sensitivity: &SensitivityLevel,
499) -> Result<Option<ContextObjectV1>, &'static str> {
500    let descriptor = &source.descriptor;
501    if descriptor.permission != EngineContextSourcePermissionV1::Permitted {
502        return Ok(None);
503    }
504    let declared_classification = match descriptor.classification.as_ref() {
505        Some(DataClassification::Public) => ClassificationV1::Public,
506        Some(DataClassification::Internal) => ClassificationV1::Internal,
507        Some(DataClassification::Confidential) => ClassificationV1::Confidential,
508        Some(DataClassification::Restricted) => ClassificationV1::Restricted,
509        None => return Ok(None),
510    };
511    for (timestamp, expiry) in [
512        (&descriptor.observed_at, false),
513        (&descriptor.valid_until, true),
514    ] {
515        if let Some(timestamp) = timestamp {
516            let time = DateTime::parse_from_rfc3339(timestamp.as_str())
517                .map_err(|_| "invalid_source_manifest")?;
518            if (expiry && time <= now) || (!expiry && time > now) {
519                return Ok(None);
520            }
521        }
522    }
523    let admission = match crate::core::context_admission::admit_context_source(&source.content) {
524        Ok(admission) => admission,
525        // Withheld sources do not enter selection. The admission layer records
526        // the content-free decision; the plan never echoes the rejected body.
527        Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => return Ok(None),
528        Err(_) => return Err("source_admission_unavailable"),
529    };
530    let admitted_maximum = match max_sensitivity {
531        SensitivityLevel::Public => ClassificationV1::Public,
532        SensitivityLevel::Internal => ClassificationV1::Internal,
533        SensitivityLevel::Confidential => ClassificationV1::Confidential,
534        SensitivityLevel::Restricted => ClassificationV1::Restricted,
535    };
536    // What an unmarked object is classified as in the configured mode: only a
537    // classification above it comes from a marking detected in the body.
538    let unmarked = crate::core::config::Config::load_arc()
539        .context_gateway
540        .mode
541        .unclassified();
542    let detected = admission.classification;
543    if detected > unmarked && detected > declared_classification && detected > admitted_maximum {
544        return Ok(None);
545    }
546    let classification = declared_classification.join(admission.classification);
547    let sensitivity = match classification {
548        ClassificationV1::Public => SensitivityLevel::Public,
549        ClassificationV1::Internal => SensitivityLevel::Internal,
550        ClassificationV1::Confidential => SensitivityLevel::Confidential,
551        ClassificationV1::Restricted => SensitivityLevel::Restricted,
552    };
553    let tokens = crate::core::tokens::count_tokens(&admission.text).max(1);
554    // Only full content is materialized: never advertise imaginary cheap views.
555    let mut view_costs = ViewCosts::new();
556    view_costs.set(ViewKind::Full, tokens);
557    let observed = descriptor
558        .observed_at
559        .as_ref()
560        .map(|time| time.as_str().to_owned());
561    let mut metadata = HashMap::new();
562    metadata.insert(
563        "source_binding".to_owned(),
564        serde_json::to_string(descriptor).map_err(|_| "invalid_source_manifest")?,
565    );
566    Ok(Some(ContextObjectV1 {
567        id: ContextItemId(descriptor.object_ref.as_str().to_owned()),
568        kind: ContextObjectKind::SearchChunk,
569        source: descriptor.source_id.as_str().to_owned(),
570        content_ref: descriptor.content_digest.as_str().to_owned(),
571        title: descriptor.object_ref.as_str().to_owned(),
572        content: Some(admission.text),
573        freshness: Freshness {
574            created_at: observed.clone().unwrap_or_default(),
575            ttl_secs: None,
576            stale: observed.is_none(),
577        },
578        // No verified authority/reliability is inferred from producer metadata.
579        confidence: 0.0,
580        sensitivity,
581        token_estimate: tokens,
582        view_costs,
583        provenance: Provenance {
584            tool: Some("engine.explicit-sources.v1".to_owned()),
585            timestamp: observed,
586            ..Provenance::default()
587        },
588        semantic_fingerprint: None,
589        metadata,
590    }))
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596
597    fn source(content: &str, declared: &str) -> EngineContextSourceV1 {
598        let digest = super::super::super::sha256_digest(content.as_bytes()).expect("digest");
599        serde_json::from_value(serde_json::json!({
600            "descriptor": {
601                "object_ref": "marked", "source_id": "source-marked",
602                "source_type": "filesystem", "content_digest": digest.as_str(),
603                "revision": "revision-one", "owner": "operator",
604                "observed_at": null, "valid_until": null,
605                "classification": declared, "permission": "permitted"
606            },
607            "content": content
608        }))
609        .expect("source fixture")
610    }
611
612    /// Both branches of the pre-candidate decision. Before, a source whose
613    /// body carries a marking above its descriptor became a candidate, and its
614    /// canonical exclusion projected the digest of the restricted body.
615    #[test]
616    fn a_detected_classification_above_declaration_and_policy_is_no_candidate() {
617        let _data = crate::core::data_dir::isolated_data_dir();
618        let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
619        let dir = crate::core::paths::config_dir_read_only().expect("config dir");
620        std::fs::create_dir_all(&dir).expect("config dir");
621        std::fs::write(
622            dir.join("config.toml"),
623            "[context_gateway]\nclassification = \"warn\"\n",
624        )
625        .expect("config");
626        let now = Utc::now();
627        let marked = "*** TOP SECRET *** invoice ledger plans";
628
629        // Detected restricted, declared public, policy admits internal:
630        // rejected before candidacy — no selection entry, no digest.
631        let laundered = candidate(&source(marked, "Public"), now, &SensitivityLevel::Internal)
632            .expect("no transport error");
633        assert!(laundered.is_none(), "old code returned a candidate");
634
635        // The policy admits restricted content: it stays a candidate, with
636        // the DETECTED sensitivity, never the declared one.
637        let admitted = candidate(
638            &source(marked, "Public"),
639            now,
640            &SensitivityLevel::Restricted,
641        )
642        .expect("no transport error")
643        .expect("candidate");
644        assert_eq!(admitted.sensitivity, SensitivityLevel::Restricted);
645
646        // Declared too high, nothing detected: unchanged — a candidate, so the
647        // kernel records its canonical policy exclusion with its metadata.
648        let declared = candidate(
649            &source("invoice ledger plans", "Restricted"),
650            now,
651            &SensitivityLevel::Internal,
652        )
653        .expect("no transport error")
654        .expect("candidate for the canonical exclusion");
655        assert_eq!(declared.sensitivity, SensitivityLevel::Restricted);
656
657        // Unmarked and public: an ordinary candidate with its body unchanged.
658        let plain = candidate(
659            &source("invoice ledger plans", "Public"),
660            now,
661            &SensitivityLevel::Internal,
662        )
663        .expect("no transport error")
664        .expect("candidate");
665        assert_eq!(plain.sensitivity, SensitivityLevel::Public);
666        assert_eq!(plain.content.as_deref(), Some("invoice ledger plans"));
667    }
668}