Fabro's TerminalSession trait, its DriverTerminalSession wrapper, and
TerminalSize were four method forwards and a size struct over the
driver's PtySession and PtySize. RunSandbox::open_terminal now returns
the driver's session, the server's websocket loop drives it directly and
renders its errors with display_for_log, and open_terminal_for_run sits
with the other reconnect helpers. terminal.rs is deleted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
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>
Closes the sandbox-driver adoption: any provider a sandbox-driver plugin
executable serves can now host a fabro run, and fabro's own bundled
providers can be served the same way.
- `SandboxSpec::Plugin` builds a normalized driver spec from the
environment (image or Dockerfile source, or a provider-managed
directory; resources; network policy; labels; env) and lays fabro's
repository checkout out inside the provider's working directory. The
layout is recorded on the run through the new `workspace_layout` trait
method.
- Plugin settings on a bundled kind (`[server.sandbox.providers.docker]
path = ...`) serve that kind out of process through the driver's
executable; the config layer no longer rejects them.
- `ProviderAccess` carries the server's provider settings and the vault's
Daytona credentials to every reconnect: run resume, sandbox details,
terminals, previews, and the worker's start path. The worker receives
the settings through `StartServices`. No "plugin not wired" errors
remain.
- The CLI worker requires GitHub credentials only when a repository will
be cloned; a `none` target on a clone-based provider creates an empty
workspace and needs none.
- fabro-db tracks its migrations directory so a new migration file
recompiles the crate; the environment provider migration had been
silently missing from stale builds. Environment store 500s now log
their cause.
- The CLI workflow scenarios run against `host-plugin` (the driver's
Host executable under the non-bundled `host` kind) and `docker-plugin`
(the bundled `docker` kind served over stdio), each on an isolated
server, printing the server log on failure. A live Daytona gate runs
the native git clone over the JSON-RPC wire. A new CI job runs the
plugin scenarios and the driver-backed Docker integration tests with
the plugin executables built.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The `daytona` provider kind now maps onto the sandbox-driver Daytona
provider instead of fabro's own SDK client. Fabro keeps what is its own:
the HMAC-named snapshot built from the environment's image or Dockerfile,
the explicit 120 minute auto-stop default, the managed labels that gate
destructive operations, the clone decision and layout, and push
credentials. The driver creates the sandbox, clones natively, and serves
exec, files, search, terminal, SSH, preview, and VNC through its facets.
- `daytona.rs` builds the driver `SandboxSpec` (snapshot source,
`/home/daytona/workspace`, labels, timers, network policy, run name),
ensures the snapshot through the driver `SnapshotProvider`, attaches by
persisted id with fabro's label guard, and probes credentials through
the provider health check under fabro's 20 second budget.
- `DriverSandbox` gains a create plan that settles the spec right before
the provider call, records the snapshot a sandbox came from, and
reports the provider console URL on `Ready`.
- Terminals use the driver `Pty` facet; the server's SSH, preview, and
VNC endpoints use the `SshAccess`, `PreviewUrls`, and `Vnc` facets
through the driver-typed reconnect. The preview endpoint now answers
for every provider with a preview facet, so the local sandbox returns
its loopback URL.
- Daytona credentials travel as `DaytonaCredentials` built from the
vault key plus configured URL and organization; nothing reads the
process environment implicitly. The inventory registry uses the shared
`DriverInventoryProvider`.
- The SDK-based `daytona/mod.rs`, `provider/daytona.rs`, the Daytona
terminal, the `daytona` cargo feature, and the direct daytona-sdk,
git2 (in fabro-sandbox), tungstenite, and rustls dependencies are
gone. The live Daytona tests run against the driver-backed sandbox.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SandboxProviderKind is now a validated string newtype instead of a
closed enum. The bundled kinds (local, docker, daytona) keep their
constants and a BundledProvider enum for the code paths that still
dispatch on them; any other well-formed sandbox-driver kind name is
accepted and names a plugin executable. EnvironmentProvider is gone:
environment settings carry SandboxProviderKind directly, and
is_clone_based is replaced by a workspace policy where local runs in a
designated directory and every other provider clones.
Server sandbox policy is keyed by kind. [server.sandbox.providers.<kind>]
accepts the bundled kinds with `enabled` and any plugin kind with its
launch settings (path, sha256, dev, args, env, inherit_env); bundled
kinds reject the plugin keys and a kind with no entry is disabled. The
OpenAPI schema, generated Rust and TypeScript clients, web settings
pages, and docs follow. The environments table drops its provider CHECK
enumeration in favour of the kind name rules so a plugin environment
can be stored.
Bundled-only code paths (run start, preflight, reconnect, terminal,
details) now fail with an explicit message for a plugin kind until the
driver construction function lands in the next step.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Add AppState::cached_run with the standard 500/404 mapping and use it
everywhere handlers read the shared run-projection cache. This also
normalizes two inconsistencies: graph-source cache errors now map to
500 (was 502), and a missing projection in PR create/unlink now maps
to the canonical 404 (was a bespoke 500).
- Extract an EventScan cursor shared by the four run-event scan loops,
delegate list_events_from to the paginated variant, and stop the
stage-event scan once its page is full instead of walking the rest of
the log.
- Hold Arc<RunProjection> in the local projection cache so opening a run
no longer deep-copies the projection (copy-on-write via Arc::make_mut),
and drop the now-unreachable shared-cache branch in last_event_seq.
- Trim hot-path clones: run_files serves the projection Arc directly,
run-state serializes by reference, artifacts only checks existence, and
the command-log handler opens a reader only for the CAS-blob branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>