1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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,
}