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
//! `moat`: command-line entry point for OpenMoat.
//!
//! The binary owns all I/O (files, environment, terminal); decisions are made
//! by `openmoat-core`. Exit codes are a stable contract used by host hooks:
//! see [`exit::Code`].
mod anchor;
mod approvals;
mod cli;
mod commands;
mod context;
mod environment;
mod exit;
mod guard_relay;
mod hangs;
mod home;
mod install;
mod integrity;
mod json5;
mod mcp;
mod packs;
mod project;
mod protection;
mod push;
mod realpath;
mod render;
mod repo;
mod sandbox;
mod secrets;
mod signing;
mod taint;
mod terminal;
mod time;
mod trust;
mod upgrade;
use std::process::ExitCode;
use clap::Parser;
fn main() -> ExitCode {
// A host runs `moat guard` and blocks the tool call only on exit 2; any other
// failure lets the call through. So a `guard` that cannot even parse its
// arguments denies (ADR-015), and every other command reports usage errors
// with the dedicated code, never 2.
let failed = if invoked_as_hook() {
exit::Code::Deny
} else {
exit::Code::Usage
};
let cli = match cli::Cli::try_parse() {
Ok(cli) => cli,
Err(error) => {
let informational = !error.use_stderr();
let _ = error.print();
return if informational {
exit::Code::Ok
} else {
failed
}
.into();
}
};
match commands::run(cli) {
Ok(code) => code.into(),
// Never for `guard`: a hook that cannot answer must not exit 0 (ADR-015).
Err(error) if !invoked_as_hook() && broken_pipe(&error) => exit::Code::Ok.into(),
Err(error) => {
eprintln!("moat: {error:#}");
failed.into()
}
}
}
/// True when the first argument is `guard`, the subcommand host hooks run.
fn invoked_as_hook() -> bool {
std::env::args_os().nth(1).is_some_and(|arg| arg == "guard")
}
/// A reader that stops early (`moat show | head`) closes the pipe. Like other
/// command-line tools, stop quietly instead of reporting it as an error.
fn broken_pipe(error: &anyhow::Error) -> bool {
error.chain().any(|cause| {
let kind = cause
.downcast_ref::<std::io::Error>()
.map(std::io::Error::kind)
.or_else(|| cause.downcast_ref::<serde_json::Error>()?.io_error_kind());
kind == Some(std::io::ErrorKind::BrokenPipe)
})
}