use std::path::Path;
use serde_json::Value;
use crate::cli::args::ProjectOtelArgs;
use crate::exit_codes::{EXIT_CONFIG_ERROR, EXIT_SUCCESS};
fn read_json(path: &Path) -> anyhow::Result<Value> {
let text = std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("cannot read {}: {e}", path.display()))?;
serde_json::from_str(&text)
.map_err(|e| anyhow::anyhow!("invalid JSON in {}: {e}", path.display()))
}
pub fn run(args: ProjectOtelArgs) -> anyhow::Result<i32> {
if let Some(bundle) = args.evidence_bundle.as_deref() {
return run_tool_decision_truth(bundle, args.out.as_deref());
}
let Some(capability_surface_path) = args.capability_surface.as_deref() else {
eprintln!("error: one of --capability-surface or --evidence-bundle is required");
return Ok(EXIT_CONFIG_ERROR);
};
let capability_surface = match read_json(capability_surface_path) {
Ok(v) => v,
Err(e) => {
eprintln!("error: {e}");
return Ok(EXIT_CONFIG_ERROR);
}
};
let observation_health = match args
.observation_health
.as_deref()
.map(read_json)
.transpose()
{
Ok(v) => v,
Err(e) => {
eprintln!("error: {e}");
return Ok(EXIT_CONFIG_ERROR);
}
};
let enforcement_health = match args
.enforcement_health
.as_deref()
.map(read_json)
.transpose()
{
Ok(v) => v,
Err(e) => {
eprintln!("error: {e}");
return Ok(EXIT_CONFIG_ERROR);
}
};
let projection = assay_core::otel::projection::project(
&capability_surface,
observation_health.as_ref(),
enforcement_health.as_ref(),
);
let json = serde_json::to_string_pretty(&projection)?;
match &args.out {
Some(path) => {
if let Err(e) = std::fs::write(path, format!("{json}\n")) {
eprintln!("error: cannot write {}: {e}", path.display());
return Ok(EXIT_CONFIG_ERROR);
}
}
None => println!("{json}"),
}
Ok(EXIT_SUCCESS)
}
fn run_tool_decision_truth(bundle: &Path, out: Option<&Path>) -> anyhow::Result<i32> {
let file = match std::fs::File::open(bundle) {
Ok(f) => f,
Err(e) => {
eprintln!("error: cannot open bundle {}: {e}", bundle.display());
return Ok(EXIT_CONFIG_ERROR);
}
};
let reader = match assay_evidence::bundle::BundleReader::open(file) {
Ok(r) => r,
Err(e) => {
eprintln!("error: bundle integrity verification failed: {e}");
return Ok(EXIT_CONFIG_ERROR);
}
};
let events = match reader.events_vec() {
Ok(e) => e,
Err(e) => {
eprintln!("error: cannot read bundle events: {e}");
return Ok(EXIT_CONFIG_ERROR);
}
};
let (report, verified) =
crate::cli::commands::evidence::verify_tool_decision_truth::verify_and_collect(&events);
if !report.ok {
eprintln!("error: tool-decision-truth verification failed; refusing to project unverified evidence");
for check in report.checks.iter().filter(|c| !c.ok) {
eprintln!(" fail: {} — {}", check.id, check.detail);
}
return Ok(EXIT_CONFIG_ERROR);
}
let projection = assay_core::otel::projection::project_tool_decision_truth(&verified);
let json = serde_json::to_string_pretty(&projection)?;
match out {
Some(path) => {
if let Err(e) = std::fs::write(path, format!("{json}\n")) {
eprintln!("error: cannot write {}: {e}", path.display());
return Ok(EXIT_CONFIG_ERROR);
}
}
None => println!("{json}"),
}
Ok(EXIT_SUCCESS)
}
#[cfg(test)]
mod tdt_bundle_tests {
use super::*;
use assay_core::mcp::policy::McpPolicy;
use assay_core::mcp::tool_decision_truth::{self as tdt, DecisionEvidence};
use assay_evidence::bundle::BundleWriter;
use assay_evidence::types::EvidenceEvent;
use serde_json::{json, Value};
fn carrier() -> Value {
let policy: McpPolicy = serde_json::from_value(json!({
"version": "1",
"tools": {"allow": ["deploy"], "deny": ["delete_all"]},
"schemas": {"deploy": {"type": "object", "required": ["env"],
"properties": {"env": {"enum": ["staging", "prod"]}}}},
"enforcement": {"unconstrained_tools": "warn"}
}))
.unwrap();
tdt::build_classified_record(
&policy,
"deploy",
&json!({"env": "prod"}),
0,
b"project-otel-test-key-v0",
"fixture-kid-v0",
"authoritative_boundary",
"c0",
"ok",
"present",
&DecisionEvidence::default(),
)
.unwrap()
}
fn ev(type_: &str, payload: Value, seq: u64) -> EvidenceEvent {
EvidenceEvent::new(type_, "urn:assay:test", "run", seq, payload)
}
fn write_bundle(path: &Path, events: Vec<EvidenceEvent>) {
let f = std::fs::File::create(path).unwrap();
let mut w = BundleWriter::new(f);
for e in events {
w.add_event(e);
}
w.finish().unwrap();
}
#[test]
fn projects_a_verified_bundle() {
let dir = tempfile::tempdir().unwrap();
let bundle = dir.path().join("tdt.tar.gz");
let out = dir.path().join("projection.json");
let c = carrier();
let row = tdt::pack_recipe_row(
&c,
c["decision_verdict"].as_str().unwrap(),
"assay://evidence-event/run/0",
)
.unwrap();
write_bundle(
&bundle,
vec![
ev("assay.tool_decision_truth.v0", c, 0),
ev("assay.tool_decision_truth.recipe_row.v0", row, 1),
],
);
let code = run_tool_decision_truth(&bundle, Some(&out)).unwrap();
assert_eq!(code, EXIT_SUCCESS);
let p: Value = serde_json::from_str(&std::fs::read_to_string(&out).unwrap()).unwrap();
let attrs = &p["spans"][0]["attributes"];
assert_eq!(attrs["assay.claim_class"], json!("derived"));
assert_eq!(attrs["openinference.span.kind"], json!("TOOL"));
assert!(attrs["assay.tdt.carrier_content_digest"].is_string());
}
#[test]
fn refuses_to_write_an_unverified_bundle() {
let dir = tempfile::tempdir().unwrap();
let bundle = dir.path().join("tdt.tar.gz");
let out = dir.path().join("projection.json");
write_bundle(
&bundle,
vec![ev("assay.tool_decision_truth.v0", carrier(), 0)],
);
let code = run_tool_decision_truth(&bundle, Some(&out)).unwrap();
assert_eq!(code, EXIT_CONFIG_ERROR);
assert!(!out.exists());
}
}