Skip to main content

agent_bridle_tool_shell/
lib.rs

1//! `agent-bridle-tool-shell` — capability-confined safe-subset, Brush, and host-shell engines.
2//!
3//! Per **ADR 0005** the object-capability *boundary* is L3 (kernel) and this
4//! crate supplies complementary L2 engines. The lean [`ShellTool`] is an
5//! **exec funnel**: it parses each request itself ([`crate::parse`]), accepts
6//! argv form (`program` + `args`) or a restricted free-form `cmd`, checks the
7//! `exec`/`fs` leash, spawns directly, and refuses dynamic constructs by
8//! design (command substitution `$(...)`, arithmetic expansion `$((...))`,
9//! backticks, and subshells). [`BrushShellTool`] instead carries full Brush
10//! grammar in a dedicated worker; its interceptor gates the worker's own
11//! external spawns and opens. When effective caveats engage an available
12//! Landlock, Seatbelt, or AppContainer backend, the worker/process tree
13//! inherits that L3 boundary; otherwise the run honestly reports
14//! [`agent_bridle_core::SandboxKind::None`] (I9). Coverage is per-axis and
15//! scope-shaped: inspect the result's enforcement report rather than inferring
16//! every guarantee from the coarse sandbox kind.
17//!
18//! The engine (agent-bridle#34 Track A + #45): a sequence of pipelines joined by
19//! `&&`/`||`/`;` (short-circuit semantics), each pipeline simple commands with
20//! quoted arguments, **redirections** (`> out`, `>> out`, `< in`, `2> err`,
21//! `2>&1`), **filename globbing** (`*`/`?`/`[…]`) and **allowlisted `$VAR`
22//! expansion** — every filesystem/env touch bridle performs (redirect opens,
23//! glob directory listings, variable allowlist) is leash-/policy-checked before
24//! any spawn. Those dynamic constructs stay refused by the safe-subset engine.
25//! The process spawning is behind a `Spawner` seam (mocked in unit tests; real
26//! path in `tests/real_spawn.rs`). Brush is the carried full-grammar alternative
27//! behind the same construction-time registry seam.
28
29#![forbid(unsafe_code)]
30#![warn(missing_docs)]
31
32#[cfg(feature = "brush")]
33mod brush_shell;
34#[cfg(feature = "brush")]
35mod brush_worker;
36#[cfg(feature = "brush")]
37mod caveat_interceptor;
38#[cfg(feature = "brush")]
39mod coreutils_dispatch;
40#[cfg(all(feature = "brush", any(target_os = "linux", target_os = "macos")))]
41mod private_control;
42#[cfg(all(feature = "brush", not(any(target_os = "linux", target_os = "macos"))))]
43mod private_control {
44    use serde::de::DeserializeOwned;
45
46    pub(crate) fn receive_worker_request<P: DeserializeOwned>(
47    ) -> Result<agent_bridle_core::TrustedWorkerRequest<P>, String> {
48        Err("authenticated private worker control is unavailable on this platform".to_string())
49    }
50
51    #[cfg(feature = "carried-coreutils")]
52    pub(crate) fn authenticate_carried_dispatch(
53        _name: &std::ffi::OsStr,
54        _args: &[std::ffi::OsString],
55    ) -> Result<(), String> {
56        Err("authenticated carried dispatch is unavailable on this platform".to_string())
57    }
58}
59#[cfg(feature = "host-shell")]
60mod host_shell;
61#[cfg(feature = "brush")]
62mod shell_inspect;
63// #257: the loopback egress proxy moved to agent-bridle-core (shared with
64// `ConfinedCommand::spawn_tokio` and external no-subprocess callers). This
65// alias keeps every `crate::net_proxy::…` path — and the audit re-exports
66// below — resolving unchanged, now to the single core implementation.
67#[cfg(feature = "shell")]
68pub(crate) use agent_bridle_core::net_proxy;
69mod output_observer;
70#[cfg(feature = "shell")]
71mod parse;
72#[cfg(feature = "shell")]
73mod shell_tool;
74
75/// Stop the existing stage/worker process group before terminating its members.
76/// Killing a waited-on child first can wake a shell long enough to run its next
77/// command before the group's kill reaches it. Callers retain their wait/reap
78/// handling; this does not reach descendants that leave the process group.
79#[cfg(any(feature = "shell", feature = "brush"))]
80fn kill_child_tree(child: &mut std::process::Child) {
81    #[cfg(unix)]
82    if let Some(pid) = rustix::process::Pid::from_raw(child.id() as i32) {
83        let _ = rustix::process::kill_process_group(pid, rustix::process::Signal::STOP);
84        let _ = rustix::process::kill_process_group(pid, rustix::process::Signal::KILL);
85    }
86    // Preserve the direct-child fallback even if either group signal fails.
87    let _ = child.kill();
88}
89
90pub use output_observer::{ShellInvocationId, ShellOutputObserver, ShellOutputStream};
91#[cfg(feature = "shell")]
92pub use shell_tool::ShellTool;
93
94/// The sandboxed-host engine (ADR 0019 / #194): full-shell semantics with the
95/// guarantee entirely on L3. Opt-in via the `host-shell` feature; a
96/// construction-time alternative to [`ShellTool`] behind the ADR 0005 D2 seam.
97#[cfg(feature = "host-shell")]
98pub use host_shell::HostShellTool;
99
100/// The carried **brush** engine (agent-bridle#20 / Track 2): a bash-in-Rust
101/// shell run in a dedicated sandboxed worker. Its `CommandInterceptor` provides
102/// the worker-local L2 leash; when an effective native backend engages, the
103/// worker and descendants inherit that L3 boundary. Opt-in via the `brush`
104/// feature; a construction-time alternative to [`ShellTool`] behind the ADR
105/// 0005 D2 seam, using the temporary `brush-ocap-*` fork
106/// (reubeno/brush#1184).
107#[cfg(feature = "brush")]
108pub use brush_shell::BrushShellTool;
109
110/// Whether this target provides the kernel-authenticated private transport
111/// required by [`BrushShellTool`] and carried-coreutils re-exec.
112///
113/// A host must use this probe before advertising or selecting the Brush engine.
114/// Unsupported targets fail closed at invocation; they must select the
115/// safe-subset [`ShellTool`] instead of treating full access as authentication.
116#[cfg(feature = "brush")]
117#[must_use]
118pub const fn brush_private_control_supported() -> bool {
119    cfg!(any(target_os = "linux", target_os = "macos"))
120}
121#[cfg(feature = "brush")]
122pub use shell_inspect::{
123    inspect_shell, DescendantExec, InspectedCommand, InspectedConstruct, InspectedRedirect,
124    RedirectOperation, ShellConstructKind, ShellInspection, ShellInspectionError,
125};
126
127/// Private Brush-worker dispatch. An embedder's binary calls
128/// [`maybe_dispatch`] at the top of `main` so the sandboxed worker re-exec
129/// resolves before normal application startup.
130#[cfg(feature = "brush")]
131pub use coreutils_dispatch::maybe_dispatch;
132
133/// Carried-coreutils registration (agent-bridle#20 / issue #206). With
134/// `carried-coreutils`, the Brush engine's non-conflicting shims re-exec
135/// `<self> --invoke-bundled <name>` and resolve against the dispatch-capable
136/// host binary. These functions are used by the engine internally.
137#[cfg(feature = "carried-coreutils")]
138pub use coreutils_dispatch::{install_default_providers, register_shims};
139
140/// Network egress audit surface (#124, ADR 0016): the loopback proxy records
141/// every proxy-visible connection as a [`NetAuditEvent`] through an [`AuditSink`]
142/// (default off; enable via the `BRIDLE_NET_AUDIT` setting). The `bridle-netmon`
143/// binary renders the JSON-lines stream as a live monitor.
144#[cfg(feature = "shell")]
145pub use net_proxy::{AuditSink, JsonlSink, NetAuditEvent, NetDecision, NetKind, NullSink};