Fabro advertised Bash while its three backends implemented three
different contracts: Daytona evaluated commands through `sh`, and
Docker's streaming, stdio, and setup paths used a login shell. Bash-only
syntax silently misbehaved depending on provider and code path, and
login profiles could change PATH and command behavior per image.
Make `bash -c` the enforced interpreter for every command string the
Unix sandbox API accepts, on every production backend and through both
buffered and streaming execution. This selects the interpreter only —
no `errexit`, no `pipefail`, no login mode — so `false | true` still
succeeds and a workflow that wants other semantics writes them into its
own command.
Local resolves `bash` through the worker's PATH (NixOS has no
/bin/bash) and reuses that one executable across all three command
paths. Docker and Daytona require /bin/bash with no `sh` fallback.
Fresh initialization and resume/start now verify Bash through a shared
marker-validating probe before reporting the sandbox usable, so a
missing or non-Bash interpreter fails at the lifecycle boundary with
provider-specific remediation instead of on the first command. The
probe also rejects Bash in POSIX mode, which an image whose `bash` is
really `sh` would otherwise pass.
Sandbox MCP scripts and the detached launch wrapper move under the same
contract; host-side stdio MCP scripts, hooks, and interactive terminals
are separate executors and keep their existing `sh` behavior.
The `shell` tool's name and JSON schema are unchanged across providers;
only its prose now identifies `command` as Bash source.
BREAKING CHANGE: sandbox commands no longer load login-shell profiles,
so environment set in /etc/profile.d/*.sh, ~/.bash_profile, or
nvm/rbenv/sdkman initializers is gone. Move those exports into the
Dockerfile's ENV or the Daytona snapshot image.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cumulative implement + simplify_fable diff recovered from the run's meta
branch (fabro/meta/01KY7YH7RYCJ1BDVTTP96ZA4HV, stage 006 diff.patch).
The run validated this tree clean: cargo nextest (7,007 passed), clippy,
fmt, TS client regen + typecheck, web tests (679 passed), docs check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Document the contract of read_last_file_routing_json (terminal JSON
extraction only; routing validation happens downstream), extract a
shared sandbox_with_file test helper, and drop the misleading
"standalone" wording from the fallback docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add two lint rules so the graph format stops silently accepting
attributes that nothing reads:
- inert_attribute: handler-specific attributes (script, language,
duration, join_policy, max_parallel, output_schema, prompt) placed on
node types that never read them. Attributes read by several handlers
(timeout), resolved for every node (fidelity, retry_policy), or
injectable via model stylesheets (model, reasoning_effort, ...) are
deliberately excluded.
- parallel_branch_inert_attribute: fidelity/thread_id on parallel
branch nodes and fork->branch edges. Branch dispatch bypasses the
fidelity lifecycle, so these are dead letters today; the warning
points at the parallel node, where fidelity does take effect.
Also reconcile the loop_restart docs with actual executor behavior:
taking a loop_restart edge restarts from the target with a fresh empty
context (visit counts preserved), on success as well as failure; the
transient_infra guard applies only to failure crossings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add Poolside as a built-in OpenAI-compatible provider and expose Laguna S 2.1 and XS 2.1 both directly and through OpenRouter. Include vault/env credential registration, secret redaction, live coverage, catalog tests, and user documentation.
## Summary
Provider `extra_headers` previously required values to be typed TOML
tables (`{ env = "X" }`, `{ literal = "Y" }`, `{ vault = "Z" }`). This
PR migrates them to the project's standard interpolation string format:
plain text for literals, `{{ env.NAME }}` tokens for environment
variables, and `{{ secrets.NAME }}` tokens for vault secrets. This
brings `extra_headers` in line with the rest of the interpolation system
and unlocks mixed-segment values like `Bearer {{ secrets.GATEWAY_TOKEN
}}`.
### What changed and why
**Config authoring surface (`fabro-config`):**
`ProviderSettings.extra_headers` changes from `Option<HashMap<String,
HeaderValueRef>>` to `Option<HashMap<String, InterpString>>`. The
`Combine` impl and all re-exports are updated accordingly.
**Catalog layer (`fabro-model`):**
`ProviderCatalogSettings.extra_headers` and
`CatalogProvider.extra_headers` become `HashMap<String, String>` — raw
interpolation source strings. This is required by the crate dependency
direction: `fabro-types` (which owns `InterpString`) depends on
`fabro-model`, so `fabro-model` cannot hold `InterpString` without
creating a cycle. The source string is re-parsed and resolved in
`fabro-auth` at credential-build time.
**Credential resolution (`fabro-auth`):** Both `CredentialResolver`
(vault-backed) and `EnvCredentialSource` (env-only) are rewritten to
parse each header source string as an `InterpString` and resolve it with
a `ResolveCtx` scoped to `env` + `secrets` only. A new
`resolve_extra_headers` helper is shared between the two paths. Vault
resolution uses `vault_token_lookup`, which wraps `vault_get_token` and
maps any non-Token vault entry to `None` — so file and OAuth vault
entries fail closed rather than resolving incorrectly. `vars.*` and
`inputs.*` tokens are not in scope and produce `Unavailable` errors
automatically.
**New error variant:** `ResolveError::Interpolation { provider, source
}` surfaces header resolution failures as diagnosable auth issues. The
inner `source` (an `InterpResolveError`) names only the token namespace
and name — never a resolved value.
**`{ literal = "..." }` guardrail removed:** `HeaderValueRef`
deliberately rejected bare string header values to discourage pasting
credentials. `InterpString` accepts any string. This is an intentional
change; the mitigation is documentation — use `{{ secrets.NAME }}` for
credential-shaped values, not bare literals.
**Redactor registration gap (noted, not fixed here):** Secrets resolved
into provider headers at the credential boundary do not flow through the
run boundary's exact-match redaction registry. Exposure is low (headers
are host-side and outbound-only, never logged), but a follow-up should
thread a registering lookup through `VaultCredentialSource`. A code
comment at the resolution site marks the gap.
### Breaking change
Existing `extra_headers` config using `{ env = "X" }`, `{ literal = "Y"
}`, or `{ vault = "Z" }` table syntax **will fail to parse** after this
change. Users must migrate to the token form: plain strings for
literals, `{{ env.X }}` for env vars, `{{ secrets.X }}` for vault
secrets. A changelog entry is included.
### Plan Summary
- Update `ProviderSettings.extra_headers` → `InterpString` in
`fabro-config`
- Collapse authoring `InterpString` → source `String` in
`provider_settings_to_catalog` (allowlisted `as_source()` call)
- Delete `HeaderValueRef` and its serde/display/parse machinery from
`fabro-model`
- Rewrite both auth resolution paths to use `InterpString::parse +
resolve_with`; add `Interpolation` error variant
- Add `vault_token_lookup` helper for token-only fail-closed vault
resolution
- Update test TOML in `fabro-llm`, builtin catalog comment in
`openrouter.toml`, and all hand-written + generated docs
### Fabro Details
<details>
<summary>Ran 8 stages in 108m 43s for $34.44</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 5m 33s | – | 0 |
| preflight_lint | 5m 59s | – | 0 |
| implement | 43m 23s | $17.34 | 0 |
| simplify_fable | 32m 49s | $13.09 | 0 |
| simplify_gpt | 6m 25s | $4.01 | 0 |
| verify | 13m 58s | – | 0 |
| **Total** | **108m 43s** | **$34.44** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-8; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD. Be sure to use the rust-style-guide skill to help you follow this repo's Rust style conventions.", model="gpt-55", reasoning_effort="xhigh"]
simplify_fable [label="Simplify (Fable)", prompt="@prompts/simplify.md", model="claude-fable-5", reasoning_effort="xhigh"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, timeout="1800s", script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_fable -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Adds **Amazon Bedrock** as an opt-in built-in provider, over Bedrock's
unified **Converse / ConverseStream** API. One codec serves every
Converse-capable family — Claude, Amazon Nova, Meta Llama, Mistral,
DeepSeek, Moonshot Kimi, Z.AI GLM, MiniMax, NVIDIA Nemotron, and OpenAI
gpt-oss — because AWS translates the envelope to each model's native
dialect server-side. Auth is either **AWS SigV4** (the default
credential chain — env / profile / IMDS / IRSA / SSO, resolved per
request so sessions refresh) or a **Bedrock API key**
(`AWS_BEARER_TOKEN_BEDROCK`, bearer). Disabled by default (the Ollama /
OpenRouter opt-in pattern).
This is the redo of #459's original Claude-only `InvokeModel` adapter,
rebuilt on the gateway-refactor seams (#481–#497). @depopry's SigV4
signer, AWS event-stream frame decoder, `BedrockAuth`, the `aws_sigv4`
credential grammar, `AdapterKind::Bedrock`, region-from-base_url, and
the lean-deps decision are preserved and authored by him on the first
two commits; the per-family `BedrockCodec` trait he wrote turned out to
be the crate-wide `Codec` seam in miniature, so the refactor promoted
exactly that shape. The original Claude-only description is preserved in
a comment below.
## What's here
- **`AdapterKind::Bedrock` × `CodecKind::BedrockConverse`** on the
route, plus the `aws_sigv4` credential source (no static secret — the
adapter signs at request time; `fabro-auth` stays AWS-free).
*(@depopry)*
- **SigV4 signer + AWS event-stream `FrameDecoder`** on the lean AWS
stack (no `aws-sdk-bedrockruntime`; transport stays on `fabro-http`).
Re-targeted at Converse's direct-JSON stream frames; the signer resolves
credentials per request. *(@depopry)*
- **`bedrock_converse` codec** — Converse envelope (`system[]`, typed
content blocks, `inferenceConfig`, `toolConfig`), prompt caching via
`cachePoint`, thinking-signature round-trip through `reasoningContent`,
usage mapped onto the disjoint `TokenCounts` buckets,
`provider_options.bedrock` passthrough. Plus the adapter shell and an
event-stream byte loop beside the transport's shared SSE loop.
- **Catalog**: `bedrock.toml` (Claude incl. Fable 5, Nova 2, Llama 4,
Mistral, DeepSeek, Kimi, GLM, MiniMax, Nemotron, gpt-oss — cross-region
inference-profile ids, per-model `billing_policy` so Claude bills
Anthropic-style) and a companion **`bedrock-openai`** provider for
GPT-5.5/5.4 over the `bedrock-mantle` Responses endpoint (pure config
over the existing `openai_responses` codec, zero new code).
- Secrets registry (`AWS_BEARER_TOKEN_BEDROCK`), gitleaks rules for both
Bedrock key formats, the `docs/integrations/bedrock` guide, and live e2e
tests.
## Live verification (confirmed end-to-end against a real AWS account)
Verified on a real Bedrock account (us-east-2, SigV4 + bearer):
- **SigV4 + Converse** — multiple families (Claude, Nova, DeepSeek, …)
via the full settings → catalog → route → adapter → codec path.
- **ConverseStream** — streaming deltas through the workflow engine.
- **Multi-turn tool use** — agent loop with tool calls round-tripping
(no-arg tools included).
- **Multi-model routing** — Claude + DeepSeek pinned in one run through
the single Converse codec.
- **mantle Responses** — `openai.gpt-5.5` answered via the
`bedrock-openai` provider (bearer auth).
The exercise caught and fixed several issues that unit tests (static
creds, mocked transports) could not — see the follow-up commits below.
## Follow-up fixes from live testing (commits on top of the foundation)
1. **Worker AWS env** — the workflow worker scrubs its env to an
allowlist, so SigV4 (which re-resolves from the ambient chain per
request) couldn't work through `fabro run`. The AWS credential-chain
inputs now cross into the worker.
2. **Vault bearer key** — Bedrock was the only key-based provider
missing a `vault:` credential ref, so `fabro secret set
AWS_BEARER_TOKEN_BEDROCK` silently didn't feed it. Now resolves env →
vault → SigV4.
3. **Converse tool-encoding hardening** — a no-arg tool call's
`toolUse.input` is now a `{}` object (Bedrock rejects null), and every
tool `inputSchema` gets a top-level `type: "object"` (strict families
like DeepSeek reject a typeless schema Claude tolerates).
4. **Nova output cap** — `amazon.nova-2-lite` max_output 65536 → 65535
(Bedrock's per-request limit).
Earlier fixes already folded into the foundation commits: the
`aws-config` sleep-impl (default chain panicked) and AWS error-body
decoding (top-level `message`/`Message`/`__type` → proper messages
instead of "Unknown error").
## Manual testing & setup
See `docs/integrations/bedrock` — now documents the non-obvious account
setup that live testing surfaced: the per-Region Anthropic use-case
approval, `aws-marketplace:Subscribe` for third-party models, the Fable
5 / Mythos-class data-sharing opt-in, and the bearer-vs-SigV4 precedence
override for running Converse + mantle side by side.
## Open decision / discussion
- **Model-id naming** — Bedrock rows use dotted ids mirroring Bedrock's
native inference-profile ids (`us.anthropic.claude-sonnet-4-6`,
`openai.gpt-5.5`), which also makes them the wire `api_id`. Third scheme
alongside bare ids and OpenRouter's `vendor/model` slashes. No collision
risk (enforced at catalog build). Open to a uniform scheme if preferred.
- **`BEDROCK_API_KEY` alias** — see the comment thread; the AWS console
hands some users `export BEDROCK_API_KEY=` while the SDK-standard var is
`AWS_BEARER_TOKEN_BEDROCK`. Question of whether to accept both.
## Deferred (named follow-ups)
- **`qwen.qwen3-coder-next`** — omitted pending a verified Bedrock
model/inference-profile id (its fabro id isn't a valid Bedrock
identifier; needs an explicit `api_id`). Re-add once confirmed via `aws
bedrock list-inference-profiles`.
- **Claude Mythos 5** — Anthropic-Messages-only on `bedrock-mantle`
(limited preview).
- **Converse structured output** (`response_format` rejected with a
clear error).
- **`reasoning_effort` on Converse rows** via
`additionalModelRequestFields` (the `bedrock-openai` GPT rows already
accept effort levels).
- **CountTokens** route (`count_input_tokens` returns `None`).
## Verification
`cargo nextest run --workspace`: green except the pre-existing
environment-dependent fabro-workflow failures (identical on main).
clippy `-D warnings` + pinned-nightly fmt clean. Codec unit tests +
adapter httpmock tests + frame-decoder/signer locks.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Scott Werner <scott@sublayer.com>
Co-authored-by: Scott Werner <stwerner@vt.edu>
The first feature payoff of the gateway refactor series (#481–#496):
OpenRouter lands as **pure configuration over the `openai_compatible`
codec** — no new adapter, no new `AdapterKind`, no OpenRouter codec
fork. Redone from #438, which prototyped this pre-refactor as ~2,500
lines including a dedicated adapter and parallel codec plumbing; this
PR's fabro-llm diff is the usage-superset decode plus a TOML file.
## What's here (3 commits)
**Per-model `billing_policy` override (fabro-model)** — a model row may
override its provider's billing family: the aggregator case, where
Claude served through an OpenAI-compatible provider bills
Anthropic-style cache reads/writes. `pricing_for`/`billing_facts_for`
and the resolved `Route` read the model-effective policy; unknown
passthrough model ids keep the provider policy. Pinned by a pricing test
(cache writes bill at 1.25× input under the override, $0 under the
provider's OpenAI default).
**Aggregator usage superset in the `openai_compatible` codec** — the
wire usage struct gains tolerant optional fields:
- `prompt_tokens_details.cached_tokens` / `cache_write_tokens` and
`completion_tokens_details.reasoning_tokens` normalize into their
disjoint `TokenCounts` buckets with the same subtraction convention as
the `openai_responses` codec
- in-band `usage.cost` (OpenRouter returns it on every response)
surfaces as `Response.cost_usd` with `cost_source = authoritative`, on
both blocking and streamed responses — #494's client-side estimate
stamping already defers to it by construction
- **deliberate behavior change owned here**: compat providers that
report cached-token details now see them split out of `input_tokens`
(previously ignored — the wire pin placed in PR 0 anticipating exactly
this change flips, and two new OpenRouter-shaped wire pins land)
**The provider package** — `openrouter.toml` (disabled by default, the
Ollama opt-in pattern; curated vendor-namespaced model list; Claude rows
set `billing_policy = "anthropic"`; attribution headers deliberately not
sent unless the operator opts in via `extra_headers`),
`OPENROUTER_API_KEY` env/secret registry entries, a gitleaks rule for
`sk-or-v1-` keys, a live e2e test asserting authoritative cost, and docs
(integration guide + models concept + config reference).
## Deliberate scope cuts (fidelity follow-ups, per the plan)
- `reasoning_details[]` parse + verbatim multi-turn echo,
`cache_control` multipart emission, `provider`/`native_finish_reason`
field reads — the new wire pin proves they're tolerated and ignored
today
- Typed reasoning-param-style / routing codec params — no catalog row
can request reasoning effort yet (no `controls.reasoning_effort`
declared), and routing prefs already pass through
`provider_options.openrouter` verbatim via the existing
adapter-name-keyed merge; typed params land when an operator-level knob
actually needs them
- The OpenRouter Anthropic skin (`/api/v1/messages`) — a future pure
config row pairing the existing `anthropic_messages` codec with bearer
transport
## Verification
- `cargo nextest run --workspace --no-fail-fast`: 6724 passed; only the
known 5 pre-existing environment-dependent fabro-workflow failures
(identical on main)
- Wire snapshots: one deliberate flip
(`decode_usage_ignores_token_details` →
`decode_usage_parses_token_details`) + two new OpenRouter pins (blocking
cost/cache-write, streamed cost); all other snapshots unmodified
- clippy `-D warnings` + pinned-nightly fmt clean
- Builtin catalog unchanged for existing providers: OpenRouter is
`enabled = false`, so the #493 route-equivalence table is untouched
Credit to #438 for the provider research, catalog curation, gitleaks
rule, and docs structure.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
Adds Anthropic Claude Fable 5 as a first-class Fabro model without
changing the default Anthropic model. The catalog now exposes
`claude-fable-5` with `fable` and `claude-fable` aliases, 1M context,
128k max output, effort levels, vision/tools, prompt caching, and the
documented pricing.
The Anthropic adapter now handles Fable's API behavior directly: it uses
the `claude-fable-5` API ID, omits the legacy 1M context beta header,
avoids injecting default `thinking`, preserves `output_config.effort`,
omits deprecated `temperature`/`top_p` sampling fields for Fable, and
rejects unsupported manual enabled/disabled thinking configs locally.
Fable refusals are converted into content-filter LLM errors with
`stop_details` preserved. Those refusal errors are fallback-eligible, so
existing `run.model.fallbacks` chains work for both prompt and agent
paths, while no-fallback refusals surface clearly as LLM errors.
## Live QA
Manually exercised the PR branch against a live Anthropic API key from
`~/.fabro.bak/.env.bak` using a temporary local harness that was removed
before commit. The run covered non-streaming completion via `fable`,
token counting via `claude-fable`, streaming completion, the deep
model-test path with tools/reasoning, local rejection of manual thinking
config, and a live refusal probe. The live run initially exposed
Anthropic's Fable rejection of `temperature`; this PR now strips
deprecated sampling fields for Fable and the live harness then passed
6/6 checks.
## Testing
- `cargo test -p fabro-llm --test live_fable_manual -- --nocapture
--test-threads=1` -> 6 passed against live Anthropic, temporary harness
removed afterward
- `cargo nextest run -p fabro-llm
encode_fable_uses_api_id_effort_and_omits_1m_beta`
- `cargo nextest run -p fabro-model -p fabro-llm -p fabro-workflow` ->
1808 passed, 41 skipped
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo insta pending-snapshots` -> no pending snapshots
- `git diff --check`
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
Separates Fabro server secrets into two explicit scopes: **bootstrap**
secrets that come from process env or `server.env`, and **optional
integration** secrets that come exclusively from the vault. This makes
secret resolution simple and predictable, and removes all `process env →
server.env` fallback paths for optional integrations such as GitHub App,
Slack, Daytona, Brave Search, and LLM provider keys.
## What changed
**New `ToolSecrets` struct in `fabro-agent`** — Brave Search API key is
now passed explicitly through `SessionOptions.tool_secrets` rather than
read from process env inside the tool. The standalone CLI reads the key
at the CLI boundary (with an explicit
`#[expect(clippy::disallowed_methods)]` annotation); the server will
read it from the vault. The error message changes from
`"BRAVE_SEARCH_API_KEY environment variable is not set"` to
`"BRAVE_SEARCH_API_KEY is not configured"`.
**`VaultCredentialSource::vault_only` constructor in `fabro-auth`** —
Adds a constructor that passes `|_| None` as the env lookup, ensuring
the server LLM credential source never resolves provider keys from
process env.
**GitHub App secrets move to vault in install flows** — Both the CLI
`fabro install github` path and the browser install finish handler now
write `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_CLIENT_SECRET`, and
`GITHUB_APP_WEBHOOK_SECRET` to the vault instead of `server.env`.
Switching strategies removes stale secrets from the other strategy's
storage location. The `vault_set` field type changes from `Vec<(String,
String)>` to `Vec<VaultSecretWrite>` to carry per-secret type metadata
(file vs. token).
**`fabro-vault` gains a `fabro-static` dependency** — Needed so the
vault crate can reference canonical env-var names from the shared
registry without a cycle.
**`GH_TOKEN` fallback removed** — `GITHUB_TOKEN` is now read from the
vault only; the changelog and `server-configuration.mdx` note drops
mention of `GH_TOKEN` as an accepted fallback.
**Version bump** — Workspace crates promoted from `0.244.0-nightly.0` to
`0.244.0`.
**Docs** — Internal strategy doc, public admin docs (Docker, Railway,
server-configuration, security, troubleshooting), and integration docs
(GitHub, Slack, Daytona, Brave Search, LiteLLM, tools reference, models)
all updated to reflect vault-only optional secrets and direct users to
`fabro secret set` rather than process env or `server.env`.
### Plan Summary
- **Task 1** (secret registry) — not yet present in this diff;
classification lives in the places that consume it.
- **Task 3–6** (vault-only lookups for GitHub, Slack, Daytona, LLM) —
implemented via `vault_only` constructor, `tool_secrets` threading, and
install-path changes.
- **Task 7** (Brave Search explicit injection) — `ToolSecrets`,
`register_core_tools` wiring, CLI boundary read.
- **Task 8** (install persistence) — GitHub App secrets written to
vault; token strategy writes `GITHUB_TOKEN` to vault and clears app
vault keys; app strategy clears `GITHUB_TOKEN` vault key.
- **Task 9** (docs) — all public and internal docs updated.
### Fabro Details
<details>
<summary>Ran 0 stages in 155m 26s for $60.85</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **155m 26s** | **$60.85** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Demo mode remains available via the X-Fabro-Demo header or the
fabro-demo=1 cookie set manually in browser devtools, but the UI
button and the POST /api/v1/demo/toggle endpoint are gone. The
fixture machinery and the auth/me demoMode flag (used by the SPA to
render Automations and the /start landing) are unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use raw sandbox reads for memory and skills, keep line-numbered reads focused on display, and share retry-delay handling across agent and LLM code.
Trim task tool descriptions, bound multi-file read concurrency, restore Docker's text read path, and add the reviewed implementation plan docs.
## What
Makes the run-detail stage sidebar (shown on the Overview and Stages
tabs) collapsible with a slide animation.
- A toggle button slides the panel between full width (`w-56`) and an
icon-only rail (`w-12`), animating `width` over 300ms with the same
easing as the Ask Fabro panel.
- When collapsed, **stage status icons stay visible** — green check /
red X / spinning teal for running — so run progress is still scannable
at a glance. Workflow links (Graph Source, Run Logs, etc.) collapse to
icons too so they remain reachable.
- Labels and durations become `sr-only` with `title` tooltips for hover.
- The open/closed choice persists to `localStorage`
(`fabro:stage-sidebar-collapsed`), carrying across the Overview and
Stages tabs and reloads.
## Layout
- The collapse toggle is inline with the `STAGES` heading row (or
`WORKFLOW` when a run has no stages yet), so it doesn't push the stage
list down.
- The stage sidebar's top padding on the Stages tab was reduced (`pt-6`
→ `pt-3`) so the heading aligns with the adjacent content column and
sits closer to the tab nav.
## Notes
Self-contained in `StageSidebar` — `run-overview.tsx` and
`run-stages.tsx` render it inside flex layouts that already track its
width, so the slide works in both with no parent changes (aside from the
padding tweak).
Verified: `tsc` typecheck passes; `stage-sidebar` lib tests pass
(10/10).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: fabro-sh-0530[bot] <281434857+fabro-sh-0530[bot]@users.noreply.github.com>
Co-authored-by: Fabro <noreply@fabro.sh>
## Summary
Ask Fabro sessions are now run-bound instead of standalone. Sessions are
created under their owning run, then accessed by flat session ID routes,
with durable state projected from the run event stream rather than a
separate session store.
## Changes
- Move session creation/listing to `POST/GET /api/v1/runs/{id}/sessions`
while keeping flat session reads, turns, interrupts, and event streams
under `/api/v1/sessions/{id}/...`.
- Add typed `run.session.*` events, ULID-backed session/turn IDs,
read-only default permissions, and a rebuildable SlateDB `session_id ->
run_id` index.
- Remove the old file-backed session store and wire the server, runtime,
Rust client, generated API crates, and TypeScript client around run
event projections.
- Replace the old top-level CLI session command with `fabro run ask` for
chatting with a run.
- Regenerate the TypeScript API client; this also catches up existing
generated models for Pair/run event detail schemas already present in
the OpenAPI spec.
## Validation
- `cargo build -p fabro-api -p fabro-client -p fabro-server -p
fabro-cli`
- `cargo nextest run -p fabro-server --features test-support -E
'test(run_bound_session_is_created_as_run_event_and_resolves_by_flat_id)
| test(sessions_are_listed_only_under_their_owning_run)'`
- `cargo nextest run -p fabro-store
projection_rebuilds_runtime_context_from_run_events`
- `cargo +nightly-2026-04-14 clippy -p fabro-api -p fabro-client -p
fabro-store -p fabro-server -p fabro-cli --all-targets -- -D warnings`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cd lib/packages/fabro-api-client && bun run typecheck && cd
../../../apps/fabro-web && bun run typecheck`
- `git diff --check`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
Generated with GPT-5 via [Codex](https://openai.com/codex)
## Summary
This PR makes agent execution a strict two-backend contract: API-backed
stages use Fabro-owned model/provider auth, while ACP-backed stages
launch a user-supplied stdio process that owns its own auth and tools.
That removes the legacy CLI backend and prevents ACP execution from
accidentally resolving or forwarding provider credentials.
## Changes
- Replaces the old `api`/`cli`/`acp` backend model with `AgentBackend {
api, acp }`, with `backend=\"cli\"` rejected and migrated toward
explicit ACP process configuration.
- Splits ACP process configuration into `acp.command` for shell command
strings and `acp.config` for JSON stdio configs, while rejecting legacy
`acp_command`.
- Restricts ACP to `agent` nodes and rejects API-only attributes such as
`model`, `provider`, `reasoning_effort`, `max_tokens`, and `speed` on
ACP nodes.
- Deletes the workflow CLI runtime, CLI credential resolver surface, CLI
live smoke tests, and `agent.cli.*` event handling.
- Updates ACP events and projections to report process identity
(`command`, optional `config_name`) rather than provider/model metadata.
- Updates import/stylesheet propagation, CLI workflow smoke coverage,
server steering tests, and web model extraction for the new
event/backend contract.
## Validation
- `cargo check -p fabro-auth -p fabro-acp -p fabro-workflow -p fabro-cli
--all-targets`
- `cargo nextest run -p fabro-auth -p fabro-acp -p fabro-validate -p
fabro-store -p fabro-workflow --lib`
- `cargo nextest run -p fabro-acp`
- `cargo nextest run -p fabro-cli --test it
workflow::acp::acp_backend_workflow`
- `cargo nextest run -p fabro-workflow --test it
codergen_without_backend_simulated`
- `cargo nextest run -p fabro-workflow --test it
import_e2e_through_engine`
- `cargo nextest run -p fabro-workflow --test it stylesheet_application`
- `cargo nextest run -p fabro-server
steer_with_active_acp_stage_returns_non_steerable_conflict`
- `cargo nextest run -p fabro-server
active_acp_stage_marker_clears_on_terminal_paths`
- `cargo nextest run -p fabro-types
agent_backend_accepts_only_api_and_acp`
- `cd apps/fabro-web && bun test app/routes/run-stages.test.ts`
- `cd apps/fabro-web && bun run typecheck`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
---------
Co-authored-by: Peter Bell <4843+PeterBell@users.noreply.github.com>
## Summary
Moves provider-specific facts out of `AdapterKind` metadata and into
provider catalog data, leaving adapters responsible for runtime protocol
behavior. This makes providers that share an adapter mostly TOML-driven
while still surfacing adapter construction failures during readiness
checks.
## What Changed
- Provider TOML now owns auth mode, API-key/header policy, billing
policy, agent profile, base URLs/env overrides, extra headers, and probe
markers.
- Auth, install, config, diagnostics, and server flows resolve provider
credentials from catalog auth config, including API-key, header-only,
and no-auth providers.
- LLM client registration now reports adapter construction failures,
validates final adapter requests before HTTP dispatch, and preserves
custom primary auth headers.
- Billing and docs now use provider-owned billing policy instead of
adapter metadata, and the old adapter metadata surface is removed.
## Reviewer Notes
OpenAI-compatible `base_url` validation now happens during
adapter/client registration rather than catalog build. That keeps
catalog parsing adapter-agnostic while still letting readiness and model
listing reflect providers that cannot register.
## Verification
- `cargo check -p fabro-model -p fabro-auth -p fabro-llm -p fabro-server
-p fabro-cli`
- `cargo nextest run -p fabro-llm -- adapter_registry`
- `cargo nextest run -p fabro-model -- catalog`
- `cargo nextest run -p fabro-auth -- api_key`
- `cargo nextest run -p fabro-server -- install`
- `cargo +nightly-2026-04-14 fmt --check --all`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Adds catalog-level `agent_profile` overrides so custom providers and
individual models can choose Anthropic, OpenAI, or Gemini agent behavior
independently from their adapter default. The effective precedence is
model override, then provider override, then adapter metadata.
## What Changed
- Added typed provider/model `agent_profile` settings in `fabro-config`
and `fabro-model`, with serde/strum support for `anthropic`, `openai`,
and `gemini`.
- Centralized effective profile resolution in the catalog, including
provider alias canonicalization and a guard against unrelated model
overrides leaking across providers.
- Updated run startup, API sessions, CLI/ACP backends, prompt
project-memory discovery, and standalone agent startup to use the
resolved catalog profile.
- Documented provider-level and model-level `agent_profile`
configuration in the public model and user configuration docs.
No OpenAPI or model-list response shape changes are included.
## Validation
- `cargo nextest run -p fabro-model -p fabro-config -p fabro-workflow -p
fabro-agent` passed: 1826 passed, 125 skipped.
- `cargo +nightly-2026-04-14 fmt --check --all` passed.
- `cargo +nightly-2026-04-14 clippy -p fabro-model -p fabro-config -p
fabro-workflow -p fabro-agent --all-targets -- -D warnings` passed.
- `git diff --check` passed.
- `cargo nextest list -p fabro-dev` confirmed there is no docs-options
reference test target to run.
## Post-Deploy Monitoring & Validation
Watch workflow and agent-session logs for provider/model resolution
errors, unexpected project-memory file selection, or CLI/ACP launch
command mismatches on custom catalog providers. Healthy signal: custom
provider/model runs start normally and use the intended profile-specific
behavior. Failure trigger: repeated `Provider ... is not configured`
errors, missing expected project memory, or profile-specific agent
startup failures after configuring `agent_profile`. Mitigation is to
remove the override from config or revert this PR. Validation window:
first deploy cycle after merge; owner: release/on-call engineer.
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
## Summary
Add CLI support for run parent relationships now that the server API can
store them. This lets users create child runs, filter children, inspect
parent metadata, and link or unlink parents without dropping to raw API
calls.
## What Changed
- Added top-level `fabro parent link` and `fabro parent unlink` commands
with selector resolution, text output, and JSON summaries.
- Added `--parent` to `fabro run`, `fabro create`, and `fabro ps`;
create/run send `parent_id` in manifests and `ps` uses server-side
parent filtering.
- Surfaced `parent_id` in `ps --json` and `inspect`, with a conditional
`PARENT` column for unfiltered tables.
- Extended `fabro-client` parent-link APIs and
`list_store_runs(parent_id)`.
## Test Plan
- `cargo nextest run -p fabro-cli`
- `cargo nextest run -p fabro-client`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy -p fabro-cli -p fabro-client
--all-targets -- -D warnings`
- `cargo insta pending-snapshots`
- `git diff --check`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
Generated with GPT-5 via [Codex](https://openai.com/codex)
## Summary
Adds event-sourced pull request association management for runs while
preserving Fabro-created PR creation. A run can now store a current
GitHub PR association, replace it by linking another GitHub PR URL, and
remove it through an unlink event.
## What Changed
- Added `pull_request.linked` and `pull_request.unlinked` events,
projection replay support, and optional PR metadata fields in shared
pull request records.
- Added API, server, and client support for `PUT
/runs/{id}/pull_request` and `DELETE /runs/{id}/pull_request`; linking
accepts GitHub PR URLs, infers owner/repo/number, and captures live
GitHub title and branch metadata when available.
- Added `fabro pr link` and `fabro pr unlink`, updated `fabro pr view`,
and kept create/merge/close behavior guarded to GitHub PRs with usable
coordinates.
- Updated web UI rendering and internal event docs so stored PR links
display cleanly when live GitHub details are unavailable.
## Testing
- `cargo +nightly-2026-04-14 fmt --check --all`
- `git diff --check`
- `cargo build -p fabro-api`
- `cargo nextest run -p fabro-types -p fabro-store -p fabro-server -p
fabro-cli`
- `bun run typecheck` in `lib/packages/fabro-api-client`
- `bun run typecheck` in `apps/fabro-web`
- `bun test` in `apps/fabro-web`
Refs https://github.com/fabro-sh/fabro/issues/235
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
---------
Co-authored-by: Haroldo Olivieri <6575718+haroldolivieri@users.noreply.github.com>
## Summary
Finish phase 9 of the configurable LLM provider/model work by aligning
public docs, release notes, and guardrails with the implementation
already landed in phases 0-8.
- documents settings-driven providers/models, OpenAI-compatible gateway
examples, typed `extra_headers`, model `api_id`, controls, and per-speed
costs
- adds the 2026-05-13 changelog entry and provider string migration note
- updates the internal phase plan ledger to reflect current
implementation status
- adds a workspace policy test blocking direct production
`Catalog::builtin()` usage outside catalog owner/test code
- clarifies `Provider` as a built-in compatibility enum while open-ended
identity is `ProviderId`
## Verification
- `cargo nextest run -p fabro-dev --features dev --test it policy`
- `cargo dev docs check`
- `cargo nextest run -p fabro-model -p fabro-config -p fabro-auth -p
fabro-llm`
- `cargo build --workspace`
- `cargo nextest run --workspace` (5717 passed, 182 skipped, nextest
reported 1 leaky test)
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `git diff --check`
## Summary
Implemented ACP support as a first-class Fabro backend alongside `api`
and `cli`. This adds a new `fabro-acp` crate using the official ACP Rust
crates, routes `backend=\"acp\"` for agent and prompt nodes, adds
sandbox stdio support for local/Docker/test-support paths, emits ACP
workflow events/projections, updates server steerability handling,
validation, documentation, and black-box CLI coverage.
## Test Plan
Passed strict non-live verification:
- `ulimit -n 4096 && cargo nextest run -p fabro-workflow --run-ignored
all --no-fail-fast` — 1162 passed, 0 skipped.
- `ulimit -n 4096 && cargo nextest run -p fabro-acp -p fabro-sandbox -p
fabro-workflow -p fabro-validate -p fabro-store -p fabro-server -p
fabro-cli --run-ignored all --no-fail-fast -E 'not
test(daytona_streaming_live_smoke)'` — 3125 passed.
- `cargo build --workspace` — passed.
- `ulimit -n 4096 && cargo nextest run --workspace --run-ignored all
--no-fail-fast -E 'not test(daytona_streaming_live_smoke)'` — 5666
passed.
- `cargo +nightly-2026-04-14 fmt --check --all` — passed.
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings` — passed.
Live-environment tests skipped/excluded under explicit user override:
- `daytona_streaming_live_smoke` was excluded from final nextest runs
because it requires live Daytona infrastructure and `DAYTONA_API_KEY`.
- Confirmed with `env -u DAYTONA_API_KEY cargo test -p fabro-sandbox
--features daytona --test daytona_streaming_live
daytona_streaming_live::daytona_streaming_live_smoke -- --ignored
--exact --nocapture`: failed fast with `DAYTONA_API_KEY must be set to
run this live smoke test`.