Skip to main content

aidens_cli/
agent.rs

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}