Commit graph

187 commits

Author SHA1 Message Date
Scott Werner
1aaca98ea4
Merge pull request #845 from fabro-sh/codex/cli-workflow-target-selection
Select CLI workflow sources and run targets independently
2026-09-12 16:38:01 -04:00
Bryan Helmkamp
7879e6d223
Merge pull request #856 from fabro-sh/brynary/run-git-identity
Resolve one Git identity per run and inject it into every workflow command
2026-09-12 14:23:30 -06:00
Scott Werner
86bcc8f128 Simplify CLI repository selectors and add workflow shorthand 2026-09-12 14:18:34 -06:00
Scott Werner
94c7159ef8 Preserve shared target inference with explicit CLI targets 2026-09-12 11:54:48 -06:00
Scott Werner
a9f28828a6 Adapt workflow target selection to sandbox provider kinds 2026-09-12 11:48:21 -06:00
Scott Werner
321a190315 Share one supervised process runner between server and CLI Git
The CLI's native Git runner and the server's run_git_plan each hand-rolled
the same mechanics: kill-on-drop, a wall-clock timeout, output capture,
and (only in the CLI) process-group teardown, bounded capture, and
cancellation. Add fabro_proc::SupervisedCommand, which owns stdio, the
process group, the timeout, cooperative cancellation, and bounded
capture, and put both runners on it. The server gains group teardown on
timeout, so helpers a stuck clone or fetch spawned no longer outlive it;
the CLI keeps discarding output on failure and gains nothing but less
code. The hardened -c overrides become one named list in the CLI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 11:48:21 -06:00
Scott Werner
7c97ba1b70 Move the run-driven remote workflow test to cmd/run.rs
remote_workflow_run_starts_once_create_leaves_submitted_and_failures_do_not_refetch
lived in cmd/create.rs but drove fabro run in four of its five
iterations and asserted the start call, which is fabro run's contract.
Split it: cmd/create.rs keeps the single create invocation that must
leave the run submitted without starting it, cmd/run.rs owns the
run-driven success and failure iterations, and the workflow and remote
repository fixtures move to the shared command test support module.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 11:48:21 -06:00
Scott Werner
f15972f8f2 Keep Ctrl-C owned for the whole native Git command phase
owned() polled tokio's ctrl_c() only for the duration of one Git
acquisition. On Unix that listener permanently replaces the default
SIGINT disposition, so once acquisition finished nothing handled Ctrl-C
and fabro create, fabro run --detach, and fabro run before attach
silently ignored it while waiting on the server. Introduce an
Interruption handle that the command entry points create from the run
arguments: it installs a listener only when --workflow-git or
--target-git is in play, guards create (and start for fabro run) as one
phase, and tracks owned Git tasks so interruption waits for their
cleanup before returning. attach keeps installing its own listener.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 11:48:21 -06:00
Scott Werner
39694ec5e8 Acquire the remote workflow before observing a local Git target
For --workflow-git selections, target resolution ran before the remote
workflow ref was verified to exist. On Docker and Daytona environments a
path target that is a GitHub checkout is observed via
observe_git_run_target, which may silently push the attached branch, so
a typo in --workflow-ref produced a remote side effect with no run
created. Resolve the remote workflow after parent and environment
validation but before target observation, restoring the pre-existing
workflow-then-target order, and cover it with a caller checkout whose
unpushed branch must stay unpublished.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 11:48:21 -06:00
Scott Werner
0998f60624 Contain the selected workflow file instead of pre-walking the checkout
Remote acquisition walked the entire depth-1 checkout and failed on any
symlink that dangled or resolved outside the root, even when the link
was nowhere near the selected workflow. Submodule-style dangling links
and links into the host are common in workflow repositories and made
--workflow-git fail where the same commit collected fine locally. The
bundler already root-checks every file it opens; the only unchecked
reads were the selected TOML (or a graph selector's sibling TOML) during
location resolution. Check those in collect_workflow_versions and drop
the O(repo) walk. walkdir stays a dev-dependency for the dump tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 11:48:21 -06:00
Scott Werner
bf41ad8a24 Classify workflow refs once when parsing --workflow-ref
RemoteWorkflowRevision::parse already enforced which ref namespaces a
value may name, but RefCandidates re-derived the branch/tag split from
the string and treated everything under refs/ that was not refs/heads/
as a tag, relying on an invariant checked in another file. Parse now
yields Branch, Tag, or Name variants and resolution matches on them
directly, replacing the Option/Option candidate encoding and its
impossible (None, None) input state.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 11:48:21 -06:00
Scott Werner
3163e71236 Reject unusable remote default branches with a --target-branch hint
Target resolution accepted any remote default HEAD that passed the ref
selector grammar, then failed inside GitRunTarget::validate with a
generic branch-grammar error when the default branch was something like
heads/main or tags/release. Validate the default branch as a working
branch name up front and point the user at --target-branch, since they
passed no branch at all.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 11:48:21 -06:00
Scott Werner
bba5e2b20b Run native Git metadata lookups from an owned scratch repository
ls-remote ran in the caller's working directory while fetch, cat-file,
checkout, and rev-parse ran inside the temporary checkout, so the lookup
honored repository-local config (url.*.insteadOf, credential.*, http.*,
core.sshCommand) that the fetch never saw, and a broken .git in the
caller's directory failed the lookup outright. Initialize the scratch
repository first and run every command from it, so all steps see the
same configuration. Target resolution uses a short-lived scratch
repository of its own and still creates no checkout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 11:48:21 -06:00
Scott Werner
d87f94bead Only kill the Git process group when a command fails
The native Git runner SIGKILLed its child's process group after every
command, including successful ones. Git spawns credential-cache--daemon
into the same group, so each ls-remote or fetch destroyed the cache it
had just warmed and every later command re-ran the full helper chain.
Kill the group only on timeout, cancellation, or failure, and drop the
redundant kill/wait on an already-reaped child.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 11:48:21 -06:00
Scott Werner
e99d3cebe8 Disable fsmonitor during native Git workflow checkout
The hardened -c list for CLI-owned Git acquisition disabled hooks, LFS
filters, submodules, and maintenance but omitted core.fsmonitor, so a
user's global fsmonitor hook (or the builtin daemon) still ran during the
temporary checkout. Match the sandbox's hardening and cover it in the
hooks/filters test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 11:48:21 -06:00
Scott Werner
b0ed545092 Simplify CLI workflow source and run target selection
Remove validation that ran twice on the same inputs: clap already
enforces the flag co-occurrence rules, and the native Git layer no
longer re-checks selectors, branch names, refs, and commit SHAs that
selection parsing already validated. Remote selector shape rules now
delegate to the shared WorkflowPath validator.

Reuse fabro_proc for the process-group kill and liveness probe instead
of calling nix directly, dropping the extra nix features. Fold the
duplicated branch/tag candidate derivation into one RefCandidates type,
label each Git command explicitly instead of inferring it from argv,
hoist the duplicated workflow resolver call in create_run, and merge the
two directory target arms now that the default is just the caller path.

Share the run-argument parser and workflow/commit fixtures across the
unit tests through a test_support module, drop an integration test that
duplicated one cell of the cross-product test, and make the malformed
slug vectors assert the clap rejection they exercise.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 11:48:21 -06:00
Scott Werner
0e0d986634 Select CLI workflow sources and run targets independently 2026-09-12 11:48:21 -06:00
Bryan Helmkamp
1385030fa4
Cover the run Git identity across sandbox providers
Add a `sandbox_tests!` scenario that initializes a repository inside the
sandbox from a script stage, commits, and prints the author and committer
the commit object carries. It runs on the local host and, when the plugin
executables are on PATH, on the host and Docker sandbox plugins, with
conflicting `GIT_*` variables inherited from the launching shell.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 11:41:20 -06:00
Bryan Helmkamp
ee6576cff7
Resolve one Git identity per run and inject it everywhere
A run now resolves a single author and committer identity once, after its
GitHub credentials are selected and before anything can commit, and uses it
for every commit it creates. Resolution order: a complete explicit
`run.git.author`; the run's GitHub App bot account
(`<slug>[bot] <id+slug[bot]@users.noreply.github.com>`); the authenticated
user of the run's PAT; the generic `Fabro <noreply@fabro.sh>`. A partial
explicit author overlays the fields it supplies. Only the selected
credential is consulted; a failed lookup is a setup error. A standalone
installation token falls back to the generic identity with a warning.

The resolved identity is carried on `RunOptions` and `EngineServices`,
recorded as a `git.identity.resolved` event and `RunProjection.git_identity`
so resume reuses it, and exposed through the run state API. Engine
checkpoints and metadata commits read it through `RunOptions::git_author`.
Every workflow execution path receives it as `GIT_AUTHOR_NAME`,
`GIT_AUTHOR_EMAIL`, `GIT_COMMITTER_NAME`, and `GIT_COMMITTER_EMAIL`, applied
last so it wins over inherited host variables and `[run.environment]`
entries: prepare steps, command stages, native agent shell tools, and ACP
launches. The identity is injected even without a Git origin, and the old
local `git config user.*` write is removed.

fabro-github gains `GET /user` and `/users/{slug}[bot]` lookups with mocked
tests for success, unauthorized, malformed, and transient cases. Real-Git
integration tests commit in the primary checkout, a clone, and a fresh
repository under conflicting local config, `[run.environment]`, and host
variables, and prove concurrent runs do not leak identities. CLI workflow
tests cover host script stages and ACP launch env through `fabro run`.

Docs and generated option metadata now describe the credential-derived
defaults instead of the stale `fabro`/`fabro@local` values.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 11:35:46 -06:00
Scott Werner
21a5e5b86f Simplify run creation to registered workflow versions 2026-09-12 11:04:15 -06:00
Scott Werner
1e8e1c9a30 Fix child target inheritance and inline workflow path validation 2026-09-12 10:59:07 -06:00
Scott Werner
5e79285a43 Honor the workflow's configured run.scm repository in target derivation
The replaced manifest builder resolved the run's repository identity from
the workflow's run.scm settings before falling back to the checkout's
origin. The new standalone derivation always used the checkout's origin,
so a fork checkout of a workflow that names its upstream repository
silently targeted the fork and pushed there.

Read the run.scm layer from the resolved workflow.toml and project.toml
(or from the inline workflow.toml bytes) and pass it through both the CLI
and the standalone run-tool adapter. When the configured repository is
not the checkout's origin, nothing can be proven about it, so derivation
now fails with a message naming that mismatch instead of the generic
"push the commit" hint.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 10:58:42 -06:00
Scott Werner
20f3766e75 Derive standalone run-tool targets from the selected environment
Standalone fabro_run_create ignored the environment's provider and always
produced a Git target or failed, so a Local environment with no explicit
target was rejected by admission and a directory without Git metadata
hard-failed, while fabro run derived a folder target and a none target
for the same inputs.

Move the CLI's provider-aware derivation into fabro-manifest as a shared
helper with a typed error, and have the standalone adapter look up the
selected environment and call it. The helper also distinguishes a failed
remote query from an unpublished commit, so an offline ls-remote no
longer reports "push the commit and try again" when the branch is
already on the origin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 10:58:42 -06:00
Scott Werner
b12eb8d84e Harden run-tool target and workflow resolution 2026-09-12 10:58:05 -06:00
Scott Werner
c2d6dc4a92 Align run-tool creation with server admission 2026-09-12 10:57:46 -06:00
Scott Werner
b38279fc44 Apply cleanup review fixes to the run-create path
Deduplicate shared-filesystem capability checks and simplify workflow-source dispatch and types. Move Git observation and local package collection onto spawn_blocking, and flush inline workflow files before collection.

Simplify validated source and input types, derive inline size-limit messages from shared constants, add target schema-parity coverage, and remove dead producer pass-through parameters.
2026-09-12 10:57:46 -06:00
Scott Werner
45741a3e6e Fix RunIntent producer CI failures 2026-09-12 10:57:46 -06:00
Scott Werner
c67c60eeba Create run tools from immutable workflow versions 2026-09-12 10:57:22 -06:00
Scott Werner
d611ef2bb1 Port workflow version creation to Pebble native tools 2026-09-12 10:23:34 -06:00
Scott Werner
ec81170c8a Share workflow packaging results and validation across callers 2026-09-12 10:15:42 -06:00
Scott Werner
e6eb59537a Move the supplied-content packager next to its collector
ServerWorkflowVersionPackager was a pure adapter over
fabro_manifest::collect_supplied_workflow_versions that touched no
server state, yet it lived in fabro-server and was imported from there
by the standalone MCP server and the CLI run worker. fabro-manifest can
depend on fabro-tool without a cycle, so the adapter now lives beside
the collector as SuppliedWorkflowVersionPackager and fabro-server no
longer exports a non-server module for it.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 09:41:20 -06:00