bee is a Rust library and CLI that provides kernel-enforced, capability-scoped sandboxing for AI coding agents. It uses eBPF LSM hooks (via Aya) to enforce per-tool-call and per-subagent policy — file access control, process-exec allowlisting, and network egress filtering — without containers, VMs, or namespace overhead.
See specs/001-ebpf-agent-sandbox/ for the full spec, plan, research,
data model, and contracts.
- Deny-by-default & fail-closed — zero capabilities to start; refuse rather than degrade silently.
- Capability attenuation — a subagent policy is provably a subset of its parent.
- Kernel enforcement is authoritative — user space compiles/loads/observes; the kernel decides.
- Policy-as-data — declarative, diffable, reviewable TOML is the source of truth.
- Library-first, runtime-free core — the CLI is a thin wrapper; the core needs no async runtime.
| Crate | Role | Status |
|---|---|---|
| Five packages, one host executable (ADR-0002): |
Cargo.toml # workspace root *and* the `bee` application package
src/ # the harness library (episodes, tools, REPL core, render pipeline)
src/app/ # the application layer: commands, config resolution, session construction
crates/
core/ common/ userspace/ ebpf/
| Package | Role | Status |
|---|---|---|
bee (root) |
The application and sole host executable, plus the harness library it drives | ✅ host-tested; enforcement behind features |
crates/common |
#[repr(C)] map layouts + verifier-safe matcher primitives (no_std) |
✅ implemented + tested |
crates/core |
Policy types, TOML parsing, compiler/glob-lowering, attenuation validator, audit types | ✅ implemented + tested |
crates/userspace |
Support detection, cgroup lifecycle, hardened launcher (incl. process hardening, FR-011), engine gating | ✅ host logic tested; ⏳ eBPF attach behind --features enforce |
crates/ebpf |
LSM programs (file_open, bprm_check_security, socket_connect), built as bee-lsm |
⏳ requires nightly bpf toolchain + BPF-LSM kernel |
The four crates/ packages stay separate because their runtime, kernel, no_std, and BPF-target
constraints earn it. The harness library does not: it has exactly one consumer.
User-space crates build on stable Rust:
cargo build
cargo test # ~47 unit/integration/property tests (no kernel needed)
cargo clippy --workspace --all-targetsTry the CLI (works without a special kernel):
./target/debug/bee # help — bare `bee` runs nothing
./target/debug/bee check # kernel support gates
./target/debug/bee validate --policy policies/cargo-test.toml # prove policy is runnable
./target/debug/bee validate --policy policies/subagent.toml \
--parent policies/parent.toml # attenuation check# Headless: one agent episode, a batch of them, or a concurrent batch.
./target/debug/bee run --scenario <scenario.toml> --provider <provider.toml>
# Interactive: chat with a sandboxed agent, inline or full-screen.
./target/debug/bee repl --policy <policy.toml> --provider <provider.toml>Both resolve the same effective configuration — explicit flags, then --config <file>, then
.bee/config.toml, then ~/.config/bee/config.toml — and both refuse to start rather than run
unenforced by accident. A session with no policy needs an explicit --host, which says out loud
that tools will run as hardened host processes with no kernel scope. See
ADR-0001 and
ADR-0002.
bee exec --policy P -- COMMAND runs a single host command inside a scope. It is a diagnostic —
a way to prove a policy enforces with no agent in the picture — and needs a BPF-LSM kernel, so it
fails closed on an ordinary host. bee metrics reports recorded usage, cost, and latency.
bee repl runs the classic inline REPL by default. Build with the tui feature and pass --tui for
a full-screen chat surface with model-owned live panels (008-grid-tui):
cargo build --release --features tui
./target/release/bee repl --provider <provider.toml> --tui # --no-tui forces inlineThe agent can address a rendered widget to a named side panel with render_to("metrics", widget)
(and render_to_ttl / remove_panel / clear_panels to manage them); an untargeted render(widget)
still flows inline. --tui degrades honestly — piped output, TERM=dumb, or a terminal below 40×10
fall back to the inline REPL with a one-line note. The feature is off by default, so the standard
build pulls in no terminal backend.
Assistant replies render as markdown once the message finishes streaming — headings, emphasis,
lists, links and code, styled through the active theme's semantic roles rather than a second palette.
A skill's instructions render the same way when you /skill <name>, and the agent can emit a block
itself with markdown(source) in a render script.
Keys: ↑/↓ and the mouse wheel scroll the conversation (they move the cursor first when the input
holds a Shift+Enter newline), PgUp/PgDn page it, gg/G jump to the ends, Ctrl-P/Ctrl-N
walk input history, Tab focus, p panel overlay on narrow terminals, y yank, ? help, q quit.
Mouse reporting is on so the wheel works, which means drag-to-select is the terminal's Shift-click
gesture there.
Tools for using an agent to review code, rather than only to write it. All default-off: a build that selects none of them has the dependency graph it had before they existed.
cargo build --features sec,astgrep-rust,astgrep-python # everything, with two grammars
cargo build --features findings # just the ledger
cargo build --features astgrep,astgrep-rust # just structural search, one grammar| Feature | Tool | What it is |
|---|---|---|
astgrep + astgrep-<lang> |
ast_grep |
Structural search over the parse tree, so a match in a comment or a string literal is not a match. One feature per grammar — each is compiled C, so a build pays only for the languages it scans. |
findings |
record_finding, list_findings |
The durable finding ledger. |
cvss |
cvss |
Severity computed from a vector, never asserted by the model. |
scanners |
scan |
External scanner adapters — Opengrep for pattern rules, CodeQL for whole-program analysis. Implies findings. |
gitlog |
git_log |
Repository history via gix — log for the commits touching a path, blame for the commit that introduced one line. Read-only; runs in a scope-joined child like every other file tool, because .git is a directory of files. |
sec |
— | Umbrella for all of the above. Grammars stay explicit. |
Two tiers, one rule. When the value is the engine, bee links the crate and runs it as a
scope-joined child. When the value is a curated rule corpus, bee drives the third-party binary
instead of reimplementing it — under an inode-pinned exec.allow grant, with argv built from typed
inputs. The model chooses what to scan; never how the scanner is configured.
# policy — the capability
[exec]
allow = ["!/home/you/.local/bin/opengrep"] # `!` pins the inode
# config — how the granted binary is configured (operator-only)
[security.scanners.opengrep]
rules = "/etc/bee/opengrep-rules" # `auto` is refused: a scanning scope has no egressCodeQL is the same shape, plus a version pin. The grant names the bundle's own CLI; the pin is verified by running it before anything is analysed, so a bundle that is not the one the operator provisioned is an explicit unavailability rather than a thin set of results.
[exec]
allow = ["!/opt/codeql-bundle/codeql/codeql"]
[security.scanners.codeql]
bundle_version = "codeql-bundle-v2.26.1" # or "2.26.1" — either spelling of the same pinDatabases are built one way, --build-mode=none, so actions, csharp, java, javascript,
python, and ruby are analysable and everything else is declined by name. Extracting a
compiled language means intercepting the build's process spawns, which would mean admitting every
compiler and linker that build happens to invoke — an unbounded widening of the very scope bee
exists to hold. Half-support would be worse than none: a thin database yields few findings, which
reads exactly like clean code.
Findings outlive the session. They land in .bee/findings/ledger.jsonl — append-only, one JSON
object per line, meant to be committed and reviewed in a pull request. Re-running merges rather than
duplicating, and identity excludes the line number so code movement does not mint a duplicate.
./target/debug/bee findings list
./target/debug/bee findings adjudicate <id> --state false-positive --note "sanitised upstream"Adjudication is CLI-only and has no tool equivalent: an agent can record what it saw, and only a person can rule on it. A later automated rediscovery appends a sighting and cannot clear the verdict.
Every tool answers in three states — completed, could-not-run, or failed. A missing binary, an ungranted scanner, a substituted binary, a timeout, or an unparseable report each say so explicitly. None of them can render as "scanned, found nothing", because a security tool that reports no issues when it did not run is worse than one that was never installed.
MSRV note. The application package declares Rust 1.88 (
ast-grep-core's floor). The embeddable crates —bee-core,bee-common,bee-userspace— keep 1.85.
The enforce feature and the crates/ebpf package require:
- Linux ≥ 5.7 with
CONFIG_BPF_LSM=yandCONFIG_DEBUG_INFO_BTF=y, bpfin the active LSM list — checkcat /sys/kernel/security/lsmfor abpftoken; if absent, addlsm=...,bpfto the kernel cmdline and reboot (CONFIG_BPF_LSM=yalone is not sufficient),- cgroup v2 unified hierarchy, and
- the nightly bpf toolchain:
rustup toolchain install nightly --component rust-srcandcargo install bpf-linker(LLVM version matched to the pinned nightly — the top build-break risk).
bee check verifies these gates and fails closed (exit 65) when enforcement is unavailable, so bee
never runs with silently-absent enforcement.
- Policy preparation has two stages.
bee-corecompiles author intent into a backend-neutralCompiledPolicy;bee-userspacemust then produce an eBPFEnforcementPlanbefore any kernel state is touched.bee validateruns both stages, so success means the policy is runnable by this backend. - Current eBPF glob support is exact/subtree plus postfix (
*.ext). Segment (**/name) and bounded-star patterns are normalized by the core but rejected during enforcement planning, as are inode-pinned executables and enabled exfiltration detection. Nothing is silently weakened. - Path matching sees resolved paths. LSM hooks fire after path resolution, so enforcement is TOCTOU-resistant, but a rule written against a symlink's name won't match its target, and name-based rules don't catch hard links / alternate bind mounts to the same inode. Inode-pinned rules are not yet implemented and are rejected during enforcement planning.
- Capabilities to load LSM programs:
CAP_BPF + CAP_PERFMON(possibly+ CAP_MAC_ADMIN) orCAP_SYS_ADMIN. The exact minimal set is kernel-version-dependent — verify empirically per target.
Exfiltration detection (US3), the Landlock+seccomp degraded fallback, macOS/Windows, and behavior when bee runs inside an existing container are out of scope for the MVP (see the spec's assumptions).
MIT OR Apache-2.0