Commit graph

435 commits

Author SHA1 Message Date
Scott Werner
ad980607e8 Complete transport fixtures for finalization regressions 2026-10-05 17:16:13 -04:00
Scott Werner
8cb57cf867 Synchronize finalization regressions with persisted execution evidence 2026-10-05 17:13:58 -04:00
Scott Werner
372018cd87 Preserve committed outcomes during host failure cleanup 2026-10-05 17:08:57 -04:00
Scott Werner
ba215111f4 Commit publication failures through required run finalization 2026-10-05 16:59:50 -04:00
Scott Werner
8d848333b3 Merge remote-tracking branch 'origin/worker-fencing' into codex/required-publication 2026-10-05 16:45:31 -04:00
Scott Werner
063ee15ca8
Merge pull request #918 from fabro-sh/codex/durable-final-patch
Some checks failed
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Rustdoc (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Sandbox providers (Docker) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
Rust / Process titles (musl) (push) Waiting to run
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
Restore Files Changed from saved final patch blobs
2026-10-02 16:31:32 -04:00
Scott Werner
b6de8bf8d3 Resolve the saved final patch when creating a pull request afterwards
Creating a pull request for a finished run read the projection's final
patch field directly. Petri stores that patch in the blob table and the
field holds a blob reference, so the description model was handed the
reference string instead of the diff, and the empty-diff check could
never fire.

Move the reference resolution out of Files Changed into a shared
final_patch::load helper and use it for both readers. Pull request input
extraction now reads the real patch, judges emptiness by its text, and
reports a missing or unreadable blob instead of describing a placeholder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 14:49:40 -04:00
Scott Werner
fae391c4dc Bind CLI model flags in one place when building Petri's check
The create, validate and preflight paths each wrapped their launch with
the run's --model and --provider flags before building the check
request, so a new caller could build a check without them. The check
request now takes the flags as a required argument and binds them onto
the launch itself, and unit tests cover the flags, a provider-only flag,
and no flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:10:52 -04:00
Scott Werner
b401ac0a10 Bind CLI model flags as Petri's launch model and settings as its default
Petri now treats `petri.launch_model` and `petri.launch_provider` as the
model a run's flags ask for, above the file layers and the graph's
defaults. A host's last-resort default moved to `petri.default_model` and
`petri.default_provider`. Pin Petri at the merge of that change and bind
to it: the explicit `--model`/`--provider` flags go to the launch
variables, and the model the settings resolved (or the catalog default)
goes to the default variables.

`Launch` now names the two pairs `model`/`provider` and
`default_model`/`default_provider`, matching Petri, in place of the
separate override fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:28:27 -04:00
Scott Werner
4e71554773 Apply CLI model and provider overrides to agent execution 2026-10-02 11:28:16 -04:00
Scott Werner
500cd25814 Keep network policy off host runs and tidy the network tests
The host provider manages no networking and refuses any policy but its
default, so sending the resolved AllowAll to local runs failed every
non-dry-run local execution. Apply the run's policy only on container
backends; dry runs, which always use the host backend, are covered by the
same check.

Also fold the Docker environment test helper into one that takes a typed
network mode, share the probe setup between the live Docker and Daytona
network tests, count canary hits per mode, and bind the run environment
once in the worker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:17:17 -04:00
Scott Werner
cd5f56a4ed Enforce resolved network policy for Docker and Daytona runs 2026-10-01 17:12:03 -04:00
Scott Werner
68f0c37d47 Fence worker credentials across server restarts 2026-10-01 14:44:22 -04:00
Scott Werner
82156442a8 Resolve durable final patch blobs for Files Changed 2026-10-01 14:42:33 -04:00
Scott Werner
aad954693e Keep checkpoints and run branch pushes in the sandbox 2026-10-01 13:01:13 -04:00
Scott Werner
0250b40586 Publish a successful run from its run_finished hook
Pushing the run branch and opening the pull request now happen in the
run's worker, in Fabro's run_finished hook, after the last stage and
before the run's terminal record, as the legacy publish step did. A
failed push or pull request fails the run with publish_failed instead of
leaving a warning on a run that already succeeded.

fabro-petri gains a RunPublisher the hooks call for a successful run with
its run branch, final commit, snapshot repository and patch; the worker's
GitHub publisher pushes from the snapshot repository with a push token it
mints at that moment, opens the pull request its settings ask for, and
records it. The worker resolves the server's GitHub credentials itself
for both the read-only checkout token and the push token, so the server
no longer hands it a clone credential or publishes after the run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:00:26 -04:00
Scott Werner
5c6195a352 Check GitHub targets out in the sandbox and publish their run branch
Since the Petri cutover, a run with a GitHub target started in an empty
workspace, nothing pushed its run branch to GitHub, and nothing asked for
the automatic pull request when it succeeded.

Fabro's hooks now check a fresh run's GitHub target out inside the
sandbox when Petri hands them the scope, before the first stage: the
workspace fetches the selected commit, tag or branch at the run's clone
depth, with a read-only token the server resolves at each worker launch.
The worker scrubs the token from its environment at startup and presents
it only to the fetch, so it never lands in the repository or its remote.
The files belong to the sandbox user, so git accepts them.

The same checkout seeds the workspace's snapshot repository with the
starting commit. Checkpoint bundles from a shallow clone then import, a
stage's own commits never make a bundle carry the source's history, a
restore into a fresh sandbox fetches the base again and applies the run's
commits, and a fork carries the base with its checkpoints.

When a successful GitHub-target run ends, the server pushes its final
commit from the snapshot repository to fabro/run/<id> with its own write
credentials, then, when the run changed files and asks for one, records
the pull request request for the existing creation supervisor. A failed
push or request is a warning notice on the run. The manual pull request
endpoint shares the request step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:00:26 -04:00
Scott Werner
3c62d851fe Retry a finished run from the start
Retry forked the source run at its last checkpoint and reran the failed
stage. It now creates a new run from the source's saved spec and starts
the workflow from the beginning in a fresh workspace, as retry did
before the Petri cutover. The new run records `retried_from` and no
`fork_source_ref`.

Retry no longer needs a checkpoint, a retained workspace, or a published
run branch, so it works for any terminal run that is not archived. To
continue from where a run stopped, fork it at a checkpoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:14:45 -04:00
Scott Werner
c6464fb646 Restore Daytona container runs and configured resources 2026-09-28 23:02:34 -04:00
Scott Werner
d26b2324f0 Merge remote-tracking branch 'origin/main' into codex/restore-configured-artifact-storage
# Conflicts:
#	lib/apps/fabro-cli/tests/it/scenario/artifacts.rs
2026-09-28 16:27:30 -04:00
Scott Werner
7f55dd87b5 Harden artifact capture writes
- The store checks that captured bytes match their digest in every build,
  hashing on a blocking thread, and the upload handler relies on that
  check instead of hashing a second time.
- Capture bytes travel as `Bytes` from the hooks through the client and
  the store, so uploads and retries share one buffer.
- The artifact writer takes the run ID from the hooks, so objects are
  stored under the run their records name.
- Concurrent captures of the same file and content wait on one another,
  so the file is uploaded and recorded once.
- When a record append fails, the hooks re-read the run's captures and
  treat a record that did land as done, so a lost response does not
  record the capture twice.
- An upload that finishes after its run was deleted removes itself,
  instead of leaving an object nothing references.
- Listing a run's stage artifacts skips everything under `captures/`, so
  an unexpected object there cannot fail the listing or the ZIP.
- The capture record derives its `digest` key from its source instead of
  storing it twice, still writing and checking it on the wire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 14:30:36 -04:00
Scott Werner
ead2ca53b8 Keep local artifacts under the storage directory
Servers have always moved a local artifact store under the storage
directory at startup, whatever `local.root` said. Browser-wizard installs
write `local.root = "<storage>/objects"`, so honoring that root would move
their store and hide every artifact already written, with nothing to
migrate it. Restore the storage-directory override for local roots and
leave honoring custom roots to a change that migrates existing objects.

Installer metadata still goes through the override, so it lands where the
server reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 14:30:36 -04:00
Scott Werner
c56f1173c9 fix(ci): prepare Docker images for nightly release tests 2026-09-26 15:24:51 -04:00
Scott Werner
2e500b0e2f Simplify artifact capture writer and storage helpers
- The artifact writer takes the digest the hooks already computed, so
  captured bytes are hashed once on each side, and the dead integrity
  error goes away.
- The writer is a required part of HooksSpec, not an optional field on
  RunRequest, so a run with capture globs always has a writer and the
  no-writer error goes away.
- ArtifactStore routes put/get and the capture methods through shared
  put_at/get_at helpers.
- The upload handler parses the digest with parse_blob_hash_path before
  any store reads, and builds its size-limit message from the constant.
- The default local artifact root comes from one helper used by both
  config resolution and the storage-dir override.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:32:13 -04:00
Scott Werner
a1a98c69d0 fix: address review of the in-process sandbox providers
- Keep plugin-era Daytona lease fingerprints: read only DAYTONA_API_URL and
  DAYTONA_ORGANIZATION_ID (no URL alias, no placement target), and stop
  forwarding DAYTONA_SERVER_URL and DAYTONA_TARGET to the worker.
- Take the Docker fingerprint and network from this process's DOCKER_HOST,
  the endpoint the Docker client actually connects to; make the provider
  configuration's fields private.
- Return an error instead of panicking when Petri supplies no Host registry.
- Run deletion reads the Daytona key only for a Daytona run, and a forced
  or restarted delete goes on when the secret store fails, as it does for
  every other prune failure.
- Stop putting DAYTONA_API_KEY in the worker's environment; the worker reads
  it from the vault. Give the worker's Daytona client the shared HTTP client.
- Fork, rewind and retry no longer read the vault: a fork acquires no sandbox.
- Remove the dead worker plugin forwarding and document that runs execute
  only on the built-in providers.
- Build every Petri runtime through providers::standard_runtime or
  bare_runtime, with a Clippy lint against Runtime::standard/bare.
- Share the Docker require-or-skip policy in fabro-test, tighten the Host
  scope assertion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:31:37 -04:00
Scott Werner
71b08b61a1 Preserve custom local artifact roots when overriding storage 2026-09-25 11:56:59 -04:00
Scott Werner
8a09e8fa08 refactor: tidy the in-process sandbox provider wiring
Load the Daytona key for fork and prune through one AppState method
instead of two copied vault reads, and pass the sandbox configuration
into runtime_spec rather than building it and overwriting it. The
worker reuses the CLI's process_env_var lookup.

Share one Docker availability check and the backend-requirement
variable through fabro-test, drop the built-in plugin path and pin
constants nothing reads any more, and let enabled_plugins() exclude the
bundled kinds itself. Refresh the comments and the spawn_env test that
still described built-in plugins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:29:29 -04:00
Scott Werner
6ac6d5495f fix: run built-in sandbox providers in process
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>
2026-09-24 17:14:47 -04:00
Scott Werner
a1926c8632 Restore configured artifact storage for workflow captures 2026-09-24 13:53:46 -04:00
Scott Werner
00984ce241 build: track Lithos git dependencies on branch main
Every lithoscomputer git dependency (sandbox-driver, pebble, petri,
lithos-llm, twins) now uses `branch = "main"` instead of an exact rev,
matching the libraries, so the workspace resolves one Cargo source per
repository. Cargo.lock is the single place the commits are chosen; move
one with `cargo update -p <crate>`.

The lockfile keeps every commit except sandbox-driver, which moves from
583a164 to b30203c: Daytona removed its paginated sandbox listing, and
b30203c lists through cursors instead (it also moves the driver's
daytona-sdk-rust dependency to 0e69058). The Daytona auth-probe test
mocks now serve the cursor endpoint the driver calls.

CI reads the sandbox-driver commit for the plugin install from the
lockfile through cargo metadata instead of from Cargo.toml.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 12:40:52 -04:00
Bryan Helmkamp
d3d6697aa1
Merge origin/main and pin sandbox-driver 583a164, Pebble eb08b70, Petri 23f78a2
main (#891) upstreamed the Pebble sandbox adapter and deleted
fabro-pebble-sandbox, so Fabro now builds Pebble's sandbox-driver feature
beside Petri's crates and must hold one sandbox-driver copy. The three
pins move together: sandbox-driver to its main after #61 (host
attach-by-path, merged), Pebble to pebble#27's head after it moved its
own sandbox-driver pin to the same revision, and Petri to petri#36's
head, which carries both bumps.

Resolution: Cargo.toml keeps main's shape with the three revisions
rewritten; Cargo.lock regenerated from main's copy and holds one copy of
each library.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 07:50:57 -04:00
Bryan Helmkamp
baf32b70a7
Attach to a run's host sandbox with the worker's managed record
A run's host sandbox is a managed directory the worker recorded, with
Petri's labels, in the host registry inside the run's Petri directory.
The server reached it only by designating the directory again: a handle
with no record, so no labels and no ownership check, unlike Docker and
Daytona where `petri.run` is checked on every attach.

The server now observes the run's registry (`HostProvider::
observe_registry`, read and never written, so the worker stays its only
writer), resolves the directory to its record by path
(`attach_directory`), and runs the same `petri.run` ownership check as
on Docker (`OwnedProvider::check`). The handle refuses every lifecycle
change, so starting, stopping, and deleting stay the worker's, and a
stopped sandbox's retained workspace is usable through it; the access
paths therefore return a host handle as attached instead of activating
it. `ProviderAccess` carries the storage root the registry is found
under; a directory no registry records (a run older than this driver, a
caller without a storage root, a pruned Petri directory) is designated
again as before, without labels, for the read paths only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 17:44:48 -04:00
Bryan Helmkamp
45d94ce711
Use pebble's sandbox-driver adapter and delete fabro-pebble-sandbox
`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>
2026-09-21 17:42:15 -04:00
Bryan Helmkamp
968c46d69b
Settle an in-process run at Petri's finish, not at its terminal record
The in-process Petri path settled the run's in-memory managed run once
Fabro's terminal lifecycle record was stored, after the engine returned.
GET /runs/{id} reads the stored summary, which the projector ends at
Petri's own `run.finished` record, a moment earlier, so a delete issued
the moment the run read as ended could reach the delete precheck, which
prefers the managed run, while it still said running, and was refused
with 409 "cannot remove active run". Against the real engine the window
hit eight times in thirty.

The worker path settles its run at the worker's records endpoint, ahead
of the store (#888). The in-process run now settles at the same record
through the run store it executes over: a store whose coordinator
appends settle the managed run at the `run.finished` record before the
record reaches the store and the projector's signal, with the same
finish mapping and settle the worker path uses. The settle is in memory
only. The terminal lifecycle record stored once the engine returns
refines the status and error and ends the run's live state as before,
and stays the settle of a run that ends without an engine finish.

The prune scenario no longer waits for the managed run to settle before
its delete: the wait guarded only this window.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 12:08:48 -04:00
Bryan Helmkamp
81b7427add
Merge pull request #888 from fabro-sh/settle-worker-run-on-terminal-record
Settle a worker's run at Petri's finish, not at the worker's exit
2026-09-21 08:08:31 -04:00
Bryan Helmkamp
2b30689e77
Settle a worker's run at Petri's finish, not at the worker's exit
A worker-backed run's in-memory managed run settled only when the
worker process exited. GET /runs/{id} reads the stored summary, which
the projector ends at Petri's own `run.finished` record, a moment
before the worker stores Fabro's terminal lifecycle record and exits.
The delete precheck prefers the managed run, so a delete issued in that
window was refused with 409 "cannot remove active run". Against a real
worker the window hit about six times in ten.

The server sees both records before they are stored: `run.finished` on
the coordinator log through the worker's records endpoint, and the
terminal lifecycle record through the platform-records endpoint. The
managed run now settles at either, ahead of the store, so the view
never reports the run ended while the managed run still says running.
The stream follower no longer reopens a settled run with the records
that precede its terminal one, and the worker's exit keeps the settled
status: it reaps the process, records a missing terminal record as it
did, and takes the store's status only when the store ended the run
differently. The mapping from Petri's finish to the run's status is the
projection's own, shared with its fold.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 03:49:44 -04:00
Bryan Helmkamp
1fda9633e4
Bind the run's resolved goal into Petri's check
An intent's goal override, and a `[run.goal]` layer, reached the run's
display graph and its settings but not Petri's check, so the agent stages
executed with the workflow's own goal while the run showed the override.
The launch now carries the run's resolved goal (the settings' inline
`run.goal`, layered as the create path layers it) as Petri's
`petri.launch_goal` compile variable, which Petri binds over the bundle's
`[run] goal` and the graph's own `goal`, so admission's frozen plan carries
the goal the run shows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 03:48:35 -04:00
Bryan Helmkamp
b5c63474e0
Qualify the terminal check: the scenario already imports the engine's RunStatus
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 16:27:02 -04:00
Bryan Helmkamp
c4086f1804
Name the terminal check directly in the managed settle wait
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 16:16:06 -04:00
Bryan Helmkamp
9a93bbcfbd
Wait for the managed run to settle before the prune scenario deletes it
The stored view reports a Petri run ended as soon as its own run.finished
record is folded, which is before the server stores the terminal
lifecycle record and settles the managed run in its map. The delete
precheck reads that map, so a delete sent as soon as the API reports the
run ended can be refused as active instead of by the held lease. The
scenario now waits for the managed run to settle, through a test-support
accessor for its status, before it deletes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 16:03:20 -04:00
Bryan Helmkamp
4216364e92
Split the projector into the pass, its ordering rule, its stream and its wake-up
projector.rs mixed the commit rule with things that are not the pass:
the ordering rule and its tests, the in-process signalling store, the
stream table's rows and reads, and six readers documented "for a test".
projector/mod.rs now holds the pass alone; order.rs, signalling.rs and
stream.rs hold the rest; the test readers (rebuild, stored_projection,
stored_stream, stored_platform_records, event_json) live in test_support
behind the test-support feature, as the test-support boundary rule asks,
and the two test callers reach them there. The fault injection and the
cache's test-only reader are gated the same way, so neither ships.

pass() reads as three steps: at_head, read_new_events (a NewEvents
struct in place of a tuple) and stream::stream_rows, which rebuild
shares instead of repeating the fold loop. PassReport::skipped and
PassReport::contended replace the zero-filled literals.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 15:10:03 -04:00
Bryan Helmkamp
1f6e53c949
Share one poison-tolerant lock helper from fabro-util
Seven crates' files each carried the same four-line lock function that
recovers a poisoned mutex. fabro_util::sync::lock is that function, once;
the copies in fabro-petri and fabro-server are gone. fabro-template's
helper panics on poison instead, a different policy, and is left as is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 14:47:46 -04:00
Bryan Helmkamp
c264a3567c
Settle the managed run as soon as its terminal record is stored
The in-process Petri path persisted the run's terminal lifecycle
record, settled the projector, aggregated usage, and only then settled
the managed run. GET /runs/{id} reads the stored summary, so it reported
the run as ended while the delete precheck, which prefers the managed
run, still saw it running and refused the delete as active. The prune
scenario hit that window about once in thirty runs. Settle the managed
run right after the record is stored, before the view catches up.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 18:56:48 -04:00
Bryan Helmkamp
399aef8111
Keep the Daytona key rendering out of the test's assertion messages
CodeQL read the assertion messages as a log of the credentials'
Debug output. The test proves that output never holds the key, so the
messages added nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 18:05:01 -04:00
Bryan Helmkamp
6082f82950
Fix the gate findings in the prune change
Clippy's absolute-paths lint on the rendered prune error, the sync
directory reads the prune test documents, and a redundant rustdoc link.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 15:02:20 -04:00
Bryan Helmkamp
c7aa50c943
Delete a run's sandboxes through Petri's lease ledger
Run deletion called the driver's `provider.delete(id)` under the run's
`petri.run` scope, a delete of Fabro's own over a sandbox whose lease
record Petri owns. It now goes the way `petri sandbox prune` goes:
`fabro_petri::prune` builds the run's Petri runtime over the server's
store (the run key, the run directory, the sandbox backend) and calls
Petri's prune, which opens the run for writing, checks each lease's
provider fingerprint, writes the delete intent and the tombstone beside
the run's other records, and lets each provider remove its managed
workspace, a host workspace included.

A run a live process holds answers 409 unless the delete is forced; a
lease Petri could not prune answers 409 with the problem text, or is
warned and skipped under force or a delete that already started. The
server drops the worker's handles before the prune, on the store
instance the prune opens, so the lease a stopped worker held is released
first. The projection reads only the coordinator and execution logs, so
the resource records change nothing it reports.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 14:54:03 -04:00
Bryan Helmkamp
809b3891b5
Forward every configured sandbox plugin to the Petri worker
The worker's environment carried only the host, Docker and Daytona plugin
variables from the server's own environment, so a run on a third-party
provider kind never learned where its plugin was, although the server
kept `[server.sandbox.providers.<kind>]` plugin settings for its own
attach. The launch spec now derives `PETRI_SANDBOX_<KIND>_PLUGIN` and
`PETRI_SANDBOX_<KIND>_SHA256` from every enabled kind's plugin settings,
and `PETRI_SANDBOX_PLUGIN_DEV=1` when any of them sets `dev`, set after
the allowlist so the settings win over an ambient variable of the same
name and the allowlist stays the fallback.

The server's default plugin binary name is `sandbox-driver-<kind>`, the
name Petri looks up, since one executable serves both sides.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 14:45:42 -04:00
Bryan Helmkamp
956feda0ca
Fix the gate findings in the sandbox tests
The grep test reads paths as the driver reports them for a resolved
absolute path; the local preflight check test gives the manifest a
source directory that exists; the Docker attach scenario accepts that
the in-process app has no daemon record for the run-tools client an Ask
Fabro turn builds after the sandbox attach, and asserts the turn got past
the sandbox. The inventory's lazy connection is boxed for clippy's
variant-size lint.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 14:07:52 -04:00
Bryan Helmkamp
2a4f2aa718
Test the server's attach to the container Petri created
A server scenario runs a command workflow on Docker through Petri's
plugin (skipped without the plugin or a daemon), then reaches the
container without Petri: the sandbox tab describes it under its
`petri.run` label, Run Files writes, lists and reads a file in its
workspace after starting the stopped container, a preview URL opens to a
port in it, and an Ask Fabro turn runs against it through the OpenAI
twin. A unit test attaches through the ownership seam with a scripted
provider: the run's own container attaches, another run's and one that
carries only Fabro's retired `sh.fabro.*` labels are refused as not
owned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 14:00:56 -04:00
Bryan Helmkamp
f054082f86
Delete fabro-sandbox
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>
2026-09-19 13:57:30 -04:00