# agent-bridle-tool-shell
A capability-confined family of shell engines for agent-bridle:
- `BrushShellTool` (`brush`): a carried bash-in-Rust engine running in a fresh
worker whose command interceptor gates Brush-originated external spawns and
opens.
- `carried-coreutils`: adds bundled `ls`, `cat`, `echo`, `head`, `sort`, and
`wc` to Brush. The top-level facade selects it by default on supported
targets and falls back to `ShellTool` elsewhere.
- `ShellTool` (`shell`): the lean **argv + safe-subset engine** (ADR 0005).
- `HostShellTool` (`host-shell`): full host-shell semantics when the OS sandbox
can carry the confinement guarantee.
The safe-subset engine makes agent-bridle the exec funnel: it parses the request
itself, checks the `exec`/`fs` leash, spawns programs directly, and refuses
dynamic constructs by design. It accepts two input shapes:
- argv form (`program` + `args`), and
- free-form `cmd` — a safe subset: pipelines (`|`), redirections
(`>`/`>>`/`<`/`2>`/`2>&1`), `&&`/`||`/`;` sequencing, filename globbing
(`*`/`?`/`[…]`), and allowlisted `$VAR` expansion.
Because agent-bridle performs each redirect's open and each glob's directory
listing itself, those filesystem touches are leash-checked (`fs_read`/`fs_write`)
**before any stage spawns**; out-of-scope `exec` is denied at the funnel.
The Brush engine creates its worker through the shared confined-spawn funnel.
Linux and macOS authenticate that private transition over a fresh kernel socket:
the child verifies that the frame sender is its real parent and the exact same
executable image, while core freezes the launch nonce, effective caveats, and
strength floor into a take-once envelope. Knowing the private argv, nonce, or
child-generated challenge is not authority; direct invocation exits 126 before
Brush runs. The worker replaces its control fd 0 with `/dev/null` after the
handshake.
`brush_private_control_supported()` is the construction-time selection probe. It
is true only on Linux and macOS in this release. Other targets deliberately fail
closed rather than treating full access as authentication; a host must select
`ShellTool` there. The top-level facade's default registry performs that
fallback automatically, and its advertised input schema therefore changes from
Brush's full `cmd` grammar to the safe-subset `program`/`args` schema.
When the effective caveats engage an available native L3 backend, the worker,
carried utilities, and all other descendants inherit it; otherwise the result
honestly reports `SandboxKind::None`. It receives only explicit environment
values. Timeout supervision terminates the worker's whole process group.
Restricted filesystem authority fails closed when no kernel backend is
available; `unbridled` is the explicit policy opt-out, not a private-control
bypass.
Each bundled provider is offered as a Brush shim, but Brush's native builtins
win name conflicts, so `echo` normally remains Brush's in-process builtin.
Invoking a non-conflicting utility such as `wc` re-executes the
dispatch-capable embedding binary as
`<current-exe> --invoke-bundled wc ...`; `maybe_dispatch()` recognizes that
private entrypoint before an async runtime is constructed and calls the bundled
uutils implementation. The carried child repeats a PID/image/peer-credential
handshake that binds the exact utility name and raw argv; a direct
`--invoke-bundled` is refused. Any L3 policy engaged for the worker therefore
covers the carried child too. A host embedding Agent Bridle must call
`maybe_dispatch()` at the very top of `main`.
When the safe-subset parser refuses dynamic grammar and the `brush` feature is
present, its denial envelope includes a versioned, non-executing
`shell_inspection`. Embedders may also call `inspect_shell` before dispatching
to Brush; the top-level `agent-bridle` facade re-exports that function and its
schema types under the `brush` feature. It preserves the exact source and
provides a flattened inventory of direct command stages, command substitutions,
arithmetic expansions, redirections, quote context, and recognized delegated
descendants. Pipeline and control-flow relationships remain visible in the
exact source but are not encoded as graph edges; warnings call out those
flattened relationships. Arithmetic is projected only when Brush's arithmetic
AST proves it is state-free: integer literals and operators are accepted, while
variables, array references, assignments, and nested shell expansions fail
closed. This matters because bash-like arithmetic recursively interprets shell
state and array indices. The same state-free check covers parameter array
subscripts and substring offsets/lengths. Indirect parameter expansion and the
prompt-expanding `@P` transformation also fail closed because they can
reinterpret a runtime value as expansion syntax. It cannot show a
substitution's value: that value exists only after the inner command runs.
Known dispatch wrappers and interpreters fail closed when their runtime command
cannot be projected. `find -exec` is explicitly marked as delegated because the
external `find` process—not Brush—spawns that child. This projection is approval
metadata, not enforcement or a complete process graph.
The safe-subset path is the **L2 convenience** layer; the subprocess boundary is
L3 (ADR 0005). Coverage is backend- and scope-shaped:
- Landlock confines both filesystem axes, narrows direct exec (reported
`Interceptor` because the loader trampoline remains), and can deny all TCP on
ABI-v4 kernels.
- Seatbelt confines both filesystem axes, restricted exec, and empty or
loopback-only network scopes.
- AppContainer confines filesystem paths, empty or loopback-only network
scopes, and exec deny-all for supported subprocess engines; the Brush private
transport is currently unavailable on Windows, so hosts select `ShellTool`.
The result's `sandbox_kind` and per-axis enforcement report are authoritative.
When no backend engages, the kind is `None`; a restricted filesystem scope
fails closed rather than running without its required boundary. The cross-OS L3
strategy is ADR 0009.
- Every engine registers under the same `shell` identity; the embedder selects
one at registry construction.
- Compiles with the `shell` feature off (exposing only the lightweight observer
contract), so workspaces build under `--no-default-features`
- Brush remains isolated behind leaf-crate features; `agent-bridle-core` stays
free of heavy shell dependencies.
## Live output observation
`ShellTool`, `HostShellTool`, and `BrushShellTool` each provide a
`with_output_observer` construction-time builder. The shared
`ShellOutputObserver` receives bounded raw-byte chunks tagged with a process-local
`ShellInvocationId` and `ShellOutputStream::Stdout` or
`ShellOutputStream::Stderr`. Distinct IDs keep concurrent dispatches separate.
The top-level `agent-bridle` facade re-exports all three observer types whenever
any shell-engine feature is enabled.
The ordinary `Registry::dispatch` and `Tool::invoke` paths are unchanged, so
observation adds no authority and cannot bypass the gate.
Chunks may split UTF-8 code points. Callbacks run on a dedicated presentation
thread, so slow observer code cannot stall child pipes, timeout, or cancellation.
Callbacks for one ID are serialized, but different IDs use different presentation
threads and may enter the same observer concurrently. On ordinary completion,
`on_finish(id)` follows every queued `on_output` call for that ID; dispatch does
not wait for it. Cancellation stops accepting new chunks, though a callback
already dequeued may finish afterward. Callback panics are contained.
At most the configured output cap is delivered per stream. For `ShellTool` and
`HostShellTool` this is a live view. The Brush worker's single-response protocol
delivers its bounded captured chunks after the worker completes; it is
observation, not streaming. In every engine the completed envelope is
authoritative. Separate safe-shell pipeline stderr readers follow live reader
scheduling, while that envelope assembles stderr in pipeline-stage order and
then applies its cap, so the two can differ. Excess host/Brush output is drained
and discarded to preserve child behavior without unbounded capture.
Part of [agent-bridle](https://github.com/Gilamonster-Foundation/agent-bridle),
the capability leash for agent tools — a shared, capability-governed tool
registry for the Gilamonster agent line.
## License
Apache-2.0