Skip to main content

lean_ctx/core/policy/org/
mod.rs

1//! Central, signed org policy distribution (GL #674).
2//!
3//! An organisation authors one policy pack, **signs** it into an
4//! [`OrgPolicyV1`] artifact ([`model`]) with its org key, and distributes both
5//! the artifact and its **public key**. Each endpoint *pins* that key once,
6//! out-of-band ([`trust`]), then installs artifacts ([`store`]); from then on
7//! the runtime folds the org pack in as an un-bypassable **floor**
8//! ([`crate::core::policy::floor`]) beneath the local project pack.
9//!
10//! Two independent checks gate application, both required:
11//! 1. **signature** — the artifact's bytes were signed by the embedded key
12//!    ([`OrgPolicyV1::verify`]); and
13//! 2. **trust** — that key is one this endpoint pinned ([`trust::is_trusted`]).
14//!
15//! With no key pinned, a present artifact is informational only (opt-in). An
16//! invalid or untrusted artifact is never applied and never bricks the agent
17//! (fail-open) — it is logged and surfaced by `policy org status`.
18
19pub mod model;
20pub mod store;
21pub mod trust;
22
23use std::path::PathBuf;
24
25pub use model::{OrgPolicyV1, OrgVerifyResult};
26pub use trust::{TrustStore, TrustedKey};
27
28use crate::core::policy::ResolvedPolicy;
29
30/// Keystore identity used for an org's signing key. Namespaced + sanitised so
31/// `org sign --org "ACME Corp"` maps to a safe, stable key file.
32#[must_use]
33pub fn org_key_id(org: &str) -> String {
34    let safe: String = org
35        .chars()
36        .map(|c| {
37            if c.is_ascii_alphanumeric() || c == '-' {
38                c.to_ascii_lowercase()
39            } else {
40                '-'
41            }
42        })
43        .collect();
44    format!("org-{safe}")
45}
46
47/// The org policy resolved to a [`ResolvedPolicy`] **iff** it is present,
48/// signed by a trusted key, and resolves cleanly. Returns `None` (after a
49/// `warn!`) in every other case so the caller falls back to the local pack
50/// alone. This is the single entry point the runtime uses.
51#[must_use]
52pub fn active_resolved() -> Option<ResolvedPolicy> {
53    let artifact = store::load_active()?;
54
55    let verdict = artifact.verify();
56    if !verdict.signature_valid {
57        tracing::warn!(
58            "org policy: signature invalid ({}); not applied",
59            verdict.error.as_deref().unwrap_or("unknown")
60        );
61        return None;
62    }
63
64    let signer = verdict.signer_public_key.as_deref().unwrap_or_default();
65    if !trust::is_trusted(signer) {
66        tracing::warn!(
67            "org policy: signer key not pinned; not applied \
68             (pin it with `lean-ctx policy org trust <key>`)"
69        );
70        return None;
71    }
72
73    // Advisory rollout: a signed, trusted artifact with `enforced = false` is
74    // distributed for preview (`policy org show`) but is deliberately NOT folded
75    // into enforcement, so admins can stage a policy before turning it on.
76    if !artifact.enforced {
77        return None;
78    }
79
80    match artifact.resolved() {
81        Ok(resolved) => Some(resolved),
82        Err(e) => {
83            tracing::warn!("org policy: pack does not resolve ({e}); not applied");
84            None
85        }
86    }
87}
88
89/// A snapshot of org-policy state for `policy org status` / `show`.
90#[derive(Debug, Clone, Default)]
91pub struct OrgStatus {
92    /// Whether an artifact is present via the source chain at all.
93    pub present: bool,
94    pub source: Option<PathBuf>,
95    pub org: Option<String>,
96    pub policy_version: Option<String>,
97    pub enforced: bool,
98    pub issued_at: Option<String>,
99    pub signer_public_key: Option<String>,
100    /// Cryptographic signature check (independent of trust).
101    pub signature_valid: bool,
102    /// Whether the signer key is pinned on this endpoint.
103    pub trusted: bool,
104    /// `true` ⇔ the policy is actually enforced (present ∧ signed ∧ trusted ∧
105    /// resolves). This is the bottom line for an auditor.
106    pub applied: bool,
107    /// Set when the pack body fails to resolve despite a valid signature.
108    pub resolve_error: Option<String>,
109    /// Count of pinned trust anchors on this endpoint.
110    pub pinned_anchors: usize,
111}
112
113/// Compute the current org-policy [`OrgStatus`] for display.
114#[must_use]
115pub fn status() -> OrgStatus {
116    let mut s = OrgStatus {
117        pinned_anchors: trust::trusted_keys().len(),
118        ..OrgStatus::default()
119    };
120    let Some(source) = store::source_path() else {
121        return s;
122    };
123    s.present = true;
124    s.source = Some(source.clone());
125    let Ok(artifact) = store::read(&source) else {
126        return s;
127    };
128    s.org = Some(artifact.org.clone());
129    s.policy_version = Some(artifact.policy_version.clone());
130    s.enforced = artifact.enforced;
131    s.issued_at = Some(artifact.issued_at.clone());
132
133    let verdict = artifact.verify();
134    s.signature_valid = verdict.signature_valid;
135    s.signer_public_key.clone_from(&verdict.signer_public_key);
136    s.trusted = verdict
137        .signer_public_key
138        .as_deref()
139        .is_some_and(trust::is_trusted);
140
141    if s.signature_valid && s.trusted {
142        match artifact.resolved() {
143            // Advisory (`enforced = false`) artifacts are valid + trusted but
144            // intentionally not enforced — see `active_resolved`.
145            Ok(_) => s.applied = artifact.enforced,
146            Err(e) => s.resolve_error = Some(e.to_string()),
147        }
148    }
149    s
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::core::data_dir::isolated_data_dir;
156
157    const PACK: &str = r#"
158name = "acme-floor"
159version = "1.0.0"
160description = "ACME org floor"
161
162[context]
163deny_tools = ["ctx_url_read"]
164"#;
165
166    fn install_signed(enforced: bool) -> String {
167        let key = crate::core::agent_identity::get_or_create_keypair(&org_key_id("acme")).unwrap();
168        let mut a = OrgPolicyV1::build("acme", "2026.06.1", enforced, PACK).unwrap();
169        a.sign_with_key(&key);
170        store::install(&a).unwrap();
171        crate::core::agent_identity::hex_encode(&key.verifying_key().to_bytes())
172    }
173
174    #[test]
175    fn org_key_id_is_sanitised() {
176        assert_eq!(org_key_id("ACME Corp"), "org-acme-corp");
177        assert_eq!(org_key_id("acme"), "org-acme");
178    }
179
180    #[test]
181    fn not_applied_without_trust_even_if_signed() {
182        let _iso = isolated_data_dir();
183        install_signed(true);
184        // Signed, present — but the key is not pinned → not applied.
185        assert!(active_resolved().is_none());
186        let s = status();
187        assert!(s.present);
188        assert!(s.signature_valid);
189        assert!(!s.trusted);
190        assert!(!s.applied);
191    }
192
193    #[test]
194    fn applied_once_signer_is_pinned() {
195        let _iso = isolated_data_dir();
196        let pk = install_signed(true);
197        trust::pin("acme", &pk).unwrap();
198        let resolved = active_resolved().expect("trusted + signed → applied");
199        assert!(resolved.deny_tools.contains(&"ctx_url_read".to_string()));
200        let s = status();
201        assert!(s.applied);
202        assert!(s.trusted);
203        assert_eq!(s.org.as_deref(), Some("acme"));
204    }
205
206    #[test]
207    fn advisory_artifact_is_trusted_but_not_enforced() {
208        let _iso = isolated_data_dir();
209        let pk = install_signed(false); // enforced = false
210        trust::pin("acme", &pk).unwrap();
211        assert!(
212            active_resolved().is_none(),
213            "advisory policy must not be enforced"
214        );
215        let s = status();
216        assert!(s.present && s.signature_valid && s.trusted);
217        assert!(!s.enforced);
218        assert!(!s.applied, "advisory ⇒ not applied");
219    }
220
221    #[test]
222    fn tampered_artifact_is_not_applied() {
223        let _iso = isolated_data_dir();
224        let pk = install_signed(true);
225        trust::pin("acme", &pk).unwrap();
226        // Tamper with the installed file on disk.
227        let path = store::installed_path().unwrap();
228        let text = std::fs::read_to_string(&path).unwrap();
229        std::fs::write(&path, text.replace("ctx_url_read", "ctx_read")).unwrap();
230        assert!(active_resolved().is_none(), "tampered body must not apply");
231        let s = status();
232        assert!(!s.signature_valid);
233        assert!(!s.applied);
234    }
235}