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
//! # procage
//!
//! Process sandboxing for Rust. procage runs a child process
//! in a cage on macOS (Seatbelt, applied through libsandbox rather than the
//! deprecated `/usr/bin/sandbox-exec`) and on Linux (Landlock, seccomp and
//! namespaces), without helper binaries. It is meant for commands that run
//! untrusted, adaptive code as your user: an LLM agent's tool calls, a
//! project's build scripts.
//!
//! **Status.** Pre-1.0: the API may change between minor versions. The
//! backends are `macos` and `linux` (tiers T1 and
//! T2); each module states its limits, and the [threat model](threat_model)
//! lists every known escape class and its status.
//!
//! Policies are written with [`policy`] (the `procage-policy` crate): a
//! portable intent layer, lowered for each backend.
//!
//! ```no_run
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use procage::native::{Cage, Grants}; // procage::macos or procage::linux
//! use procage::policy::{AbsPath, Access, Base, FsRule, FsTarget, Policy};
//!
//! let mut policy = Policy::default();
//! policy.base = Base::V3; // what ordinary programs and developer tools need
//! policy.fs.push(FsRule::new(Access::Read, FsTarget::Subpath(AbsPath::new("/tmp/project")?)));
//!
//! let cage = Cage::new(&policy)?;
//! let status = cage
//! .spawn(&mut std::process::Command::new("/bin/ls"), &Grants::new())?
//! .wait()?;
//! # let _ = status;
//! # Ok(())
//! # }
//! ```
//!
//! # Lifetime and hygiene
//!
//! The run ends when the [`Child`](native::Child) handle is dropped or
//! `kill_all` is called: every process the run started is killed,
//! double-forked descendants included. The child receives only standard
//! input, output and error (hand others over with `spawn_with_fds`); its
//! environment follows the policy's [`EnvPolicy`](policy::EnvPolicy) (see
//! [`hygiene`]); declaring the `tmpdir` grant gives each run a private
//! `TMPDIR`.
//!
//! # Helpers: becoming the caged command
//!
//! Sometimes other code starts the process: an MCP client, a terminal,
//! `tokio::process::Command` with `kill_on_drop`. It holds the pid, the
//! pipes and the exit status, and it kills the process when it is done.
//! Give that code a small helper program instead of the command. The helper
//! builds the cage and calls `Cage::exec`, which confines the helper itself
//! and executes the command in its place:
//!
//! ```no_run
//! # fn main() {
//! use std::process::Command;
//! use procage::native::{Cage, Grants};
//! use procage::policy::{Base, Policy};
//!
//! let mut policy = Policy::default();
//! policy.base = Base::V3;
//! let cage = Cage::new(&policy).unwrap_or_else(|e| panic!("{e}"));
//! let mut command = Command::new("/usr/bin/env");
//! command.current_dir("/tmp"); // a directory the policy lets it read
//! let err = cage.exec(&mut command, &Grants::new()); // returns only on failure
//! eprintln!("helper: {err}");
//! std::process::exit(126);
//! # }
//! ```
//!
//! On the other side, start the helper in its own process group and end it
//! by killing the group:
//!
//! ```no_run
//! # fn main() -> std::io::Result<()> {
//! use std::os::unix::process::CommandExt;
//!
//! let helper = std::process::Command::new("my-helper")
//! .arg("--")
//! .arg("server")
//! .process_group(0)
//! .spawn()?;
//! // … later: kill -9 -<pid> ends the command and the descendants in its group.
//! # let _ = helper;
//! # Ok(())
//! # }
//! ```
//!
//! The helper must be single-threaded on Linux tier T1 (call `exec` early
//! in `main`). On Linux, descendants cannot leave the process group under
//! the default [`Lifetime::Run`](policy::Lifetime::Run), and the command
//! dies with the helper's parent. On macOS a descendant that starts its own
//! session is not reached by the group kill; use `Cage::spawn`, whose handle
//! finds it, when that matters. For a command whose descendants start
//! process groups (test runners), use `Cage::exec_session`: start the
//! helper as a plain child and end the run with `end_session(pid)` before
//! reaping it. The `examples/exec_helper.rs` program in the repository is a
//! complete helper.
//!
//! # Fitting a policy to the host
//!
//! A host that cannot enforce part of a policy refuses the cage and lists
//! each part as a capability gap: procage never runs a child under a weaker
//! policy on its own. A harness that would rather run with a known, stated
//! limit than not at all names the relaxations it accepts and calls
//! `Cage::fit`. It gets the cage and the list of what was relaxed, each with
//! what would lift it, to show its user:
//!
//! ```no_run
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use procage::native::{Cage, CageOptions};
//! use procage::policy::{AbsPath, Access, FsRule, FsTarget, HardDenyClass, Policy, Relax};
//!
//! let mut policy = Policy::default();
//! policy.fs.push(FsRule::new(Access::Write, FsTarget::Subpath(AbsPath::new("/w")?)));
//! policy.fs.push(FsRule::new(Access::Protect, FsTarget::Subpath(AbsPath::new("/w/.git")?)));
//!
//! let relax = Relax::new()
//! .leave_out_protect() // e.g. `.git` inside a writable workspace
//! .waive(HardDenyClass::TrustHandoff)
//! .signals_same_user(); // Linux older than 6.12
//! let (cage, relaxed) = Cage::fit(&policy, &CageOptions::default(), &relax)?;
//! for r in &relaxed {
//! eprintln!("sandbox: {r}");
//! // fs-protect /w/.git on Linux T2 (Landlock ABI 8): the protect rule is
//! // left out; tier T1 (user namespaces) would enforce it
//! }
//! # let _ = cage;
//! # Ok(())
//! # }
//! ```
//!
//! Each relaxation has one effect: leave out the protect or hide rule a gap
//! names, waive a hard-deny class, allow no Unix sockets or no network
//! (stricter), or let signals reach every process of the user. Any gap
//! without an accepted relaxation still fails, with every remaining gap.
//! Approximations are a different consent and stay in
//! [`LowerOptions::allow_degrade`](policy::LowerOptions::allow_degrade).
//!
//! # Violations: what the sandbox denied, and why
//!
//! A denied operation reaches the caged program as `EPERM`, which it may
//! print, swallow or misreport. A cage built with
//! `CageOptions::with_violations` also tells the harness: after the child
//! exits, `Child::violations` lists each denied operation with the process,
//! the target and the kind of rule that denied it, so the harness can
//! explain the failure or ask its user for a grant.
//!
//! ```no_run
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use std::process::Command;
//! use procage::native::{Cage, CageOptions, Grants};
//! # let policy = procage::policy::Policy::default();
//!
//! let cage = Cage::with_options(&policy, &CageOptions::default().with_violations())?;
//! let mut child = cage.spawn(&mut Command::new("/usr/bin/make"), &Grants::new())?;
//! child.wait()?;
//! for v in &child.violations() {
//! eprintln!("sandbox: {v}");
//! // make (pid 812) was denied file-write-create /Users/a/out.o: not granted by the policy
//! }
//! # Ok(())
//! # }
//! ```
//!
//! A record says whether it is complete and what it can show ([`Coverage`]):
//! every denial on macOS, every refused system call on Linux. Identical denials are merged into one entry with a count
//! ([`Violation::repeats`]). Reporting is diagnostic. Enforcement never
//! depends on it.
//!
//! # Receipts: evidence of how a run was caged
//!
//! When a run has exited, its child handle gives a [`Receipt`]: the policy's
//! digest ([`policy_digest`]), the preset and base, the backend and tier, the
//! OS release and build or the kernel release, everything the cage accepted short of the policy
//! (approximations, `fit` relaxations, waivers, and one field that says
//! whether there was any), the violation record's coverage and count, and
//! how the run ended. Store it with the command's output:
//!
//! ```no_run
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use procage::native::{Cage, Grants};
//! use procage::policy::presets;
//!
//! let cage = Cage::new(&presets::minimal::v3())?;
//! let mut child = cage.spawn(&mut std::process::Command::new("/bin/ls"), &Grants::new())?;
//! child.wait()?;
//! let receipt = child.receipt().expect("the child exited");
//! println!("{}", receipt.to_json());
//! # Ok(())
//! # }
//! ```
//!
//! A receipt is the caller's own unsigned record, not an attestation: keep
//! it outside every path a later run may write. An empty, complete violation
//! record in it does not prove that nothing was denied (macOS reports are
//! best effort). It holds no environment value, argument or path unless the
//! cage was built with `CageOptions::with_policy_in_receipt`, which adds
//! every path of the policy to each receipt. A reader refuses a newer version
//! or an unknown field. See the [`receipt`] module for the details.
//!
//! # Audits: what path rules cannot stop
//!
//! Before every spawn, a protected, hidden or credential file with a second
//! name (a hard link) outside the protected set refuses the spawn
//! (`SpawnError::HardLinks`; see `CageOptions::with_link_exemption` and
//! [`LinkAudit`]). With `CageOptions::with_handoff_audit`, `Child::audit`
//! lists the trust-handoff files, symbolic links and repositories a run
//! created or changed. See the [`audit`] module.
//!
//! # Egress through an allowlist (proxy)
//!
//! With the `proxy` feature, a `Network::Proxy` policy and
//! `CageOptions::with_proxy`, each run reaches only the hosts an allowlist
//! names, through a proxy of its own (`procage::proxy`), and no other route:
//! macOS and Linux tier T1; a gap on T2. Refusals are in the run's
//! violations.
//!
//! # Async (tokio)
//!
//! With the `tokio` feature, `Cage::spawn_async` takes a
//! `tokio::process::Command` and returns an `AsyncChild` that dereferences
//! to `tokio::process::Child`: await its exit and read its pipes on the
//! runtime. The checks, the child sequence and the errors are the std
//! path's; dropping the handle or awaiting `AsyncChild::kill_all` ends the
//! run as it does for [`native::Child`]. `AsyncChild::violation_stream`
//! yields the run's violations as they happen and ends when the record is
//! complete. Without a runtime on the calling thread the spawn fails with
//! `SpawnError::NoRuntime` before forking. Without the feature procage has
//! no async dependency.
//!
//! Read the [threat model](threat_model) for the adversary, the scope, the
//! limits of an OS sandbox and the status of every known escape class.
pub use procage_policy as policy;
/// The backend of the system procage is built for: `procage::macos` on macOS,
/// `procage::linux` on Linux.
///
/// Code that uses only the subset both backends share builds on both
/// without `cfg`: [`Cage::new`](native::Cage::new),
/// [`Cage::with_options`](native::Cage::with_options) and
/// [`Cage::fit`](native::Cage::fit) with
/// [`CageOptions::default`](native::CageOptions)`().with_lower(…)`,
/// [`Cage::spawn`](native::Cage::spawn),
/// [`Cage::spawn_with_fds`](native::Cage::spawn_with_fds),
/// [`Grants::new`](native::Grants::new)`().bind(…)`, and the
/// [`Child`](native::Child) methods `kill_all`, `detach`, `dropped_env`,
/// `tmpdir` and `receipt`. Errors are each backend's own [`SpawnError`](native::SpawnError),
/// with a `Display` message.
pub use macos as native;
/// The backend of the system procage is built for: `procage::macos` on macOS,
/// `procage::linux` on Linux.
pub use linux as native;
pub use ;
/// The egress proxy a cage runs per run (feature `proxy`; see
/// `CageOptions::with_proxy`).
pub use procage_proxy as proxy;
pub use ;
pub use ViolationStream;
pub use ;