Adds a Defining Workflows page covering the import placeholder
syntax, node ID prefixing, the imported-file contract, default
attribute and class propagation, retry_target remapping, templating
behavior, nested imports, empty-import bypass, and the import_error
validation surface.
## Summary
Makes LLM setup explicitly skippable in both the web installer and
`fabro install`, without making omission accidental. A skipped LLM step
lets install complete with zero LLM credentials; later LLM-dependent
workflows keep using the existing provider-not-configured behavior.
`fabro doctor` is intentionally unchanged.
Plan: `docs/superpowers/plans/2026-05-14-optional-llm-install.md`
## Key changes
**Server + API**
- `PUT /install/llm` now accepts `{"providers":[]}` as "LLM step
completed, skipped" — the empty-list rejection is removed; per-provider
validation for non-empty lists is retained.
- OpenAPI: dropped `minItems: 1` from
`InstallLlmProvidersInput.providers`, updated schema descriptions so
empty = skipped and `llm: null` = incomplete. TypeScript client
regenerated.
- `/install/finish` still requires the LLM step to be present, but
tolerates zero credentials — it writes settings, runtime auth secrets,
and GitHub secrets normally and writes no LLM vault entries.
**Web installer**
- New "Skip LLM setup" secondary action on the LLM step (via a
`secondaryAction` prop on `StepPanel`) that records an empty provider
list and advances to GitHub.
- Review screen shows `LLM providers: Skipped` (step completed, empty)
vs `Not configured` (step never completed), via a new
`describeLlmSummary` helper.
- Continue with no API keys still shows the existing validation error —
skipping is only reachable through the explicit skip action.
**CLI**
- Interactive `fabro install` asks "Configure LLM providers now?"
(default yes) before provider selection; declining returns an empty
selection and continues to GitHub.
- Hidden non-interactive `--skip-llm` flag, mutually exclusive with
`--llm-provider` / `--llm-api-key-stdin` / `--llm-api-key-env` via clap
`conflicts_with_all`. Missing LLM flags are still validation errors
unless `--skip-llm` is present. Non-interactive usage text updated with
a skip example.
## Code review
Ran a 12-reviewer `ce:review` pass (correctness, testing,
maintainability, project-standards, agent-native, learnings, security,
api-contract, reliability, adversarial, cli-readiness,
kieran-typescript). No P0/P1 findings; agent-native parity PASS. Applied
fixes in `40a29c591`:
- Re-entrancy guard on `runStepSubmit` so a fast double-click on "Skip
LLM setup" can't fire two requests.
- `validate()` only suggests `--skip-llm` in the missing-provider error
when no credential flag is set (it conflicts with those flags).
- Added tests: all three `--skip-llm` conflict arms, the review screen's
"Not configured" branch, and the skip-button failure path.
One advisory finding left as report-only: an empty `PUT /install/llm`
overwrites previously-saved credentials if a user navigates Back and
clicks Skip — judged acceptable since the button is explicitly labeled
and clicking it is deliberate.
## Testing
- `cargo nextest run -p fabro-server -p fabro-cli -p fabro-install` —
1521 passed
- `cargo build -p fabro-api`, `cargo fmt --check`, `cargo clippy`
(changed crates) — clean
- `bun test` (install-app) — 14 passed; `bun run typecheck` — clean
- New coverage: server accepts empty providers + session shows `llm`
complete with `providers:[]`; finish with skipped LLM persists no LLM
vault credentials but keeps GitHub secrets; web skip button PUTs
`providers:[]` and navigates to GitHub; review renders Skipped / Not
configured; CLI `--skip-llm` requires `--non-interactive`, conflicts
with all credential flags, `validate()` succeeds with `--skip-llm`,
usage text documents `--skip-llm`.
Not added (out of plan scope): an automated test for the interactive
`InstallInputSource` skip branch — `InteractiveInstallInputSource` is
TTY-coupled and has no existing tests; the non-interactive `--skip-llm`
path is fully covered.
## Post-Deploy Monitoring & Validation
This change is install-time only; there is no continuous runtime impact.
Validate during the next install/release smoke:
- **Web installer:** run a fresh browser install, click "Skip LLM setup"
on the LLM step, confirm it advances to GitHub and the review screen
reads `LLM providers: Skipped`. Finish the install and confirm the
server restarts into normal mode with no LLM credentials in the vault
(`secrets.json` has no credential entries) and
GitHub/server/object-store/sandbox settings written normally.
- **CLI:** run `fabro install --non-interactive --skip-llm
--github-strategy token --github-username <user>` and confirm it
completes; run interactive `fabro install` and confirm declining
"Configure LLM providers now?" continues to GitHub.
- **Healthy signals:** install completes (web `/install/finish` → 202;
CLI exits 0), server boots in normal mode, `fabro doctor` runs and
reports no LLM providers configured (expected, unchanged behavior).
- **Failure signals / rollback trigger:** install fails to finish,
server fails to boot after a skipped install, or `/install/finish`
rejects a completed-but-empty LLM step. Rollback = revert this PR;
install behavior returns to requiring at least one LLM provider.
- **Validation window/owner:** next install smoke / release
verification, owned by whoever runs the release.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Adds Venice as a catalog-only built-in OpenAI-compatible provider. The
preparatory catalog/test work is already merged in #264, so this PR is
intentionally limited to the provider TOML.
## Changes
- Adds `lib/crates/fabro-model/src/catalog/providers/venice.toml`.
- Registers provider ID `venice` with alias `venice-ai`,
OpenAI-compatible base URL, and `credential:venice` / `VENICE_API_KEY`
credential lookup.
- Adds the two initial Venice-owned models and pricing metadata.
## Verification
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo nextest run -p fabro-model`
- `cargo nextest run -p fabro-cli cmd::model`
- `cargo nextest run --workspace --status-level slow --profile ci`
- `cargo insta pending-snapshots`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (unknown context, medium reasoning) via
[Codex](https://openai.com/codex)
---------
Co-authored-by: Jesse <606+jesseproudman@users.noreply.github.com>
## Summary
Prepares the model catalog tests for catalog-only built-in providers so
a future provider can be added with just its catalog TOML.
## Changes
- Replaces the closed-enum round-trip guardrail with a catalog metadata
guardrail, allowing built-in TOML providers that do not have `Provider`
enum variants.
- Makes the all-model `fabro model` CLI tests assert stable table
structure instead of snapshotting every built-in catalog row.
- Renames synthetic custom-provider and missing-provider fixtures away
from provider names that can become real catalog entries.
## Verification
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo nextest run -p fabro-model -p fabro-auth -p fabro-llm -p
fabro-server -p fabro-workflow -p fabro-config`
- `cargo nextest run -p fabro-cli cmd::model`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (unknown context, medium reasoning) via
[Codex](https://openai.com/codex)
---------
Co-authored-by: Jesse <606+jesseproudman@users.noreply.github.com>
## Summary
This adds run-configuration support for mounting existing Daytona
volumes into Fabro-managed Daytona sandboxes.
Concretely, this PR:
- adds `[[run.sandbox.daytona.volumes]]` with `volume_id`, `mount_path`,
and optional `subpath`
- resolves that config through the layer/settings/runtime pipeline
- forwards configured mounts to `daytona_sdk::SandboxBaseParams.volumes`
when creating the sandbox
- documents the configuration surface in the Daytona environment and run
configuration docs
## Motivation
Daytona already supports attaching volumes when a sandbox is created,
but Fabro currently owns that sandbox creation call. That means users
cannot attach a pre-created Daytona volume to a Fabro-managed sandbox
from run config.
The intended use is persistent, provider-owned state such as agent
credentials, caches, datasets, or other files that should survive
ephemeral sandbox lifecycles.
## Scope
This is intentionally a narrow passthrough. Fabro does not create,
delete, list, wait on, or otherwise manage Daytona volume lifecycle.
Users create the volume in Daytona first, then reference its `volume_id`
from Fabro run config.
`volumes` defaults to an empty list in resolved settings for backwards
compatibility with existing serialized settings.
## Testing
- `cargo test -p fabro-server
runtime_daytona_config_preserves_volume_mounts`
- `cargo test -p fabro-config resolves_daytona_volume_mounts`
- `cargo test -p fabro-sandbox --features daytona volume_mounts`
- `cargo test -p fabro-workflow
runtime_daytona_config_preserves_volume_mounts`
- `cargo check -p fabro-server`
---
_Re-opened from #262 (originally by @kimprobably) to land a rustfmt fix
— the original PR came from an org-owned fork, which blocks maintainer
pushes. Branch is now on the base repo. Original commit preserved; one
additional commit fixes rustfmt formatting._
Co-authored-by: Tim Keen <tim@keen.digital>
## Summary
This is a cleanup pass over the configurable LLM provider/catalog work
from issue #210. It addresses reuse, quality, and efficiency findings
from the phase 0-9 review without changing the public provider settings
contract.
Notable changes:
- skip LLM client initialization during run preflight when the workflow
has no LLM nodes
- make preflight provider checks use alias-aware `Client::has_provider`
- resolve `run.model.fallbacks` through the catalog instead of the old
empty-key bridge
- paginate `/models` before cloning returned rows
- share label parsing, provider default-adapter lookup, enum
expected-value formatting, and billing token formatting helpers
- use catalog provider display names for OpenAI-compatible agent
profiles
- align process-env configured-provider discovery with
`EnvCredentialSource`
## Verification
- `cargo check -p fabro-config -p fabro-model -p fabro-auth -p
fabro-agent -p fabro-workflow -p fabro-server -p fabro-cli`
- `cargo nextest run -p fabro-config parse_labels_keeps_key_value_pairs`
- `cargo nextest run -p fabro-workflow resolve_fallback_chain_resolves`
- `cargo nextest run -p fabro-auth configured_providers`
- `cargo nextest run -p fabro-server list_models`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy -p fabro-model -p fabro-auth -p
fabro-workflow -p fabro-server -p fabro-agent -p fabro-config -p
fabro-cli --all-targets -- -D warnings`
- `cd apps/fabro-web && bun test app/routes/run-billing.test.tsx`
- `cd apps/fabro-web && bun run typecheck`
- `git diff --check`
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.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
This PR advances the catalog-driven LLM work from fabro-sh/fabro#210 by
making the resolved model catalog the source of truth for provider
registration, request control validation, and billing identity. Runs now
preserve canonical provider/model/speed identity through pricing and API
responses instead of collapsing billing around provider API aliases or
model IDs alone.
## What Changed
- Register LLM provider adapters from the resolved catalog, including
custom OpenAI-compatible providers and their credential resolution
paths.
- Validate effective model request controls, including run-level
defaults and node overrides, before dispatching LLM requests.
- Add catalog-aware billing lookup that prices canonical `ModelRef`
values, uses base model costs for standard speed, applies per-speed cost
overrides, and returns an unknown estimate instead of silently billing
zero for unsupported combinations.
- Move Anthropic Opus fast-mode pricing into the built-in catalog for
`claude-opus-4-6` and `claude-opus-4-7`.
- Thread the injected catalog and effective speed controls through
workflow billing, including API-mode and CLI-mode handlers.
- Update billing APIs, server aggregation, generated clients, and the
web billing view to expose provider/model/speed billing identity and
keep standard and fast usage in separate rows.
## Notes for Review
Billing lookup intentionally uses canonical catalog model IDs. Provider
`api_id` substitution remains limited to provider request construction,
so aliases can be used on the wire without changing billing identity.
Event conversion paths that do not have catalog access now preserve
token counts with a null dollar estimate rather than falling back to the
bootstrap catalog.
## Verification
- `cargo build -p fabro-api`
- `cd lib/packages/fabro-api-client && bun run generate`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `ulimit -n 4096 && cargo nextest run -p fabro-model -p fabro-workflow
-p fabro-server -p fabro-api -p fabro-cli --no-fail-fast`
- `ulimit -n 4096 && cargo nextest run --workspace --no-fail-fast`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test`
- `git diff --check`
---
[](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
Supports `dockerfile = { path = "..." }` for Daytona snapshots declared
in `.fabro/project.toml` and workflow-local `workflow.toml`, resolving
each path relative to the TOML file that declared it. Manifest building
now bundles project-level Dockerfiles into the target workflow file
bundle, and server manifest preparation rewrites bundled Dockerfile
paths to inline content before settings reach sandbox creation.
The repo Daytona snapshot config now uses `.fabro/Dockerfile` instead of
embedding the Dockerfile in TOML, preserving the prior Dockerfile
content exactly.
## Testing
- `cargo nextest run -p fabro-manifest
build_manifest_bundles_project_config_daytona_dockerfile_relative_to_project_config`
- `cargo nextest run -p fabro-manifest`
- `cargo nextest run -p fabro-server
prepare_manifest_inlines_project_config_daytona_dockerfile_from_bundle
prepare_manifest_errors_when_project_config_dockerfile_bundle_is_missing`
- `cargo nextest run -p fabro-server`
- `cargo nextest run -p fabro-config`
- `cargo nextest run -p fabro-manifest -p fabro-server -p fabro-config`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
Optional credentialed Daytona live smoke was not run.
## Post-Deploy Monitoring & Validation
- Log queries/search terms: `missing bundled dockerfile`, `unsupported
dockerfile reference`, `invalid manifest project config path`,
`dockerfile path should have been resolved to inline content before
sandbox creation`, Daytona snapshot creation failures.
- Metrics/dashboards: run submission/preflight failure rate, Daytona
sandbox startup failure rate, snapshot creation failure rate, and run
validation errors for manifests using bundled files.
- Healthy signals: runs using `.fabro/project.toml` with `dockerfile = {
path = "Dockerfile" }` progress past manifest preparation and Daytona
snapshot creation without path-resolution errors.
- Failure signals and rollback trigger: any sustained increase in
manifest preparation failures or Daytona snapshot failures containing
the log terms above; rollback by reverting this PR or temporarily
restoring inline Dockerfile TOML for affected deployments.
- Validation window and owner: first 24 hours after deploy; owner is the
deploying operator/on-call engineer.

## Summary
Patches all three open Dependabot alerts on `main`:
- **#26 / #28 (openssl, high + medium)** — bump `openssl` 0.10.78 →
0.10.79. Patches `GHSA-xp3w-r5p5-63rr` (UB in `X509Ref::ocsp_responders`
for certs with non-UTF-8 OCSP URLs) and `GHSA-xv59-967r-8726` (heap
buffer overflow in AES key-wrap-with-padding). Lockfile-only.
- **#27 (rmcp, high)** — bump workspace `rmcp` from `1.3` to `1.4`,
which resolves to 1.7.0. Patches `GHSA-89vp-x53w-74fx` (DNS rebinding in
the Streamable HTTP **server** transport). Fabro only uses the
streamable-http **client** transport
(`lib/crates/fabro-mcp/src/client.rs`), so practical exposure was nil —
bumping anyway to stay on a supported, patched line.
Each fix is in its own commit so it can be reverted independently.
## Test plan
- [x] `cargo build --workspace` clean after each bump
- [x] `cargo nextest run -p fabro-mcp -p fabro-mcp-server` — 30/30 pass
on rmcp 1.7
- [ ] CI green
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes#179.
## Summary
- `fabro validate`'s DOT parser rejected multi-line node attribute
blocks unless every attribute was comma-separated, forcing long node
definitions onto a single line.
- Per the DOT spec, the separator between attributes is optional —
whitespace (including newlines) alone is sufficient, and `,` or `;` are
both accepted as explicit separators.
- `attr_block` now uses `many0(terminated(attr, opt(',' | ';')))`
instead of `separated_list0(',', attr)`, so all three forms parse
identically.
## Before / after
```dot
// previously rejected — now parses
inspect [
label="Inspect Code"
shape=tab
prompt="@prompts/inspect.md"
class="heavy"
reasoning_effort="high"
]
```
## Test plan
- [x] Added regression test `parse_attr_block_multiline_without_commas`
covering the exact form from #179.
- [x] `cargo nextest run -p fabro-graphviz` — 109/109 pass, including
the new test and existing comma-separated multi-line tests.
- [x] `cargo +nightly-2026-04-14 clippy -p fabro-graphviz --all-targets
-- -D warnings` clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Nate Aune <118984+natea@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This branch contains both commits:
1. The action_id uniqueness fix from #252 (`fix(slack): make per-button
action_id unique to satisfy Slack's invalid_blocks check`).
2. The new context+link fix (`feat(slack): render context_display and
run link in interview messages`).
The first commit needs to land (or be rebuilt by the maintainer
workflow) before the second is meaningful, because without it
multi-button gates still fail with `invalid_blocks` and the new context
block never reaches Slack. Reviewing #252 first and this issue/PR second
is the cleanest flow.
## What
Two-file change in `lib/crates/fabro-slack/` and
`lib/crates/fabro-server/`. The Slack interview message now includes the
upstream stage's response (`context_display`) and a link back to the
run, so a reviewer in Slack can decide A vs R without opening the web
UI.
## Why
See **#253** for the full repro, screenshots, and threat model. Short
version: today the Slack message contains only the hexagon node's
`label` plus the buttons, which is not enough information to act on.
## Diff shape
- `question_to_blocks` gains a `run_web_url: Option<&str>` argument.
- New helpers:
- `header_section(question, run_web_url)`: bold question text, optional
`stage \`{stage}\`` hint, and an "Open in Fabro" link when the URL is
known.
- `context_section(context_display)`: renders the upstream stage's
response (e.g. plan summary, Dossier URLs) below the header, separated
by a `divider`. Empty context_display is skipped so the message falls
back cleanly to the old two-block shape.
- `escape_slack_controls(text)`: HTML-entity escapes `&`, `<`, `>` in
untrusted strings (question, stage, context_display, answered_blocks
question and answer text). Neutralises LLM-produced payloads like
`<!here>`, `<@U…>`, `<#C…>` while leaving Markdown formatting (`*bold*`,
`_italic_`, `` `code` ``, `~strike~`) intact. Per
https://docs.slack.dev/messaging/formatting-message-text/#escaping.
- `truncate_to_limit`: clamps both the header text and the context block
against Slack's documented 3000-character section text limit, with the
truncation suffix counted against the budget so the result is always
under the cap. Defends against pathological questions or oversized LLM
responses producing `invalid_blocks`.
- `server.rs`: `start_optional_slack_service`'s event subscriber calls
`state.run_web_url(&envelope.event.run_id)` per event and forwards the
result to `SlackService::handle_event`, which threads it into
`question_to_blocks`. Returns `None` (and the link is omitted) when
`server.web.enabled` is `false` or `server.web.url` is unset.
## Tests
10 new in `lib/crates/fabro-slack/src/blocks.rs`:
- `header_includes_run_link_when_url_provided`
- `header_omits_link_when_url_missing`
- `header_shows_stage_when_present`
- `header_truncates_when_inputs_exceed_section_limit`
- `context_display_renders_between_header_and_actions`
- `context_display_truncates_oversized_text_to_fit_slack_budget`
- `empty_context_display_is_skipped`
- `slack_control_chars_in_question_text_are_escaped`
- `slack_control_chars_in_context_display_are_escaped` (covers
`<!here>`/`<@U…>`/`<#C…>` neutralisation and verifies Markdown survives)
- `answered_blocks_escape_slack_control_chars`
84/84 `fabro-slack` tests pass (was 74 after #252). `cargo
+nightly-2026-04-14 fmt --check --all` and `cargo +nightly-2026-04-14
clippy -p fabro-slack -p fabro-server --all-targets -- -D warnings` both
clean.
## Verified end-to-end
Built a patched binary, swapped it for the brew install, triggered a
fresh multi-choice approve gate against a real Slack workspace. The
Slack message now renders with bold "Approve Plan" header, `stage
\`approve\`` hint, "Open in Fabro" link, the upstream plan summary block
(Dossier canonical and version URLs, `tmp-docs/fabro-plan.html` artifact
path, the plan-summary bullets), a divider, and the `[A] Approve` and
`[R] Revise` buttons. Reviewer can act on the gate from Slack alone.
## Latent observation, not in this diff
`lib/crates/fabro-server/src/server.rs::AppState::run_web_url` has a
comment saying it is snapshotted at create-time so attach replays remain
stable when `server.web.url` changes, but the implementation reads
current settings via `server_settings()`/`canonical_origin()`. This
patch is unaffected (per-event call), but the comment looks stale.
## Closes
Closes#253 if you choose to land this directly. Otherwise this PR is
background material for the issue.
---------
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Heads up:
[CONTRIBUTING.md](https://github.com/fabro-sh/fabro/blob/main/CONTRIBUTING.md)
says you don't accept outside PRs, *"Instead of accepting outside pull
requests, we accept bug reports and feature requests as GitHub Issues."*
I filed the canonical bug report as **#251**, and that's where any
actual discussion belongs.
This PR is a courtesy ready-made diff in case it's useful to whoever
supervises the AI workflow that lands this fix. Feel free to close it
without comment; nothing is being asked of you here. I just thought it'd
be useful to have a reference for what I did locally to fix it.
## What
Two-file behaviour fix in `lib/crates/fabro-slack/`: every interview
button on a multi-button gate now gets a Slack-unique `action_id`. Today
they all share `"interview.answer"`, so Slack rejects the
`chat.postMessage` with `invalid_blocks` and
`SlackService::handle_event` silently drops the error.
## Why
Multi-button Slack interview gates never reach Slack. Full reproducer,
MITM-captured `invalid_blocks` response, and root-cause walkthrough are
in **#251**.
## Diff shape
- `blocks.rs`: each button gets a unique suffix.
- `YesNo` / `Confirmation`: `interview.answer.yes` /
`interview.answer.no`
- `MultipleChoice`: `interview.answer.<index>` (index, not raw key, to
dodge Slack's 255-char `action_id` cap and any author-supplied charset
surprises; selected key still rides in the button `value`)
- `interaction.rs`: `parse_interaction` accepts both the legacy
exact-prefix shape (in-flight buttons keep working across upgrade) and
the new suffixed shape, via a pre-computed `ANSWER_ACTION_ID_PREFIX_DOT`
constant so the parse hot path doesn't `format!` on every event.
- Tests: +5 in `interaction.rs` (suffixed yes/no, suffixed multi-choice,
legacy exact prefix, lookalike `interview.answers.yes` rejected,
prefix-sync assertion). Updated the existing block-builder tests to
assert uniqueness instead of the old single constant. One fixture each
in `dispatch.rs` and `connection.rs` updated to the suffixed shape; one
legacy fixture left in each to document backwards compatibility.
74/74 `fabro-slack` tests pass (was 69/69). `cargo +nightly-2026-04-14
fmt --check --all` and `cargo +nightly-2026-04-14 clippy -p fabro-slack
--all-targets -- -D warnings` clean.
## Verified end-to-end
Built a patched `fabro` binary, swapped it for the brew install,
triggered a fresh multi-choice `Approve Plan` gate against a real Slack
workspace, message rendered correctly in the configured channel with two
clickable `[A] Approve` and `[R] Revise` buttons. Before the patch, the
exact same gate produced zero Slack output and only the swallowed
`invalid_blocks` was visible via MITM.
## Suggested follow-up (separate concern, not in this diff)
The silent error swallow in `SlackService::handle_event` (`if let
Ok(posted) = self.client.post_message(...)`) is what hid this bug. Worth
logging at `WARN`. Mentioned in #251 as a separate item.
## Closes
Closes#251 if you choose to land this directly. Otherwise this PR is
just background material for the issue.
Switch the contribution policy from an issue-only model to welcoming
outside PRs. Small fixes go straight to a PR; larger changes start with
an issue or discussion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
This PR moves the server-facing model catalog paths onto a resolved
catalog stored in `AppState`, using configured `[llm]` provider/model
overrides layered on top of the built-in catalog.
The server now uses the injected catalog for:
- `/models` listing and model test lookup
- `/completions` default model and provider inference
- manifest preflight materialization and LLM model alias resolution
- diagnostics LLM probes
- pull request default model selection
- runtime settings refresh via `replace_settings`
It also adds catalog overlay helpers in `fabro-model` and converts
resolved server runtime `[llm]` settings into the catalog shape in
`fabro-config`.
This branch also includes the earlier `chore: update dockerfile` commit,
which updates the Daytona snapshot to `fabro-v10` and installs Chromium
through the xtradeb PPA with an XFCE/browser wrapper.
Related: #210
## Tests
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo nextest run -p fabro-config -p fabro-model -p fabro-server`
(844 tests passed)
- `cargo +nightly-2026-04-14 clippy -p fabro-config -p fabro-model -p
fabro-server --all-targets -- -D warnings`
## Summary
This PR moves Fabro’s provider/model catalog toward settings-driven
provider identity by replacing the closed provider schema at the
API/auth/model boundary with `ProviderId`, then loading built-in
provider and model metadata from embedded per-provider TOML files.
The immediate result is that built-ins now use the same settings-shaped
catalog data that custom providers will use later, while request-serving
paths still keep the existing bootstrap/default catalog behavior until
the resolved-catalog plumbing lands.
## Changes
- Replaces API-facing provider enum usage with string-backed
`ProviderId`, including OpenAPI/progenitor replacements and regenerated
TypeScript client models.
- Routes model, auth, billing, CLI, server, and workflow call sites
through provider IDs where they cross product identity boundaries.
- Builds `Catalog` from settings-shaped provider/model data with
validation for adapter keys, OpenAI-compatible `base_url`, duplicate
aliases, provider defaults, disabled entries, model controls, and
per-speed cost rows.
- Replaces `catalog.json` with embedded provider TOML files under
`lib/crates/fabro-model/src/catalog/providers/`.
- Adds an explicit `fabro_model::bootstrap_catalog` hatch for
setup/install paths and extends the dev policy test to keep bootstrap
access contained.
- Preserves public training and knowledge-cutoff labels in LLM model
settings while still accepting bare TOML dates.
## Verification
- `cargo nextest run -p fabro-model -p fabro-config -p fabro-api` — 416
passed
- `cargo nextest run -p fabro-dev --features dev
bootstrap_catalog_references_stay_in_allowlist` — 1 passed
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo build --workspace`
- `git diff --check`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
## Summary
Adds the Phase 1 settings surface for gateway-backed LLM providers. This
was prompted by @haroldolivieri's Portkey/Bedrock field report on PR
#207, which showed that gateway auth and routing often live in custom
headers rather than the adapter's primary API-key header.
This PR is schema and seam work only. It does not make settings-defined
providers runnable yet; later phases still own ProviderId migration,
catalog construction, auth resolution, and production adapter
registration.
## Changes
- add typed `extra_headers` values to `[llm.providers.<id>]`
- support explicit `literal`, `env`, and `credential` header value forms
while rejecting bare strings, empty values, ambiguous tables, and
unknown keys
- cover whole-map header merge behavior and adapter header pass-through
tests
- update the settings-driven LLM plan with the Phase 1 gateway header
attribution and completion notes
## Non-goals
- does not make settings-defined providers runnable yet
- does not migrate ProviderId/OpenAPI/auth resolver/runtime catalog
plumbing
- does not route Codex OAuth through custom provider settings
## Tests
- `cargo nextest run -p fabro-config -p fabro-llm`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy -p fabro-config -p fabro-llm
--all-targets -- -D warnings`
- `git diff --check origin/main...HEAD`
## Post-Deploy Monitoring & Validation
No additional operational monitoring required. This is schema and
adapter-seam coverage only; production provider registration and runtime
credential/header resolution remain deferred.
## Attribution
Motivated by @haroldolivieri's Portkey/Bedrock report on PR #207:
https://github.com/fabro-sh/fabro/pull/207#issuecomment-4377929769
Commits include `Co-authored-by: Haroldo Olivieri
<6575718+haroldolivieri@users.noreply.github.com>`.
---
Compound Engineered: Codex, `ce:work`.
---------
Co-authored-by: Haroldo Olivieri <6575718+haroldolivieri@users.noreply.github.com>
## Summary
- `fabro validate path/to/workflow.fabro` now auto-discovers a sibling
`workflow.toml` and loads its `[run.inputs]`, so templated graphs
validate the same way they do when invoked by name or by toml path.
- The discovery is opt-in to the user's specific graph: we only pick up
the sibling toml if its `[workflow].graph` resolves back to the `.fabro`
the user passed. Unrelated tomls in the same directory are ignored.
## Why
`fabro validate` is the natural fast-feedback tool for CI/pre-commit
hooks that iterate on changed `.fabro` files. Previously, a graph using
`{{ inputs.* }}` would fail with a generic MiniJinja "undefined value"
error when validated by path, even when a sibling `workflow.toml`
defined those inputs. The other two invocation forms (by name, by toml)
worked, which made the path form a usability cliff.
Fixes#195.
## Test plan
- [x] New integration test:
`bare_fabro_picks_up_sibling_workflow_toml_inputs` validates
`test/templated_inputs/workflow.fabro` (uses `{{ inputs.app_dir }}`) and
expects `Validation: OK`.
- [x] New unit tests in `fabro-config::project`:
- `resolve_workflow_path_picks_up_sibling_workflow_toml` — happy path.
- `resolve_workflow_path_ignores_sibling_toml_pointing_elsewhere` —
guard: don't apply an unrelated sibling toml.
- [x] `cargo nextest run --workspace` — 5585 tests pass.
- [x] `cargo +nightly-2026-04-14 fmt --check --all`, `clippy --workspace
--all-targets -- -D warnings` clean.
- [x] Manual: `fabro validate /tmp/fabro-issue-195/workflow.fabro`
(templated graph + sibling toml with `[run.inputs]`) prints `Validation:
OK`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Nate Aune <118984+natea@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Adds run-level controls for clone behavior, managed run branch
setup/pushes, and metadata branch writes/pushes so workflows can opt out
of Fabro-managed Git behavior without relying on provider-specific
`skip_clone` settings. This closesfabro-sh/fabro#240.
## What Changed
- Introduced `[run.clone]`, `[run.run_branch]`, and `[run.meta_branch]`
settings with defaults that preserve current behavior.
- Removed user-facing `skip_clone` from Docker/Daytona config while
mapping the new run-level clone setting into the internal sandbox
runtime options.
- Gated run branch setup/push, metadata branch writer creation/push, and
PR branch output on the new settings.
- Enforced invalid combinations: pull requests require an enabled pushed
run branch, and disabling the run branch also disables metadata branch
behavior.
- Updated OpenAPI, the generated TypeScript API client, frontend fixture
data, and docs for the new configuration shape.
## Testing
- `cargo nextest run -p fabro-config -p fabro-types -p fabro-workflow -p
fabro-server`
- `cargo build -p fabro-api`
- `cd lib/packages/fabro-api-client && bun run generate`
- `cd lib/packages/fabro-api-client && bun run typecheck`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test`
- `cargo build --workspace`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo insta pending-snapshots`
- `git diff --check`
## Post-Deploy Monitoring & Validation
- Validation window: first 24 hours after release; owner: release
owner/on-call engineer.
- Log queries/search terms: `run_branch`, `meta_branch`,
`clone.enabled`, `skip_clone`, `pull request requires an enabled pushed
run branch`, `metadata branch`.
- Healthy signals: runs without custom branch config continue creating
and pushing run/meta branches; runs with `[run.clone] enabled = false`
start provider sandboxes without cloning; runs with branch pushes
disabled complete without Git push errors.
- Failure signals: increased run startup failures for Docker/Daytona,
unexpected PR creation conflicts, missing metadata for default-config
runs, or validation errors for configurations that previously used
default settings.
- Mitigation trigger: if default-config runs stop producing expected
branch/metadata artifacts or sandbox startup failures increase, roll
back the release or temporarily restore previous defaults while
investigating the run-level setting resolution path.
---
[](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
The local sandbox provider hard-codes `/bin/bash` at three call sites in
`fabro-sandbox/src/local.rs` (`exec_command`, `exec_command_streaming`,
`spawn_stdio_process`). NixOS doesn't ship `/bin/bash` — only `/bin/sh`
and `/usr/bin/env` are managed under `/`, with bash living on `PATH` at
`/run/current-system/sw/bin/bash`. The result: a first run on NixOS dies
on the very first sandbox call (the git probe) with `No such file or
directory (os error 2)`, surfaced as `sandbox git unavailable`.
## Fix
Switch all three sites from `Command::new("/bin/bash")` to
`Command::new("bash")`. `PATH` is already preserved by
`filtered_env_vars` (and explicitly tested at `local.rs:1164`), so
libc's `execvp` lookup resolves bash on every distribution that has it
installed, including NixOS, without forcing users to symlink
`/bin/bash`.
The `/bin/bash` references in `docker.rs` are unaffected — those execute
inside containers where the path always exists.
## Credit
Diagnosis and proposed fix by @allouis in #232 — they ran the
PATH-lookup variant locally on NixOS 26.05 and confirmed workflows ran
cleanly without the symlink workaround.
Closes#232
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via
[Claude Code](https://claude.com/claude-code)
Co-authored-by: Fabien O'Carroll <3218915+allouis@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
This lays the groundwork for settings-driven LLM providers and models
without switching production routing yet. The new schemas and shared
vocabulary let later catalog construction treat provider/model identity
as data while keeping adapter behavior and control values Rust-owned.
## What changed
- Added `[llm.providers]` and `[llm.models]` settings layers with sparse
per-entry merging, whole-array replacement for credential/alias/control
lists, TOML date support for `knowledge_cutoff`, and typed `credential:`
/ `env:` references that reject literal secrets.
- Added `ProviderId`, `ModelId`, and a shared `ReasoningEffort` enum in
`fabro-model`, plus adapter metadata for `anthropic`, `openai`,
`gemini`, and `openai_compatible`.
- Added a matching `fabro-llm` adapter factory registry with parity
tests to keep metadata keys and factory keys in sync.
- Added `[run.model.controls]` defaults through config resolution and
runtime settings types.
- Added a workspace policy test to prevent future `bootstrap_catalog`
use outside install/test-support paths.
### Plan Summary
- This is the foundation slice of the settings-driven catalog plan.
- Production still uses the existing `Provider` enum and
`Catalog::builtin()` call paths.
- ProviderId routing, OpenAPI regeneration, auth resolver changes,
resolved `Arc<Catalog>` injection, typed request speed, and per-speed
billing are deferred follow-ups.
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: fabro-bot <fabro-bot@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Wait for protocol activity from the fake ACP agent before cancelling instead of polling a temp file. This keeps the test synchronized with session/prompt handling under CI load.
## 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`.
## Summary
Adds a stdio-based Fabro MCP server so MCP clients can manage Fabro
workflow runs through the authenticated `fabro` CLI, without a separate
MCP auth flow.
## What Changed
- Adds `fabro mcp start`, `fabro mcp config`, and `fabro mcp init
<agent>` for launching and configuring the MCP server.
- Introduces a new `fabro-mcp-server` crate with run-management tools:
- `fabro_run_create`
- `fabro_run_search`
- `fabro_run_interact`
- `fabro_run_gather`
- `fabro_run_events`
- Reuses the CLI's authenticated server connection behavior, including
OAuth refresh, dev-token/local-server handling, explicit server targets,
proxy behavior, and stdio env/cwd isolation.
- Moves shared run-manifest construction into `fabro-manifest` so CLI
runs and MCP-created runs use the same override semantics.
- Extends MCP client stdio support with configured cwd and exact
environment handling for reliable spawned-server tests.
---------
Co-authored-by: fabro-sh-0530[bot] <281434857+fabro-sh-0530[bot]@users.noreply.github.com>
Co-authored-by: Fabro <noreply@fabro.sh>
Skip cancellation signaling when the durable run projection is already terminal so deletion cannot append cancelled failure events after a successful run.
Use the canonical nested Run DTO directly and reject the old flat run summary JSON shape. Update store, server, CLI, and fixtures to read and produce canonical fields.
Reuse shared frontend formatting and SSE dedupe helpers, tighten typed sandbox handling, remove obsolete run DTOs, and collapse auth-session revoke into a single store operation.
Return canonical Run payloads across run list, board, create, and lifecycle endpoints. Move archive state out of RunStatus and into lifecycle metadata, split sandbox runtime from planned sandbox data, and separate static pull request records from live pull request details.
Regenerate the TypeScript API client and migrate web, CLI, server, store, workflow, and API tests to the new contract.
Split the unified list into separate Browser and CLI panels and drop
provider, login, kind/current badges, and user agent. Each panel shows
just the timestamps that matter, with revoke gated to CLI sessions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Render /profile/sessions from the new GET /api/v1/auth/sessions API.
The page shows the current browser session and active CLI sessions in one
list, with revoke buttons gated by the backend-supplied revocable field.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expose browser and CLI auth sessions through a normalized API, and allow revoking active CLI refresh-token chains while keeping browser sessions non-revocable for v1.