agent_bridle/lib.rs
1//! `agent-bridle` — the facade.
2//!
3//! Re-exports the [`agent_bridle_core`] leash and assembles the default tool
4//! [`Registry`] a host consumes. Tools are registered through the **explicit
5//! builder** (DESIGN §5) — the DCE-proof path under `strip+lto` release
6//! profiles — and each tool's symbol is anchored here by a `pub use`, so the
7//! linker can never silently drop one from `tools/list`.
8//!
9//! ```
10//! use agent_bridle::registry;
11//! use agent_bridle::{Caveats, CountBound, Scope};
12//!
13//! # async fn demo() -> anyhow::Result<()> {
14//! let reg = registry();
15//! let granted = Caveats {
16//! exec: Scope::only(["echo".to_string()]),
17//! max_calls: CountBound::AtMost(2),
18//! ..Caveats::top()
19//! };
20//! let out = reg
21//! .dispatch("shell", serde_json::json!({ "program": "echo", "args": ["hi"] }), &granted)
22//! .await?;
23//! assert_eq!(out["exit_code"], 0);
24//! # Ok(())
25//! # }
26//! ```
27
28#![forbid(unsafe_code)]
29#![warn(missing_docs)]
30
31// Re-export the whole leash so hosts depend on one crate.
32pub use agent_bridle_core::*;
33
34// Anchor each tool's symbol in the facade (DESIGN §5): an explicit `pub use`
35// keeps the linker from DCE-ing a tool module under strip+lto.
36#[cfg(any(feature = "shell", feature = "carried-coreutils"))]
37pub use agent_bridle_tool_shell::ShellTool;
38#[cfg(any(feature = "shell", feature = "host-shell", feature = "brush"))]
39pub use agent_bridle_tool_shell::{ShellInvocationId, ShellOutputObserver, ShellOutputStream};
40// The sandboxed-host engine (ADR 0019 / #194). Anchored here so a host can
41// construct its own registry with it (`Registry::builder().tool(Arc::new(
42// HostShellTool::new()))`); it is deliberately NOT added to `registry()` — it
43// is a complementary construction-time engine choice, and it shares the
44// `"shell"` name with `ShellTool` (ADR 0019 D3).
45#[cfg(feature = "host-shell")]
46pub use agent_bridle_tool_shell::HostShellTool;
47// The carried brush engine (agent-bridle#20): a bash-in-Rust shell run in a
48// dedicated worker. Its CommandInterceptor provides the worker-local L2 leash;
49// when effective caveats engage a native L3 backend, the worker and descendants
50// inherit it. Behind the `brush` feature (the crates.io `brush-ocap-*` fork);
51// NOT auto-added to `registry()` — it shares the `"shell"` name with ShellTool
52// (ADR 0005 D2), so the embedder selects it.
53#[cfg(feature = "brush")]
54pub use agent_bridle_tool_shell::{brush_private_control_supported, BrushShellTool};
55/// Parse and inspect Brush shell source without expansion or execution.
56///
57/// The returned source-bound schema lets an embedder present command
58/// substitutions, state-free arithmetic expansions, redirections, and
59/// statically discoverable commands for approval before invoking
60/// [`BrushShellTool`]. Arithmetic that depends on shell variables, arrays,
61/// assignments, or nested expansion fails closed, as do parameter forms that
62/// can reinterpret runtime values as expansion syntax.
63#[cfg(feature = "brush")]
64pub use agent_bridle_tool_shell::{
65 inspect_shell, DescendantExec, InspectedCommand, InspectedConstruct, InspectedRedirect,
66 RedirectOperation, ShellConstructKind, ShellInspection, ShellInspectionError,
67};
68// An embedder calls `maybe_dispatch()` at the very top of `main` so the private
69// sandboxed Brush worker re-exec resolves before normal application startup.
70#[cfg(feature = "brush")]
71pub use agent_bridle_tool_shell::maybe_dispatch;
72// With carried-coreutils, non-conflicting `ls`/`cat`/… shims additionally
73// re-exec `<self> --invoke-bundled <name>` and resolve against the host binary.
74// Registration helpers are re-exported for completeness.
75#[cfg(feature = "carried-coreutils")]
76pub use agent_bridle_tool_shell::{install_default_providers, register_shims};
77#[cfg(feature = "web")]
78pub use agent_bridle_tool_web::WebFetchTool;
79
80/// Build the default tool registry for this host's compiled feature set.
81///
82/// Uses the explicit [`Registry::builder`] — never `inventory` — so the tool
83/// set is deterministic and DCE-proof. Which tools are present depends on the
84/// compiled features:
85///
86/// - `carried-coreutils` (default): adds the carried Brush-backed `shell` tool
87/// where authenticated private control is supported; otherwise it selects
88/// the safe-subset shell rather than advertising an unusable worker.
89/// - `shell`: selects the lean argv + safe-subset `shell` instead when
90/// `carried-coreutils` is disabled.
91/// - `web`: adds the confined `web_fetch` tool — the `net` enforcer (host
92/// allowlist + SSRF block + per-redirect re-check + IP pinning).
93///
94/// Under `--no-default-features` the registry is empty but valid; a host adds
95/// tools by enabling features (or building its own registry).
96#[must_use]
97pub fn registry() -> Registry {
98 #[allow(unused_mut)]
99 let mut builder = Registry::builder();
100
101 #[cfg(feature = "carried-coreutils")]
102 {
103 if brush_private_control_supported() {
104 builder = builder.tool(std::sync::Arc::new(BrushShellTool::new()));
105 } else {
106 builder = builder.tool(std::sync::Arc::new(ShellTool::new()));
107 }
108 }
109
110 #[cfg(all(feature = "shell", not(feature = "carried-coreutils")))]
111 {
112 builder = builder.tool(std::sync::Arc::new(ShellTool::new()));
113 }
114
115 #[cfg(feature = "web")]
116 {
117 builder = builder.tool(std::sync::Arc::new(WebFetchTool::new()));
118 }
119
120 builder.build()
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 /// Presence test (DESIGN §5): either registry-selected shell feature must
128 /// register exactly the public `shell` identity.
129 #[cfg(any(feature = "shell", feature = "carried-coreutils"))]
130 #[test]
131 fn shell_tool_is_present_with_feature() {
132 let reg = registry();
133 assert!(
134 reg.contains("shell"),
135 "expected `shell` tool to be registered"
136 );
137 let names = reg.tool_names();
138 assert!(
139 names.contains(&"shell"),
140 "tool_names missing shell: {names:?}"
141 );
142 }
143
144 /// The default carried engine publishes its full-shell `cmd` schema, not
145 /// the safe-subset engine's argv form.
146 #[cfg(all(
147 feature = "carried-coreutils",
148 any(target_os = "linux", target_os = "macos")
149 ))]
150 #[test]
151 fn default_shell_is_the_carried_brush_engine() {
152 let reg = registry();
153 let shell = reg
154 .tool_definitions()
155 .into_iter()
156 .find(|definition| definition["name"] == "shell")
157 .expect("carried shell present");
158 let properties = shell["inputSchema"]["properties"]
159 .as_object()
160 .expect("shell schema properties");
161 assert!(properties.contains_key("cmd"), "Brush schema needs `cmd`");
162 assert!(
163 !properties.contains_key("program"),
164 "default registry must not select the argv safe-subset engine"
165 );
166 }
167
168 /// A default build on a target without authenticated private control keeps
169 /// a functional shell, but advertises the safe-subset argv schema rather
170 /// than pretending the unavailable Brush worker can run.
171 #[cfg(all(
172 feature = "carried-coreutils",
173 not(any(target_os = "linux", target_os = "macos"))
174 ))]
175 #[test]
176 fn default_shell_falls_back_to_safe_subset_when_private_control_is_unsupported() {
177 assert!(!brush_private_control_supported());
178 let reg = registry();
179 let shell = reg
180 .tool_definitions()
181 .into_iter()
182 .find(|definition| definition["name"] == "shell")
183 .expect("safe-subset shell present");
184 let properties = shell["inputSchema"]["properties"]
185 .as_object()
186 .expect("shell schema properties");
187 assert!(
188 properties.contains_key("program"),
189 "fallback schema must disclose the argv safe-subset engine"
190 );
191 }
192
193 #[cfg(feature = "brush")]
194 #[test]
195 fn private_control_probe_matches_the_compiled_transport() {
196 assert_eq!(
197 brush_private_control_supported(),
198 cfg!(any(target_os = "linux", target_os = "macos"))
199 );
200 }
201
202 /// Without either registry-selected shell feature the tool must be absent.
203 #[cfg(not(any(feature = "shell", feature = "carried-coreutils")))]
204 #[test]
205 fn shell_tool_absent_without_feature() {
206 let reg = registry();
207 assert!(!reg.contains("shell"));
208 }
209
210 /// Presence test (DESIGN §5): under `--features web` the `web_fetch` tool —
211 /// the `net` enforcer — must be registered (and thus exposed by
212 /// `agent-bridle-mcp`). This is the CI guard that linker DCE has not dropped
213 /// it under strip+lto.
214 #[cfg(feature = "web")]
215 #[test]
216 fn web_fetch_tool_is_present_with_feature() {
217 let reg = registry();
218 assert!(
219 reg.contains("web_fetch"),
220 "expected `web_fetch` tool to be registered"
221 );
222 assert!(
223 reg.tool_names().contains(&"web_fetch"),
224 "tool_names missing web_fetch: {:?}",
225 reg.tool_names()
226 );
227 }
228
229 /// Without the `web` feature the web tool must be absent.
230 #[cfg(not(feature = "web"))]
231 #[test]
232 fn web_fetch_tool_absent_without_feature() {
233 let reg = registry();
234 assert!(!reg.contains("web_fetch"));
235 }
236
237 /// Under `--no-default-features` (no `shell`, no `web`) the registry is empty
238 /// but valid.
239 #[cfg(all(
240 not(feature = "shell"),
241 not(feature = "carried-coreutils"),
242 not(feature = "web")
243 ))]
244 #[test]
245 fn registry_is_empty_with_no_tool_features() {
246 let reg = registry();
247 assert!(reg.tool_names().is_empty());
248 }
249
250 /// The facade re-exports the core leash types.
251 #[test]
252 fn leash_types_are_reexported() {
253 let _c = Caveats::top();
254 let _s: Scope<String> = Scope::top();
255 let _b = CountBound::Unlimited;
256 let _k = SandboxKind::None;
257 }
258
259 /// The facade forwards the EdDSA WebAuthn verifier feature to core.
260 #[cfg(feature = "verifier-webauthn")]
261 #[test]
262 fn webauthn_verifier_is_reexported() {
263 let _ = WebAuthnVerifier;
264 }
265
266 /// The facade forwards the ES256 WebAuthn verifier feature to core.
267 #[cfg(feature = "verifier-webauthn-es256")]
268 #[test]
269 fn webauthn_es256_verifier_is_reexported() {
270 let _ = WebAuthnEs256Verifier;
271 }
272
273 /// The facade exposes parse-only Brush inspection so a host can preflight
274 /// dynamic constructs before any shell tool is invoked.
275 #[cfg(feature = "brush")]
276 #[test]
277 fn brush_inspection_is_reexported_for_preflight() {
278 let inspected: ShellInspection =
279 inspect_shell(r#"echo "$(printf '%s' "$((1 + 2))")""#).expect("inspection");
280
281 let substitution: &InspectedConstruct = &inspected.constructs[0];
282 assert_eq!(substitution.kind, ShellConstructKind::CommandSubstitution);
283 assert!(substitution.quoted);
284
285 let nested = substitution
286 .inspection
287 .as_deref()
288 .expect("recursive substitution inspection");
289 assert_eq!(
290 nested.constructs[0].kind,
291 ShellConstructKind::ArithmeticExpansion
292 );
293 assert_eq!(nested.constructs[0].body, "1 + 2");
294
295 let error = inspect_shell("echo $((runtime_value))")
296 .expect_err("runtime-state arithmetic must fail closed through the facade");
297 assert!(error.message().contains("runtime shell state"));
298 }
299}