qualia_client_core/wellfair/api/
guardianship.rs1use super::super::host_state::{GuardianshipProposalView, SubmitOutcome};
4use super::super::journal::JournalEntry;
5use super::super::live_share::{
6 append_live_share_journal, live_share_decision_journal_entry, live_share_request_journal_entry,
7 sanctuary_allows_classified_projection, validate_live_share_decision, LiveShareStore,
8};
9use super::super::policy::DecisionResult;
10use super::super::sanctuary::load_prefs as load_sanctuary_prefs;
11use wellfare_core::conditions::{build_condition_envelope, condition_summary};
12use wellfare_core::guardianship::{
13 build_proposal_envelope, build_vote_envelope, derive_status, parse_proposal_summary,
14 parse_vote_summary, proposal_summary, vote_summary, GuardianshipProposal, GuardianshipVote,
15 ProposalState,
16};
17use wellfare_core::live_share::{LiveSectionRequest, UsageAgreement};
18use wellfare_core::record::RecordEnvelope;
19
20use super::*;
21
22impl WebizenHostApi {
23 pub fn submit_proxy_record(
33 &mut self,
34 qapp_id: &str,
35 envelope: RecordEnvelope,
36 source: &str,
37 summary: Option<String>,
38 ) -> Result<SubmitOutcome, String> {
39 let outcome = self.submit_record_guarded(qapp_id, envelope, source, summary)?;
40 self.finalize_batch().ok();
41 Ok(outcome)
42 }
43
44 pub fn propose_proxy_condition(
48 &mut self,
49 proxy_did: &str,
50 report: &wellfare_core::conditions::ConditionReport,
51 ) -> Result<SubmitOutcome, String> {
52 let asserted = Self::now_unix() as u32;
53 let mut envelope =
54 build_condition_envelope(report, &self.owner_did, proxy_did, asserted, None);
55 envelope.proxy_did = Some(proxy_did.to_string());
56 let summary = condition_summary(report);
57 self.submit_proxy_record(QAPP_CLINICAL, envelope, SOURCE_CLINICAL, Some(summary))
58 }
59
60 pub(crate) fn escrow_proxy_write(
62 &mut self,
63 envelope: &RecordEnvelope,
64 summary: Option<String>,
65 threshold: u8,
66 ) -> Result<GuardianshipProposal, String> {
67 let proxy = envelope
68 .proxy_did
69 .clone()
70 .unwrap_or_else(|| self.author_did.clone());
71 let kind = wellfare_core::conditions::journal_kind_for_record_id(&envelope.id);
72 let reason = format!(
73 "Proxy write of a protected '{kind}' record on the principal's behalf requires guardian co-signature"
74 );
75 let proposal = GuardianshipProposal::new(
76 &envelope.owner_did,
77 proxy,
78 threshold,
79 envelope,
80 summary,
81 reason,
82 Self::now_unix() as u32,
83 );
84 let asserted = Self::now_unix() as u32;
85 let prop_env =
86 build_proposal_envelope(&proposal, &self.owner_did, &self.author_did, asserted);
87 self.submit_record_with_summary(
89 QAPP_GUARDIANSHIP,
90 prop_env,
91 SOURCE_GUARDIANSHIP,
92 Some(proposal_summary(&proposal)),
93 )?;
94 self.finalize_batch().ok();
95 Ok(proposal)
96 }
97
98 pub fn list_guardianship_proposals(
100 &self,
101 limit: usize,
102 ) -> Result<Vec<GuardianshipProposalView>, String> {
103 let rows = self.list_health_records(limit)?;
104 let mut proposals = Vec::new();
105 let mut votes = Vec::new();
106 for row in &rows {
107 let Some(ref summary) = row.summary else {
108 continue;
109 };
110 match row.kind.as_str() {
111 "guardianship_proposal" => {
112 if let Some(p) = parse_proposal_summary(summary) {
113 proposals.push(p);
114 }
115 }
116 "guardianship_vote" => {
117 if let Some(v) = parse_vote_summary(summary) {
118 votes.push(v);
119 }
120 }
121 _ => {}
122 }
123 }
124 let committed_ids: std::collections::HashSet<&str> =
125 rows.iter().map(|r| r.id.as_str()).collect();
126 let mut views: Vec<GuardianshipProposalView> = proposals
127 .iter()
128 .map(|p| {
129 let status = derive_status(p, &votes);
130 let committed = p
131 .escrowed_record_id()
132 .map(|id| committed_ids.contains(id.as_str()))
133 .unwrap_or(false);
134 GuardianshipProposalView::from_status(p, &status, committed)
135 })
136 .collect();
137 views.sort_by(|a, b| b.created_unix.cmp(&a.created_unix));
138 Ok(views)
139 }
140
141 pub fn vote_guardianship_proposal(
145 &mut self,
146 proposal_id: &str,
147 guardian_did: &str,
148 approve: bool,
149 reason: Option<String>,
150 ) -> Result<GuardianshipProposalView, String> {
151 let proposal = self
152 .find_proposal(proposal_id)?
153 .ok_or_else(|| format!("Unknown guardianship proposal: {proposal_id}"))?;
154
155 let vote = GuardianshipVote::new(
156 proposal_id,
157 guardian_did,
158 approve,
159 reason,
160 Self::now_unix() as u32,
161 );
162 let asserted = Self::now_unix() as u32;
163 let vote_env = build_vote_envelope(&vote, &self.owner_did, &self.author_did, asserted);
164 self.submit_record_with_summary(
165 QAPP_GUARDIANSHIP,
166 vote_env,
167 SOURCE_GUARDIANSHIP,
168 Some(vote_summary(&vote)),
169 )?;
170 self.finalize_batch().ok();
171
172 let votes = self.list_guardianship_votes(proposal_id)?;
173 let status = derive_status(&proposal, &votes);
174
175 let mut committed = self.escrowed_already_committed(&proposal)?;
176 if status.state == ProposalState::Ratified && !committed {
177 if let Some(escrowed) = proposal.escrowed_envelope() {
178 let decision = DecisionResult::Permit {
179 obligations: vec!["guardianship_ratified".into(), "emit_wal_receipt".into()],
180 };
181 self.commit_permitted(
183 QAPP_GUARDIANSHIP,
184 &escrowed,
185 SOURCE_GUARDIANSHIP,
186 proposal.escrowed_summary.clone(),
187 &decision,
188 )?;
189 self.finalize_batch().ok();
190 committed = true;
191 }
192 }
193
194 Ok(GuardianshipProposalView::from_status(
195 &proposal, &status, committed,
196 ))
197 }
198
199 pub(crate) fn find_proposal(
200 &self,
201 proposal_id: &str,
202 ) -> Result<Option<GuardianshipProposal>, String> {
203 let rows =
204 self.list_journal_by_kind("guardianship_proposal", super::super::journal::MAX_LIST)?;
205 Ok(rows
206 .into_iter()
207 .filter_map(|r| r.summary.as_deref().and_then(parse_proposal_summary))
208 .find(|p| p.id == proposal_id))
209 }
210
211 fn list_guardianship_votes(&self, proposal_id: &str) -> Result<Vec<GuardianshipVote>, String> {
212 let rows =
213 self.list_journal_by_kind("guardianship_vote", super::super::journal::MAX_LIST)?;
214 Ok(rows
215 .into_iter()
216 .filter_map(|r| r.summary.as_deref().and_then(parse_vote_summary))
217 .filter(|v| v.proposal_id == proposal_id)
218 .collect())
219 }
220
221 fn escrowed_already_committed(&self, proposal: &GuardianshipProposal) -> Result<bool, String> {
222 let Some(escrowed_id) = proposal.escrowed_record_id() else {
223 return Ok(false);
224 };
225 let kind = wellfare_core::conditions::journal_kind_for_record_id(&escrowed_id);
226 let rows = self.list_journal_by_kind(kind, super::super::journal::MAX_LIST)?;
227 Ok(rows.iter().any(|r| r.id == escrowed_id))
228 }
229
230 pub fn submit_live_share_request(
232 &self,
233 request: &LiveSectionRequest,
234 ) -> Result<JournalEntry, String> {
235 let now = Self::now_unix();
236 let store = LiveShareStore::open(&self.storage_root).map_err(|e| e.to_string())?;
237 let record = store
238 .enqueue_request(request.clone(), now)
239 .map_err(|e| e.to_string())?;
240 let committed_unix = now as u32;
241 let entry = live_share_request_journal_entry(&record, committed_unix);
242 append_live_share_journal(&self.storage_root, &entry)?;
243 Ok(entry)
244 }
245
246 pub fn decide_live_share_request(
248 &self,
249 request_id: &str,
250 approved: bool,
251 projection_kinds: &[String],
252 deny_reason: Option<&str>,
253 ) -> Result<JournalEntry, String> {
254 let now = Self::now_unix();
255 let store = LiveShareStore::open(&self.storage_root).map_err(|e| e.to_string())?;
256 let pending = store
257 .get_request(request_id)
258 .map_err(|e| e.to_string())?
259 .ok_or_else(|| format!("live share request '{request_id}' not found"))?;
260 if pending.status != super::super::live_share::LiveShareRequestStatus::Pending {
261 return Err(format!("live share request '{request_id}' already decided"));
262 }
263 let sanctuary_prefs = load_sanctuary_prefs(&self.storage_root);
264 let sanctuary_unlocked = sanctuary_allows_classified_projection(&sanctuary_prefs);
265 validate_live_share_decision(&pending, approved, projection_kinds, sanctuary_unlocked)?;
266 let deny = if approved {
267 None
268 } else {
269 Some(
270 deny_reason
271 .filter(|s| !s.is_empty())
272 .unwrap_or("owner denied live share request"),
273 )
274 };
275 let updated = store
276 .decide(request_id, approved, projection_kinds, now, deny.as_deref())
277 .map_err(|e| e.to_string())?;
278 let committed_unix = now as u32;
279 let entry = live_share_decision_journal_entry(&updated, committed_unix);
280 append_live_share_journal(&self.storage_root, &entry)?;
281 Ok(entry)
282 }
283
284 pub fn get_live_share_record(
285 &self,
286 request_id: &str,
287 ) -> Result<Option<super::super::live_share::LiveShareRequestRecord>, String> {
288 LiveShareStore::open(&self.storage_root)
289 .map_err(|e| e.to_string())?
290 .get_request(request_id)
291 .map_err(|e| e.to_string())
292 }
293
294 pub fn list_pending_live_shares(
295 &self,
296 limit: usize,
297 ) -> Result<Vec<LiveSectionRequest>, String> {
298 LiveShareStore::open(&self.storage_root)
299 .map_err(|e| e.to_string())?
300 .list_pending(limit)
301 .map_err(|e| e.to_string())
302 }
303
304 pub fn register_usage_agreement(&self, agreement: &UsageAgreement) -> Result<(), String> {
305 LiveShareStore::open(&self.storage_root)
306 .map_err(|e| e.to_string())?
307 .save_usage_agreement(agreement)
308 .map_err(|e| e.to_string())
309 }
310}