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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Fabro's TerminalSession trait, its DriverTerminalSession wrapper, and
TerminalSize were four method forwards and a size struct over the
driver's PtySession and PtySize. RunSandbox::open_terminal now returns
the driver's session, the server's websocket loop drives it directly and
renders its errors with display_for_log, and open_terminal_for_run sits
with the other reconnect helpers. terminal.rs is deleted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fabro kept its own ExecResult, streaming request and result, output
capture stats, stdio process types, and an Error::Exec variant, each a
field-for-field copy of a sandbox-driver type with a translation layer
between them. Every command a tool, a stage, or a hook ran crossed that
layer twice.
The driver's types are now the ones fabro uses. SandboxExec applies
fabro's policy to an ExecSpec (stop grace, the run's working directory,
the explicit-env filter, the Bash helper's BASH_ENV blank winning over a
caller value) and returns the driver's ExecResult and
ExecStreamingResult as they are. Callers that stream build an ExecSpec
and ExecControls; the buffered exec_command keeps its signature.
ExecResultExt adds fabro's reading of a result: the event-facing
duration, the exit code only when the command exited on its own, the
redacted output tail, and the ExecFailure a non-zero exit becomes. The
three-way termination collapse the run events use lives in one function,
command_termination, called where events are built.
Error::Exec and the git-shaped stderr hint table are gone; a failed
command is the driver's ExecFailure, whose Display carries the label and
the classified metadata and never the raw output. OutputCaptureStats
moves to fabro-agent, whose tool output accounting it belongs to, and
converts from the driver's CaptureStats at the exec boundary. The stdio
process the ACP transport drives is the driver's own, so the cancel-token
bridge and StderrCollector go too.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>