Commit graph

19 commits

Author SHA1 Message Date
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
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
Bryan Helmkamp
ec5faab116
Merge remote-tracking branch 'origin/main' into petri-integration
# Conflicts:
#	lib/apps/fabro-cli/src/commands/run/runner.rs
2026-09-19 15:25:20 -04:00
Bryan Helmkamp
1166caed0b
Give up on an unreachable worker control stream and reap .ft- test daemons
A worker whose server is killed outright retried its control-stream
connect forever at a 5 s capped backoff, so it never finalized, never
stopped its sandbox, and lived until reboot. The worker now tracks the
start of each run of continuous connection failure and gives up after
60 s (10 s once its parent is pid 1), through the existing fatal
control-loss path that interrupts interviews and cancels the run. After
that fatal fires, the runner keeps driving the cancelled pipeline for a
bounded grace so `conclude` can stop the sandbox before the process
exits, instead of dropping the pipeline future mid-flight.

The test harness's stale-daemon reaper only matched `fabro server`
titles bound under `/tmp/.tmp*`, but `TestContext` roots are
`.ft-<label>-*` under `std::env::temp_dir()`, so daemons bound there
were never reaped. The regex now also matches sockets below a `.ft-`
directory component under any parent, and the unit test covers both
roots plus real-looking paths and `.ft-` outside a directory component.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 07:27:55 -04:00
Bryan Helmkamp
b354a2f948
Render the branch, identity, diff and artifact records and re-record the run snapshots
`run events --pretty` reads the flattened `git.identity` fields, and
shows a `run.diff` record as its summary and an `artifact.collected`
record as its path and size. The snapshot filters redact the base commit a
`Branch:` line names. The CLI snapshots now carry the `Base:` line, the
`run.branch` and `git.identity` stream items, the dry run's simulated
response and the two response files a dry run dumps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 18:39:58 -04:00
Bryan Helmkamp
f0b23fe426
Project a Petri run's sandbox instance from its scope records
Petri a5906f6 records where each scope's sandbox ran (`scope.acquired`,
`scope.failed`) and how its lease was released (`scope.released`). The
projection folds the root invocation's records into `Run.sandbox`:
`initializing` from `run.started`, `ready` with the `RunSandboxInstance`
(the provider, Petri's `host` as Fabro's `local`, the provider's id, the
image and snapshot, the working directory) from `scope.acquired`, `failed`
from `scope.failed`; the retention outcome is kept in the fold state, since
the view has no field for it. Ask Fabro reconnect and `sandbox cp`,
`preview` and `ssh` reach the run's sandbox again.

A local reconnect designates the recorded working directory again when the
host provider does not know the id: the provider mints a registry-only id
for a workspace path too long for a path-derived one, and that registry
belongs to the run's worker. The stream listing redacts its items the way
the attached stream does, so a client that pages after a stream sees the
same items.

Pins move to Petri a5906f6 (run format 6, engine log v11, event contract
4). The attach stream snapshot is re-recorded with the new record and a
filter for the host provider's minted ids; `sandbox cp` reads an upload
back through the run's workspace, which is no longer the target folder.
Server scenario tests prove the projected instance on the host and Docker
providers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 15:57:12 -04:00
Bryan Helmkamp
97f74c3ab0
Forward the Docker daemon selection and Daytona credentials to Petri workers
A Petri run's worker launches the sandbox-driver plugins itself, and the
Docker plugin forwards `DOCKER_HOST`, `DOCKER_TLS_VERIFY`,
`DOCKER_CERT_PATH`, `DOCKER_API_VERSION`, `DOCKER_CONFIG` and
`DOCKER_CONTEXT` from the process that launches it. They now cross the
worker's environment allowlist, so the worker's sandboxes go to the daemon
the server uses. The concern that kept them out, the legacy worker's own
Docker client picking up a daemon it was not meant to, is moot: the
legacy executor is being deleted. The same variables pass through the
test harness's isolation, so a developer's daemon selection reaches the
servers tests start.

Daytona's non-secret selectors, `DAYTONA_API_URL` and
`DAYTONA_ORGANIZATION_ID`, cross the allowlist too. The API key stays the
vault's: a Daytona run's worker command carries it the way the GitHub app
key travels, and the Daytona plugin reads it from the worker's process.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 09:10:29 -04:00
Bryan Helmkamp
1e2a205ec3
Carry Petri's sandbox plugin variables into workers and test servers
A Petri run executes in the worker process, which resolves the
sandbox-driver plugins itself. The `PETRI_SANDBOX_*` variables (plugin
paths, checksum overrides, dev mode, the Docker host address and the action
host image) now have `EnvVars` names, cross the worker's environment
allowlist with `PATH`, and pass through the test harness's isolation so a
developer's plugin override reaches the servers tests start and the workers
those servers launch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 21:20:22 -04:00
Scott Werner
012b556367 Remove tests and fixtures tied to retired manifest fields 2026-09-13 08:55:55 -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
6599f7cdc0
Cover the pebble agent loop with workflow-level tests
Each behaviour the pebble backend owes a run now has one test that drives
it through the workflow engine against a scripted OpenAI-compatible model:

- an agent stage under every harness profile (openai, anthropic, claude-5,
  gemini, kimi) writing a file with that profile's own tool spelling, with
  the event sequence, files touched, response, usage, and cost checked; the
  codex vocabulary applies a patch through the OpenAI twin's custom tool
  call, which `TwinToolCall::custom` now scripts
- steering delivered mid-stage, an interrupt with a steer, run cancellation,
  and the executor-enforced stage timeout
- a question answered through the interviewer, a subagent whose events carry
  its parent's session id, and an MCP tool served by a stdio server
- failover to a second provider after a tool ran, continuing the recorded
  conversation without running the tool again
- a failing event sink ending the stage with the sink's error
- Ask Fabro resuming a stored record across turns, with the cursor moved
  past the run's event log when the record's own cursor fell behind
- Docker and Daytona smokes running an agent stage through the provider
  sandboxes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 16:32:52 -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
580bb85f5b
Depend on lithos-llm and the published twin-openai crate
Add lithos-llm as a pinned git dependency and replace the in-repo
`test/twin/openai` crate with the published `twins` revision that
lithos-llm verifies its codecs against. Move the Fabro policy overlay
(`fabro-policy.toml`) into fabro-llm so Fabro owns its own catalog
policy layer.

Drop the twin-openai nextest overrides and CI package filter now that the
crate is no longer a workspace member.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 17:26:57 -06:00
Bryan Helmkamp
ca6d9a46da
Merge remote-tracking branch 'origin/main' into fix/redaction-corrupts-executable-spec
# Conflicts:
#	lib/components/fabro-dump/src/lib.rs
#	lib/components/fabro-store/src/run_state.rs
#	lib/components/fabro-store/tests/serializable_projection.rs
#	lib/components/fabro-workflow/src/billing_rollup.rs
#	lib/components/fabro-workflow/src/run_lookup.rs
#	lib/components/fabro-workflow/src/runtime_store.rs
#	lib/foundation/fabro-api/tests/run_projection_round_trip.rs
#	lib/foundation/fabro-test/src/lib.rs
#	lib/foundation/fabro-types/src/run.rs
#	lib/foundation/fabro-types/src/run_event/run.rs
#	lib/foundation/fabro-types/src/run_projection.rs
#	lib/foundation/fabro-types/tests/run_spec_methods.rs
2026-08-20 20:42:20 -04:00
Bryan Helmkamp
2b095612c8
Address run spec persistence review findings 2026-08-20 20:35:52 -04:00
Scott Werner
3524cd76d4 Generate the blob-field snapshot filters from a field list
The manifest_blob and definition_blob filter entries were copy-paste
twins that had to be edited identically; build them from one loop like
the elapsed-ms filters above so the pattern and placeholder cannot
drift apart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 13:56:19 -04:00
Scott Werner
bf4265e1b8 Unify blob hash vocabulary 2026-08-17 13:56:19 -04:00
Bryan Helmkamp
3421c4f06f
Keep the executable run spec out of reach of event redaction
Two root-cause fixes for the sandbox failure where an inline Dockerfile
came back from the store as `ARG REDACTED` and the Daytona snapshot
build died on the unset variable.

Entropy redaction measures values, not assignment pairs. The detector
matched `NAME=value` as one token, so an uppercase name merged its
charset into a pure-hex value (which alone can never exceed 4.0 bits)
and pushed the pair over the 4.5-bit threshold — then replaced the
whole pair, destroying the name. `find_entropy_regions` now strips an
identifier-shaped `NAME=` prefix before measuring and redacts only the
value, matching the gitleaks layer's `key=REDACTED` shape.

Execution no longer reads redacted content. Every stored event passes
through the redaction sink, and `load_from_store` rehydrated the
worker's RunSpec from the projection folded from those events — so a
redactor false positive silently rewrote the spec the sandbox builds
from (and changed its snapshot identity). The creation path now writes
the exact spec bytes to the content-addressed blob store and records
`spec_blob` on run.created; `load_from_store` loads the spec from the
blob, keeping the event stream authoritative for run identity,
provenance, and event-recorded blob ids. Retry and fork carry the
source run's `spec_blob` forward, so derived runs stop inheriting the
redacted copy. Runs created before the blob existed fall back to the
folded spec.

The projection and every API surface keep serving the redacted fold;
blobs were already stored unredacted (the workflow bundle carries the
same bytes), so this adds no new exposure at rest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 21:44:46 -04:00
Scott Werner
47bc772f7b refactor: organize crates into three layers 2026-07-23 17:59:34 -04:00