assay-cli 3.35.0

Policy-as-code gate for MCP agent tool calls, with verifiable evidence and Linux kernel enforcement.
use super::ToolArgs;
use crate::cli::commands::kill::KillArgs;
use clap::{Parser, Subcommand};
use std::path::PathBuf;

#[derive(Parser, Debug)]
pub struct McpArgs {
    #[command(subcommand)]
    pub cmd: McpSub,
}

#[derive(Subcommand, Debug)]
pub enum McpSub {
    /// Wrap an MCP server process
    Wrap(McpWrapArgs),
    /// Detect config path and generate setup
    ConfigPath(ConfigPathArgs),
    /// Discover MCP servers on this machine
    Discover(DiscoverArgs),
    /// Emit the assay.mcp_server_inventory.v0 carrier (coverage-honest, hashed command/args)
    Inventory(InventoryArgs),
    /// Kill or terminate MCP server processes
    Kill(KillArgs),
    /// Sign and verify MCP tool definitions
    Tool(ToolArgs),
}

#[derive(Parser, Clone, Debug)]
pub struct ConfigPathArgs {
    /// Client to configure: claude|cursor
    pub client: String,

    /// Policy path to include in config
    #[arg(long)]
    pub policy: Option<String>,

    /// Wrapped server config (command)
    #[arg(long)]
    pub server: Option<String>,

    /// Output JSON only
    #[arg(long)]
    pub json: bool,
}

#[derive(clap::Args, Debug, Clone)]
pub struct SetupArgs {
    /// Dry run: show what would be installed but do not make changes (default)
    #[arg(long, default_value_t = true)]
    pub dry_run: bool,

    /// Apply changes: actually perform installation (may require sudo)
    #[arg(long, conflicts_with = "dry_run")]
    pub apply: bool,

    /// Install helper binary from this local path (e.g. target/release/assay-bpf)
    #[arg(long)]
    pub helper_from: Option<PathBuf>,

    /// Installation prefix for binary (default: /usr/local/bin)
    #[arg(long, default_value = "/usr/local/bin")]
    pub prefix: PathBuf,

    /// Runtime directory for socket (default: /run/assay)
    #[arg(long, default_value = "/run/assay")]
    pub runtime_dir: PathBuf,

    /// Non-interactive mode (for CI/automation)
    #[arg(long)]
    pub non_interactive: bool,
}

#[derive(Parser, Clone, Debug)]
pub struct McpWrapArgs {
    /// Policy file (default: assay.yaml)
    #[arg(long, default_value = "assay.yaml")]
    pub policy: PathBuf,

    /// Log decisions but do not block
    #[arg(long)]
    pub dry_run: bool,

    /// Print decisions to stderr
    #[arg(long)]
    pub verbose: bool,

    /// Unique label for this server (used for tool identity)
    #[arg(long)]
    pub label: Option<String>,

    /// Write lifecycle events (mandate.used, mandate.revoked) to this NDJSON log.
    /// Requires --event-source. May contain duplicates on retries; deduplicate by CloudEvents.id.
    #[arg(long, requires = "event_source")]
    pub audit_log: Option<PathBuf>,

    /// Write tool decision events (assay.tool.decision) to this NDJSON log.
    /// Requires --event-source.
    #[arg(long, requires = "event_source")]
    pub decision_log: Option<PathBuf>,

    /// Generate a coverage_report_v1 from wrap decision events at session end.
    /// Requires --event-source. If --decision-log is omitted, a temporary decision log is used.
    #[arg(long, requires = "event_source")]
    pub coverage_out: Option<PathBuf>,

    /// Write a session_state_window_v1 informational report after the wrapped MCP session completes.
    #[arg(long, requires = "event_source")]
    pub state_window_out: Option<PathBuf>,

    /// CloudEvents source URI (e.g. assay://org/app).
    /// Must be absolute URI with scheme://. Required when --audit-log or --decision-log is set.
    #[arg(long, value_name = "URI")]
    pub event_source: Option<String>,

    /// EXPERIMENTAL (unstable): opt-in tool-decision-truth carrier producer. Append
    /// `assay.tool_decision_truth.v0` carriers (one JSON object per line) to this NDJSON file during the
    /// run, as a separate sink alongside the decision log. Requires ASSAY_TDT_HMAC_KEY and
    /// ASSAY_TDT_HMAC_KEY_ID in the environment; the producer fails closed at startup if either is
    /// missing or malformed. The key is read from the environment, held in memory only, and never logged
    /// or persisted. Evidence-only: no runtime action is taken on the verdict.
    #[arg(long = "tool-decision-truth-out", value_name = "PATH")]
    pub tool_decision_truth_out: Option<PathBuf>,

    /// Command to wrap (use -- to separate args)
    #[arg(required = true, last = true, allow_hyphen_values = true)]
    pub command: Vec<String>,

    /// Fail if deprecated v1 policy format is detected
    #[arg(long)]
    pub deny_deprecations: bool,
}

#[derive(clap::Args, Debug, Clone)]
pub struct DiscoverArgs {
    /// Scan local machine (config files & processes)
    #[clap(long, default_value_t = true)]
    pub local: bool,

    /// Output format (table, json, yaml)
    #[clap(long, default_value = "table")]
    pub format: String,

    /// Write output to file
    #[arg(short, long)]
    pub output: Option<PathBuf>,

    /// Fail if specific conditions met (comma separated): unmanaged, no_auth
    #[arg(long, value_delimiter = ',')]
    pub fail_on: Option<Vec<String>>,

    /// Policy file to use for configuration
    #[arg(long)]
    pub policy: Option<PathBuf>,
}

#[derive(clap::Args, Debug, Clone)]
pub struct InventoryArgs {
    /// Write the assay.mcp_server_inventory.v0 carrier to this path ("-" or omitted = stdout).
    #[arg(long, default_value = "-")]
    pub out: String,

    /// Scope the scan to config files only: skip running-process discovery and report
    /// process_scan coverage as not_scanned (honest: not looked), never partial. Deterministic,
    /// useful for CI and reproducible inventory review.
    #[arg(long)]
    pub no_process_scan: bool,
}