Skip to main content

qualia_client_core/wellfair/api/
accountability.rs

1//! Accountability fabric: ledger + consent credentials
2
3use sha2::{Digest, Sha256};
4
5use super::*;
6
7impl WebizenHostApi {
8    // --- Accountability fabric (ADR 0011) — tamper-evident ledger + revocable consent credentials ---
9    //
10    // Turns the tested domain models (`crate::accountability_ledger`, `crate::consent_credential`) into a
11    // usable loop: grant a worker scoped access, record how/why they acted (attributable, court-auditable),
12    // let the person revoke (crypto-enforced — the key is destroyed, access ends), and keep the conduct trail
13    // un-erasable. All acts are written into a signed, hash-chained ledger the person's own key signs; a
14    // betrayer cannot quietly drop the inconvenient act without `verify()` naming it. Anti-deletion durability
15    // across parties (commons replication) and real envelope encryption of the wrapped key are the deferred
16    // composition steps (coordinate) — the wrapped key is carried as opaque bytes here, as the model intends.
17
18    pub(crate) fn accountability_store(
19        &self,
20    ) -> Result<crate::accountability_store::AccountabilityStore, String> {
21        crate::accountability_store::AccountabilityStore::open(&self.storage_root)
22            .map_err(|e| e.to_string())
23    }
24
25    /// Append a raw record to the person's tamper-evident accountability ledger, signed by the owner key.
26    pub fn ledger_append(
27        &self,
28        kind: &str,
29        payload_json: &str,
30    ) -> Result<crate::accountability_ledger::LedgerEntry, String> {
31        self.accountability_store()?
32            .append_ledger(kind, payload_json, &self.signing_key, Self::now_unix())
33            .map_err(|e| e.to_string())
34    }
35
36    /// Verify the whole ledger chain. `Ok(None)` = intact; `Ok(Some(tamper))` = a detected, named tamper.
37    pub fn ledger_verify(
38        &self,
39    ) -> Result<Option<crate::accountability_ledger::LedgerTamper>, String> {
40        let verdict = self
41            .accountability_store()?
42            .verify_ledger()
43            .map_err(|e| e.to_string())?;
44        Ok(verdict.err())
45    }
46
47    /// The most-recent ledger entries (newest first), capped to `limit`.
48    pub fn ledger_entries(
49        &self,
50        limit: usize,
51    ) -> Result<Vec<crate::accountability_ledger::LedgerEntry>, String> {
52        self.accountability_store()?
53            .ledger_entries(limit)
54            .map_err(|e| e.to_string())
55    }
56
57    /// **Grant a consent credential** to an agent (e.g. a social worker) over a committed payload. The
58    /// subject is the vault owner. `commitment_hex` is the 32-byte payload commitment; `wrapped_key_hex` is
59    /// the (opaque) wrapped data key that revocation destroys; `expiry_unix` optionally auto-expires access.
60    pub fn grant_consent_credential(
61        &self,
62        agent_did: &str,
63        scope: &str,
64        purpose: &str,
65        commitment_hex: &str,
66        wrapped_key_hex: &str,
67        expiry_unix: Option<u64>,
68    ) -> Result<crate::consent_credential::ConsentCredential, String> {
69        let commitment = crate::accountability_store::parse_commitment_hex(commitment_hex)?;
70        let wrapped_key =
71            hex::decode(wrapped_key_hex.trim()).map_err(|e| format!("wrapped key not hex: {e}"))?;
72        let now = Self::now_unix();
73        let id = {
74            let digest = Sha256::digest(format!("{agent_did}:{scope}:{now}").as_bytes());
75            format!("cc-{}", hex::encode(&digest[..6]))
76        };
77        let cred = crate::consent_credential::ConsentCredential::grant(
78            id,
79            &self.owner_did,
80            agent_did,
81            scope,
82            purpose,
83            commitment,
84            wrapped_key,
85            now,
86            expiry_unix,
87        );
88        self.accountability_store()?
89            .grant_credential(cred, &self.signing_key, now)
90            .map_err(|e| e.to_string())
91    }
92
93    /// **Revoke a consent credential** — crypto-enforced (the wrapped key is destroyed). Returns whether a
94    /// live credential was revoked. The conduct trail under it persists.
95    pub fn revoke_consent_credential(&self, credential_id: &str) -> Result<bool, String> {
96        self.accountability_store()?
97            .revoke_credential(credential_id, &self.signing_key, Self::now_unix())
98            .map_err(|e| e.to_string())
99    }
100
101    /// All stored consent credentials (active and revoked — revoked rows remain as the audit anchor).
102    pub fn list_consent_credentials(
103        &self,
104    ) -> Result<Vec<crate::consent_credential::ConsentCredential>, String> {
105        self.accountability_store()?
106            .list_credentials()
107            .map_err(|e| e.to_string())
108    }
109
110    /// **Record an agent's conduct** under a credential — signed (attributable + court-auditable) — into the
111    /// durable trail and the tamper-evident ledger. Binds to the payload commitment, not the payload.
112    pub fn record_conduct(
113        &self,
114        agent_did: &str,
115        credential_id: &str,
116        action: &str,
117        reason: &str,
118        commitment_hex: &str,
119    ) -> Result<crate::consent_credential::ConductRecord, String> {
120        let commitment = crate::accountability_store::parse_commitment_hex(commitment_hex)?;
121        self.accountability_store()?
122            .record_conduct(
123                agent_did,
124                credential_id,
125                action,
126                reason,
127                commitment,
128                &self.signing_key,
129                Self::now_unix(),
130            )
131            .map_err(|e| e.to_string())
132    }
133
134    /// The **audit view** — every conduct record taken under one credential (survives its revocation).
135    pub fn conduct_audit_trail(
136        &self,
137        credential_id: &str,
138    ) -> Result<Vec<crate::consent_credential::ConductRecord>, String> {
139        self.accountability_store()?
140            .audit_trail(credential_id)
141            .map_err(|e| e.to_string())
142    }
143
144    /// **Record guardian notifications** from a flagged ingest into the tamper-evident ledger — so a flagged
145    /// ingest under a guardianship relation is both a notification to the guardian AND an auditable,
146    /// un-erasable event (who was notified, about what, when). Composes the hypermedia flags → guardian layer
147    /// (`super::super::ingest_guardian`) with the accountability ledger.
148    pub fn record_guardian_notifications(
149        &self,
150        notifications: &[super::super::ingest_guardian::GuardianNotification],
151    ) -> Result<(), String> {
152        let store = self.accountability_store()?;
153        for n in notifications {
154            let payload = serde_json::to_string(n).map_err(|e| e.to_string())?;
155            store
156                .append_ledger(
157                    "guardian_notified",
158                    &payload,
159                    &self.signing_key,
160                    Self::now_unix(),
161                )
162                .map_err(|e| e.to_string())?;
163        }
164        Ok(())
165    }
166}