1#![allow(clippy::too_many_arguments)]
13
14use std::sync::Arc;
15
16use wasm_bindgen::prelude::*;
17
18use agentstategraph::speculation::SpecHandle;
21use agentstategraph::{CommitOptions, Repository, SCHEMA_VERSION};
22use agentstategraph_core::SessionStatus;
23use agentstategraph_core::{IntentCategory, Object};
24use agentstategraph_migrate::{CheckResult, Registry, RunMode, StepStatus};
25use agentstategraph_policy::{ChangeProposal, Policy, PolicyStore as PolicyBackend, Situation};
26use agentstategraph_storage::IndexedDbStorage;
27use agentstategraph_taint::{
28 QuarantineParams, TaintEffect, TaintKind, TaintMetadata, TaintParams, TaintSeverity,
29 UntaintParams, UnwatchParams, WatchDirection, WatchParams,
30};
31use agentstategraph_tasks::{
32 AddTaskOptions, OnCompleteHook, Priority, Proof, ProofKind, TaskId, TaskStore,
33};
34use semver::Version;
35
36fn parse_category(s: &str) -> IntentCategory {
37 match s.to_lowercase().as_str() {
38 "explore" => IntentCategory::Explore,
39 "refine" => IntentCategory::Refine,
40 "fix" => IntentCategory::Fix,
41 "rollback" => IntentCategory::Rollback,
42 "checkpoint" => IntentCategory::Checkpoint,
43 "merge" => IntentCategory::Merge,
44 "migrate" => IntentCategory::Custom("Migrate-requested".into()),
50 "plan" => IntentCategory::Plan,
51 other => IntentCategory::Custom(other.to_string()),
52 }
53}
54
55fn make_opts(
56 description: &str,
57 category: &str,
58 reasoning: Option<String>,
59 confidence: Option<f64>,
60) -> CommitOptions {
61 let cat = parse_category(category);
62 let mut opts = CommitOptions::new("wasm", cat, description);
63 if let Some(r) = reasoning {
64 opts = opts.with_reasoning(r);
65 }
66 if let Some(c) = confidence {
67 opts = opts.with_confidence(c);
68 }
69 opts
70}
71
72#[wasm_bindgen]
80pub struct WasmAgentStateGraph {
81 repo: Arc<Repository>,
82 storage: std::sync::Arc<IndexedDbStorage>,
83}
84
85#[wasm_bindgen]
86impl WasmAgentStateGraph {
87 #[wasm_bindgen(constructor)]
90 pub fn new(db_name: Option<String>) -> Result<WasmAgentStateGraph, JsValue> {
91 let name = db_name.unwrap_or_else(|| "agentstategraph".to_string());
92 let storage = std::sync::Arc::new(IndexedDbStorage::new(&name));
96 let repo = Repository::from_shared(storage.clone());
97 repo.init()
98 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
99
100 Ok(Self {
101 repo: Arc::new(repo),
102 storage,
103 })
104 }
105
106 pub fn load_objects(&self, json_pairs: &str) -> Result<(), JsValue> {
109 let pairs: Vec<(String, String)> = serde_json::from_str(json_pairs)
110 .map_err(|e| JsValue::from_str(&format!("parse error: {}", e)))?;
111 self.storage
112 .load_objects(&pairs)
113 .map_err(|e| JsValue::from_str(&format!("{}", e)))
114 }
115
116 pub fn load_commits(&self, json_pairs: &str) -> Result<(), JsValue> {
118 let pairs: Vec<(String, String)> = serde_json::from_str(json_pairs)
119 .map_err(|e| JsValue::from_str(&format!("parse error: {}", e)))?;
120 self.storage
121 .load_commits(&pairs)
122 .map_err(|e| JsValue::from_str(&format!("{}", e)))
123 }
124
125 pub fn load_refs(&self, json_pairs: &str) -> Result<(), JsValue> {
127 let pairs: Vec<(String, String)> = serde_json::from_str(json_pairs)
128 .map_err(|e| JsValue::from_str(&format!("parse error: {}", e)))?;
129 self.storage
130 .load_refs(&pairs)
131 .map_err(|e| JsValue::from_str(&format!("{}", e)))
132 }
133
134 pub fn drain_pending_objects(&self) -> String {
136 let pending = self.storage.drain_pending_objects();
137 serde_json::to_string(&pending).unwrap_or_else(|_| "[]".to_string())
138 }
139
140 pub fn drain_pending_commits(&self) -> String {
142 let pending = self.storage.drain_pending_commits();
143 serde_json::to_string(&pending).unwrap_or_else(|_| "[]".to_string())
144 }
145
146 pub fn drain_pending_refs(&self) -> String {
148 let pending = self.storage.drain_pending_refs();
149 serde_json::to_string(&pending).unwrap_or_else(|_| "[]".to_string())
150 }
151
152 pub fn drain_deleted_refs(&self) -> String {
155 let pending = self.storage.drain_deleted_refs();
156 serde_json::to_string(&pending).unwrap_or_else(|_| "[]".to_string())
157 }
158
159 pub fn db_name(&self) -> String {
161 self.storage.db_name().to_string()
162 }
163
164 pub fn get(
166 &self,
167 path: &str,
168 reference: Option<String>,
169 namespace: Option<String>,
170 ) -> Result<String, JsValue> {
171 let ref_name = reference.unwrap_or_else(|| "main".to_string());
172 let forked;
173 let repo: &agentstategraph::Repository = if let Some(ns_str) = namespace {
174 let ns = agentstategraph_core::Namespace::new(&ns_str)
175 .map_err(|e| JsValue::from_str(&format!("Invalid namespace: {}", e)))?;
176 forked = self.repo.fork_namespace(ns);
177 &forked
178 } else {
179 &self.repo
180 };
181 let val = repo
182 .get_json(&ref_name, path)
183 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
184 Ok(serde_json::to_string(&val).unwrap_or_default())
185 }
186
187 pub fn set(
189 &self,
190 path: &str,
191 json_value: &str,
192 category: &str,
193 description: &str,
194 reference: Option<String>,
195 reasoning: Option<String>,
196 confidence: Option<f64>,
197 namespace: Option<String>,
198 ) -> Result<String, JsValue> {
199 let ref_name = reference.unwrap_or_else(|| "main".to_string());
200 let value: serde_json::Value = serde_json::from_str(json_value)
201 .map_err(|e| JsValue::from_str(&format!("JSON error: {}", e)))?;
202 let opts = make_opts(description, category, reasoning, confidence);
203 let forked;
204 let repo: &agentstategraph::Repository = if let Some(ns_str) = namespace {
205 let ns = agentstategraph_core::Namespace::new(&ns_str)
206 .map_err(|e| JsValue::from_str(&format!("Invalid namespace: {}", e)))?;
207 forked = self.repo.fork_namespace(ns);
208 &forked
209 } else {
210 &self.repo
211 };
212 let id = repo
213 .set_json(&ref_name, path, &value, opts)
214 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
215 Ok(id.to_string())
216 }
217
218 pub fn delete(
220 &self,
221 path: &str,
222 category: &str,
223 description: &str,
224 reference: Option<String>,
225 namespace: Option<String>,
226 ) -> Result<String, JsValue> {
227 let ref_name = reference.unwrap_or_else(|| "main".to_string());
228 let opts = make_opts(description, category, None, None);
229 let forked;
230 let repo: &agentstategraph::Repository = if let Some(ns_str) = namespace {
231 let ns = agentstategraph_core::Namespace::new(&ns_str)
232 .map_err(|e| JsValue::from_str(&format!("Invalid namespace: {}", e)))?;
233 forked = self.repo.fork_namespace(ns);
234 &forked
235 } else {
236 &self.repo
237 };
238 let id = repo
239 .delete(&ref_name, path, opts)
240 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
241 Ok(id.to_string())
242 }
243
244 pub fn branch(
246 &self,
247 name: &str,
248 from: Option<String>,
249 namespace: Option<String>,
250 ) -> Result<String, JsValue> {
251 let from_ref = from.unwrap_or_else(|| "main".to_string());
252 let forked;
253 let repo: &agentstategraph::Repository = if let Some(ns_str) = namespace {
254 let ns = agentstategraph_core::Namespace::new(&ns_str)
255 .map_err(|e| JsValue::from_str(&format!("Invalid namespace: {}", e)))?;
256 forked = self.repo.fork_namespace(ns);
257 &forked
258 } else {
259 &self.repo
260 };
261 let id = repo
262 .branch(name, &from_ref)
263 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
264 Ok(id.to_string())
265 }
266
267 pub fn merge(
269 &self,
270 source: &str,
271 target: Option<String>,
272 description: Option<String>,
273 namespace: Option<String>,
274 ) -> Result<String, JsValue> {
275 let target_ref = target.unwrap_or_else(|| "main".to_string());
276 let desc = description.unwrap_or_else(|| "merge".to_string());
277 let opts = make_opts(&desc, "Merge", None, None);
278 let forked;
279 let repo: &agentstategraph::Repository = if let Some(ns_str) = namespace {
280 let ns = agentstategraph_core::Namespace::new(&ns_str)
281 .map_err(|e| JsValue::from_str(&format!("Invalid namespace: {}", e)))?;
282 forked = self.repo.fork_namespace(ns);
283 &forked
284 } else {
285 &self.repo
286 };
287 let id = repo
288 .merge(source, &target_ref, opts)
289 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
290 Ok(id.to_string())
291 }
292
293 pub fn diff(
295 &self,
296 ref_a: &str,
297 ref_b: &str,
298 namespace: Option<String>,
299 ) -> Result<String, JsValue> {
300 let forked;
301 let repo: &agentstategraph::Repository = if let Some(ns_str) = namespace {
302 let ns = agentstategraph_core::Namespace::new(&ns_str)
303 .map_err(|e| JsValue::from_str(&format!("Invalid namespace: {}", e)))?;
304 forked = self.repo.fork_namespace(ns);
305 &forked
306 } else {
307 &self.repo
308 };
309 let ops = repo
310 .diff(ref_a, ref_b)
311 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
312 Ok(serde_json::to_string(&ops).unwrap_or_default())
313 }
314
315 pub fn log(
317 &self,
318 reference: Option<String>,
319 limit: Option<u32>,
320 namespace: Option<String>,
321 ) -> Result<String, JsValue> {
322 let ref_name = reference.unwrap_or_else(|| "main".to_string());
323 let max = limit.unwrap_or(10) as usize;
324 let forked;
325 let repo: &agentstategraph::Repository = if let Some(ns_str) = namespace {
326 let ns = agentstategraph_core::Namespace::new(&ns_str)
327 .map_err(|e| JsValue::from_str(&format!("Invalid namespace: {}", e)))?;
328 forked = self.repo.fork_namespace(ns);
329 &forked
330 } else {
331 &self.repo
332 };
333 let commits = repo
334 .log(&ref_name, max)
335 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
336 let entries: Vec<serde_json::Value> = commits
337 .iter()
338 .map(|c| {
339 serde_json::json!({
340 "id": c.id.short(),
341 "agent": c.agent_id,
342 "intent": {
343 "category": format!("{:?}", c.intent.category),
344 "description": c.intent.description,
345 "tags": c.intent.tags,
346 },
347 "reasoning": c.reasoning,
348 "confidence": c.confidence,
349 "timestamp": c.timestamp.to_rfc3339(),
350 })
351 })
352 .collect();
353 Ok(serde_json::to_string(&entries).unwrap_or_default())
354 }
355
356 pub fn blame(
358 &self,
359 path: &str,
360 reference: Option<String>,
361 namespace: Option<String>,
362 ) -> Result<String, JsValue> {
363 let ref_name = reference.unwrap_or_else(|| "main".to_string());
364 let forked;
365 let repo: &agentstategraph::Repository = if let Some(ns_str) = namespace {
366 let ns = agentstategraph_core::Namespace::new(&ns_str)
367 .map_err(|e| JsValue::from_str(&format!("Invalid namespace: {}", e)))?;
368 forked = self.repo.fork_namespace(ns);
369 &forked
370 } else {
371 &self.repo
372 };
373 let entry = repo
374 .blame(&ref_name, path)
375 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
376 Ok(serde_json::to_string(&entry).unwrap_or_default())
377 }
378
379 pub fn speculate(
381 &self,
382 from: Option<String>,
383 label: Option<String>,
384 namespace: Option<String>,
385 ) -> Result<u32, JsValue> {
386 let from_ref = from.unwrap_or_else(|| "main".to_string());
387 let forked;
388 let repo: &agentstategraph::Repository = if let Some(ns_str) = namespace {
389 let ns = agentstategraph_core::Namespace::new(&ns_str)
390 .map_err(|e| JsValue::from_str(&format!("Invalid namespace: {}", e)))?;
391 forked = self.repo.fork_namespace(ns);
392 &forked
393 } else {
394 &self.repo
395 };
396 let handle = repo
397 .speculate(&from_ref, label)
398 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
399 Ok(handle.id() as u32)
400 }
401
402 pub fn spec_get(&self, handle_id: u32, path: &str) -> Result<String, JsValue> {
404 let handle = SpecHandle::from_id(handle_id as u64);
405 let obj = self
406 .repo
407 .spec_get(handle, path)
408 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
409 let val = match &obj {
410 Object::Atom(a) => match a {
411 agentstategraph_core::Atom::Null => serde_json::Value::Null,
412 agentstategraph_core::Atom::Bool(b) => serde_json::json!(b),
413 agentstategraph_core::Atom::Int(i) => serde_json::json!(i),
414 agentstategraph_core::Atom::UInt(u) => serde_json::json!(u),
415 agentstategraph_core::Atom::Float(f) => serde_json::json!(f),
416 agentstategraph_core::Atom::String(s) => serde_json::json!(s),
417 _ => serde_json::json!(format!("{:?}", obj)),
418 },
419 _ => serde_json::json!(format!("{:?}", obj)),
420 };
421 Ok(serde_json::to_string(&val).unwrap_or_default())
422 }
423
424 pub fn spec_set(&self, handle_id: u32, path: &str, json_value: &str) -> Result<(), JsValue> {
426 let handle = SpecHandle::from_id(handle_id as u64);
427 let value: serde_json::Value = serde_json::from_str(json_value)
428 .map_err(|e| JsValue::from_str(&format!("JSON: {}", e)))?;
429 self.repo
431 .spec_set_json(handle, path, &value)
432 .map_err(|e| JsValue::from_str(&format!("{}", e)))
433 }
434
435 pub fn commit_speculation(
437 &self,
438 handle_id: u32,
439 category: &str,
440 description: &str,
441 reasoning: Option<String>,
442 confidence: Option<f64>,
443 ) -> Result<String, JsValue> {
444 let handle = SpecHandle::from_id(handle_id as u64);
445 let opts = make_opts(description, category, reasoning, confidence);
446 let id = self
447 .repo
448 .commit_speculation(handle, opts)
449 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
450 Ok(id.to_string())
451 }
452
453 pub fn discard_speculation(&self, handle_id: u32) -> Result<(), JsValue> {
455 let handle = SpecHandle::from_id(handle_id as u64);
456 self.repo
457 .discard_speculation(handle)
458 .map_err(|e| JsValue::from_str(&format!("{}", e)))
459 }
460
461 pub fn create_epoch(&self, id: &str, description: &str) -> Result<String, JsValue> {
463 self.repo
464 .create_epoch(id, description, vec![])
465 .map(|e| format!("Epoch '{}' created", e.id))
466 .map_err(|e| JsValue::from_str(&format!("{}", e)))
467 }
468
469 pub fn seal_epoch(&self, id: &str, summary: &str) -> Result<(), JsValue> {
471 self.repo
472 .seal_epoch(id, summary)
473 .map_err(|e| JsValue::from_str(&format!("{}", e)))
474 }
475
476 #[wasm_bindgen(js_name = tasksCreatePlan)]
485 pub fn tasks_create_plan(
486 &self,
487 prefix: &str,
488 agent_id: &str,
489 ref_name: &str,
490 name: &str,
491 description: Option<String>,
492 ) -> Result<String, JsValue> {
493 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
494 let plan = store
495 .create_plan(ref_name, name, description)
496 .map_err(js_err)?;
497 serde_json::to_string(&plan).map_err(js_err)
498 }
499
500 #[wasm_bindgen(js_name = tasksListPlans)]
501 pub fn tasks_list_plans(
502 &self,
503 prefix: &str,
504 agent_id: &str,
505 ref_name: &str,
506 ) -> Result<String, JsValue> {
507 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
508 let plans = store.list_plans(ref_name).map_err(js_err)?;
509 serde_json::to_string(&plans).map_err(js_err)
510 }
511
512 #[wasm_bindgen(js_name = tasksListPlansByStatus)]
513 pub fn tasks_list_plans_by_status(
514 &self,
515 prefix: &str,
516 agent_id: &str,
517 ref_name: &str,
518 status: Option<String>,
519 ) -> Result<String, JsValue> {
520 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
521 let parsed = status.as_deref().and_then(parse_plan_status);
522 let plans = store
523 .list_plans_by_status(ref_name, parsed)
524 .map_err(js_err)?;
525 serde_json::to_string(&plans).map_err(js_err)
526 }
527
528 #[wasm_bindgen(js_name = tasksGetPlan)]
529 pub fn tasks_get_plan(
530 &self,
531 prefix: &str,
532 agent_id: &str,
533 ref_name: &str,
534 name: &str,
535 ) -> Result<String, JsValue> {
536 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
537 let plan = store.get_plan(ref_name, name).map_err(js_err)?;
538 serde_json::to_string(&plan).map_err(js_err)
539 }
540
541 #[wasm_bindgen(js_name = tasksArchivePlan)]
542 pub fn tasks_archive_plan(
543 &self,
544 prefix: &str,
545 agent_id: &str,
546 ref_name: &str,
547 name: &str,
548 ) -> Result<String, JsValue> {
549 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
550 let plan = store.archive_plan(ref_name, name).map_err(js_err)?;
551 serde_json::to_string(&plan).map_err(js_err)
552 }
553
554 #[wasm_bindgen(js_name = tasksDeletePlan)]
555 pub fn tasks_delete_plan(
556 &self,
557 prefix: &str,
558 agent_id: &str,
559 ref_name: &str,
560 name: &str,
561 ) -> Result<(), JsValue> {
562 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
563 store.delete_plan(ref_name, name).map_err(js_err)
564 }
565
566 #[wasm_bindgen(js_name = tasksAddTask)]
569 #[allow(clippy::too_many_arguments)]
570 pub fn tasks_add_task(
571 &self,
572 prefix: &str,
573 agent_id: &str,
574 ref_name: &str,
575 plan: &str,
576 title: &str,
577 priority: &str,
578 parent_id: Option<String>,
579 blockers_json: Option<String>,
580 assigned_to: Option<String>,
581 ) -> Result<String, JsValue> {
582 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
583 let prio = parse_priority(priority);
584 let parent = parent_id.filter(|s| !s.is_empty()).map(TaskId);
585 let blockers: Vec<TaskId> = match blockers_json {
586 None => Vec::new(),
587 Some(s) if s.is_empty() => Vec::new(),
588 Some(s) => serde_json::from_str::<Vec<String>>(&s)
589 .map_err(js_err)?
590 .into_iter()
591 .map(TaskId)
592 .collect(),
593 };
594 let task = store
595 .add_task(ref_name, plan, title, prio, parent, blockers, assigned_to)
596 .map_err(js_err)?;
597 serde_json::to_string(&task).map_err(js_err)
598 }
599
600 #[wasm_bindgen(js_name = tasksAddTaskWithExtensions)]
606 #[allow(clippy::too_many_arguments)]
607 pub fn tasks_add_task_with_extensions(
608 &self,
609 prefix: &str,
610 agent_id: &str,
611 ref_name: &str,
612 plan: &str,
613 title: &str,
614 priority: &str,
615 parent_id: Option<String>,
616 blockers_json: Option<String>,
617 assigned_to: Option<String>,
618 payload_json: Option<String>,
619 parent_change: Option<String>,
620 on_complete_json: Option<String>,
621 ) -> Result<String, JsValue> {
622 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
623 let prio = parse_priority(priority);
624 let parent = parent_id.filter(|s| !s.is_empty()).map(TaskId);
625 let blockers: Vec<TaskId> = match blockers_json {
626 None => Vec::new(),
627 Some(s) if s.is_empty() => Vec::new(),
628 Some(s) => serde_json::from_str::<Vec<String>>(&s)
629 .map_err(js_err)?
630 .into_iter()
631 .map(TaskId)
632 .collect(),
633 };
634 let payload: Option<serde_json::Value> = match payload_json {
635 None => None,
636 Some(s) if s.is_empty() => None,
637 Some(s) => Some(serde_json::from_str(&s).map_err(js_err)?),
638 };
639 let on_complete: Option<OnCompleteHook> = match on_complete_json {
640 None => None,
641 Some(s) if s.is_empty() => None,
642 Some(s) => Some(serde_json::from_str(&s).map_err(js_err)?),
643 };
644 let task = store
645 .add_task_with_extensions(
646 ref_name,
647 plan,
648 title,
649 prio,
650 parent,
651 blockers,
652 assigned_to,
653 AddTaskOptions {
654 payload,
655 parent_change,
656 on_complete,
657 },
658 )
659 .map_err(js_err)?;
660 serde_json::to_string(&task).map_err(js_err)
661 }
662
663 #[wasm_bindgen(js_name = tasksListTasks)]
664 pub fn tasks_list_tasks(
665 &self,
666 prefix: &str,
667 agent_id: &str,
668 ref_name: &str,
669 plan: &str,
670 ) -> Result<String, JsValue> {
671 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
672 let tasks = store.list_tasks(ref_name, plan).map_err(js_err)?;
673 serde_json::to_string(&tasks).map_err(js_err)
674 }
675
676 #[wasm_bindgen(js_name = tasksTaskIds)]
677 pub fn tasks_task_ids(
678 &self,
679 prefix: &str,
680 agent_id: &str,
681 ref_name: &str,
682 plan: &str,
683 ) -> Result<String, JsValue> {
684 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
685 let ids: Vec<String> = store
686 .task_ids(ref_name, plan)
687 .map_err(js_err)?
688 .into_iter()
689 .map(|i| i.0)
690 .collect();
691 serde_json::to_string(&ids).map_err(js_err)
692 }
693
694 #[wasm_bindgen(js_name = tasksGetTask)]
695 pub fn tasks_get_task(
696 &self,
697 prefix: &str,
698 agent_id: &str,
699 ref_name: &str,
700 plan: &str,
701 task_id: &str,
702 ) -> Result<String, JsValue> {
703 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
704 let task = store
705 .get_task(ref_name, plan, &TaskId(task_id.to_string()))
706 .map_err(js_err)?;
707 serde_json::to_string(&task).map_err(js_err)
708 }
709
710 #[wasm_bindgen(js_name = tasksStartTask)]
711 pub fn tasks_start_task(
712 &self,
713 prefix: &str,
714 agent_id: &str,
715 ref_name: &str,
716 plan: &str,
717 task_id: &str,
718 ) -> Result<String, JsValue> {
719 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
720 let task = store
721 .start_task(ref_name, plan, &TaskId(task_id.to_string()))
722 .map_err(js_err)?;
723 serde_json::to_string(&task).map_err(js_err)
724 }
725
726 #[wasm_bindgen(js_name = tasksCompleteTask)]
728 #[allow(clippy::too_many_arguments)]
729 pub fn tasks_complete_task(
730 &self,
731 prefix: &str,
732 agent_id: &str,
733 ref_name: &str,
734 plan: &str,
735 task_id: &str,
736 proof_kind: &str,
737 proof_value: &str,
738 proof_note: Option<String>,
739 ) -> Result<String, JsValue> {
740 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
741 let kind = parse_proof_kind(proof_kind)
742 .ok_or_else(|| JsValue::from_str(&format!("invalid proof kind: {proof_kind}")))?;
743 let proof = Proof {
744 kind,
745 value: proof_value.to_string(),
746 note: proof_note,
747 };
748 let task = store
749 .complete_task(ref_name, plan, &TaskId(task_id.to_string()), proof)
750 .map_err(js_err)?;
751 serde_json::to_string(&task).map_err(js_err)
752 }
753
754 #[wasm_bindgen(js_name = tasksAbandonTask)]
755 pub fn tasks_abandon_task(
756 &self,
757 prefix: &str,
758 agent_id: &str,
759 ref_name: &str,
760 plan: &str,
761 task_id: &str,
762 reason: &str,
763 ) -> Result<String, JsValue> {
764 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
765 let task = store
766 .abandon_task(ref_name, plan, &TaskId(task_id.to_string()), reason)
767 .map_err(js_err)?;
768 serde_json::to_string(&task).map_err(js_err)
769 }
770
771 #[wasm_bindgen(js_name = tasksSetPriority)]
772 pub fn tasks_set_priority(
773 &self,
774 prefix: &str,
775 agent_id: &str,
776 ref_name: &str,
777 plan: &str,
778 task_id: &str,
779 priority: &str,
780 ) -> Result<String, JsValue> {
781 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
782 let task = store
783 .set_priority(
784 ref_name,
785 plan,
786 &TaskId(task_id.to_string()),
787 parse_priority(priority),
788 )
789 .map_err(js_err)?;
790 serde_json::to_string(&task).map_err(js_err)
791 }
792
793 #[wasm_bindgen(js_name = tasksSetTitle)]
794 pub fn tasks_set_title(
795 &self,
796 prefix: &str,
797 agent_id: &str,
798 ref_name: &str,
799 plan: &str,
800 task_id: &str,
801 title: &str,
802 ) -> Result<String, JsValue> {
803 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
804 let task = store
805 .set_title(ref_name, plan, &TaskId(task_id.to_string()), title)
806 .map_err(js_err)?;
807 serde_json::to_string(&task).map_err(js_err)
808 }
809
810 #[wasm_bindgen(js_name = tasksSetBlockers)]
811 pub fn tasks_set_blockers(
812 &self,
813 prefix: &str,
814 agent_id: &str,
815 ref_name: &str,
816 plan: &str,
817 task_id: &str,
818 blockers_json: &str,
819 ) -> Result<String, JsValue> {
820 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
821 let blockers: Vec<TaskId> = if blockers_json.is_empty() {
822 Vec::new()
823 } else {
824 serde_json::from_str::<Vec<String>>(blockers_json)
825 .map_err(js_err)?
826 .into_iter()
827 .map(TaskId)
828 .collect()
829 };
830 let task = store
831 .set_blockers(ref_name, plan, &TaskId(task_id.to_string()), blockers)
832 .map_err(js_err)?;
833 serde_json::to_string(&task).map_err(js_err)
834 }
835
836 #[wasm_bindgen(js_name = tasksAssignTask)]
837 pub fn tasks_assign_task(
838 &self,
839 prefix: &str,
840 agent_id: &str,
841 ref_name: &str,
842 plan: &str,
843 task_id: &str,
844 agent: &str,
845 ) -> Result<String, JsValue> {
846 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
847 let task = store
848 .assign_task(ref_name, plan, &TaskId(task_id.to_string()), agent)
849 .map_err(js_err)?;
850 serde_json::to_string(&task).map_err(js_err)
851 }
852
853 #[wasm_bindgen(js_name = tasksUnassignTask)]
854 pub fn tasks_unassign_task(
855 &self,
856 prefix: &str,
857 agent_id: &str,
858 ref_name: &str,
859 plan: &str,
860 task_id: &str,
861 ) -> Result<String, JsValue> {
862 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
863 let task = store
864 .unassign_task(ref_name, plan, &TaskId(task_id.to_string()))
865 .map_err(js_err)?;
866 serde_json::to_string(&task).map_err(js_err)
867 }
868
869 #[wasm_bindgen(js_name = tasksNextTask)]
870 pub fn tasks_next_task(
871 &self,
872 prefix: &str,
873 agent_id: &str,
874 ref_name: &str,
875 plan: &str,
876 ) -> Result<String, JsValue> {
877 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
878 let task = store.next_task(ref_name, plan).map_err(js_err)?;
879 serde_json::to_string(&task).map_err(js_err)
880 }
881
882 #[wasm_bindgen(js_name = tasksNextTaskFor)]
883 pub fn tasks_next_task_for(
884 &self,
885 prefix: &str,
886 agent_id: &str,
887 ref_name: &str,
888 plan: &str,
889 agent: Option<String>,
890 include_unassigned: bool,
891 ) -> Result<String, JsValue> {
892 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
893 let task = store
894 .next_task_for(ref_name, plan, agent.as_deref(), include_unassigned)
895 .map_err(js_err)?;
896 serde_json::to_string(&task).map_err(js_err)
897 }
898
899 #[wasm_bindgen(js_name = tasksDerivedStatus)]
900 pub fn tasks_derived_status(
901 &self,
902 prefix: &str,
903 agent_id: &str,
904 ref_name: &str,
905 plan: &str,
906 parent_id: &str,
907 ) -> Result<String, JsValue> {
908 let store = TaskStore::new(self.repo.clone(), prefix, agent_id);
909 let status = store
910 .derived_status(ref_name, plan, &TaskId(parent_id.to_string()))
911 .map_err(js_err)?;
912 serde_json::to_string(&status).map_err(js_err)
913 }
914
915 #[wasm_bindgen(js_name = migrateCheck)]
921 pub fn migrate_check(&self, ref_name: &str, target: Option<String>) -> Result<String, JsValue> {
922 let target_str = target.unwrap_or_else(|| SCHEMA_VERSION.to_string());
923 let target_version = Version::parse(&target_str).map_err(js_err)?;
924 let registry = Registry::builtin();
925 let r = agentstategraph_migrate::check(&self.repo, ref_name, &target_version, ®istry)
926 .map_err(js_err)?;
927 Ok(check_result_json(&r))
928 }
929
930 #[wasm_bindgen(js_name = migrateRun)]
932 pub fn migrate_run(
933 &self,
934 ref_name: &str,
935 target: Option<String>,
936 mode: &str,
937 ) -> Result<String, JsValue> {
938 let target_str = target.unwrap_or_else(|| SCHEMA_VERSION.to_string());
939 let target_version = Version::parse(&target_str).map_err(js_err)?;
940 let run_mode = match mode.to_lowercase().as_str() {
941 "apply" => RunMode::Apply,
942 "dry-run" | "dryrun" | "dry_run" => RunMode::DryRun,
943 other => return Err(JsValue::from_str(&format!("invalid mode: {other}"))),
944 };
945 let registry = Registry::builtin();
946 let r = registry
947 .run(&self.repo, ref_name, &target_version, run_mode)
948 .map_err(js_err)?;
949 Ok(report_json(&r))
950 }
951
952 #[wasm_bindgen(js_name = createSession)]
963 #[allow(clippy::too_many_arguments)]
964 pub fn create_session(
965 &self,
966 agent_id: &str,
967 working_branch: Option<String>,
968 parent_session: Option<String>,
969 delegated_intent: Option<String>,
970 report_to: Option<String>,
971 path_scope: Option<String>,
972 ) -> Result<String, JsValue> {
973 let branch = working_branch.unwrap_or_else(|| "main".to_string());
974 let log = self.repo.log(&branch, 1).map_err(js_err)?;
975 let head = log
976 .into_iter()
977 .next()
978 .map(|c| c.id)
979 .ok_or_else(|| JsValue::from_str(&format!("ref {branch:?} empty")))?;
980 let mgr = self.repo.sessions();
981 let s = mgr
982 .create(
983 agent_id,
984 &branch,
985 head,
986 agentstategraph::session::CreateSessionParams {
987 parent_session,
988 delegated_intent,
989 report_to,
990 path_scope,
991 scope_namespace: None,
992 },
993 )
994 .map_err(js_err)?;
995 serde_json::to_string(&s).map_err(js_err)
996 }
997
998 #[wasm_bindgen(js_name = getSession)]
1000 pub fn get_session(&self, id: &str) -> Result<String, JsValue> {
1001 let mgr = self.repo.sessions();
1002 let s = mgr.get(id).map_err(js_err)?;
1003 serde_json::to_string(&s).map_err(js_err)
1004 }
1005
1006 #[wasm_bindgen(js_name = listSessions)]
1008 pub fn list_sessions(&self, agent_filter: Option<String>) -> Result<String, JsValue> {
1009 let mgr = self.repo.sessions();
1010 let sessions = mgr.list(agent_filter.as_deref()).map_err(js_err)?;
1011 serde_json::to_string(&sessions).map_err(js_err)
1012 }
1013
1014 #[wasm_bindgen(js_name = endSession)]
1016 pub fn end_session(&self, id: &str, status: &str) -> Result<(), JsValue> {
1017 let st = parse_session_status(status)
1018 .ok_or_else(|| JsValue::from_str(&format!("invalid session status: {status}")))?;
1019 let mgr = self.repo.sessions();
1020 mgr.end(id, st).map_err(js_err)
1021 }
1022
1023 pub fn taint(&self, ref_name: &str, path: &str, params_json: &str) -> Result<String, JsValue> {
1038 let params = parse_taint_params(params_json)?;
1039 self.repo.taint(ref_name, path, params).map_err(js_err)
1040 }
1041
1042 pub fn untaint(
1045 &self,
1046 ref_name: &str,
1047 path: &str,
1048 name: &str,
1049 params_json: &str,
1050 ) -> Result<(), JsValue> {
1051 let params = parse_untaint_params(params_json)?;
1052 self.repo
1053 .untaint(ref_name, path, name, params)
1054 .map_err(js_err)
1055 }
1056
1057 pub fn quarantine(
1060 &self,
1061 ref_name: &str,
1062 path: &str,
1063 params_json: &str,
1064 ) -> Result<String, JsValue> {
1065 let params = parse_quarantine_params(params_json)?;
1066 self.repo.quarantine(ref_name, path, params).map_err(js_err)
1067 }
1068
1069 pub fn unquarantine(
1071 &self,
1072 ref_name: &str,
1073 path: &str,
1074 name: &str,
1075 params_json: &str,
1076 ) -> Result<(), JsValue> {
1077 let params = parse_untaint_params(params_json)?;
1078 self.repo
1079 .unquarantine(ref_name, path, name, params)
1080 .map_err(js_err)
1081 }
1082
1083 pub fn watch(&self, ref_name: &str, path: &str, params_json: &str) -> Result<String, JsValue> {
1087 let params = parse_watch_params(params_json)?;
1088 self.repo.watch_path(ref_name, path, params).map_err(js_err)
1089 }
1090
1091 pub fn unwatch(
1093 &self,
1094 ref_name: &str,
1095 path: &str,
1096 name: &str,
1097 params_json: &str,
1098 ) -> Result<(), JsValue> {
1099 let params = parse_unwatch_params(params_json)?;
1100 self.repo
1101 .unwatch(ref_name, path, name, params)
1102 .map_err(js_err)
1103 }
1104
1105 #[wasm_bindgen(js_name = listTaints)]
1108 pub fn list_taints(
1109 &self,
1110 path_prefix: Option<String>,
1111 kind: Option<String>,
1112 include_resolved: bool,
1113 ) -> Result<String, JsValue> {
1114 let parsed_kind = match kind.as_deref() {
1115 None => None,
1116 Some(s) => match parse_taint_kind_wasm(s) {
1117 Some(k) => Some(k),
1118 None => return Err(JsValue::from_str(&format!("unknown kind: {s}"))),
1119 },
1120 };
1121 let list = self
1122 .repo
1123 .list_taints(path_prefix.as_deref(), parsed_kind, include_resolved)
1124 .map_err(js_err)?;
1125 serde_json::to_string(&list).map_err(js_err)
1126 }
1127
1128 #[wasm_bindgen(js_name = checkTaint)]
1131 pub fn check_taint(
1132 &self,
1133 path: &str,
1134 agent_id: Option<String>,
1135 confidence: Option<f64>,
1136 ) -> Result<String, JsValue> {
1137 let agent = agent_id.unwrap_or_default();
1138 let conf = confidence.unwrap_or(0.0);
1139 let check = self.repo.check_taint(path, &agent, conf).map_err(js_err)?;
1140 serde_json::to_string(&check).map_err(js_err)
1141 }
1142
1143 pub fn list_epochs(&self) -> Result<String, JsValue> {
1145 let entries = self
1146 .repo
1147 .list_epochs()
1148 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
1149 let json: Vec<serde_json::Value> = entries
1150 .iter()
1151 .map(|e| {
1152 serde_json::json!({
1153 "id": e.id,
1154 "description": e.description,
1155 "status": format!("{:?}", e.status),
1156 "commits": e.commit_count,
1157 })
1158 })
1159 .collect();
1160 Ok(serde_json::to_string(&json).unwrap_or_default())
1161 }
1162
1163 pub fn list_all_epochs(&self) -> Result<String, JsValue> {
1169 let entries = self
1170 .repo
1171 .list_all_epochs()
1172 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
1173 let json: Vec<serde_json::Value> = entries
1174 .iter()
1175 .map(|e| {
1176 serde_json::json!({
1177 "id": e.id,
1178 "description": e.description,
1179 "status": format!("{:?}", e.status),
1180 "commits": e.commit_count,
1181 "scope": e.scope.as_storage_str(),
1182 "namespace": e.namespace,
1183 })
1184 })
1185 .collect();
1186 Ok(serde_json::to_string(&json).unwrap_or_default())
1187 }
1188}
1189
1190#[wasm_bindgen]
1205pub struct WasmPolicyStore {
1206 inner: PolicyBackend,
1207}
1208
1209impl WasmPolicyStore {
1210 #[doc(hidden)]
1215 pub fn from_repo(repo: Arc<Repository>, prefix: &str, agent_id: &str) -> Self {
1216 Self {
1217 inner: PolicyBackend::new(repo, prefix, agent_id),
1218 }
1219 }
1220}
1221
1222#[wasm_bindgen]
1223impl WasmPolicyStore {
1224 #[wasm_bindgen(constructor)]
1226 pub fn new(asg: &WasmAgentStateGraph, prefix: &str, agent_id: &str) -> WasmPolicyStore {
1227 WasmPolicyStore {
1228 inner: PolicyBackend::new(asg.repo.clone(), prefix, agent_id),
1229 }
1230 }
1231
1232 pub fn propose(&self, ref_name: &str, policy_json: &str) -> Result<String, JsValue> {
1235 let p: Policy = serde_json::from_str(policy_json).map_err(js_err)?;
1236 self.inner.propose(ref_name, p).map_err(js_err)
1237 }
1238
1239 pub fn ratify(
1241 &self,
1242 ref_name: &str,
1243 path: &str,
1244 ratifier: &str,
1245 reasoning: &str,
1246 ) -> Result<(), JsValue> {
1247 self.inner
1248 .ratify(ref_name, path, ratifier, reasoning)
1249 .map_err(js_err)
1250 }
1251
1252 pub fn supersede(
1255 &self,
1256 ref_name: &str,
1257 path: &str,
1258 new_policy_json: &str,
1259 ) -> Result<String, JsValue> {
1260 let p: Policy = serde_json::from_str(new_policy_json).map_err(js_err)?;
1261 self.inner.supersede(ref_name, path, p).map_err(js_err)
1262 }
1263
1264 pub fn list(
1273 &self,
1274 ref_name: &str,
1275 prefix_filter: Option<String>,
1276 tenant_filter: Option<String>,
1277 ) -> Result<String, JsValue> {
1278 let policies = self
1279 .inner
1280 .list_scoped(ref_name, prefix_filter.as_deref(), tenant_filter.as_deref())
1281 .map_err(js_err)?;
1282 serde_json::to_string(&policies).map_err(js_err)
1283 }
1284
1285 pub fn active(
1289 &self,
1290 ref_name: &str,
1291 prefix_filter: Option<String>,
1292 tenant_filter: Option<String>,
1293 ) -> Result<String, JsValue> {
1294 let policies = self
1295 .inner
1296 .active_scoped(ref_name, prefix_filter.as_deref(), tenant_filter.as_deref())
1297 .map_err(js_err)?;
1298 serde_json::to_string(&policies).map_err(js_err)
1299 }
1300
1301 pub fn get(&self, ref_name: &str, path: &str, version: Option<u64>) -> Result<String, JsValue> {
1304 let policy = self.inner.get(ref_name, path, version).map_err(js_err)?;
1305 serde_json::to_string(&policy).map_err(js_err)
1306 }
1307
1308 pub fn history(&self, ref_name: &str, path: &str) -> Result<String, JsValue> {
1310 let policies = self.inner.history(ref_name, path).map_err(js_err)?;
1311 serde_json::to_string(&policies).map_err(js_err)
1312 }
1313
1314 pub fn evaluate(
1323 &self,
1324 ref_name: &str,
1325 situation_json: &str,
1326 action: &str,
1327 agent_id: &str,
1328 tenant_filter: Option<String>,
1329 ) -> Result<String, JsValue> {
1330 let sit: Situation = serde_json::from_str(situation_json).map_err(js_err)?;
1331 let decision = self
1332 .inner
1333 .evaluate_scoped(ref_name, &sit, action, agent_id, tenant_filter.as_deref())
1334 .map_err(js_err)?;
1335 serde_json::to_string(&decision).map_err(js_err)
1336 }
1337
1338 #[wasm_bindgen(js_name = evaluateChange)]
1343 pub fn evaluate_change(
1344 &self,
1345 ref_name: &str,
1346 proposal_json: &str,
1347 tenant_filter: Option<String>,
1348 ) -> Result<String, JsValue> {
1349 let prop: ChangeProposal = serde_json::from_str(proposal_json).map_err(js_err)?;
1350 let decision = self
1351 .inner
1352 .evaluate_change_scoped(ref_name, &prop, tenant_filter.as_deref())
1353 .map_err(js_err)?;
1354 serde_json::to_string(&decision).map_err(js_err)
1355 }
1356
1357 pub fn sign(
1369 &self,
1370 ref_name: &str,
1371 path: &str,
1372 signer_key_id: &str,
1373 private_key_hex: &str,
1374 ) -> Result<String, JsValue> {
1375 use agentstategraph_policy::PolicySignature;
1376 use agentstategraph_policy_sign::{canonicalize, Ed25519Signer, PolicySigner};
1377
1378 let seed_vec = hex::decode(private_key_hex)
1379 .map_err(|e| js_err(format!("invalid private_key_hex: {e}")))?;
1380 let seed: [u8; 32] = seed_vec
1381 .as_slice()
1382 .try_into()
1383 .map_err(|_| js_err("private_key_hex must decode to 32 bytes"))?;
1384 let signer = Ed25519Signer::from_bytes(signer_key_id.to_string(), &seed);
1385
1386 let policy = self
1387 .inner
1388 .get(ref_name, path, None)
1389 .map_err(|e| js_err(e.to_string()))?;
1390 let canonical = canonicalize(&policy).map_err(|e| js_err(e.to_string()))?;
1391 let (key_id, sig_bytes) = signer.sign(&canonical).map_err(|e| js_err(e.to_string()))?;
1392 let sig_hex = hex::encode(&sig_bytes);
1393 let signature = PolicySignature::Ed25519 {
1394 signer_key_id: key_id.clone(),
1395 signature_hex: sig_hex.clone(),
1396 };
1397 self.inner
1398 .set_signature(ref_name, path, signature)
1399 .map_err(|e| js_err(e.to_string()))?;
1400 let envelope = serde_json::json!({
1401 "algorithm": "ed25519",
1402 "signer_key_id": key_id,
1403 "signature_hex": sig_hex,
1404 });
1405 serde_json::to_string(&envelope).map_err(js_err)
1406 }
1407
1408 pub fn verify(
1413 &self,
1414 ref_name: &str,
1415 path: &str,
1416 public_key_hex: &str,
1417 ) -> Result<String, JsValue> {
1418 use agentstategraph_policy::PolicySignature;
1419 use agentstategraph_policy_sign::{
1420 canonicalize, Ed25519Verifier, InMemoryKeyRegistry, PolicyVerifier,
1421 };
1422
1423 let policy = self
1424 .inner
1425 .get(ref_name, path, None)
1426 .map_err(|e| js_err(e.to_string()))?;
1427 let Some(sig) = policy.signature.as_ref() else {
1428 let env = serde_json::json!({"valid": false, "reason": "unsigned"});
1429 return serde_json::to_string(&env).map_err(js_err);
1430 };
1431 let PolicySignature::Ed25519 {
1432 signer_key_id,
1433 signature_hex,
1434 } = sig;
1435
1436 let pk_vec = hex::decode(public_key_hex)
1437 .map_err(|e| js_err(format!("invalid public_key_hex: {e}")))?;
1438 let pk_bytes: [u8; 32] = pk_vec
1439 .as_slice()
1440 .try_into()
1441 .map_err(|_| js_err("public_key_hex must decode to 32 bytes"))?;
1442 let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&pk_bytes)
1443 .map_err(|e| js_err(format!("invalid verifying key: {e}")))?;
1444
1445 let mut registry = InMemoryKeyRegistry::new();
1446 registry.insert(signer_key_id.clone(), verifying_key);
1447 let verifier = Ed25519Verifier::new(registry);
1448
1449 let sig_bytes = match hex::decode(signature_hex) {
1450 Ok(b) => b,
1451 Err(e) => {
1452 let env = serde_json::json!({
1453 "valid": false,
1454 "reason": format!("invalid signature_hex: {e}"),
1455 });
1456 return serde_json::to_string(&env).map_err(js_err);
1457 }
1458 };
1459 let canonical = canonicalize(&policy).map_err(|e| js_err(e.to_string()))?;
1460 match verifier.verify(signer_key_id, &sig_bytes, &canonical) {
1461 Ok(()) => {
1462 let env = serde_json::json!({
1463 "valid": true,
1464 "algorithm": "ed25519",
1465 "signer_key_id": signer_key_id,
1466 });
1467 serde_json::to_string(&env).map_err(js_err)
1468 }
1469 Err(e) => {
1470 let env = serde_json::json!({
1471 "valid": false,
1472 "reason": e.to_string(),
1473 });
1474 serde_json::to_string(&env).map_err(js_err)
1475 }
1476 }
1477 }
1478
1479 #[wasm_bindgen(js_name = setExternalEvaluator)]
1485 #[allow(unused_variables)]
1486 pub fn set_external_evaluator(
1487 &self,
1488 ref_name: &str,
1489 path: &str,
1490 config_json: Option<String>,
1491 ) -> Result<String, JsValue> {
1492 let envelope = serde_json::json!({
1493 "error": "not yet wired",
1494 "hint": "set policy.external_evaluator before propose/supersede",
1495 });
1496 serde_json::to_string(&envelope).map_err(js_err)
1497 }
1498
1499 #[wasm_bindgen(js_name = checkTokens)]
1503 pub fn check_tokens(&self, ref_name: &str, tokens_json: &str) -> Result<String, JsValue> {
1504 let tokens: Vec<String> = serde_json::from_str(tokens_json).map_err(js_err)?;
1505 let token_set: std::collections::HashSet<&str> =
1506 tokens.iter().map(|s| s.as_str()).collect();
1507 let actives = self.inner.active(ref_name, None).map_err(js_err)?;
1508 let matched: Vec<Policy> = actives
1509 .into_iter()
1510 .filter(|p| p.triggers.iter().any(|t| token_set.contains(t.as_str())))
1511 .collect();
1512 serde_json::to_string(&matched).map_err(js_err)
1513 }
1514}
1515
1516fn js_err<E: std::fmt::Display>(e: E) -> JsValue {
1521 JsValue::from_str(&format!("{}", e))
1522}
1523
1524fn parse_priority(s: &str) -> Priority {
1525 match s.to_lowercase().as_str() {
1526 "low" => Priority::Low,
1527 "high" => Priority::High,
1528 "critical" => Priority::Critical,
1529 _ => Priority::Medium,
1530 }
1531}
1532
1533fn parse_plan_status(s: &str) -> Option<agentstategraph_tasks::PlanStatus> {
1534 match s.to_lowercase().as_str() {
1535 "active" => Some(agentstategraph_tasks::PlanStatus::Active),
1536 "completed" => Some(agentstategraph_tasks::PlanStatus::Completed),
1537 "archived" => Some(agentstategraph_tasks::PlanStatus::Archived),
1538 _ => None,
1539 }
1540}
1541
1542fn parse_session_status(s: &str) -> Option<SessionStatus> {
1543 match s.to_lowercase().as_str() {
1544 "active" => Some(SessionStatus::Active),
1545 "completed" => Some(SessionStatus::Completed),
1546 "abandoned" => Some(SessionStatus::Abandoned),
1547 _ => None,
1548 }
1549}
1550
1551fn parse_proof_kind(s: &str) -> Option<ProofKind> {
1552 match s.to_lowercase().as_str() {
1553 "commit" => Some(ProofKind::Commit),
1554 "file" => Some(ProofKind::File),
1555 "test" => Some(ProofKind::Test),
1556 "text" => Some(ProofKind::Text),
1557 _ => None,
1558 }
1559}
1560
1561fn check_result_json(r: &CheckResult) -> String {
1562 let v = match r {
1563 CheckResult::UpToDate { version } => serde_json::json!({
1564 "status": "up_to_date",
1565 "version": version.to_string(),
1566 }),
1567 CheckResult::UpgradeAvailable {
1568 from,
1569 to,
1570 migrations,
1571 } => serde_json::json!({
1572 "status": "upgrade_available",
1573 "from": from.to_string(),
1574 "to": to.to_string(),
1575 "migrations": migrations,
1576 }),
1577 CheckResult::Downgrade { db, binary } => serde_json::json!({
1578 "status": "downgrade",
1579 "db": db.to_string(),
1580 "binary": binary.to_string(),
1581 }),
1582 CheckResult::Unversioned { implicit } => serde_json::json!({
1583 "status": "unversioned",
1584 "implicit": implicit.to_string(),
1585 }),
1586 CheckResult::Corrupt(msg) => serde_json::json!({
1587 "status": "corrupt",
1588 "message": msg,
1589 }),
1590 };
1591 serde_json::to_string(&v).unwrap_or_default()
1592}
1593
1594fn report_json(r: &agentstategraph_migrate::Report) -> String {
1595 let mode = match r.mode {
1596 RunMode::DryRun => "dry-run",
1597 RunMode::Apply => "apply",
1598 };
1599 let steps: Vec<serde_json::Value> = r
1600 .steps
1601 .iter()
1602 .map(|s| {
1603 serde_json::json!({
1604 "name": s.name,
1605 "describe": s.describe,
1606 "from": s.from.to_string(),
1607 "to": s.to.to_string(),
1608 "status": step_status_str(s.status),
1609 "commit_id": s.commit_id.as_ref().map(|c| c.to_string()),
1610 "notes": s.notes,
1611 })
1612 })
1613 .collect();
1614 let v = serde_json::json!({
1615 "from": r.from.to_string(),
1616 "target": r.target.to_string(),
1617 "final_version": r.final_version.to_string(),
1618 "mode": mode,
1619 "steps": steps,
1620 });
1621 serde_json::to_string(&v).unwrap_or_default()
1622}
1623
1624fn parse_taint_effect_wasm(s: &str) -> Option<TaintEffect> {
1632 match s.to_lowercase().as_str() {
1633 "warn" => Some(TaintEffect::Warn),
1634 "block" => Some(TaintEffect::Block),
1635 "review" => Some(TaintEffect::Review),
1636 "isolate" => Some(TaintEffect::Isolate),
1637 "advisory" => Some(TaintEffect::Advisory),
1638 _ => None,
1639 }
1640}
1641
1642fn parse_taint_severity_wasm(s: Option<&str>) -> TaintSeverity {
1643 match s.unwrap_or("medium").to_lowercase().as_str() {
1644 "low" => TaintSeverity::Low,
1645 "high" => TaintSeverity::High,
1646 "critical" => TaintSeverity::Critical,
1647 _ => TaintSeverity::Medium,
1648 }
1649}
1650
1651fn parse_taint_kind_wasm(s: &str) -> Option<TaintKind> {
1652 match s.to_lowercase().as_str() {
1653 "taint" => Some(TaintKind::Taint),
1654 "quarantine" => Some(TaintKind::Quarantine),
1655 "watch" => Some(TaintKind::Watch),
1656 _ => None,
1657 }
1658}
1659
1660fn parse_rfc3339(v: Option<&serde_json::Value>) -> Option<chrono::DateTime<chrono::Utc>> {
1661 v.and_then(|x| x.as_str())
1662 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
1663 .map(|d| d.with_timezone(&chrono::Utc))
1664}
1665
1666fn parse_taint_params(json: &str) -> Result<TaintParams, JsValue> {
1667 let v: serde_json::Value =
1668 serde_json::from_str(json).map_err(|e| js_err(format!("invalid params: {e}")))?;
1669 let name = v
1670 .get("name")
1671 .and_then(|x| x.as_str())
1672 .ok_or_else(|| JsValue::from_str("missing 'name'"))?
1673 .to_string();
1674 let effect = v
1675 .get("effect")
1676 .and_then(|x| x.as_str())
1677 .and_then(parse_taint_effect_wasm)
1678 .ok_or_else(|| JsValue::from_str("missing or invalid 'effect'"))?;
1679 let metadata: TaintMetadata = match v.get("metadata") {
1680 Some(m) if !m.is_null() => serde_json::from_value(m.clone()).map_err(js_err)?,
1681 _ => TaintMetadata::new(),
1682 };
1683 Ok(TaintParams {
1684 name,
1685 effect,
1686 reason: v
1687 .get("reason")
1688 .and_then(|x| x.as_str())
1689 .unwrap_or("")
1690 .to_string(),
1691 severity: parse_taint_severity_wasm(v.get("severity").and_then(|x| x.as_str())),
1692 expires_at: parse_rfc3339(v.get("expires")),
1693 propagate: v.get("propagate").and_then(|x| x.as_bool()).unwrap_or(true),
1694 metadata,
1695 agent_id: v
1696 .get("agent_id")
1697 .and_then(|x| x.as_str())
1698 .unwrap_or("")
1699 .to_string(),
1700 })
1701}
1702
1703fn parse_untaint_params(json: &str) -> Result<UntaintParams, JsValue> {
1704 let v: serde_json::Value =
1705 serde_json::from_str(json).map_err(|e| js_err(format!("invalid params: {e}")))?;
1706 Ok(UntaintParams {
1707 reason: v
1708 .get("reason")
1709 .and_then(|x| x.as_str())
1710 .unwrap_or("")
1711 .to_string(),
1712 proof: v.get("proof").and_then(|x| x.as_str()).map(str::to_string),
1713 agent_id: v
1714 .get("agent_id")
1715 .and_then(|x| x.as_str())
1716 .unwrap_or("")
1717 .to_string(),
1718 })
1719}
1720
1721fn parse_quarantine_params(json: &str) -> Result<QuarantineParams, JsValue> {
1722 let v: serde_json::Value =
1723 serde_json::from_str(json).map_err(|e| js_err(format!("invalid params: {e}")))?;
1724 let name = v
1725 .get("name")
1726 .and_then(|x| x.as_str())
1727 .ok_or_else(|| JsValue::from_str("missing 'name'"))?
1728 .to_string();
1729 let authorized: Vec<String> = v
1730 .get("authorized_agents")
1731 .and_then(|x| x.as_array())
1732 .map(|arr| {
1733 arr.iter()
1734 .filter_map(|x| x.as_str().map(str::to_string))
1735 .collect()
1736 })
1737 .unwrap_or_default();
1738 Ok(QuarantineParams {
1739 name,
1740 reason: v
1741 .get("reason")
1742 .and_then(|x| x.as_str())
1743 .unwrap_or("")
1744 .to_string(),
1745 severity: parse_taint_severity_wasm(v.get("severity").and_then(|x| x.as_str())),
1746 authorized_agents: authorized,
1747 expires_at: parse_rfc3339(v.get("expires")),
1748 propagate: v.get("propagate").and_then(|x| x.as_bool()).unwrap_or(true),
1749 agent_id: v
1750 .get("agent_id")
1751 .and_then(|x| x.as_str())
1752 .unwrap_or("")
1753 .to_string(),
1754 })
1755}
1756
1757fn parse_watch_params(json: &str) -> Result<WatchParams, JsValue> {
1758 let v: serde_json::Value =
1759 serde_json::from_str(json).map_err(|e| js_err(format!("invalid params: {e}")))?;
1760 let name = v
1761 .get("name")
1762 .and_then(|x| x.as_str())
1763 .ok_or_else(|| JsValue::from_str("missing 'name'"))?
1764 .to_string();
1765 let direction = match v
1766 .get("direction")
1767 .and_then(|x| x.as_str())
1768 .unwrap_or("above")
1769 {
1770 "below" => WatchDirection::Below,
1771 _ => WatchDirection::Above,
1772 };
1773 Ok(WatchParams {
1774 name,
1775 reason: v
1776 .get("reason")
1777 .and_then(|x| x.as_str())
1778 .unwrap_or("")
1779 .to_string(),
1780 metric: v.get("metric").and_then(|x| x.as_str()).map(str::to_string),
1781 threshold: v.get("threshold").and_then(|x| x.as_f64()),
1782 direction,
1783 check_interval_secs: v.get("check_interval_secs").and_then(|x| x.as_u64()),
1784 expires_at: parse_rfc3339(v.get("expires")),
1785 severity: parse_taint_severity_wasm(v.get("severity").and_then(|x| x.as_str())),
1786 propagate: v.get("propagate").and_then(|x| x.as_bool()).unwrap_or(true),
1787 agent_id: v
1788 .get("agent_id")
1789 .and_then(|x| x.as_str())
1790 .unwrap_or("")
1791 .to_string(),
1792 })
1793}
1794
1795fn parse_unwatch_params(json: &str) -> Result<UnwatchParams, JsValue> {
1796 let v: serde_json::Value =
1797 serde_json::from_str(json).map_err(|e| js_err(format!("invalid params: {e}")))?;
1798 Ok(UnwatchParams {
1799 reason: v.get("reason").and_then(|x| x.as_str()).map(str::to_string),
1800 agent_id: v
1801 .get("agent_id")
1802 .and_then(|x| x.as_str())
1803 .unwrap_or("")
1804 .to_string(),
1805 })
1806}
1807
1808fn step_status_str(s: StepStatus) -> &'static str {
1809 match s {
1810 StepStatus::WouldApply => "would-apply",
1811 StepStatus::WouldSkip => "would-skip",
1812 StepStatus::Applied => "applied",
1813 StepStatus::Skipped => "skipped",
1814 StepStatus::Failed => "failed",
1815 }
1816}