1#![allow(clippy::not_unsafe_ptr_arg_deref)]
17#![allow(clippy::missing_safety_doc)]
18
19use std::ffi::{CStr, CString};
20use std::os::raw::c_char;
21use std::ptr;
22use std::sync::Arc;
23
24use agentstategraph::{
25 CommitOptions, CreateSessionParams, Repository, RetentionPolicy, SpecHandle, SCHEMA_VERSION,
26};
27use agentstategraph_core::{
28 IntentCategory, Namespace, ObjectId, QueryFilters, SessionStatus, ToolCall,
29};
30use agentstategraph_migrate::{CheckResult, Registry, RunMode, StepStatus};
31use agentstategraph_policy::{ChangeProposal, Policy, PolicyStore, Situation};
32use agentstategraph_storage::SqliteStorage;
33use agentstategraph_taint::{
34 QuarantineParams, TaintEffect, TaintKind, TaintMetadata, TaintParams, TaintSeverity,
35 UntaintParams, UnwatchParams, WatchDirection, WatchParams,
36};
37use agentstategraph_tasks::{AddTaskOptions, Priority, Proof, ProofKind, TaskId, TaskStore};
38use semver::Version;
39use serde::Serialize;
40
41pub struct SgRepo {
43 inner: Arc<Repository>,
44}
45
46pub struct SgTaskStore {
48 inner: TaskStore,
49}
50
51pub struct SgPolicyStore {
53 inner: PolicyStore,
54}
55
56#[no_mangle]
58pub extern "C" fn agentstategraph_new_memory() -> *mut SgRepo {
59 let storage = match SqliteStorage::in_memory() {
60 Ok(s) => s,
61 Err(_) => return ptr::null_mut(),
62 };
63 let repo = Repository::new(Box::new(storage));
64 if repo.init().is_err() {
65 return ptr::null_mut();
66 }
67 Box::into_raw(Box::new(SgRepo {
68 inner: Arc::new(repo),
69 }))
70}
71
72#[no_mangle]
74pub extern "C" fn agentstategraph_new_sqlite(path: *const c_char) -> *mut SgRepo {
75 let path = unsafe {
76 if path.is_null() {
77 return ptr::null_mut();
78 }
79 match CStr::from_ptr(path).to_str() {
80 Ok(s) => s.to_string(),
81 Err(_) => return ptr::null_mut(),
82 }
83 };
84 let storage = match SqliteStorage::open(&path) {
85 Ok(s) => s,
86 Err(_) => return ptr::null_mut(),
87 };
88 let repo = Repository::new(Box::new(storage));
89 if repo.init().is_err() {
90 return ptr::null_mut();
91 }
92 Box::into_raw(Box::new(SgRepo {
93 inner: Arc::new(repo),
94 }))
95}
96
97#[cfg(feature = "postgres")]
106#[no_mangle]
107pub extern "C" fn agentstategraph_new_postgres(
108 url: *const c_char,
109 tenant_id: *const c_char,
110) -> *mut SgRepo {
111 use agentstategraph_storage::PostgresStorage;
112 if url.is_null() || tenant_id.is_null() {
113 return ptr::null_mut();
114 }
115 let url = match unsafe { CStr::from_ptr(url) }.to_str() {
116 Ok(s) => s.to_string(),
117 Err(_) => return ptr::null_mut(),
118 };
119 let tenant = match unsafe { CStr::from_ptr(tenant_id) }.to_str() {
120 Ok(s) => s.to_string(),
121 Err(_) => return ptr::null_mut(),
122 };
123 let rt = match tokio::runtime::Builder::new_multi_thread()
131 .enable_all()
132 .build()
133 {
134 Ok(r) => r,
135 Err(_) => return ptr::null_mut(),
136 };
137 let storage = match rt.block_on(PostgresStorage::connect_tenant(&url, &tenant)) {
138 Ok(s) => s,
139 Err(_) => return ptr::null_mut(),
140 };
141 let _guard = rt.enter();
150 std::mem::forget(rt);
151 let repo = Repository::new(Box::new(storage));
152 if repo.init().is_err() {
153 return ptr::null_mut();
154 }
155 Box::into_raw(Box::new(SgRepo {
156 inner: Arc::new(repo),
157 }))
158}
159
160#[no_mangle]
162pub extern "C" fn agentstategraph_free(repo: *mut SgRepo) {
163 if !repo.is_null() {
164 unsafe {
165 drop(Box::from_raw(repo));
166 }
167 }
168}
169
170#[no_mangle]
172pub extern "C" fn agentstategraph_free_string(s: *mut c_char) {
173 if !s.is_null() {
174 unsafe {
175 drop(CString::from_raw(s));
176 }
177 }
178}
179
180#[no_mangle]
182pub extern "C" fn agentstategraph_get(
183 repo: *const SgRepo,
184 ref_name: *const c_char,
185 path: *const c_char,
186) -> *mut c_char {
187 let (repo, ref_name, path) = match unsafe { parse_repo_ref_path(repo, ref_name, path) } {
188 Some(v) => v,
189 None => return ptr::null_mut(),
190 };
191 match repo.inner.get_json(&ref_name, &path) {
192 Ok(val) => to_c_string(&serde_json::to_string(&val).unwrap_or_default()),
193 Err(_) => ptr::null_mut(),
194 }
195}
196
197#[no_mangle]
199pub extern "C" fn agentstategraph_set(
200 repo: *const SgRepo,
201 ref_name: *const c_char,
202 path: *const c_char,
203 json_value: *const c_char,
204 intent_category: *const c_char,
205 intent_description: *const c_char,
206) -> *mut c_char {
207 let (repo, ref_name, path) = match unsafe { parse_repo_ref_path(repo, ref_name, path) } {
208 Some(v) => v,
209 None => return ptr::null_mut(),
210 };
211 let json_str = unsafe { c_to_str(json_value) };
212 let category_str = unsafe { c_to_str(intent_category) };
213 let desc_str = unsafe { c_to_str(intent_description) };
214
215 let value: serde_json::Value = match serde_json::from_str(&json_str) {
216 Ok(v) => v,
217 Err(_) => return ptr::null_mut(),
218 };
219
220 let category = parse_category(&category_str);
221 let opts = CommitOptions::new("ffi", category, &desc_str);
222
223 match repo.inner.set_json(&ref_name, &path, &value, opts) {
224 Ok(id) => to_c_string(&id.to_string()),
225 Err(_) => ptr::null_mut(),
226 }
227}
228
229#[no_mangle]
231pub extern "C" fn agentstategraph_delete(
232 repo: *const SgRepo,
233 ref_name: *const c_char,
234 path: *const c_char,
235 intent_category: *const c_char,
236 intent_description: *const c_char,
237) -> *mut c_char {
238 let (repo, ref_name, path) = match unsafe { parse_repo_ref_path(repo, ref_name, path) } {
239 Some(v) => v,
240 None => return ptr::null_mut(),
241 };
242 let category_str = unsafe { c_to_str(intent_category) };
243 let desc_str = unsafe { c_to_str(intent_description) };
244 let category = parse_category(&category_str);
245 let opts = CommitOptions::new("ffi", category, &desc_str);
246
247 match repo.inner.delete(&ref_name, &path, opts) {
248 Ok(id) => to_c_string(&id.to_string()),
249 Err(_) => ptr::null_mut(),
250 }
251}
252
253#[no_mangle]
255pub extern "C" fn agentstategraph_branch(
256 repo: *const SgRepo,
257 name: *const c_char,
258 from: *const c_char,
259) -> *mut c_char {
260 let repo = unsafe { repo.as_ref() };
261 let repo = match repo {
262 Some(r) => r,
263 None => return ptr::null_mut(),
264 };
265 let name = unsafe { c_to_str(name) };
266 let from = unsafe { c_to_str(from) };
267
268 match repo.inner.branch(&name, &from) {
269 Ok(id) => to_c_string(&id.to_string()),
270 Err(_) => ptr::null_mut(),
271 }
272}
273
274#[no_mangle]
279pub extern "C" fn agentstategraph_list_branches(
280 repo: *const SgRepo,
281 prefix: *const c_char,
282) -> *mut c_char {
283 let repo = match unsafe { repo.as_ref() } {
284 Some(r) => r,
285 None => return ptr::null_mut(),
286 };
287 let prefix_filter: Option<String> = if prefix.is_null() {
288 None
289 } else {
290 let s = unsafe { c_to_str(prefix) };
291 if s.is_empty() {
292 None
293 } else {
294 Some(s)
295 }
296 };
297 match repo.inner.list_branches(prefix_filter.as_deref()) {
298 Ok(entries) => {
299 let payload: Vec<serde_json::Value> = entries
300 .into_iter()
301 .map(|(name, id)| {
302 serde_json::json!({
303 "name": name,
304 "target": id.to_string(),
305 })
306 })
307 .collect();
308 json_ok(&payload)
309 }
310 Err(e) => json_err(&e.to_string()),
311 }
312}
313
314#[no_mangle]
317pub extern "C" fn agentstategraph_delete_branch(
318 repo: *const SgRepo,
319 name: *const c_char,
320) -> *mut c_char {
321 let repo = match unsafe { repo.as_ref() } {
322 Some(r) => r,
323 None => return ptr::null_mut(),
324 };
325 let name = unsafe { c_to_str(name) };
326 match repo.inner.delete_branch(&name) {
327 Ok(deleted) => to_c_string(&format!("{{\"deleted\":{}}}", deleted)),
328 Err(e) => json_err(&e.to_string()),
329 }
330}
331
332#[no_mangle]
334pub extern "C" fn agentstategraph_diff(
335 repo: *const SgRepo,
336 ref_a: *const c_char,
337 ref_b: *const c_char,
338) -> *mut c_char {
339 let repo = unsafe { repo.as_ref() };
340 let repo = match repo {
341 Some(r) => r,
342 None => return ptr::null_mut(),
343 };
344 let ref_a = unsafe { c_to_str(ref_a) };
345 let ref_b = unsafe { c_to_str(ref_b) };
346
347 match repo.inner.diff(&ref_a, &ref_b) {
348 Ok(ops) => to_c_string(&serde_json::to_string(&ops).unwrap_or_default()),
349 Err(_) => ptr::null_mut(),
350 }
351}
352
353#[no_mangle]
355pub extern "C" fn agentstategraph_merge(
356 repo: *const SgRepo,
357 source: *const c_char,
358 target: *const c_char,
359 description: *const c_char,
360) -> *mut c_char {
361 let repo = unsafe { repo.as_ref() };
362 let repo = match repo {
363 Some(r) => r,
364 None => return ptr::null_mut(),
365 };
366 let source = unsafe { c_to_str(source) };
367 let target = unsafe { c_to_str(target) };
368 let desc = unsafe { c_to_str(description) };
369
370 let opts = CommitOptions::new("ffi", IntentCategory::Merge, &desc);
371 match repo.inner.merge(&source, &target, opts) {
372 Ok(id) => to_c_string(&id.to_string()),
373 Err(e) => to_c_string(&format!("error:{}", e)),
374 }
375}
376
377#[no_mangle]
379pub extern "C" fn agentstategraph_log(
380 repo: *const SgRepo,
381 ref_name: *const c_char,
382 limit: u32,
383) -> *mut c_char {
384 let repo = unsafe { repo.as_ref() };
385 let repo = match repo {
386 Some(r) => r,
387 None => return ptr::null_mut(),
388 };
389 let ref_name = unsafe { c_to_str(ref_name) };
390
391 match repo.inner.log(&ref_name, limit as usize) {
392 Ok(commits) => {
393 let entries: Vec<serde_json::Value> = commits
394 .iter()
395 .map(|c| {
396 serde_json::json!({
397 "id": c.id.short(),
398 "agent": c.agent_id,
399 "intent_category": format!("{:?}", c.intent.category),
400 "intent_description": c.intent.description,
401 "reasoning": c.reasoning,
402 "confidence": c.confidence,
403 })
404 })
405 .collect();
406 to_c_string(&serde_json::to_string(&entries).unwrap_or_default())
407 }
408 Err(_) => ptr::null_mut(),
409 }
410}
411
412#[no_mangle]
414pub extern "C" fn agentstategraph_blame(
415 repo: *const SgRepo,
416 ref_name: *const c_char,
417 path: *const c_char,
418) -> *mut c_char {
419 let (repo, ref_name, path) = match unsafe { parse_repo_ref_path(repo, ref_name, path) } {
420 Some(v) => v,
421 None => return ptr::null_mut(),
422 };
423 match repo.inner.blame(&ref_name, &path) {
424 Ok(entry) => to_c_string(&serde_json::to_string(&entry).unwrap_or_default()),
425 Err(_) => ptr::null_mut(),
426 }
427}
428
429#[no_mangle]
434pub extern "C" fn agentstategraph_repository_capabilities() -> *mut c_char {
435 json_ok(&serde_json::json!({
436 "contract_version": 1,
437 "operations": [
438 "head", "set_cas", "query.commits",
439 "namespace.current", "namespace.create", "namespace.list", "namespace.delete",
440 "merge.base", "merge.preview", "merge.checked",
441 "explore.list_paths", "explore.get_tree", "explore.search_values",
442 "explore.stats", "explore.commit_graph", "explore.intent_tree",
443 "explore.history", "gc.dry_run", "gc.sweep", "gc.vacuum",
444 "spec.create", "spec.set", "spec.delete", "spec.compare", "spec.commit",
445 "spec.discard", "spec.list",
446 "session.create", "session.get", "session.list", "session.children",
447 "session.update_head", "session.end", "session.active.get", "session.active.set",
448 "epoch.create", "epoch.get", "epoch.list", "epoch.seal", "epoch.archive",
449 "epoch.export", "epoch.active.get", "epoch.active.set"
450 ]
451 }))
452}
453
454#[no_mangle]
458pub extern "C" fn agentstategraph_fork_namespace(
459 repo: *const SgRepo,
460 namespace: *const c_char,
461) -> *mut SgRepo {
462 let Some(repo) = (unsafe { repo.as_ref() }) else {
463 return ptr::null_mut();
464 };
465 let namespace = match Namespace::new(unsafe { c_to_str(namespace) }) {
466 Ok(ns) => ns,
467 Err(_) => return ptr::null_mut(),
468 };
469 let fork = repo.inner.fork_namespace(namespace);
470 if fork.init().is_err() {
471 return ptr::null_mut();
472 }
473 Box::into_raw(Box::new(SgRepo {
474 inner: Arc::new(fork),
475 }))
476}
477
478#[no_mangle]
485pub extern "C" fn agentstategraph_repository_call(
486 repo: *const SgRepo,
487 operation: *const c_char,
488 request_json: *const c_char,
489) -> *mut c_char {
490 let Some(repo) = (unsafe { repo.as_ref() }) else {
491 return ptr::null_mut();
492 };
493 let operation = unsafe { c_to_str(operation) };
494 let request: serde_json::Value = if request_json.is_null() {
495 serde_json::json!({})
496 } else {
497 match serde_json::from_str(&unsafe { c_to_str(request_json) }) {
498 Ok(serde_json::Value::Object(map)) => serde_json::Value::Object(map),
499 Ok(_) => return json_err("request_json must be a JSON object"),
500 Err(e) => return json_err(&format!("invalid request_json: {e}")),
501 }
502 };
503
504 match repository_call(&repo.inner, &operation, &request) {
505 Ok(value) => json_ok(&value),
506 Err(error) => json_err(&error),
507 }
508}
509
510fn repository_call(
511 repo: &Repository,
512 operation: &str,
513 request: &serde_json::Value,
514) -> Result<serde_json::Value, String> {
515 let s = |key: &str| request_string(request, key);
516 let optional = |key: &str| request_optional_string(request, key);
517 let ref_name = || optional("ref").unwrap_or_else(|| "main".to_string());
518 let commit_options = || -> Result<CommitOptions, String> {
519 let mut options = CommitOptions::new(
520 optional("agent_id").unwrap_or_else(|| "ffi".to_string()),
521 parse_category(&optional("category").unwrap_or_else(|| "checkpoint".to_string())),
522 s("description")?,
523 );
524 if let Some(reasoning) = optional("reasoning") {
525 options = options.with_reasoning(reasoning);
526 }
527 if let Some(confidence) = request.get("confidence").and_then(|v| v.as_f64()) {
528 options = options.with_confidence(confidence);
529 }
530 if request.get("tags").is_some() {
531 options = options.with_tags(request_string_array(request, "tags")?);
532 }
533 let tool_calls = request_tool_calls(request)?;
534 if !tool_calls.is_empty() {
535 options = options.with_tool_calls(tool_calls);
536 }
537 Ok(options)
538 };
539
540 match operation {
541 "head" => Ok(
542 serde_json::json!({ "head": repo.head(&ref_name()).map_err(err_string)?.to_string() }),
543 ),
544 "set_cas" => {
545 let expected = parse_object_id(&s("expected_head")?)?;
546 let value = request
547 .get("value")
548 .cloned()
549 .ok_or_else(|| "missing field: value".to_string())?;
550 let id = repo
551 .set_json_cas(
552 &ref_name(),
553 expected,
554 &s("path")?,
555 &value,
556 commit_options()?,
557 )
558 .map_err(err_string)?;
559 Ok(serde_json::json!({ "commit": id.to_string() }))
560 }
561 "query.commits" => {
562 let confidence_range = request
563 .get("confidence_min")
564 .and_then(|v| v.as_f64())
565 .zip(request.get("confidence_max").and_then(|v| v.as_f64()));
566 let filters = QueryFilters {
567 agent_id: optional("agent_id"),
568 intent_category: optional("intent_category"),
569 tags: request
570 .get("tags")
571 .map(|_| request_string_array(request, "tags"))
572 .transpose()?,
573 reasoning_contains: optional("reasoning_contains"),
574 confidence_range,
575 has_deviations: request.get("has_deviations").and_then(|v| v.as_bool()),
576 ..Default::default()
577 };
578 let commits = repo
579 .query_commits(
580 &ref_name(),
581 &filters,
582 request_usize(request, "limit").unwrap_or(50),
583 )
584 .map_err(err_string)?;
585 Ok(serde_json::Value::Array(commits.iter().map(|commit| serde_json::json!({
586 "id": commit.id.to_string(), "agent_id": commit.agent_id,
587 "intent_category": format!("{:?}", commit.intent.category),
588 "intent_description": commit.intent.description, "tags": commit.intent.tags,
589 "reasoning": commit.reasoning, "confidence": commit.confidence,
590 "parents": commit.parents.iter().map(ToString::to_string).collect::<Vec<_>>(),
591 "timestamp": commit.timestamp.to_rfc3339()
592 })).collect()))
593 }
594 "namespace.current" => Ok(serde_json::json!({ "namespace": repo.namespace().as_str() })),
595 "namespace.create" => {
596 let namespace = Namespace::new(s("name")?).map_err(err_string)?;
597 repo.create_namespace(namespace.as_str())
598 .map_err(err_string)?;
599 repo.fork_namespace(namespace).init().map_err(err_string)?;
600 Ok(serde_json::json!({ "created": true }))
601 }
602 "namespace.list" => Ok(
603 serde_json::to_value(repo.list_namespaces().map_err(err_string)?)
604 .map_err(err_string)?,
605 ),
606 "namespace.delete" => Ok(serde_json::json!({
607 "deleted": repo.delete_namespace(&s("name")?).map_err(err_string)?
608 })),
609 "merge.base" => Ok(serde_json::json!({
610 "commit": repo.merge_base(&s("source")?, &s("target")?).map_err(err_string)?.to_string()
611 })),
612 "merge.preview" => {
613 let p = repo
614 .preview_merge(&s("source")?, &s("target")?)
615 .map_err(err_string)?;
616 Ok(serde_json::json!({
617 "fast_forward": p.fast_forward, "added": p.added, "changed": p.changed,
618 "removed": p.removed, "conflicts": p.conflicts
619 }))
620 }
621 "merge.checked" => {
622 let id = repo
623 .merge_checked(
624 &s("source")?,
625 &s("target")?,
626 commit_options()?,
627 request
628 .get("allow_deletions")
629 .and_then(|v| v.as_bool())
630 .unwrap_or(false),
631 )
632 .map_err(err_string)?;
633 Ok(serde_json::json!({ "commit": id.to_string() }))
634 }
635 "explore.list_paths" => Ok(serde_json::to_value(
636 repo.list_paths(
637 &ref_name(),
638 &optional("prefix").unwrap_or_else(|| "/".to_string()),
639 request_usize(request, "max_depth"),
640 )
641 .map_err(err_string)?,
642 )
643 .map_err(err_string)?),
644 "explore.get_tree" => repo
645 .get_tree(
646 &ref_name(),
647 &optional("prefix").unwrap_or_else(|| "/".to_string()),
648 )
649 .map_err(err_string),
650 "explore.search_values" => {
651 let rows = repo
652 .search_values(
653 &ref_name(),
654 &s("query")?,
655 request_usize(request, "max_results"),
656 )
657 .map_err(err_string)?;
658 Ok(serde_json::Value::Array(
659 rows.into_iter()
660 .map(|(path, value)| serde_json::json!({ "path": path, "value": value }))
661 .collect(),
662 ))
663 }
664 "explore.stats" => repo.stats(&ref_name()).map_err(err_string),
665 "explore.commit_graph" => Ok(serde_json::Value::Array(
666 repo.commit_graph(&ref_name(), request_usize(request, "depth").unwrap_or(100))
667 .map_err(err_string)?,
668 )),
669 "explore.intent_tree" => repo
670 .intent_tree(&ref_name(), optional("root_commit_id").as_deref())
671 .map_err(err_string),
672 "explore.history" => repo
673 .history_report(
674 optional("namespace").as_deref(),
675 &optional("by").unwrap_or_else(|| "day".to_string()),
676 request_usize(request, "milestones").unwrap_or(50),
677 request
678 .get("refresh")
679 .and_then(|v| v.as_bool())
680 .unwrap_or(true),
681 request
682 .get("store")
683 .and_then(|v| v.as_bool())
684 .unwrap_or(false),
685 )
686 .map_err(err_string),
687 "gc.dry_run" => repo.gc_dry_run().map_err(err_string),
688 "gc.sweep" => {
689 let default = RetentionPolicy::default();
690 let policy = RetentionPolicy {
691 keep_recent: request_usize(request, "keep_recent").unwrap_or(default.keep_recent),
692 checkpoint_every: request_usize(request, "checkpoint_every")
693 .unwrap_or(default.checkpoint_every),
694 keep_milestones: request
695 .get("keep_milestones")
696 .and_then(|v| v.as_bool())
697 .unwrap_or(default.keep_milestones),
698 };
699 let mutate = request
700 .get("mutate")
701 .and_then(|v| v.as_bool())
702 .unwrap_or(false);
703 let vacuum = request
704 .get("vacuum")
705 .and_then(|v| v.as_bool())
706 .unwrap_or(false);
707 repo.gc_sweep(policy, mutate, vacuum).map_err(err_string)
708 }
709 "gc.vacuum" => repo.gc_vacuum().map_err(err_string),
710 "spec.create" => Ok(serde_json::json!({
711 "handle": repo.speculate(&ref_name(), optional("label")).map_err(err_string)?.id()
712 })),
713 "spec.set" => {
714 let value = request
715 .get("value")
716 .cloned()
717 .ok_or_else(|| "missing field: value".to_string())?;
718 repo.spec_set_json(spec_handle(request)?, &s("path")?, &value)
719 .map_err(err_string)?;
720 Ok(serde_json::json!({ "updated": true }))
721 }
722 "spec.delete" => {
723 repo.spec_delete(spec_handle(request)?, &s("path")?)
724 .map_err(err_string)?;
725 Ok(serde_json::json!({ "deleted": true }))
726 }
727 "spec.compare" => {
728 let ids = request
729 .get("handles")
730 .and_then(|v| v.as_array())
731 .ok_or_else(|| "missing field: handles".to_string())?;
732 let handles: Result<Vec<_>, _> = ids
733 .iter()
734 .map(|v| {
735 v.as_u64()
736 .map(SpecHandle::from_id)
737 .ok_or_else(|| "handles must contain integers".to_string())
738 })
739 .collect();
740 let comparison = repo.compare_speculations(&handles?).map_err(err_string)?;
741 Ok(serde_json::json!({
742 "base_ref": comparison.base_ref,
743 "entries": comparison.entries.into_iter().map(|entry| serde_json::json!({
744 "handle": entry.handle.id(), "label": entry.label, "diff": entry.diff_from_base
745 })).collect::<Vec<_>>()
746 }))
747 }
748 "spec.commit" => Ok(serde_json::json!({
749 "commit": repo.commit_speculation(spec_handle(request)?, commit_options()?)
750 .map_err(err_string)?.to_string()
751 })),
752 "spec.discard" => {
753 repo.discard_speculation(spec_handle(request)?)
754 .map_err(err_string)?;
755 Ok(serde_json::json!({ "discarded": true }))
756 }
757 "spec.list" => Ok(serde_json::Value::Array(
758 repo.list_speculations()
759 .into_iter()
760 .map(|(handle, label)| serde_json::json!({ "handle": handle.id(), "label": label }))
761 .collect(),
762 )),
763 "session.create" => {
764 let branch = optional("working_branch").unwrap_or_else(|| "main".to_string());
765 let namespace = match optional("scope_namespace") {
766 Some(name) => Some(Namespace::new(name).map_err(err_string)?),
767 None => None,
768 };
769 let session = repo
770 .sessions()
771 .create(
772 &s("agent_id")?,
773 &branch,
774 repo.head(&branch).map_err(err_string)?,
775 CreateSessionParams {
776 parent_session: optional("parent_session"),
777 delegated_intent: optional("delegated_intent"),
778 report_to: optional("report_to"),
779 path_scope: optional("path_scope"),
780 scope_namespace: namespace,
781 },
782 )
783 .map_err(err_string)?;
784 Ok(session_json(&session))
785 }
786 "session.get" => Ok(repo
787 .sessions()
788 .get(&s("id")?)
789 .map_err(err_string)?
790 .as_ref()
791 .map(session_json)
792 .unwrap_or(serde_json::Value::Null)),
793 "session.list" => Ok(serde_json::Value::Array(
794 repo.sessions()
795 .list(optional("agent_id").as_deref())
796 .map_err(err_string)?
797 .iter()
798 .map(session_json)
799 .collect(),
800 )),
801 "session.children" => Ok(serde_json::Value::Array(
802 repo.sessions()
803 .children(&s("parent_id")?)
804 .map_err(err_string)?
805 .iter()
806 .map(session_json)
807 .collect(),
808 )),
809 "session.update_head" => {
810 repo.sessions()
811 .update_head(&s("id")?, parse_object_id(&s("head")?)?)
812 .map_err(err_string)?;
813 Ok(serde_json::json!({ "updated": true }))
814 }
815 "session.end" => {
816 let status = match s("status")?.to_ascii_lowercase().as_str() {
817 "completed" => SessionStatus::Completed,
818 "abandoned" => SessionStatus::Abandoned,
819 _ => return Err("status must be completed or abandoned".to_string()),
820 };
821 repo.sessions().end(&s("id")?, status).map_err(err_string)?;
822 Ok(serde_json::json!({ "ended": true }))
823 }
824 "session.active.get" => {
825 Ok(serde_json::json!({ "session": repo.active_session().map_err(err_string)? }))
826 }
827 "session.active.set" => {
828 repo.set_active_session(optional("id"))
829 .map_err(err_string)?;
830 Ok(serde_json::json!({ "updated": true }))
831 }
832 "epoch.create" => Ok(epoch_json(
833 &repo
834 .create_epoch(
835 &s("id")?,
836 &s("description")?,
837 request_string_array(request, "root_intents")?,
838 )
839 .map_err(err_string)?,
840 )),
841 "epoch.get" => Ok(epoch_json(&repo.get_epoch(&s("id")?).map_err(err_string)?)),
842 "epoch.list" => Ok(serde_json::Value::Array(
843 repo.list_epochs()
844 .map_err(err_string)?
845 .iter()
846 .map(|entry| {
847 serde_json::json!({
848 "id": entry.id, "description": entry.description,
849 "status": format!("{:?}", entry.status),
850 "created_at": entry.created_at.to_rfc3339(),
851 "sealed_at": entry.sealed_at.map(|v| v.to_rfc3339()),
852 "root_intents": entry.root_intents, "agents": entry.agents,
853 "commit_count": entry.commit_count,
854 "seal_hash": entry.seal_hash.map(|v| v.to_string()), "tags": entry.tags
855 })
856 })
857 .collect(),
858 )),
859 "epoch.seal" => {
860 repo.seal_epoch(&s("id")?, &s("summary")?)
861 .map_err(err_string)?;
862 Ok(serde_json::json!({ "sealed": true }))
863 }
864 "epoch.archive" => {
865 repo.archive_epoch(&s("id")?).map_err(err_string)?;
866 Ok(serde_json::json!({ "archived": true }))
867 }
868 "epoch.export" => repo.export_epoch(&s("id")?).map_err(err_string),
869 "epoch.active.get" => {
870 Ok(serde_json::json!({ "epoch": repo.active_epoch().map_err(err_string)? }))
871 }
872 "epoch.active.set" => {
873 repo.set_active_epoch(optional("id")).map_err(err_string)?;
874 Ok(serde_json::json!({ "updated": true }))
875 }
876 _ => Err(format!("unsupported repository operation: {operation}")),
877 }
878}
879
880fn request_string(request: &serde_json::Value, key: &str) -> Result<String, String> {
881 request
882 .get(key)
883 .and_then(|v| v.as_str())
884 .map(str::to_owned)
885 .ok_or_else(|| format!("missing field: {key}"))
886}
887
888fn request_optional_string(request: &serde_json::Value, key: &str) -> Option<String> {
889 request.get(key).and_then(|v| v.as_str()).map(str::to_owned)
890}
891
892fn request_usize(request: &serde_json::Value, key: &str) -> Option<usize> {
893 request
894 .get(key)
895 .and_then(|v| v.as_u64())
896 .and_then(|v| usize::try_from(v).ok())
897}
898
899fn request_string_array(request: &serde_json::Value, key: &str) -> Result<Vec<String>, String> {
900 request
901 .get(key)
902 .and_then(|v| v.as_array())
903 .ok_or_else(|| format!("missing field: {key}"))?
904 .iter()
905 .map(|v| {
906 v.as_str()
907 .map(str::to_owned)
908 .ok_or_else(|| format!("{key} must contain strings"))
909 })
910 .collect()
911}
912
913fn request_tool_calls(request: &serde_json::Value) -> Result<Vec<ToolCall>, String> {
918 let Some(array) = request.get("tool_calls") else {
919 return Ok(Vec::new());
920 };
921 let array = array
922 .as_array()
923 .ok_or_else(|| "tool_calls must be an array".to_string())?;
924 array
925 .iter()
926 .map(|entry| {
927 let tool_name = entry
928 .get("tool_name")
929 .and_then(|v| v.as_str())
930 .ok_or_else(|| "each tool_call needs a tool_name".to_string())?
931 .to_owned();
932 let timestamp = match entry.get("timestamp").and_then(|v| v.as_str()) {
933 Some(s) => chrono::DateTime::parse_from_rfc3339(s)
934 .map_err(|e| format!("invalid tool_call timestamp: {e}"))?
935 .with_timezone(&chrono::Utc),
936 None => chrono::Utc::now(),
937 };
938 Ok(ToolCall {
939 tool_name,
940 arguments: entry
941 .get("arguments")
942 .cloned()
943 .unwrap_or(serde_json::Value::Null),
944 result: entry
945 .get("result")
946 .and_then(|v| v.as_str())
947 .map(str::to_owned),
948 timestamp,
949 })
950 })
951 .collect()
952}
953
954fn parse_object_id(value: &str) -> Result<ObjectId, String> {
955 ObjectId::from_hex(value).ok_or_else(|| format!("invalid object id: {value}"))
956}
957
958fn spec_handle(request: &serde_json::Value) -> Result<SpecHandle, String> {
959 request
960 .get("handle")
961 .and_then(|v| v.as_u64())
962 .map(SpecHandle::from_id)
963 .ok_or_else(|| "missing field: handle".to_string())
964}
965
966fn err_string(error: impl std::fmt::Display) -> String {
967 error.to_string()
968}
969
970fn session_json(session: &agentstategraph::Session) -> serde_json::Value {
971 serde_json::json!({
972 "id": session.id, "agent_id": session.agent_id,
973 "working_branch": session.working_branch, "head": session.head.to_string(),
974 "parent_session": session.parent_session,
975 "delegated_intent": session.delegated_intent, "report_to": session.report_to,
976 "path_scope": session.path_scope, "scope_tenant": session.scope_tenant,
977 "scope_namespace": session.scope_namespace.as_ref().map(|v| v.as_str()),
978 "status": format!("{:?}", session.status),
979 "created_at": session.created_at.to_rfc3339(),
980 "ended_at": session.ended_at.map(|v| v.to_rfc3339())
981 })
982}
983
984fn epoch_json(epoch: &agentstategraph_core::Epoch) -> serde_json::Value {
985 serde_json::json!({
986 "id": epoch.id, "description": epoch.description,
987 "root_intents": epoch.root_intents, "status": format!("{:?}", epoch.status),
988 "created_at": epoch.created_at.to_rfc3339(),
989 "sealed_at": epoch.sealed_at.map(|v| v.to_rfc3339()),
990 "seal_summary": epoch.seal_summary,
991 "seal_hash": epoch.seal_hash.map(|v| v.to_string()),
992 "commits": epoch.commits.iter().map(ToString::to_string).collect::<Vec<_>>(),
993 "agents": epoch.agents, "branches": epoch.branches, "tags": epoch.tags,
994 "sealed_commits": epoch.sealed_commits.iter().map(ToString::to_string).collect::<Vec<_>>()
995 })
996}
997
998#[no_mangle]
1007pub extern "C" fn agentstategraph_taskstore_new(
1008 repo: *const SgRepo,
1009 prefix: *const c_char,
1010 agent_id: *const c_char,
1011) -> *mut SgTaskStore {
1012 let repo = unsafe { repo.as_ref() };
1013 let repo = match repo {
1014 Some(r) => r,
1015 None => return ptr::null_mut(),
1016 };
1017 let prefix = unsafe { c_to_str(prefix) };
1018 let agent_id = unsafe { c_to_str(agent_id) };
1019 if prefix.is_empty() || agent_id.is_empty() {
1020 return ptr::null_mut();
1021 }
1022 let store = TaskStore::new(repo.inner.clone(), prefix, agent_id);
1023 Box::into_raw(Box::new(SgTaskStore { inner: store }))
1024}
1025
1026#[no_mangle]
1028pub extern "C" fn agentstategraph_taskstore_free(store: *mut SgTaskStore) {
1029 if !store.is_null() {
1030 unsafe {
1031 drop(Box::from_raw(store));
1032 }
1033 }
1034}
1035
1036fn taskstore_ref<'a>(store: *const SgTaskStore) -> Option<&'a SgTaskStore> {
1037 unsafe { store.as_ref() }
1038}
1039
1040fn json_ok<T: Serialize>(v: &T) -> *mut c_char {
1041 match serde_json::to_string(v) {
1042 Ok(s) => to_c_string(&s),
1043 Err(_) => ptr::null_mut(),
1044 }
1045}
1046
1047fn json_err(msg: &str) -> *mut c_char {
1048 to_c_string(&format!(
1049 "{{\"error\":{}}}",
1050 serde_json::to_string(msg).unwrap_or_else(|_| "\"error\"".into())
1051 ))
1052}
1053
1054#[no_mangle]
1058pub extern "C" fn agentstategraph_taskstore_create_plan(
1059 store: *const SgTaskStore,
1060 ref_name: *const c_char,
1061 name: *const c_char,
1062 description: *const c_char,
1063) -> *mut c_char {
1064 let Some(store) = taskstore_ref(store) else {
1065 return ptr::null_mut();
1066 };
1067 let ref_name = unsafe { c_to_str(ref_name) };
1068 let name = unsafe { c_to_str(name) };
1069 let description = if description.is_null() {
1070 None
1071 } else {
1072 let s = unsafe { c_to_str(description) };
1073 if s.is_empty() {
1074 None
1075 } else {
1076 Some(s)
1077 }
1078 };
1079 match store.inner.create_plan(&ref_name, &name, description) {
1080 Ok(plan) => json_ok(&plan),
1081 Err(e) => json_err(&e.to_string()),
1082 }
1083}
1084
1085#[no_mangle]
1087pub extern "C" fn agentstategraph_taskstore_list_plans(
1088 store: *const SgTaskStore,
1089 ref_name: *const c_char,
1090) -> *mut c_char {
1091 let Some(store) = taskstore_ref(store) else {
1092 return ptr::null_mut();
1093 };
1094 let ref_name = unsafe { c_to_str(ref_name) };
1095 match store.inner.list_plans(&ref_name) {
1096 Ok(p) => json_ok(&p),
1097 Err(e) => json_err(&e.to_string()),
1098 }
1099}
1100
1101#[no_mangle]
1104pub extern "C" fn agentstategraph_taskstore_list_plans_by_status(
1105 store: *const SgTaskStore,
1106 ref_name: *const c_char,
1107 status: *const c_char,
1108) -> *mut c_char {
1109 let Some(store) = taskstore_ref(store) else {
1110 return ptr::null_mut();
1111 };
1112 let ref_name = unsafe { c_to_str(ref_name) };
1113 let status_str = unsafe { c_to_str(status) };
1114 let parsed = parse_plan_status(&status_str);
1115 match store.inner.list_plans_by_status(&ref_name, parsed) {
1116 Ok(p) => json_ok(&p),
1117 Err(e) => json_err(&e.to_string()),
1118 }
1119}
1120
1121#[no_mangle]
1123pub extern "C" fn agentstategraph_taskstore_get_plan(
1124 store: *const SgTaskStore,
1125 ref_name: *const c_char,
1126 name: *const c_char,
1127) -> *mut c_char {
1128 let Some(store) = taskstore_ref(store) else {
1129 return ptr::null_mut();
1130 };
1131 let ref_name = unsafe { c_to_str(ref_name) };
1132 let name = unsafe { c_to_str(name) };
1133 match store.inner.get_plan(&ref_name, &name) {
1134 Ok(p) => json_ok(&p),
1135 Err(e) => json_err(&e.to_string()),
1136 }
1137}
1138
1139#[no_mangle]
1140pub extern "C" fn agentstategraph_taskstore_archive_plan(
1141 store: *const SgTaskStore,
1142 ref_name: *const c_char,
1143 name: *const c_char,
1144) -> *mut c_char {
1145 let Some(store) = taskstore_ref(store) else {
1146 return ptr::null_mut();
1147 };
1148 let ref_name = unsafe { c_to_str(ref_name) };
1149 let name = unsafe { c_to_str(name) };
1150 match store.inner.archive_plan(&ref_name, &name) {
1151 Ok(p) => json_ok(&p),
1152 Err(e) => json_err(&e.to_string()),
1153 }
1154}
1155
1156#[no_mangle]
1157pub extern "C" fn agentstategraph_taskstore_delete_plan(
1158 store: *const SgTaskStore,
1159 ref_name: *const c_char,
1160 name: *const c_char,
1161) -> *mut c_char {
1162 let Some(store) = taskstore_ref(store) else {
1163 return ptr::null_mut();
1164 };
1165 let ref_name = unsafe { c_to_str(ref_name) };
1166 let name = unsafe { c_to_str(name) };
1167 match store.inner.delete_plan(&ref_name, &name) {
1168 Ok(()) => to_c_string("{\"ok\":true}"),
1169 Err(e) => json_err(&e.to_string()),
1170 }
1171}
1172
1173#[no_mangle]
1181pub extern "C" fn agentstategraph_taskstore_add_task(
1182 store: *const SgTaskStore,
1183 ref_name: *const c_char,
1184 plan: *const c_char,
1185 title: *const c_char,
1186 priority: *const c_char,
1187 parent_id: *const c_char,
1188 blockers_json: *const c_char,
1189 assigned_to: *const c_char,
1190) -> *mut c_char {
1191 let Some(store) = taskstore_ref(store) else {
1192 return ptr::null_mut();
1193 };
1194 let ref_name = unsafe { c_to_str(ref_name) };
1195 let plan = unsafe { c_to_str(plan) };
1196 let title = unsafe { c_to_str(title) };
1197 let priority_str = unsafe { c_to_str(priority) };
1198 let prio = parse_priority(&priority_str);
1199
1200 let parent = if parent_id.is_null() {
1201 None
1202 } else {
1203 let s = unsafe { c_to_str(parent_id) };
1204 if s.is_empty() {
1205 None
1206 } else {
1207 Some(TaskId(s))
1208 }
1209 };
1210
1211 let blockers: Vec<TaskId> = if blockers_json.is_null() {
1212 Vec::new()
1213 } else {
1214 let s = unsafe { c_to_str(blockers_json) };
1215 if s.is_empty() {
1216 Vec::new()
1217 } else {
1218 match serde_json::from_str::<Vec<String>>(&s) {
1219 Ok(v) => v.into_iter().map(TaskId).collect(),
1220 Err(e) => return json_err(&format!("invalid blockers_json: {e}")),
1221 }
1222 }
1223 };
1224
1225 let assigned = if assigned_to.is_null() {
1226 None
1227 } else {
1228 let s = unsafe { c_to_str(assigned_to) };
1229 if s.is_empty() {
1230 None
1231 } else {
1232 Some(s)
1233 }
1234 };
1235
1236 match store
1237 .inner
1238 .add_task(&ref_name, &plan, &title, prio, parent, blockers, assigned)
1239 {
1240 Ok(t) => json_ok(&t),
1241 Err(e) => json_err(&e.to_string()),
1242 }
1243}
1244
1245#[no_mangle]
1251#[allow(clippy::too_many_arguments)]
1252pub extern "C" fn agentstategraph_taskstore_add_task_ex(
1253 store: *const SgTaskStore,
1254 ref_name: *const c_char,
1255 plan: *const c_char,
1256 title: *const c_char,
1257 priority: *const c_char,
1258 parent_id: *const c_char,
1259 blockers_json: *const c_char,
1260 assigned_to: *const c_char,
1261 payload_json: *const c_char,
1262 parent_change: *const c_char,
1263 on_complete_json: *const c_char,
1264) -> *mut c_char {
1265 let Some(store) = taskstore_ref(store) else {
1266 return ptr::null_mut();
1267 };
1268 let ref_name = unsafe { c_to_str(ref_name) };
1269 let plan = unsafe { c_to_str(plan) };
1270 let title = unsafe { c_to_str(title) };
1271 let priority_str = unsafe { c_to_str(priority) };
1272 let prio = parse_priority(&priority_str);
1273
1274 let parent = if parent_id.is_null() {
1275 None
1276 } else {
1277 let s = unsafe { c_to_str(parent_id) };
1278 if s.is_empty() {
1279 None
1280 } else {
1281 Some(TaskId(s))
1282 }
1283 };
1284
1285 let blockers: Vec<TaskId> = if blockers_json.is_null() {
1286 Vec::new()
1287 } else {
1288 let s = unsafe { c_to_str(blockers_json) };
1289 if s.is_empty() {
1290 Vec::new()
1291 } else {
1292 match serde_json::from_str::<Vec<String>>(&s) {
1293 Ok(v) => v.into_iter().map(TaskId).collect(),
1294 Err(e) => return json_err(&format!("invalid blockers_json: {e}")),
1295 }
1296 }
1297 };
1298
1299 let assigned = if assigned_to.is_null() {
1300 None
1301 } else {
1302 let s = unsafe { c_to_str(assigned_to) };
1303 if s.is_empty() {
1304 None
1305 } else {
1306 Some(s)
1307 }
1308 };
1309
1310 let payload: Option<serde_json::Value> = if payload_json.is_null() {
1311 None
1312 } else {
1313 let s = unsafe { c_to_str(payload_json) };
1314 if s.is_empty() {
1315 None
1316 } else {
1317 match serde_json::from_str(&s) {
1318 Ok(v) => Some(v),
1319 Err(e) => return json_err(&format!("invalid payload_json: {e}")),
1320 }
1321 }
1322 };
1323
1324 let parent_change_opt: Option<String> = if parent_change.is_null() {
1325 None
1326 } else {
1327 let s = unsafe { c_to_str(parent_change) };
1328 if s.is_empty() {
1329 None
1330 } else {
1331 Some(s)
1332 }
1333 };
1334
1335 let on_complete_opt: Option<agentstategraph_tasks::OnCompleteHook> =
1336 if on_complete_json.is_null() {
1337 None
1338 } else {
1339 let s = unsafe { c_to_str(on_complete_json) };
1340 if s.is_empty() {
1341 None
1342 } else {
1343 match serde_json::from_str(&s) {
1344 Ok(v) => Some(v),
1345 Err(e) => return json_err(&format!("invalid on_complete_json: {e}")),
1346 }
1347 }
1348 };
1349
1350 match store.inner.add_task_with_extensions(
1351 &ref_name,
1352 &plan,
1353 &title,
1354 prio,
1355 parent,
1356 blockers,
1357 assigned,
1358 AddTaskOptions {
1359 payload,
1360 parent_change: parent_change_opt,
1361 on_complete: on_complete_opt,
1362 },
1363 ) {
1364 Ok(t) => json_ok(&t),
1365 Err(e) => json_err(&e.to_string()),
1366 }
1367}
1368
1369#[no_mangle]
1370pub extern "C" fn agentstategraph_taskstore_list_tasks(
1371 store: *const SgTaskStore,
1372 ref_name: *const c_char,
1373 plan: *const c_char,
1374) -> *mut c_char {
1375 let Some(store) = taskstore_ref(store) else {
1376 return ptr::null_mut();
1377 };
1378 let ref_name = unsafe { c_to_str(ref_name) };
1379 let plan = unsafe { c_to_str(plan) };
1380 match store.inner.list_tasks(&ref_name, &plan) {
1381 Ok(t) => json_ok(&t),
1382 Err(e) => json_err(&e.to_string()),
1383 }
1384}
1385
1386#[no_mangle]
1387pub extern "C" fn agentstategraph_taskstore_task_ids(
1388 store: *const SgTaskStore,
1389 ref_name: *const c_char,
1390 plan: *const c_char,
1391) -> *mut c_char {
1392 let Some(store) = taskstore_ref(store) else {
1393 return ptr::null_mut();
1394 };
1395 let ref_name = unsafe { c_to_str(ref_name) };
1396 let plan = unsafe { c_to_str(plan) };
1397 match store.inner.task_ids(&ref_name, &plan) {
1398 Ok(ids) => {
1399 let strs: Vec<String> = ids.into_iter().map(|i| i.0).collect();
1400 json_ok(&strs)
1401 }
1402 Err(e) => json_err(&e.to_string()),
1403 }
1404}
1405
1406#[no_mangle]
1407pub extern "C" fn agentstategraph_taskstore_get_task(
1408 store: *const SgTaskStore,
1409 ref_name: *const c_char,
1410 plan: *const c_char,
1411 task_id: *const c_char,
1412) -> *mut c_char {
1413 let Some(store) = taskstore_ref(store) else {
1414 return ptr::null_mut();
1415 };
1416 let ref_name = unsafe { c_to_str(ref_name) };
1417 let plan = unsafe { c_to_str(plan) };
1418 let id = TaskId(unsafe { c_to_str(task_id) });
1419 match store.inner.get_task(&ref_name, &plan, &id) {
1420 Ok(t) => json_ok(&t),
1421 Err(e) => json_err(&e.to_string()),
1422 }
1423}
1424
1425#[no_mangle]
1426pub extern "C" fn agentstategraph_taskstore_start_task(
1427 store: *const SgTaskStore,
1428 ref_name: *const c_char,
1429 plan: *const c_char,
1430 task_id: *const c_char,
1431) -> *mut c_char {
1432 let Some(store) = taskstore_ref(store) else {
1433 return ptr::null_mut();
1434 };
1435 let ref_name = unsafe { c_to_str(ref_name) };
1436 let plan = unsafe { c_to_str(plan) };
1437 let id = TaskId(unsafe { c_to_str(task_id) });
1438 match store.inner.start_task(&ref_name, &plan, &id) {
1439 Ok(t) => json_ok(&t),
1440 Err(e) => json_err(&e.to_string()),
1441 }
1442}
1443
1444#[no_mangle]
1446pub extern "C" fn agentstategraph_taskstore_complete_task(
1447 store: *const SgTaskStore,
1448 ref_name: *const c_char,
1449 plan: *const c_char,
1450 task_id: *const c_char,
1451 proof_kind: *const c_char,
1452 proof_value: *const c_char,
1453 proof_note: *const c_char,
1454) -> *mut c_char {
1455 let Some(store) = taskstore_ref(store) else {
1456 return ptr::null_mut();
1457 };
1458 let ref_name = unsafe { c_to_str(ref_name) };
1459 let plan = unsafe { c_to_str(plan) };
1460 let id = TaskId(unsafe { c_to_str(task_id) });
1461 let kind_str = unsafe { c_to_str(proof_kind) };
1462 let value = unsafe { c_to_str(proof_value) };
1463 let kind = match parse_proof_kind(&kind_str) {
1464 Some(k) => k,
1465 None => return json_err(&format!("invalid proof kind: {kind_str}")),
1466 };
1467 let note = if proof_note.is_null() {
1468 None
1469 } else {
1470 let s = unsafe { c_to_str(proof_note) };
1471 if s.is_empty() {
1472 None
1473 } else {
1474 Some(s)
1475 }
1476 };
1477 let proof = Proof { kind, value, note };
1478 match store.inner.complete_task(&ref_name, &plan, &id, proof) {
1479 Ok(t) => json_ok(&t),
1480 Err(e) => json_err(&e.to_string()),
1481 }
1482}
1483
1484#[no_mangle]
1485pub extern "C" fn agentstategraph_taskstore_abandon_task(
1486 store: *const SgTaskStore,
1487 ref_name: *const c_char,
1488 plan: *const c_char,
1489 task_id: *const c_char,
1490 reason: *const c_char,
1491) -> *mut c_char {
1492 let Some(store) = taskstore_ref(store) else {
1493 return ptr::null_mut();
1494 };
1495 let ref_name = unsafe { c_to_str(ref_name) };
1496 let plan = unsafe { c_to_str(plan) };
1497 let id = TaskId(unsafe { c_to_str(task_id) });
1498 let reason = unsafe { c_to_str(reason) };
1499 match store.inner.abandon_task(&ref_name, &plan, &id, &reason) {
1500 Ok(t) => json_ok(&t),
1501 Err(e) => json_err(&e.to_string()),
1502 }
1503}
1504
1505#[no_mangle]
1506pub extern "C" fn agentstategraph_taskstore_set_priority(
1507 store: *const SgTaskStore,
1508 ref_name: *const c_char,
1509 plan: *const c_char,
1510 task_id: *const c_char,
1511 priority: *const c_char,
1512) -> *mut c_char {
1513 let Some(store) = taskstore_ref(store) else {
1514 return ptr::null_mut();
1515 };
1516 let ref_name = unsafe { c_to_str(ref_name) };
1517 let plan = unsafe { c_to_str(plan) };
1518 let id = TaskId(unsafe { c_to_str(task_id) });
1519 let prio = parse_priority(&unsafe { c_to_str(priority) });
1520 match store.inner.set_priority(&ref_name, &plan, &id, prio) {
1521 Ok(t) => json_ok(&t),
1522 Err(e) => json_err(&e.to_string()),
1523 }
1524}
1525
1526#[no_mangle]
1528pub extern "C" fn agentstategraph_taskstore_set_title(
1529 store: *const SgTaskStore,
1530 ref_name: *const c_char,
1531 plan: *const c_char,
1532 task_id: *const c_char,
1533 title: *const c_char,
1534) -> *mut c_char {
1535 let Some(store) = taskstore_ref(store) else {
1536 return ptr::null_mut();
1537 };
1538 let ref_name = unsafe { c_to_str(ref_name) };
1539 let plan = unsafe { c_to_str(plan) };
1540 let id = TaskId(unsafe { c_to_str(task_id) });
1541 let title = unsafe { c_to_str(title) };
1542 match store.inner.set_title(&ref_name, &plan, &id, &title) {
1543 Ok(t) => json_ok(&t),
1544 Err(e) => json_err(&e.to_string()),
1545 }
1546}
1547
1548#[no_mangle]
1550pub extern "C" fn agentstategraph_taskstore_set_blockers(
1551 store: *const SgTaskStore,
1552 ref_name: *const c_char,
1553 plan: *const c_char,
1554 task_id: *const c_char,
1555 blockers_json: *const c_char,
1556) -> *mut c_char {
1557 let Some(store) = taskstore_ref(store) else {
1558 return ptr::null_mut();
1559 };
1560 let ref_name = unsafe { c_to_str(ref_name) };
1561 let plan = unsafe { c_to_str(plan) };
1562 let id = TaskId(unsafe { c_to_str(task_id) });
1563 let blockers: Vec<TaskId> = if blockers_json.is_null() {
1564 Vec::new()
1565 } else {
1566 let s = unsafe { c_to_str(blockers_json) };
1567 if s.is_empty() {
1568 Vec::new()
1569 } else {
1570 match serde_json::from_str::<Vec<String>>(&s) {
1571 Ok(v) => v.into_iter().map(TaskId).collect(),
1572 Err(e) => return json_err(&format!("invalid blockers_json: {e}")),
1573 }
1574 }
1575 };
1576 match store.inner.set_blockers(&ref_name, &plan, &id, blockers) {
1577 Ok(t) => json_ok(&t),
1578 Err(e) => json_err(&e.to_string()),
1579 }
1580}
1581
1582#[no_mangle]
1583pub extern "C" fn agentstategraph_taskstore_assign_task(
1584 store: *const SgTaskStore,
1585 ref_name: *const c_char,
1586 plan: *const c_char,
1587 task_id: *const c_char,
1588 agent: *const c_char,
1589) -> *mut c_char {
1590 let Some(store) = taskstore_ref(store) else {
1591 return ptr::null_mut();
1592 };
1593 let ref_name = unsafe { c_to_str(ref_name) };
1594 let plan = unsafe { c_to_str(plan) };
1595 let id = TaskId(unsafe { c_to_str(task_id) });
1596 let agent = unsafe { c_to_str(agent) };
1597 match store.inner.assign_task(&ref_name, &plan, &id, &agent) {
1598 Ok(t) => json_ok(&t),
1599 Err(e) => json_err(&e.to_string()),
1600 }
1601}
1602
1603#[no_mangle]
1604pub extern "C" fn agentstategraph_taskstore_unassign_task(
1605 store: *const SgTaskStore,
1606 ref_name: *const c_char,
1607 plan: *const c_char,
1608 task_id: *const c_char,
1609) -> *mut c_char {
1610 let Some(store) = taskstore_ref(store) else {
1611 return ptr::null_mut();
1612 };
1613 let ref_name = unsafe { c_to_str(ref_name) };
1614 let plan = unsafe { c_to_str(plan) };
1615 let id = TaskId(unsafe { c_to_str(task_id) });
1616 match store.inner.unassign_task(&ref_name, &plan, &id) {
1617 Ok(t) => json_ok(&t),
1618 Err(e) => json_err(&e.to_string()),
1619 }
1620}
1621
1622#[no_mangle]
1624pub extern "C" fn agentstategraph_taskstore_next_task(
1625 store: *const SgTaskStore,
1626 ref_name: *const c_char,
1627 plan: *const c_char,
1628) -> *mut c_char {
1629 let Some(store) = taskstore_ref(store) else {
1630 return ptr::null_mut();
1631 };
1632 let ref_name = unsafe { c_to_str(ref_name) };
1633 let plan = unsafe { c_to_str(plan) };
1634 match store.inner.next_task(&ref_name, &plan) {
1635 Ok(opt) => json_ok(&opt),
1636 Err(e) => json_err(&e.to_string()),
1637 }
1638}
1639
1640#[no_mangle]
1643pub extern "C" fn agentstategraph_taskstore_next_task_for(
1644 store: *const SgTaskStore,
1645 ref_name: *const c_char,
1646 plan: *const c_char,
1647 agent: *const c_char,
1648 include_unassigned: u8,
1649) -> *mut c_char {
1650 let Some(store) = taskstore_ref(store) else {
1651 return ptr::null_mut();
1652 };
1653 let ref_name = unsafe { c_to_str(ref_name) };
1654 let plan = unsafe { c_to_str(plan) };
1655 let agent_str = if agent.is_null() {
1656 None
1657 } else {
1658 let s = unsafe { c_to_str(agent) };
1659 if s.is_empty() {
1660 None
1661 } else {
1662 Some(s)
1663 }
1664 };
1665 match store.inner.next_task_for(
1666 &ref_name,
1667 &plan,
1668 agent_str.as_deref(),
1669 include_unassigned != 0,
1670 ) {
1671 Ok(opt) => json_ok(&opt),
1672 Err(e) => json_err(&e.to_string()),
1673 }
1674}
1675
1676#[no_mangle]
1679pub extern "C" fn agentstategraph_taskstore_derived_status(
1680 store: *const SgTaskStore,
1681 ref_name: *const c_char,
1682 plan: *const c_char,
1683 parent_id: *const c_char,
1684) -> *mut c_char {
1685 let Some(store) = taskstore_ref(store) else {
1686 return ptr::null_mut();
1687 };
1688 let ref_name = unsafe { c_to_str(ref_name) };
1689 let plan = unsafe { c_to_str(plan) };
1690 let id = TaskId(unsafe { c_to_str(parent_id) });
1691 match store.inner.derived_status(&ref_name, &plan, &id) {
1692 Ok(s) => json_ok(&s),
1693 Err(e) => json_err(&e.to_string()),
1694 }
1695}
1696
1697#[no_mangle]
1707pub extern "C" fn agentstategraph_policy_store_new(
1708 repo: *const SgRepo,
1709 prefix: *const c_char,
1710 agent_id: *const c_char,
1711) -> *mut SgPolicyStore {
1712 let repo = unsafe { repo.as_ref() };
1713 let repo = match repo {
1714 Some(r) => r,
1715 None => return ptr::null_mut(),
1716 };
1717 let prefix = unsafe { c_to_str(prefix) };
1718 let agent_id = unsafe { c_to_str(agent_id) };
1719 if prefix.is_empty() || agent_id.is_empty() {
1720 return ptr::null_mut();
1721 }
1722 let store = PolicyStore::new(repo.inner.clone(), prefix, agent_id);
1723 Box::into_raw(Box::new(SgPolicyStore { inner: store }))
1724}
1725
1726#[no_mangle]
1728pub extern "C" fn agentstategraph_policy_store_free(store: *mut SgPolicyStore) {
1729 if !store.is_null() {
1730 unsafe {
1731 drop(Box::from_raw(store));
1732 }
1733 }
1734}
1735
1736fn policystore_ref<'a>(store: *const SgPolicyStore) -> Option<&'a SgPolicyStore> {
1737 unsafe { store.as_ref() }
1738}
1739
1740fn opt_c_to_str(ptr: *const c_char) -> Option<String> {
1741 if ptr.is_null() {
1742 None
1743 } else {
1744 let s = unsafe { c_to_str(ptr) };
1745 if s.is_empty() {
1746 None
1747 } else {
1748 Some(s)
1749 }
1750 }
1751}
1752
1753#[no_mangle]
1757pub extern "C" fn agentstategraph_policy_propose(
1758 store: *const SgPolicyStore,
1759 ref_name: *const c_char,
1760 policy_json: *const c_char,
1761) -> *mut c_char {
1762 let Some(store) = policystore_ref(store) else {
1763 return ptr::null_mut();
1764 };
1765 let ref_name = unsafe { c_to_str(ref_name) };
1766 let policy_str = unsafe { c_to_str(policy_json) };
1767 let policy: Policy = match serde_json::from_str(&policy_str) {
1768 Ok(p) => p,
1769 Err(e) => return json_err(&format!("invalid policy: {e}")),
1770 };
1771 match store.inner.propose(&ref_name, policy) {
1772 Ok(handle) => json_ok(&handle),
1773 Err(e) => json_err(&e.to_string()),
1774 }
1775}
1776
1777#[no_mangle]
1780pub extern "C" fn agentstategraph_policy_ratify(
1781 store: *const SgPolicyStore,
1782 ref_name: *const c_char,
1783 path: *const c_char,
1784 ratifier: *const c_char,
1785 reasoning: *const c_char,
1786) -> *mut c_char {
1787 let Some(store) = policystore_ref(store) else {
1788 return ptr::null_mut();
1789 };
1790 let ref_name = unsafe { c_to_str(ref_name) };
1791 let path = unsafe { c_to_str(path) };
1792 let ratifier = unsafe { c_to_str(ratifier) };
1793 let reasoning = unsafe { c_to_str(reasoning) };
1794 match store.inner.ratify(&ref_name, &path, &ratifier, &reasoning) {
1795 Ok(()) => to_c_string("{\"ok\":true}"),
1796 Err(e) => json_err(&e.to_string()),
1797 }
1798}
1799
1800#[no_mangle]
1803pub extern "C" fn agentstategraph_policy_supersede(
1804 store: *const SgPolicyStore,
1805 ref_name: *const c_char,
1806 path: *const c_char,
1807 new_policy_json: *const c_char,
1808) -> *mut c_char {
1809 let Some(store) = policystore_ref(store) else {
1810 return ptr::null_mut();
1811 };
1812 let ref_name = unsafe { c_to_str(ref_name) };
1813 let path = unsafe { c_to_str(path) };
1814 let policy_str = unsafe { c_to_str(new_policy_json) };
1815 let policy: Policy = match serde_json::from_str(&policy_str) {
1816 Ok(p) => p,
1817 Err(e) => return json_err(&format!("invalid policy: {e}")),
1818 };
1819 match store.inner.supersede(&ref_name, &path, policy) {
1820 Ok(handle) => json_ok(&handle),
1821 Err(e) => json_err(&e.to_string()),
1822 }
1823}
1824
1825#[no_mangle]
1828pub extern "C" fn agentstategraph_policy_list(
1829 store: *const SgPolicyStore,
1830 ref_name: *const c_char,
1831 prefix_or_null: *const c_char,
1832) -> *mut c_char {
1833 let Some(store) = policystore_ref(store) else {
1834 return ptr::null_mut();
1835 };
1836 let ref_name = unsafe { c_to_str(ref_name) };
1837 let filter = opt_c_to_str(prefix_or_null);
1838 match store.inner.list(&ref_name, filter.as_deref()) {
1839 Ok(ps) => json_ok(&ps),
1840 Err(e) => json_err(&e.to_string()),
1841 }
1842}
1843
1844#[no_mangle]
1848pub extern "C" fn agentstategraph_policy_active(
1849 store: *const SgPolicyStore,
1850 ref_name: *const c_char,
1851 prefix_or_null: *const c_char,
1852) -> *mut c_char {
1853 let Some(store) = policystore_ref(store) else {
1854 return ptr::null_mut();
1855 };
1856 let ref_name = unsafe { c_to_str(ref_name) };
1857 let filter = opt_c_to_str(prefix_or_null);
1858 match store.inner.active(&ref_name, filter.as_deref()) {
1859 Ok(ps) => json_ok(&ps),
1860 Err(e) => json_err(&e.to_string()),
1861 }
1862}
1863
1864#[no_mangle]
1866pub extern "C" fn agentstategraph_policy_get(
1867 store: *const SgPolicyStore,
1868 ref_name: *const c_char,
1869 path: *const c_char,
1870) -> *mut c_char {
1871 let Some(store) = policystore_ref(store) else {
1872 return ptr::null_mut();
1873 };
1874 let ref_name = unsafe { c_to_str(ref_name) };
1875 let path = unsafe { c_to_str(path) };
1876 match store.inner.get(&ref_name, &path, None) {
1877 Ok(p) => json_ok(&p),
1878 Err(e) => json_err(&e.to_string()),
1879 }
1880}
1881
1882#[no_mangle]
1885pub extern "C" fn agentstategraph_policy_history(
1886 store: *const SgPolicyStore,
1887 ref_name: *const c_char,
1888 path: *const c_char,
1889) -> *mut c_char {
1890 let Some(store) = policystore_ref(store) else {
1891 return ptr::null_mut();
1892 };
1893 let ref_name = unsafe { c_to_str(ref_name) };
1894 let path = unsafe { c_to_str(path) };
1895 match store.inner.history(&ref_name, &path) {
1896 Ok(ps) => json_ok(&ps),
1897 Err(e) => json_err(&e.to_string()),
1898 }
1899}
1900
1901#[no_mangle]
1905pub extern "C" fn agentstategraph_policy_evaluate(
1906 store: *const SgPolicyStore,
1907 ref_name: *const c_char,
1908 situation_json: *const c_char,
1909 action: *const c_char,
1910 agent_id: *const c_char,
1911) -> *mut c_char {
1912 let Some(store) = policystore_ref(store) else {
1913 return ptr::null_mut();
1914 };
1915 let ref_name = unsafe { c_to_str(ref_name) };
1916 let situation_str = unsafe { c_to_str(situation_json) };
1917 let action = unsafe { c_to_str(action) };
1918 let agent_id = unsafe { c_to_str(agent_id) };
1919 let situation = match parse_situation(&situation_str) {
1920 Ok(s) => s,
1921 Err(e) => return json_err(&format!("invalid situation: {e}")),
1922 };
1923 match store
1924 .inner
1925 .evaluate(&ref_name, &situation, &action, &agent_id)
1926 {
1927 Ok(d) => json_ok(&d),
1928 Err(e) => json_err(&e.to_string()),
1929 }
1930}
1931
1932#[no_mangle]
1935pub extern "C" fn agentstategraph_policy_evaluate_change(
1936 store: *const SgPolicyStore,
1937 ref_name: *const c_char,
1938 proposal_json: *const c_char,
1939) -> *mut c_char {
1940 let Some(store) = policystore_ref(store) else {
1941 return ptr::null_mut();
1942 };
1943 let ref_name = unsafe { c_to_str(ref_name) };
1944 let proposal_str = unsafe { c_to_str(proposal_json) };
1945 let proposal: ChangeProposal = match serde_json::from_str(&proposal_str) {
1946 Ok(p) => p,
1947 Err(e) => return json_err(&format!("invalid proposal: {e}")),
1948 };
1949 match store.inner.evaluate_change(&ref_name, &proposal) {
1950 Ok(d) => json_ok(&d),
1951 Err(e) => json_err(&e.to_string()),
1952 }
1953}
1954
1955#[no_mangle]
1960pub extern "C" fn agentstategraph_policy_evaluate_scoped(
1961 store: *const SgPolicyStore,
1962 ref_name: *const c_char,
1963 situation_json: *const c_char,
1964 action: *const c_char,
1965 agent_id: *const c_char,
1966 tenant_filter: *const c_char,
1967) -> *mut c_char {
1968 let Some(store) = policystore_ref(store) else {
1969 return ptr::null_mut();
1970 };
1971 let ref_name = unsafe { c_to_str(ref_name) };
1972 let situation_str = unsafe { c_to_str(situation_json) };
1973 let action = unsafe { c_to_str(action) };
1974 let agent_id = unsafe { c_to_str(agent_id) };
1975 let tenant = if tenant_filter.is_null() {
1976 None
1977 } else {
1978 Some(unsafe { c_to_str(tenant_filter) })
1979 };
1980 let situation = match parse_situation(&situation_str) {
1981 Ok(s) => s,
1982 Err(e) => return json_err(&format!("invalid situation: {e}")),
1983 };
1984 match store
1985 .inner
1986 .evaluate_scoped(&ref_name, &situation, &action, &agent_id, tenant.as_deref())
1987 {
1988 Ok(d) => json_ok(&d),
1989 Err(e) => json_err(&e.to_string()),
1990 }
1991}
1992
1993#[no_mangle]
1997pub extern "C" fn agentstategraph_policy_evaluate_change_scoped(
1998 store: *const SgPolicyStore,
1999 ref_name: *const c_char,
2000 proposal_json: *const c_char,
2001 tenant_filter: *const c_char,
2002) -> *mut c_char {
2003 let Some(store) = policystore_ref(store) else {
2004 return ptr::null_mut();
2005 };
2006 let ref_name = unsafe { c_to_str(ref_name) };
2007 let proposal_str = unsafe { c_to_str(proposal_json) };
2008 let tenant = if tenant_filter.is_null() {
2009 None
2010 } else {
2011 Some(unsafe { c_to_str(tenant_filter) })
2012 };
2013 let proposal: ChangeProposal = match serde_json::from_str(&proposal_str) {
2014 Ok(p) => p,
2015 Err(e) => return json_err(&format!("invalid proposal: {e}")),
2016 };
2017 match store
2018 .inner
2019 .evaluate_change_scoped(&ref_name, &proposal, tenant.as_deref())
2020 {
2021 Ok(d) => json_ok(&d),
2022 Err(e) => json_err(&e.to_string()),
2023 }
2024}
2025
2026#[no_mangle]
2030pub extern "C" fn agentstategraph_policy_check_tokens(
2031 store: *const SgPolicyStore,
2032 ref_name: *const c_char,
2033 tokens_json: *const c_char,
2034) -> *mut c_char {
2035 let Some(store) = policystore_ref(store) else {
2036 return ptr::null_mut();
2037 };
2038 let ref_name = unsafe { c_to_str(ref_name) };
2039 let tokens_str = unsafe { c_to_str(tokens_json) };
2040 let tokens: Vec<String> = if tokens_str.is_empty() {
2041 Vec::new()
2042 } else {
2043 match serde_json::from_str(&tokens_str) {
2044 Ok(v) => v,
2045 Err(e) => return json_err(&format!("invalid tokens_json: {e}")),
2046 }
2047 };
2048 let actives = match store.inner.active(&ref_name, None) {
2049 Ok(v) => v,
2050 Err(e) => return json_err(&e.to_string()),
2051 };
2052 let token_set: std::collections::HashSet<&str> = tokens.iter().map(|s| s.as_str()).collect();
2053 let matched: Vec<&Policy> = actives
2054 .iter()
2055 .filter(|p| p.triggers.iter().any(|t| token_set.contains(t.as_str())))
2056 .collect();
2057 json_ok(&matched)
2058}
2059
2060#[no_mangle]
2068pub extern "C" fn agentstategraph_policy_sign(
2069 store: *const SgPolicyStore,
2070 ref_name: *const c_char,
2071 path: *const c_char,
2072 signer_key_id: *const c_char,
2073) -> *mut c_char {
2074 let Some(_store) = policystore_ref(store) else {
2075 return ptr::null_mut();
2076 };
2077 let _ref_name = unsafe { c_to_str(ref_name) };
2078 let _path = unsafe { c_to_str(path) };
2079 let _signer_key_id = opt_c_to_str(signer_key_id);
2080 to_c_string("{\"error\":\"no signer registered\"}")
2082}
2083
2084#[no_mangle]
2092pub extern "C" fn agentstategraph_policy_verify(
2093 store: *const SgPolicyStore,
2094 ref_name: *const c_char,
2095 path: *const c_char,
2096) -> *mut c_char {
2097 let Some(_store) = policystore_ref(store) else {
2098 return ptr::null_mut();
2099 };
2100 let _ref_name = unsafe { c_to_str(ref_name) };
2101 let _path = unsafe { c_to_str(path) };
2102 to_c_string("{\"valid\":null,\"reason\":\"no verifier registered\"}")
2104}
2105
2106#[no_mangle]
2122pub extern "C" fn agentstategraph_policy_set_external_evaluator(
2123 store: *const SgPolicyStore,
2124 config_json: *const c_char,
2125) -> *mut c_char {
2126 let Some(_store) = policystore_ref(store) else {
2127 return ptr::null_mut();
2128 };
2129 let _config = unsafe { c_to_str(config_json) };
2130 to_c_string(
2137 "{\"error\":\"external evaluators not configured via FFI; use the server builder\"}",
2138 )
2139}
2140
2141fn parse_taint_effect_ffi(s: &str) -> Option<TaintEffect> {
2146 match s.to_lowercase().as_str() {
2147 "warn" => Some(TaintEffect::Warn),
2148 "block" => Some(TaintEffect::Block),
2149 "review" => Some(TaintEffect::Review),
2150 "isolate" => Some(TaintEffect::Isolate),
2151 "advisory" => Some(TaintEffect::Advisory),
2152 _ => None,
2153 }
2154}
2155
2156fn parse_taint_severity_ffi(s: Option<&str>) -> TaintSeverity {
2157 match s.unwrap_or("medium").to_lowercase().as_str() {
2158 "low" => TaintSeverity::Low,
2159 "high" => TaintSeverity::High,
2160 "critical" => TaintSeverity::Critical,
2161 _ => TaintSeverity::Medium,
2162 }
2163}
2164
2165fn parse_taint_kind_ffi(s: &str) -> Option<TaintKind> {
2166 match s.to_lowercase().as_str() {
2167 "taint" => Some(TaintKind::Taint),
2168 "quarantine" => Some(TaintKind::Quarantine),
2169 "watch" => Some(TaintKind::Watch),
2170 _ => None,
2171 }
2172}
2173
2174#[no_mangle]
2179pub extern "C" fn agentstategraph_taint_apply(
2180 repo: *const SgRepo,
2181 ref_name: *const c_char,
2182 path: *const c_char,
2183 params_json: *const c_char,
2184) -> *mut c_char {
2185 let Some(repo) = (unsafe { repo.as_ref() }) else {
2186 return ptr::null_mut();
2187 };
2188 let ref_name = unsafe { c_to_str(ref_name) };
2189 let path = unsafe { c_to_str(path) };
2190 let params_str = unsafe { c_to_str(params_json) };
2191 let v: serde_json::Value = match serde_json::from_str(¶ms_str) {
2192 Ok(v) => v,
2193 Err(e) => return json_err(&format!("invalid params: {e}")),
2194 };
2195 let Some(name) = v.get("name").and_then(|x| x.as_str()).map(str::to_string) else {
2196 return json_err("missing 'name'");
2197 };
2198 let Some(effect) = v
2199 .get("effect")
2200 .and_then(|x| x.as_str())
2201 .and_then(parse_taint_effect_ffi)
2202 else {
2203 return json_err("missing or invalid 'effect'");
2204 };
2205 let reason = v
2206 .get("reason")
2207 .and_then(|x| x.as_str())
2208 .unwrap_or("")
2209 .to_string();
2210 let severity = parse_taint_severity_ffi(v.get("severity").and_then(|x| x.as_str()));
2211 let expires_at = v
2212 .get("expires")
2213 .and_then(|x| x.as_str())
2214 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
2215 .map(|d| d.with_timezone(&chrono::Utc));
2216 let propagate = v.get("propagate").and_then(|x| x.as_bool()).unwrap_or(true);
2217 let agent_id = v
2218 .get("agent_id")
2219 .and_then(|x| x.as_str())
2220 .unwrap_or("")
2221 .to_string();
2222 let params = TaintParams {
2223 name,
2224 effect,
2225 reason,
2226 severity,
2227 expires_at,
2228 propagate,
2229 metadata: TaintMetadata::new(),
2230 agent_id,
2231 };
2232 match repo.inner.taint(&ref_name, &path, params) {
2233 Ok(id) => json_ok(&serde_json::json!({ "ok": true, "id": id })),
2234 Err(e) => json_err(&e.to_string()),
2235 }
2236}
2237
2238#[no_mangle]
2240pub extern "C" fn agentstategraph_taint_remove(
2241 repo: *const SgRepo,
2242 ref_name: *const c_char,
2243 path: *const c_char,
2244 params_json: *const c_char,
2245) -> *mut c_char {
2246 let Some(repo) = (unsafe { repo.as_ref() }) else {
2247 return ptr::null_mut();
2248 };
2249 let ref_name = unsafe { c_to_str(ref_name) };
2250 let path = unsafe { c_to_str(path) };
2251 let v: serde_json::Value = match serde_json::from_str(&unsafe { c_to_str(params_json) }) {
2252 Ok(v) => v,
2253 Err(e) => return json_err(&format!("invalid params: {e}")),
2254 };
2255 let Some(name) = v.get("name").and_then(|x| x.as_str()).map(str::to_string) else {
2256 return json_err("missing 'name'");
2257 };
2258 let params = UntaintParams {
2259 reason: v
2260 .get("reason")
2261 .and_then(|x| x.as_str())
2262 .unwrap_or("")
2263 .to_string(),
2264 proof: v.get("proof").and_then(|x| x.as_str()).map(str::to_string),
2265 agent_id: v
2266 .get("agent_id")
2267 .and_then(|x| x.as_str())
2268 .unwrap_or("")
2269 .to_string(),
2270 };
2271 match repo.inner.untaint(&ref_name, &path, &name, params) {
2272 Ok(()) => json_ok(&serde_json::json!({ "ok": true })),
2273 Err(e) => json_err(&e.to_string()),
2274 }
2275}
2276
2277#[no_mangle]
2280pub extern "C" fn agentstategraph_quarantine_apply(
2281 repo: *const SgRepo,
2282 ref_name: *const c_char,
2283 path: *const c_char,
2284 params_json: *const c_char,
2285) -> *mut c_char {
2286 let Some(repo) = (unsafe { repo.as_ref() }) else {
2287 return ptr::null_mut();
2288 };
2289 let ref_name = unsafe { c_to_str(ref_name) };
2290 let path = unsafe { c_to_str(path) };
2291 let v: serde_json::Value = match serde_json::from_str(&unsafe { c_to_str(params_json) }) {
2292 Ok(v) => v,
2293 Err(e) => return json_err(&format!("invalid params: {e}")),
2294 };
2295 let Some(name) = v.get("name").and_then(|x| x.as_str()).map(str::to_string) else {
2296 return json_err("missing 'name'");
2297 };
2298 let authorized: Vec<String> = v
2299 .get("authorized_agents")
2300 .and_then(|x| x.as_array())
2301 .map(|arr| {
2302 arr.iter()
2303 .filter_map(|x| x.as_str().map(str::to_string))
2304 .collect()
2305 })
2306 .unwrap_or_default();
2307 let params = QuarantineParams {
2308 name,
2309 reason: v
2310 .get("reason")
2311 .and_then(|x| x.as_str())
2312 .unwrap_or("")
2313 .to_string(),
2314 severity: parse_taint_severity_ffi(v.get("severity").and_then(|x| x.as_str())),
2315 authorized_agents: authorized,
2316 expires_at: v
2317 .get("expires")
2318 .and_then(|x| x.as_str())
2319 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
2320 .map(|d| d.with_timezone(&chrono::Utc)),
2321 propagate: v.get("propagate").and_then(|x| x.as_bool()).unwrap_or(true),
2322 agent_id: v
2323 .get("agent_id")
2324 .and_then(|x| x.as_str())
2325 .unwrap_or("")
2326 .to_string(),
2327 };
2328 match repo.inner.quarantine(&ref_name, &path, params) {
2329 Ok(id) => json_ok(&serde_json::json!({ "ok": true, "id": id })),
2330 Err(e) => json_err(&e.to_string()),
2331 }
2332}
2333
2334#[no_mangle]
2336pub extern "C" fn agentstategraph_quarantine_release(
2337 repo: *const SgRepo,
2338 ref_name: *const c_char,
2339 path: *const c_char,
2340 params_json: *const c_char,
2341) -> *mut c_char {
2342 let Some(repo) = (unsafe { repo.as_ref() }) else {
2343 return ptr::null_mut();
2344 };
2345 let ref_name = unsafe { c_to_str(ref_name) };
2346 let path = unsafe { c_to_str(path) };
2347 let v: serde_json::Value = match serde_json::from_str(&unsafe { c_to_str(params_json) }) {
2348 Ok(v) => v,
2349 Err(e) => return json_err(&format!("invalid params: {e}")),
2350 };
2351 let Some(name) = v.get("name").and_then(|x| x.as_str()).map(str::to_string) else {
2352 return json_err("missing 'name'");
2353 };
2354 let params = UntaintParams {
2355 reason: v
2356 .get("reason")
2357 .and_then(|x| x.as_str())
2358 .unwrap_or("")
2359 .to_string(),
2360 proof: v.get("proof").and_then(|x| x.as_str()).map(str::to_string),
2361 agent_id: v
2362 .get("agent_id")
2363 .and_then(|x| x.as_str())
2364 .unwrap_or("")
2365 .to_string(),
2366 };
2367 match repo.inner.unquarantine(&ref_name, &path, &name, params) {
2368 Ok(()) => json_ok(&serde_json::json!({ "ok": true })),
2369 Err(e) => json_err(&e.to_string()),
2370 }
2371}
2372
2373#[no_mangle]
2377pub extern "C" fn agentstategraph_watch_apply(
2378 repo: *const SgRepo,
2379 ref_name: *const c_char,
2380 path: *const c_char,
2381 params_json: *const c_char,
2382) -> *mut c_char {
2383 let Some(repo) = (unsafe { repo.as_ref() }) else {
2384 return ptr::null_mut();
2385 };
2386 let ref_name = unsafe { c_to_str(ref_name) };
2387 let path = unsafe { c_to_str(path) };
2388 let v: serde_json::Value = match serde_json::from_str(&unsafe { c_to_str(params_json) }) {
2389 Ok(v) => v,
2390 Err(e) => return json_err(&format!("invalid params: {e}")),
2391 };
2392 let Some(name) = v.get("name").and_then(|x| x.as_str()).map(str::to_string) else {
2393 return json_err("missing 'name'");
2394 };
2395 let direction = match v
2396 .get("direction")
2397 .and_then(|x| x.as_str())
2398 .unwrap_or("above")
2399 {
2400 "below" => WatchDirection::Below,
2401 _ => WatchDirection::Above,
2402 };
2403 let params = WatchParams {
2404 name,
2405 reason: v
2406 .get("reason")
2407 .and_then(|x| x.as_str())
2408 .unwrap_or("")
2409 .to_string(),
2410 metric: v.get("metric").and_then(|x| x.as_str()).map(str::to_string),
2411 threshold: v.get("threshold").and_then(|x| x.as_f64()),
2412 direction,
2413 check_interval_secs: v.get("check_interval_secs").and_then(|x| x.as_u64()),
2414 expires_at: v
2415 .get("expires")
2416 .and_then(|x| x.as_str())
2417 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
2418 .map(|d| d.with_timezone(&chrono::Utc)),
2419 severity: parse_taint_severity_ffi(v.get("severity").and_then(|x| x.as_str())),
2420 propagate: v.get("propagate").and_then(|x| x.as_bool()).unwrap_or(true),
2421 agent_id: v
2422 .get("agent_id")
2423 .and_then(|x| x.as_str())
2424 .unwrap_or("")
2425 .to_string(),
2426 };
2427 match repo.inner.watch_path(&ref_name, &path, params) {
2428 Ok(id) => json_ok(&serde_json::json!({ "ok": true, "id": id })),
2429 Err(e) => json_err(&e.to_string()),
2430 }
2431}
2432
2433#[no_mangle]
2435pub extern "C" fn agentstategraph_watch_remove(
2436 repo: *const SgRepo,
2437 ref_name: *const c_char,
2438 path: *const c_char,
2439 params_json: *const c_char,
2440) -> *mut c_char {
2441 let Some(repo) = (unsafe { repo.as_ref() }) else {
2442 return ptr::null_mut();
2443 };
2444 let ref_name = unsafe { c_to_str(ref_name) };
2445 let path = unsafe { c_to_str(path) };
2446 let v: serde_json::Value = match serde_json::from_str(&unsafe { c_to_str(params_json) }) {
2447 Ok(v) => v,
2448 Err(e) => return json_err(&format!("invalid params: {e}")),
2449 };
2450 let Some(name) = v.get("name").and_then(|x| x.as_str()).map(str::to_string) else {
2451 return json_err("missing 'name'");
2452 };
2453 let params = UnwatchParams {
2454 reason: v.get("reason").and_then(|x| x.as_str()).map(str::to_string),
2455 agent_id: v
2456 .get("agent_id")
2457 .and_then(|x| x.as_str())
2458 .unwrap_or("")
2459 .to_string(),
2460 };
2461 match repo.inner.unwatch(&ref_name, &path, &name, params) {
2462 Ok(()) => json_ok(&serde_json::json!({ "ok": true })),
2463 Err(e) => json_err(&e.to_string()),
2464 }
2465}
2466
2467#[no_mangle]
2470pub extern "C" fn agentstategraph_list_taints(
2471 repo: *const SgRepo,
2472 path_prefix_or_null: *const c_char,
2473 kind_or_null: *const c_char,
2474 include_resolved: bool,
2475) -> *mut c_char {
2476 let Some(repo) = (unsafe { repo.as_ref() }) else {
2477 return ptr::null_mut();
2478 };
2479 let prefix = if path_prefix_or_null.is_null() {
2480 None
2481 } else {
2482 Some(unsafe { c_to_str(path_prefix_or_null) })
2483 };
2484 let kind = if kind_or_null.is_null() {
2485 None
2486 } else {
2487 let s = unsafe { c_to_str(kind_or_null) };
2488 match parse_taint_kind_ffi(&s) {
2489 Some(k) => Some(k),
2490 None => return json_err(&format!("unknown kind: {s}")),
2491 }
2492 };
2493 match repo
2494 .inner
2495 .list_taints(prefix.as_deref(), kind, include_resolved)
2496 {
2497 Ok(list) => json_ok(&serde_json::json!({ "ok": true, "taints": list })),
2498 Err(e) => json_err(&e.to_string()),
2499 }
2500}
2501
2502#[no_mangle]
2504pub extern "C" fn agentstategraph_check_taint(
2505 repo: *const SgRepo,
2506 path: *const c_char,
2507 agent_id: *const c_char,
2508 confidence: f64,
2509) -> *mut c_char {
2510 let Some(repo) = (unsafe { repo.as_ref() }) else {
2511 return ptr::null_mut();
2512 };
2513 let path = unsafe { c_to_str(path) };
2514 let agent_id = unsafe { c_to_str(agent_id) };
2515 match repo.inner.check_taint(&path, &agent_id, confidence) {
2516 Ok(c) => json_ok(&serde_json::json!({ "ok": true, "check": c })),
2517 Err(e) => json_err(&e.to_string()),
2518 }
2519}
2520
2521fn parse_situation(s: &str) -> Result<Situation, serde_json::Error> {
2522 if s.is_empty() {
2523 return Ok(Situation::default());
2524 }
2525 serde_json::from_str(s)
2528}
2529
2530#[no_mangle]
2539pub extern "C" fn agentstategraph_migrate_check(
2540 repo: *const SgRepo,
2541 ref_name: *const c_char,
2542 target: *const c_char,
2543) -> *mut c_char {
2544 let Some(repo) = (unsafe { repo.as_ref() }) else {
2545 return ptr::null_mut();
2546 };
2547 let ref_name = unsafe { c_to_str(ref_name) };
2548 let target_str = if target.is_null() {
2549 SCHEMA_VERSION.to_string()
2550 } else {
2551 let s = unsafe { c_to_str(target) };
2552 if s.is_empty() {
2553 SCHEMA_VERSION.to_string()
2554 } else {
2555 s
2556 }
2557 };
2558 let target_version = match Version::parse(&target_str) {
2559 Ok(v) => v,
2560 Err(e) => return json_err(&format!("invalid target: {e}")),
2561 };
2562 let registry = Registry::builtin();
2563 match agentstategraph_migrate::check(&repo.inner, &ref_name, &target_version, ®istry) {
2564 Ok(r) => to_c_string(&check_result_json(&r)),
2565 Err(e) => json_err(&e.to_string()),
2566 }
2567}
2568
2569#[no_mangle]
2572pub extern "C" fn agentstategraph_migrate_run(
2573 repo: *const SgRepo,
2574 ref_name: *const c_char,
2575 target: *const c_char,
2576 mode: *const c_char,
2577) -> *mut c_char {
2578 let Some(repo) = (unsafe { repo.as_ref() }) else {
2579 return ptr::null_mut();
2580 };
2581 let ref_name = unsafe { c_to_str(ref_name) };
2582 let target_str = if target.is_null() {
2583 SCHEMA_VERSION.to_string()
2584 } else {
2585 let s = unsafe { c_to_str(target) };
2586 if s.is_empty() {
2587 SCHEMA_VERSION.to_string()
2588 } else {
2589 s
2590 }
2591 };
2592 let target_version = match Version::parse(&target_str) {
2593 Ok(v) => v,
2594 Err(e) => return json_err(&format!("invalid target: {e}")),
2595 };
2596 let mode_str = unsafe { c_to_str(mode) };
2597 let run_mode = match mode_str.to_lowercase().as_str() {
2598 "apply" => RunMode::Apply,
2599 "dry-run" | "dryrun" | "dry_run" => RunMode::DryRun,
2600 other => return json_err(&format!("invalid mode: {other}")),
2601 };
2602 let registry = Registry::builtin();
2603 match registry.run(&repo.inner, &ref_name, &target_version, run_mode) {
2604 Ok(r) => to_c_string(&report_json(&r)),
2605 Err(e) => json_err(&e.to_string()),
2606 }
2607}
2608
2609fn check_result_json(r: &CheckResult) -> String {
2610 let v = match r {
2611 CheckResult::UpToDate { version } => serde_json::json!({
2612 "status": "up_to_date",
2613 "version": version.to_string(),
2614 }),
2615 CheckResult::UpgradeAvailable {
2616 from,
2617 to,
2618 migrations,
2619 } => serde_json::json!({
2620 "status": "upgrade_available",
2621 "from": from.to_string(),
2622 "to": to.to_string(),
2623 "migrations": migrations,
2624 }),
2625 CheckResult::Downgrade { db, binary } => serde_json::json!({
2626 "status": "downgrade",
2627 "db": db.to_string(),
2628 "binary": binary.to_string(),
2629 }),
2630 CheckResult::Unversioned { implicit } => serde_json::json!({
2631 "status": "unversioned",
2632 "implicit": implicit.to_string(),
2633 }),
2634 CheckResult::Corrupt(msg) => serde_json::json!({
2635 "status": "corrupt",
2636 "message": msg,
2637 }),
2638 };
2639 serde_json::to_string(&v).unwrap_or_default()
2640}
2641
2642fn report_json(r: &agentstategraph_migrate::Report) -> String {
2643 let mode = match r.mode {
2644 RunMode::DryRun => "dry-run",
2645 RunMode::Apply => "apply",
2646 };
2647 let steps: Vec<serde_json::Value> = r
2648 .steps
2649 .iter()
2650 .map(|s| {
2651 serde_json::json!({
2652 "name": s.name,
2653 "describe": s.describe,
2654 "from": s.from.to_string(),
2655 "to": s.to.to_string(),
2656 "status": step_status_str(s.status),
2657 "commit_id": s.commit_id.as_ref().map(|c| c.to_string()),
2658 "notes": s.notes,
2659 })
2660 })
2661 .collect();
2662 let v = serde_json::json!({
2663 "from": r.from.to_string(),
2664 "target": r.target.to_string(),
2665 "final_version": r.final_version.to_string(),
2666 "mode": mode,
2667 "steps": steps,
2668 });
2669 serde_json::to_string(&v).unwrap_or_default()
2670}
2671
2672fn step_status_str(s: StepStatus) -> &'static str {
2673 match s {
2674 StepStatus::WouldApply => "would-apply",
2675 StepStatus::WouldSkip => "would-skip",
2676 StepStatus::Applied => "applied",
2677 StepStatus::Skipped => "skipped",
2678 StepStatus::Failed => "failed",
2679 }
2680}
2681
2682unsafe fn c_to_str(ptr: *const c_char) -> String {
2687 if ptr.is_null() {
2688 String::new()
2689 } else {
2690 unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() }
2691 }
2692}
2693
2694unsafe fn parse_repo_ref_path<'a>(
2695 repo: *const SgRepo,
2696 ref_name: *const c_char,
2697 path: *const c_char,
2698) -> Option<(&'a SgRepo, String, String)> {
2699 let repo = unsafe { repo.as_ref()? };
2700 let ref_name = unsafe { c_to_str(ref_name) };
2701 let path = unsafe { c_to_str(path) };
2702 Some((repo, ref_name, path))
2703}
2704
2705fn parse_category(s: &str) -> IntentCategory {
2706 match s.to_lowercase().as_str() {
2707 "explore" | "exploration" => IntentCategory::Explore,
2708 "refine" | "refinement" => IntentCategory::Refine,
2709 "fix" | "correction" => IntentCategory::Fix,
2710 "rollback" => IntentCategory::Rollback,
2711 "checkpoint" => IntentCategory::Checkpoint,
2712 "merge" => IntentCategory::Merge,
2713 "migrate" => IntentCategory::Custom("Migrate-requested".into()),
2720 "plan" => IntentCategory::Plan,
2721 other => IntentCategory::Custom(other.to_string()),
2722 }
2723}
2724
2725fn parse_priority(s: &str) -> Priority {
2726 match s.to_lowercase().as_str() {
2727 "low" => Priority::Low,
2728 "high" => Priority::High,
2729 "critical" => Priority::Critical,
2730 _ => Priority::Medium,
2731 }
2732}
2733
2734fn parse_plan_status(s: &str) -> Option<agentstategraph_tasks::PlanStatus> {
2735 match s.to_lowercase().as_str() {
2736 "" => None,
2737 "active" => Some(agentstategraph_tasks::PlanStatus::Active),
2738 "completed" => Some(agentstategraph_tasks::PlanStatus::Completed),
2739 "archived" => Some(agentstategraph_tasks::PlanStatus::Archived),
2740 _ => None,
2741 }
2742}
2743
2744fn parse_proof_kind(s: &str) -> Option<ProofKind> {
2745 match s.to_lowercase().as_str() {
2746 "commit" => Some(ProofKind::Commit),
2747 "file" => Some(ProofKind::File),
2748 "test" => Some(ProofKind::Test),
2749 "text" => Some(ProofKind::Text),
2750 _ => None,
2751 }
2752}
2753
2754fn to_c_string(s: &str) -> *mut c_char {
2755 match CString::new(s) {
2756 Ok(cs) => cs.into_raw(),
2757 Err(_) => ptr::null_mut(),
2758 }
2759}