Skip to content

Present bee as one user-facing application - #5

Merged
jlgore merged 14 commits into
mainfrom
011-application-consolidation
Jul 23, 2026
Merged

Present bee as one user-facing application#5
jlgore merged 14 commits into
mainfrom
011-application-consolidation

Conversation

@jlgore

@jlgore jlgore commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Implements ADR-0002. Stacked on #4 — review that first; this retargets to main automatically once it merges.

Four installed binaries across seven packages become one executable across five.

bee run | bee repl | bee exec | bee check | bee validate | bee metrics
Cargo.toml     workspace root and the `bee` package
src/           harness library
src/app/       commands, config resolution, session construction
crates/        core · common · userspace · ebpf (built as bee-lsm)

Planned first as a PRD plus eight issues under .scratch/application-consolidation/, then landed one issue per commit. Each commit leaves the tree green, so this is reviewable commit by commit.

The parts that aren't just moves

One session construction. bee-episode and bee-repl each hand-rolled the same startup — non-dumpable, visual gate, theme, provider, key-from-env, model — in two orders with two sets of error strings. When they drifted, the enforcement behaviour of a headless and an interactive run drifted with them, and nothing noticed. Now one path, with the ordering constraints written down rather than implied by line order.

One configuration resolution (src/app/config/). Four sources — flags, --config, .bee/config.toml, ~/.config/bee/config.toml — with precedence applied per field, so a project file setting only the tool list doesn't blank the user file's theme. [provider], [policy] ceiling, and [mcp] are operator-only: each names a credential-bearing variable or sets the bound everything else is checked against, and a project file setting one is rejected by name rather than quietly dropped. A requested policy is attenuated with Policy::derive — the existing validator behind subagent policies and skill grants — so an over-grant produces its words, not a second refusal vocabulary.

Fail-closed sessions. The one real behaviour change. An interactive session with no policy used to become a host session, announced by one line of a six-line banner; a policy on a build without enforcement was parsed, accepted, and ignored. Both now refuse. Three distinct problems get three distinct messages, and --host is how you ask for unenforced execution on purpose. It's flag-only — that decision belongs to the invocation, not to a file written months ago or by a repository.

Decisions worth disagreeing with

  • The raw runner became bee exec, not deleted. test/vm/remote-matrix.sh drives nearly every live enforcement case through it with no LLM involved, and that harness-free signal is worth keeping.
  • User config is ~/.config/bee/config.toml, not ~/.bee/. This supersedes what ADR-0001 originally said; the amendment is recorded in the ADR rather than applied silently. Bee had already shipped the XDG path for [theme].
  • The specs/00* quickstarts got a note, not edits. They record what each feature shipped as; rewriting them would falsify the record.

Two limitations, stated plainly

Headless --host is refused on an enforcement build. run_episode builds its own sandbox and has no unenforced branch under that feature, so honouring the flag would announce an unenforced session and then run an enforced one — exactly the divergence this PR exists to stop. bee repl constructs its own sandbox and is unaffected. Lifting it means giving the harness a host branch: follow-up, not this PR.

The live VM matrix has not run. It needs the BPF-LSM VM. Every enforcement-touching change here is covered by it and unproven until it does: the bee exec rename, the seven episode call sites now going through bee run, the fail-closed invariant, and the bee-lsm artifact rename. That last one is verified to build, copy, and embed on the nightly bpf toolchain — attaching is a separate question.

Verification

features tests clippy
default 249 clean
enforce 245 clean
concurrent 246 clean
mcp 255 clean
tui 408 clean
tui,mcp,concurrent 411 clean

cargo metadata declares one binary in the workspace. --features enforce builds on the nightly bpf toolchain. xtask is unaffected. Every command in the README runs as written. The dependency guards still fail when violated — verified by temporarily adding tokio to bee-core, not by assuming.

Two pre-existing problems surfaced. anthropic_smoke fails on the clean tree too (the environment's key is rejected with Auth) — confirmed against a stash, untouched here. And cargo test --features concurrent hadn't compiled since 008 added two ProviderConfig fields a test helper never got; fixed in passing because it blocked the feature-matrix verification.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BWKsWk33azkprDw7mv2JxP

jlgore and others added 13 commits July 22, 2026 20:53
ADR-0002 decided bee installs one host executable; the working design
under it ended with seven open decisions and a migration sequence nobody
could pick up as written.

Resolve all seven and file the work as a PRD plus eight
independently-landable issues under .scratch/application-consolidation/.
The load-bearing decisions: the raw process runner becomes `bee exec`
(the live VM matrix drives nearly every enforcement case through it,
harness-free, and that signal is worth keeping); user configuration stays
at the XDG path the code already ships rather than the ~/.bee/ ADR-0001
proposed; and the crates/ move lands last, alone, so mechanical churn
stays out of the behaviour changes.

Also lands CONTEXT.md, ADR-0001, and ADR-0002 themselves, which were
untracked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWKsWk33azkprDw7mv2JxP
ADR-0002 puts the headless harness session at `bee run`, and nothing else
in the consolidation can move until the raw runner has vacated that name.
Doing the rename first, alone, means no later issue has to reason about
`run` meaning two things and no window exists where a VM script silently
runs the wrong thing.

Behaviour, flags, and exit codes are unchanged — this is a rename plus a
reframing. `bee exec`'s help now says "diagnostic" so it reads as
secondary to the two primary journeys, and bare `bee` prints help instead
of erroring on a missing subcommand.

The five call sites in test/vm/remote-matrix.sh move with it. That script
is why the runner is kept rather than deleted: it drives nearly every
live enforcement case through it with no LLM involved, and that
harness-free signal is worth keeping.

Adds tests/command_surface.rs to pin the tree shape — in particular that
`bee run` resolves to nothing, which is the regression that would quietly
undo this.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWKsWk33azkprDw7mv2JxP
ADR-0001 committed bee to resolving explicit arguments, an explicit
file, project configuration, and user configuration into a complete
effective configuration — or failing with what is missing. CONTEXT.md
has defined the three terms since. Neither existed in code: every axis
resolved on its own, from its own sources, so nothing was positioned to
say "this configuration is incomplete" and refuse.

Adds the resolver. Precedence is per *field* rather than per file, so a
project file setting only the tool list does not blank out the user
file's theme. Two properties carry the weight:

- Project configuration is untrusted, so [provider], [policy] ceiling,
  and [mcp] are operator-only — each either names a credential-bearing
  variable or sets the bound everything else is checked against. A
  project file setting one is rejected by name, not silently dropped.
- A requested policy is attenuated with Policy::derive, the same
  validator behind subagent policies and skill grants, so an over-grant
  produces its words rather than a second refusal vocabulary.

Also amends ADR-0001 and CONTEXT.md: user configuration is
~/.config/bee/config.toml, not ~/.bee/. Bee already shipped that path
for [theme] and the XDG state path for metrics; introducing ~/.bee/
would have meant two locations and a migration for the only bee config
file anyone has. The amendment is recorded in the ADR rather than
applied silently.

Nothing consumes the resolver yet — issues 03-05 wire it in, and the
temporary allow(dead_code) says so.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWKsWk33azkprDw7mv2JxP
ADR-0002 names the headless harness session as a primary journey; it
shipped as `bee-episode`, a name describing an internal unit of work
rather than something an operator wants to do.

Extracts session construction first, because moving the journey without
it would just relocate the duplication: `bee-episode` and `bee-repl`
each hand-rolled the same sequence — non-dumpable, visual gate, theme,
provider, key-from-env, model — in different orders with different error
strings, and when they drifted the enforcement behaviour of a headless
and an interactive run drifted with them. src/session.rs is that
sequence once, with the ordering constraints written down rather than
implied by line order.

`bee run` carries bee-episode's full surface — scenario or ad-hoc task,
batch, concurrent batch, output path, quiet, theme — on the issue 02
resolver, keeping the artifact contract (transcript on stdout, progress
on stderr) and every exit code. The async runtime is built inside the
command rather than with #[tokio::main], so `check`, `validate`, and
`exec` never enter one.

tests/run_parity.rs drives identical inputs through both binaries and
compares transcript JSON and exit codes across seven cases. It says so
loudly when `bee-episode` is absent rather than comparing against
nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWKsWk33azkprDw7mv2JxP
The second primary journey, on the same session construction as
`bee run` — so the enforcement path for a headless and an interactive
session is now one piece of code rather than two that happened to agree.

Moves four things out of the old binary and into shared construction,
placed by whether a headless session could want them: skills discovery,
grant resolution, sandbox construction, and the MCP bridge with its
per-turn refresh hook. All four qualify — scenarios already carry a
skills list, and grant resolution is the same attenuation-bounded
operation either way. What genuinely differs is the consent sink, so
that is the injected part; the interactive prompt is a plain function,
since ConsentSink is already blanket-implemented for
Fn(&GrantRequest) -> bool.

The MCP drop ordering is carried over deliberately rather than
reimplemented: config, then bridge, then scope, so the stdio children
die inside the scope they were launched in. Nothing in the type system
enforces that sequence — it was held by a comment, and still is.

Front-end choice, banner, and transcript-on-exit stay REPL-local.
tests/repl_parity.rs compares banner fields and exit codes against
`bee-repl` across seven cases, including the piped `--tui` fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWKsWk33azkprDw7mv2JxP
Bee's first design principle is deny-by-default and fail-closed. `bee
exec` honoured it; the sessions did not. An interactive session without
a policy became a host session, announced by one line of a six-line
banner. A session *with* a policy against a build lacking enforcement
had that policy parsed, accepted, and ignored, with a parenthetical as
the only sign. In both cases the agent then executed tools, and the
operator's belief about what was enforcing came from startup text they
had already scrolled past.

Adds `--host`: unenforced execution is now something you ask for. Its
absence is a startup error, and three distinct problems get three
distinct messages, because an operator shown the wrong one is sent in
the wrong direction — no policy configured, a policy this build cannot
enforce, or a kernel that cannot enforce (which now refuses up front,
the way `bee exec` always has, instead of failing half-built as an
infrastructure error). Host mode announces itself on stderr, in a line
that survives a pipe.

`--host` is flag-only. Not settable from project configuration, and not
from user configuration either: running an agent unenforced belongs to
the invocation, not to a file written months ago or by a repository.

Two things this surfaced. The ad-hoc `--task` policy guard duplicated
the invariant and predated `--host`, so it refused host-mode runs the
operator had explicitly asked for; it is gone, and the invariant owns
that refusal. And headless `--host` on an enforcement build is refused
rather than faked: `run_episode` builds its own sandbox with no
unenforced branch under that feature, so honouring the flag would
announce one thing and run another — the exact divergence this issue
exists to stop. `bee repl` is unaffected; it constructs its own sandbox.

The library entry points are untouched, which is why the harness's own
integration tests pass unchanged: the invariant lives at the command
boundary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWKsWk33azkprDw7mv2JxP
`bee metrics` absorbs the last unmoved journey, and the three retired
`[[bin]]` targets go with their source files. `cargo metadata` now
declares exactly one binary in the workspace: `bee`.

The parity tests lose their counterpart, so they become tests of what
the parity was protecting — the transcript on stdout, progress on
stderr, the exit code per failure — and are renamed for what they now
assert. On an enforcement build the host-mode cases skip with a stated
reason rather than silently: that path needs a BPF-LSM kernel, and it is
covered where it can actually run, in test/vm/.

The VM matrix gets simpler rather than merely renamed. It built and
shipped a second binary, chmod'd both, located it at three points in the
remote runner, and SKIPped the concurrent-audit-isolation case whenever
it was absent or built without the feature. One `--features concurrent`
build of `bee` covers every case, so all of that goes.

Living documents updated and their commands run as written. The
`specs/00*` quickstarts get a one-line note pointing at the rename
rather than edits: they record what each feature shipped as, and
rewriting them would falsify the record.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWKsWk33azkprDw7mv2JxP
A package with one source file, one consumer, and no constraint
justifying its separation. The other four crates are separate for
reasons that pay for themselves — bee-core must stay runtime-free and a
test enforces it, bee-common is no_std because the eBPF side shares its
layouts, bee-ebpf targets another architecture on another toolchain.
bee-hardening had none of those: ordinary host code, using the same
syscall crates bee-userspace already depends on, one directory from its
only caller.

Moved with git mv so history follows, and with its tests, which are the
only thing that would notice the failure mode here — hardening that is
slightly less applied than it claims and behaves identically otherwise.
They pass unedited; the two call sites in spawn.rs changed import prefix
and nothing else.

The optional `ctor` feature crosses over intact. Nothing in the
workspace enables it, but it is a public capability an embedder can ask
for, and dropping it silently would have been a quiet removal rather
than a move.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWKsWk33azkprDw7mv2JxP
…artifact

git mv for bee-core, bee-common, bee-userspace, and bee-ebpf, plus the
workspace members, the shared dependency paths, and the three relative
paths in the userspace build script that reach the eBPF crate.

Renames the BPF binary target from `bee` to `bee-lsm`. Naming it after
the host command meant the object copied out of the eBPF build and
embedded into bee-userspace was, by name, indistinguishable from the
executable people install — tolerable when there were four host
binaries, confusing now there is one. The target name, the path the
build script copies from, the name it copies into OUT_DIR, and the
include in loader.rs all move together; verified by building
`--features enforce` on the nightly bpf toolchain and confirming the
renamed object is produced, copied, and embedded.

The dependency guards resolve manifests by relative path, so both
updated. Verified they still fail by temporarily adding tokio to
bee-core rather than assuming.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWKsWk33azkprDw7mv2JxP
The target layout from ADR-0002: the root manifest is both the workspace
root and the `bee` package, with the harness library at src/ and the
application layer — commands, configuration resolution, session
construction — at src/app/, reached only from src/main.rs. Two module
trees in one package, because the library has exactly one consumer but
the direction of that dependency is still worth seeing.

The four crates/ packages stay separate: their runtime, kernel, no_std,
and BPF-target constraints earn it. The harness library's did not.

Mechanical, and sequenced last for that reason. Everything before it
changed behaviour and was reviewable by reading a diff; this moves ~90
source files and ~30 test files without changing any. Moves are git mv
so history follows. The only edits beyond paths and imports:

- `use bee::viz::bee` in an example now shadows the crate root, since
  the crate and the mascot module are both `bee`. Aliased.
- The dependency guards resolve manifests relative to CARGO_MANIFEST_DIR,
  which is now the workspace root rather than one below it.
- src/concurrent.rs had a test helper missing two ProviderConfig fields
  added in 008, so `cargo test --features concurrent` had not compiled
  since. Pre-existing and unrelated, fixed in passing because it blocked
  the feature-matrix verification.

Verified: 249/245/246/255/408/411 tests pass across default, enforce,
concurrent, mcp, tui, and tui+mcp+concurrent, with clippy clean on each;
`--features enforce` builds on the nightly bpf toolchain; xtask is
untouched; and `cargo metadata` declares one binary in the workspace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWKsWk33azkprDw7mv2JxP
Marks the PRD and all eight issues complete, and records what deviated
from the plan: the harness dependency moved a issue earlier than
written, the consent sink became a function rather than a trait impl,
the ad-hoc policy guard was deleted rather than kept, headless --host is
refused on enforcement builds, and a pre-existing concurrent-feature
compile break was fixed in passing.

Also records what is *not* verified: the live VM matrix has not run.
Every change touching enforcement is covered by it and unproven until it
does.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWKsWk33azkprDw7mv2JxP
The consolidation renamed the packages the workflow drives: there is no
`bee-harness` or `bee-cli` to pass to `-p` any more, and the BPF
artifact is `bee-lsm`. Missed in the issue 08 reference sweep, which
covered docs, VM scripts, and xtask but not .github/.

The mcp and enforce jobs now target the application package directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWKsWk33azkprDw7mv2JxP
@jlgore
jlgore changed the base branch from 010-chat-reading to main July 23, 2026 13:08
@jlgore jlgore closed this Jul 23, 2026
@jlgore jlgore reopened this Jul 23, 2026
Two runtime path derivations still walked up from CARGO_MANIFEST_DIR to
find the workspace. That was right when the manifest dir was
bee-harness/; the application package *is* the root now, so `.parent()`
overshoots and the 009 example scripts resolve outside the repo.

Same class as the grid.rs include_str! fixed during the move, but at run
time rather than compile time, so it did not fail the build — it failed
five tests I reported as passing. The skills fixture lookup had the same
walk; it skips when the directory is missing, so it was silently
skipping rather than failing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWKsWk33azkprDw7mv2JxP
@jlgore
jlgore merged commit 0d67451 into main Jul 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant