1use std::sync::Arc;
18
19use chio_core_types::capability::GovernedApprovalToken;
20use chio_core_types::crypto::PublicKey;
21use chio_kernel::{
22 resume_with_decision, ApprovalDecision, ApprovalFilter, ApprovalOutcome, ApprovalRequest,
23 ApprovalStore, ApprovalStoreError, ApprovalToken, KernelError, ResolvedApproval,
24};
25use serde::{Deserialize, Serialize};
26
27#[derive(Debug, Clone)]
31pub enum ApprovalHandlerError {
32 BadRequest(String),
34 NotFound(String),
36 Conflict(String),
38 ReplayDetected(String),
40 Rejected(String),
42 Internal(String),
44}
45
46impl ApprovalHandlerError {
47 #[must_use]
48 pub fn status(&self) -> u16 {
49 match self {
50 Self::BadRequest(_) => 400,
51 Self::NotFound(_) => 404,
52 Self::Conflict(_) => 409,
53 Self::ReplayDetected(_) => 409,
54 Self::Rejected(_) => 403,
55 Self::Internal(_) => 500,
56 }
57 }
58
59 #[must_use]
60 pub fn code(&self) -> &'static str {
61 match self {
62 Self::BadRequest(_) => "bad_request",
63 Self::NotFound(_) => "not_found",
64 Self::Conflict(_) => "conflict",
65 Self::ReplayDetected(_) => "replay_detected",
66 Self::Rejected(_) => "approval_rejected",
67 Self::Internal(_) => "internal_error",
68 }
69 }
70
71 #[must_use]
72 pub fn message(&self) -> String {
73 match self {
74 Self::BadRequest(m)
75 | Self::NotFound(m)
76 | Self::Conflict(m)
77 | Self::ReplayDetected(m)
78 | Self::Rejected(m)
79 | Self::Internal(m) => m.clone(),
80 }
81 }
82
83 #[must_use]
84 pub fn body(&self) -> serde_json::Value {
85 serde_json::json!({
86 "error": self.code(),
87 "message": self.message(),
88 })
89 }
90}
91
92impl std::fmt::Display for ApprovalHandlerError {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 write!(f, "{}: {}", self.code(), self.message())
95 }
96}
97
98impl std::error::Error for ApprovalHandlerError {}
99
100impl From<ApprovalStoreError> for ApprovalHandlerError {
101 fn from(e: ApprovalStoreError) -> Self {
102 match e {
103 ApprovalStoreError::NotFound(m) => Self::NotFound(m),
104 ApprovalStoreError::AlreadyResolved(m) => {
105 Self::Conflict(format!("already resolved: {m}"))
106 }
107 ApprovalStoreError::Replay(m) => Self::ReplayDetected(m),
108 ApprovalStoreError::Backend(m) => Self::Internal(m),
109 ApprovalStoreError::Serialization(m) => Self::Internal(m),
110 }
111 }
112}
113
114impl From<KernelError> for ApprovalHandlerError {
115 fn from(e: KernelError) -> Self {
116 match e {
117 KernelError::ApprovalRejected(m) => {
118 if m.contains("replay") {
119 Self::ReplayDetected(m)
120 } else {
121 Self::Rejected(m)
122 }
123 }
124 other => Self::Internal(other.to_string()),
125 }
126 }
127}
128
129#[derive(Clone)]
131pub struct ApprovalAdmin {
132 store: Arc<dyn ApprovalStore>,
133}
134
135impl std::fmt::Debug for ApprovalAdmin {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 f.debug_struct("ApprovalAdmin").finish_non_exhaustive()
138 }
139}
140
141impl ApprovalAdmin {
142 #[must_use]
143 pub fn new(store: Arc<dyn ApprovalStore>) -> Self {
144 Self { store }
145 }
146
147 #[must_use]
148 pub fn store(&self) -> &Arc<dyn ApprovalStore> {
149 &self.store
150 }
151}
152
153#[derive(Debug, Clone, Default, Serialize, Deserialize)]
157pub struct PendingQuery {
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub subject_id: Option<String>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub tool_server: Option<String>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub tool_name: Option<String>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub not_expired_at: Option<u64>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub limit: Option<usize>,
168}
169
170impl From<PendingQuery> for ApprovalFilter {
171 fn from(q: PendingQuery) -> Self {
172 Self {
173 subject_id: q.subject_id,
174 tool_server: q.tool_server,
175 tool_name: q.tool_name,
176 not_expired_at: q.not_expired_at,
177 limit: q.limit,
178 }
179 }
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct PendingListResponse {
184 pub approvals: Vec<ApprovalRequest>,
185 pub count: usize,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct RespondRequest {
191 pub outcome: ApprovalOutcome,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub reason: Option<String>,
194 pub approver: PublicKey,
195 pub token: GovernedApprovalToken,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct RespondResponse {
200 pub approval_id: String,
201 pub outcome: ApprovalOutcome,
202 pub resolved_at: u64,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct BatchRespondRequest {
208 pub decisions: Vec<BatchDecisionEntry>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct BatchDecisionEntry {
213 pub approval_id: String,
214 pub outcome: ApprovalOutcome,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub reason: Option<String>,
217 pub approver: PublicKey,
218 pub token: GovernedApprovalToken,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct BatchRespondResponse {
223 pub results: Vec<BatchRespondResult>,
224 pub summary: BatchRespondSummary,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct BatchRespondResult {
229 pub approval_id: String,
230 pub status: String,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub outcome: Option<ApprovalOutcome>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub error: Option<String>,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct BatchRespondSummary {
239 pub total: usize,
240 pub approved: usize,
241 pub denied: usize,
242 pub rejected: usize,
243}
244
245pub fn handle_list_pending(
250 admin: &ApprovalAdmin,
251 query: PendingQuery,
252) -> Result<PendingListResponse, ApprovalHandlerError> {
253 let filter: ApprovalFilter = query.into();
254 let approvals = admin.store.list_pending(&filter)?;
255 let count = approvals.len();
256 Ok(PendingListResponse { approvals, count })
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct GetApprovalResponse {
267 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub pending: Option<ApprovalRequest>,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub resolution: Option<ResolvedApproval>,
271}
272
273pub fn handle_get_approval(
274 admin: &ApprovalAdmin,
275 approval_id: &str,
276) -> Result<GetApprovalResponse, ApprovalHandlerError> {
277 let pending = admin.store.get_pending(approval_id)?;
278 let resolution = admin.store.get_resolution(approval_id)?;
279 if pending.is_none() && resolution.is_none() {
280 return Err(ApprovalHandlerError::NotFound(approval_id.to_string()));
281 }
282 Ok(GetApprovalResponse {
283 pending,
284 resolution,
285 })
286}
287
288pub fn handle_respond(
290 admin: &ApprovalAdmin,
291 approval_id: &str,
292 body: RespondRequest,
293 now: u64,
294) -> Result<RespondResponse, ApprovalHandlerError> {
295 if body.token.request_id != approval_id {
299 return Err(ApprovalHandlerError::BadRequest(format!(
300 "approval_id {approval_id} does not match signed token request_id {}",
301 body.token.request_id
302 )));
303 }
304
305 let decision = ApprovalDecision {
306 approval_id: approval_id.to_string(),
307 outcome: body.outcome.clone(),
308 reason: body.reason,
309 approver: body.approver.clone(),
310 token: body.token,
311 received_at: now,
312 };
313
314 let outcome = resume_with_decision(admin.store.as_ref(), &decision, now)?;
315
316 let approval_token = ApprovalToken::from_decision(&decision);
320 let _ = approval_token; Ok(RespondResponse {
323 approval_id: approval_id.to_string(),
324 outcome,
325 resolved_at: now,
326 })
327}
328
329pub fn handle_batch_respond(
332 admin: &ApprovalAdmin,
333 body: BatchRespondRequest,
334 now: u64,
335) -> Result<BatchRespondResponse, ApprovalHandlerError> {
336 if body.decisions.is_empty() {
337 return Err(ApprovalHandlerError::BadRequest(
338 "batch respond requires at least one decision".into(),
339 ));
340 }
341
342 let mut results = Vec::with_capacity(body.decisions.len());
343 let mut approved = 0usize;
344 let mut denied = 0usize;
345 let mut rejected = 0usize;
346
347 for entry in body.decisions {
348 let approval_id = entry.approval_id.clone();
349 if entry.token.request_id != approval_id {
350 rejected += 1;
351 results.push(BatchRespondResult {
352 approval_id,
353 status: "rejected".into(),
354 outcome: None,
355 error: Some(format!(
356 "token request_id {} mismatches approval_id",
357 entry.token.request_id
358 )),
359 });
360 continue;
361 }
362
363 let decision = ApprovalDecision {
364 approval_id: approval_id.clone(),
365 outcome: entry.outcome.clone(),
366 reason: entry.reason,
367 approver: entry.approver,
368 token: entry.token,
369 received_at: now,
370 };
371
372 match resume_with_decision(admin.store.as_ref(), &decision, now) {
373 Ok(outcome) => {
374 match outcome {
375 ApprovalOutcome::Approved => approved += 1,
376 ApprovalOutcome::Denied => denied += 1,
377 }
378 results.push(BatchRespondResult {
379 approval_id,
380 status: "resolved".into(),
381 outcome: Some(outcome),
382 error: None,
383 });
384 }
385 Err(e) => {
386 rejected += 1;
387 let handler_err: ApprovalHandlerError = e.into();
388 results.push(BatchRespondResult {
389 approval_id,
390 status: "rejected".into(),
391 outcome: None,
392 error: Some(handler_err.message()),
393 });
394 }
395 }
396 }
397
398 let total = results.len();
399 Ok(BatchRespondResponse {
400 results,
401 summary: BatchRespondSummary {
402 total,
403 approved,
404 denied,
405 rejected,
406 },
407 })
408}