Skip to main content

a1/
governance.rs

1use crate::error::A1Error;
2use crate::identity::Signer;
3use crate::registry::fresh_nonce;
4use blake3::Hasher;
5use serde::{Deserialize, Serialize};
6
7const DOMAIN_GOV_POLICY: &str = "a1::dyolo::governance::policy::v2.8.0";
8const DOMAIN_GOV_APPROVAL: &str = "a1::dyolo::governance::approval::v2.8.0";
9const DOMAIN_GOV_AUDIT: &str = "a1::dyolo::governance::audit::v2.8.0";
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ApprovalGate {
13    pub capability: String,
14    pub approver_did: String,
15    pub approval_ttl_secs: u64,
16    #[serde(default)]
17    pub allow_retroactive: bool,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ApprovalToken {
22    pub capability: String,
23    pub approver_did: String,
24    pub agent_did: String,
25    pub action_id: String,
26    pub granted_at_unix: u64,
27    pub expires_at_unix: u64,
28    pub nonce: String,
29    pub signature: String,
30}
31
32impl ApprovalToken {
33    pub fn issue(
34        approver: &dyn Signer,
35        capability: impl Into<String>,
36        agent_did: impl Into<String>,
37        action_id: impl Into<String>,
38        granted_at: u64,
39        ttl_secs: u64,
40    ) -> Self {
41        let vk = approver.verifying_key();
42        let approver_did = format!("did:a1:{}", hex::encode(vk.as_bytes()));
43        let capability = capability.into();
44        let agent_did = agent_did.into();
45        let action_id = action_id.into();
46        let nonce = fresh_nonce();
47        let expires = granted_at + ttl_secs;
48        let msg = approval_signable(
49            &capability,
50            &approver_did,
51            &agent_did,
52            &action_id,
53            granted_at,
54            expires,
55            &nonce,
56        );
57        let sig = approver.sign_message(&msg);
58        Self {
59            capability,
60            approver_did,
61            agent_did,
62            action_id,
63            granted_at_unix: granted_at,
64            expires_at_unix: expires,
65            nonce: hex::encode(nonce),
66            signature: hex::encode(sig.to_bytes()),
67        }
68    }
69
70    pub fn verify(&self, now_unix: u64) -> Result<(), A1Error> {
71        if now_unix > self.expires_at_unix {
72            return Err(A1Error::Expired(0, self.expires_at_unix, now_unix));
73        }
74        let pk_hex = self
75            .approver_did
76            .strip_prefix("did:a1:")
77            .ok_or_else(|| A1Error::WireFormatError("invalid approver DID".into()))?;
78        let pk_bytes =
79            hex::decode(pk_hex).map_err(|_| A1Error::WireFormatError("invalid DID hex".into()))?;
80        let pk_arr: [u8; 32] = pk_bytes
81            .try_into()
82            .map_err(|_| A1Error::WireFormatError("key must be 32 bytes".into()))?;
83        let vk = ed25519_dalek::VerifyingKey::from_bytes(&pk_arr)
84            .map_err(|_| A1Error::WireFormatError("invalid Ed25519 key".into()))?;
85        let nonce_bytes = hex::decode(&self.nonce)
86            .map_err(|_| A1Error::WireFormatError("invalid nonce hex".into()))?;
87        let nonce: [u8; 16] = nonce_bytes
88            .try_into()
89            .map_err(|_| A1Error::WireFormatError("nonce must be 16 bytes".into()))?;
90        let msg = approval_signable(
91            &self.capability,
92            &self.approver_did,
93            &self.agent_did,
94            &self.action_id,
95            self.granted_at_unix,
96            self.expires_at_unix,
97            &nonce,
98        );
99        let sig_bytes = hex::decode(&self.signature)
100            .map_err(|_| A1Error::WireFormatError("invalid sig hex".into()))?;
101        let sig_arr: [u8; 64] = sig_bytes
102            .try_into()
103            .map_err(|_| A1Error::WireFormatError("sig must be 64 bytes".into()))?;
104        let sig = ed25519_dalek::Signature::from_bytes(&sig_arr);
105        use ed25519_dalek::Verifier;
106        vk.verify(&msg, &sig)
107            .map_err(|_| A1Error::HybridSignatureInvalid {
108                component: "approval-token",
109            })
110    }
111}
112
113fn approval_signable(
114    capability: &str,
115    approver_did: &str,
116    agent_did: &str,
117    action_id: &str,
118    granted_at: u64,
119    expires_at: u64,
120    nonce: &[u8; 16],
121) -> Vec<u8> {
122    let mut h = Hasher::new_derive_key(DOMAIN_GOV_APPROVAL);
123    for s in [capability, approver_did, agent_did, action_id] {
124        h.update(&(s.len() as u64).to_le_bytes());
125        h.update(s.as_bytes());
126    }
127    h.update(&granted_at.to_le_bytes());
128    h.update(&expires_at.to_le_bytes());
129    h.update(nonce);
130    h.finalize().as_bytes().to_vec()
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct KeyRotationPolicy {
135    pub max_age_days: u32,
136    pub mandatory: bool,
137    pub rotation_authority_did: Option<String>,
138}
139
140impl Default for KeyRotationPolicy {
141    fn default() -> Self {
142        Self {
143            max_age_days: 90,
144            mandatory: true,
145            rotation_authority_did: None,
146        }
147    }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub enum RotationStatus {
152    Valid { days_remaining: u64 },
153    Recommended { age_days: u64 },
154    Required { age_days: u64 },
155}
156
157impl KeyRotationPolicy {
158    pub fn check(&self, issued_at_unix: u64, now_unix: u64) -> RotationStatus {
159        let age_days = now_unix.saturating_sub(issued_at_unix) / 86400;
160        if age_days >= self.max_age_days as u64 {
161            if self.mandatory {
162                RotationStatus::Required { age_days }
163            } else {
164                RotationStatus::Recommended { age_days }
165            }
166        } else {
167            RotationStatus::Valid {
168                days_remaining: self.max_age_days as u64 - age_days,
169            }
170        }
171    }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct GovernancePolicy {
176    #[serde(default)]
177    pub approval_gates: Vec<ApprovalGate>,
178    #[serde(default)]
179    pub key_rotation: KeyRotationPolicy,
180    #[serde(default = "default_max_depth")]
181    pub max_chain_depth: u8,
182    #[serde(default)]
183    pub allowed_namespaces: Vec<String>,
184    #[serde(default)]
185    pub blocked_capabilities: Vec<String>,
186    #[serde(default)]
187    pub require_human_approval_for: Vec<String>,
188    #[serde(default = "default_true")]
189    pub audit_all_authorizations: bool,
190}
191
192fn default_max_depth() -> u8 {
193    16
194}
195fn default_true() -> bool {
196    true
197}
198
199impl Default for GovernancePolicy {
200    fn default() -> Self {
201        Self {
202            approval_gates: vec![],
203            key_rotation: KeyRotationPolicy::default(),
204            max_chain_depth: 16,
205            allowed_namespaces: vec![],
206            blocked_capabilities: vec![],
207            require_human_approval_for: vec![],
208            audit_all_authorizations: true,
209        }
210    }
211}
212
213impl GovernancePolicy {
214    pub fn from_json(json: &str) -> Result<Self, A1Error> {
215        serde_json::from_str(json).map_err(|e| A1Error::WireFormatError(e.to_string()))
216    }
217    pub fn from_env() -> Result<Option<Self>, A1Error> {
218        match std::env::var("A1_GOVERNANCE_POLICY_FILE") {
219            Ok(path) => {
220                let json = std::fs::read_to_string(&path)
221                    .map_err(|e| A1Error::WireFormatError(format!("cannot read {path}: {e}")))?;
222                Ok(Some(Self::from_json(&json)?))
223            }
224            Err(_) => Ok(None),
225        }
226    }
227    pub fn check_namespace(&self, ns: &str) -> Result<(), A1Error> {
228        if self.allowed_namespaces.is_empty() || self.allowed_namespaces.iter().any(|n| n == ns) {
229            Ok(())
230        } else {
231            Err(A1Error::PolicyViolation(format!(
232                "namespace '{ns}' not in governance allowlist"
233            )))
234        }
235    }
236    pub fn check_capability_not_blocked(&self, cap: &str) -> Result<(), A1Error> {
237        if self.blocked_capabilities.iter().any(|c| c == cap) {
238            Err(A1Error::PolicyViolation(format!(
239                "capability '{cap}' is blocked by governance policy"
240            )))
241        } else {
242            Ok(())
243        }
244    }
245    pub fn check_chain_depth(&self, depth: usize) -> Result<(), A1Error> {
246        if depth > self.max_chain_depth as usize {
247            Err(A1Error::MaxDepthExceeded(depth, self.max_chain_depth))
248        } else {
249            Ok(())
250        }
251    }
252    pub fn requires_human_approval(&self, cap: &str) -> bool {
253        self.require_human_approval_for.iter().any(|c| c == cap)
254    }
255    pub fn commitment(&self) -> Result<[u8; 32], A1Error> {
256        let json =
257            serde_json::to_string(self).map_err(|e| A1Error::WireFormatError(e.to_string()))?;
258        let mut h = Hasher::new_derive_key(DOMAIN_GOV_POLICY);
259        h.update(json.as_bytes());
260        Ok(h.finalize().into())
261    }
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct AuditReport {
266    pub title: String,
267    pub scope: String,
268    pub period_start_unix: u64,
269    pub period_end_unix: u64,
270    pub total_authorizations: u64,
271    pub denied_authorizations: u64,
272    pub revocations_issued: u64,
273    pub passports_due_rotation: u64,
274    pub policy_commitment_hex: String,
275    pub generated_at: String,
276    pub compliance_standards: Vec<String>,
277    pub findings: Vec<AuditFinding>,
278    pub report_hash: String,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct AuditFinding {
283    pub severity: FindingSeverity,
284    pub code: String,
285    pub description: String,
286    pub count: u64,
287    pub recommendation: String,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
291#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
292pub enum FindingSeverity {
293    Info,
294    Warning,
295    Critical,
296}
297
298impl AuditReport {
299    pub fn new(
300        scope: impl Into<String>,
301        period_start: u64,
302        period_end: u64,
303        policy: &GovernancePolicy,
304    ) -> Result<Self, A1Error> {
305        let scope = scope.into();
306        let policy_hex = hex::encode(policy.commitment()?);
307        Ok(Self {
308            title: format!("A1 Compliance Audit — {scope}"),
309            scope,
310            period_start_unix: period_start,
311            period_end_unix: period_end,
312            total_authorizations: 0,
313            denied_authorizations: 0,
314            revocations_issued: 0,
315            passports_due_rotation: 0,
316            policy_commitment_hex: policy_hex,
317            generated_at: unix_to_iso(period_end),
318            compliance_standards: vec![
319                "EU AI Act (Art. 13, 14, 17)".into(),
320                "NIST AI RMF Govern 1.7, 6.2".into(),
321                "SOC 2 Type II (CC6.1, CC7.2)".into(),
322                "ISO/IEC 27001:2022 A.9".into(),
323            ],
324            findings: vec![],
325            report_hash: String::new(),
326        })
327    }
328    pub fn add_finding(&mut self, f: AuditFinding) {
329        self.findings.push(f);
330    }
331    pub fn finalize(&mut self) -> Result<(), A1Error> {
332        let json =
333            serde_json::to_string(self).map_err(|e| A1Error::WireFormatError(e.to_string()))?;
334        let mut h = Hasher::new_derive_key(DOMAIN_GOV_AUDIT);
335        h.update(json.as_bytes());
336        self.report_hash = hex::encode(h.finalize().as_bytes());
337        Ok(())
338    }
339}
340
341fn unix_to_iso(unix: u64) -> String {
342    let (s, m, h) = (unix % 60, (unix / 60) % 60, (unix / 3600) % 24);
343    let mut d = unix / 86400;
344    let mut y = 1970u64;
345    loop {
346        let yl = if (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400) {
347            366
348        } else {
349            365
350        };
351        if d < yl {
352            break;
353        }
354        d -= yl;
355        y += 1;
356    }
357    let ml: [u64; 12] = if (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400)
358    {
359        [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
360    } else {
361        [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
362    };
363    let mut mo = 1u64;
364    for mlen in ml {
365        if d < mlen {
366            break;
367        }
368        d -= mlen;
369        mo += 1;
370    }
371    format!("{y:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
372}