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
mod audit;
mod canary;
mod commands;
mod compose;
mod egress_log;
mod manifest;
mod paths;
mod proc;
mod secrets;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "moor", about = "Containerized, keel-driven AI sandboxes")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Create a new project: manifest, sandbox+egress containers, optional
/// GitHub repo, `keel init` inside the sandbox.
New {
name: String,
/// moor/base, moor/node, moor/rust, or moor/python
#[arg(long, default_value = "moor/base:latest")]
image: String,
/// Also create a private GitHub repo via `gh repo create` (asks
/// for confirmation before doing anything).
#[arg(long)]
github: bool,
},
/// Bring an existing local git repo (e.g. one you're already working
/// on outside moor) into a new sandboxed project. Transfers its
/// full history via a `git bundle` — the source directory is never
/// bind-mounted, only a one-shot bundle file crosses into the
/// container. Preserves an existing GitHub remote if there is one.
Import {
name: String,
/// Path to the existing local repo to import.
#[arg(long, value_name = "PATH")]
from: PathBuf,
/// moor/base, moor/node, moor/rust, or moor/python
/// — auto-detected from the source repo (Cargo.toml, package.json,
/// pyproject.toml/requirements.txt) if not given.
#[arg(long)]
image: Option<String>,
/// If the source repo has no GitHub remote, create one (asks for
/// confirmation). Ignored if it already has one.
#[arg(long)]
github: bool,
},
/// Start (or restart) a project's sandbox + egress containers.
Up { name: String },
/// Stop a project's containers.
Down { name: String },
/// Open an interactive shell inside a project's sandbox.
Shell { name: String },
/// Run one command inside a project's sandbox (e.g. `keel run <spec>`).
Run {
name: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
cmd: Vec<String>,
},
/// Run `keel` inside a project's sandbox — sugar for
/// `moor run <project> -- keel <args...>`. Project defaults to
/// whatever `moor use` last set (or the one project you have, if
/// there's only one); pass `--project` to override.
Keel {
#[arg(short = 'p', long = "project")]
project: Option<String>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Print one of keel's spec-produced markdown artifacts (spec.md,
/// plan.md, tasks.md) for a given spec slug, lightly highlighted.
/// Project defaults the same way `moor keel` does.
View {
#[arg(short = 'p', long = "project")]
project: Option<String>,
/// The spec slug, e.g. `blast-radius`.
slug: String,
/// spec | plan | tasks
artifact: String,
},
/// Set the default project `moor keel`/`moor view` (and their
/// `--project`-taking siblings) use when it's omitted.
Use { name: String },
/// List all projects and their container status.
Status,
/// Show the host-side audit trail for a project (folds in new egress
/// gateway log entries first).
Audit {
name: String,
/// Recompute the hash chain and report whether it's intact instead
/// of printing the trail.
#[arg(long)]
verify: bool,
/// Export the chain plus the latest keel evidence bundle as a
/// tar.gz into this directory instead of printing the trail.
#[arg(long, value_name = "DIR")]
export: Option<PathBuf>,
},
/// Verify a running project's container actually has every hardening
/// control applied (read-only rootfs, dropped capabilities, no bind
/// mounts, non-root user, no default-bridge network, ...) and run the
/// active breakout battery (canary domain, read-only fs, docker.sock).
Selftest { name: String },
/// Manage a project's secrets in the macOS Keychain, as an
/// alternative to exporting them into your shell before every
/// `moor up`/`new`.
#[command(subcommand)]
Secrets(SecretsCommand),
/// Drive keel's spec -> gate -> plan -> gate -> run -> gate pipeline
/// from a loosely-described recipe file, stopping whenever a stage
/// needs a human decision (spec/plan/merge approval, or a gate that
/// still fails after retrying). Re-run the same command to continue
/// once you've approved or fixed things by hand — see
/// docs/decisions/0005-recipe.md.
Recipe {
name: String,
/// Path to the recipe file (YAML front matter — slug, scope —
/// followed by a free-text description of the desired outcome).
file: PathBuf,
},
/// Show (or follow) a running `moor recipe`'s progress — stage
/// transitions, gate attempts, pauses for approval — from the same
/// tamper-evident audit chain `moor audit` reads, so you can check
/// where it's at from a different terminal than the one driving it.
/// Not deep detail: for full command output, see `moor audit`.
Logs {
name: String,
/// Keep watching for new events instead of exiting after printing
/// what's there so far.
#[arg(short, long)]
follow: bool,
/// How many of the most recent events to print before following.
#[arg(short = 'n', long, default_value_t = 20)]
lines: usize,
},
}
#[derive(Subcommand)]
enum SecretsCommand {
/// Store a secret's value in the Keychain (prompts for it, hidden
/// where the terminal supports it). Overwrites any existing value.
Set { project: String, name: String },
/// Remove a secret from the Keychain.
Unset { project: String, name: String },
/// Show where each of the project's declared secrets (moor.yaml's
/// `secrets:` list) would currently be resolved from: your shell's
/// environment, the Keychain, or neither.
Status { project: String },
}
fn main() {
let cli = Cli::parse();
let result = match cli.command {
Command::New {
name,
image,
github,
} => commands::new_cmd::run(&name, &image, github),
Command::Import {
name,
from,
image,
github,
} => commands::import_cmd::run(&name, &from, image, github),
Command::Up { name } => commands::up::run(&name),
Command::Down { name } => commands::down::run(&name),
Command::Shell { name } => commands::shell::run(&name),
Command::Run { name, cmd } => commands::run_cmd::run(&name, &cmd),
Command::Keel { project, args } => commands::resolve_project_announced(project)
.and_then(|name| commands::keel_cmd::run(&name, &args)),
Command::View {
project,
slug,
artifact,
} => commands::resolve_project_announced(project)
.and_then(|name| commands::view::run(&name, &slug, &artifact)),
Command::Use { name } => commands::use_cmd::run(&name),
Command::Status => commands::status::run(),
Command::Audit {
name,
verify,
export,
} => commands::audit_cmd::run(&name, verify, export),
Command::Selftest { name } => commands::selftest::run(&name),
Command::Secrets(SecretsCommand::Set { project, name }) => secrets::set(&project, &name),
Command::Secrets(SecretsCommand::Unset { project, name }) => {
secrets::unset(&project, &name)
}
Command::Secrets(SecretsCommand::Status { project }) => {
commands::secrets_status::run(&project)
}
Command::Recipe { name, file } => commands::recipe::run(&name, &file),
Command::Logs {
name,
follow,
lines,
} => commands::logs::run(&name, follow, lines),
};
if let Err(e) = result {
eprintln!("error: {e:?}");
std::process::exit(1);
}
}