137 lines
7.0 KiB
Markdown
137 lines
7.0 KiB
Markdown
# nucleic-brush
|
|
|
|
A fork of [**brush**](https://github.com/reubeno/brush) — the Bo(u)rn(e) RUsty SHell by
|
|
reuben olinsky — carrying the patches Nucleic needs to build **hash**, its agent shell.
|
|
|
|
Pinned at upstream tag **`brush-shell-v0.4.0`** (commit `96a26d0`).
|
|
|
|
> **Looking for the shell itself?** Go [upstream](https://github.com/reubeno/brush).
|
|
> This repository is not a distribution of brush and publishes no binaries or crates —
|
|
> it is a vendored fork whose only consumer is Nucleic's `shell/` workspace. Everything
|
|
> here that isn't marked as a fork change is upstream's work, under upstream's
|
|
> [MIT license](LICENSE).
|
|
|
|
## What the fork adds
|
|
|
|
Nucleic runs agent-authored shell commands and needs to *see* what they do — the
|
|
commands, their exits, and the data crossing pipes and redirections — from inside the
|
|
shell rather than by wrapping it. That is what these patches provide. Everything else
|
|
is upstream brush, unmodified.
|
|
|
|
Every divergence carries a `// hydrashell:` comment, so the complete diff against upstream is
|
|
one grep away:
|
|
|
|
```console
|
|
$ grep -rn "hydrashell:" --include="*.rs" --include="*.yaml" .
|
|
```
|
|
|
|
### 1. The observation gate
|
|
|
|
A process-global, verdict-shaped hook at the shell's command choke point
|
|
(`brush-core/src/gate.rs`). The default implementation allows everything and observes
|
|
nothing, so **an unconfigured shell behaves exactly like upstream brush** — including
|
|
the fast paths: pipe teeing is only wired up when a real gate is installed.
|
|
|
|
| Surface | What it reports |
|
|
| --- | --- |
|
|
| `Gate::on_exec` / `on_exit` | every simple command (builtin, function, external): argv, cwd, redirections, pipeline slot; then its exit code (128+signal for signal deaths) |
|
|
| `Gate::on_pipe` | the bytes crossing each `a \| b` link once it drains, with both stages rendered from their *pre-expansion* AST text |
|
|
| `Gate::on_cmdsub` | the trimmed output of `$(…)` / backtick substitutions |
|
|
|
|
`on_exec` returns a `Verdict`, so the same seam supports enforcement (`Deny`) later
|
|
without further changes to this crate. Today Nucleic only observes.
|
|
|
|
The plumbing behind those hooks:
|
|
|
|
| File | Change |
|
|
| --- | --- |
|
|
| `brush-core/src/gate.rs` (new), `lib.rs` | the `Gate` trait, event types, and process-global install/allow-all default |
|
|
| `brush-core/src/commands.rs` | `SimpleCommand::execute` split into a gate wrapper + `execute_inner`; `on_exec` runs before dispatch and correlates completion across all three spawn-result variants |
|
|
| `brush-core/src/processes.rs` | `ChildProcess.gate_token` and `on_exit` reporting from both `wait()` and `poll()`, exactly once |
|
|
| `brush-core/src/interp.rs` | redirection recording (`>`, `>>`, `<`, `2>`, `&>`, `<>`, heredocs, here-strings) for post-run read-back; a tee interposed on each pipe link when observation is active — a pooled copier thread mirrors a bounded prefix while preserving backpressure and SIGPIPE, with a `splice` passthrough after the cap |
|
|
| `brush-core/src/expansion.rs` | command-substitution arm reports its output to the gate |
|
|
| `brush-shell/src/entry.rs` | `set_exit_hook` seam so an embedder can flush buffered events before `process::exit` |
|
|
|
|
### 2. Upstream-candidate fixes
|
|
|
|
Bugs found by running real-world scripts through brush. These aren't Nucleic-specific
|
|
and belong upstream:
|
|
|
|
| File | Fix |
|
|
| --- | --- |
|
|
| `brush-core/src/expansion.rs` | literal unquoted word text is no longer field-split (bash splits only the *results* of expansions) while keeping its glob characters active. Parameter-expansion substitutions convert back to splittable at the expansion boundary; `ExpanderOptions.field_split_literal_text` lets data-string callers opt in. Repairs 3 previously `known_failure` IFS tests. |
|
|
| `brush-core/src/completion.rs` | `compgen -W` opts into `field_split_literal_text` — the `-W` string is data, not source text |
|
|
| `brush-builtins/src/dot.rs` | `.` / `source` searches `PATH` for slashless operands when `sourcepath` is set, as bash and POSIX require. Fixes `git-subtree`, which sources `git-sh-setup` this way. |
|
|
|
|
### 3. Branding
|
|
|
|
The binary this workspace builds is still named `brush`, but identifies itself as hash
|
|
so an agent-facing shell doesn't misrepresent what it is:
|
|
|
|
| File | Change |
|
|
| --- | --- |
|
|
| `brush-shell/src/productinfo.rs` | `PRODUCT_NAME` → `hydrashell`; display string reads `hydrashell (hash — Hydrangea agent shell, brush fork) …` |
|
|
| `brush-shell/src/args.rs` | usage and version strings rebranded |
|
|
| `brush-core/src/prompt.rs` | `\$` renders the atom sign `⚛` (U+269B) for non-root; root still shows `#` |
|
|
|
|
[`NUCLEIC_FORK.md`](NUCLEIC_FORK.md) tracks the same list at file granularity, with the
|
|
current verification status.
|
|
|
|
## How it's consumed
|
|
|
|
Nucleic's `shell/` workspace depends on `brush-shell` and `brush-parser` by path. The
|
|
`hydrashell` binary is a thin wrapper that loads a root-owned operator policy, installs its
|
|
`Gate` implementation (`hydrashell-observe`), flushes events at exit, and otherwise defers
|
|
entirely to brush's bash-compatible CLI. None of the fork's crates are published; the
|
|
`brush` binary built from here is a development artifact.
|
|
|
|
## Building and testing
|
|
|
|
Standard upstream workflow — a recent Rust toolchain (see `rust-version` in
|
|
[`Cargo.toml`](Cargo.toml)), then:
|
|
|
|
```console
|
|
$ cargo build --release
|
|
$ cargo test --workspace
|
|
$ cargo test -p brush-shell --test brush-compat-tests # the bash-oracle compat suite
|
|
```
|
|
|
|
The compat suite is the fork's regression gate: it must stay level with a pristine
|
|
build of the pinned upstream tag. As last recorded in
|
|
[`NUCLEIC_FORK.md`](NUCLEIC_FORK.md), it does — the only failures are environmental
|
|
ones a baseline run reproduces, plus the 3 IFS tests the expansion fix repairs.
|
|
|
|
## Staying in sync with upstream
|
|
|
|
1. Diff this tree against the pinned upstream tag.
|
|
2. Re-vendor the new tag.
|
|
3. Re-apply the fork patches — `grep -rn "hydrashell:"` on the old tree enumerates all of
|
|
them.
|
|
4. Re-run the brush compat suite and Nucleic's transcript-replay corpus.
|
|
5. Update the pin and the patch table in [`NUCLEIC_FORK.md`](NUCLEIC_FORK.md).
|
|
|
|
Fixes that aren't Nucleic-specific should go to
|
|
[upstream](https://github.com/reubeno/brush/issues) rather than accumulate here — a
|
|
smaller diff is a cheaper rebase.
|
|
|
|
## Upstream documentation
|
|
|
|
Retained as vendored, and still accurate for everything the fork doesn't touch:
|
|
|
|
* [Compatibility reference](docs/reference/compatibility.md) — what does and doesn't work versus bash
|
|
* [Configuration](docs/reference/configuration.md)
|
|
* [Building from source](docs/how-to/build.md) · [Running tests](docs/how-to/run-tests.md)
|
|
* [Full docs index](docs/README.md)
|
|
|
|
## Credits and license
|
|
|
|
brush is written by **reuben olinsky** and its contributors:
|
|
<https://github.com/reubeno/brush>. It is an excellent piece of work, and the reason
|
|
Nucleic could get an observable shell by patching rather than by writing one.
|
|
|
|
This fork is distributed under the same [MIT license](LICENSE) as upstream; the
|
|
copyright notice is unchanged. Upstream's [community
|
|
guidelines](CODE_OF_CONDUCT.md) and [contribution
|
|
guidelines](CONTRIBUTING.md) are vendored here and describe *upstream's* process —
|
|
follow them there, not in this repository.
|