Commit graph

159 commits

Author SHA1 Message Date
Scott Werner
d611ef2bb1 Port workflow version creation to Pebble native tools 2026-09-12 10:23:34 -06:00
Scott Werner
ec81170c8a Share workflow packaging results and validation across callers 2026-09-12 10:15:42 -06:00
Scott Werner
e6eb59537a Move the supplied-content packager next to its collector
ServerWorkflowVersionPackager was a pure adapter over
fabro_manifest::collect_supplied_workflow_versions that touched no
server state, yet it lived in fabro-server and was imported from there
by the standalone MCP server and the CLI run worker. fabro-manifest can
depend on fabro-tool without a cycle, so the adapter now lives beside
the collector as SuppliedWorkflowVersionPackager and fabro-server no
longer exports a non-server module for it.

The adapter also cloned every version's file map out of a closure it
already owned. CollectedWorkflowClosure::into_versions hands the
versions over by value inside the blocking task instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 10:15:29 -06:00
Scott Werner
bafdd880f5 Simplify workflow version registration tool layering
Move supplied-content packaging into fabro-manifest beside the checkout
collector, and narrow the injected seam to a packager that returns the
dependency-ordered closure so ClientBackend registers versions with the
client it already owns.

Validate the tool input once through a ValidatedWorkflowVersionCreate
newtype, matching the other tools, instead of re-validating at three
layers. Reuse the fabro-types unique-map deserializer and the shared
"not available" error helper, derive budget messages from the limit
constants, and render the tool result through the shared summary+JSON
path used by sibling tools.

Share one extension dispatch between WorkflowLocation::resolve and
from_exact_path, compute the bundler's normalized reference once, key
path-collision checks by a Cow so the canonical exact check no longer
allocates, and log the full packaging error chain before returning the
curated tool message. Replace the hand-rolled axum test server with
httpmock and declare the new unicode dependencies at the workspace.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 10:15:29 -06:00
Scott Werner
d5dec0fffb Add content-based workflow version registration tools 2026-09-12 10:15:19 -06:00
Bryan Helmkamp
957fc97c5c
Merge origin/main into pebble-agent-loop
Main merged the sandbox-driver adoption (#849) in a later form than this
branch was stacked on: the driver's own exec types replace fabro-sandbox's,
shell quoting moved to fabro-util, the sandbox lifecycle collapsed, and the
driver's events are stored as run events. This branch had deleted
`fabro-agent` and put the coding agent, the environment adapter, and the
steering hub on pebble.

The resolution takes main's sandbox API and re-applies pebble on top: the
`RunSandbox` `Environment` adapter moves to `pebble_environment.rs` (main's
`environment.rs` is the sandbox spec) and runs commands through `ExecSpec`
and `ExecControls`, feeding pebble's output sink from the driver's; the
driver-era `sandbox.*` names leave the known-event list, as on main, so a
stored event with that name and no driver shape is `Unknown` rather than an
error; `program_exit_code` matches pebble's non-exhaustive termination; the
Docker and Daytona smokes use main's constructor and credentials; the
remaining `fabro_agent` paths point at fabro-sandbox.

Pebble's `mcp` feature pins sandbox-driver, and the preview-url trait
objects only cross when both sides name one revision, so pebble moved to
main's `a92c0db6` (lithoscomputer/pebble#10) and fabro pins that pebble
revision until it lands on pebble main.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 09:40:00 -06:00
Bryan Helmkamp
66360c2896
Run fabro exec through pebble's command-line session
Pebble's command line is a library now. `fabro exec` builds its agent as
before, with fabro's client, sandbox, MCP servers, skills, search, and
redaction, and hands it to pebble's session: the events rendered as they
happen, the answer on standard output, the summary after it, the agent shut
down for the reason the prompt ended with, and the terminal approval prompt
for tools the permission level does not allow. Fabro's own progress printer,
approval prompt, summary, and MCP report are gone. The event stream of
`--output-format json` stays on standard output. Standard output now carries
the final answer alone rather than every assistant message; `--verbose` no
longer prints tool results, since the session's renderer shows tool failures
only. The lockfile moves tempfile to the version pebble pins, and the SQLite
backup migration uses the replacement for the constructor that version
deprecates.

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

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 19:51:12 -06:00
Bryan Helmkamp
d4e483925c
Let pebble discover memory and skills, and name the reuse rules
Steps 5 and 8 of .ai/plans/pebble-absorbs-embedder-concerns.md, pinning
pebble 49da137.

Agent stages and `fabro exec` ask pebble for the profile's instruction
files from the repository root down to the working directory
(`MemoryDiscovery::from_git_root`), which fabro lacked: it read the
working directory alone. Skill directories are pebble's to resolve too:
the user's skills directory, then `.fabro/skills` and `skills` under the
repository root. Prompt stages keep reading the working directory alone,
through the same discovery and loader, so `agent_memory.rs` keeps only
that call; the filename table is pebble's now.

A retained thread's export comes from `export_for_reuse`, which closes
the session and hands back an export whose cursor is already past the
close, in place of export, shutdown, and a cursor advance by hand. Ask
Fabro resumes a stored record with `resume_after`, the rule it applied
under the older name.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 14:19:15 -06:00
Bryan Helmkamp
705f411c56
Expect the driver's stop event in the stored event history snapshot
A dry run's stored history ends with the sandbox stop, which now carries
the driver's event instead of fabro's provider and duration fields.

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

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

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

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

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 13:37:11 -06:00
Bryan Helmkamp
a9086f8442
Load the hook e2e fixtures from a workflow config
The hook tests wrote their `[[run.hooks]]` entries into the user's
settings file. `fabro run` no longer transmits `run` settings from
there — it warns and points at `workflow.toml` — so no hook ran in any of
these tests. The three that expect the run to proceed kept passing for
the wrong reason.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 23:23:49 -06:00
Bryan Helmkamp
2a2fc41807
Read catalog policy from lithos core fields and metadata.agent
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>
2026-09-09 23:20:12 -06:00
Bryan Helmkamp
6dfc96d3fd
Resolve provider secrets through lithos conventional credentials
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>
2026-09-09 23:00:08 -06:00
Bryan Helmkamp
ff231e890b
Pin the driver PR stack and run its own plugin executables
The plugin scenarios launched two executables fabro built itself,
`fabro-sandbox-host` and `fabro-sandbox-docker`, that only wrapped the
driver's providers in a stdio server the driver already ships as
`sandbox-driver-host` and `sandbox-driver-docker`. Fabro now finds the
driver's executables on PATH: CI installs them at the rev the workspace
pins, read from Cargo.toml so the plugins and the in-process providers
are one build, and a developer installs them the same way. The plugin
proof in fabro-sandbox skips without the executable unless the CI
environment forbids skipping; it was also never running in CI, which
ran it under `--run-ignored only` although it is not ignored, so the
job now runs it on its own.

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

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 20:13:50 -06:00
Bryan Helmkamp
030e653abf
Run plugin providers end to end and gate them in CI
Closes the sandbox-driver adoption: any provider a sandbox-driver plugin
executable serves can now host a fabro run, and fabro's own bundled
providers can be served the same way.

- `SandboxSpec::Plugin` builds a normalized driver spec from the
  environment (image or Dockerfile source, or a provider-managed
  directory; resources; network policy; labels; env) and lays fabro's
  repository checkout out inside the provider's working directory. The
  layout is recorded on the run through the new `workspace_layout` trait
  method.
- Plugin settings on a bundled kind (`[server.sandbox.providers.docker]
  path = ...`) serve that kind out of process through the driver's
  executable; the config layer no longer rejects them.
- `ProviderAccess` carries the server's provider settings and the vault's
  Daytona credentials to every reconnect: run resume, sandbox details,
  terminals, previews, and the worker's start path. The worker receives
  the settings through `StartServices`. No "plugin not wired" errors
  remain.
- The CLI worker requires GitHub credentials only when a repository will
  be cloned; a `none` target on a clone-based provider creates an empty
  workspace and needs none.
- fabro-db tracks its migrations directory so a new migration file
  recompiles the crate; the environment provider migration had been
  silently missing from stale builds. Environment store 500s now log
  their cause.
- The CLI workflow scenarios run against `host-plugin` (the driver's
  Host executable under the non-bundled `host` kind) and `docker-plugin`
  (the bundled `docker` kind served over stdio), each on an isolated
  server, printing the server log on failure. A live Daytona gate runs
  the native git clone over the JSON-RPC wire. A new CI job runs the
  plugin scenarios and the driver-backed Docker integration tests with
  the plugin executables built.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 19:54:57 -06:00
Bryan Helmkamp
28d4242df0
Route Daytona sandboxes through the sandbox driver
The `daytona` provider kind now maps onto the sandbox-driver Daytona
provider instead of fabro's own SDK client. Fabro keeps what is its own:
the HMAC-named snapshot built from the environment's image or Dockerfile,
the explicit 120 minute auto-stop default, the managed labels that gate
destructive operations, the clone decision and layout, and push
credentials. The driver creates the sandbox, clones natively, and serves
exec, files, search, terminal, SSH, preview, and VNC through its facets.

- `daytona.rs` builds the driver `SandboxSpec` (snapshot source,
  `/home/daytona/workspace`, labels, timers, network policy, run name),
  ensures the snapshot through the driver `SnapshotProvider`, attaches by
  persisted id with fabro's label guard, and probes credentials through
  the provider health check under fabro's 20 second budget.
- `DriverSandbox` gains a create plan that settles the spec right before
  the provider call, records the snapshot a sandbox came from, and
  reports the provider console URL on `Ready`.
- Terminals use the driver `Pty` facet; the server's SSH, preview, and
  VNC endpoints use the `SshAccess`, `PreviewUrls`, and `Vnc` facets
  through the driver-typed reconnect. The preview endpoint now answers
  for every provider with a preview facet, so the local sandbox returns
  its loopback URL.
- Daytona credentials travel as `DaytonaCredentials` built from the
  vault key plus configured URL and organization; nothing reads the
  process environment implicitly. The inventory registry uses the shared
  `DriverInventoryProvider`.
- The SDK-based `daytona/mod.rs`, `provider/daytona.rs`, the Daytona
  terminal, the `daytona` cargo feature, and the direct daytona-sdk,
  git2 (in fabro-sandbox), tungstenite, and rustls dependencies are
  gone. The live Daytona tests run against the driver-backed sandbox.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 19:14:13 -06:00
Bryan Helmkamp
75e5f34c0d
Expose lithos request and response shapes through the API, server, CLI, and web
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>
2026-09-09 17:26:57 -06:00
Bryan Helmkamp
80bc51c40e
Open sandbox provider identity to plugin kinds
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>
2026-09-09 15:17:32 -06:00
Bryan Helmkamp
b6482910e5
Remove expired startup secret migrations 2026-09-05 14:05:48 -04:00
Bryan Helmkamp
6cf027282f
Update inspect snapshots for projected conclusion stages 2026-09-05 11:53:44 -04:00
Scott Werner
a00b95abe0 Report available environments when default is missing
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>
2026-09-03 12:56:47 -04:00
Scott Werner
cd20e453ec Use a compatible environment in pull request settings test 2026-09-02 17:24:22 -04:00
Scott Werner
49fb160108 Use async reads during CLI intent preparation 2026-09-02 13:44:33 -04:00
Scott Werner
e3cbc31ca1 Harden CLI run target identity 2026-09-02 12:59:04 -04:00
Scott Werner
ccfd23104d Harden CLI run intent creation 2026-09-01 17:08:34 -04:00
Scott Werner
204dd29e73 Make CLI intent runs independent of run-tool changes 2026-09-01 16:28:58 -04:00
Scott Werner
694d1981ff Simplify the CLI run-intent create path
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>
2026-09-01 16:14:34 -04:00
Scott Werner
afe1133878 Fix CLI RunIntent producer CI failures 2026-09-01 16:14:34 -04:00
Scott Werner
2507a0075f Create CLI runs from immutable workflow intents 2026-09-01 16:14:34 -04:00
Bryan Helmkamp
6ac769c4cd
Merge pull request #810 from fabro-sh/mcp-config-name
Add --name to fabro mcp config and fabro mcp init
2026-08-26 09:22:49 -04:00
Bryan Helmkamp
e5046d8b1b
Refactor MCP config argument handling 2026-08-26 09:01:37 -04:00
Bryan Helmkamp
a049f94042
Add on_failure="succeed" as an explicit failure policy
A failed node with an effective `succeed` policy and no explicit recovery
route now finishes as `succeeded` and follows normal success routing. The
original failure stays on the outcome so the stage.completed event and the
checkpoint keep the diagnostic, and the outcome notes record which scope
promoted it.

- OnFailure gains a Succeed variant; Node::on_failure resolves the
  deprecated auto_status=true attribute as an alias, with an explicit
  on_failure winning
- The core executor applies the policy before the lifecycle observes the
  result, so the recorded outcome, context keys, goal gates, events, and
  routing all see the effective outcome; this replaces AutoStatusLifecycle
- Explicit routes take priority: a matching condition, preferred label,
  suggested next node, or handler jump keeps the outcome failed. A failed
  outcome takes an unconditional edge only under route, so under succeed
  any edge selection is an explicit route
- succeed applies only to failed, matching exit; the auto_status alias no
  longer promotes partially_succeeded
- Parallel branches promote after their retry loop, so a failed succeed
  branch counts as succeeded in the parent aggregate
- Validation accepts succeed and adds an auto_status_deprecated warning
  that suggests on_failure="succeed"
- Document the policy table, semantics, and deprecation; add a changelog
  entry

Closes #807

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 07:34:24 -04:00
Bryan Helmkamp
24165b10f5
Add --name to fabro mcp config and fabro mcp init
Both commands always registered the MCP client entry under the fixed
`mcpServers` key `fabro`, so users could not register separate Fabro
servers (for example production and testing) without editing the client
JSON by hand.

`--name <NAME>` now selects the `mcpServers` key. It defaults to `fabro`
for backward compatibility and rejects empty values. `fabro mcp init`
upserts only the named entry and preserves entries with other names, so
reusing a name updates that entry in place.

Closes #808

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 07:34:06 -04:00
Bryan Helmkamp
c90d195c2f
Merge pull request #806 from fabro-sh/node-on-failure
Add node-level on_failure override
2026-08-26 07:01:49 -04:00