use anyhow::{Context, Result};
use clap::{Parser, Subcommand, ValueHint};
use nucleus_substrate_sdk::{Client, verify_receipt_fully};
#[derive(Parser, Debug)]
#[command(
name = "nucleus-substrate",
about = "Verifiable agent substrate — Identity + Capability + Flow + Economic compose into a signed receipt",
version,
propagate_version = true
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
Verify {
receipt_path: String,
#[arg(long, value_hint = ValueHint::Url, env = "NUCLEUS_HUB_URL")]
hub: Option<String>,
#[arg(long, value_hint = ValueHint::FilePath)]
jwks: Option<String>,
},
Fetch {
auction_id: String,
#[arg(long, value_hint = ValueHint::Url, env = "NUCLEUS_HUB_URL")]
hub: String,
},
Counters {
#[arg(long, value_hint = ValueHint::Url, env = "NUCLEUS_HUB_URL")]
hub: String,
},
AgentCard {
#[arg(long, value_hint = ValueHint::Url, env = "NUCLEUS_HUB_URL")]
hub: String,
},
}
#[tokio::main]
async fn main() {
rustls::crypto::ring::default_provider().install_default().ok();
if let Err(e) = run().await {
eprintln!("error: {e:#}");
std::process::exit(1);
}
}
async fn run() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Verify {
receipt_path,
hub,
jwks,
} => verify(&receipt_path, hub.as_deref(), jwks.as_deref()).await,
Command::Fetch { auction_id, hub } => {
let client = Client::new(&hub)?;
let receipt = client.fetch_receipt(&auction_id).await?;
println!("{}", serde_json::to_string_pretty(&receipt)?);
Ok(())
}
Command::Counters { hub } => {
let client = Client::new(&hub)?;
let counters = client.counters().await?;
println!("{}", serde_json::to_string_pretty(&counters)?);
Ok(())
}
Command::AgentCard { hub } => {
let client = Client::new(&hub)?;
let card = client.agent_card().await?;
println!("{}", serde_json::to_string_pretty(&card)?);
Ok(())
}
}
}
async fn verify(
receipt_path: &str,
hub: Option<&str>,
jwks_path: Option<&str>,
) -> Result<()> {
use nucleus_substrate_sdk::Receipt;
let receipt_bytes = if receipt_path == "-" {
use std::io::Read;
let mut buf = Vec::new();
std::io::stdin()
.read_to_end(&mut buf)
.context("reading receipt from stdin")?;
buf
} else {
std::fs::read(receipt_path)
.with_context(|| format!("reading receipt from {receipt_path}"))?
};
let receipt: Receipt =
serde_json::from_slice(&receipt_bytes).context("parsing receipt JSON")?;
let jwks = if let Some(path) = jwks_path {
let bytes = std::fs::read(path)
.with_context(|| format!("reading JWKS from {path}"))?;
serde_json::from_slice(&bytes).context("parsing JWKS JSON")?
} else if let Some(hub_url) = hub {
let client = Client::new(hub_url)?;
client.jwks().await?
} else {
anyhow::bail!("either --jwks <file> or --hub <url> is required");
};
match verify_receipt_fully(&receipt, &jwks) {
Ok(report) => {
println!("VERIFY PASS");
println!(" session: {}", receipt.session.session_id);
println!(" issuer_kid: {}", receipt.session.issuer_kid);
println!(" root_hash: {}", receipt.root_hash_hex);
println!(" projections:");
for k in &report.projection_kinds {
println!(" - {k}");
}
if let Some(sub) = report.identity_subject {
println!(" identity sub: {sub}");
}
if report.has_adversarial_bid {
eprintln!(
" WARNING: flow projection reports `has_adversarial_bid=true` — \
mechanism truthfulness cannot be assumed for this clearing."
);
}
Ok(())
}
Err(e) => {
eprintln!("VERIFY FAIL: {e:#}");
std::process::exit(2);
}
}
}