Commit graph

7 commits

Author SHA1 Message Date
Bryan Helmkamp
957fc97c5c
Merge origin/main into pebble-agent-loop
Main merged the sandbox-driver adoption (#849) in a later form than this
branch was stacked on: the driver's own exec types replace fabro-sandbox's,
shell quoting moved to fabro-util, the sandbox lifecycle collapsed, and the
driver's events are stored as run events. This branch had deleted
`fabro-agent` and put the coding agent, the environment adapter, and the
steering hub on pebble.

The resolution takes main's sandbox API and re-applies pebble on top: the
`RunSandbox` `Environment` adapter moves to `pebble_environment.rs` (main's
`environment.rs` is the sandbox spec) and runs commands through `ExecSpec`
and `ExecControls`, feeding pebble's output sink from the driver's; the
driver-era `sandbox.*` names leave the known-event list, as on main, so a
stored event with that name and no driver shape is `Unknown` rather than an
error; `program_exit_code` matches pebble's non-exhaustive termination; the
Docker and Daytona smokes use main's constructor and credentials; the
remaining `fabro_agent` paths point at fabro-sandbox.

Pebble's `mcp` feature pins sandbox-driver, and the preview-url trait
objects only cross when both sides name one revision, so pebble moved to
main's `a92c0db6` (lithoscomputer/pebble#10) and fabro pins that pebble
revision until it lands on pebble main.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 09:40:00 -06:00
Bryan Helmkamp
a33c9069cf
Run steering over pebble's bus and keep only fabro's attribution
Pebble's `SteeringBus` now owns the map of live sessions, the buffer for
steers that arrive between sessions, fan-out of steers and interrupts, the
close-the-door detach, and the hold that keeps a paired session open. The
hub keeps what only fabro knows: pair records, principals, stage ids, and
the run events that put bus activity on the run's stream in the order its
consumers expect. `PebbleControlHandle` is gone, since the coding agent's
control handle is a bus session natively; the ACP session joins the bus
through a small adapter and carries pebble's steering message end to end,
so a human steer keeps its author on the ACP `agent.steering.injected`
event. A pair message that evicted an older steer is now accepted and the
eviction recorded, where before it was queued and reported as refused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 20:35:31 -06:00
Bryan Helmkamp
8771ef6d8e
Move the Read tool's line numbering and the shell quoting wrapper out of fabro-sandbox
format_lines_numbered renders a file the way the agent's Read tool shows
it: every line prefixed with its number, from an offset for a limit. That
is the agent's presentation, not a sandbox concern, and it only lived in
fabro-sandbox so RunSandbox::read_file(path, offset, limit) could call
it. The function now lives in fabro-agent next to the Read tool, with its
tests, and the Read, ReadManyFiles, and Kimi ReadFile tools number the
text they get from read_file_text themselves. RunSandbox::read_file goes
away; the sandbox returns bytes or text and nothing else.

fabro-sandbox also re-exported shell_quote through a one-line wrapper so
callers could reach it from the sandbox crate or from fabro-agent. The
audited implementation is fabro_util:🐚:shell_quote; the six
importers now use it directly and the wrapper and both re-exports are
gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 15:00:17 -06:00
Bryan Helmkamp
623e8e1c25
Speak the driver's exec vocabulary instead of mirroring it
Fabro kept its own ExecResult, streaming request and result, output
capture stats, stdio process types, and an Error::Exec variant, each a
field-for-field copy of a sandbox-driver type with a translation layer
between them. Every command a tool, a stage, or a hook ran crossed that
layer twice.

The driver's types are now the ones fabro uses. SandboxExec applies
fabro's policy to an ExecSpec (stop grace, the run's working directory,
the explicit-env filter, the Bash helper's BASH_ENV blank winning over a
caller value) and returns the driver's ExecResult and
ExecStreamingResult as they are. Callers that stream build an ExecSpec
and ExecControls; the buffered exec_command keeps its signature.
ExecResultExt adds fabro's reading of a result: the event-facing
duration, the exit code only when the command exited on its own, the
redacted output tail, and the ExecFailure a non-zero exit becomes. The
three-way termination collapse the run events use lives in one function,
command_termination, called where events are built.

Error::Exec and the git-shaped stderr hint table are gone; a failed
command is the driver's ExecFailure, whose Display carries the label and
the classified metadata and never the raw output. OutputCaptureStats
moves to fabro-agent, whose tool output accounting it belongs to, and
converts from the driver's CaptureStats at the exec boundary. The stdio
process the ACP transport drives is the driver's own, so the cancel-token
bridge and StderrCollector go too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 16:11:45 -06:00
Bryan Helmkamp
6e874aff5b
Retire the fabro Sandbox trait for one concrete RunSandbox
Fabro carried its own Sandbox trait long after every implementation
became a thin layer over the sandbox driver: one production type
implemented it, a delegation macro forwarded it, and each consumer crate
kept hand-written fakes of its thirty methods for tests. The trait
existed to be mocked, and the mocks pinned behavior that no provider
had — canned walk listings that ignored the traversal root, opaque
provider paths, activation failures with no lifecycle behind them.

There is now one sandbox type. RunSandbox keeps fabro's semantics — path
resolution against the run's working directory, the Bash exec policy,
git setup and push, credential refresh — as inherent methods over the
driver's exec, filesystem, search, and git facets, and every consumer
takes Arc<RunSandbox>. The directory, grep, and walk types are the
driver's own, re-exported from fabro-sandbox. The exec policy reports
the provider's measured duration rather than its own clock.

Tests script a sandbox through fabro-sandbox's MockSandbox: a struct of
fields (seeded files, the result every command returns, the platform,
a runtime directory) that hands out a RunSandbox over the driver's
scripted doubles and reads back what the code did — commands, timeouts,
environment, term stops, writes, deletes, existence probes. The
hand-written fakes in fabro-agent, fabro-acp, fabro-hooks,
fabro-workflow, and fabro-server are gone; the one wrapper a git
integration test still needs sits at the driver level, hiding a path
from a real Host sandbox. The refresh-ahead loop takes the refresh as a
closure so its schedule is tested without a sandbox at all.

The driver pin moves to the testing-crate stack head, which gained the
double behavior these ports needed: retention caps on scripted output,
canned walks narrowed to the requested base, upload and download on the
memory filesystem, and recorders for deletes, existence probes, and
term stops. One test that modelled a provider handing back opaque object
paths from a walk is removed: the driver contract has no such thing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 00:14:42 -06:00
Bryan Helmkamp
ba3adb92c7
Serve the local sandbox through the driver Host provider
SandboxSpec::Local and run reconnect now build a DriverSandbox over the
sandbox-driver Host provider instead of fabro's own LocalSandbox, which
is deleted. fabro_sandbox::local_sandbox designates the working
directory (created when missing, never removed), creates the Host handle
in a per-process registry, and learns the platform up front. A local
sandbox reports no provider id: it is its directory, which the run
record already carries, so reconnect rebuilds the handle over that
directory rather than by id.

The credential filter for explicit environment variables and the Bash
readiness probe now come from the exec layer and the driver's activate
helper. Test call sites move to the async constructor; test factories
that must stay synchronous share the parent session's sandbox handle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 16:09:45 -06:00
Scott Werner
47bc772f7b refactor: organize crates into three layers 2026-07-23 17:59:34 -04:00