nucleus-substrate-cli 0.1.0

Command-line client for the Nucleus substrate. Single binary `nucleus-substrate` driving offline receipt verification (signature + all-projections), live hub fetches, and counter inspection.
//! `nucleus-substrate` — CLI driving the Nucleus substrate SDK.
//!
//! v0.1 subcommands:
//!
//!   * `verify <receipt.json> [--hub <url>] [--jwks <file>]` — read a
//!     receipt, fetch JWKS (or load from file), run
//!     [`nucleus_substrate_sdk::verify_receipt_fully`], print
//!     PASS/FAIL.
//!   * `fetch <auction_id> --hub <url>` — fetch a receipt for an
//!     auction and write it to stdout.
//!   * `counters --hub <url>` — print the hub's public counter JSON.
//!   * `agent-card --hub <url>` — print the hub's A2A AgentCard JSON.

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 a receipt offline.
    Verify {
        /// Receipt JSON file path (or `-` for stdin).
        receipt_path: String,
        /// Hub base URL — used to fetch JWKS when `--jwks` is absent.
        #[arg(long, value_hint = ValueHint::Url, env = "NUCLEUS_HUB_URL")]
        hub: Option<String>,
        /// Pre-fetched JWKS JSON file (skips the network call).
        #[arg(long, value_hint = ValueHint::FilePath)]
        jwks: Option<String>,
    },
    /// Fetch a receipt for `auction_id` and write JSON to stdout.
    Fetch {
        auction_id: String,
        #[arg(long, value_hint = ValueHint::Url, env = "NUCLEUS_HUB_URL")]
        hub: String,
    },
    /// Print the hub's `/v1/metrics/counters` body.
    Counters {
        #[arg(long, value_hint = ValueHint::Url, env = "NUCLEUS_HUB_URL")]
        hub: String,
    },
    /// Print the hub's A2A AgentCard.
    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);
        }
    }
}