qualia_client_core/wellfair/
policy.rs1use super::consent_store::ConsentGrantRecord;
2use super::host_state::{ConsentGrantDraft, PolicyDecisionDto};
3use wellfare_core::record::{EpistemicStatus, SensitivityClass};
4
5#[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#[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 health_writers: &'static [&'static str],
52 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 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 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 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}