Skip to content

brush-bridle-core: optional full-bash shell engine (deferred, reversible — not the path to a usable shell) #20

Description

@hartsock

Why brush instead of bash

The shell tool in agent-bridle needs to run user-requested shell commands while enforcing the caveats policy — denying writes outside the workspace, blocking dangerous exec calls, etc.

The naive approach is std::process::Command::new("bash").arg("-c").arg(cmd). The problem: once you fork a bash process, interception has to happen at the OS level (seccomp on Linux, sandbox-exec on macOS). That's platform-specific, fragile, and still doesn't give you structured information about why a command was denied.

The alternative we considered was parsing the command AST before execution to predict what it would do. This is a dead end: bash's expansion rules (aliases, variable substitution, subshells, process substitution, globs) mean the AST you parse is not the program that will run. You'd be reimplementing bash semantics — a rabbit hole with no bottom.

brush solves this cleanly: it's a Rust-native bash-compatible shell interpreter. We forked it at hartsock/brush to add a CommandInterceptor trait — a pre-exec hook and a pre-open hook that fire inside the interpreter, after all expansions, on the resolved program name and file path. This is exactly the right boundary: interception happens on what bash actually does, not on what the source text looks like.

The interceptor implementation is in agent-bridle-tool-shell/src/caveat_interceptor.rs. It maps caveats policy decisions to ExecDecision and OpenDecision responses before any syscall crosses the boundary.

Current state (stub)

PR reubeno/brush#1184 adds the CommandInterceptor trait to upstream brush. Until that merges and a brush release ships, we cannot take a crates.io dep on brush — only a git dep, which blocks publishing agent-bridle itself to crates.io.

As of the feat/stub-shell branch, the shell tool is built in degraded mode:

  • Without --features brush-shell: returns a clear error explaining shell support is unavailable in the published build
  • With --features brush-shell (git dep only, local builds): full CommandInterceptor interception as designed

newt-agent uses [patch.crates-io] to get the real brush support in local builds while keeping the published chain clean.

What to do when brush#1184 merges

  1. Wait for a brush release that includes the CommandInterceptor trait (check https://crates.io/crates/brush-core for the new version)
  2. In agent-bridle-tool-shell/Cargo.toml: replace the git deps with brush-core = "<new-version>" etc. from crates.io, move them out of the brush-shell feature into default deps
  3. In agent-bridle/Cargo.toml: make brush-shell a default feature
  4. Publish a new agent-bridle version to crates.io — shell support is now on by default
  5. In newt-agent: remove [patch.crates-io] override, bump agent-bridle version, re-run just check, cut a patch release
  6. Close this issue

References


Filed by Beaver (MacBook agent, Claude Sonnet 4.6)

Metadata

Metadata

Assignees

No one assigned

    Labels

    blocked:upstream-brushWaiting on reubeno/brush#1184 (CommandInterceptor hook) to land upstream

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions