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>
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 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 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>
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>
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>
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>
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-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>
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>
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>
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 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>
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>
Three test panics printed the resolved `Credentials` value with `{:?}`.
The lithos credential types redact secrets in their Debug output, but
CodeQL's cleartext-logging rule cannot see that and flagged each site.
The variant name is enough to diagnose a failing test, so drop the value.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fabro-model's ids and billing rollup now live in fabro-types, and its
pricing, catalog, provider TOMLs, and legacy index are replaced by the
lithos built-in catalog plus the Fabro policy layer.
Regenerate the configuration reference for the `[llm]` overlay and
`metadata.fabro`, and rewrite the SDK, models, and integration docs for
the lithos provider and model shapes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The OpenAPI spec adopts the lithos request, response, content part,
tool, usage, and cost schemas. The completions endpoint returns the
lithos `Response` JSON verbatim and SSE carries lithos `StreamEvent`s
verbatim. The models and providers endpoints serve the fabro-types
catalog views, and the install and model-test flows probe providers
through fabro-llm.
The CLI builds its catalog from the operator overlay, drives `fabro exec`
through the server gateway adapter, and parses reasoning effort with the
shared controls. The web app reads content parts as lithos-tagged
objects. The TypeScript client is regenerated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fabro-types re-exports the lithos request, response, content, tool, and
stream types and absorbs the identifiers, billing rollup, provider ids,
controls, catalog API views, and Fabro catalog policy (`metadata.fabro`)
that lived in fabro-model. Stored and wire formats use the lithos serde
shapes directly with no compatibility shims.
fabro-auth becomes a lithos `CredentialProvider`: `CredentialSource`
resolves credentials per catalog provider, with env, vault, SQL vault,
extra-headers, and API-key sources.
fabro-config's `[llm]` settings become an opaque TOML overlay layer
(`LlmLayer`) that is applied on top of the lithos built-ins and the Fabro
policy layer.
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>
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>
When `fabro run` or `fabro create` omits `--environment` and the server
has no environment named `default`, the CLI previously failed with only
"could not retrieve environment `default`". It now lists the server's
environment catalog in the error so the user can pass an explicit
`--environment <id>` or create the missing `default` entry. Explicit
`--environment` lookups keep their existing not-found message.
Adds `Client::list_environments` to fabro-client for the catalog read.
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>
Quality pass over the intent-producer changes, no behavior changes
intended:
- Move the TOML->JSON scalar conversion into fabro-types as
toml_scalar_to_json_value, next to its inverse, with typed errors and
round-trip tests; the CLI now calls the shared helper.
- Reuse goal_layer_from_args for --goal/--goal-file resolution instead
of a second copy of the exclusivity check and cwd anchoring.
- Delete the dead run_manifest_args helper and the test that kept it
compiling; preflight_manifest_args is the remaining real builder.
- Make run_target_for_environment a pure (provider, cwd) -> target
mapping using is_clone_based(), warning at the call site, and default
the environment id from DEFAULT_ENVIRONMENT_ID instead of a literal.
- Resolve the parent run and retrieve the environment concurrently.
- Drop the ResolvedCommandSettings pass-through struct and the
duplicated parse-error mapping in the project settings presence read.
- Share the environment/workflow-version/git test mocks from the cmd
test support module instead of three per-file copies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Drop the post-decode run_id/session_id/event-body checks in
find_session_owner: decode_event_row already verifies every stored
column against the decoded envelope, and the WHERE clause pins
session_id and event_name to the requested values.
- Build the lookup query from SELECT_EVENT_COLUMNS like the sibling
event queries instead of duplicating the column list.
- Carry the stored run_id text in the unparseable-id error instead of
an "<invalid>" placeholder.
- Fetch applied migration versions once per migrate() and share the
set between the session-owner preflight and the pre-migration
snapshot; check the applied version first so steady-state startups
skip the sqlite_master probe. Mark the preflight as removable with
the run-history compatibility window.
- Restore session_by_id_key as a #[cfg(test)] helper so tests stop
hand-rolling the legacy reverse-index key shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the content-derived id check into create_workflow_version so every
caller gets it, drop the redundant expected_id parameter from
register_workflow_versions, and collapse the duplicated httpmock setups
and ordering machinery in the client tests. Use in-scope imports and the
neighbouring reader idiom in the server intent tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>