Skip to main content

qualia_client_core/wellfair/
policy.rs

1use super::consent_store::ConsentGrantRecord;
2use super::host_state::{ConsentGrantDraft, PolicyDecisionDto};
3use wellfare_core::record::{EpistemicStatus, SensitivityClass};
4
5/// The outcome of a policy decision (maps to `PolicyDecisionDto` for UI).
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum DecisionResult {
8    Permit {
9        obligations: Vec<String>,
10    },
11    Deny {
12        reasons: Vec<String>,
13    },
14    Prompt {
15        requested_consent: ConsentGrantDraft,
16    },
17    Suspend {
18        required_approvals: u8,
19    },
20}
21
22/// The receipt bound to a policy decision for auditability.
23#[derive(Debug, Clone)]
24pub struct DecisionReceipt {
25    pub id: String,
26    pub timestamp_unix: u32,
27    pub result: DecisionResult,
28}
29
30impl DecisionResult {
31    pub fn to_dto(&self) -> PolicyDecisionDto {
32        match self {
33            Self::Permit { obligations } => PolicyDecisionDto::Permit {
34                obligations: obligations.clone(),
35            },
36            Self::Deny { reasons } => PolicyDecisionDto::Deny {
37                reasons: reasons.clone(),
38            },
39            Self::Prompt { requested_consent } => PolicyDecisionDto::Prompt {
40                requested_consent: requested_consent.clone(),
41            },
42            Self::Suspend { required_approvals } => PolicyDecisionDto::Suspend {
43                required_approvals: *required_approvals,
44            },
45        }
46    }
47}
48
49pub struct PolicyDecisionService {
50    /// qApp IDs permitted to write health observations without extra prompt.
51    health_writers: &'static [&'static str],
52    /// qApps permitted to write Classified sanctuary/wellbeing records (Phase 3).
53    classified_writers: &'static [&'static str],
54}
55
56impl PolicyDecisionService {
57    pub fn new() -> Self {
58        Self {
59            health_writers: &[
60                "wellfair-health",
61                "wellfair-medication",
62                "wellfair-shell",
63                "wellfair-life",
64                "wellfair-wellbeing",
65                "wellfair-finance",
66                "wellfair-projects",
67                "wellfair-credentials",
68                "wellfair-clinical",
69                "wellfair-welfare",
70                "qualia-cooperative",
71                "wellfair-guardianship",
72                "wellfair",
73            ],
74            classified_writers: &[
75                "wellfair-shell",
76                "wellfair-sanctuary",
77                "wellfair-wellbeing",
78                "wellfair-life",
79            ],
80        }
81    }
82
83    fn has_active_grant(
84        grants: &[ConsentGrantRecord],
85        qapp_id: &str,
86        scope: &str,
87        now_unix: u64,
88    ) -> bool {
89        grants
90            .iter()
91            .any(|g| g.is_active(now_unix) && g.recipient == qapp_id && g.scope == scope)
92    }
93
94    /// Evaluates if a qApp capability is permitted to act on a record with a given sensitivity.
95    ///
96    /// `is_proxy_action` marks a write made by an agent acting *on behalf of* the principal
97    /// (the envelope carries a `proxy_did` distinct from the owner). Supported-agency
98    /// accountability holds such a write in escrow for M-of-N guardian co-signature rather than
99    /// committing it silently — see [`super::guardianship`]. Non-proxy writes (the principal
100    /// acting for themselves) are unaffected.
101    pub fn evaluate_access(
102        &self,
103        qapp_id: &str,
104        requested_scope: &str,
105        sensitivity: SensitivityClass,
106        epistemic: EpistemicStatus,
107        active_grants: &[ConsentGrantRecord],
108        now_unix: u64,
109        is_proxy_action: bool,
110    ) -> DecisionResult {
111        if sensitivity == SensitivityClass::Classified {
112            if requested_scope == "write_record"
113                && self.classified_writers.iter().any(|id| *id == qapp_id)
114            {
115                return DecisionResult::Permit {
116                    obligations: vec![
117                        "emit_wal_receipt".into(),
118                        "sanctuary_projection_required".into(),
119                    ],
120                };
121            }
122            return DecisionResult::Deny {
123                reasons: vec!["Classified records require explicit guardian approval".into()],
124            };
125        }
126
127        if epistemic == EpistemicStatus::Refuted {
128            return DecisionResult::Deny {
129                reasons: vec!["Refuted claims cannot be written as active records".into()],
130            };
131        }
132
133        // Supported-agency escrow: a proxy writing a protected record on the principal's behalf
134        // does not auto-commit — it suspends pending M-of-N guardian co-signature. (Classified is
135        // handled above by the fail-closed writer allowlist; Public needs no escrow.)
136        if is_proxy_action
137            && requested_scope == "write_record"
138            && sensitivity == SensitivityClass::Restricted
139        {
140            return DecisionResult::Suspend {
141                required_approvals: 2,
142            };
143        }
144
145        match requested_scope {
146            "write_record" | "read_record" => {
147                if self.health_writers.iter().any(|id| *id == qapp_id) {
148                    return DecisionResult::Permit {
149                        obligations: vec!["emit_wal_receipt".into()],
150                    };
151                }
152                if Self::has_active_grant(active_grants, qapp_id, requested_scope, now_unix) {
153                    return DecisionResult::Permit {
154                        obligations: vec![
155                            "emit_wal_receipt".into(),
156                            "honour_consent_expiry".into(),
157                        ],
158                    };
159                }
160                let fields = if requested_scope == "write_record" {
161                    vec!["health.observation".into()]
162                } else {
163                    vec!["health.observation".into(), "profile.display_name".into()]
164                };
165                DecisionResult::Prompt {
166                    requested_consent: ConsentGrantDraft {
167                        recipient: qapp_id.to_string(),
168                        purpose: format!("{requested_scope} via WellFair host"),
169                        fields,
170                        expires_at_unix: None,
171                    },
172                }
173            }
174            _ => DecisionResult::Deny {
175                reasons: vec![format!("Unknown scope: {requested_scope}")],
176            },
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn classified_fails_closed() {
187        let svc = PolicyDecisionService::new();
188        let d = svc.evaluate_access(
189            "wellfair-health",
190            "write_record",
191            SensitivityClass::Classified,
192            EpistemicStatus::Asserted,
193            &[],
194            0,
195            false,
196        );
197        assert!(matches!(d, DecisionResult::Deny { .. }));
198    }
199
200    #[test]
201    fn health_writer_permitted() {
202        let svc = PolicyDecisionService::new();
203        let d = svc.evaluate_access(
204            "wellfair-health",
205            "write_record",
206            SensitivityClass::Restricted,
207            EpistemicStatus::Asserted,
208            &[],
209            0,
210            false,
211        );
212        assert!(matches!(d, DecisionResult::Permit { .. }));
213    }
214
215    #[test]
216    fn medication_writer_permitted() {
217        let svc = PolicyDecisionService::new();
218        let d = svc.evaluate_access(
219            "wellfair-medication",
220            "write_record",
221            SensitivityClass::Restricted,
222            EpistemicStatus::Asserted,
223            &[],
224            0,
225            false,
226        );
227        assert!(matches!(d, DecisionResult::Permit { .. }));
228    }
229
230    #[test]
231    fn active_grant_permits_third_party_qapp() {
232        use super::super::consent_store::ConsentGrantRecord;
233        let svc = PolicyDecisionService::new();
234        let grant = ConsentGrantRecord {
235            id: "g1".into(),
236            recipient: "wellfair-care".into(),
237            purpose: "care team write".into(),
238            fields: vec!["health.observation".into()],
239            scope: "write_record".into(),
240            granted_at_unix: 1,
241            expires_at_unix: None,
242            revoked: false,
243        };
244        let d = svc.evaluate_access(
245            "wellfair-care",
246            "write_record",
247            SensitivityClass::Restricted,
248            EpistemicStatus::Asserted,
249            &[grant],
250            100,
251            false,
252        );
253        assert!(matches!(d, DecisionResult::Permit { .. }));
254    }
255
256    #[test]
257    fn proxy_restricted_write_suspends_for_guardian_cosignature() {
258        let svc = PolicyDecisionService::new();
259        // Even a trusted health-writer qapp: a *proxy* write on protected data escrows.
260        let d = svc.evaluate_access(
261            "wellfair-health",
262            "write_record",
263            SensitivityClass::Restricted,
264            EpistemicStatus::Asserted,
265            &[],
266            0,
267            true,
268        );
269        assert!(matches!(
270            d,
271            DecisionResult::Suspend {
272                required_approvals: 2
273            }
274        ));
275    }
276
277    #[test]
278    fn proxy_public_write_is_not_escrowed() {
279        let svc = PolicyDecisionService::new();
280        let d = svc.evaluate_access(
281            "wellfair-health",
282            "write_record",
283            SensitivityClass::Public,
284            EpistemicStatus::Asserted,
285            &[],
286            0,
287            true,
288        );
289        assert!(matches!(d, DecisionResult::Permit { .. }));
290    }
291
292    #[test]
293    fn non_proxy_restricted_write_still_permits() {
294        let svc = PolicyDecisionService::new();
295        let d = svc.evaluate_access(
296            "wellfair-health",
297            "write_record",
298            SensitivityClass::Restricted,
299            EpistemicStatus::Asserted,
300            &[],
301            0,
302            false,
303        );
304        assert!(matches!(d, DecisionResult::Permit { .. }));
305    }
306}