Commit graph

17 commits

Author SHA1 Message Date
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
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
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
623e8e1c25
Speak the driver's exec vocabulary instead of mirroring it
Fabro kept its own ExecResult, streaming request and result, output
capture stats, stdio process types, and an Error::Exec variant, each a
field-for-field copy of a sandbox-driver type with a translation layer
between them. Every command a tool, a stage, or a hook ran crossed that
layer twice.

The driver's types are now the ones fabro uses. SandboxExec applies
fabro's policy to an ExecSpec (stop grace, the run's working directory,
the explicit-env filter, the Bash helper's BASH_ENV blank winning over a
caller value) and returns the driver's ExecResult and
ExecStreamingResult as they are. Callers that stream build an ExecSpec
and ExecControls; the buffered exec_command keeps its signature.
ExecResultExt adds fabro's reading of a result: the event-facing
duration, the exit code only when the command exited on its own, the
redacted output tail, and the ExecFailure a non-zero exit becomes. The
three-way termination collapse the run events use lives in one function,
command_termination, called where events are built.

Error::Exec and the git-shaped stderr hint table are gone; a failed
command is the driver's ExecFailure, whose Display carries the label and
the classified metadata and never the raw output. OutputCaptureStats
moves to fabro-agent, whose tool output accounting it belongs to, and
converts from the driver's CaptureStats at the exec boundary. The stdio
process the ACP transport drives is the driver's own, so the cancel-token
bridge and StderrCollector go too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 16:11:45 -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
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
Bryan Helmkamp
3510d5081d
Implement the lithos CredentialProvider trait directly
fabro-auth defined its own `CredentialSource` trait beside the lithos
`CredentialProvider`, with a parallel `ResolveError` and an adapter between
them, because lithos had no way to ask which providers a store can serve
right now. It does now: `credentials::readiness`, `ClientBuilder::build_ready`,
and `CredentialError::Unusable`.

- The vault, SQL vault, API-key, and extra-headers stores implement
  `CredentialProvider` directly. Material that is present but unusable (an
  expired token with no refresh, a wrong-typed vault entry, a header secret
  that did not resolve, a store read failure) is `CredentialError::Unusable`
  with the operator-facing reason; its `Display` replaces
  `auth_issue_message`. `is_configured` is the cheap presence check.
- `fabro_llm::build_client` calls `build_ready`; `FabroClient::auth_issues`
  carries `CredentialError`. `fabro_llm::configured_providers` replaces the
  per-store `configured_providers` method.
- `CredentialSource`, `ResolvedCredentials`, `lithos_credentials`,
  `ResolveError`, and `auth_issue_message` are deleted. Twenty files that
  held `Arc<dyn CredentialSource>` hold `Arc<dyn CredentialProvider>`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 09:16:39 -06:00
Bryan Helmkamp
6e874aff5b
Retire the fabro Sandbox trait for one concrete RunSandbox
Fabro carried its own Sandbox trait long after every implementation
became a thin layer over the sandbox driver: one production type
implemented it, a delegation macro forwarded it, and each consumer crate
kept hand-written fakes of its thirty methods for tests. The trait
existed to be mocked, and the mocks pinned behavior that no provider
had — canned walk listings that ignored the traversal root, opaque
provider paths, activation failures with no lifecycle behind them.

There is now one sandbox type. RunSandbox keeps fabro's semantics — path
resolution against the run's working directory, the Bash exec policy,
git setup and push, credential refresh — as inherent methods over the
driver's exec, filesystem, search, and git facets, and every consumer
takes Arc<RunSandbox>. The directory, grep, and walk types are the
driver's own, re-exported from fabro-sandbox. The exec policy reports
the provider's measured duration rather than its own clock.

Tests script a sandbox through fabro-sandbox's MockSandbox: a struct of
fields (seeded files, the result every command returns, the platform,
a runtime directory) that hands out a RunSandbox over the driver's
scripted doubles and reads back what the code did — commands, timeouts,
environment, term stops, writes, deletes, existence probes. The
hand-written fakes in fabro-agent, fabro-acp, fabro-hooks,
fabro-workflow, and fabro-server are gone; the one wrapper a git
integration test still needs sits at the driver level, hiding a path
from a real Host sandbox. The refresh-ahead loop takes the refresh as a
closure so its schedule is tested without a sandbox at all.

The driver pin moves to the testing-crate stack head, which gained the
double behavior these ports needed: retention caps on scripted output,
canned walks narrowed to the requested base, upload and download on the
memory filesystem, and recorders for deletes, existence probes, and
term stops. One test that modelled a provider handing back opaque object
paths from a walk is removed: the driver contract has no such thing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 00:14:42 -06:00
Bryan Helmkamp
82bcafcfca
Port fabro-workflow, hooks, validate, and store to the lithos types
Workflow LLM handlers build lithos requests, bill from lithos usage and
cost, and classify failures from lithos `ErrorKind`. Model resolution and
fallback use the fabro-llm selection and catalog helpers. Validation
rules read the lithos catalog, and store fixtures use the new
`BilledModelUsage` shape.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 17:26:57 -06:00
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
6226c8c517
fix: address env interpolation review findings
Restore the documented SDK env credential facade without reintroducing run fallback behavior. Fail closed on GitHub permission resolution, require worker storage at the CLI boundary, and align interpolation names and generated docs.
2026-07-28 17:30:50 -04:00
Bryan Helmkamp
f0a7423b51
refactor(config): stop resolving {{ env.* }} in interpolated config
The process environment is no longer a configuration source. `{{ vars.NAME }}`
(non-sensitive, server-stored) and `{{ secrets.NAME }}` (vault-backed) cover
both cases, and reading the worker's ambient environment made a run's inputs
depend on how its process happened to be launched.

`Namespace::Env` is kept but wired to nothing, so `{{ env.NAME }}` still
parses and fails with a message naming its replacement rather than reaching
a consumer as literal text. `ResolveCtx::with_env` is gone, so no call site
can opt back in.

Two long-standing warts were env-only and go with it:

- `InterpString::resolve_or_source`, the "fall back to the raw template
  source on failure" path, which let an unresolved token reach a sandbox or
  the GitHub API as literal `{{ ... }}` text. Its own comment noted it was
  slated for hard-error semantics.
- `RunEnvironmentSettings::resolve_env`'s matching source fallback for
  env-only values.

Both carried `#[expect(clippy::disallowed_methods)]` escape hatches. Every
run-boundary resolver — sandbox env, prepare steps, MCP transports, GitHub
permissions, Slack channels, run goal files, provider extra_headers — now
fails closed instead.

Hooks lose their `allowed_env_vars` allowlist, `resolve_header`, and
`HeaderResolveError` along with the `E: Env` generic threaded through the
executor. They keep `{{ vars.* }}`, which `RunSettings::substitute_variables`
already substitutes server-side at run creation.

`allowed_env_vars` is removed from the OpenAPI spec and the generated
TypeScript client. The docs example showing `{{ env.* }}` in
`[server.slatedb.s3].bucket` was already wrong — that field is a plain
String and never interpolated — and is now a literal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 21:09:35 -04:00
Bryan Helmkamp
e54fef760a
refactor(auth): remove EnvCredentialSource and make the run vault required
`EnvCredentialSource` resolved provider credentials from the process
environment. It had no production entry point of its own — it was only
ever reached as the `None` arm of an `Option<Vault>` in three places:
`build_llm_source`, `configured_providers_for_start`, and
`configured_providers_from_process_env`.

That optional vault is not a state the product can be in. Every run has a
server behind it, the server always spawns workers with `--storage-dir`
(`worker_runtime.rs`), and `SqlVaultCredentialSource` backs both the
server and the CLI. So the fallback only served to silently degrade
credential resolution to whatever the worker process happened to have in
its environment.

Make the vault required across the run path — `RunOptions`,
`StartServices`, `build_llm_source`, `tool_secrets_from_configured_sources`,
`vault_token_lookup`, and the CLI GitHub helpers — so the invariant is
enforced by types rather than assumed. A worker spawned without
`--storage-dir` now fails with a clear message instead of quietly
continuing without a vault.

`configured_providers_from_process_env` had no callers at all and is
deleted. `AgentApiBackend::new_from_env` was public but only ever called
from its own tests; it is deleted too.

Test-only credential sources move to a feature-gated
`fabro_auth::test_support`, wired through dev-dependencies so they never
link into production builds. The CLI worker tests now pass
`--storage-dir`, matching what the server actually does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 20:36:54 -04:00
Bryan Helmkamp
e15defe4c5
refactor: centralize agent profile tool configuration 2026-07-24 14:06:29 -04:00
Scott Werner
47bc772f7b refactor: organize crates into three layers 2026-07-23 17:59:34 -04:00