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>
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 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>
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>
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>
Fabro's policy layer restated the lithos built-ins under `metadata.fabro`:
enabled flags, credentials, display facts, probe and small-default roles,
and agent profiles. lithos-llm now carries every one of those as a core
field or under the shared `metadata.agent` namespace, so the layer and its
typed view go:
- Delete `fabro-policy.toml` and `FABRO_POLICY_TOML`. The catalog is the
lithos built-ins plus the operator's `[llm]` overlay, nothing between.
- Delete `fabro_types::catalog_policy`. `enabled`, `stands_in_for`,
`api_key_url`, `family`, the cutoffs, `estimated_output_tps`,
`small_default`, and `probe` are read from lithos accessors; the agent
profile and `reasoning_by_default` come from `metadata.agent`, which
Pebble reads too.
- `catalog::provider`, `enabled_providers`, and `listed_providers` return
the lithos `CatalogProvider` directly; `ModelEntry` loses its policy
field and gains `agent_profile()`.
- Test fixtures move `[providers.x.metadata.fabro] enabled = true` onto
the provider table, drop `credentials` lists in favor of the secret name
lithos derives from the provider id, and spell `agent_profile` as
`metadata.agent.profile`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
lithos-llm now owns which named secrets each provider reads and how they
shape into its auth scheme, including a derived `<PROVIDER>_API_KEY` for
operator-defined providers. Fabro's job shrinks to supplying the store:
`VaultCredentialSource` hands lithos a lookup that reads the process
environment, then the vault, under the same conventional names.
What Fabro still adds on top: the Codex OAuth credential in the vault,
refreshed and persisted when it expires; `{{ secrets.NAME }}` tokens in a
provider's `default_headers`, resolved against the vault and re-sent as
credential headers; and OpenAI organization and project headers from the
environment.
Deleted with the `metadata.fabro.credentials` list: `CredentialRef`,
`CredentialResolver`, `EnvCredentialSource` (now
`VaultCredentialSource::environment_only`), and the `env_var_names` /
`expected_vault_secret_name` helpers, replaced by `secret_names` and
`expected_secret_name` over the lithos table. `openai-codex` joins the
first-party provider id constants.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lithos-llm sends the application name as the codex `originator` header,
so the OpenAI Codex deployment can tell which harness a request came from.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lithos-llm now owns `enabled` and `stands_in_for` as core provider
fields, and its `CatalogResolver` refuses disabled providers and reroutes
a request to the provider standing in for an unavailable one. Fabro's
resolver re-implemented both from `metadata.fabro`, so it goes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
lithos ships `metadata.agent.profile = "gpt6"` on the GPT-6 Astra row.
Fabro runs it on the GPT-5.6 harness: the same Codex core tool set,
memory filenames, question tool, and command timeout. The shared
`uses_codex_core_tools` predicate replaces the `== Gpt56` checks so the
two kinds cannot drift apart.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
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>
Closes the sandbox-driver adoption: any provider a sandbox-driver plugin
executable serves can now host a fabro run, and fabro's own bundled
providers can be served the same way.
- `SandboxSpec::Plugin` builds a normalized driver spec from the
environment (image or Dockerfile source, or a provider-managed
directory; resources; network policy; labels; env) and lays fabro's
repository checkout out inside the provider's working directory. The
layout is recorded on the run through the new `workspace_layout` trait
method.
- Plugin settings on a bundled kind (`[server.sandbox.providers.docker]
path = ...`) serve that kind out of process through the driver's
executable; the config layer no longer rejects them.
- `ProviderAccess` carries the server's provider settings and the vault's
Daytona credentials to every reconnect: run resume, sandbox details,
terminals, previews, and the worker's start path. The worker receives
the settings through `StartServices`. No "plugin not wired" errors
remain.
- The CLI worker requires GitHub credentials only when a repository will
be cloned; a `none` target on a clone-based provider creates an empty
workspace and needs none.
- fabro-db tracks its migrations directory so a new migration file
recompiles the crate; the environment provider migration had been
silently missing from stale builds. Environment store 500s now log
their cause.
- The CLI workflow scenarios run against `host-plugin` (the driver's
Host executable under the non-bundled `host` kind) and `docker-plugin`
(the bundled `docker` kind served over stdio), each on an isolated
server, printing the server log on failure. A live Daytona gate runs
the native git clone over the JSON-RPC wire. A new CI job runs the
plugin scenarios and the driver-backed Docker integration tests with
the plugin executables built.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The `daytona` provider kind now maps onto the sandbox-driver Daytona
provider instead of fabro's own SDK client. Fabro keeps what is its own:
the HMAC-named snapshot built from the environment's image or Dockerfile,
the explicit 120 minute auto-stop default, the managed labels that gate
destructive operations, the clone decision and layout, and push
credentials. The driver creates the sandbox, clones natively, and serves
exec, files, search, terminal, SSH, preview, and VNC through its facets.
- `daytona.rs` builds the driver `SandboxSpec` (snapshot source,
`/home/daytona/workspace`, labels, timers, network policy, run name),
ensures the snapshot through the driver `SnapshotProvider`, attaches by
persisted id with fabro's label guard, and probes credentials through
the provider health check under fabro's 20 second budget.
- `DriverSandbox` gains a create plan that settles the spec right before
the provider call, records the snapshot a sandbox came from, and
reports the provider console URL on `Ready`.
- Terminals use the driver `Pty` facet; the server's SSH, preview, and
VNC endpoints use the `SshAccess`, `PreviewUrls`, and `Vnc` facets
through the driver-typed reconnect. The preview endpoint now answers
for every provider with a preview facet, so the local sandbox returns
its loopback URL.
- Daytona credentials travel as `DaytonaCredentials` built from the
vault key plus configured URL and organization; nothing reads the
process environment implicitly. The inventory registry uses the shared
`DriverInventoryProvider`.
- The SDK-based `daytona/mod.rs`, `provider/daytona.rs`, the Daytona
terminal, the `daytona` cargo feature, and the direct daytona-sdk,
git2 (in fabro-sandbox), tungstenite, and rustls dependencies are
gone. The live Daytona tests run against the driver-backed sandbox.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Workflow LLM handlers build lithos requests, bill from lithos usage and
cost, and classify failures from lithos `ErrorKind`. Model resolution and
fallback use the fabro-llm selection and catalog helpers. Validation
rules read the lithos catalog, and store fixtures use the new
`BilledModelUsage` shape.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fabro-llm is now a thin integration crate: catalog construction from the
lithos built-ins, the Fabro policy layer, and operator overlays; client
construction from catalog plus credentials; a Fabro `ModelResolver` that
enforces `metadata.fabro` policy; model selection; a server gateway
adapter; attachment inlining middleware; reasoning normalization;
one-shot structured output; probe wiring; and catalog API views. The
in-house codecs, transports, providers, tool loop, retry, cost, and
token-count code are deleted along with the wire snapshots that covered
them.
fabro-agent consumes lithos `StreamEvent`s and `Response`s directly.
Retry is split: lithos's retry middleware handles failures before any
visible output, and the agent replays the turn after.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add lithos-llm as a pinned git dependency and replace the in-repo
`test/twin/openai` crate with the published `twins` revision that
lithos-llm verifies its codecs against. Move the Fabro policy overlay
(`fabro-policy.toml`) into fabro-llm so Fabro owns its own catalog
policy layer.
Drop the twin-openai nextest overrides and CI package filter now that the
crate is no longer a workspace member.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fabro's Docker provider kind now maps onto the sandbox-driver Docker
provider instead of its own bollard implementation. Fabro keeps what is
its own: the clone decision and repository layout, the GitHub App push
credentials embedded in origin, git retry classification, the managed
labels that gate destructive operations, and the run-facing events.
- `clone.rs` performs fabro's clone over the driver `Git` and `Exec`
facets. An exact commit goes through the driver's pinned clone; a tag
pin runs fabro's init/fetch/attach sequence through `Exec` so the
fully qualified tag ref is the only revision consulted. Network
failures retry through `git_retry`, which now classifies driver errors
(exec output, provider retryability) and never replays an operation
whose outcome is unknown.
- `DriverSandbox` gains a pending-create state, a `RepoWorkspace` with
push-credential state, path resolution against the cloned working
directory, git lifecycle methods, image-pull progress mapped from
driver events, and an embedded terminal over the driver `Pty` facet.
- `docker.rs` builds the driver `SandboxSpec` (image, `/workspace`,
fabro labels, env, cpu/memory, network policy, run name) and attaches
by persisted container id, refusing containers without fabro's labels.
- Inventory, details, diagnostics, terminal, and reconnect run over the
driver: a `DriverInventoryProvider` lists by fabro's managed label and
projects `SandboxStatus` into fabro's inventory and details shapes.
- The bollard-based `docker.rs`, `provider/docker.rs`, Docker terminal,
Docker error variants, the `docker` cargo feature, and the bollard and
tar dependencies are gone. The Docker integration tests, the agent
shell test, the workflow artifact test, and the driver benchmark run
against the driver-backed sandbox.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SandboxSpec::Local and run reconnect now build a DriverSandbox over the
sandbox-driver Host provider instead of fabro's own LocalSandbox, which
is deleted. fabro_sandbox::local_sandbox designates the working
directory (created when missing, never removed), creates the Host handle
in a per-process registry, and learns the platform up front. A local
sandbox reports no provider id: it is its directory, which the run
record already carries, so reconnect rebuilds the handle over that
directory rather than by id.
The credential filter for explicit environment variables and the Bash
readiness probe now come from the exec layer and the driver's activate
helper. Test call sites move to the async constructor; test factories
that must stay synchronous share the parent session's sandbox handle.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
DriverSandbox implements fabro's Sandbox trait over a driver handle.
Files go through the Filesystem facet, content and tree search through
the Search facet (grep rendered as path:line:content, walks reported
relative to the caller's base with sizes filled from metadata when the
transport has none), commands through fabro's exec policy, preview URLs
through the access facet, and lifecycle through the handle with fabro's
run events emitted around each step. Initialize and start use the
driver's activate helper, which brings the sandbox to Running and
verifies non-login Bash on both transports, and learn the platform name
from the sandbox itself. Local kinds keep the credential filter on
explicit environment variables; isolated kinds take the caller's
environment as composed. Tests run every operation against the
in-process Host provider.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fabro_sandbox::exec::SandboxExec runs Bash source through a driver Exec
facet with fabro's own stop policy. Fabro disables the driver's hard
timeout and runs its own timer; a timeout or a caller's cancellation
fires the driver's TERM token, waits the grace period, then fires KILL.
The result reports why fabro stopped the command (timed out or
cancelled) rather than which signal the provider observed, and a stopped
command carries no exit code so a shell's 143 is never read as a program
result. Output streams through the caller's callback, is drained past
the retention cap, and explicit environment variables pass a fail-closed
credential filter on the host and through unchanged on isolated
providers. spawn_stdio wraps the driver's bidirectional process and its
rolling stderr tail in fabro's existing stdio types.
Driver errors now map into fabro's error type as a boxed variant with
accessors for the not-found, unsupported, transport, and incomplete
cases, and the redacted output tail helper reads a driver ExecFailure
as well as fabro's own exec error. The Local sandbox's credential name
filter moves to the exec module so both paths share one policy.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fabro_sandbox::driver::connect_provider turns one
[server.sandbox.providers.<kind>] entry into an Arc<dyn SandboxProvider>
from the sandbox-driver crates. Bundled kinds link the driver's Host,
Docker, and Daytona providers in-process; Daytona receives its API key
and endpoint explicitly through connect_explicit with a
fabro-sandbox/<version> user agent, never from the process environment.
Any other kind launches the configured plugin executable through a
PluginSupervisor and returns a wrapper that replaces a crashed plugin
for new work only, never replaying a failed call. Disabled entries are
refused at the construction point.
The Host and Docker providers also ship as fabro-sandbox-host and
fabro-sandbox-docker plugin executables so CI can drive the bundled
providers over stdio and a deployment can move one out of process by
configuration alone. An integration test registers the Host executable
under the non-bundled kind `host`, creates a sandbox and runs a command
over the wire, then attaches to it by persisted id from a fresh plugin
process, and checks that a plugin declaring a different kind is refused.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SandboxProviderKind is now a validated string newtype instead of a
closed enum. The bundled kinds (local, docker, daytona) keep their
constants and a BundledProvider enum for the code paths that still
dispatch on them; any other well-formed sandbox-driver kind name is
accepted and names a plugin executable. EnvironmentProvider is gone:
environment settings carry SandboxProviderKind directly, and
is_clone_based is replaced by a workspace policy where local runs in a
designated directory and every other provider clones.
Server sandbox policy is keyed by kind. [server.sandbox.providers.<kind>]
accepts the bundled kinds with `enabled` and any plugin kind with its
launch settings (path, sha256, dev, args, env, inherit_env); bundled
kinds reject the plugin keys and a kind with no entry is disabled. The
OpenAPI schema, generated Rust and TypeScript clients, web settings
pages, and docs follow. The environments table drops its provider CHECK
enumeration in favour of the kind name rules so a plugin environment
can be stored.
Bundled-only code paths (run start, preflight, reconnect, terminal,
details) now fail with an explicit message for a plugin kind until the
driver construction function lands in the next step.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Phase 2 of the sandbox-driver adoption: an ignored test that unpacks
fabro's own lib tree into each provider and times file reads and
content searches through fabro's current providers, the driver
providers in-process, and the driver providers served over JSON-RPC on
an in-process pipe. Docker reads match, Docker grep is faster through
the driver, Host grep costs about 20 ms more through the derived
search, and the wire hop adds about 0.1 ms per call against the plan's
100 ms per tool call budget. The plan records the full table.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fabro pins the sandbox-driver workspace the same way it pins the
Daytona SDK: every crate at one commit on main. The bundled Host,
Docker, and Daytona provider libraries link in-process, and the
protocol crate reaches third-party providers over stdio. Nothing uses
the crates yet; the following commits move fabro onto them one layer
at a time.
The driver pins tracing-subscriber exactly, so the lockfile settles on
that version for the whole workspace.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Both fabro and sandbox-driver now pin the same daytona-sdk-rust commit
on main. The newer SDK adds region and sandbox class fields to snapshot
creation; fabro leaves both unset and keeps its current behavior.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Move the "which failures can happen before Starting" classification onto
FailureReason as an exhaustive predicate and use it for every
Runnable -> Failed transition, replacing the hand-maintained allowlist.
Give the pending-cancel precedence rule a single owner shared by the
worker launch and worker exit paths.
Test cleanups: share the Notify wait loop, server record fixture, and
post-failure assertions; simplify the pre-start test runtime's hold
flag; and parameterize the slate run.failed payload helper.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Move ProjectedRun next to EventProjectionCache in run_state, since the
summary store both produces and consumes it, and give it a replay
constructor that owns the events-to-head derivation. load_projection now
returns RunNotFound directly instead of erasing it to None and having
callers rebuild it; load_run_projection is the single Option translation
point. install_in_memory_state reuses the existing From impl, and the
commit path passes its Arc through instead of unwrapping and
reallocating it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The supervisor's periodic recovery scan asked SQLite for every run that
had ever recorded an explicit pull request creation request and then
replayed each inactive candidate's full history to learn whether the
request was still pending. With projections now loaded on demand that
set grows without bound and was replayed every scan.
The candidate query now mirrors the projection reducer: a run is a
candidate only when its latest creation request has no later request,
created, linked, or unlinked event, and no later failure naming the same
creation id. Callers still replay each candidate to confirm, so the query
only has to avoid omitting a pending run, and the replayed set is bounded
by in-flight requests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Drop the unreachable active-run mismatch guard that was copied into
load_run_projection: the active-runs map is only ever inserted under the
handle's own run ID, so the check could never fire. Remove it from the two
pre-existing sites too and delete matches_run.
Trim install_in_memory_state to take only the committed projection, since
the event envelope duplicated last_seq and the inner scope only existed to
release the lock before the now-removed shared cache update. Add a From
impl so RunDatabase::build no longer hand-builds EventProjectionCache, and
rename projected_state_locked to match its projection_snapshot sibling.
In fabro-server, have reject_if_archived and ensure_run_exists read the
run summary row instead of replaying the full event history for inactive
runs; the summary is written in the same transaction as the event.
Fold the repeated store-reopen fixtures in fabro-store and fabro-server
tests into helpers, and fix a stale comment about the deleted shared
projection cache.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Load recovery candidates from the warm projection cache instead of
replaying each run's full event history, and check dispatch
eligibility before any I/O
- Extract the shared can_dispatch predicate used by both the recovery
scan and the worker dispatch loop
- Drop load_durable_run_status, now identical to durable_run_status
- Share parse_stored_run_id across the three stored-id decode sites
- Reuse push_order for the canonical run ordering in list_all and
list_by_statuses
- Replace the test-only queue clear accessor with the existing drain
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>