Commit graph

4998 commits

Author SHA1 Message Date
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
705f411c56
Expect the driver's stop event in the stored event history snapshot
A dry run's stored history ends with the sandbox stop, which now carries
the driver's event instead of fabro's provider and duration fields.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 13:58:09 -06:00
Bryan Helmkamp
895735a829
Redact the driver's event ids in CLI snapshots and leave a local sandbox unnamed
The attach snapshots now carry the driver's create events, whose event
source and operation ids are minted per process and whose durations run
to the nanosecond, and the local sandbox's id is derived from a temporary
directory; the shared snapshot filters cover all three. A local sandbox's
ready event no longer names that id: the record already holds the
directory, and the id is nothing a person reads.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 13:55:45 -06:00
Bryan Helmkamp
f568688154
Move the sandbox-driver pin to the supervisor kind fix
The plugin supervisor now answers to the configured kind, which fabro's
plugin test expects of the provider it holds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 13:52:52 -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
d986fc9751
Keep the sandbox driver's events whole as run events
A bridge translated the driver's events into thirteen lifecycle variants
of fabro's own (start, stop, and delete phases, image pulls, snapshot
builds) and dropped everything else the driver reported, pairing an image
pull's first progress report with the create's completion to invent a
duration. The driver's event is now stored as the run event itself, under
a name derived from it: subject, action, and phase (sandbox.stop.completed,
sandbox.create.progress for an image pull, snapshot.create.started), or
<subject>.state and <subject>.notice. Every operation the driver performs
on the run's sandbox lands on the run, including creates and state
observations the bridge skipped. The CLI reads image pulls and snapshot
builds from the driver's event for its setup progress and pretty output,
the thirteen variants and their props go, and a run stored under the old
names still reads as an unknown body. Checkpoint file numbers in a dump
shift because the run records more events before each checkpoint.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 13:32:54 -06:00
Bryan Helmkamp
d27897c39f
Move the sandbox-driver pin to the follow-up commit
The driver branch gained equality on statuses and events, a git failure
that says when a command timed out, and lost the misleading
GitAttempt::succeeded; nothing in fabro used the method.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 13:24:37 -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
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
220faa3a58
Describe the driver-owned pin in the project notes
The clone notes said both providers verify HEAD after a pinned clone.
The driver now performs and checks the pin, and fabro no longer runs a
second `rev-parse`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 09:33:11 -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
33cdbf6afc
Finish the merge with the fixes the merge commit left out
The merge commit was made from the staged hunk resolutions and did not
include the changes that followed them: the BTreeMap import the kept
Combine impl needs, main's four new session tests ported to the mock
helper, a duplicated truncation import removed, the boxed event future
the CLI runner needs to stay under clippy's size budget, the formatting
of a merged import list, and the lock refreshed after the merge. Without
these the merge commit does not compile. This is the tree the merge was
verified on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 13:41:35 -06:00
Bryan Helmkamp
2e8d6b8a3d
Merge origin/main into the sandbox-driver adoption
Both sides rewrote the same crates. This branch replaced fabro's sandbox
layer with the sandbox driver: one RunSandbox, no Sandbox trait, driver
events consumed directly, MockSandbox over the driver's doubles. Main
replaced fabro's LLM layer with lithos-llm: fabro-model deleted, the
catalog and provider ids from lithos, credentials through the lithos
CredentialProvider, clients built with build_client.

Every conflict was one of those two renames meeting in an import list or
a signature, so the rule was mechanical: sandbox names resolve to this
branch, LLM names to main. Where main's newer code still used the old
sandbox API — new session tests over Arc::new(MockSandbox), the SDK
example's LocalSandbox, test fakes typed as Arc<dyn Sandbox> — it is
ported to RunSandbox and the mock helper. Where this branch still used
fabro-model or Client::from_source, main's replacement stands. One
combined future in the CLI runner crossed clippy's size budget and is
boxed at its call.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 13:37:11 -06:00
Bryan Helmkamp
3e2f00b423
Pin the sandbox driver to main with the git facet options merged
Pull request #18 on the driver is merged as a merge commit, so the
commit fabro was pinned to is an ancestor of main. Follow main.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 13:24:05 -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
c3ba5ce666
Merge pull request #848 from fabro-sh/rust-boundary
Some checks failed
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
Rust / Format (push) Waiting to run
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
Adopt the lithos-llm Rust boundary API
2026-09-10 10:22:31 -06:00
Bryan Helmkamp
a9086f8442
Load the hook e2e fixtures from a workflow config
The hook tests wrote their `[[run.hooks]]` entries into the user's
settings file. `fabro run` no longer transmits `run` settings from
there — it warns and points at `workflow.toml` — so no hook ran in any of
these tests. The three that expect the run to proceed kept passing for
the wrong reason.

Each hooked test now writes a workflow config that names its graph and
carries the hooks, and runs that config. The twin-mode server settings
stay in the settings file, which is where they belong.

The hooks reach the run now, but the twin-mode tests still cannot pass
on this branch: the run executes in the isolated server, which never
learns the twin's base URL and so calls the real OpenAI API with the
namespace as a key. That plumbing belongs with the lithos credential
resolution on main, not here.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 10:22:05 -06:00
Bryan Helmkamp
b4191d4431
Give the managed-labels live smoke a fresh run id each time
The smoke named its sandbox from a fixed run id. Daytona names sandboxes
uniquely, so when a run of the test was interrupted after the create had
gone out — a killed process, a test budget that expired mid-create — the
leftover sandbox made every later run fail with "already exists" until
someone deleted it by hand.

The test now generates a run id per execution and checks the label the
provider returns against that id, so an interrupted run leaves at most a
stray sandbox to prune and never blocks the next one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 10:22:04 -06:00
Bryan Helmkamp
619cb44e3c
Import lithos-llm types directly instead of through fabro-types
fabro-types no longer re-exports the lithos catalog and request types
(ProviderId, ModelId, ModelHandle, Message, ContentPart, TokenCounts,
Cost, Speed, ReasoningEffort, ReasoningOutput, and the rest). Every
crate that uses them depends on lithos-llm and names them there, and
the fabro-api progenitor replacements point at the lithos paths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 10:03:08 -06:00
Bryan Helmkamp
fa3e485c95
Name built-in providers through lithos catalog::builtin
lithos-llm now ships the built-in provider ids and constructors, so
fabro-types drops its provider_ids module and every caller uses
lithos_llm::catalog::builtin directly. The crates that name a provider
now depend on lithos-llm themselves.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 09:48:37 -06:00
Bryan Helmkamp
f8df58966b
Use the lithos closest_supported_effort on ModelCapabilities
The reasoning-effort substitution rule now lives on lithos's
ModelCapabilities, so the workflow fallback planner calls it directly
and fabro-types drops its controls module. ReasoningEffort is re-exported
from lithos alongside the other request types.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 09:44:11 -06:00
Bryan Helmkamp
87e1e3a00f
Replace fabro-llm helper modules with lithos-llm equivalents
Delete fabro-llm's attachments, reasoning, and structured modules and
the LlmError newtype and ErrorFacts trait. lithos-llm now provides all
of them: InlineLocalFiles under the local-files feature, ReasoningOutput
with Response::reasoning(), Client::complete_object, and the retry,
auth, cancel, and failover predicates directly on Error and ErrorData.
fabro-llm keeps only failure_signature_hint, which is Fabro's own loop
detection policy.

Store ErrorData directly in the agent and workflow error enums, boxed
where the variant would otherwise dominate the enum size. Repin
lithos-llm to a1e3fd3 for these additions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 09:41:20 -06:00
Bryan Helmkamp
3a998196ff
Answer catalog questions with the lithos catalog queries
fabro-llm's catalog module held some 250 lines of listing and picking
helpers over lithos data: enabled and listed providers, model lookup by
id, alias, or wire id, matches ranked as the resolver ranks, default and
probe models, the small utility model across ready providers, the nearest
model on another provider, and cost by handle. lithos-llm now answers all
of those on `Catalog` and `CatalogProvider` through `Offering`, so the
helpers and the `ModelEntry` wrapper go.

What stays in Fabro's catalog module is its own: building the catalog from
the operator overlay, and reading the agent harness and
`reasoning_by_default` from the shared `metadata.agent` namespace. The
passthrough selection policy in `selection.rs` keeps its rules and calls
lithos for the lookups.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 09:26:40 -06:00
Bryan Helmkamp
3510d5081d
Implement the lithos CredentialProvider trait directly
fabro-auth defined its own `CredentialSource` trait beside the lithos
`CredentialProvider`, with a parallel `ResolveError` and an adapter between
them, because lithos had no way to ask which providers a store can serve
right now. It does now: `credentials::readiness`, `ClientBuilder::build_ready`,
and `CredentialError::Unusable`.

- The vault, SQL vault, API-key, and extra-headers stores implement
  `CredentialProvider` directly. Material that is present but unusable (an
  expired token with no refresh, a wrong-typed vault entry, a header secret
  that did not resolve, a store read failure) is `CredentialError::Unusable`
  with the operator-facing reason; its `Display` replaces
  `auth_issue_message`. `is_configured` is the cheap presence check.
- `fabro_llm::build_client` calls `build_ready`; `FabroClient::auth_issues`
  carries `CredentialError`. `fabro_llm::configured_providers` replaces the
  per-store `configured_providers` method.
- `CredentialSource`, `ResolvedCredentials`, `lithos_credentials`,
  `ResolveError`, and `auth_issue_message` are deleted. Twenty files that
  held `Arc<dyn CredentialSource>` hold `Arc<dyn CredentialProvider>`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 09:16:39 -06:00
Bryan Helmkamp
6eb9ee8cd9
Pin lithos-llm at the Rust boundary revision
lithos-llm ba2f418 adds provider readiness, catalog queries, error policy
predicates, one-call structured output, local-file inlining, the reasoning
normalizer, the nearest supported effort, and built-in provider ids. Every
addition is additive, so this pin changes nothing yet; the commits that
follow adopt each one and delete Fabro's copy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 09:09:21 -06:00
Bryan Helmkamp
c9bfd5edaa
Pin the sandbox driver to the merged main
The second-wave driver work — the tag pin, classified git failures, the
stop ladder, snapshot ensure, owner scoping, and the testing crate — is
merged as pull requests #10 through #15, so fabro follows the merge
commit on main instead of the head of the open stack. The content is
the same; the commits were rebased, so every hash changed.

CI installs the driver's plugin executables at the rev it reads from
Cargo.toml, so this also moves the executables the plugin scenarios run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 08:33:43 -06:00
Bryan Helmkamp
1bb387a0e5
Merge pull request #847 from fabro-sh/lithos-llm-migration
Replace fabro-llm internals and fabro-model with lithos-llm
2026-09-10 08:09:19 -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
7ef09968f2
Document the catalog overlay without the metadata.fabro namespace
The `[llm]` reference now describes `enabled`, `api_key_url`,
`stands_in_for`, `small_default`, `probe`, `family`, and the cutoffs as
lithos fields, the coding harness under `metadata.agent`, and the secret
names lithos derives for operator-defined providers. Secret-bearing
headers go in `default_headers` as `{{ secrets.NAME }}` tokens. The
integration guides enable a provider with `enabled = true` on its table.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 23:26:48 -06:00
Bryan Helmkamp
b82b3dd48e
Use the lithos control enum API instead of local spellings
lithos-llm now exposes `ReasoningEffort::ALL`, `Speed::ALL`, `as_str`,
`Display`, and `FromStr` on its request-control enums. Fabro's
`controls` module kept parallel name tables and parsers for them; only
the nearest-supported-effort rule is Fabro's own, so that is what stays.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 23:23:49 -06:00