1use std::sync::Arc;
18
19use chio_core_types::capability::governance::{GovernedApprovalToken, ThresholdApprovalProposal};
20use chio_core_types::capability::threshold_approval::ThresholdApprovalRequirement;
21use chio_core_types::crypto::PublicKey;
22use chio_kernel::{
23 resume_with_decision, ApprovalDecision, ApprovalFilter, ApprovalOutcome, ApprovalRequest,
24 ApprovalStore, ApprovalStoreError, ApprovalToken, CollectedThresholdApprovalSet, KernelError,
25 ResolvedApproval, ThresholdApprovalCollector, ThresholdApprovalCollectorProposal,
26 ThresholdApprovalCollectorStoreError,
27};
28use serde::{Deserialize, Serialize};
29
30#[derive(Debug, Clone)]
34pub enum ApprovalHandlerError {
35 BadRequest(String),
37 NotFound(String),
39 Conflict(String),
41 ReplayDetected(String),
43 Rejected(String),
45 Internal(String),
47}
48
49impl ApprovalHandlerError {
50 #[must_use]
51 pub fn status(&self) -> u16 {
52 match self {
53 Self::BadRequest(_) => 400,
54 Self::NotFound(_) => 404,
55 Self::Conflict(_) => 409,
56 Self::ReplayDetected(_) => 409,
57 Self::Rejected(_) => 403,
58 Self::Internal(_) => 500,
59 }
60 }
61
62 #[must_use]
63 pub fn code(&self) -> &'static str {
64 match self {
65 Self::BadRequest(_) => "bad_request",
66 Self::NotFound(_) => "not_found",
67 Self::Conflict(_) => "conflict",
68 Self::ReplayDetected(_) => "replay_detected",
69 Self::Rejected(_) => "approval_rejected",
70 Self::Internal(_) => "internal_error",
71 }
72 }
73
74 #[must_use]
75 pub fn message(&self) -> String {
76 match self {
77 Self::BadRequest(m)
78 | Self::NotFound(m)
79 | Self::Conflict(m)
80 | Self::ReplayDetected(m)
81 | Self::Rejected(m)
82 | Self::Internal(m) => m.clone(),
83 }
84 }
85
86 #[must_use]
87 pub fn body(&self) -> serde_json::Value {
88 serde_json::json!({
89 "error": self.code(),
90 "message": self.message(),
91 })
92 }
93}
94
95impl std::fmt::Display for ApprovalHandlerError {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 write!(f, "{}: {}", self.code(), self.message())
98 }
99}
100
101impl std::error::Error for ApprovalHandlerError {}
102
103impl From<ApprovalStoreError> for ApprovalHandlerError {
104 fn from(e: ApprovalStoreError) -> Self {
105 match e {
106 ApprovalStoreError::NotFound(m) => Self::NotFound(m),
107 ApprovalStoreError::AlreadyResolved(m) => {
108 Self::Conflict(format!("already resolved: {m}"))
109 }
110 ApprovalStoreError::Replay(m) => Self::ReplayDetected(m),
111 ApprovalStoreError::Backend(m) => Self::Internal(m),
112 ApprovalStoreError::Serialization(m) => Self::Internal(m),
113 }
114 }
115}
116
117impl From<KernelError> for ApprovalHandlerError {
118 fn from(e: KernelError) -> Self {
119 match e {
120 KernelError::ApprovalRejected(m) => {
121 if m.contains("replay") {
122 Self::ReplayDetected(m)
123 } else {
124 Self::Rejected(m)
125 }
126 }
127 other => Self::Internal(other.to_string()),
128 }
129 }
130}
131
132impl From<ThresholdApprovalCollectorStoreError> for ApprovalHandlerError {
133 fn from(error: ThresholdApprovalCollectorStoreError) -> Self {
134 match error {
135 ThresholdApprovalCollectorStoreError::NotFound(message) => Self::NotFound(message),
136 ThresholdApprovalCollectorStoreError::Conflict(message) => Self::Conflict(message),
137 ThresholdApprovalCollectorStoreError::Backend(message)
138 | ThresholdApprovalCollectorStoreError::Serialization(message) => {
139 Self::Internal(message)
140 }
141 }
142 }
143}
144
145#[derive(Clone)]
147pub struct ApprovalAdmin {
148 store: Arc<dyn ApprovalStore>,
149 threshold_collector: Option<ThresholdApprovalCollector>,
150}
151
152impl std::fmt::Debug for ApprovalAdmin {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 f.debug_struct("ApprovalAdmin").finish_non_exhaustive()
155 }
156}
157
158impl ApprovalAdmin {
159 #[must_use]
160 pub fn new(store: Arc<dyn ApprovalStore>) -> Self {
161 Self {
162 store,
163 threshold_collector: None,
164 }
165 }
166
167 #[must_use]
168 pub fn with_threshold_collector(
169 store: Arc<dyn ApprovalStore>,
170 threshold_collector: ThresholdApprovalCollector,
171 ) -> Self {
172 Self {
173 store,
174 threshold_collector: Some(threshold_collector),
175 }
176 }
177
178 #[must_use]
179 pub fn store(&self) -> &Arc<dyn ApprovalStore> {
180 &self.store
181 }
182
183 fn threshold_collector(&self) -> Result<&ThresholdApprovalCollector, ApprovalHandlerError> {
184 self.threshold_collector.as_ref().ok_or_else(|| {
185 ApprovalHandlerError::Internal(
186 "threshold approval collector is not configured".to_string(),
187 )
188 })
189 }
190}
191
192#[derive(Debug, Clone, Default, Serialize, Deserialize)]
196pub struct PendingQuery {
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub subject_id: Option<String>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub tool_server: Option<String>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub tool_name: Option<String>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub not_expired_at: Option<u64>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub limit: Option<usize>,
207}
208
209impl From<PendingQuery> for ApprovalFilter {
210 fn from(q: PendingQuery) -> Self {
211 Self {
212 subject_id: q.subject_id,
213 tool_server: q.tool_server,
214 tool_name: q.tool_name,
215 not_expired_at: q.not_expired_at,
216 limit: q.limit,
217 }
218 }
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct PendingListResponse {
223 pub approvals: Vec<ApprovalRequest>,
224 pub count: usize,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct RespondRequest {
230 pub outcome: ApprovalOutcome,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub reason: Option<String>,
233 pub approver: PublicKey,
234 pub token: GovernedApprovalToken,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct RespondResponse {
239 pub approval_id: String,
240 pub outcome: ApprovalOutcome,
241 pub resolved_at: u64,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct CreateThresholdProposalRequest {
246 pub proposal: ThresholdApprovalProposal,
247 pub requirement: ThresholdApprovalRequirement,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub submitter: Option<PublicKey>,
250 #[serde(default)]
251 pub require_submitter_separation: bool,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct SubmitThresholdApprovalRequest {
256 pub token: GovernedApprovalToken,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct BatchRespondRequest {
262 pub decisions: Vec<BatchDecisionEntry>,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct BatchDecisionEntry {
267 pub approval_id: String,
268 pub outcome: ApprovalOutcome,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub reason: Option<String>,
271 pub approver: PublicKey,
272 pub token: GovernedApprovalToken,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct BatchRespondResponse {
277 pub results: Vec<BatchRespondResult>,
278 pub summary: BatchRespondSummary,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct BatchRespondResult {
283 pub approval_id: String,
284 pub status: String,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub outcome: Option<ApprovalOutcome>,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub error: Option<String>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct BatchRespondSummary {
293 pub total: usize,
294 pub approved: usize,
295 pub denied: usize,
296 pub rejected: usize,
297}
298
299pub fn handle_list_pending(
304 admin: &ApprovalAdmin,
305 query: PendingQuery,
306) -> Result<PendingListResponse, ApprovalHandlerError> {
307 let filter: ApprovalFilter = query.into();
308 let approvals = admin.store.list_pending(&filter)?;
309 let count = approvals.len();
310 Ok(PendingListResponse { approvals, count })
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct GetApprovalResponse {
321 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub pending: Option<ApprovalRequest>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub resolution: Option<ResolvedApproval>,
325}
326
327pub fn handle_get_approval(
328 admin: &ApprovalAdmin,
329 approval_id: &str,
330) -> Result<GetApprovalResponse, ApprovalHandlerError> {
331 let pending = admin.store.get_pending(approval_id)?;
332 let resolution = admin.store.get_resolution(approval_id)?;
333 if pending.is_none() && resolution.is_none() {
334 return Err(ApprovalHandlerError::NotFound(approval_id.to_string()));
335 }
336 Ok(GetApprovalResponse {
337 pending,
338 resolution,
339 })
340}
341
342pub fn handle_respond(
344 admin: &ApprovalAdmin,
345 approval_id: &str,
346 body: RespondRequest,
347 now: u64,
348) -> Result<RespondResponse, ApprovalHandlerError> {
349 if body.token.request_id != approval_id {
353 return Err(ApprovalHandlerError::BadRequest(format!(
354 "approval_id {approval_id} does not match signed token request_id {}",
355 body.token.request_id
356 )));
357 }
358
359 let decision = ApprovalDecision {
360 approval_id: approval_id.to_string(),
361 outcome: body.outcome.clone(),
362 reason: body.reason,
363 approver: body.approver.clone(),
364 token: body.token,
365 received_at: now,
366 };
367
368 let outcome = resume_with_decision(admin.store.as_ref(), &decision, now)?;
369
370 let approval_token = ApprovalToken::from_decision(&decision);
374 let _ = approval_token; Ok(RespondResponse {
377 approval_id: approval_id.to_string(),
378 outcome,
379 resolved_at: now,
380 })
381}
382
383pub fn handle_batch_respond(
386 admin: &ApprovalAdmin,
387 body: BatchRespondRequest,
388 now: u64,
389) -> Result<BatchRespondResponse, ApprovalHandlerError> {
390 if body.decisions.is_empty() {
391 return Err(ApprovalHandlerError::BadRequest(
392 "batch respond requires at least one decision".into(),
393 ));
394 }
395
396 let mut results = Vec::with_capacity(body.decisions.len());
397 let mut approved = 0usize;
398 let mut denied = 0usize;
399 let mut rejected = 0usize;
400
401 for entry in body.decisions {
402 let approval_id = entry.approval_id.clone();
403 if entry.token.request_id != approval_id {
404 rejected += 1;
405 results.push(BatchRespondResult {
406 approval_id,
407 status: "rejected".into(),
408 outcome: None,
409 error: Some(format!(
410 "token request_id {} mismatches approval_id",
411 entry.token.request_id
412 )),
413 });
414 continue;
415 }
416
417 let decision = ApprovalDecision {
418 approval_id: approval_id.clone(),
419 outcome: entry.outcome.clone(),
420 reason: entry.reason,
421 approver: entry.approver,
422 token: entry.token,
423 received_at: now,
424 };
425
426 match resume_with_decision(admin.store.as_ref(), &decision, now) {
427 Ok(outcome) => {
428 match outcome {
429 ApprovalOutcome::Approved => approved += 1,
430 ApprovalOutcome::Denied => denied += 1,
431 }
432 results.push(BatchRespondResult {
433 approval_id,
434 status: "resolved".into(),
435 outcome: Some(outcome),
436 error: None,
437 });
438 }
439 Err(e) => {
440 rejected += 1;
441 let handler_err: ApprovalHandlerError = e.into();
442 results.push(BatchRespondResult {
443 approval_id,
444 status: "rejected".into(),
445 outcome: None,
446 error: Some(handler_err.message()),
447 });
448 }
449 }
450 }
451
452 let total = results.len();
453 Ok(BatchRespondResponse {
454 results,
455 summary: BatchRespondSummary {
456 total,
457 approved,
458 denied,
459 rejected,
460 },
461 })
462}
463
464pub fn handle_create_threshold_proposal(
465 admin: &ApprovalAdmin,
466 body: CreateThresholdProposalRequest,
467 now: u64,
468) -> Result<ThresholdApprovalCollectorProposal, ApprovalHandlerError> {
469 admin
470 .threshold_collector()?
471 .create_proposal(
472 body.proposal,
473 body.requirement,
474 body.submitter,
475 body.require_submitter_separation,
476 now,
477 )
478 .map_err(Into::into)
479}
480
481pub fn handle_get_threshold_proposal(
482 admin: &ApprovalAdmin,
483 proposal_id: &str,
484) -> Result<ThresholdApprovalCollectorProposal, ApprovalHandlerError> {
485 admin
486 .threshold_collector()?
487 .get_proposal(proposal_id)?
488 .ok_or_else(|| ApprovalHandlerError::NotFound(proposal_id.to_string()))
489}
490
491pub fn handle_submit_threshold_approval(
492 admin: &ApprovalAdmin,
493 proposal_id: &str,
494 body: SubmitThresholdApprovalRequest,
495 now: u64,
496) -> Result<ThresholdApprovalCollectorProposal, ApprovalHandlerError> {
497 admin
498 .threshold_collector()?
499 .submit_token(proposal_id, body.token, now)
500 .map_err(Into::into)
501}
502
503pub fn handle_deliver_threshold_approval(
504 admin: &ApprovalAdmin,
505 proposal_id: &str,
506 now: u64,
507) -> Result<CollectedThresholdApprovalSet, ApprovalHandlerError> {
508 admin
509 .threshold_collector()?
510 .deliver(proposal_id, now)
511 .map_err(Into::into)
512}