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}