openlatch-client 0.1.18

OpenLatch runtime enforcement node — the capture-and-enforce client for the AI Operations Platform
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
//! CLI command tree for the `openlatch` binary.
//!
//! This module defines the full clap command structure, global flags, noun-verb
//! aliasing, and the helper for resolving output configuration from parsed CLI args.
//!
//! ## Command grammar
//!
//! Primary verbs: `init`, `status`, `start`, `stop`, `restart`, `logs`, `doctor`,
//! `uninstall`, `docs`
//!
//! Noun-verb aliases: `hooks install` → `init`, `hooks uninstall` → `uninstall`,
//! `daemon start` → `start`, `daemon stop` → `stop`, `daemon restart` → `restart`

pub mod color;
pub mod commands;
pub mod header;
pub mod output;

use clap::{Args, Parser, Subcommand, ValueEnum};

use crate::cli::output::OutputConfig;

/// The top-level CLI struct parsed by clap.
#[derive(Parser)]
#[command(
    name = "openlatch",
    // `version` alone resolves to CARGO_PKG_VERSION, which cannot tell a build
    // from `main` apart from the release it post-dates — the exact question
    // `openlatch --version` is run to answer. Same source as `status` and the
    // wire `clientversion` (see build.rs).
    version = env!("OPENLATCH_VERSION"),
    about = "OpenLatch runtime enforcement node — capture and enforce inside every AI agent",
    after_help = "Run 'openlatch <command> --help' for more information on a command."
)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Option<Commands>,

    /// Output format: human (default), json
    #[arg(long, global = true, default_value = "human")]
    pub format: OutputFormat,

    /// Alias for --format json
    #[arg(long, global = true)]
    pub json: bool,

    /// Show verbose output
    #[arg(long, short = 'v', global = true)]
    pub verbose: bool,

    /// Show debug output (implies --verbose)
    #[arg(long, global = true)]
    pub debug: bool,

    /// Suppress all output except errors
    #[arg(long, short = 'q', global = true)]
    pub quiet: bool,

    /// Disable colored output
    #[arg(long, global = true)]
    pub no_color: bool,
}

/// Output format selection.
#[derive(Clone, ValueEnum)]
pub enum OutputFormat {
    Human,
    Json,
}

/// Top-level subcommands.
#[derive(Subcommand)]
pub enum Commands {
    /// Initialize OpenLatch — detect agent, install hooks, start daemon
    #[command(visible_alias = "setup")]
    Init(InitArgs),

    /// Show daemon status, uptime, event counts
    Status,

    /// Start the daemon
    Start(StartArgs),

    /// Stop the daemon
    Stop,

    /// Restart the daemon
    Restart,

    /// View event logs
    Logs(LogsArgs),

    /// Diagnose configuration and connectivity issues
    Doctor(DoctorArgs),

    /// Remove hooks and stop daemon
    Uninstall(UninstallArgs),

    /// Open documentation in browser
    Docs,

    /// Hook management subcommands (noun-verb alias: 'hooks install' = 'init')
    Hooks {
        #[command(subcommand)]
        cmd: HooksCommands,
    },

    /// Daemon management subcommands (noun-verb alias: 'daemon start' = 'start')
    #[command(hide = true)]
    Daemon {
        #[command(subcommand)]
        cmd: DaemonCommands,
    },

    /// Authenticate with OpenLatch cloud
    Auth {
        #[command(subcommand)]
        cmd: AuthCommands,
    },

    /// Manage anonymous usage telemetry (opt-out)
    Telemetry {
        #[command(subcommand)]
        cmd: TelemetryCommands,
    },

    /// Manage OS-native supervision (auto-restart on login/boot)
    Supervision {
        #[command(subcommand)]
        cmd: SupervisionCommands,
    },

    /// Check for or apply an auto-update of the openlatch binaries
    Update(commands::update::UpdateArgs),

    /// Inspect the configuration plane: list observed sources, tail config
    /// events, force a rescan, and report monitor health.
    Inventory {
        #[command(subcommand)]
        cmd: InventoryCommands,
    },

    /// (maintainers only) Phase 1 spike — verify, swap, rollback. Not for end users.
    #[command(name = "__spike-update", hide = true)]
    SpikeUpdate(commands::spike_update::SpikeUpdateArgs),

    /// Inspect the model-boundary listener (status/explain).
    Boundary {
        #[command(subcommand)]
        cmd: BoundaryCommands,
    },

    /// (maintainers only) Boundary verification benches. Not for end users.
    #[command(hide = true)]
    Bench {
        #[command(subcommand)]
        cmd: BenchCommands,
    },
}

/// Subcommands under `openlatch boundary`.
///
/// Read-only by design. The agent's `ANTHROPIC_BASE_URL` is written and removed
/// by the daemon that holds the pinned port — there is no command to set it by
/// hand, because a hand-written value outlives the listener it names. To route
/// agents at the provider directly, stop the daemon (`openlatch stop`); to route
/// them through the boundary, start it.
#[derive(Subcommand, Clone)]
pub enum BoundaryCommands {
    /// Report the boundary listener's up/down state and pinned port.
    Status,
    /// Print a prefix-churn block LOCALLY by its finding id (the content never
    /// leaves the host — it is read from the on-disk retention store).
    Explain {
        /// The `finding_id` from an `ai.openlatch.prefix.finding_id` field.
        finding_id: String,
    },
}

/// Subcommands under `openlatch bench` (maintainer/CI verification only).
#[derive(Subcommand, Clone)]
pub enum BenchCommands {
    /// D-24: inject a panic into the observe path; prove the forward is
    /// unmodified, the failure is recorded, and the PID is unchanged.
    PanicIsolation(BenchPanicArgs),
    /// D-05/D-25: prove the pinned port rebinds the same port within 2s and
    /// that an occupied port fails loudly rather than re-probing.
    PortStability,
    /// I-1 §7 DoD: write a canonical stream of `ai.openlatch.economics.*` events
    /// (the fixture corpus I-2 builds against) to stdout as JSONL.
    ExportFixtures,
    /// D-08: prove the capture path (observe/tokenize/session/churn) makes ZERO
    /// outbound connections — it holds no network client by construction.
    TransformEgress,
    /// D-05: re-evaluate a fixed matching request against a baseline transform rule
    /// N times and prove every would-have output is byte-identical, with zero
    /// outbound connections during evaluation.
    Replay(BenchReplayArgs),
    /// D-14/D-19: run a FIXED scripted conversation against Anthropic and write
    /// per-turn provider usage + TTFT as JSON. `--direct` is the control (straight
    /// to the provider); default routes through the boundary listener. Dev/CI-only:
    /// it makes REAL network calls and needs `ANTHROPIC_API_KEY`.
    CacheBaseline(BenchCacheBaselineArgs),
    /// D-14/D-19: compare two cache-baseline JSON reports — token-weighted
    /// cache-hit-rate delta, p95 TTFT delta, and the two-part G-2 gate (the no-MCP
    /// case is the D-19 release gate; an MCP case is disclosed, not gated).
    Compare(BenchCompareArgs),
}

/// Arguments for `bench panic-isolation`.
#[derive(Args, Clone)]
pub struct BenchPanicArgs {
    /// Which fault to inject. `observe` panics inside the observe stub.
    #[arg(long, default_value = "observe")]
    pub inject: String,
}

/// Arguments for `bench replay`.
#[derive(Args, Clone)]
pub struct BenchReplayArgs {
    /// Which baseline rule to replay (`OL-ECO-001` or `OL-ECO-002`).
    #[arg(long, default_value = "OL-ECO-001")]
    pub rule: String,
    /// How many times to re-evaluate the would-have. All outputs must match.
    #[arg(long, default_value = "100")]
    pub runs: u64,
}

/// Arguments for `bench cache-baseline`.
#[derive(Args, Clone)]
pub struct BenchCacheBaselineArgs {
    /// Number of scripted turns (requests). The D-19 gate needs >= 100.
    #[arg(long, default_value = "100")]
    pub turns: usize,
    /// Inject MCP tool definitions into the request. This workload is MEASURED &
    /// DISCLOSED, never gated (MCP forces tool defs into the invalidatable prefix).
    #[arg(long)]
    pub with_mcp: bool,
    /// Send straight to https://api.anthropic.com (the control). Default sends
    /// through the boundary listener on the pinned loopback port.
    #[arg(long)]
    pub direct: bool,
    /// Output path for the per-turn usage JSON. Defaults to stdout when omitted.
    #[arg(long, value_name = "FILE")]
    pub out: Option<std::path::PathBuf>,
    /// Positional alternative to `--out`. Defaults to stdout when omitted.
    #[arg(value_name = "OUT")]
    pub out_positional: Option<std::path::PathBuf>,
}

/// Arguments for `bench compare`.
#[derive(Args, Clone)]
pub struct BenchCompareArgs {
    /// Baseline report A (typically the `--direct` control).
    #[arg(value_name = "A")]
    pub a: std::path::PathBuf,
    /// Comparison report B (typically the through-layer run).
    #[arg(value_name = "B")]
    pub b: std::path::PathBuf,
}

/// Subcommands under `openlatch inventory`.
#[derive(Subcommand, Clone)]
pub enum InventoryCommands {
    /// List observed config sources from the local audit log.
    List(commands::inventory::ListArgs),
    /// Tail the local audit log filtered to `ai.openlatch.config.*` events.
    Log(commands::inventory::LogArgs),
    /// Force a manifest rescan now (POSTs `/admin/inventory/rescan`).
    Rescan(commands::inventory::RescanArgs),
    /// Report config-monitor health: cache size, manifest loaded, enabled flag.
    Status(commands::inventory::StatusArgs),
    /// Detail view for one source — current content_hash + pending alerts.
    Inspect(commands::inventory::InspectArgs),
    /// Show currently-known project roots inferred from cached config paths.
    Projects(commands::inventory::ProjectsArgs),
    /// Acknowledge a pending alert so it stops being injected at next interaction.
    Ack(commands::inventory::AckArgs),
}

/// Subcommands under `openlatch telemetry`.
#[derive(Subcommand)]
pub enum TelemetryCommands {
    /// Show consent state, deciding rule, and build inclusion
    Status,
    /// Opt in to anonymous usage telemetry
    Enable,
    /// Opt out of anonymous usage telemetry
    Disable,
    /// Disable and wipe in-memory queue in any running daemon
    Purge,
    /// Show how to run any command with debug-mode event output
    Debug,
}

/// Subcommands under `openlatch supervision`.
#[derive(Subcommand)]
pub enum SupervisionCommands {
    /// Install the OS supervisor so the daemon auto-starts on login/boot
    Install,
    /// Remove the OS supervisor (stops auto-start; does not stop the daemon)
    Uninstall,
    /// Show supervision state (installed, running, backend)
    Status,
    /// Re-arm supervision after `disable` (re-installs the OS artifact)
    Enable,
    /// Remove the OS artifact and mark supervision as disabled
    Disable,
}

/// Subcommands under `openlatch hooks`.
#[derive(Subcommand)]
pub enum HooksCommands {
    /// Install hooks (same as 'openlatch init')
    Install(InitArgs),
    /// Remove hooks (same as 'openlatch uninstall')
    Uninstall(UninstallArgs),
    /// Show hook status
    Status,
}

/// Subcommands under `openlatch daemon`.
#[derive(Subcommand)]
pub enum DaemonCommands {
    /// Start the daemon (same as 'openlatch start')
    Start(StartArgs),
    /// Stop the daemon (same as 'openlatch stop')
    Stop,
    /// Restart the daemon (same as 'openlatch restart')
    Restart,
}

/// Subcommands under `openlatch auth`.
#[derive(Subcommand)]
pub enum AuthCommands {
    /// Log in to OpenLatch (opens browser for authentication)
    Login(AuthLoginArgs),
    /// Log out and remove stored credentials
    Logout,
    /// Show authentication status
    Status,
}

/// Arguments for the `auth login` subcommand.
#[derive(Args, Clone)]
pub struct AuthLoginArgs {
    /// Skip browser open; print URL only
    #[arg(long)]
    pub no_browser: bool,
}

/// Arguments for the `init` subcommand.
#[derive(Args, Clone)]
pub struct InitArgs {
    /// Run in foreground (no background daemon)
    #[arg(long)]
    pub foreground: bool,
    /// Re-probe port and update configuration (use when port conflicts arise)
    #[arg(long)]
    pub reconfig: bool,
    /// Install hooks and generate token without starting the daemon
    #[arg(long)]
    pub no_start: bool,
    /// Accept anonymous usage telemetry without prompting
    #[arg(long, alias = "yes-telemetry", conflicts_with = "no_telemetry")]
    pub telemetry: bool,
    /// Decline anonymous usage telemetry without prompting
    #[arg(long)]
    pub no_telemetry: bool,
    /// Skip OS-native supervision install (launchd / systemd-user / Task Scheduler).
    /// Persistence is default-on — passing this flag keeps the daemon manually-managed only.
    #[arg(long)]
    pub no_persistence: bool,
    /// Do NOT point the agent at the model-boundary listener. The boundary is
    /// on by default (secure-by-default) — it writes `ANTHROPIC_BASE_URL` + the
    /// install-id header, which disables Claude Code Remote Control. Pass this to
    /// keep the agent connected directly to the provider.
    ///
    /// Persists `[boundary] enabled = false` in config.toml, so the opt-out
    /// survives the OS supervisor restarting the daemon. Re-enable by setting it
    /// back to `true` there, or by re-running `init` without this flag.
    #[arg(long)]
    pub no_boundary: bool,
    /// Platform origin to forward events to, written to `[cloud] api_url` in
    /// config.toml. Use for local development against a self-hosted platform,
    /// e.g. `--api-url http://127.0.0.1:5183`. Omit to keep the current value.
    #[arg(long, value_name = "URL")]
    pub api_url: Option<String>,
}

/// Arguments for the `start` subcommand.
#[derive(Args, Clone)]
pub struct StartArgs {
    /// Run in foreground mode
    #[arg(long)]
    pub foreground: bool,
    /// Port to listen on (overrides config)
    #[arg(long)]
    pub port: Option<u16>,
    /// Loopback port for the model boundary (overrides config; default 7600).
    ///
    /// A non-default port makes this instance ISOLATED: it binds the port but
    /// does not write `~/.claude/settings.json`, and prints the
    /// `ANTHROPIC_BASE_URL` to export instead. Use it to run a second daemon
    /// alongside the machine's own without stealing its agent wiring.
    ///
    /// Applies to this invocation only — like `--port`, it does not persist, so
    /// a later `openlatch status` / `doctor` / `stop` in a different shell reads
    /// the config default and reports on the wrong instance. For a whole
    /// isolated shell, export the equivalent variables instead:
    /// `OPENLATCH_DIR`, `OPENLATCH_PORT`, `OPENLATCH_BOUNDARY_PORT`.
    #[arg(long, value_name = "PORT")]
    pub boundary_port: Option<u16>,
}

/// Arguments for the `doctor` subcommand.
#[derive(Args, Clone, Default)]
pub struct DoctorArgs {
    /// Trigger a controlled panic to validate the crash-report pipeline (hidden)
    #[arg(long, hide = true)]
    pub trigger_panic: bool,

    /// Auto-heal common issues (config, hooks, daemon, binaries). Creates .bak backups.
    #[arg(long, conflicts_with = "restore")]
    pub fix: bool,

    /// Restore files from the most recent .bak backups created by --fix.
    #[arg(long, conflicts_with = "fix")]
    pub restore: bool,

    /// Bundle diagnostics into a ZIP for sharing with OpenLatch support.
    #[arg(long)]
    pub rescue: bool,

    /// Override time window for --rescue log collection (e.g. 7d, 4h).
    #[arg(long, value_name = "DURATION", requires = "rescue")]
    pub since: Option<String>,

    /// Skip the inventory confirmation prompt on --rescue (for scripting).
    #[arg(long, short = 'y', requires = "rescue")]
    pub yes: bool,

    /// Override rescue output path.
    #[arg(long, value_name = "PATH", requires = "rescue")]
    pub output: Option<std::path::PathBuf>,
}

/// Arguments for the `logs` subcommand.
#[derive(Args, Clone)]
pub struct LogsArgs {
    /// Follow log output (live tail)
    #[arg(long, short = 'f')]
    pub follow: bool,

    /// Show events since this time (e.g., "1h", "30m", "2024-01-01")
    #[arg(long)]
    pub since: Option<String>,

    /// Number of recent events to show
    #[arg(long, short = 'n', default_value = "20")]
    pub lines: usize,

    /// Show tamper-evidence events from `~/.openlatch/tamper.jsonl` instead
    /// of hook events. Each line is either a tamper_detected or
    /// tamper_healed entry emitted by the daemon reconciler.
    #[arg(long)]
    pub tamper: bool,
}

/// Arguments for the `uninstall` subcommand.
#[derive(Args, Clone)]
pub struct UninstallArgs {
    /// Also remove ~/.openlatch/ directory and all data
    #[arg(long)]
    pub purge: bool,

    /// Skip confirmation prompt
    #[arg(long, short = 'y')]
    pub yes: bool,
}

/// Resolve the parsed CLI flags into a single [`OutputConfig`].
///
/// `--json` flag takes precedence over `--format`. `--no-color` flag,
/// `NO_COLOR` env var, and TTY detection are all applied via [`color::is_color_enabled`].
pub fn build_output_config(cli: &Cli) -> OutputConfig {
    let format = if cli.json {
        output::OutputFormat::Json
    } else {
        match cli.format {
            OutputFormat::Json => output::OutputFormat::Json,
            OutputFormat::Human => output::OutputFormat::Human,
        }
    };

    let color_enabled = color::is_color_enabled(cli.no_color);

    OutputConfig {
        format,
        verbose: cli.verbose || cli.debug,
        debug: cli.debug,
        quiet: cli.quiet,
        color: color_enabled,
    }
}