Skip to main content

agent_core/core/
disclosure.rs

1//! Task 34 — per-record/event disclosure classes (spec §9.7).
2//!
3//! One typed vocabulary and ONE model-visibility gate. Every boundary that
4//! moves content into model context routes through [`gate_for_model`];
5//! persistence boundaries consult [`may_persist`]. Consent and redaction
6//! fail CLOSED: no consent → withheld, no redactor → withheld.
7
8use serde::{Deserialize, Serialize};
9
10/// Disclosure class of a record or event (spec §9.7).
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum DisclosureClass {
14    /// Baseline: freely model-visible and persistable.
15    #[default]
16    ModelVisible,
17    /// Display locally only — never enters model context.
18    LocalOnly,
19    /// Model-visible only AFTER a redactor has been applied.
20    ModelVisibleAfterRedaction,
21    /// Model-visible only after explicit per-item consent.
22    ModelVisibleAfterConsent,
23    /// May persist to disk but never transmit (model context included —
24    /// model context reaches remote providers).
25    PersistNeverTransmit,
26    /// Never persisted; visibility itself is not restricted.
27    NeverPersist,
28}
29
30impl DisclosureClass {
31    /// Canonical snake_case name (matches the serde representation).
32    pub fn as_str(&self) -> &'static str {
33        match self {
34            DisclosureClass::ModelVisible => "model_visible",
35            DisclosureClass::LocalOnly => "local_only",
36            DisclosureClass::ModelVisibleAfterRedaction => "model_visible_after_redaction",
37            DisclosureClass::ModelVisibleAfterConsent => "model_visible_after_consent",
38            DisclosureClass::PersistNeverTransmit => "persist_never_transmit",
39            DisclosureClass::NeverPersist => "never_persist",
40        }
41    }
42
43    /// Parse a canonical name.
44    pub fn parse(s: &str) -> Option<Self> {
45        [
46            DisclosureClass::ModelVisible,
47            DisclosureClass::LocalOnly,
48            DisclosureClass::ModelVisibleAfterRedaction,
49            DisclosureClass::ModelVisibleAfterConsent,
50            DisclosureClass::PersistNeverTransmit,
51            DisclosureClass::NeverPersist,
52        ]
53        .into_iter()
54        .find(|c| c.as_str() == s)
55    }
56}
57
58/// Outcome of the model-visibility gate.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum ModelVisibility {
61    /// This text (possibly redacted) may enter model context.
62    Visible(String),
63    /// Withheld, with the typed reason boundaries surface as a marker.
64    Withheld(&'static str),
65}
66
67/// THE model-visibility gate (spec §9.7). `consent_granted` is per-item
68/// explicit consent; `redactor` transforms text for the after-redaction
69/// class. Missing prerequisites fail closed.
70pub fn gate_for_model(
71    class: DisclosureClass,
72    text: &str,
73    consent_granted: bool,
74    redactor: Option<&dyn Fn(&str) -> String>,
75) -> ModelVisibility {
76    match class {
77        DisclosureClass::ModelVisible | DisclosureClass::NeverPersist => {
78            ModelVisibility::Visible(text.to_string())
79        }
80        DisclosureClass::LocalOnly => ModelVisibility::Withheld("local_only"),
81        DisclosureClass::ModelVisibleAfterRedaction => match redactor {
82            Some(redact) => ModelVisibility::Visible(redact(text)),
83            None => ModelVisibility::Withheld("redaction required but no redactor configured"),
84        },
85        DisclosureClass::ModelVisibleAfterConsent => {
86            if consent_granted {
87                ModelVisibility::Visible(text.to_string())
88            } else {
89                ModelVisibility::Withheld("explicit consent required")
90            }
91        }
92        DisclosureClass::PersistNeverTransmit => {
93            ModelVisibility::Withheld("persist_never_transmit")
94        }
95    }
96}
97
98/// Persistence gate: only [`DisclosureClass::NeverPersist`] refuses disk.
99pub fn may_persist(class: DisclosureClass) -> bool {
100    class != DisclosureClass::NeverPersist
101}