Commit graph

126 commits

Author SHA1 Message Date
Bryan Helmkamp
a4af6fac92
Show the sandbox's ready duration and retention outcome on the run
`RunSandboxInstance` carries `ready_duration_ms` from the root scope's
`scope.acquired` and `retained` from its `scope.released`, so the view
says how long the sandbox took and whether it still exists after the run.
The OpenAPI schema, the TypeScript client and the web sandbox tab's
overview show both; the host sandbox scenario asserts them on a real run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 19:34:56 -04:00
Bryan Helmkamp
0fe066d420
Fix the rustdoc link warnings and gate rustdoc in CI
Every intra-doc link `cargo doc --workspace --no-deps` warned on now
resolves or is plain code: the private constant and helper, the removed
`InterpString::resolve`, the lithos `Message`, the sandbox-driver facets,
the `RunOptions::git_author` the cutover removed, and the stale
`platform_record_for` paragraph on the platform records. The `[@REF]`
segment of `fabro run`'s help text is allowed as help, not a link. A
`Rustdoc` job runs the same command with `-D warnings`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 19:24:34 -04:00
Bryan Helmkamp
f0b23fe426
Project a Petri run's sandbox instance from its scope records
Petri a5906f6 records where each scope's sandbox ran (`scope.acquired`,
`scope.failed`) and how its lease was released (`scope.released`). The
projection folds the root invocation's records into `Run.sandbox`:
`initializing` from `run.started`, `ready` with the `RunSandboxInstance`
(the provider, Petri's `host` as Fabro's `local`, the provider's id, the
image and snapshot, the working directory) from `scope.acquired`, `failed`
from `scope.failed`; the retention outcome is kept in the fold state, since
the view has no field for it. Ask Fabro reconnect and `sandbox cp`,
`preview` and `ssh` reach the run's sandbox again.

A local reconnect designates the recorded working directory again when the
host provider does not know the id: the provider mints a registry-only id
for a workspace path too long for a path-derived one, and that registry
belongs to the run's worker. The stream listing redacts its items the way
the attached stream does, so a client that pages after a stream sees the
same items.

Pins move to Petri a5906f6 (run format 6, engine log v11, event contract
4). The attach stream snapshot is re-recorded with the new record and a
filter for the host provider's minted ids; `sandbox cp` reads an upload
back through the run's workspace, which is no longer the target folder.
Server scenario tests prove the projected instance on the host and Docker
providers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 15:57:12 -04:00
Bryan Helmkamp
60b503322c
Delete the legacy run event log, its reducer and its types
Step 4 of the legacy executor deletion, fourth commit: with no writer
and no reader left, the legacy event log goes.

- `fabro-types`: `run_event` (`EventBody`, `RunEvent` and every props
  struct), `EventEnvelope` and the `RunEventDetail*` types are deleted.
  What the projection and the API still use moves out of the event
  vocabulary: `AgentEventProps`, `AgentSessionActivatedProps`,
  `AgentToolsAvailableProps`, `StagePromptProps`, `SessionCapability`
  and the coding event names to `agent_props`; `RunNoticeLevel` and
  `RunNoticeCode` to `notice`; `InterviewOption` beside the question
  types; `RunRunnableSource` beside the run status. `Checkpoint` is
  what Fabro records for a Petri run: `timestamp`, `current_node`,
  `git_commit_sha`; the conclusion's stage summaries derive from the
  projection's stages instead of the checkpoint's node maps.
- `fabro-store`: the Slate bridge (`RunDatabase`, the Slate `Database`,
  `keys`, `record`, `EventPayload`) and the reducer (`run_state`) are
  deleted. `Database` is the blob table and the run summary store over
  one pool; the blob store is SQLite only; the run summary store keeps
  the `runs` row a projector writes and lists, and finds the pull
  request creation candidates over `platform_records`; `build_summary`
  and `projected_usage` live in `run_summary`. The SlateDB dependency
  is gone. Test fixtures build the store from its two SQLite stores.
- `fabro-workflow`: the `event` module (the `Event` enum, its
  conversion, sink, emitter, redaction, stored fields and names),
  `runtime_store`, `StageScope` and the legacy seeding test helpers are
  deleted; the tests that seeded legacy runs read platform records or
  a projection instead.
- `fabro-sandbox` owns `GitRetryReason`.
- The server builds the store without an object store; the legacy
  `POST /runs/{id}/events` tests go, an interrupt answers
  `interrupt_unsupported` in the tests as it does in the handler, and
  the tests that read a run back through the Slate handle read its
  projection or its platform records. The projection folds a block
  that lands while the run is paused as the pause's prior block, and a
  pause or unpause clears the pending control it answers; a control
  request's check-and-append holds a per-run lock so two concurrent
  cancels record one request.
- The CLI's final output is the response of the last stage that
  produced one; the workflow tests read completed nodes from the
  succeeded stages.
- The spec's `RunCheckpoint` carries the three fields the type keeps.

Still failing until the next commits: the CLI tests that seed runs
through `POST /runs/{id}/events` or wait for legacy event names, and
the two Ask Fabro resume tests (the sandbox instance gap).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 14:09:17 -04:00
Bryan Helmkamp
14f69f50cd
Surface dropped Daytona output to the agent as a stderr line
When the driver reports an output loss on a streaming exec, the pebble
Environment adapter now appends one line to the stderr it hands back:

    [sandbox] N output frame(s), M bytes dropped by the provider

Pebble renders the result's stderr into the tool output, so the model
and the run log both see that the command's output is incomplete rather
than reading a silently shortened stream. The adapter also logs one
`warn!` with `dropped_frames`, `dropped_bytes`, and the command's first
word, bounded, so the operator can find the event without the log
carrying the command itself.

The loss is not folded into pebble's per-stream capture stats: the
dropped frames' stream is unknown and the counts are of encoded bytes,
so attributing them to stdout or stderr would be a guess. The driver's
`truncated` flags on both captures already say the counts undercount.
`ExecOutputTail` is pebble-owned and mirrored in the OpenAPI spec, so
the run events are left alone.

Tests cover the appended line with and without existing stderr, a
lossless command over the scripted double staying unchanged, the
bounded program name, and a lossy command end to end through
`Environment::exec` over a scripted sandbox whose exec facet reports a
loss (the driver's scripted double has no knob for it).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 12:24:02 -06:00
Bryan Helmkamp
3b8d712edb
Delete a live Daytona test's sandbox even when the test panics
The live Daytona tests create a provider sandbox and delete it on their
last line, so any panic or failed assertion before that line leaks a
running, billed sandbox. Two leaked that way on 2026-09-14 when a
sandbox-driver decoder flake panicked daytona_playwright_mcp_sandbox_transport.

Add fabro_sandbox::test_support::DeletedOnDrop, a guard that owns the
RunSandbox (Deref keeps the tests reading unchanged), offers an explicit
delete(self) for the happy path, and deletes from Drop otherwise. The
drop-time delete runs on its own thread and runtime because the test's
runtime may be unwinding. It reconnects the provider through the
ProviderAccess the test built the sandbox with, because the sandbox's
own handle pools HTTP connections whose tasks live on the test's runtime;
a live check of that path timed out after 10s.

Every Daytona test that creates a sandbox now holds it through the guard.
Unit tests over the scripted double prove delete-on-drop runs once, an
explicit delete runs once, and a panic inside catch_unwind still deletes
with and without a runtime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 11:01:24 -06:00
Bryan Helmkamp
1d1cacbef3
Pin pebble main 6996942 and implement its PortRoutes trait
Pebble now owns the trait an application hands its MCP support to reach a
port inside the environment. SandboxPortRoutes implements PortRoutes over
the run sandbox's preview-URL facet: a missing facet is Unsupported, a
driver failure is Failed with the driver error as its source. The mcp
feature no longer pins sandbox-driver, so fabro's sandbox-driver pin moves
on its own from here.

The new pebble rev also puts the summary call's usage and cost on
CompactionCompleted, which the CLI progress test literal names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 07:03:36 -06:00
Bryan Helmkamp
e5d5c534ab
Merge remote-tracking branch 'origin/main' into remove/run-metadata-branches
Resolve conflicts between the metadata-branch removal and the
sandbox-driver adoption on main:

- fabro-sandbox docker.rs, sandbox.rs, daytona/mod.rs: take main's driver
  rewrite. The Sandbox trait is gone, so the PR's push_token_source
  removal now applies to RunSandbox instead; drop that accessor and the
  RepoCredentials::source helper that only served it.
- run_metadata.rs: keep deleted. Main's edits there were adaptations to
  the driver API and the run git identity field.
- lifecycle/git.rs, finalize.rs: keep the PR's removal of metadata
  snapshots and write_finalize_commit; carry main's RunSandbox,
  GitRetryPolicy, git_identity, local_sandbox, and test catalog changes.
- sandbox_git.rs: take main's version and drop the shadow_sha parameter
  and Fabro-Checkpoint trailer.
- git_integration.rs: remove meta_branch from the new git identity test.
- Cargo.toml: main's dependency set with fabro-dump kept as a
  dev-dependency.
- checkpoints.mdx: keep both the git identity paragraph and the durable
  execution state section.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 16:28:01 -06:00
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
6592505d0e
Take environment helpers, compaction accounting, and search providers from pebble
Steps 4, 6, and 7 of .ai/plans/pebble-absorbs-embedder-concerns.md,
pinning pebble fc907a1 with its `search-providers` feature.

The sandbox's `Environment` adapter uses pebble's `environment::support`
for the glob grammar check, the tree-order sort of a listing, and the
capture accounting, in place of its own copies; the adapter itself stays.
The stage reads the compactions a prompt performed from the report, as a
breakdown of the usage it already billed. `web_search.rs` keeps the
vault-backed credentials and the Brave-over-Venice preference, and hands
pebble's `Brave` or `Venice` provider a fabro HTTP client; the providers
and their tests are pebble's now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 19:51:12 -06:00
Bryan Helmkamp
64c578d044
Take MCP servers from pebble
Step 3 of .ai/plans/pebble-absorbs-embedder-concerns.md, pinning pebble
6cdb30a with its `mcp` feature.

Pebble starts the stage's MCP servers while the agent is built, registers
their tools under `mcp__{server}__{tool}` with `ToolSource::Mcp`, and
closes them with the agent, for all three placements: a child of the run
worker over stdio, a server over HTTP (streamable or SSE), and a server
launched in the run sandbox and reached through the sandbox's preview
URL. `fabro_mcp::pebble::pebble_server` maps `McpServerSettings` onto
pebble's `McpServer`, keeping fabro's `/sse` path for sandbox-hosted SSE
servers; `RunSandbox::port_routes` hands pebble the driver's `PreviewUrls`
facet as the route to a sandbox port. The stage's event sink mirrors
`McpServerReady` and `McpServerFailed` onto the run's `agent.mcp.ready`
and `agent.mcp.failed` events, as it mirrors `RouteFailover` onto
`agent.failover`, and stores no second copy of a mirrored fact.

Deleted: `sandbox_mcp.rs`, the MCP branches of `pebble.rs` and `fabro
exec`, and fabro-mcp's client, connection manager, HTTP helpers, and SSE
transport, whose tests moved to pebble. fabro-mcp keeps the settings
re-export, the mapping, and a stdio client behind `test-support` for the
tests of fabro's own MCP server. `fabro exec` reports each server's
outcome from the agent's snapshot. The Daytona Playwright live test now
drives the sandbox-hosted server through an agent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 19:26:41 -06:00
Bryan Helmkamp
8d32b472db
Pin the merged pebble main and adopt the pebble features petri already uses
Step 0 of .ai/plans/pebble-absorbs-embedder-concerns.md. Pebble's
fabro-exec-sink-max-turns branch is merged into its main (pebble 4db661c,
on the embedder-concerns branch until it lands), so fabro and petri pin
one line. Both prompt budgets survive the merge because they count
different things.

- Agent hooks use `with_max_tool_rounds`, which is what the
  `max_tool_rounds` setting names and what petri does: `max_tool_rounds`
  model turns may run, and the hook proceeds on a turn that still asks for
  tools, so pebble gets `max_tool_rounds - 1` rounds and `ToolRoundsExhausted`
  fails open. Zero rounds proceeds without an agent, as the old loop did.
  Agent stages set no turn budget; the stage timeout and stall watchdog
  bound them.
- Prompt stages load project memory through pebble's `ProjectMemory`, the
  loader agent stages already run over `with_memory_files`, instead of a
  hand-rolled copy of its budget, deduplication, and truncation.
- `fabro_sandbox::SecretRedactor` puts fabro's secret scanner on pebble's
  text seams (process output tails, failed tool messages) for agent
  stages, Ask Fabro, hook evaluators, and `fabro exec`. The final redaction
  pass over every stored `RunEvent` stays; this does not replace it.
- Agent stages state their compaction policy explicitly: the 80 percent
  threshold and six preserved turns fabro's own agent loop applied.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 18:16:03 -06:00
Bryan Helmkamp
350abac014
Build the local sandbox through the one provider path
SandboxSpec had a Local variant beside the provider spec, and a local
sandbox was created by hand over a bare Host provider: no workspace, no
provider connection, its own reconnect, and its own push rule for the
designated directory. The local kind is now one more SandboxSpec:
SandboxSpec::local names the directory on a HostDirectory spec with a
skip clone, and provider_sandbox builds it like a plugin kind, creating
the directory when missing since the Host provider requires it to exist.
Every RunSandbox carries a workspace; a handle wrapped as is gets the
workspace of its own working directory.

The push rule is one rule for every checkout: a checkout fabro cloned
pushes with the credentials it was cloned with, and any other checkout
pushes when it has an origin, with whatever credentials it carries. A
local run therefore pushes the same way before and after a resume;
before, a reconnected local sandbox carried an attached workspace that
never pushed while a fresh one did.

Reconnect uses the recorded id for every kind. The recompute of a local
id from its directory, kept for records written before directories had
ids, is gone, and test fixtures that wrote made-up local ids derive them
through test_support::local_sandbox_id instead. A local run's record now
carries its workspace layout like every provider-chosen directory, and
the sandbox.initializing event precedes the driver's create events for
local as for every other kind.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 16:28:17 -06:00
Bryan Helmkamp
981253f990
Collapse the run sandbox's lifecycle surface
RunSandbox grew its lifecycle methods one adopter at a time and ended up
with several names for each step. Reconnecting from a run record had
four entry points (reconnect, reconnect_for_run,
reconnect_for_run_with_events, reconnect_driver_for_run) that all
forwarded to the last one. Bringing a sandbox back had two (start and
activate) over the same make_ready, and releasing it had two (delete and
cleanup) over the same release. Two more methods had no callers at all:
set_autostop_interval, which nothing set after the driver took over
lifecycle timers, and resume_setup_commands, which resume stopped using
when checkout moved to the git facet.

There is now one of each. reconnect_for_run takes the record, the
provider access, an optional run id, and an optional event context;
callers that need none pass None. activate is the single "make usable"
step: a running sandbox only learns its platform when it has not yet, a
stopped or paused one is started and its Bash verified, and resume calls
it like every access-time caller. delete is the single release; for a
designated host directory it frees the handle and leaves the directory in
place, as cleanup did. The tests and server call sites follow the
renames; behavior is unchanged except that resuming an already running
sandbox no longer re-runs the Bash probe.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 15:05:06 -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
f69a6779c8
Shrink fabro-sandbox's error module to what callers use
After the driver rounds, fabro-sandbox's Error keeps four variants:
Message, Context, AnyhowContext, and Driver. The module still carried
helpers written for callers that never arrived: incomplete_operation,
is_transport, and is_unsupported classified driver variants nothing in
fabro branches on; From<String> and From<&str> let a bare string become
an error, which no call site did; driver_error duplicated the From impl;
exec_failure and is_not_found had only test callers, and those tests can
match on the driver error directly.

This removes them. Callers build a Driver error through Error::from, and
the two tests that inspected a failure now match on Error::driver(). The
Driver variant's doc names the driver variants fabro does act on: Exec,
Git, and NotFound. The redaction and log-rendering helpers stay; they
are what the error module is for.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 14:56:34 -06:00
Bryan Helmkamp
99d226250e
Drop fabro's explicit-env credential filter
SandboxExec carried an ExplicitEnvPolicy that, for local runs, dropped
credential-shaped names out of the caller's explicit environment before
the spec reached the driver. The filter duplicated the sandbox driver's
Host provider, which applies the same safelist and suffix list to the
inherited process environment and, by its own contract, leaves explicit
spec env alone as the deliberate channel for secrets. Since fabro
composes the explicit environment itself, the second filter added no
protection. It only stripped variables a caller had set on purpose, such
as a GITHUB_TOKEN for a local command stage, and it forced every
constructor to pick a policy by provider kind.

This removes ExplicitEnvPolicy, the safelist, is_sensitive_env_var, and
the env_policy field on SandboxExec and RunSandbox. SandboxExec::new
takes only the exec facet, and the explicit environment goes to the
provider as composed on every provider. The tests that exercised the
filter are replaced by one that shows a credential-shaped explicit
variable reaching the command on the Host provider; the BASH_ENV test
stays, since that blank is the driver's and still holds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 14:53:10 -06:00
Bryan Helmkamp
ab3ef9371a
Let the driver strip terminal control sequences from command output
fabro-sandbox kept its own sanitize_exec_output, a character walker that
removed ANSI escape sequences and control characters from a command's
output tail after redaction. The sandbox driver already offers this as
ExecSpec::output_sanitization, applied chunk-safely to buffered and
streaming output, so fabro carried a second, weaker copy of the same
logic that only ran on the rendered tail and never on the streams the
agent, the command stage, or the sink consumers read.

SandboxExec::apply_policy now sets OutputSanitization::StripAll on any
spec still at the driver's raw default, so every run and run_streaming
call through fabro's exec policy returns text with escape sequences and
stray control characters already removed. A caller that chose another
policy keeps it. spawn_stdio is untouched: long-lived stdio processes
stay raw, as the driver requires. redacted_tail now only redacts secrets
and applies the byte cap, which remain fabro's knowledge, and the
private sanitizer is gone. The tail test that built an ExecResult by
hand now runs a printf through the Host provider and checks that the
stripped output reaches both the result and the tail, and a new test
pins the policy's default and its respect for an explicit choice.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 14:48:51 -06:00
Bryan Helmkamp
18a3c4741e
Run agent stages, Ask Fabro, and fabro exec on pebble's CodingAgent
Replace fabro's hand-written agent loop with pebble's `CodingAgent` and
delete the `fabro-agent` crate.

Workflow: `PebbleBackend` builds one agent per stage over `RunSandbox`,
binds the stage's hooks as tool middleware, the interviewer as the
human-input provider, and a durable `EventSink` that writes every agent
event through the run event log before the agent goes on. Full-fidelity
threads continue across stages through `export`/`resume_from_export`.
Model failover takes the session record after the failed prompt and
continues it on the next route with `ResumeMode::UseModel`, so no tool
effect repeats. The steering hub targets pebble's control handle, with
a steering lease holding completion open while a human is paired.

Events: `EventBody::Agent` carries pebble's `CodingAgentEvent` envelope;
the per-variant bodies, the transcript projection, and the fabro-only
context-window, tool-summary, and skill types are gone in favor of
pebble's. The OpenAPI schemas, generated Rust and TypeScript clients,
and web readers follow.

Ask Fabro: the session runs a `CodingAgent` under a read-only permission
policy and a system prompt transform. Its conversation lives in a new
`run_session_records` table and resumes on the recorded model with the
event cursor advanced past the run log.

`fabro exec` builds the same agent over a local sandbox with pebble's
permission middleware and an interactive approval service.

The catalog fills in `metadata.agent.profile` for operator providers
that declare none, so pebble's lookup is the one resolution path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 14:19:15 -06:00
Bryan Helmkamp
d9929a24d7
Keep the driver's own BASH_ENV blank out of a recorded command's env
The test double's captured environment stopped filtering the BASH_ENV
blank when fabro's exec policy stopped inserting one, but the driver's
Bash helper still records its own blank on the spec, so a test comparing
the caller's variables saw an extra entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 13:36:57 -06:00
Bryan Helmkamp
5588ae3648
Run the agent's MCP servers and the service listing through the driver's services facet
The agent launched a sandbox MCP server with its own setsid wrapper and
PID handling, polled ss for the port in a shell loop, and read a fixed log
file for failures; the server listed a sandbox's services with its own ss
and procfs scripts and parsers, and told the API which one it had used.
The driver's services facet now does both: the agent spawns the server as
a service, waits for the port, reads the service's logs on failure, and
stops it on cancellation; the server lists the driver's listening ports,
grouped by port with the process names the sandbox can give. The
discovery source leaves the API and the web panel's iproute2 tip goes
with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 13:18:02 -06:00
Bryan Helmkamp
29e7e77c75
Let the Daytona provider cache images as snapshots instead of fabro
Fabro named Daytona snapshots by an HMAC of the image or Dockerfile, the
resources, and the API key, ensured them through the driver's snapshot
service before every create, and threaded that work through a create
plan so the sandbox could learn the snapshot it came from. The driver's
Daytona provider now does this inside create: an image or Dockerfile
spec is built once into a snapshot named by its inputs under the API key
and reused for the same inputs, with the build reported through the
create's events. Fabro's overlay only fixes the working directory,
names the run, sets the timers, and falls back to Daytona's default
snapshot; the sandbox reads the snapshot it came from off the driver's
status after the create. The snapshot identity module, the ensure step,
the create plan, and fabro-sandbox's hashing dependencies go. Snapshot
names change from fabro-<uuid> to the driver's sandbox-driver-<hex>, so
existing snapshots are rebuilt once.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 13:11:16 -06:00
Bryan Helmkamp
d24042ba64
Put the driver's sandbox status on the API instead of a projection
Fabro projected the driver's status into its own state enum, resource,
network, and timestamp types for the run sandbox and inventory endpoints,
losing the provider's state string, the network policy, the sandbox kind,
and the driver's own vocabulary along the way. The API now carries the
driver's SandboxStatus itself: SandboxDetails is fabro's run record beside
the status, SandboxInfo is the provider beside the status, and the
OpenAPI schema describes the driver's types (state, resources in the
units the driver reports, the network policy, the sandbox kind, workspace
ownership) which fabro-api reuses through with_replacement with round
trip tests proving identity and JSON parity. The projection types and
their conversion go; the web sandbox page and summary panel read the
status directly, and the TypeScript client is regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 13:07:00 -06:00
Bryan Helmkamp
7d9d2bf5f3
Run fabro's git plumbing through the driver's verbs
Fabro assembled its own hardened git command lines (maintenance, hooks,
fsmonitor, path quoting, signing, the file transport, external diff
drivers) in three crates and parsed raw diff, numstat, cat-file, and log
output itself. The driver's git facet now carries fetch, rev-parse,
ancestry, diff entries, numstat, patch, log, blob sizes and contents,
config, untracked files, and stage-all, hardened by default and typed, so
the checkpoint commit, the run diffs, the Run Files listing and blob
reads, the commit log, the fork fetch, the agent's changed-files
detection, and the git identity setup go through it. The parsers and the
command prefixes go; the per-run capability probe keeps its own plumbing
script. Checkpoint commits never run repository hooks now, so
skip_git_hooks and commit_timeout are accepted for compatibility only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 12:55:39 -06:00
Bryan Helmkamp
22cd5d3382
Let the driver retry git operations and decide from the credential's age
Fabro carried its own retry loop, its own reading of what a git failure
class means for the credentials in hand, and a credential context derived
from the token snapshot. The driver now owns the loop and the decision:
a rejected credential retries only while its mint time is within the
replication horizon, a remote that could not be reached retries on its
own, a static credential fails fast, and an operation whose outcome is
unknown is never replayed. Fabro keeps its budgets as retry policies
(clone, repository probe, checkpoint push, publish push), hands the mint
time along with the token, and records the driver's attempt history as
the push attempts the events carry. Host-side git (the repository probe
and the metadata push classification) goes through the same decision
from its rendered message.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 12:44:58 -06:00
Bryan Helmkamp
dfe378c8ae
Stop re-blanking BASH_ENV in fabro's exec policy
The driver's Bash helper now blanks BASH_ENV at launch on every provider
whatever the caller passed, so fabro's exec policy no longer inserts the
blank itself and the test double no longer filters it back out. The
Host-backed test that a caller's startup file never runs stays.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 12:36:29 -06:00
Bryan Helmkamp
a39b97c940
Reconnect a local sandbox by attaching to its directory
A local sandbox was rebuilt by creating a fresh Host sandbox over the
recorded working directory, so reconnect, sandbox details, the console
URL, the recorded id, and the terminal each carried a local branch. The
Host provider now derives a designated directory's id from its path and
attaches to it from any provider instance, so reconnect goes through the
one attach path: the record carries that id, a record written before
directories had ids recomputes it from the directory, and describe works
for local like every other kind. The local provider skips the ownership
scope because a designated directory carries no labels and nothing else
shares the host's directories with fabro.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 12:36:29 -06:00
Bryan Helmkamp
6fa965f8aa
Hold a plugin's supervisor as its provider
PluginBackedProvider forwarded every SandboxProvider call to the current
plugin generation and reported no snapshot or volume services because it
could not express a per-generation borrow. The driver's PluginSupervisor
now implements the provider traits itself, so fabro launches it and holds
it as the provider; the wrapper goes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 12:36:29 -06:00
Bryan Helmkamp
8add148580
Read the Daytona scopes fabro needs from the provider's health
Fabro kept its own list of the four scopes a Daytona key needs and
reordered the provider's missing list against it. The provider now
reports the scopes it requires, in the order it documents them, so the
doctor and the install check render what the health check says and the
list lives in one place.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 12:36:29 -06:00
Bryan Helmkamp
454ad13d69
Pin sandbox-driver at the Section 4 branch
The driver's section-4-driver-items branch (lithoscomputer/sandbox-driver#20)
carries the provider-owned scopes, the supervisor as provider, Host attach
by directory, the BASH_ENV launch rule, git retry and verbs, the status
image/snapshot/network split, Daytona snapshot caching in create, the
services port verbs, and RFC 3339 wire timestamps. This commit only moves
the pin and follows the two API changes that no longer compile: the status
projection reads image and snapshot instead of source, and the plugin
supervisor is launched rather than constructed. The deletion rounds follow
one item per commit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 12:29:22 -06:00
Bryan Helmkamp
5e04495740
Make RunSandbox pebble's Environment and pin pebble
Add pebble-agent and pebble-coding-agent as git dependencies pinned to
the pebble branch that carries the live exec output sink and max_turns,
and align the shared crate versions with lithos-llm's lockfile.

RunSandbox implements pebble_coding_agent::environment::Environment
directly: rename_file over the driver's rename with a pre-created
destination parent, grep rendered as path:line:text, pebble's glob
grammar enforced before the driver sees a pattern, directory listings in
tree order, and exec over the streaming path with the output sink mapped
onto the driver's OutputSink. Pebble's EnvironmentContract runs against
the Host provider in the unit tests and against Docker in the live suite.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 10:16:24 -06:00
Bryan Helmkamp
370a6c96d5
Give the checkout its GitHub credentials through the driver's store
The GitHub App token reached the agent's git commands through the origin
URL: after the clone fabro ran `git remote set-url origin` with the
token embedded, then tracked which generation the URL carried, held an
embed lease across every push so a refresh could not rewrite the URL
mid-operation, re-embedded on the first auth-shaped push failure in
case the agent had rewritten origin, and redacted the URL out of every
log line and output tail. The token showed in `git remote -v` and
`.git/config`.

The driver now installs ambient credentials for a checkout: one
credential-store line beside the checkout and a `credential.helper`
entry pointing at it, with the remote URL untouched. Fabro's part is
`credentials.rs`: the token source, one mint for the clone, one resolve
per push operation, and the facet call. The clone carries the token per
call and installs it afterwards; the ACP refresh tick rewrites the store
instead of the URL; fabro's own pushes pin one resolved token for the
whole operation and pass it per call, so nothing is ever re-embedded and
a retry after replication lag presents the same token by construction.

Gone with the URL: `push_credentials.rs`, `redact.rs`, the lease and
drift repair in `git_push`, `RefreshOutcome`, and the `credential_action`
and `refresh_error` fields on push attempt events. Stored events that
carry those keys still read. A failed store install after the clone now
fails setup, where a failed `set-url` used to be logged and repaired by
the first push. The one remaining caller of the URL redactor, the
server's repository probe, uses `DisplaySafeUrl::redact_in`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 10:02:01 -06:00
Bryan Helmkamp
51c82a2e16
Name a missing git from the driver's failure class instead of probing
Every clone ran `git --version` first so an image without git could be
told so. The probe was one extra round trip that could not stop the
clone from failing a moment later for the same reason, and it covered
only the clone: a later status or push in an empty workspace failed
unexplained. The driver now classifies exit 127 and 126 from any git
command as `GitFailureKind::GitUnavailable`, so the clone reads the
class off its own failure and names the image requirement, and the
retry table treats the class as permanent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 09:59:23 -06:00
Bryan Helmkamp
346dba6e50
Pin sandbox-driver at the ambient credentials facet
The driver branch adds `Git::set_ambient_credentials`, classifies a
missing `git` executable as `GitFailureKind::GitUnavailable`, and runs
Daytona's pinned clones through the derived clone after a new
conformance check caught the toolbox pin failing. The project notes
follow the last change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 09:50:18 -06:00
Bryan Helmkamp
c313e34605
Trust the driver's pinned clone instead of re-reading HEAD
A clone pinned to a commit or tag ran a second `git rev-parse HEAD`
through exec and compared it with the pin. The pre-driver clone could
land on the branch head when a pin was unavailable, and the check
existed for that case. The driver's clone fetches the pin directly and
attaches the branch with `checkout -B <branch> <pin>`, which fails when
the pin is absent, so a successful clone already has the pin checked
out; the driver's conformance suite verifies that on every provider.

`PinnedRevision` keeps only what the clone decision still uses: which
kind of pin was asked for, for the error messages.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 09:32:09 -06:00
Bryan Helmkamp
20a5500510
Read a mock sandbox's recordings from the driver double
MockSandbox forwarded a dozen read-backs to the driver's scripted doubles
one line each: the commands run, the term stops, the stdin fed, the files
written and deleted, the lifecycle counts, whether a walk ran. Tests now
ask the double through MockSandbox::driver. The accessors that convert a
recorded spec into the shape a test asserts on stay: the last command, the
timeouts in milliseconds, the caller's environment without the exec
policy's BASH_ENV blank, and written files as text.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 16:55:05 -06:00
Bryan Helmkamp
054a37f830
Hold Daytona credentials as the SDK's configuration
DaytonaCredentials mirrored DaytonaConfig field for field and was copied
into one at connect time. It is now a newtype over the SDK configuration
with the API key always present and a Debug that never prints it; the
driver's Daytona provider connects with the configuration as it is.
Callers build it from an API key, a settings lookup, and the optional
control-plane URL, organization, and HTTP client.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 16:53:20 -06:00
Bryan Helmkamp
91d905813f
Map an environment straight onto the driver's sandbox spec
SandboxOptions was an intermediate between the environment's settings and
the driver's SandboxSpec that mirrored the spec field for field: image
and Dockerfile for the source, cpu and byte sizes for the resources,
auto-stop for the timers, plus the two clone fields. Every provider
overlay then read the options a second time to fill the spec.

The environment now maps onto the driver spec once, in
sandbox_spec_for_environment, and the overlays read the spec: Docker
takes its image from the source and clears the timers it cannot honor,
Daytona takes its snapshot inputs from the source and resources and its
auto-stop from the timers. A plugin gets the spec trimmed to the network
and timer capabilities it declares. The snapshot carries the resources a
Daytona sandbox is sized by, so the overlay clears them from the spec
the sandbox is created with; the driver refuses them there, which the
options path never reached in a live run.

The clone selectors, depth, and skip flag travel as one CloneRequest
beside the spec instead of five loose parameters and two option fields,
so provider_sandbox takes six arguments instead of nine. The two helpers
that read environment settings for a local run, its working directory
and its unresolved variables, become methods on RunEnvironmentSettings
in fabro-types, where the settings live.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 16:44:21 -06:00
Bryan Helmkamp
2d94810a71
Keep the sandbox inventory over the driver's providers directly
Fabro had its own SandboxProvider trait with a registry over it, a
LocalSandboxProvider that listed nothing, and a DriverInventoryProvider
that adapted a driver provider to the fabro trait. The trait existed to
tag a provider with fabro's kind and to aggregate across providers; both
are the inventory's job.

SandboxInventory replaces all three: a list of driver providers, each
narrowed by fabro's ownership labels and connected on first use, with the
cross-provider aggregation, native-id lookup, and conflict detection the
registry did. The local kind keeps an entry so a caller can ask whether
it is ready, and lists nothing, since its sandboxes are directories the
run record names. The delete path nothing called is gone. Tests run
against the driver's scripted provider and, for a provider that cannot
connect, a plugin kind whose executable does not exist; the fake
provider module is deleted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 16:23:22 -06:00
Bryan Helmkamp
be4dd86fd5
Drive the run terminal through the driver's PtySession
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>
2026-09-10 16:17:16 -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
14875c2fdf
Set up and push the run branch through the driver's git facet
Fabro's run-branch setup and its pushes were the last git it assembled
by hand: two rev-parse calls, a checkout -B, and a push built as shell
and run through exec. Because those failures never passed through the
driver, fabro kept a second classifier that read git's stderr for the
same auth and not-found shapes the driver already classifies for a
clone.

Both now call the facet. Setup reads the current branch and head commit
from the driver's status and creates or moves the run branch at that
base; a push sends its refspec with what remains of the retry plan's
attempt budget as the push timeout. The retry plan, the credential
lease, and the drift repair stay as they were — they are fabro's
policy — but the decision they act on comes from the driver's failure
class, the same way the clone's does. The output-shaped classifiers and
the auth hint matchers are deleted; the message classifier remains for
the host-side repository probe and metadata push, which never run
inside a sandbox. A git failure's captured output now renders as the
attempt's output tail, as an exec failure's did.

The driver pin moves to lithoscomputer/sandbox-driver#18, which adds the
push refspec and timeout, the checkout start point, and the status head
this relies on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 13:16:16 -06:00
Bryan Helmkamp
3f721dd032
Consume the sandbox driver's events directly in the workflow
Fabro-sandbox carried its own SandboxEvent enum and a callback for it.
The run sandbox wrapped every lifecycle call to emit a start, completed,
or failed variant with its own clock, and re-described the driver's
create-time progress as snapshot events through an observer that lived
next to the run sandbox. The workflow then converted that enum to the
wire. The driver already reports every operation it performs, so the
enum was a second, hand-maintained copy of that stream.

The workflow now observes the driver's events directly. A run's sandbox
is created or attached with a driver EventContext whose observer is the
new SandboxEventBridge in the workflow's event module. The bridge turns
the driver's start, stop, and delete operations, its image pull inside a
create, and its snapshot builds into the workflow's SandboxLifecycle
events, stamping fabro's provider name so the run keeps recording
`local` rather than the driver's `host`. The pipeline emits the
initializing, ready, and failed events itself around bringing the sandbox
up, since that composite step — create, activate, prepare the workspace
— is the pipeline's, not the driver's. Fabro-sandbox emits no events of
its own any more; the run sandbox gained console_url for the ready
event, and a local sandbox can be created with an event context.

The wire keeps every name the CLI reads. Two families go: the cleanup
events, which only the server's manifest validation could have produced
and it passed no callback, and the git clone events, which nothing read
and whose facts the sandbox.initialized event and tracing already carry.
The ready event drops the cpu and memory fields no provider ever
populated. Daytona snapshot events now come from the driver's ensure
call, so a snapshot that already exists and is active reports nothing
rather than a creating-and-ready pair that did no work.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 00:37:42 -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
331935904a
Adopt the driver's tag pins, git classes, stop ladder, ensure, and ownership
The sandbox-driver stack fabro pins now carries five things fabro used
to do itself, so fabro stops doing them. A tag pin is a clone option,
so the exec-based init, fetch, and checkout path for tags is gone and
every pin goes through the driver's clone; the shell command builders
and the hermetic git proofs that only served that path go with it. The
driver classifies every git failure it produces, so the retry module
keeps only the decision (a rejected credential is retried while a fresh
App token may still be replicating; an unreachable remote is retried
whatever the credential; everything else is permanent) and its hint
tables are gone. The provider runs the TERM, grace, KILL ladder for the
timeout and for the caller's cancellation, so the exec wrapper sets the
grace on the spec, passes the cancellation token as the term stop, and
reads the driver's verdict instead of racing its own timer. Daytona's
snapshot is ensured by the driver, so the list, activate, create, and
poll sequence and its back-off loop are gone. Every provider is
connected through the driver's ownership scope, narrowed to the run when
one is known, so creates carry fabro's labels and attaches to anything
else are refused by the driver; the label module keeps only the label
names, and the inventory provider reads the scope's answers instead of
checking labels itself.

The pin moves to the stack head with the driver's fix for a command
that honours the TERM inside the grace, which fabro's own tests caught
as a timeout reported as a cancellation. The Docker checkpoint
integration tests now create their container through the driver, since
the driver attaches only to containers it created; they were reaching
for a hand-run container since the Docker cutover.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 23:00:56 -06:00
Bryan Helmkamp
0c569638fe
Map an environment onto one sandbox spec for every provider
Three copies of the same mapping turned an environment into a sandbox:
Docker had its own options type built from the environment, Daytona a
config type with its own network enum and minute-and-gigabyte units,
and plugin kinds a third options type, each with its own spec builder,
constructor, and attach function, and the run spec had a variant per
provider carrying them. The mapping now runs once. SandboxOptions is
what any environment asks of any provider; options_from_environment
builds it, and one base spec carries the source, name, labels,
variables, resources, and network policy. Docker and Daytona are
overlays on that spec: Docker fixes its working directory, supplies the
default image, and pulls; Daytona replaces the source with the snapshot
it ensures from the same image or Dockerfile, fixes its working
directory and name, and sets the timers. The snapshot's content-addressed
name is unchanged, since the identity still hashes the image or
Dockerfile and the resources in whole gigabytes.

SandboxSpec has two variants, Local and Provider, and the worker and
the server preflight build the Provider one without knowing which kind
it is; provider_sandbox and attach_provider_sandbox connect any kind
through the single construction function and lay the repository out
where that kind keeps it. The per-provider constructors, the config
module, and the environment mapping module are gone, and the SDK
reference and the integration tests use the one constructor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 22:41:09 -06:00
Bryan Helmkamp
ff231e890b
Pin the driver PR stack and run its own plugin executables
The plugin scenarios launched two executables fabro built itself,
`fabro-sandbox-host` and `fabro-sandbox-docker`, that only wrapped the
driver's providers in a stdio server the driver already ships as
`sandbox-driver-host` and `sandbox-driver-docker`. Fabro now finds the
driver's executables on PATH: CI installs them at the rev the workspace
pins, read from Cargo.toml so the plugins and the in-process providers
are one build, and a developer installs them the same way. The plugin
proof in fabro-sandbox skips without the executable unless the CI
environment forbids skipping; it was also never running in CI, which
ran it under `--run-ignored only` although it is not ignored, so the
job now runs it on its own.

The pin moves to the head of the sandbox-driver PR stack #9 through
#15: tag pins, classified git failures, the stop grace ladder, snapshot
ensure, the ownership scope, and the testing doubles, which the next
commits adopt. The `sandbox-driver-testing` crate joins the workspace
dependencies for them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 22:19:26 -06:00
Bryan Helmkamp
5b035d3f75
Name plugins by their configured kind and drop out-of-process bundled kinds
sandbox-driver PR #9 removes the check that a plugin's declared kind match
the configured one: an operator who configures a path and pins its
checksum has already chosen the executable, so the configured kind is
fabro's name for whatever it serves. With that in the driver, fabro no
longer needs plugin settings on a bundled kind to reach Docker over the
wire. Bundled kinds reject plugin keys again, `connect_provider` links a
bundled kind in-process and launches everything else, and the CLI
scenarios run the Docker executable under the non-bundled `docker-plugin`
kind. The driver pin moves to the PR head until it merges.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 20:13:50 -06:00
Bryan Helmkamp
030e653abf
Run plugin providers end to end and gate them in CI
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>
2026-09-09 19:54:57 -06:00
Bryan Helmkamp
28d4242df0
Route Daytona sandboxes through the sandbox driver
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>
2026-09-09 19:14:13 -06:00