Register lazy Host, Docker, and Daytona factories for Petri execution,
fork, and prune. Share server provider configuration, preserve lease
fingerprints, and source Daytona credentials from the vault.
Remove built-in plugin setup and skip gates; add a release-mode worker
and prune regression to catch the failure that blocked nightly builds.
Co-Authored-By: Codex <noreply@openai.com>
`lib/components/fabro-pebble-sandbox` moved into pebble as
`pebble_coding_agent::sandbox_driver` (lithoscomputer/pebble#27): the
`Environment` over a driver handle (`SandboxEnvironment`, was
`PebbleSandbox`), the `SandboxExec` policy, the port routes, and
`display_for_log`. The pebble pin moves to that branch head, 6d03b3b,
with the `sandbox-driver` feature on (`sandbox-driver-test-util` for the
server's tests, which take `MockSandbox` from pebble now). Nothing in the
crate was Fabro's by design; what was Fabro's stays: `SecretRedactor`
moves to `fabro-redact` as pebble's `Redactor` over `redact_string`, and
the log renderer takes it where a driver failure is rendered.
`fabro-petri` hands pebble types to Petri's crates, so Petri must pin the
same pebble revision: the petri pins move to lithoscomputer/petri#36
(9ee3f85), which pins pebble at the same head. Both re-pin to the pebble
merge commit together once #27 merges.
The 14 pebble commits between the pins fold the session projection's
lifetime tallies into `SessionProjection::totals` (and `PromptDelta`'s
into a flattened `totals`, which renames the prompt's `subagents` key to
`subagent_counts`, as the projection's already was). The stage progress
fold, the runs handler, the OpenAPI schema, the generated client model,
and the round-trip test follow.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nothing imports it any more: the Pebble glue lives in
fabro-pebble-sandbox, the server reaches run sandboxes through
sandbox_access, and Petri creates every run sandbox. The crate, its
test-support, its integration tests and every dependency edge go with
it. The `[server.sandbox.providers.<kind>.plugin]` settings stay: the
server still launches a plugin executable through them to attach to a
sandbox of a non-bundled kind.
AGENTS.md names the new crate and the direct-access pattern in place of
`RunSandbox`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The sandbox handlers describe, list, download, upload, open a terminal
and build SSH and VNC access on the `Arc<dyn Sandbox>` the server attaches
to the run's record, with paths resolved against the recorded working
directory. Run Files holds the handle beside that directory and runs its
git through the driver's git facet and Fabro's exec policy;
fabro-workflow's sandbox git takes the same pair, and its
`GitCommandError` carries the driver's error. Run deletion deletes by id
through the provider scoped to the run's `petri.run` label, so a foreign
sandbox is refused and a designated host directory is left in place. Ask
Fabro wraps the attached, running handle. The legacy access shim is gone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`fabro-server/src/sandbox_access.rs` is the server's own path to a run's
sandbox: it connects the record's provider (the driver's Host, Docker and
Daytona providers in process, a plugin executable for any other kind),
keys ownership on the `petri.run` label Petri stamps on every sandbox it
creates, attaches by the recorded id, and for a host record designates
the recorded directory again when the id lives only in the worker's
registry. The Docker client resolves its endpoint from the same variables
Petri forwards to its plugin, so both meet on one daemon.
The doctor's Docker check and the Daytona credential probe move here with
`DaytonaCredentials`, and the `/sandboxes` inventory is rebuilt over the
driver's `list`, narrowed to sandboxes that carry Petri's run label.
Preflight asks the provider for its health instead of creating and
deleting a throwaway sandbox in Fabro's own shape, which no run uses; the
git retry policy behind the repository probe moves into run_manifest.
The callers still on fabro-sandbox's reconnect read their access through
a `legacy_provider_access` shim until they move.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Petri creates and owns every run sandbox through the sandbox driver, so
what Fabro still needs around a driver handle is the Pebble glue: the
Environment pebble's coding agent runs its tools through, the exec policy
(stop grace, working directory, StripAll, the termination mapping, the
redacted output tail), pebble's port routes over the driver's preview
URLs, the secret redactor, the path helpers, and a log rendering that
appends a failed command's redacted tail. This crate holds that glue,
moved from fabro-sandbox, over `Arc<dyn Sandbox>` plus a working
directory instead of `RunSandbox`, with a `MockSandbox` double behind
`test-support`.
`fabro exec` creates its host sandbox directly on the driver's Host
provider and activates it; Ask Fabro wraps the attached handle. Both
keep the provider alive beside the sandbox where the session's processes
are the provider's process groups.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fabro's own DOT parser was left with one job after create-time compile
moved to Petri: walking a workflow's file references for the bundler and
the workflow-version store, and reading a name, a goal and two counts.
Petri's frontend parses the same language, so the parser goes and a small
crate reads the graph through Petri's.
`fabro-dot` is that crate: `WorkflowGraph::parse` over
`petri_frontend_attractor::dot` and its semantic model (defaults applied,
subgraphs flattened, chains expanded), `references(position)` as the one
walker over the static-reference vocabulary (each reference with its node
and position, file references checked to be template-free), and
`normalize_for_graphviz`, the re-emit of Fabro DOT with dotted attribute
keys quoted, which the SVG render needs. It sits beside `fabro-petri`
rather than inside it because `fabro-petri` depends on `fabro-workflow`,
which depends on `fabro-workflow-version`: the version store cannot reach
`fabro-petri` without a cycle, and the bundler should not pull the engine
in to read a graph.
Deleted: `fabro-graphviz`'s lexer, grammar, AST, semantic pass and
`parse_ast` (1,829 lines, plus the `nom` dependency); the DOT model in
`fabro-types::graph` (`Graph`, `Node`, `Edge`, `AttrValue`,
`shape_to_handler_type`), with only `ReferenceKind` kept, moved to
`fabro_types::reference`; `fabro-template`'s `visit_graph_references` and
the `GraphReference`/`GraphPosition` types, with the template-syntax rule
(`validate_static_reference`) kept there; the pull-request body's DOT
fallback summary, which was unreachable because the DOT source only
travels with the run spec whose display graph the summary already reads.
`fabro-graphviz` is now the render alone, over `fabro-dot`.
Parity: the old and new walkers were run over every `.fabro` and `.dot`
file in the repository (118) before the deletion. Every reference set is
identical. Five files differ in what Petri reads more correctly: a
backslash before a newline inside a quoted string is a line continuation
(four files, inline prompt text only), and a node named only by an edge
counts as a node (`test/edge_only_node.fabro`, 3 nodes rather than 2, so
the `fabro validate` snapshot moves). The checked-in bundles' shapes and
references are pinned by a snapshot in `fabro-dot`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Each edge was checked with rg over the crate's sources outside its
test paths. fabro-cli keeps git2, regex, ulid, and shlex as
dev-dependencies for its integration tests. fabro-automation keeps
chrono, tokio, and tracing: its migrations compile into the crate
through #[path]. petri_testkit was already optional behind fabro-petri's
test-support feature and dual-listed as a dev-dependency. The workspace
loses the agent-client-protocol and AWS entries no crate references;
jsonschema stays for the fabro-api and fabro-tool tests. Cargo.lock
was refreshed by a plain build and only loses entries.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Artifacts reach the blob table through the hooks, so the POST on
/runs/{id}/stages/{stageId}/artifacts, its octet-stream and multipart
handlers, the RequireStageArtifact extractor, the client's upload
functions, and the generated TypeScript operation go. The spec loses
the operation, the multipart variant writeRunBlob never served, and
the batch manifest schemas; fabro-types loses ArtifactUpload, the
batch upload's only input type. Every list and download path stays,
and the server tests seed the artifact store directly to cover them.
fabro-server drops multer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Petri judges a workflow at admission, so Fabro's lint rules go:
`fabro-validate` (its 35 rules and the `LintRule` trait) is deleted, and
with it `fabro-acp` (only a rule and two legacy executor tests used it),
the model-resolution transform, the legacy `create`, `compile_create_run`
and `materialize_create_run` stages, and `fabro-graphviz`'s `condition`
and `fidelity` modules. `Diagnostic`, `RelatedDiagnostic` and `Severity`
move to `fabro_types::diagnostic`, the one shape every diagnostic takes.
Validation is now the same question the create handler asks. A new
server module, `petri_check`, builds Petri's check request from a
workflow bundle and the run's settings (every workflow of the bundle at
its bundle-relative path, the inputs, the run variables, the launch),
runs the check, and maps the diagnostics; Fabro adds one rule of its
own, `fabro.model.no_ready_provider`, refusing a model node when no
provider is ready. Admission, the validate and preflight endpoints and
the offline `fabro validate` all go through it:
- `validate_prepared_manifest` runs Fabro's structural pass (parse and
transform, whose diagnostics stay) and then Petri's check, on the
blocking pool from the handlers;
- the offline `validate_manifest` checks with no model client and with
an unbound input as a warning (`CheckRequest.unbound_is_warning`), so
a workflow validates before its inputs exist; a collected workflow
before upload checks with unbound inputs as errors, as before;
- preflight resolves each LLM node's selector against the ready
providers and the catalog for its probe, as the deleted transform did,
and no longer probes a model Petri refused;
- the graph render endpoint needs only the structural pass;
- a run manifest now carries its `[run.goal] file`, which Petri reads
from the bundle as it does for a version.
The transforms keep the authored model selector (`sonnet` stays
`sonnet`): Petri pins the catalog model in the admitted graph, not in
the graph Fabro displays or in the settings snapshot. Tests assert that,
and the CLI's validate, preflight and graph snapshots carry Petri's
diagnostics (`attractor.no_start`, `attractor.undeclared_node`,
`attractor.bad_on_failure`, ...) in place of the lint rules' text.
Known gaps, Petri's side: a `workflow.toml` whose `[run.environment]`
names an environment the server catalog defines but the file does not
is refused (`unsupported.workflow_toml.run.environment`), as admission
already refused it; an unbound input inside an included template
partial is a render error rather than the unbound-input warning.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`fabro-hooks` ran the legacy executor's hooks; Petri's Attractor steps
run Fabro's hooks now, so nothing in the workspace uses the crate. The
engine freeze (the CI workflow, the two scripts, and the AGENTS.md and
fabro-petri README sections) guarded the engine half of `fabro-workflow`,
which the previous commit deleted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The server answers the `/api/v1/runs/{id}/petri/*` endpoints from one
`SqliteRunStore` over its pool. `PetriRuns` in `AppState` keeps the
writer handle each worker opened, keyed by the run and the worker's
owner id, so the lease semantics stay the store's: the handle drops on
the worker's `release`, and every handle of a run drops when the server
observes the run's worker exit, in the subprocess wait path. Never by
timeout. A write from an owner with no held handle reopens only when
the lease row still names that owner, so a server restart or a lost
open reply recovers, and an owner the lease moved away from gets
`petri_stale_owner`.
Every endpoint is worker-scoped through the existing worker auth; a
new `RequireWorkerRunSegment` extractor covers the two-segment routes.
Store errors answer with a machine-readable code, the leased owner and
the conflict position under `meta`, and a backend failure's cause goes
to the server log rather than the worker.
A test drives a held worker through the scheduler, opens the run over
the API with its token, ends the worker, and sees the lease end.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
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>
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>
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>
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 `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>
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'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>
fabro-workflow's and fabro-server's src/test_support.rs import
fabro_store::test_support, but their test-support features never enabled
fabro-store/test-support. Workspace builds passed only through feature
unification from other members' dev-dependencies, while per-crate builds
such as `cargo check -p fabro-cli --tests` or
`cargo check -p fabro-server --features test-support` failed with E0432.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the WorkflowVersion domain resource with exactly entrypoint, files,
and workflow_dependencies, plus strict WorkflowPath validation and
deterministic canonical raw JSON. Semantic validation of graph imports,
templates, file references, workflow.toml rules, Dockerfile paths, and
exact child-workflow dependency bindings lives in the new
fabro-workflow-version crate, which validates the complete stored
dependency closure through the shared blob store before writing a root.
The authenticated create-only POST /api/v1/workflow-versions endpoint
ships with its OpenAPI contract, Rust type replacements, and generated
TypeScript client.
Squashed from the resource commits of the original combined branch;
the walker unification this builds on landed separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`EnvCredentialSource` resolved provider credentials from the process
environment. It had no production entry point of its own — it was only
ever reached as the `None` arm of an `Option<Vault>` in three places:
`build_llm_source`, `configured_providers_for_start`, and
`configured_providers_from_process_env`.
That optional vault is not a state the product can be in. Every run has a
server behind it, the server always spawns workers with `--storage-dir`
(`worker_runtime.rs`), and `SqlVaultCredentialSource` backs both the
server and the CLI. So the fallback only served to silently degrade
credential resolution to whatever the worker process happened to have in
its environment.
Make the vault required across the run path — `RunOptions`,
`StartServices`, `build_llm_source`, `tool_secrets_from_configured_sources`,
`vault_token_lookup`, and the CLI GitHub helpers — so the invariant is
enforced by types rather than assumed. A worker spawned without
`--storage-dir` now fails with a clear message instead of quietly
continuing without a vault.
`configured_providers_from_process_env` had no callers at all and is
deleted. `AgentApiBackend::new_from_env` was public but only ever called
from its own tests; it is deleted too.
Test-only credential sources move to a feature-gated
`fabro_auth::test_support`, wired through dev-dependencies so they never
link into production builds. The CLI worker tests now pass
`--storage-dir`, matching what the server actually does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prompt used hand-rolled `{placeholder}` substitution via `str::replace`.
The app already has a MiniJinja layer for exactly this, and every other
checked-in prompt uses it, so use it here too.
`prompts/run_title.md` becomes `prompts/run_title.md.j2` with `{{ inputs.* }}`
variables, rendered through `fabro_template::render_named`. Strict undefined
handling now catches a variable the template asks for and the caller does not
supply, which the old `.replace()` chain silently left as literal text.
`build_title_prompt` returns `Result` accordingly. A checked-in template that
will not render is a bug rather than a transient failure, so the caller logs
it at `warn` — louder than the `debug` used for a generation miss — and keeps
the deterministic title.
Re-checked against claude-haiku-4-5: same titles as before the change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>