Commit graph

520 commits

Author SHA1 Message Date
Bryan Helmkamp
ba3adb92c7
Serve the local sandbox through the driver Host provider
SandboxSpec::Local and run reconnect now build a DriverSandbox over the
sandbox-driver Host provider instead of fabro's own LocalSandbox, which
is deleted. fabro_sandbox::local_sandbox designates the working
directory (created when missing, never removed), creates the Host handle
in a per-process registry, and learns the platform up front. A local
sandbox reports no provider id: it is its directory, which the run
record already carries, so reconnect rebuilds the handle over that
directory rather than by id.

The credential filter for explicit environment variables and the Bash
readiness probe now come from the exec layer and the driver's activate
helper. Test call sites move to the async constructor; test factories
that must stay synchronous share the parent session's sandbox handle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 16:09:45 -06:00
Bryan Helmkamp
ef69883fdd
Add a fabro Sandbox backed by sandbox-driver facets
DriverSandbox implements fabro's Sandbox trait over a driver handle.
Files go through the Filesystem facet, content and tree search through
the Search facet (grep rendered as path:line:content, walks reported
relative to the caller's base with sizes filled from metadata when the
transport has none), commands through fabro's exec policy, preview URLs
through the access facet, and lifecycle through the handle with fabro's
run events emitted around each step. Initialize and start use the
driver's activate helper, which brings the sandbox to Running and
verifies non-login Bash on both transports, and learn the platform name
from the sandbox itself. Local kinds keep the credential filter on
explicit environment variables; isolated kinds take the caller's
environment as composed. Tests run every operation against the
in-process Host provider.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 15:50:02 -06:00
Bryan Helmkamp
c9f5ec0a41
Add fabro's exec policy over the sandbox driver Exec facet
fabro_sandbox::exec::SandboxExec runs Bash source through a driver Exec
facet with fabro's own stop policy. Fabro disables the driver's hard
timeout and runs its own timer; a timeout or a caller's cancellation
fires the driver's TERM token, waits the grace period, then fires KILL.
The result reports why fabro stopped the command (timed out or
cancelled) rather than which signal the provider observed, and a stopped
command carries no exit code so a shell's 143 is never read as a program
result. Output streams through the caller's callback, is drained past
the retention cap, and explicit environment variables pass a fail-closed
credential filter on the host and through unchanged on isolated
providers. spawn_stdio wraps the driver's bidirectional process and its
rolling stderr tail in fabro's existing stdio types.

Driver errors now map into fabro's error type as a boxed variant with
accessors for the not-found, unsupported, transport, and incomplete
cases, and the redacted output tail helper reads a driver ExecFailure
as well as fabro's own exec error. The Local sandbox's credential name
filter moves to the exec module so both paths share one policy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 15:36:25 -06:00
Bryan Helmkamp
4dcbcec400
Add the single sandbox provider construction function
fabro_sandbox::driver::connect_provider turns one
[server.sandbox.providers.<kind>] entry into an Arc<dyn SandboxProvider>
from the sandbox-driver crates. Bundled kinds link the driver's Host,
Docker, and Daytona providers in-process; Daytona receives its API key
and endpoint explicitly through connect_explicit with a
fabro-sandbox/<version> user agent, never from the process environment.
Any other kind launches the configured plugin executable through a
PluginSupervisor and returns a wrapper that replaces a crashed plugin
for new work only, never replaying a failed call. Disabled entries are
refused at the construction point.

The Host and Docker providers also ship as fabro-sandbox-host and
fabro-sandbox-docker plugin executables so CI can drive the bundled
providers over stdio and a deployment can move one out of process by
configuration alone. An integration test registers the Host executable
under the non-bundled kind `host`, creates a sandbox and runs a command
over the wire, then attaches to it by persisted id from a fresh plugin
process, and checks that a plugin declaring a different kind is refused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 15:23:56 -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
124065ac52
Fix duration lint in the sandbox driver benchmark
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 15:08:34 -06:00
Bryan Helmkamp
d88c6064af
Benchmark agent tool calls through the sandbox driver
Phase 2 of the sandbox-driver adoption: an ignored test that unpacks
fabro's own lib tree into each provider and times file reads and
content searches through fabro's current providers, the driver
providers in-process, and the driver providers served over JSON-RPC on
an in-process pipe. Docker reads match, Docker grep is faster through
the driver, Host grep costs about 20 ms more through the derived
search, and the wire hop adds about 0.1 ms per call against the plan's
100 ms per tool call budget. The plan records the full table.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 14:30:32 -06:00
Bryan Helmkamp
07c52b07ca
Depend on the sandbox-driver crates by git revision
Fabro pins the sandbox-driver workspace the same way it pins the
Daytona SDK: every crate at one commit on main. The bundled Host,
Docker, and Daytona provider libraries link in-process, and the
protocol crate reaches third-party providers over stdio. Nothing uses
the crates yet; the following commits move fabro onto them one layer
at a time.

The driver pins tracing-subscriber exactly, so the lockfile settles on
that version for the whole workspace.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 14:18:58 -06:00
Bryan Helmkamp
aa8b919f1c
Pin daytona-sdk-rust to the merged main commit
Both fabro and sandbox-driver now pin the same daytona-sdk-rust commit
on main. The newer SDK adds region and sandbox class fields to snapshot
creation; fabro leaves both unset and keeps its current behavior.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 13:45:44 -06:00
Bryan Helmkamp
a7ed03e175
Rebuild conclusion stage summaries from run events 2026-09-05 11:44:23 -04:00
Scott Werner
f52f2a1edb
Merge pull request #838 from fabro-sh/codex/persist-pre-start-worker-failures
Persist pre-start worker failures
2026-09-03 12:48:39 -04:00
Scott Werner
c437dc012d Simplify pre-start worker failure handling
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>
2026-09-03 11:45:47 -04:00
Scott Werner
a2002175e1
Merge pull request #835 from fabro-sh/codex/projected-run-cleanup
Move run projection replay into the summary store
2026-09-03 11:40:45 -04:00
Scott Werner
54666632c8 Persist pre-start worker failures 2026-09-02 17:29:41 -04:00
Scott Werner
0f1e5e1c9c Centralize run projection replay on ProjectedRun
Move ProjectedRun next to EventProjectionCache in run_state, since the
summary store both produces and consumes it, and give it a replay
constructor that owns the events-to-head derivation. load_projection now
returns RunNotFound directly instead of erasing it to None and having
callers rebuild it; load_run_projection is the single Option translation
point. install_in_memory_state reuses the existing From impl, and the
commit path passes its Arc through instead of unwrapping and
reallocating it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 16:33:14 -04:00
Scott Werner
efcf8a0d93
Merge pull request #831 from fabro-sh/codex/cli-run-intent-producer
Create CLI runs from immutable workflow intents
2026-09-02 15:45:21 -04:00
Scott Werner
2360e8046b Move run projection replay into the summary store 2026-09-02 13:32:43 -04:00
Scott Werner
e3cbc31ca1 Harden CLI run target identity 2026-09-02 12:59:04 -04:00
Scott Werner
064074233c Narrow pull request recovery to unresolved creation requests
The supervisor's periodic recovery scan asked SQLite for every run that
had ever recorded an explicit pull request creation request and then
replayed each inactive candidate's full history to learn whether the
request was still pending. With projections now loaded on demand that
set grows without bound and was replayed every scan.

The candidate query now mirrors the projection reducer: a run is a
candidate only when its latest creation request has no later request,
created, linked, or unlinked event, and no later failure naming the same
creation id. Callers still replay each candidate to confirm, so the query
only has to avoid omitting a pending run, and the replayed set is bounded
by in-flight requests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 12:48:21 -04:00
Scott Werner
13f3ff867b Simplify on-demand run projection loading
Drop the unreachable active-run mismatch guard that was copied into
load_run_projection: the active-runs map is only ever inserted under the
handle's own run ID, so the check could never fire. Remove it from the two
pre-existing sites too and delete matches_run.

Trim install_in_memory_state to take only the committed projection, since
the event envelope duplicated last_seq and the inner scope only existed to
release the lock before the now-removed shared cache update. Add a From
impl so RunDatabase::build no longer hand-builds EventProjectionCache, and
rename projected_state_locked to match its projection_snapshot sibling.

In fabro-server, have reject_if_archived and ensure_run_exists read the
run summary row instead of replaying the full event history for inactive
runs; the summary is written in the same transaction as the event.

Fold the repeated store-reopen fixtures in fabro-store and fabro-server
tests into helpers, and fix a stale comment about the deleted shared
projection cache.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 11:43:17 -04:00
Scott Werner
204dd29e73 Make CLI intent runs independent of run-tool changes 2026-09-01 16:28:58 -04:00
Scott Werner
c1e9364490 Load inactive run projections on demand 2026-09-01 16:06:54 -04:00
Scott Werner
f2a2630ad0 Simplify pull request recovery and summary store queries
- Load recovery candidates from the warm projection cache instead of
  replaying each run's full event history, and check dispatch
  eligibility before any I/O
- Extract the shared can_dispatch predicate used by both the recovery
  scan and the worker dispatch loop
- Drop load_durable_run_status, now identical to durable_run_status
- Share parse_stored_run_id across the three stored-id decode sites
- Reuse push_order for the canonical run ordering in list_all and
  list_by_statuses
- Replace the test-only queue clear accessor with the existing drain

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 14:11:53 -04:00
Scott Werner
1a2d8a9056 Move run queries and pull request recovery to SQLite 2026-09-01 14:06:06 -04:00
Scott Werner
563ca26b98
Merge pull request #829 from fabro-sh/codex/session-ownership-index
Index session ownership from creation events
2026-09-01 14:03:25 -04:00
Scott Werner
0fd4714da7
Merge pull request #825 from swerner/codex/automation-workflow-sources
Add independent workflow sources to automations
2026-09-01 12:22:20 -04:00
Scott Werner
7a3f58c87e Tighten automation Git validation types 2026-08-31 18:02:08 -04:00
Scott Werner
5af791c812 Align remote workflow selectors with run targets 2026-08-31 17:13:20 -04:00
Scott Werner
bf05b01ba6 Simplify session owner lookup and migration preflight
- 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>
2026-08-31 14:59:38 -04:00
Scott Werner
3411199103 Index session ownership from creation events 2026-08-31 14:16:58 -04:00
Scott Werner
011876edd1 Harden local RunIntent target observation 2026-08-31 14:08:35 -04:00
Scott Werner
0b46e1d735 Simplify local RunIntent producer support
Share canonicalize_location and resolve_existing_workflow_location
between the local package resolver and the version collector, drop the
redundant package-root pre-check and the PackageFileReadError enum in
favor of anyhow context, and read HEAD's SHA from git2 instead of a
separate rev-parse subprocess.

Make GitRunTargetObservation a plain struct, replace the repo-info
tuple with a named struct, tighten the closure view trait, avoid
deep-cloning the root workflow during collected validation, remove the
unused into_closure accessor, and dedupe test helpers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 14:08:35 -04:00
Scott Werner
52ff7b2ef2 Add local RunIntent producer support 2026-08-31 14:08:35 -04:00
Scott Werner
39c018c430 Harden automation workflow source handling 2026-08-31 13:45:08 -04:00
Scott Werner
05fb173767
Merge pull request #823 from fabro-sh/codex/sqlite-run-history-activation
Activate atomic SQLite run history storage
2026-08-31 12:53:11 -04:00
Scott Werner
c68e67c841 Harden SQLite activation and run history consistency 2026-08-31 12:19:16 -04:00
Scott Werner
305381e2c2 Simplify automation workflow-source materialization
Collapse the role-paired materializer error variants into
`Credentials`/`Checkout` tagged with a `CheckoutRole`, route both
checkouts through one resolve-then-prepare helper, and replace the
test-only clone-URL field on the production materializer with a
`GitRemote` resolver seam. A workflow source in the target's repository
now reuses the already-resolved credentials instead of minting a second
token.

Also inline the one-line workflow-source normalizer, drop the `as_str`
wrapper on the new kind enum, remove the unused migration constant, move
rather than clone scheduler fields, and deduplicate the web form's
ref-validity rule and per-kind copy into a single table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 13:17:15 -04:00
Scott Werner
03f81d1f25 Add independent workflow sources to automations 2026-08-30 13:14:11 -04:00
Scott Werner
88dae80c6d Consolidate automation environment helpers and trim redundant work
- Share the clone-based provider predicate and provider label between the
  automation form and environment settings instead of duplicating them
- Hoist repeated environments query state in the new-automation route
- Normalize empty environment ids to None so validation needs one check
- Merge the scheduler's record/clear error helpers and skip the clearing
  write when no error is stored
- Guard the environment backfill with a cheap existence query
- Drop an unneeded id clone and a no-op migrator comment

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 12:25:16 -04:00
Scott Werner
e87130ae23 Require a server-managed environment for automations
Automations now store an environment_id that must reference an enabled
Docker or Daytona environment. Each trigger fire resolves the current
environment definition and snapshots its settings into the run, and
deleting an environment still referenced by an automation is rejected
with a conflict.

Existing automations are backfilled conservatively: a compatible
environment named default is selected when present, otherwise the sole
compatible environment. Anything ambiguous is left incomplete and cannot
run until an operator selects an environment in the web UI.

Scheduler failures are recorded on the automation as last_error and
cleared after the next successful scheduled run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 09:58:13 -04:00
Scott Werner
0574c818cf Share activation backup helpers and trim run-history residue
Hoist the SQLite backup/integrity helpers duplicated between the blob and
run-history activation migrations into one shared module, drop the
write-only recent-events buffer and other dead state left behind by the
SQLite cutover, reuse existing helpers for projection bootstrap, sequence
allocation, catalog keys, and test pool construction, and collapse the
tombstone and activation-marker plumbing to single statements.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 17:20:05 -04:00
Bryan Helmkamp
9dd47a8cd3
Merge pull request #819 from fabro-sh/codex/daytona-image-snapshots
Allow Daytona snapshots from Docker images
2026-08-28 16:24:55 -04:00
Bryan Helmkamp
5ebf3ebd35
Model the Daytona snapshot source as an enum
`DaytonaSnapshotSettings` carried two independent `Option`s (`image` and
`dockerfile`) that every consumer had to re-validate. Replace them with a
single `source: DaytonaSnapshotSource { Image, Dockerfile }` so the
both-set and neither-set states are unrepresentable at the sandbox layer.
This removes four unreachable error arms in `canonical_manifest` and
`create_snapshot_params`, the `.filter(...)` guard in `initialize`, and
the presence guard in `daytona_config_from_environment`. The
mutual-exclusion rule now lives only in fabro-config, which owns the
`image.docker` / `image.dockerfile` keys the old messages named.

Merge `ImageSnapshotManifest` into `SnapshotManifest` via a flattened
`SourceManifest` enum. The dockerfile case serializes to the same bytes
as before, so existing snapshot names are unchanged; the pinned identity
test still passes. Pin the image-case identity as well so a future
manifest change cannot silently orphan image snapshots.

Fold `validate_daytona_image_settings` into the existing Daytona arm of
`validate_provider_capabilities`; both callers already invoke it right
after `resolve_environment_fields`, so error order is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 16:19:56 -04:00
Bryan Helmkamp
85a0b70c98
Merge pull request #822 from fabro-sh/fix-nested-blob-resolution
Hydrate nested structured blob references
2026-08-28 15:47:46 -04:00
Scott Werner
aa9c468d3e Wait for SQLite writer lock when deleting runs 2026-08-28 15:46:13 -04:00
Scott Werner
5cd69e9c66
Merge pull request #820 from fabro-sh/codex/automation-run-intent
Move automation runs to workflow versions and RunIntent
2026-08-28 15:43:31 -04:00
Bryan Helmkamp
a125596681
Resolve nested blobs concurrently 2026-08-28 15:31:21 -04:00
Scott Werner
f073684227 Activate atomic SQLite run history storage 2026-08-28 15:18:25 -04:00
Bryan Helmkamp
5485bb1e93
Hydrate nested structured blob references 2026-08-28 15:15:39 -04:00
Scott Werner
4922fec45a Support historical run events during migration 2026-08-28 10:49:43 -04:00