1use super::*;
2
3pub fn agent_validate_command(spec: &str) -> Result<String> {
4 let (spec_path, spec_value, agent_spec) = load_agent_spec_file(spec)?;
5 let schema = generated_schema_for_rust_type("AgentSpecV1")?;
6 let schema_validation = validate_json_schema(&schema, &spec_value);
7 let support_label = agent_spec.support_label.to_string();
8 let validation = match agent_spec.validate() {
9 Ok(()) => serde_json::json!({
10 "valid": true,
11 "reason_codes": [],
12 }),
13 Err(reason_codes) => serde_json::json!({
14 "valid": false,
15 "reason_codes": reason_codes,
16 }),
17 };
18 let validation_valid = validation
19 .get("valid")
20 .and_then(Value::as_bool)
21 .unwrap_or(false);
22 let mut degradation = Vec::new();
23 if !schema_validation.valid {
24 degradation.push("agent-spec-schema-validation-failed".into());
25 }
26 if !validation_valid {
27 degradation.push("agent-spec-policy-validation-failed".into());
28 }
29 let semantic_status = if schema_validation.valid && validation_valid {
30 "exact_check"
31 } else {
32 "failed_exact_check"
33 };
34 Ok(serde_json::to_string_pretty(&serde_json::json!({
35 "schema": "AgentSpecValidationReportV1",
36 "spec_path": spec_path,
37 "agent_id": agent_spec.agent_id,
38 "display_name": agent_spec.display_name,
39 "profile": agent_spec.profile,
40 "support_label": support_label.clone(),
41 "agent_spec_digest": DisplayDigestV1::for_json_value(&spec_value),
42 "schema_validation": schema_validation,
43 "validation": validation,
44 "semantic_disclosure": semantic_disclosure_value(
45 semantic_status,
46 support_label,
47 degradation,
48 vec![
49 "strict-json-parse-with-duplicate-key-rejection".into(),
50 "generated-json-schema-validation".into(),
51 "AgentSpecV1-policy-validation".into(),
52 ],
53 vec![
54 "validation is an AiDENs-local operator check and does not create canonical memory, tool, or verification truth".into(),
55 ],
56 ),
57 "canonical_ownership": {
58 "aidens_role": "consumer-orchestrator",
59 "memory_truth_owner": "semantic-memory",
60 "tool_receipt_owner": "llm-tool-runtime",
61 "trace_identity_owner": "stack-ids",
62 "verification_truth_owner": "verification-control/verification-policy",
63 }
64 }))?)
65}
66
67pub fn agent_doctor_command(spec: &str) -> Result<String> {
68 let (spec_path, spec_value, agent_spec) = load_agent_spec_file(spec)?;
69 let validation_reasons = agent_spec.validate().err().unwrap_or_default();
70 let mut blocked = validation_reasons.clone();
71 if agent_spec.provider_policy.cloud_allowed {
72 blocked.push("cloud-provider-execution-blocked-for-supported-local-agent".into());
73 }
74 if matches!(
75 agent_spec.permit_policy.network,
76 AgentPermitRuleV1::OperatorApproved
77 ) {
78 blocked.push("network-authority-blocked-for-p26-local-agent".into());
79 }
80 let support_label = agent_spec.support_label.to_string();
81 let semantic_status = if blocked.is_empty() {
82 "exact_check"
83 } else {
84 "degraded_exact_check"
85 };
86 Ok(serde_json::to_string_pretty(&serde_json::json!({
87 "schema": "AgentSpecDoctorReportV1",
88 "spec_path": spec_path,
89 "agent_id": agent_spec.agent_id,
90 "support_label": support_label.clone(),
91 "agent_spec_digest": DisplayDigestV1::for_json_value(&spec_value),
92 "valid": validation_reasons.is_empty(),
93 "blocked_checks": blocked.clone(),
94 "memory_policy": agent_spec.memory_policy,
95 "tool_policy": agent_spec.tool_policy,
96 "permit_policy": agent_spec.permit_policy,
97 "verification_policy": agent_spec.verification_policy,
98 "support_disclosure": {
99 "supported_local": ["mock/local provider route", "sandboxed coding tools", "permit-gated writes", "run-bundle-v3 evidence"],
100 "partial": ["memory grounding through canonical seam when enabled"],
101 "deferred": ["cloud provider execution", "broad autonomous daemon behavior", "V10 runtime geometry"]
102 },
103 "semantic_disclosure": semantic_disclosure_value(
104 semantic_status,
105 support_label,
106 blocked,
107 vec![
108 "AgentSpecV1-policy-validation".into(),
109 "supported-local-cloud-boundary-check".into(),
110 "permit-policy-network-boundary-check".into(),
111 ],
112 vec![
113 "doctor output is an AiDENs-local operator diagnostic; canonical policy truth remains delegated".into(),
114 ],
115 ),
116 "consumer_only": true
117 }))?)
118}
119
120pub fn agent_new_command(template: &str, out: &str) -> Result<String> {
121 if template != "local-coding" {
122 bail!("agent new currently supports only --template local-coding");
123 }
124 let out_dir = resolve_output_path(out)?;
125 if out_dir.exists() {
126 bail!(
127 "agent new refuses to overwrite existing directory: {}",
128 out_dir.display()
129 );
130 }
131 std::fs::create_dir_all(out_dir.join("sandbox"))
132 .with_context(|| format!("failed to create {}", out_dir.join("sandbox").display()))?;
133 let spec = local_coding_agent_spec_value("agent:local-coding-example", "Local Coding Agent");
134 write_json_file(&out_dir.join("agent.json"), &spec)?;
135 std::fs::write(
136 out_dir.join("task.md"),
137 "Read README.md, search for AiDENs, and propose a bounded local patch. Apply only with a scoped permit.\n",
138 )
139 .with_context(|| format!("failed to write {}", out_dir.join("task.md").display()))?;
140 std::fs::write(
141 out_dir.join("sandbox").join("README.md"),
142 "# AiDENs Local Coding Sandbox\n\nAiDENs fixture content for local agent read/search/propose evidence.\n",
143 )
144 .with_context(|| format!("failed to write {}", out_dir.join("sandbox/README.md").display()))?;
145 std::fs::write(
146 out_dir.join("README.md"),
147 "# AiDENs AgentSpecV1 Local Coding Example\n\nRun with:\n\n```bash\ncargo run -p aidens-cli -- agent validate --spec agent.json\ncargo run -p aidens-cli -- agent run --spec agent.json --task task.md --sandbox-root sandbox --out target/run\ncargo run -p aidens-cli -- agent inspect --run target/run\n```\n",
148 )
149 .with_context(|| format!("failed to write {}", out_dir.join("README.md").display()))?;
150 Ok(format!(
151 "AiDENs agent new\ntemplate: {template}\noutput: {}\nspec: {}\ntask: {}",
152 out_dir.display(),
153 out_dir.join("agent.json").display(),
154 out_dir.join("task.md").display()
155 ))
156}
157
158pub fn agent_run_command(
159 spec: &str,
160 task: &str,
161 out: &str,
162 sandbox_root: Option<String>,
163 permit_json: Option<String>,
164 mock_response: Option<String>,
165) -> Result<String> {
166 let started_at = Utc::now();
167 let (spec_path, spec_value, agent_spec) = load_agent_spec_file(spec)?;
168 if let Err(reason_codes) = agent_spec.validate() {
169 bail!("AgentSpecV1 validation failed: {}", reason_codes.join(","));
170 }
171 let spec_root = spec_path.parent().unwrap_or_else(|| Path::new("."));
172 let task_path = resolve_input_or_spec_relative_path(task, spec_root)?;
173 let task_text = if task_path.exists() {
174 std::fs::read_to_string(&task_path)
175 .with_context(|| format!("failed to read task {}", task_path.display()))?
176 } else {
177 task.to_string()
178 };
179 if task_text.trim().is_empty() {
180 bail!("agent run requires a non-empty task");
181 }
182 let out_dir = resolve_output_path(out)?;
183 std::fs::create_dir_all(&out_dir)
184 .with_context(|| format!("failed to create output directory {}", out_dir.display()))?;
185 let sandbox_root = sandbox_root
186 .map(|root| resolve_input_or_spec_relative_path(root, spec_root))
187 .transpose()?
188 .unwrap_or_else(|| spec_root.join("sandbox"))
189 .canonicalize()
190 .with_context(|| "agent run sandbox_root must exist; pass --sandbox-root or create a sandbox directory next to the spec")?;
191 let receipt_root = out_dir.join("receipts");
192 let event_log_path = out_dir.join("event-log.ndjson");
193 let permit_policy = permit_json
194 .as_deref()
195 .map(permit_policy_from_json)
196 .transpose()?
197 .unwrap_or_default();
198 let registry = ToolRegistryV1::safe_coding_with_dispatchers(&sandbox_root)?;
199 let mock_response = mock_response.unwrap_or_else(|| {
200 agent_default_mock_response(&agent_spec, &sandbox_root, &task_text)
201 .unwrap_or_else(|_| "AgentSpecV1 local agent completed without tool calls.".into())
202 });
203 let runtime = tokio::runtime::Runtime::new()?;
204 let loop_output = runtime.block_on(async {
205 PlanActVerifyLoopV1::new(agent_spec.clone())
206 .app_id(receipt_store_segment(&agent_spec.agent_id))
207 .tools(registry)
208 .permit_policy(permit_policy)
209 .provider_mock_response(mock_response)
210 .canonical_receipt_log_config(CanonicalEventLogConfig::for_root(&receipt_root))
211 .execute(task_text.clone())
212 .await
213 })?;
214 write_agent_loop_artifacts(&out_dir, &event_log_path, &loop_output)?;
215
216 let run_id = format!(
217 "{}-agent-local",
218 receipt_store_segment(&agent_spec.agent_id)
219 );
220 let elapsed_ms = (Utc::now() - started_at).num_milliseconds().max(0);
221 let run_output = loop_output.run_output.as_ref();
222 let trace_ctx = run_output
223 .map(|output| output.receipt.context.stack_trace_ctx())
224 .unwrap_or_else(aidens_contracts::StackTraceCtx::generate);
225 let attempt_id = run_output
226 .map(|output| output.receipt.context.stack_attempt_id())
227 .unwrap_or_else(StackAttemptId::generate);
228 let trial_id = StackTrialId::generate();
229 let attempt_family_id = run_output
230 .map(|output| output.receipt.context.attempt_family_id.clone())
231 .unwrap_or_else(|| generated_artifact_id_from_material("attempt-family", &run_id));
232 let mut execution_context =
233 aidens_contracts::canonical_stack::ForgeExecutionContextV1::new(trace_ctx.clone());
234 execution_context.attempt_id = Some(attempt_id.clone());
235 execution_context.trial_id = Some(trial_id.clone());
236 execution_context.replay_link = Some(format!(
237 "cargo run -p aidens-cli -- agent run --spec {} --task {} --sandbox-root {} --out {}",
238 spec_path.display(),
239 task_path.display(),
240 sandbox_root.display(),
241 out_dir.display()
242 ));
243 execution_context.workload_class = Some("agent-spec-supported-local".into());
244 execution_context.deadline = Some(format!(
245 "{}ms",
246 agent_spec
247 .budget_policy
248 .deadline_seconds
249 .saturating_mul(1000)
250 ));
251 execution_context.cost_budget_units = Some(agent_spec.budget_policy.max_tool_calls.into());
252 execution_context.provider_route = Some("mock".into());
253 execution_context.degradation_markers = agent_degradation_markers(&loop_output);
254 execution_context.dispatch_outcome =
255 if matches!(loop_output.outcome, PlanActVerifyOutcomeV1::Success) {
256 aidens_contracts::canonical_stack::ForgeDispatchOutcomeV1::Succeeded
257 } else if loop_output.abstention_receipt.is_some() {
258 aidens_contracts::canonical_stack::ForgeDispatchOutcomeV1::Failed
259 } else {
260 aidens_contracts::canonical_stack::ForgeDispatchOutcomeV1::Degraded
261 };
262 execution_context.environment_fingerprint = Some(
263 StackContentDigest::compute_str(&format!(
264 "{}:{}:{}",
265 agent_spec.agent_id,
266 sandbox_root.display(),
267 out_dir.display()
268 ))
269 .hex()
270 .to_string(),
271 );
272
273 let event_log = event_log_digest(
274 &event_log_path,
275 run_output
276 .map(|output| output.durable_receipt_records.len())
277 .unwrap_or(0),
278 0,
279 )?;
280 let replay = AiDENsRunReplayNormalizationV1 {
281 replay_command: execution_context.replay_link.clone().unwrap_or_default(),
282 fixture_path: task_path.exists().then(|| task_path.display().to_string()),
283 normalized_fields: vec![
284 "created_at".into(),
285 "receipt_id".into(),
286 "recorded_at".into(),
287 "content_digest".into(),
288 ],
289 deterministic_compare: true,
290 normalized_digest: event_log.replay_normalized_digest.clone(),
291 reason_codes: vec!["timestamp-and-id-normalized".into()],
292 };
293 let failure = agent_failure_taxonomy(&loop_output);
294 let budget = AiDENsRunBudgetDeadlineV1 {
295 max_steps: agent_spec.budget_policy.max_turns,
296 max_tool_calls: agent_spec.budget_policy.max_tool_calls,
297 max_retries: 2,
298 max_turn_millis: agent_spec
299 .budget_policy
300 .deadline_seconds
301 .saturating_mul(1000),
302 elapsed_ms,
303 deadline: execution_context.deadline.clone(),
304 cost_budget_units: Some(agent_spec.budget_policy.max_tool_calls.into()),
305 degradation_markers: execution_context.degradation_markers.clone(),
306 };
307 let support = AiDENsRunSupportTierEvidenceV1 {
308 support_tier: agent_spec.support_label.to_string(),
309 supported: vec![
310 "AgentSpecV1 validation".into(),
311 "bounded PlanActVerifyLoopV1".into(),
312 "sandboxed local coding tools".into(),
313 "AiDENsRunBundleV3 inspection".into(),
314 ],
315 partial: vec!["canonical memory grounding evidence when enabled".into()],
316 deferred: vec![
317 "cloud-provider-execution".into(),
318 "broad-autonomous-daemon-behavior".into(),
319 "V10-runtime-geometry".into(),
320 ],
321 reason_codes: vec!["p26-supported-local-agent-spine".into()],
322 };
323 let memory_grounding = if loop_output.memory_grounding_receipts.is_empty() {
324 vec![if agent_spec.memory_policy.enabled {
325 "memory-grounding:enabled-no-receipt".into()
326 } else {
327 "memory-grounding:disabled-by-agent-spec".into()
328 }]
329 } else {
330 loop_output.memory_grounding_receipts.clone()
331 };
332 let outputs = vec![
333 out_dir.join("final.txt").display().to_string(),
334 out_dir
335 .join("plan-act-verify-output.json")
336 .display()
337 .to_string(),
338 out_dir.join("event-log.ndjson").display().to_string(),
339 receipt_root
340 .join("canonical-receipts.ndjson")
341 .display()
342 .to_string(),
343 ];
344 let blocked_checks = loop_output
345 .verification_receipts
346 .iter()
347 .filter(|receipt| !receipt.passed)
348 .map(|receipt| receipt.check.clone())
349 .collect::<Vec<_>>();
350 let mut bundle = loop_output.assemble_v3_bundle(
351 run_id.clone(),
352 agent_spec.profile.clone(),
353 execution_context,
354 event_log,
355 budget,
356 support,
357 vec![
358 agent_spec.support_label.to_string(),
359 "supported-local".into(),
360 ],
361 replay,
362 failure,
363 attempt_family_id,
364 attempt_id,
365 trial_id,
366 DisplayDigestV1::for_json_value(&spec_value),
367 memory_grounding,
368 outputs,
369 vec![
370 "re-run agent run with the same AgentSpecV1, task, sandbox root, and permit JSON"
371 .into(),
372 "inspect run-bundle.json and canonical receipt log before claiming replay success"
373 .into(),
374 ],
375 blocked_checks,
376 );
377 if let Some(output) = run_output {
378 bundle.provider_receipts = vec![output.receipt.receipt_id.to_string()];
379 bundle.tool_receipts = output
380 .receipt
381 .tool_invocation_receipts
382 .iter()
383 .map(|receipt| receipt.receipt_id.to_string())
384 .collect();
385 let mut permit_receipts: Vec<String> = output
386 .receipt
387 .permit_use_receipts
388 .iter()
389 .map(|receipt| receipt.receipt_id.to_string())
390 .collect();
391 if permit_receipts.is_empty() {
392 permit_receipts = output
393 .receipt
394 .approval_requests
395 .iter()
396 .map(|request| request.request_id.to_string())
397 .collect();
398 }
399 bundle.permit_receipts = permit_receipts;
400 }
401 bundle.support_labels.sort();
402 bundle.support_labels.dedup();
403 let bundle_store = RunBundleStore::open(RunBundleStoreConfig::for_receipt_root(&receipt_root))?;
404 let bundle_store_record = bundle_store.write_bundle(&bundle)?;
405 write_json_file(&out_dir.join("run-bundle.json"), &bundle)?;
406 write_json_file(
407 &out_dir.join("run-bundle-store-record.json"),
408 &bundle_store_record,
409 )?;
410 write_agent_final_artifact(&out_dir, &loop_output)?;
411 Ok(format!(
412 "AiDENs agent run\nspec: {}\ntask: {}\nsandbox: {}\noutput: {}\nrun_bundle: {}\nrun_bundle_store: {}\noutcome: {:?}",
413 spec_path.display(),
414 task_path.display(),
415 sandbox_root.display(),
416 out_dir.display(),
417 out_dir.join("run-bundle.json").display(),
418 bundle_store_record.bundle_path.display(),
419 loop_output.outcome
420 ))
421}
422
423fn load_agent_spec_file(path: &str) -> Result<(PathBuf, Value, AgentSpecV1)> {
424 let spec_path = resolve_cli_path(path)?;
425 let raw = std::fs::read_to_string(&spec_path)
426 .with_context(|| format!("failed to read AgentSpecV1 {}", spec_path.display()))?;
427 let value = parse_strict_json(&raw)
428 .with_context(|| format!("failed strict-parse AgentSpecV1 {}", spec_path.display()))?;
429 let spec: AgentSpecV1 = serde_json::from_value(value.clone())
430 .with_context(|| format!("failed to decode AgentSpecV1 {}", spec_path.display()))?;
431 Ok((spec_path, value, spec))
432}
433
434fn generated_schema_for_rust_type(rust_type: &str) -> Result<Value> {
435 generated_schema_documents()
436 .into_iter()
437 .find(|document| document.registration.rust_type == rust_type)
438 .map(|document| document.schema)
439 .ok_or_else(|| anyhow::anyhow!("generated schema missing for {rust_type}"))
440}
441
442fn resolve_input_or_spec_relative_path(
443 path: impl AsRef<Path>,
444 spec_root: &Path,
445) -> Result<PathBuf> {
446 let path = path.as_ref();
447 if path.is_absolute() || path.exists() {
448 return resolve_cli_path(path);
449 }
450 Ok(spec_root.join(path))
451}
452
453fn local_coding_agent_spec_value(agent_id: &str, display_name: &str) -> Value {
454 serde_json::json!({
455 "schema": "AgentSpecV1",
456 "agent_id": agent_id,
457 "display_name": display_name,
458 "support_label": "supported-local",
459 "profile": "coding",
460 "provider_policy": {
461 "provider": "local",
462 "cloud_allowed": false,
463 "fallback_allowed": false
464 },
465 "memory_policy": {
466 "enabled": false,
467 "mode": "canonical-seam",
468 "requires_view_disclosure": false
469 },
470 "tool_policy": {
471 "allowed_tools": [
472 "repo.read",
473 "repo.list",
474 "repo.search",
475 "patch.propose",
476 "patch.apply",
477 "checks.run",
478 "run.inspect"
479 ],
480 "write_tools_require_permit": true
481 },
482 "permit_policy": {
483 "writes": "operator-approved",
484 "commands": "operator-approved",
485 "network": "forbidden"
486 },
487 "verification_policy": {
488 "required_checks": ["schema", "sandbox", "digest", "support-claim"],
489 "fail_closed": true
490 },
491 "evidence_policy": {
492 "emit_run_bundle": true,
493 "emit_tool_receipts": true,
494 "emit_permit_receipts": true,
495 "emit_abstention_receipts": true
496 },
497 "budget_policy": {
498 "max_turns": 4,
499 "max_tool_calls": 8,
500 "deadline_seconds": 120
501 }
502 })
503}
504
505fn agent_default_mock_response(
506 spec: &AgentSpecV1,
507 sandbox_root: &Path,
508 task_text: &str,
509) -> Result<String> {
510 let tools = spec
511 .tool_policy
512 .allowed_tools
513 .iter()
514 .map(String::as_str)
515 .collect::<BTreeSet<_>>();
516 let lower_task = task_text.to_ascii_lowercase();
517 if lower_task.contains("apply") && tools.contains("patch.apply") {
518 let read_path = coding_agent_read_path(sandbox_root)?;
519 let diff = coding_agent_patch_diff(sandbox_root, &read_path)?;
520 return agent_tool_mock_script(
521 "aidens:patch-apply:1",
522 serde_json::json!({ "diff": diff }),
523 "patch apply finished with permit-gated evidence: {{last_tool_output_text}}",
524 );
525 }
526 if (lower_task.contains("check") || lower_task.contains("inspect"))
527 && (tools.contains("checks.run") || tools.contains("run.inspect"))
528 {
529 return agent_tool_mock_script(
530 "aidens:run-checks:1",
531 serde_json::json!({ "command": ["bash", "scripts/verify.sh"] }),
532 "check finished with permit-gated evidence: {{last_tool_output_text}}",
533 );
534 }
535 if lower_task.contains("search") && tools.contains("repo.search") {
536 return agent_tool_mock_script(
537 "aidens:repo-search:1",
538 serde_json::json!({
539 "query": agent_search_query(task_text),
540 "path": ".",
541 }),
542 "search finished with sandbox evidence: {{last_tool_output_text}}",
543 );
544 }
545 if lower_task.contains("list") && tools.contains("repo.list") {
546 return agent_tool_mock_script(
547 "aidens:repo-list:1",
548 serde_json::json!({ "path": ".", "max_entries": 50 }),
549 "list finished with sandbox evidence: {{last_tool_output_text}}",
550 );
551 }
552 if lower_task.contains("propose") && tools.contains("patch.propose") {
553 let read_path = coding_agent_read_path(sandbox_root)?;
554 let diff = coding_agent_patch_diff(sandbox_root, &read_path)?;
555 return agent_tool_mock_script(
556 "aidens:patch-propose:1",
557 serde_json::json!({
558 "summary": "AgentSpecV1 local patch proposal",
559 "diff": diff,
560 }),
561 "proposal finished without mutation: {{last_tool_output_text}}",
562 );
563 }
564 if tools.contains("repo.read") {
565 let read_path = coding_agent_read_path(sandbox_root)?;
566 return agent_tool_mock_script(
567 "aidens:repo-read:1",
568 serde_json::json!({ "path": read_path }),
569 "read finished with sandbox evidence: {{last_tool_content}}",
570 );
571 }
572 Ok("AgentSpecV1 local agent completed without tool calls.".into())
573}
574
575fn agent_tool_mock_script(tool_id: &str, input: Value, final_text: &str) -> Result<String> {
576 Ok(format!(
577 "{}{}{}",
578 serde_json::to_string(&serde_json::json!({
579 "tool_call": {
580 "tool_id": tool_id,
581 "input": input,
582 }
583 }))?,
584 TEST_AGENT_MOCK_RESPONSE_DELIMITER,
585 final_text
586 ))
587}
588
589fn agent_search_query(task_text: &str) -> String {
590 task_text
591 .split(|ch: char| !ch.is_ascii_alphanumeric())
592 .find(|part| part.len() >= 3)
593 .unwrap_or("AiDENs")
594 .to_string()
595}
596
597fn write_agent_loop_artifacts(
598 out_dir: &Path,
599 event_log_path: &Path,
600 output: &PlanActVerifyLoopV1Output,
601) -> Result<()> {
602 write_json_file(
603 &out_dir.join("plan-act-verify-output.json"),
604 &agent_loop_output_json(output),
605 )?;
606 if let Some(run) = output.run_output.as_ref() {
607 write_json_file(&out_dir.join("run-report.json"), &run.receipt)?;
608 write_json_file(&out_dir.join("turn-report.json"), &run.turn_receipt)?;
609 write_json_file(&out_dir.join("tool-exposure.json"), &run.tool_exposure)?;
610 write_json_file(
611 &out_dir.join("agency-policy-reports.json"),
612 &run.agency_policy_reports,
613 )?;
614 }
615 if let Some(abstention) = output.abstention_receipt.as_ref() {
616 write_json_file(
617 &out_dir.join("abstention.json"),
618 &agent_abstention_json(abstention),
619 )?;
620 }
621 if let Some(repair) = output.repair_plan.as_ref() {
622 write_json_file(
623 &out_dir.join("repair-plan.json"),
624 &agent_repair_json(repair),
625 )?;
626 }
627 write_agent_event_log(event_log_path, output)
628}
629
630fn write_agent_final_artifact(out_dir: &Path, output: &PlanActVerifyLoopV1Output) -> Result<()> {
631 let final_text = output
632 .run_output
633 .as_ref()
634 .map(|run| run.text.clone())
635 .or_else(|| {
636 output
637 .abstention_receipt
638 .as_ref()
639 .map(|receipt| format!("abstained: {}", receipt.reason_code))
640 })
641 .unwrap_or_else(|| format!("outcome: {:?}", output.outcome));
642 std::fs::write(out_dir.join("final.txt"), final_text)
643 .with_context(|| format!("failed to write {}", out_dir.join("final.txt").display()))
644}
645
646fn agent_loop_output_json(output: &PlanActVerifyLoopV1Output) -> Value {
647 let degradation = agent_degradation_markers(output);
648 let semantic_status = if degradation.is_empty() {
649 "exact_check"
650 } else {
651 "degraded_exact_check"
652 };
653 serde_json::json!({
654 "schema": "PlanActVerifyLoopV1OutputDisplay",
655 "agent_id": output.agent_id,
656 "app_id": output.app_id,
657 "profile": output.profile,
658 "support_label": output.support_label,
659 "outcome": format!("{:?}", output.outcome),
660 "turns_used": output.turns_used,
661 "max_turns": output.max_turns,
662 "plan_receipts": output.plan_receipts.iter().map(|receipt| serde_json::json!({
663 "receipt_id": receipt.receipt_id,
664 "plan_id": receipt.plan_id,
665 "step": receipt.step,
666 "action": receipt.action,
667 "reason_codes": receipt.reason_codes,
668 })).collect::<Vec<_>>(),
669 "tool_route_receipts": output.tool_route_receipts.iter().map(|receipt| serde_json::json!({
670 "receipt_id": receipt.receipt_id,
671 "plan_id": receipt.plan_id,
672 "step": receipt.step,
673 "requested_tool_ids": receipt.requested_tool_ids,
674 "exposed_tool_ids": receipt.exposed_tool_ids,
675 "reason_codes": receipt.reason_codes,
676 })).collect::<Vec<_>>(),
677 "tool_call_receipts": output.tool_call_receipts.iter().map(|receipt| serde_json::json!({
678 "receipt_id": receipt.receipt_id,
679 "plan_id": receipt.plan_id,
680 "step": receipt.step,
681 "run_id": receipt.run_id,
682 "permitted_tool_calls": receipt.permitted_tool_calls,
683 "blocked_tool_calls": receipt.blocked_tool_calls,
684 "succeeded_tool_calls": receipt.succeeded_tool_calls,
685 "failed_tool_calls": receipt.failed_tool_calls,
686 "reason_codes": receipt.reason_codes,
687 })).collect::<Vec<_>>(),
688 "verification_receipts": output.verification_receipts.iter().map(|receipt| serde_json::json!({
689 "receipt_id": receipt.receipt_id,
690 "step": receipt.step,
691 "check": receipt.check,
692 "passed": receipt.passed,
693 "reason_codes": receipt.reason_codes,
694 })).collect::<Vec<_>>(),
695 "memory_grounding_receipts": output.memory_grounding_receipts,
696 "abstention_receipt": output.abstention_receipt.as_ref().map(agent_abstention_json),
697 "repair_plan": output.repair_plan.as_ref().map(agent_repair_json),
698 "finalization": output.finalization.as_ref().map(|receipt| serde_json::json!({
699 "receipt_id": receipt.receipt_id,
700 "step": receipt.step,
701 "outcome": receipt.outcome,
702 "final_state": receipt.final_state,
703 "blocked": receipt.blocked,
704 "support_label": receipt.support_label,
705 "reason_codes": receipt.reason_codes,
706 })),
707 "semantic_disclosure": semantic_disclosure_value(
708 semantic_status,
709 output.support_label.clone(),
710 degradation,
711 vec![
712 "plan-receipts-emitted".into(),
713 "tool-route-and-call-receipts-emitted".into(),
714 "verification-receipts-emitted".into(),
715 "canonical-owner-disclosure-emitted".into(),
716 ],
717 vec![
718 "display output summarizes local runner receipts; receipt truth remains in canonical owner artifacts".into(),
719 ],
720 ),
721 "canonical_ownership": {
722 "memory_truth_owner": "semantic-memory/knowledge-runtime through aidens-memory-kit",
723 "tool_receipt_owner": "llm-tool-runtime through aidens-tool-kit",
724 "verification_truth_owner": "verification-control and verification-policy",
725 "repair_truth_owner": "canonical repair/governance crates; this is display evidence only"
726 }
727 })
728}
729
730fn agent_abstention_json(receipt: &aidens_runner::AbstentionReceiptV1) -> Value {
731 serde_json::json!({
732 "receipt_id": receipt.receipt_id,
733 "step": receipt.step,
734 "reason_code": receipt.reason_code,
735 "blocked_action": receipt.blocked_action,
736 "evidence": receipt.evidence,
737 "required_permits": receipt.required_permits,
738 "can_resume": receipt.can_resume,
739 "support_impact": receipt.support_impact,
740 })
741}
742
743fn agent_repair_json(receipt: &aidens_runner::RepairPlanDisplayReceiptV1) -> Value {
744 serde_json::json!({
745 "repair_id": receipt.repair_id,
746 "source_run_id": receipt.source_run_id,
747 "failure_kind": receipt.failure_kind,
748 "candidate_repair_actions": receipt.candidate_repair_actions,
749 "required_verification": receipt.required_verification,
750 "required_permits": receipt.required_permits,
751 "risk_level": receipt.risk_level,
752 "canonical_owner": receipt.canonical_owner,
753 "display_only": true,
754 })
755}
756
757fn write_agent_event_log(path: &Path, output: &PlanActVerifyLoopV1Output) -> Result<()> {
758 let mut events = vec![serde_json::json!({
759 "event": "agent_spec_loaded",
760 "agent_id": output.agent_id,
761 "support_label": output.support_label,
762 })];
763 for receipt in &output.plan_receipts {
764 events.push(serde_json::json!({
765 "event": "plan_receipt",
766 "receipt_id": receipt.receipt_id,
767 "step": receipt.step,
768 "action": receipt.action,
769 "reason_codes": receipt.reason_codes,
770 }));
771 }
772 for receipt in &output.tool_route_receipts {
773 events.push(serde_json::json!({
774 "event": "tool_route_receipt",
775 "receipt_id": receipt.receipt_id,
776 "requested_tool_ids": receipt.requested_tool_ids,
777 "exposed_tool_ids": receipt.exposed_tool_ids,
778 }));
779 }
780 for receipt in &output.tool_call_receipts {
781 events.push(serde_json::json!({
782 "event": "tool_call_receipt",
783 "receipt_id": receipt.receipt_id,
784 "permitted_tool_calls": receipt.permitted_tool_calls,
785 "blocked_tool_calls": receipt.blocked_tool_calls,
786 "succeeded_tool_calls": receipt.succeeded_tool_calls,
787 "failed_tool_calls": receipt.failed_tool_calls,
788 "reason_codes": receipt.reason_codes,
789 }));
790 }
791 for receipt in &output.verification_receipts {
792 events.push(serde_json::json!({
793 "event": "verification_receipt",
794 "receipt_id": receipt.receipt_id,
795 "check": receipt.check,
796 "passed": receipt.passed,
797 "reason_codes": receipt.reason_codes,
798 }));
799 }
800 for receipt in &output.memory_grounding_receipts {
801 events.push(serde_json::json!({
802 "event": "memory_grounding_receipt",
803 "receipt": receipt,
804 }));
805 }
806 if let Some(receipt) = output.abstention_receipt.as_ref() {
807 events.push(serde_json::json!({
808 "event": "abstention_receipt",
809 "receipt_id": receipt.receipt_id,
810 "reason_code": receipt.reason_code,
811 "blocked_action": receipt.blocked_action,
812 "required_permits": receipt.required_permits,
813 }));
814 }
815 if let Some(receipt) = output.repair_plan.as_ref() {
816 events.push(serde_json::json!({
817 "event": "repair_display_receipt",
818 "repair_id": receipt.repair_id,
819 "failure_kind": receipt.failure_kind,
820 "canonical_owner": receipt.canonical_owner,
821 }));
822 }
823 if let Some(receipt) = output.finalization.as_ref() {
824 events.push(serde_json::json!({
825 "event": "finalization_receipt",
826 "receipt_id": receipt.receipt_id,
827 "outcome": receipt.outcome,
828 "final_state": receipt.final_state,
829 "blocked": receipt.blocked,
830 "reason_codes": receipt.reason_codes,
831 }));
832 }
833 let mut lines = events
834 .iter()
835 .map(serde_json::to_string)
836 .collect::<std::result::Result<Vec<_>, _>>()?
837 .join("\n");
838 lines.push('\n');
839 std::fs::write(path, lines).with_context(|| format!("failed to write {}", path.display()))
840}
841
842fn agent_degradation_markers(output: &PlanActVerifyLoopV1Output) -> Vec<String> {
843 let mut markers = Vec::new();
844 if !matches!(output.outcome, PlanActVerifyOutcomeV1::Success) {
845 markers.push(format!("agent-outcome:{:?}", output.outcome));
846 }
847 if let Some(receipt) = output.abstention_receipt.as_ref() {
848 markers.push(format!("abstention:{}", receipt.reason_code));
849 }
850 if let Some(receipt) = output.finalization.as_ref() {
851 for reason in &receipt.reason_codes {
852 if reason.contains("provider-policy-local-routed-to-explicit-mock-fixture") {
853 markers.push(reason.clone());
854 }
855 }
856 }
857 for receipt in &output.verification_receipts {
858 if !receipt.passed {
859 markers.push(format!("verification-failed:{}", receipt.check));
860 }
861 }
862 markers.sort();
863 markers.dedup();
864 markers
865}
866
867fn agent_failure_taxonomy(output: &PlanActVerifyLoopV1Output) -> AiDENsRunFailureTaxonomyV1 {
868 let class = match output.outcome {
869 PlanActVerifyOutcomeV1::Success => AiDENsRunFailureClassV1::None,
870 PlanActVerifyOutcomeV1::Abstained => AiDENsRunFailureClassV1::OperatorAbstained,
871 PlanActVerifyOutcomeV1::RepairNeeded => AiDENsRunFailureClassV1::VerificationUnavailable,
872 PlanActVerifyOutcomeV1::Failed => AiDENsRunFailureClassV1::ToolFailed,
873 };
874 let mut reason_codes = output
875 .finalization
876 .as_ref()
877 .map(|receipt| receipt.reason_codes.clone())
878 .unwrap_or_default();
879 if let Some(receipt) = output.abstention_receipt.as_ref() {
880 reason_codes.push(receipt.reason_code.clone());
881 }
882 if reason_codes.is_empty() {
883 reason_codes.push(format!("agent-outcome:{:?}", output.outcome));
884 }
885 reason_codes.sort();
886 reason_codes.dedup();
887 AiDENsRunFailureTaxonomyV1 {
888 class,
889 reason_codes,
890 degraded: !matches!(output.outcome, PlanActVerifyOutcomeV1::Success),
891 blocked: output
892 .finalization
893 .as_ref()
894 .is_some_and(|receipt| receipt.blocked),
895 }
896}
897
898pub fn inspect_run_bundle_command(dir: &str) -> Result<String> {
899 let target = resolve_cli_path(dir)?;
900 let bundle_path = if target.is_file() {
901 target
902 } else if target.join("run-bundle.json").exists() {
903 target.join("run-bundle.json")
904 } else if target.join("run-bundles").join("index.ndjson").exists() {
905 RunBundleStore::open(RunBundleStoreConfig::for_receipt_root(&target))?
906 .single_bundle_path()?
907 } else {
908 target.join("run-bundle.json")
909 };
910 let dir = bundle_path
911 .parent()
912 .map(Path::to_path_buf)
913 .unwrap_or_else(|| PathBuf::from("."));
914 let bundle_text = std::fs::read_to_string(&bundle_path)
915 .with_context(|| format!("failed to read {}", bundle_path.display()))?;
916 let bundle: Value = parse_strict_json(&bundle_text)
917 .with_context(|| format!("failed strict-parse {}", bundle_path.display()))?;
918 let bundle_schema = bundle
919 .get("schema")
920 .and_then(Value::as_str)
921 .unwrap_or_default();
922 if !matches!(
923 bundle_schema,
924 schema if schema == AiDENsRunBundleV2::SCHEMA || schema == AiDENsRunBundleV3::SCHEMA
925 ) {
926 bail!("inspect-run requires AiDENsRunBundleV2 or AiDENsRunBundleV3");
927 }
928 let schema_rust_type = if bundle_schema == AiDENsRunBundleV3::SCHEMA {
929 "AiDENsRunBundleV3"
930 } else {
931 "AiDENsRunBundleV2"
932 };
933 let schema = generated_schema_for_rust_type(schema_rust_type)?;
934 let schema_validation = validate_json_schema(&schema, &bundle);
935 if !schema_validation.valid {
936 bail!(
937 "run bundle schema validation failed: {}",
938 schema_validation.errors.join("; ")
939 );
940 }
941 let mut required_pointers = vec![
942 "/canonical_execution_context",
943 "/canonical_execution_context/trace_ctx",
944 "/trace_ctx",
945 "/attempt_id",
946 "/trial_id",
947 "/event_log/digest",
948 "/event_log/replay_normalized_digest",
949 "/support/support_tier",
950 "/replay/replay_command",
951 "/failure/class",
952 ];
953 if bundle_schema == AiDENsRunBundleV3::SCHEMA {
954 required_pointers.extend([
955 "/attempt_family_id",
956 "/agent_spec_digest",
957 "/support_labels",
958 "/memory_grounding_receipts",
959 "/verification_receipts",
960 "/replay_instructions",
961 ]);
962 }
963 for pointer in required_pointers {
964 if bundle.pointer(pointer).is_none() {
965 bail!("run bundle missing required field {pointer}");
966 }
967 }
968 let event_log_declared = bundle
969 .pointer("/event_log/event_log_path")
970 .and_then(Value::as_str)
971 .ok_or_else(|| anyhow::anyhow!("run bundle missing event_log_path"))?;
972 let event_log_path = resolve_bundle_path(&dir, event_log_declared);
973 let event_log_text = std::fs::read_to_string(&event_log_path)
974 .with_context(|| format!("failed to read {}", event_log_path.display()))?;
975 let event_log_digest = StackContentDigest::compute_str(&event_log_text);
976 let event_log_digest_verified = bundle
977 .pointer("/event_log/digest")
978 .and_then(Value::as_str)
979 .is_some_and(|expected| expected == event_log_digest.hex());
980 let output_dir_receipt_log_path = dir.join("receipts").join("canonical-receipts.ndjson");
981 let store_root = receipt_store_root_for_bundle_dir(&dir);
982 let store_root_receipt_log_path = store_root.join("canonical-receipts.ndjson");
983 let canonical_log_path = if output_dir_receipt_log_path.exists() {
984 output_dir_receipt_log_path
985 } else {
986 store_root_receipt_log_path
987 };
988 let canonical_record_count = if canonical_log_path.exists() {
989 std::fs::read_to_string(&canonical_log_path)
990 .with_context(|| format!("failed to read {}", canonical_log_path.display()))?
991 .lines()
992 .filter(|line| !line.trim().is_empty())
993 .count()
994 } else {
995 0
996 };
997 let run_bundle_store_record_path = dir.join("run-bundle-store-record.json");
998 let store_record = if run_bundle_store_record_path.exists() {
999 Some(
1000 parse_strict_json(
1001 &std::fs::read_to_string(&run_bundle_store_record_path).with_context(|| {
1002 format!("failed to read {}", run_bundle_store_record_path.display())
1003 })?,
1004 )
1005 .with_context(|| {
1006 format!(
1007 "failed strict-parse {}",
1008 run_bundle_store_record_path.display()
1009 )
1010 })?,
1011 )
1012 } else if bundle_schema == AiDENsRunBundleV3::SCHEMA {
1013 let run_id = bundle
1014 .get("run_id")
1015 .and_then(Value::as_str)
1016 .unwrap_or_default();
1017 let store = RunBundleStore::open(RunBundleStoreConfig::for_receipt_root(&store_root))?;
1018 store
1019 .inspect(run_id)
1020 .ok()
1021 .and_then(|inspection| serde_json::to_value(inspection.record).ok())
1022 } else {
1023 None
1024 };
1025 let support_tier = bundle
1026 .pointer("/support/support_tier")
1027 .and_then(Value::as_str)
1028 .unwrap_or("unknown")
1029 .to_string();
1030 let mut semantic_degradation = bundle
1031 .pointer("/budget/degradation_markers")
1032 .and_then(Value::as_array)
1033 .map(|markers| {
1034 markers
1035 .iter()
1036 .filter_map(Value::as_str)
1037 .map(str::to_string)
1038 .collect::<Vec<_>>()
1039 })
1040 .unwrap_or_default();
1041 if bundle
1042 .pointer("/failure/degraded")
1043 .and_then(Value::as_bool)
1044 .unwrap_or(false)
1045 {
1046 semantic_degradation.push("failure-taxonomy-degraded".into());
1047 }
1048 if !event_log_digest_verified {
1049 semantic_degradation.push("event-log-digest-mismatch".into());
1050 }
1051 semantic_degradation.sort();
1052 semantic_degradation.dedup();
1053 let store_semantic_status = store_record
1054 .as_ref()
1055 .and_then(|record| record.get("semantic_status"))
1056 .and_then(Value::as_str)
1057 .unwrap_or("exact_check");
1058 let semantic_status = if !schema_validation.valid || !event_log_digest_verified {
1059 "failed_exact_check"
1060 } else if !semantic_degradation.is_empty() || store_semantic_status.contains("degraded") {
1061 "degraded_exact_check"
1062 } else {
1063 store_semantic_status
1064 };
1065 let semantic_disclosure = semantic_disclosure_value(
1066 semantic_status,
1067 support_tier.clone(),
1068 semantic_degradation,
1069 vec![
1070 "strict-json-parse-with-duplicate-key-rejection".into(),
1071 "generated-run-bundle-schema-validation".into(),
1072 "event-log-digest-verification".into(),
1073 "durable-run-bundle-store-record-inspection".into(),
1074 ],
1075 vec![
1076 "inspect-run is an AiDENs-local operator report; canonical receipt semantics remain in owner crates".into(),
1077 ],
1078 );
1079 let report = serde_json::json!({
1080 "schema": if bundle_schema == AiDENsRunBundleV3::SCHEMA {
1081 "AiDENsRunInspectReportV3"
1082 } else {
1083 "AiDENsRunInspectReportV2"
1084 },
1085 "bundle_dir": dir,
1086 "bundle_path": bundle_path,
1087 "bundle_schema": bundle.get("schema"),
1088 "run_id": bundle.get("run_id"),
1089 "support_tier": support_tier,
1090 "support": bundle.get("support"),
1091 "schema_validation": schema_validation,
1092 "support_labels": bundle.get("support_labels"),
1093 "trace_ctx": bundle.get("trace_ctx"),
1094 "attempt_family_id": bundle.get("attempt_family_id"),
1095 "attempt_id": bundle.get("attempt_id"),
1096 "trial_id": bundle.get("trial_id"),
1097 "agent_spec_digest": bundle.get("agent_spec_digest"),
1098 "provider_route": bundle.pointer("/canonical_execution_context/provider_route"),
1099 "budget": bundle.get("budget"),
1100 "degradation": bundle.pointer("/budget/degradation_markers"),
1101 "failure": bundle.get("failure"),
1102 "tool_receipts": bundle.get("tool_receipts"),
1103 "permit_receipts": bundle.get("permit_receipts"),
1104 "memory_grounding_receipts": bundle.get("memory_grounding_receipts"),
1105 "verification_receipts": bundle.get("verification_receipts"),
1106 "abstention_receipts": bundle.get("abstention_receipts"),
1107 "repair_plan_receipts": bundle.get("repair_plan_receipts"),
1108 "event_log_digest_verified": event_log_digest_verified,
1109 "event_log_digest": event_log_digest,
1110 "replay": bundle.get("replay"),
1111 "replay_instructions": bundle.get("replay_instructions"),
1112 "blocked_checks": bundle.get("blocked_checks"),
1113 "canonical_record_count": canonical_record_count,
1114 "canonical_record_log_path": canonical_log_path,
1115 "run_bundle_store_record": store_record,
1116 "semantic_disclosure": semantic_disclosure,
1117 "inspection_scope": "AiDENs-local operator report; canonical receipt semantics remain in owner crates.",
1118 });
1119 Ok(serde_json::to_string_pretty(&report)?)
1120}
1121
1122fn receipt_store_root_for_bundle_dir(dir: &Path) -> PathBuf {
1123 for ancestor in dir.ancestors() {
1124 if ancestor.file_name().and_then(|name| name.to_str()) == Some("run-bundles") {
1125 return ancestor
1126 .parent()
1127 .unwrap_or_else(|| Path::new("."))
1128 .to_path_buf();
1129 }
1130 }
1131 dir.parent()
1132 .unwrap_or_else(|| Path::new("."))
1133 .parent()
1134 .unwrap_or_else(|| Path::new("."))
1135 .to_path_buf()
1136}