assay-cli 5.3.0

Policy-as-code gate for MCP agent tool calls, with verifiable evidence and Linux kernel enforcement.
use super::common::OutputFormat;
use clap::Parser;
use std::path::PathBuf;

#[derive(Parser, Clone)]
pub struct CalibrateArgs {
    /// Path to a run.json file to analyze (if omitted, reads from DB)
    #[arg(long)]
    pub run: Option<PathBuf>,

    #[arg(long, default_value = ".eval/eval.db")]
    pub db: PathBuf,

    /// Test suite name (required if using --db)
    #[arg(long)]
    pub suite: Option<String>,

    /// Number of recent runs to include from DB
    #[arg(long, default_value_t = 200)]
    pub last: u32,

    /// Output JSON path
    #[arg(long, default_value = "calibration.json")]
    pub out: PathBuf,

    /// Target tail for recommended min score (e.g. 0.10 for p10)
    #[arg(long, default_value_t = 0.10)]
    pub target_tail: f64,
}

#[derive(clap::Args, Debug, Clone)]
pub struct DoctorArgs {
    #[arg(long)]
    pub config: Option<std::path::PathBuf>,

    #[arg(long)]
    pub trace_file: Option<std::path::PathBuf>,

    #[arg(long)]
    pub baseline: Option<std::path::PathBuf>,

    #[arg(long)]
    pub db: Option<std::path::PathBuf>,

    #[arg(long, default_value = "false")]
    pub replay_strict: bool,

    // Typed rather than a `String` with a `== "json"` test downstream (#2039). The declaration
    // here used to read `// text|json` while `--format totally-invalid` silently produced text and
    // exited 0 — a documented set that nothing enforced. A `//` comment, not a doc comment: this
    // is why the type is what it is, and `--help` is not where that belongs.
    /// Output format for the report.
    #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
    pub format: OutputFormat,

    #[arg(long)]
    pub out: Option<std::path::PathBuf>,

    /// Diagnose and offer/apply automated fixes for known issues
    #[arg(long)]
    pub fix: bool,

    /// Apply all available fixes without interactive confirmation
    #[arg(long)]
    pub yes: bool,

    /// Preview fixes without writing files
    #[arg(long)]
    pub dry_run: bool,
}

#[derive(clap::Args, Debug, Clone)]
pub struct WatchArgs {
    #[arg(long, default_value = "eval.yaml")]
    pub config: std::path::PathBuf,

    #[arg(long)]
    pub trace_file: Option<std::path::PathBuf>,

    #[arg(long)]
    pub baseline: Option<std::path::PathBuf>,

    #[arg(long, default_value = ".eval/eval.db")]
    pub db: std::path::PathBuf,

    #[arg(long)]
    pub strict: bool,

    #[arg(long, default_value = "false")]
    pub replay_strict: bool,

    /// Clear terminal before each rerun
    #[arg(long)]
    pub clear: bool,

    /// Debounce file events before rerunning (milliseconds)
    #[arg(long, default_value_t = 350)]
    pub debounce_ms: u64,
}

#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum MaxRisk {
    Low,
    Medium,
    High,
}

#[derive(clap::Args, Debug, Clone)]
pub struct FixArgs {
    #[arg(long, default_value = "assay.yaml")]
    pub config: std::path::PathBuf,

    #[arg(long)]
    pub trace_file: Option<std::path::PathBuf>,

    #[arg(long)]
    pub baseline: Option<std::path::PathBuf>,

    #[arg(long, default_value = "false")]
    pub replay_strict: bool,

    /// Apply all suggested patches without prompting
    #[arg(long)]
    pub yes: bool,

    /// Do not write files; show diffs of what would change
    #[arg(long)]
    pub dry_run: bool,

    /// Only apply patch(es) with these id(s). Can be repeated.
    #[arg(long)]
    pub only: Vec<String>,

    /// Skip patches above this risk level
    #[arg(long, value_enum, default_value_t = MaxRisk::High)]
    pub max_risk: MaxRisk,

    /// List suggested patches (after --only/--max-risk filtering) and exit
    #[arg(long)]
    pub list: bool,
}

/// The shapes `sandbox --profile` can write. A type rather than a string: the fallback used to
/// write YAML into a file the caller had named as JSON, and derive the evidence path from the raw
/// string, so the record described bytes that were not on disk.
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum ProfileFormat {
    Yaml,
    Json,
}

impl ProfileFormat {
    /// The spelling that names this format in a path or an evidence record. Derived from the
    /// variant so the bytes on disk and the name in the record cannot disagree.
    pub fn as_str(self) -> &'static str {
        match self {
            ProfileFormat::Yaml => "yaml",
            ProfileFormat::Json => "json",
        }
    }
}

#[derive(clap::Args, Debug, Clone)]
pub struct SandboxArgs {
    /// Command to run in the sandbox
    #[arg(allow_hyphen_values = true, required = true, trailing_var_arg = true)]
    pub command: Vec<String>,

    /// Path to policy file (optional)
    #[arg(long, short)]
    pub policy: Option<std::path::PathBuf>,

    /// Working directory for command
    #[arg(long, short)]
    pub workdir: Option<std::path::PathBuf>,

    /// Timeout in seconds
    #[arg(long)]
    pub timeout: Option<u64>,

    /// Active enforcement: hard-block unauthorized actions
    #[arg(long)]
    pub enforce: bool,

    /// Dry-run mode: allow and log unauthorized actions (exit 4 if they occur)
    #[arg(long, conflicts_with = "enforce")]
    pub dry_run: bool,

    /// Fail if policy cannot be enforced (exit 2)
    #[arg(long)]
    pub fail_closed: bool,

    /// Enforce the network policy as a Landlock TCP-connect port allowlist (ABI >= 4). Allowlist-only:
    /// `net.allow` must be explicit TCP ports; any IP/CIDR, host, non-TCP protocol, range, deny rule,
    /// or port 0 fails closed. Requires --enforce.
    #[arg(long = "enforce-net", requires = "enforce")]
    pub enforce_net: bool,

    /// With --enforce-net, run a self-probe before the workload: from inside the enforcing ruleset,
    /// attempt one connect to an ephemeral denied port. Only a real block (EACCES + the harness
    /// listener never reached) writes the `probe` block into enforcement_health.v1. A probe that does
    /// not prove a block does not fail the run; it is reported, never silently dropped.
    #[arg(long = "probe-enforcement", requires = "enforce_net")]
    pub probe_enforcement: bool,

    /// Write the `assay.enforcement_health.v1` artifact (Landlock TCP-connect domain) to this path.
    /// A requested artifact that cannot be written is a command failure, never a silent absence.
    #[arg(long = "enforcement-health")]
    pub enforcement_health: Option<PathBuf>,

    // Six digests -- subject, substrate, corpus, catch policy, observation vocabulary, run entropy
    // -- supplied by whoever owns the AEE run. The sandbox is one enforcement episode inside a run,
    // not the run, and it cannot derive them; the seventh input, the network posture, is the one it
    // genuinely observes and is built here rather than carried (#2093).
    //
    // `//`, not `///`. A long doc comment here flips clap into multi-line help for the whole
    // command, which moves `[possible values]` onto its own line and breaks `--profile-format`'s
    // advertisement. That is the same mistake this repo made on `doctor --format` earlier the same
    // day: why a flag exists belongs next to the code, not in `--help`.
    /// AEE run context for the seal
    #[arg(long, requires = "aee_seal_key", requires = "aee_seal")]
    pub aee_run_context: Option<PathBuf>,

    // The key signs the run-end seal inside this process, at the moment enforcement is still in
    // force. A later command signing a health artifact off disk would claim a past moment.
    /// Substrate observation key descriptor
    #[arg(long, requires = "aee_run_context")]
    pub aee_seal_key: Option<PathBuf>,

    // Absent, no seal is emitted and the run is unchanged.
    /// Where to write the signed seal envelope
    #[arg(long, requires = "aee_run_context")]
    pub aee_seal: Option<PathBuf>,

    // The seal's `aeeObservedSet` is a digest over the interception and examination records the run
    // built. Emitting the digest without them leaves a member only this producer can re-derive,
    // which is the party it exists to constrain (#2135). Optional rather than implied by
    // `--aee-seal`, because a caller who wants only the signed payload should not silently get a
    // second file; asking for the commitment's inputs is a separate decision.
    /// Where to write the observation records the seal commits to (NDJSON)
    #[arg(long, requires = "aee_seal")]
    pub aee_records: Option<PathBuf>,

    /// Strict env mode: only safe base vars + explicit allows
    #[arg(long = "env-strict")]
    pub env_strict: bool,

    /// Strip execution-influence vars (LD_PRELOAD, etc.)
    #[arg(long = "env-strip-exec")]
    pub env_strip_exec: bool,

    /// Allow specific env vars through the filter (comma-separated or repeated)
    #[arg(long = "env-allow", value_delimiter = ',')]
    pub env_allow: Option<Vec<String>>,

    /// DANGER: Pass all env vars without scrubbing
    #[arg(long = "env-passthrough")]
    pub env_passthrough: bool,

    /// Force a safe PATH (/usr/bin:/bin on Linux)
    #[arg(long = "env-safe-path")]
    pub env_safe_path: bool,

    /// Profile execution and generate policy suggestion at this path
    #[arg(long)]
    pub profile: Option<PathBuf>,

    /// Profile output format: yaml | json (default: yaml)
    #[arg(long, default_value = "yaml")]
    pub profile_format: ProfileFormat,

    /// Optional path for human-readable profile report
    #[arg(long)]
    pub profile_report: Option<PathBuf>,

    /// Emit observed effects as a canonical evidence bundle (.tar.gz) at this path.
    /// Requires --profile (the bundle is built from the profiled observations).
    #[arg(long, requires = "profile")]
    pub bundle: Option<PathBuf>,

    /// Emit observed tool effects as OTel GenAI execute_tool spans (semconv JSONL)
    /// at this path, each carrying the claim-class outcome. Requires --profile.
    #[arg(long = "otel-jsonl", requires = "profile")]
    pub otel_jsonl: Option<PathBuf>,

    /// Show detailed sandbox setup
    #[arg(long, short)]
    pub verbose: bool,

    /// Suppress banner output
    #[arg(long, short)]
    pub quiet: bool,
}