mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
2057 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bc0bda73a6
|
feat(web): add server-managed Environments CRUD settings UI (#462)
Some checks are pending
Rust / Clippy (push) Waiting to run
Rust / Format (push) Waiting to run
TypeScript / Build (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
## What Adds a CRUD interface for **server-managed Environments** at `/settings/environments`, driven by the `/api/v1/environments` REST API (list / create / retrieve / replace / delete), and reshapes how built-in environments are provisioned and protected. The page lives in the **Workflows** settings nav section (also introduced in this branch), positioned before Variables. ## Why The Environments REST API shipped (#453) but had no UI — environments could only be managed via the API/CLI. This gives operators a web UI alongside Variables and Secrets, and along the way tightens the model: environments are seeded at install time (not silently re-created on every boot), and the `default` fallback is an ordinary, deletable environment. ## Web UI **Pages & component** - `settings-environments.tsx` — list view: provider badge, image/resource summary, row actions (Edit/Delete). **"New environment" is a dropdown** of the enabled sandbox providers; the chosen provider is fixed for the environment's lifetime. - `settings-environments-new.tsx` / `settings-environments-edit.tsx` — create/edit flows; create reads the provider from a query param. - `environment-form.tsx` — shared form, reorganized: - **General** panel (merged identity + image): id, and an **image-source selector** (Image reference *vs* inline Dockerfile) that shows, requires, and sends only the selected, mutually-exclusive source. - **Resources**: CPU / memory / disk as **range sliders** (CPU 1–8, memory 1–16 GB, disk 1–20 GB), each always writing a concrete value. - **Environment variables** key/value editor. - **Advanced** progressive-disclosure section holding **Network** (a single "Block all network access" toggle — allow-all vs block) and **Lifecycle** (preserve / stop-on-terminal / auto-stop). Opens by default when any advanced value is non-default. - The in-form **provider control and the Labels editor were removed** — labels remain API-managed and are round-tripped untouched so UI edits never clear them. **Data layer**: `environmentsApi` client, `queryKeys.environments`, `useEnvironments` / `useEnvironment` SWR hooks. **Nav & routing**: "Environments" item in the Workflows section before Variables; routes registered in `router.tsx`. ## Backend: seed at install, deletable `default` - **Seeding moved to install time.** The server no longer seeds built-ins on startup; `EnvironmentStore::load_or_seed` → `load` (load-only). A new public `seed_environments(dir)` (idempotent, preserves operator edits) is called by both the web installer and the CLI installer. An uninstalled instance therefore has no managed environments, and a run selecting an absent environment fails explicitly (`unknown environment: default`) rather than resurrecting a built-in. - **`default` is no longer protected.** The delete guard and the `Protected` error variant are gone; deleting `default` succeeds (204) and removes the run fallback on purpose — forcing an explicit choice. `local` is unchanged (reserved, in-memory). - **`volumes` removed** from environment settings across the OpenAPI spec, generated Rust + TS clients, config layers, sandbox/server/workflow plumbing, docs, and tests. ## API contract details honored - Edit sends the environment `revision` as `If-Match`; 409 conflicts surface a "changed since you opened it" message. - The REST API accepts inline Dockerfiles only — the form never sends a Dockerfile path. ## Verification - Rust: `cargo build` (touched crates) ✅, `cargo nextest -p fabro-environment` 21/21 ✅, server env unit + `tests/it` integration 2/2 + 15/15 ✅, `clippy` (nightly, touched crates, all targets) clean ✅, `fmt --check` clean ✅. Full `--workspace` suite not run here — worth a CI pass. - Web: `bun run typecheck` ✅, `bun run build` ✅, `environment-form.test.ts` 5/5 ✅. Web suite: 512 pass / 1 unrelated pre-existing `RunDetail` failure. - **Not visually verified in-browser** — the local app is login-gated and automated loads redirect to `/login`; rendering of the form, the New-environment dropdown, and `default` delete should be confirmed in a logged-in session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (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> Co-authored-by: Release Repro <release-repro@example.com> |
||
|
|
64ece23473
|
feat(llm): add OpenRouter as an opt-in built-in provider (#497)
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> |
||
|
|
62486c8103
|
fix(server): escalate automation materialization failures
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
|
||
|
|
eff3a5a9cb
|
refactor(llm): resolve request dispatch through the catalog route (#496)
PR 8 of the gateway refactor series (after #493) — the optional closer: `Client` dispatch goes through the route machinery #493 introduced, instead of an inline ad-hoc lookup. ## What's here `Client::resolve_provider`'s hand-rolled catalog hop (`catalog.get(model)` → provider id) becomes `adapter_registry::resolve_route`. Fallback order is byte-identical: explicit `request.provider` wins, then the model's catalog route, then the default provider, then the existing configuration error. This puts route resolution on the live request path, so the route-equivalence table from #493 now pins actual dispatch rather than a helper nothing calls: a new live-dispatch sweep asserts every built-in model's request lands on the provider its route names, alongside explicit-provider-wins and unknown-model-default pins. ## Scope notes - **No public API change** — `resolve_provider` is private; all frozen `Client` methods are untouched. - The route's `codec`/`deployment_id` still aren't handed to adapters: `ProviderAdapter::complete(&Request)` is frozen (prod-implemented in fabro-cli), and every allowed pairing equals the adapter's built-in codec until the feature PRs. This PR is deliberately just the dispatch seam, so the OpenRouter redo's Client-side wiring is a no-op. ## Verification - `cargo nextest run --workspace --no-fail-fast` (post-rebase onto #493's merge): green except the same 5 pre-existing environment-dependent fabro-workflow failures, identical on main - clippy `-D warnings` + pinned-nightly fmt clean - Wire snapshots untouched This closes the refactor series. Remaining: the already-open cost PR (#494), then the feature redos — OpenRouter (#438) and Bedrock (#459). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
23d3644787
|
feat(llm): catalog-estimated completion cost on Response (#494)
Standalone pre-OpenRouter step, pulled forward from the #438 triage (the gateway-refactor plan's "additive feature PR alongside the redo"): completion responses carry a USD cost with provenance. ## What's here **`Response.cost_usd` + `Response.cost_source`** — new optional fields (`skip_serializing_if` keeps the wire shape byte-identical when unset). `CostSource` (`authoritative` | `estimated`) lives in fabro-model's billing vocabulary next to `UsdMicros`/`TokenCounts`, since the API layer reuses it. **`fabro-llm/src/cost.rs`** — `estimate_cost_usd`, a thin wrapper over the existing `Catalog::price_tokens` billing machinery (billing-policy- and speed-aware), ported from #438's prototype with attribution. One fix over the prototype: model aliases and provider names are canonicalized before building the `ModelRef` — `ModelPricing::bill` rejects non-canonical refs, so the original would silently skip cost on alias requests (caught by a new test). **Client-level stamping** — one generic post-decode site instead of #438's ~8 per-adapter sites (which predate the codec refactor): `Client::complete` stamps blocking responses and `Client::stream` stamps `Finish` events, beneath the middleware chain so middleware observes final responses. Codecs stay wire-translation-only — zero wire-snapshot churn — and every registered adapter (including custom `register_provider` ones) gets the same treatment. Stamping never overwrites an existing cost, so future authoritative in-band costs (OpenRouter) take precedence by construction. **API surface** — `cost_usd`/`cost_source` on `CompletionResponse` (OpenAPI spec + handler + regenerated TS client). The streaming endpoint already carries cost implicitly since `Finish` events serialize the `Response` verbatim; this makes the blocking surface match. `CostSource` reuses the canonical fabro-model type via `with_replacement`, with the standard round-trip test pinning type identity and JSON parity. ## Deliberately not here (stays with the OpenRouter redo per the plan's hard rule) - Authoritative `usage.cost` parsing in the `openai_compatible` codec wire structs - Cached-token usage parsing (changes observable usage values) - Per-model `billing_policy` schema field ## Verification - `cargo nextest run --workspace --no-fail-fast`: 6701 passed; only the known 5 pre-existing environment-dependent fabro-workflow failures (identical on main) - All fabro-llm wire snapshots unmodified; new pins: cost estimation unit tests (incl. alias canonicalization), Client stamping tests (blocking, streaming, beneath middleware, no-catalog), fabro-api `CostSource` round-trip - clippy `-D warnings` + pinned-nightly fmt clean; `bun run typecheck` clean in fabro-web Independent of the route-vocabulary work in #493 — branches directly off main. After both land, the OpenRouter redo shrinks to config + typed codec params + authoritative-cost decode. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d10fcd5e91
|
refactor(model): put the codec on the route (#493)
PR 7 of the gateway refactor series (after #481, #488, #487, #489, #491) — the series capstone: the wire dialect becomes route vocabulary in fabro-model config instead of a structural implication of the adapter type. ## What's here **`fabro-model/src/codec.rs` (new)** — `CodecKind` (`anthropic_messages`, `openai_responses`, `openai_compatible`, `gemini_generate`; strum per house style). `CodecKind::default_for(AdapterKind)` reproduces the historical adapter→dialect fusion exactly. **Catalog schema** — optional `codec` on provider rows and model rows (the multiplexer case), sparse-merged with the existing `.or()` pattern. Omitted everywhere in the built-in catalog, so **all defaults reproduce today's routes**. Explicit pairings outside the adapter's default are rejected at catalog build (`UnsupportedProviderCodec` / `UnsupportedModelCodec`) so no new route combination is silently enabled by configuration — the field is vocabulary for the OpenRouter/Bedrock feature PRs, not a new capability. `Catalog::effective_codec` mirrors `effective_agent_profile`. fabro-config mirrors the field through `LlmLayer` (`ProviderSettings.codec`, `ModelSettings.codec`) and the catalog-settings conversion. **Route resolution** — `adapter_registry::resolve_route(catalog, model)` assembles `(provider row, model row)` into `Route { provider, transport, codec, deployment_id, billing_policy, agent_profile }`. **Route-equivalence table test** — every built-in model row pinned to its resolved tuple as an executable table (23 rows), with a coverage assert so a new built-in model can't land without a deliberate table edit. This is the "compat mapping as an executable table, not a comment" test from the plan. **`AdapterConfig` cleanup** — the OpenAI-only fields (`codex_mode`, `org_id`, `project_id`) move out of the shared struct into `AdapterKindOptions::OpenAi(OpenAiAdapterOptions)`; the client populates them only for OpenAi-kind routes, which is the only factory that ever read them. ## Deliberate scope cuts - **No per-model `billing_policy`** — that schema change exists solely for the OpenRouter redo, which owns it. - **`codec_params` and `supports_count_tokens` stay adapter-internal** — the registry `Route` carries what the catalog defines; the per-route knobs in the adapters' `RouteConfig` move out when a second codec/transport pairing actually exists (OpenRouter's anthropic skin / Bedrock). Wiring `resolve_route` into `Client` request dispatch is the optional PR 8 and is likewise deferred. - **No user-facing docs for `codec`** — every accepted value equals the default, so there is nothing actionable to document yet; docs land with the first feature PR that enables a non-default pairing. ## Verification - `cargo nextest run --workspace --no-fail-fast` (re-run post-rebase onto #491's merge): 6701 passed; the only failures are the same 5 pre-existing environment-dependent fabro-workflow failures noted in #491, identical on main - fabro-llm: 548 passed — all wire snapshots unmodified - clippy `-D warnings` + pinned-nightly fmt clean This ends the refactor series: the seams exist. Next up are a standalone cost PR (`cost.rs` + `Response.cost_usd`/`CostSource`, pulled forward from the #438 triage as its own pre-OpenRouter step) and then the feature redos — OpenRouter (#438: one TOML + typed codec params) and Bedrock (#459: sigv4/eventstream transport + config, private codec layer deleted). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
074f90c391
|
refactor(llm): consolidate the HTTP transport behind the codec seam (#491)
PR 6 of the gateway refactor series (after #481, #488, #487, #489): collapse the four per-adapter transport copies into one `transport` module. Net −157 lines, and every cross-adapter duplication flagged in the #487/#488 simplify findings is resolved here. ## What moved where **`transport.rs` (new)** — how bytes travel, dialect-blind: - `HttpTransport` (promoted from `providers::http_api::HttpApi`): client, auth key, base URL, timeouts - `LineReader` + `parse_retry_after` + `parse_rate_limit_headers` (moved from `providers::common`, re-export shims kept there for the frozen fabro-cli imports; `LineReader::new` keeps its 2-arg signature) - `complete_via_http` / `send_for_body`: blocking send with the shared timeout/error/status warn logs, non-2xx mapped through `Codec::decode_error` - `stream_via_http` + one SSE decode loop, parameterized by `SseFraming::{EventBlocks, DataLines}` — replaces the four verbatim `StreamLoop` + unfold copies and the four divergent framers (anthropic's `parse_sse_block`, openai's `parse_sse_message`, the inline data-line handling in openai_compatible/gemini, and fabro_server's private block parser) **`codec/mod.rs`** — gains the dialect-neutral pure helpers `parse_error_body` and `extract_system_prompt` (moved from `providers::common`), so the codec layer no longer imports from the transport-side providers module. **Adapters** — shrink to auth + route config + codec composition. `send_and_read_response` and its `error_code_field` parameter are deleted: the dialect error-body key now lives only in the codecs, and any future `decode_error` override applies to blocking and streaming paths alike. ## Unified SSE framing semantics (deliberate decisions) The four framers disagreed on edge cases; the shared framer picks one behavior, stated here rather than chosen silently: - data payloads are trimmed; multi-line `data:` payloads join with `\n`; CRLF tolerated in both modes - comment (`:`), blank, and non-data lines are skipped - events with an **empty payload are dropped** rather than handed to the decoder — previously anthropic would error the whole stream on a bare `data:` line and openai_compatible would feed the decoder an empty string (also an error); openai/gemini already skipped All streaming wire snapshots pass unmodified through the shared loop, and the framer has direct unit tests for these cases. ## Behavior notes (beyond the framing edge cases) - **Error values are byte-identical**: `Codec::decode_error`'s default is exactly the `parse_error_body("type")` + `error_from_status_code` path the deleted call sites inlined; gemini's gRPC-aware override is what its paths already used. - **Logging only**: gemini's blocking paths gain the shared timeout/error/status warn logs (they had none); count-tokens requests are uniformly tagged `operation="input_token_count"` (previously only openai's was). The openai count-tokens logging pin passes unchanged. - gemini's timeout error message now uses the configured provider name instead of a hardcoded `gemini:` prefix (visible only on custom-named gemini routes). ## Verification - `cargo nextest run --workspace`: green except the 5 pre-existing fabro-workflow failures that fail identically on main (environment-dependent, unrelated) - fabro-llm: 545 passed — all PR 0 wire snapshots unmodified - clippy `-D warnings` + pinned-nightly fmt clean - fabro-cli compiles against the frozen `providers::common::{LineReader, parse_retry_after}` paths Next in the series: PR 7 (codec on the route in fabro-model) — route vocabulary + the route-equivalence table test. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
548c1574d2
|
refactor(llm): extract codec/gemini_generate behind the Codec trait (#489)
Some checks failed
Rust / Format (push) Has been cancelled
Rust / Clippy (push) Has been cancelled
Rust / Generated Docs (push) Has been cancelled
Rust / Test (Linux) (push) Has been cancelled
Rust / Test (macOS) (push) Has been cancelled
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
## Summary Final dialect extraction in the gateway refactor series (after #481 / #485, sibling of #487 and #488): the Gemini `generateContent` wire translation moves out of `providers/gemini.rs` into `codec/gemini_generate/`, behind the `Codec` / `StreamDecoder` traits. The adapter becomes a thin transport shell (1,607 → ~400 lines) owning auth (`x-goog-api-key`), base URL, and the streaming byte loop; all translation is in the codec. Two commits, each independently green: 1. **Add the codec** (`wire`/`encode`/`decode`/`stream`/`mod`) — compiling but unused behind a scoped `dead_code` allow. 2. **Rewire the adapter** to it, migrate the ~22 unit tests, and add the previously missing stream-decoder tests. Gemini is the simplest route story in the series — no provider-name branching, no mode flags, count-tokens always available, no forced streaming — so there is no route config and **no `CodecParams` changes** (this PR is conflict-free with #487/#488 apart from one `mod` line; if it lands after them, the unit-struct `CodecParams` literals become `::default()` on rebase, mechanical). It does exercise two trait seams the other codecs don't: - **Fully-formed endpoints from the codec**: model-in-path `:generateContent` / `:streamGenerateContent?alt=sse` / `:countTokens` ride on `EncodedRequest.endpoint` (the count body wraps the request in `generateContentRequest`). - **The first `decode_error` override**: Gemini's gRPC-status mapping (`error_from_grpc_status` with HTTP-status fallback) moves behind the codec; the adapter feeds it status + body + retry-after. The send-side timeout mapping stays transport-side. Other moves, wholesale and already pure: synthetic-UUID tool-call/response ids, the id→name recovery map for `functionResponse`, usage arithmetic (cache subtraction + tool-use addition + thoughts→reasoning), default `safety_settings` injection (flagged profile-ish in a comment, unchanged), `thoughtSignature` round-trip, and the `provider_options.gemini` merge. `translate_messages` goes sync: file-backed Image/Audio/Document attachments resolve via the shared `attachments::resolve` (#485) before encode. The streaming decoder preserves Gemini's distinct stream-end contract exactly: data-only SSE (no event types, no `[DONE]`), and `finish()` synthesizes the `Finish` from accumulated state unconditionally at byte-stream end — there is no terminal wire event. ## Behavior preservation No behavior change. The 32 gemini wire snapshots from #471 (encode round-trips, attachments, response_format, provider_options merges, streaming happy path / tool deltas / reasoning deltas / the unconditional-Finish stream-end pin) pass unmodified, and the full fabro-llm suite is green at 525: all 22 migrated tests plus 10 new ones — 9 stream-decoder unit tests (gemini previously had **zero**: text/thought deltas, reasoning→text transition, single-chunk function calls, finish-reason handling, Finish synthesis with and without a wire finish reason, ToolCalls inference, malformed-chunk errors) and 1 pinning the three model-in-path endpoints. ## Testing - `cargo nextest run -p fabro-llm` — 525 passed (126 wire snapshots included) - `cargo check --workspace` - `cargo +nightly-2026-04-14 clippy -p fabro-llm --all-targets -- -D warnings` - `cargo +nightly-2026-04-14 fmt --check` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
45f564cbfe
|
refactor(llm): extract codec/openai_responses behind the Codec trait (#487)
## Summary Next dialect extraction in the gateway refactor series (after #481 / #485, sibling of the anthropic extraction): the OpenAI Responses API wire translation moves out of `providers/openai.rs` into `codec/openai_responses/`, behind the `Codec` / `StreamDecoder` traits. The adapter becomes a thin transport shell (2,784 → 692 lines) owning auth (bearer + org/project headers), base URL, the streaming byte loop, and route config; all translation is in the codec. Two commits, each independently green: 1. **Add the codec** (`wire`/`encode`/`decode`/`stream`/`mod`) — compiling but unused behind a scoped `dead_code` allow. 2. **Rewire the adapter** to it and migrate the ~54 unit tests into the codec submodules they now cover. Key moves: - **Codex mode splits along the codec seam**: encode-side param omission (`temperature`/`top_p`/`max_output_tokens` omitted, `instructions` always sent) rides on a new `CodecParams::openai_codex` flag; the transport-side half (blocking requests served via streaming) is route config on the adapter. No provider-name branching — codex is OpenAI's only route split. - **`translate_input` goes sync**: its only async-ness was file-path image loading, now handled by the shared `attachments::resolve` (#485) in the adapter before encode (images only; audio/documents render as text placeholders in the codec without I/O). - The invariant-dense pieces move wholesale, already pure: opaque `openai_reasoning`/`openai_message` item round-trip, the `fc_…`/`call_…` dual-id preservation via `provider_metadata`, custom-tool (apply_patch) emission and raw-input accumulation, `store: false` + `include: ["reasoning.encrypted_content"]`. - The SSE state machine becomes `SseAccumulator` behind `StreamDecoder`: the transport owns byte reading + framing; the decoder is fed framed `RawEvent`s, resolves the event type from the SSE `event:` line or the JSON `type` field, and `finish()` synthesizes nothing (`response.completed`/`incomplete` are the finishers — matching the old EOF behavior exactly). Coordination note: this PR makes the same unit→fielded `CodecParams` change as the sibling anthropic extraction (each adds only its own fields) — whichever lands second resolves a trivial field-union conflict in `codec/mod.rs`. ## Behavior preservation No behavior change. The 33 openai_responses wire snapshots from #471 (codex mode, dual-id round-trip, opaque items, attachment drop-on-error, response_format, streaming happy path / tool deltas / reasoning deltas / failure events) pass unmodified, and the full fabro-llm suite is back to count (516: all 54 migrated tests plus one new test pinning the count-tokens endpoint + filtered body on the codec). ## Testing - `cargo nextest run -p fabro-llm` — 516 passed (126 wire snapshots included) - `cargo check --workspace` - `cargo +nightly-2026-04-14 clippy -p fabro-llm --all-targets -- -D warnings` - `cargo +nightly-2026-04-14 fmt --check` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
269eca719f
|
refactor(llm): extract codec/anthropic_messages behind the Codec trait (#488)
## Summary Dialect extraction in the gateway refactor series (after #481 / #485, sibling of #487): the Anthropic Messages wire translation moves out of `providers/anthropic.rs` into `codec/anthropic_messages/`, behind the `Codec` / `StreamDecoder` traits. The adapter becomes a thin transport shell owning auth, base URL, the streaming byte loop, and route config; all translation is in the codec. Three commits, each independently green: 1. **Add the codec** (`wire`/`encode`/`decode`/`stream`/`mod`) — compiling but unused behind a scoped `dead_code` allow. 2. **Rewire the adapter** to it and migrate the ~70 unit tests into the codec submodules they now cover. 3. **Port #482's Claude Fable 5 handling into the codec layout** (see below). Key moves: - **Route config replaces the request-time `provider_name == "anthropic"` branches**: auth scheme (x-api-key vs bearer), version/beta headers, the count-tokens availability gate, and Kimi-over-anthropic forced streaming resolve once per call into a `RouteConfig`. Dialect headers ride on `CodecParams` (`AnthropicVersion::Header("2023-06-01")` + beta-header emission for the direct route; inert defaults for Kimi). - **`build_api_request`'s `(ApiRequest, RequestBuilder)` dual-return dies**: codec `encode` produces body + headers as data (`EncodedRequest`); the transport applies them. This also kills the duplicated header rebuild in `count_input_tokens`. - **Encode goes sync**: file-backed Image/Document attachments resolve to inline data via the shared `attachments::resolve` (#485) in the adapter before encode (drop-on-error preserved; audio stays a text placeholder in the codec). - The SSE state machine becomes `SseAccumulator` behind `StreamDecoder`: the transport owns byte reading + `\n\n` framing; the decoder is fed framed `RawEvent`s. `finish()` returns nothing — `message_stop` is the only finisher, matching today's no-synthesis contract. - json_schema synthetic-tool machinery (encode injection, decode extraction, stream rewrite) moves intact around the shared `SYNTHETIC_TOOL_NAME`. ### The #482 (Claude Fable 5) port #482 modifies the old-layout `anthropic.rs` directly, so this branch re-homes its behavior into the codec structure (commit 3): `stop_details` on the wire type, the Fable encode gates keyed off the deployment id (no default adaptive `thinking`, no `temperature`/`top_p`, no legacy 1M-context beta header — which now lands **once** instead of twice, since both routes share `build_headers`), refusal → failover-eligible content-filter errors in decode and stream, and the `validate_request` rejection of manual thinking configs. The port is inert until the Fable catalog entry lands. Validated by merging #482's head into this branch on a scratch branch: the only conflict is `anthropic.rs` (resolved as this branch's version), and **all of #482's Fable/refusal tests pass against the codec implementation** (521 fabro-llm tests + fabro-model/fabro-workflow 1286 green on the merged tree). If #482 merges first, this PR's rebase resolves the same single-file conflict the same way. Coordination note: this PR makes the same unit→fielded `CodecParams` change as #487 (each adds only its own fields) — whichever lands second resolves a trivial field-union conflict in `codec/mod.rs`. ## Behavior preservation No behavior change. The anthropic wire snapshots from #471 (direct route, Kimi-over-anthropic bearer/no-version pin, prompt-cache with catalog, json_schema, count-tokens wire, streaming happy path / tool deltas / error events / no-message_stop-no-Finish) pass unmodified, and the full fabro-llm suite is back to count (515). ## Testing - `cargo nextest run -p fabro-llm` — 515 passed (126 wire snapshots included) - Scratch-merge validation against #482's head — 521 passed incl. its 6 Fable/refusal tests; `cargo nextest run -p fabro-model -p fabro-workflow` — 1286 passed - `cargo build --workspace` - `cargo +nightly-2026-04-14 clippy -p fabro-llm --all-targets -- -D warnings` - `cargo +nightly-2026-04-14 fmt --check` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a4e8987da8
|
feat(llm): add Claude Fable 5 support (#482)
## 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> |
||
|
|
8ed47d31ba
|
refactor(llm): add attachment-resolution infra (#485)
## Summary Next step of the gateway refactor (after #481): a small, codec-agnostic step for resolving file-backed attachments to inline data, shared by the per-dialect codec extractions that follow (anthropic, openai_responses, gemini). Codec `encode` is sync and never touches the filesystem. Today each adapter loads file-path `Image`/`Document`/`Audio` parts inline via `common::load_file_as_base64` mid-translation; the codec split needs that I/O hoisted out so encode can stay pure. `attachments::resolve` does it: clone the request, load each file-path part (per the caller's `AttachmentPolicy`) into inline bytes + MIME, drop the part on load error (the long-standing contract), and leave non-file URLs and already-inline data untouched. - `AttachmentPolicy { images, documents, audio }` — each dialect adapter constructs the policy it wants when it wires this in (anthropic: images+documents; openai: images only; gemini: all three). - `common::load_file_bytes` (raw bytes + MIME) factored out of `load_file_as_base64`, which now delegates to it. Splitting this out of the anthropic extraction makes the three dialect-codec PRs independent siblings — they can go up and land in parallel once this merges. Added ahead of its consumers, so the module sits behind a justified `dead_code` allow until the first dialect codec calls it (the anthropic PR drops the allow). No behavior change. ## Testing - `cargo nextest run -p fabro-llm` — 515 passed (including the 126 wire snapshots; byte-identical, nothing reachable changes) - `cargo check --workspace` - `cargo +nightly-2026-04-14 clippy -p fabro-llm --all-targets -- -D warnings` - `cargo +nightly-2026-04-14 fmt --check` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ce404cddef
|
Interpolation foundation (InterpString v2) (#472)
# Interpolation foundation (InterpString v2)
First step of unifying config-string interpolation across Fabro. This PR
is the
**behavior-neutral foundation** only — it introduces the type machinery
and a
clippy gate, but changes no field's interpolation behavior. The actual
field
work follows as separate stacked PRs, sequenced **reduce-first**:
narrowing
changes (demote fields that shouldn't interpolate, de-template DOT
attrs) land
before capability additions (resolve env in MCP / prepare / hooks).
## Why
Config strings interpolate `{{ ... }}` inconsistently today — some
fields
resolve `{{ env.X }}`, others are typed as if they do but silently pass
the
literal template text downstream. We're converging on three field types
(`String`, `InterpString`, and later an importable template for
prompts/goals)
with four namespaces (`env`, `vars`, `secrets`, `inputs`). This PR lays
the
`InterpString` foundation; it does not migrate any field.
## What's in it
- Segments generalize to `Token { namespace, name }` with a `Namespace`
enum
(`env`/`vars`/`secrets`/`inputs`). `secrets`/`inputs` are **reserved** —
parsed as tokens ahead of their resolvers.
- `ResolveCtx` with per-namespace lookups. `resolve_with()` fails loudly
(`Unavailable`) for a token whose namespace isn't provided in context;
`substitute_with()` substitutes provided namespaces and preserves the
rest.
`resolve()` / `substitute_variables()` are thin wrappers over one core
path.
- `ResolveEnvError` → `ResolveError { namespace, name, kind: Missing |
Unavailable }`
(message text unchanged for env/vars; the kind no longer bakes the
namespace
in, so it scales to four namespaces without an enum explosion).
- `Provenance` tracks secret-sourced names alongside env-sourced, for
uniform
redaction later.
- **`as_source()` is clippy-gated** (`disallowed-methods`). It keeps its
name;
every call site carries an `#[expect(..., reason)]` classifying it
(serialization, error display, known-leak-pending-fix, demotion-pending,
test). The lint turns the leak surface into a greppable, reasoned
work-list
and the method stays for its permanent uses (serde round-trip of the
unresolved template + diagnostics).
- fabro-server: five duplicate `process_env_var` facades and two
duplicate
`resolve_interp` helpers consolidated into one `crate::interp` module.
## Behavior changes (honest list)
- **`{{ secrets.* }}` / `{{ inputs.* }}` are now reserved.** On main
they
weren't recognized as tokens → silent literal passthrough. Now, at
`resolve()` consumers they **fail loud** (`Unavailable`) instead of
passing
the literal string through (nobody wants the literal characters as a
value —
strictly better, but technically a change). At `as_source` sites they
round-trip unchanged. Actual resolution lands in later enhancing PRs.
- Some fabro-server resolution errors gain a `"failed to resolve
<source>"`
context line.
Otherwise behavior-neutral: every field resolves exactly as it did on
main.
## What's deferred to follow-up PRs (reduce-first order)
- **Reducing / cleanup (next):** demote leak fields to `String`
(`run.model.*`, `cli.exec.model.*`, `run.git.author.*`,
`run.scm.owner/repository`); de-template `condition`/`label`/`model`/
`provider`/`speed` and `output_schema`.
- **Enhancing (after):** resolve `{{ env.* }}` in MCP transports,
prepare
steps, and hooks; wire `secrets`/`inputs`.
## Verification
- `cargo build --workspace`
- `cargo nextest run --workspace` → 6449 passed, 181 skipped
- `cargo +nightly fmt --check --all`
- `cargo +nightly clippy --workspace --all-targets -- -D warnings` →
clean
## Reviewer notes
- The reserved-namespace `Unavailable` error for `secrets`/`inputs` is
**intentional**, not a missing case — they're parsed ahead of their
resolvers so misuse fails loud instead of leaking.
- `as_source` is clippy-gated but keeps its name deliberately — the gate
is
the enforcement; renaming was avoided as unnecessary churn.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
3985eaf1d7
|
refactor(llm): introduce Codec trait seam + extract openai_compatible (#481)
## What Introduces the `Codec` / `StreamDecoder` trait seam in `fabro-llm` and extracts the OpenAI Chat Completions wire logic behind it as the first conforming codec. Two commits: 1. **`codec/mod.rs`** — the pure translation contract (`encode` / `decode_response` / `stream_decoder`, plus defaulted `encode_count_tokens` / `decode_count_tokens` / `decode_error`) and its data types (`CodecCtx`, `CodecParams`, `EncodedRequest`, `RawEvent`). A codec knows *what the bytes say*; it owns no HTTP, auth, or base URL. 2. **`codec/openai_compatible/`** — the Chat Completions codec split into `wire` / `translate` / `request` / `response` / `stream`. `providers/openai_compatible.rs` shrinks from 1,608 → ~330 lines: a thin transport shell that keeps the public struct/builders/auth/`validate_request`, owns the streaming byte loop + SSE `data:` framing, and delegates all translation to the codec. The two hand-rolled stream unfolds collapse into one. This is the first step of a gateway refactor that separates codec (wire dialect) from transport/auth/route, so later work (Bedrock, OpenRouter) becomes mostly config rather than parallel adapters. ## Behavior No behavior change. The public adapter API (`OpenAiCompatibleAdapter::new` / `with_name` / `with_catalog` / …) is unchanged, and **all 126 wire snapshots pass without edits** — the parity proof that the extracted codec produces byte-identical output. The 29 in-module unit tests move into the codec submodules alongside the code they exercise. ## On the trait `openai_compatible` is the simplest dialect, so its `impl Codec` is just three methods — count-tokens and error mapping inherit the defaults. The contract is defined in full now (a scoped `dead_code` allow on `codec/mod.rs` covers the seams the anthropic/openai/gemini codecs will exercise in follow-up PRs) so those extractions only *override* methods, never extend the trait. Extracting a real codec refined two trait signatures vs. the initial sketch: the canonical `Request` lives in `CodecCtx` (decoders need it for tool-argument parsing and the stream model fallback), and the header-parsed `rate_limit` threads into `decode_response` / `stream_decoder`. `on_event` returns `Result` so dialect error events propagate as stream errors. ## Tests - `cargo nextest run -p fabro-llm` — 515 passed (incl. 126 wire snapshots, unmodified) - `cargo nextest run --workspace` — green - fabro-agent `parity_matrix` (the frozen `OpenAiCompatibleAdapter` contract) — green - `cargo +nightly fmt --check` / `clippy --all-targets -- -D warnings` — clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9e30804ae0
|
test(llm): refresh wire snapshots for omitted null Message fields (#480)
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
## Summary Fixes the 33 `fabro-llm::it wire::*` snapshot failures currently red on `main`. These are a **semantic merge conflict** between two PRs that landed in parallel, not a behavior regression: - **#450** made the canonical `fabro_types::Message` omit absent optional fields (`name`, `tool_call_id`) via `#[serde(skip_serializing_if = "Option::is_none")]`, to match the OpenAPI completions wire contract. - **#471** added the per-dialect wire snapshots in parallel, authored against the older shape that emitted explicit `"name": null` / `"tool_call_id": null`. Each PR was green on its own branch (#450 never contained #471's snapshots; #471 predated #450's serde change). They only collided once both sat on `main` together — and because the serde attribute and the snapshots live in different files, there was no textual git conflict to flag it at merge time. ## What changed Regenerated the 33 affected snapshots (anthropic / gemini / openai_compatible / openai_responses) via `cargo insta accept`. The **only** change in every snapshot is the removal of the two trailing null fields: ```diff - ], - "name": null, - "tool_call_id": null + ] ``` No decode/stream behavior changed; the new shape is the intended canonical serialization. ## Test plan - [x] `cargo nextest run -p fabro-llm` — 515 passed, 0 failed - [x] Verified the diff across all 33 snapshots is uniformly the null-field omission (plus the `],`→`]` reflow), nothing else 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d590122531
|
feat: chat-driven workflow builder at /playground (#450)
## Summary
Adds a new `/playground` route where users build a Fabro workflow by
chatting with Ask Fabro on the right while watching a live canvas
re-render on the left. The workflow can be downloaded as a `.fabro.zip`
or — eventually — launched as a real Fabro run; today the "Run for
real" button POSTs to `/api/v1/runs` and redirects to the resulting
`/runs/{id}` page, with a placeholder project/repo/folder picker.
The feature is built as a standalone component subtree under
`apps/fabro-web/app/components/playground/` with no `AppShell` or
`react-router` dependencies, so it can be re-embedded in other contexts
later by passing `chatEndpoint`, `authMode`, and an optional
`realRunRedirect` prop.
## What changed
**Frontend (`apps/fabro-web/`)**
- New `/playground` route + `<Playground>` component tree.
- Live SVG canvas via `@viz-js/viz` with click-to-inspect (read-only
node detail panel), pan, zoom, fit-to-window, and a simulated walk
through the graph driven by a Play button.
- Docked chat sidebar (assistant-ui) wired to the new
`/api/v1/playground/chat` endpoint, with auto-retry on parse failure
and a playground-specific tool-call summary that reads
`Wrote workflow.fabro (N nodes, M edges)`.
- File tabs (`workflow.fabro` / `workflow.toml` / `README.md`),
`.fabro.zip` download via `fflate`, and a "Run for real" toolbar
button that POSTs an inline `RunManifest` to `/api/v1/runs`.
- Draft persists across page refreshes via `localStorage`.
**Backend (`lib/crates/fabro-server/`)**
- New `POST /api/v1/playground/chat` SSE endpoint. Server is stateless
across turns: each request carries the full draft, the server runs
the LLM with a single `write_workflow_file` tool, streams
`StreamEvent` frames back, and lets the client own diffing/animating
the result into the canvas.
- Request-size caps before the LLM call (50 messages, 100 nodes, 200
edges) so a misbehaving or malicious client can't drag multi-MB
transcripts through token billing.
**Spec / wire contract**
- OpenAPI: new `playground/chat` operation + four new schemas
(`CreatePlaygroundChatRequest`, `PlaygroundWorkflowDraft`,
`PlaygroundWorkflowNode`, `PlaygroundWorkflowEdge`).
- `lib/packages/fabro-api-client` not regenerated yet (the playground
uses raw `fetch`); reviewers who want the TS client to pick up the
new types can run `bun run generate` in that package.
## Key design decisions
1. **Single `write_workflow_file` tool, not six per-op tools.** The
first cut exposed `add_node`/`update_node`/`connect`/etc. as
discrete tool calls. The model would routinely add nodes without
wiring them up, leaving the canvas in a broken half-state. Pivoted
to a single tool that takes the full new `workflow.fabro` content;
the browser parses the DOT, diffs it against the local draft, and
animates the resulting reducer ops in. The model only has to "get
the file right", and the canvas still paints node-by-node thanks
to the client-side animator.
2. **Stateless server.** Each chat turn POSTs the full current draft;
nothing is persisted server-side. Keeps the endpoint cheap, makes
refresh-resumption trivial (browser owns the truth), and means the
same endpoint can later sit behind a rate-limited anonymous variant
without growing per-session state.
3. **Standalone component subtree.** `<Playground>` has no
`AppShell`/router/store dependencies. All cross-cutting concerns
flow in as props (`chatEndpoint`, `authMode`, `realRunRedirect`).
This is the structural hook that makes future re-embedding possible
without a refactor.
4. **Chat is the only mutation path.** Click-to-inspect on the canvas
is read-only. Bi-directional canvas editing was explicitly cut from
scope to keep one source of truth for "how the workflow changed."
5. **Inline `RunManifest` instead of temp-dir-then-clone.** The
playground has no project to run against, so the `Run for real`
modal builds a `RunManifest` that carries the full DOT and
`workflow.toml` source inline (`workflows[key].{source, config}`).
`cwd` is pinned to a fixed `/tmp/fabro-playground` constant — no
LLM-controlled segment in a filesystem-looking field.
6. **React effects policy compliance.** All `useEffect` calls in
playground component code go through the existing primitives in
`app/hooks/effects.ts` (`useDocumentEvent`, `useInterval`) or a
purpose-named hook (`useCanvasRender`).
## Still outstanding (planned follow-ups)
- [ ] **Actually kicking off the ad-hoc run.** "Run for real" today
POSTs a manifest with a placeholder project/repo/folder
fieldset. The intent is to reuse the project-picker pattern
being introduced on the in-flight automations branch — once
that pattern lands, the disabled inputs in
`run-for-real-modal.tsx` become the live surface.
- [ ] **Header link to `/playground`.** No nav entry yet; users have
to type the URL directly.
- [ ] **Live SSE-driven canvas overlay** via
`GET /api/v1/runs/{id}/attach` — currently the modal redirects
to the standard run-view page; the "watch it build on the
playground canvas" experience comes when the `stage.*` events
are wired through.
- [ ] **Regenerate `lib/packages/fabro-api-client`** so the new types
ship to TS consumers.
- [ ] **Smoke test:** end-to-end download → unzip →
`fabro run <name>` round-trip.
- [ ] **`scripts/build.ts` dist-symlink bug:** `pruneOldBuilds` can
delete the directory `apps/fabro-web/dist` points at, which
pins the dev server in 503 "build in progress" forever.
Workaround documented; the real fix is a separate PR.
## Test plan
- [ ] `cd apps/fabro-web && bun run test app/components/playground/` —
111 tests pass
- [ ] `cd apps/fabro-web && bun run typecheck` — clean
- [ ] `cargo test -p fabro-server playground` — 6 tests pass
- [ ] Visit `/playground`; the canvas renders the welcome `start → ??? →
exit` ghost.
- [ ] Type "build me a release-notes workflow" in chat; nodes/edges
animate in; ack reads `Wrote workflow.fabro (N nodes, M edges)`.
- [ ] Click a node → inspector panel populates; click empty canvas →
deselects.
- [ ] Click `Simulate`; nodes light up `start → ... → exit` along the
resolved path.
- [ ] Click `Download .fabro`; unzip; `cd <unzipped> && fabro run
<name>` runs locally.
- [ ] Click `Run for real` → modal opens → confirm → POST succeeds →
redirected to `/runs/{id}` → run executes.
- [ ] Refresh the page; the draft persists from localStorage.
- [ ] Click `Start over` → `Yes`; canvas resets to welcome state.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
eb5d8c53f9
|
fix(llm): stamp configured provider name into responses and error details (#479)
## What Threads the configured provider name through the response and error paths so custom-named providers report their real identity. Previously several sites used hardcoded literals: - streamed `Response.provider` always said `"anthropic"` / `"openai"` / `"gemini"` regardless of the configured name; - OpenAI's **non-stream** `Response.provider` ignored `with_name` entirely; - `ProviderErrorDetail.provider` in error paths (stream error events, HTTP/gRPC status mapping, request error contexts) carried the same literals. Now the name flows through anthropic's `StreamAccumulator`, openai's `SseStreamState` / error-json mapper / complete + error paths, and gemini's stream state and error helpers. ## Why One adapter code path already serves multiple providers — e.g. Kimi runs through the anthropic adapter via `with_name`, and the seven compat providers share one adapter. The "this file == this provider" assumption baked into the literals is wrong for those routes: a Kimi request that 429s reported `"Server error from anthropic"`, and its usage/error records were misattributed. The fix was also inconsistent before this change — some paths already used `provider_name` while the stream paths didn't, so the same request could be attributed differently depending on whether it streamed. This is foundational for an upcoming gateway refactor that makes codec/transport/provider orthogonal, where identity must travel with the route as data rather than being hardcoded per adapter. ## Behavior change The one intentional, behavior-visible delta: **custom-named providers** now report their configured name in `Response.provider` and `ProviderErrorDetail.provider`. Built-in default-named providers are byte-identical — the wire snapshot suite from #471 passes unmodified. Failover/retry policy keys on `ProviderErrorKind` and the `retryable()` / `failover_eligible()` flags, never on the provider string, so the error-detail change is display/log/signature-only (confirmed by a consumer sweep). ## Tests New per-dialect `custom_named_*` wire tests pin the intentional deltas, including a capture of the Kimi-over-anthropic route shape (bearer auth, no `anthropic-version` header) — useful as a pin for the route-config work later in the refactor. - `cargo nextest run -p fabro-llm` — 515 passed - `cargo +nightly fmt --check` / `clippy --all-targets -- -D warnings` — clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4ba2c11926
|
test(llm): pin provider wire behavior with per-dialect snapshot tests (#471)
## What
Adds wire snapshot tests pinning the exact encode/decode/stream behavior
of all four provider adapters — **109 tests / 117 insta snapshots** in
`fabro-llm/tests/it/wire/{anthropic,openai_compatible,openai_responses,gemini}.rs`,
driven by a shared canonical request corpus in `tests/it/support.rs`.
Tests only; no `src/` changes.
Each test points a real adapter at a local httpmock server,
side-channels the full received request (method, path, headers, body)
out of an `is_true` matcher closure, responds with a canned provider
body or scripted SSE transcript, and snapshots both the captured wire
request and the decoded canonical `Response` / `Vec<StreamEvent>`.
## Why
This is the behavior-pinning net for an upcoming refactor series that
separates fabro-llm's wire translation (codec) from transport/auth
concerns. The refactor must be behavior-preserving; these snapshots make
that checkable per PR instead of asserted. The anthropic and gemini
dialects have no twin coverage, so these tests are the only net for
those paths.
httpmock matcher-capture is used for all four dialects (rather than twin
request-logs for the OpenAI ones): the corpus deliberately exercises
shapes a strict twin would reject (provider_options merges,
response_format variants, bad-file-path attachment parts), one mechanism
is cheaper to maintain than two, and the twin already validates the
OpenAI dialects via `parity_matrix` and the server scenario tests.
## Coverage
Per dialect:
- **Encode** — multi-turn/system mapping, `tool_choice` ×4, tool
round-trips (incl. error results), thinking round-trips, attachments
(inline data, URL passthrough, silent bad-file-path drop, audio
fallback), `response_format` (json + json_schema), sampling params,
per-dialect `provider_options` merges (incl. the adapter-name-keyed
compat case), catalog-driven reasoning effort and prompt cache (beta
header), and the count-tokens wire route.
- **Decode** — finish-reason mappings, each dialect's distinct usage
arithmetic (anthropic direct cache reads with `reasoning_tokens: 0`;
openai-responses cached/reasoning subtraction; gemini `(prompt − cached)
+ tool_use_prompt`; compat prompt/completion only), thinking/tool/opaque
items, dual-id (`fc_…`/`call_…`) preservation.
- **Stream** — tool-call and reasoning deltas, error events (pinning
`retryable`/`failover_eligible`), and each dialect's stream-end
contract: anthropic emits no `Finish` without `message_stop`;
openai_compatible synthesizes one only if content started (both halves
of the minimax tolerance pinned); gemini synthesizes unconditionally.
Notable current behaviors pinned as-is (documented divergences, not
changed here): `ToolResult.image_data` is dropped by every encoder;
`ToolChoice::None` drops the whole `tools` array on anthropic only;
canonical `Thinking` parts are dropped by openai-responses/gemini;
`Request.metadata` is dropped by compat/gemini; gemini ignores
`reasoning_effort` and mints synthetic UUID tool-call/response ids
(normalized to `[UUID]` in snapshots).
## Test plan
- `cargo nextest run -p fabro-llm` — 498 passed (new `it` target run
twice to verify snapshot determinism incl. UUID normalization)
- `cargo +nightly fmt --check --all` / `cargo +nightly clippy -p
fabro-llm --all-targets -- -D warnings` — clean
- `fabro-llm/tests/integration.rs` and
`fabro-agent/tests/it/parity_matrix.rs` untouched
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
911e080f3c
|
Limit DOT templates to prompt + goal (#474)
# Limit DOT templates to prompt + goal
Part of unifying config interpolation in Fabro. Per the field taxonomy,
full
MiniJinja templates (`ImportableTemplate`) should be limited to
**`prompt`
(node) and `goal` (graph)** — the content fields that legitimately need
`{{ inputs.* }}` / `{{ goal }}`. Every other graph/node/edge attribute
should
be a plain value, not a Turing-complete template.
This is a **behavior-reducing** slice and is **independent of the
InterpString
foundation PR** (it touches the MiniJinja/template-engine path, not the
`InterpString` config path), so it branches off `main` and can be
reviewed on
its own.
## What changes
- `TemplateTransform::render_attrs` still renders node `prompt`
(unchanged) and
the graph `goal` (rendered separately, as before), but **no longer
renders**
`label`, `model`, `provider`, `speed`, edge `label`, or `condition`.
Those
are left as literal text.
- When a now-demoted attribute still contains `{{ … }}` / `{% … %}`, a
`detemplated_attribute` **warning** is emitted so authors can migrate
(the
syntax is now literal, not rendered).
- `condition` keeps its dedicated routing-expression evaluator
(`evaluate_condition` / `parse_condition_expr`); only the Jinja
pre-render is
removed, so routing still works exactly as before.
- `output_schema` becomes a string-or-`@file` value, not a template:
`FileInliningTransform` still resolves an `@file` reference but loads
its
contents **verbatim**, and neither the inline value nor the loaded file
is
MiniJinja-rendered.
`prompt` and `goal` are unaffected — both inline and `@file` forms are
still
MiniJinja-rendered (the `@` only selects whether the template is in-band
or
loaded from a file).
## Behavior change
`{{ … }}` in a demoted attribute (`label`/`model`/`provider`/`speed`/
`condition`/`output_schema`) is now **literal text** instead of being
rendered.
A parse-time `detemplated_attribute` warning flags any remaining
occurrences so
they're not silently dropped. This was rarely a sensible thing to do
anyway
(e.g. `label = "{{ goal }}"` would splat the entire goal into a short
display
label).
## Verification
- `cargo build --workspace`
- `cargo nextest run -p fabro-workflow` → 1164 passed
- `cargo +nightly fmt --check --all`
- `cargo +nightly clippy --workspace --all-targets -- -D warnings` →
clean
- No pending `insta` snapshots
## Tests
- `template_transform_renders_prompt_and_leaves_other_attrs_literal` —
`prompt`
still renders; node/graph/edge `label` stay literal; one migration
warning per
demoted label.
- `file_inlining_transform_does_not_render_templates_in_output_schema`
and
`file_inlining_transform_loads_output_schema_file_verbatim` —
`output_schema`
inline and `@file` contents are used verbatim, no Jinja.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
ccbc62ea5d
|
fix(ci): restore Rust checks | ||
|
|
e952bb4c7f
|
fix(config): disable Slack unless configured
Require an explicit server.integrations.slack table before Slack reports enabled or starts from vault tokens. |
||
|
|
160f587a1d
|
feat(install): enable only allowed sandbox providers
Add an "Allow local sandboxes" checkbox (checked by default) below the Docker/Daytona choice in the web installer, and stop unconditionally enabling all three providers when generating settings.toml. The wizard now enables only the selected runtime plus local when allowed; the unselected runtime is written as `enabled = false` so the config resolver does not default it back on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
efbd2d02e0
|
chore(server): switch CSP to report-only while tuning
Emit the policy via Content-Security-Policy-Report-Only instead of the enforcing header so browsers log violations without blocking resources while we debug remaining CSP issues. The policy string is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b3528a982e
|
chore: update CSP | ||
|
|
3e881e9938
|
fix(server): allow required CSP handoffs
Permit GitHub App manifest form posts and signed HTTPS VNC preview iframes while keeping the rest of the SPA CSP locked down. Mirror the policy in the split-web Caddy config. |
||
|
|
b5de404354
|
ci: lock Cargo resolution and fix cancellation flake (#461)
Some checks failed
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
## Summary
Enforce Cargo lockfile use across CI and release automation so jobs fail
on stale `Cargo.lock` state instead of resolving dependencies
implicitly. This adds `--locked` to Rust CI, release builds/tests,
nightly release tagging, the TypeScript workflow's embedded Rust build,
and helper-owned Cargo calls in `fabro-dev`.
This also fixes the Linux CI flake exposed by the PR: canceling a
durably blocked in-process run could take the abort path while the
workflow was still unwinding a human-input gate, causing
`run.failed(cancelled)` to be followed by `run.unblocked`. That invalid
event order broke projection rebuilds and made `GET /runs/{id}` return
404. Cancellation now uses the durable lifecycle status when selecting
the in-process blocked-run path, so the pending interview is cancelled
before the terminal event is emitted.
The release command's intentional `cargo update --workspace` step is
unchanged, because that step updates `Cargo.lock` after bumping the
workspace version.
## Testing
- `cargo nextest run --locked -p fabro-dev --features dev -E
'test(dry_run_computes_stable_version_from_date) |
test(dry_run_prints_equivalent_build_commands)'`
- `cargo --locked dev release --dry-run --skip-tests --release-date
2026-01-01`
- `cargo --locked dev docs check`
- `cargo nextest run --locked -p fabro-server --features test-support
cancel_durably_blocked_in_process_run_cancels_pending_interview_without_abort_signal
--status-level fail --final-status-level fail --show-progress none`
- `cargo nextest run --locked -p fabro-server --features test-support
--test it scenario::lifecycle --profile ci --status-level fail
--final-status-level fail --show-progress none --no-fail-fast`
- Linux Docker stress reproduction: `cargo nextest run --locked -p
fabro-server --features test-support --test it
scenario::lifecycle::full_http_lifecycle_cancel --profile ci
--stress-count 200 --status-level fail --final-status-level fail
--show-progress none`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --locked -p fabro-dev --features dev
--all-targets -- -D warnings`
- `cargo +nightly-2026-04-14 clippy --locked -p fabro-server --features
test-support --all-targets -- -D warnings`
- `git diff --check`
Full `cargo nextest run --locked -p fabro-dev --features dev` currently
has two unrelated policy-test failures:
`policy::catalog_builtin_references_stay_in_allowlist` and
`policy::workflow_template_rendering_call_sites_stay_in_allowlist`.
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
---------
Co-authored-by: Release Repro <release-repro@example.com>
|
||
|
|
d7a00d52d0
|
fix(automation): honor schedule trigger enabled state
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
Remove the stale top-level automation enabled gate from schedule filtering now that automations only carry trigger-level enabled flags. |
||
|
|
17cae07e5a
|
Add scheduled automation executor with in-memory cron planner (#457)
## Summary
Automation schedule triggers now fire automatically at their UTC cron
times. A new long-lived server task (`spawn_automation_scheduler`) owns
an in-memory planner that tracks one cursor per enabled schedule trigger
and creates/starts a normal Fabro run whenever a cursor comes due —
reusing the same materialization and run-creation path as API-triggered
runs.
### Plan Summary
- **Planner** (`AutomationSchedulePlanner`) — pure in-memory, no
persistent state. Reconciles cursors from the live automation list on
every tick; advances each due cursor _before_ spawning the fire task so
a failed materialization never hot-loops.
- **Executor loop** (`spawn_automation_scheduler`) — wakes on the
nearest cursor due time (capped at 30 s), on automation store mutations
(via `Notify`), or on shutdown. Spawns one Tokio task per due trigger so
slow materializations don't block other triggers.
- **Run firing** (`fire_scheduled_automation_run`) — materializes, calls
`create_run_from_manifest` with `Principal::System { Engine }`, then
calls `queue_run_start`. Warnings on any failure; next attempt waits for
the next cron occurrence.
- **Wiring** — `automation_scheduler_notify: Notify` added to
`AppState`; `create_automation`, `replace_automation`, and
`delete_automation` handlers call `notify_automation_scheduler()` so the
planner reacts immediately to changes.
- **Visibility widening** — `handler/lifecycle.rs` (`queue_run_start`)
and `handler/mod.rs` (`lifecycle`) promoted from `pub(super)` to `pub(in
crate::server)` so the scheduler (a sibling of `handler`) can call the
same start path.
- **Shared cron parser** — `parse_schedule_expression` extracted to
`fabro-automation` and re-exported so both validation and the scheduler
use the same parser configuration (no seconds, no year).
### Key design decisions
| Decision | Rationale |
|---|---|
| Cursor advances before fire task spawns | Guarantees at-most-one
attempt per occurrence even if materialization panics |
| No backfill on startup | Matches locked spec; `next_occurrence(expr,
now)` always starts from the current time |
| `Principal::System { Engine }` for actor | Avoids adding new public
enum variants or OpenAPI surface |
| `automation_temp_root()` extracted to `AppState` | Removes duplicated
`Storage::new(…).scratch_dir().join("automations")` from the automations
handler |
| Tests use `run_due_schedules_once` helper | Drives the planner
directly with fixed `DateTime<Utc>` values; no wall-clock sleeps in
tests |
### Fabro Details
<details>
<summary>Ran 9 stages in 60m 12s for $22.11</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 9s | – | 0 |
| preflight_lint | 2m 26s | – | 0 |
| implement | 20m 14s | $12.44 | 0 |
| simplify_opus | 12m 29s | $6.34 | 0 |
| simplify_gpt | 3m 53s | $2.79 | 0 |
| verify | 11m 5s | – | 0 |
| fixup | 6m 46s | $0.54 | 0 |
| **Total** | **60m 12s** | **$22.11** | **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>
|
||
|
|
fe1d33c041
|
Remove top-level automation enabled master gate (#456)
The top-level `enabled` flag on automations created a confusing
two-level activation model (automation-level + trigger-level). Since
automations are brand new with no existing data to migrate, the master
gate is removed entirely — trigger-level `enabled` is now the sole
activation control.
## What changed
**Domain model (`fabro-automation`):** `enabled` removed from
`Automation`, `AutomationDraft`, `AutomationReplace`, and
`PersistedAutomation`. `enabled_api_trigger()` no longer short-circuits
on the automation flag. The `default_true()` helper is gone. A new test
asserts that TOML with a top-level `enabled` key is rejected (no silent
compatibility path).
**Server handler:** Conflict detail updated from `"automation is
disabled or has no enabled API trigger"` → `"automation has no enabled
API trigger"`. The
`disabled_automation_run_endpoint_returns_conflict_code` test is
deleted; the trigger-disabled and missing-trigger tests remain as the
authoritative inactive-run coverage.
**OpenAPI + generated clients:** `enabled` removed from `Automation`,
`CreateAutomationRequest`, and `ReplaceAutomationRequest` schemas and
from the generated TypeScript interfaces. Trigger-level `enabled` on
`AutomationApiTrigger` and `AutomationScheduleTrigger` is untouched.
**Web UI:** `AutomationFormValues.enabled` and the "Enabled" toggle row
are gone. `isFormValid` no longer requires at least one enabled trigger.
`canRun` in the detail view is now just `apiTrigger?.enabled === true`.
The `StatusChip` component is removed. The automations list uses a new
`apiEnabled` field (derived from `hasEnabledApiTrigger`) to drive
run-button state and tooltip copy. A shared `lib/automation.ts` helper
centralises `findApiTrigger`, `findScheduleTrigger`, and
`hasEnabledApiTrigger` to avoid repeated inline `.find()` calls across
routes.
### Fabro Details
<details>
<summary>Ran 8 stages in 41m 34s for $17.84</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 15s | – | 0 |
| preflight_lint | 2m 25s | – | 0 |
| implement | 13m 0s | $9.25 | 0 |
| simplify_opus | 9m 43s | $6.18 | 0 |
| simplify_gpt | 3m 56s | $2.41 | 0 |
| verify | 9m 17s | – | 0 |
| **Total** | **41m 34s** | **$17.84** | **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>
|
||
|
|
037073d2b2
|
feat: Add Environment REST CRUD API under /api/v1/environments (#453)
## Summary
Adds a server-managed Environment CRUD API at `/api/v1/environments`,
modeled after the existing Automations API and backed by
`EnvironmentStore`. The API manages only server-side environment
definitions in `environments/*.toml`; client-side catalogs (workflow,
project TOML, run inputs) are unaffected.
### Plan Summary
- **OpenAPI contract**: new `Environments` tag, `EnvironmentId` path
parameter, five CRUD paths, list envelope, and REST-specific inline-only
image schema (`EnvironmentApiImageSettings`)
- **Server handler** (`environments.rs`): mirrors `automations.rs` —
auth guard, ETag/If-Match, and `EnvironmentStoreError → ApiError`
mapping
- **Shared handler utilities**: `parse_required_if_match` and
`json_with_etag_response` extracted from `automations.rs` into
`handler/mod.rs` so both modules share them
- **Inline-only Dockerfile enforcement**: `ApiDockerfileSource::Path` is
parsed and immediately rejected with `422`; the file is never read
- **Manifest refresh**:
`refresh_manifest_run_settings_from_environment_catalog()` called after
create, replace, and delete so `/system/info` and default run settings
stay consistent
- **Client regeneration**: TypeScript Axios client regenerated with
`EnvironmentsApi` and new model files; Rust `fabro-api` type aliases
updated
- **Tests**: integration suite in `tests/it/api/environments.rs`
covering all CRUD paths, error cases, and the manifest-refresh
invariant; OpenAPI conformance test verifies generated surfaces
## Key Design Decisions
**Inline-only Dockerfile at the REST boundary.** Allowing `path` sources
over REST would let callers silently read arbitrary server-local files
into the environment catalog. The handler recognizes the `path`
discriminant so it can return a descriptive `422` rather than a generic
parse error, but the payload is discarded via `IgnoredAny` — no disk
access occurs.
**Shared ETag utilities instead of per-handler helpers.** The original
`parse_required_if_match` and ETag header builder in `automations.rs`
were duplicated for environments. They're now generic over any `FromStr`
revision type in `handler/mod.rs`, making future resource handlers
cheaper to add.
**`Environment` response type aliased to domain type.** The
OpenAPI-generated `Environment` response struct is replaced with
`fabro_environment::Environment` via `build.rs` `with_replacement`. A
compile-time function-cast witness in
`fabro-api/tests/environment_round_trip.rs` confirms the alias holds.
Request types (`CreateEnvironmentRequest`, `ReplaceEnvironmentRequest`)
stay API-specific because their image schema differs from the
workflow/settings schema.
**Stale revision → `409`.** Consistent with Automations; `428` is
reserved for missing `If-Match` only.
### Fabro Details
<details>
<summary>Ran 8 stages in 59m 23s for $30.41</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 9s | – | 0 |
| preflight_lint | 2m 25s | – | 0 |
| implement | 25m 43s | $19.79 | 0 |
| simplify_opus | 14m 34s | $6.98 | 0 |
| simplify_gpt | 4m 39s | $3.64 | 0 |
| verify | 9m 14s | – | 0 |
| **Total** | **59m 23s** | **$30.41** | **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>
|
||
|
|
7c73f7ac02
|
fix(server): inline dockerfiles defined in the [environments.*] catalog
The manifest bundler collects Dockerfile path references from both the
named-environment catalog and [run.environment], but the server-side
resolver only inlined [run.environment.image]. A Dockerfile declared
under [environments.<slug>.image] therefore reached the Daytona provider
as an un-inlined Path and tripped its guard ("dockerfile path should have
been resolved to inline content before sandbox creation"), so no run
could use a catalog-defined Dockerfile environment.
Walk layer.environments alongside run.environment when resolving manifest
dockerfiles, mirroring the bundler. Add a regression test proving a
catalog dockerfile path is inlined.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
ac66f6c1d6
|
Merge remote-tracking branch 'origin/main' into fix-center-size-column
# Conflicts: # lib/crates/fabro-server/src/automation_materializer.rs # lib/crates/fabro-server/src/server.rs |
||
|
|
fee245d788
|
fix(web): make plural /automations/:id the canonical detail route
The list card linked to the singular /automation/:id, which mismatched the rest of the new automations CRUD surface (/automations, /automations/new, /automations/:id/edit). Switch the card link and the slug-preview text on the create form to the plural form, and mount /automations/:id in the router alongside the existing singular route (kept as a back-compat alias for any older bookmarks). Drive-by: fold two adjacent `use super::*` imports into one and reflow a long `if let` line in the automations handler (linter cleanup; no behavior change). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7f9b31074c
|
perf(server): cache bare GitHub clones for automation materialization
Materializing an automation run cloned the full repo fresh into a tempdir on every click — 5–15s of git activity on the HTTP request thread, paid in full for every run, then thrown away. Add a per-`(owner, repo)` bare-clone cache under `<Storage::cache_dir>/automation-repos/<owner>/<repo>.git`, and replace the per-call clone+fetch+checkout dance with: 1. `KeyedMutex` lock on `(owner, repo)` so concurrent calls serialize per repo and parallelize across repos. 2. If the bare clone is missing, `git clone --bare --depth 1`. Otherwise `git worktree prune` to clean up any admin entries leaked by previous `TempDir` drops. 3. `git fetch --depth 1 origin <ref>` against the bare clone. 4. `git rev-parse FETCH_HEAD` for the SHA. 5. `git worktree add --detach --force <temp>/repo FETCH_HEAD` into the per-call scratch dir, then build the manifest as today. First run for a repo still pays the clone cost. Every subsequent run for any ref or automation against that repo pays only the fetch delta plus a near-free worktree add (~100–500ms). Corruption recovery: if the bare clone's `HEAD` file is missing or zero-length after a failure, the cache wipes the directory and retries once before surfacing `CloneFailed` as before. Auth and network errors do not trigger a wipe. Promote `fabro_store::KeyedMutex` and its guard to `pub` so the server can reuse the existing primitive instead of duplicating it. Tests: - `bare_clone_reused_across_calls` seeds a local upstream, runs `prepare_worktree` twice, and asserts the bare clone's `objects/` tree is identical before and after the second call (i.e., no re-clone). - `bare_clone_recovers_from_corruption` truncates `HEAD` between calls and asserts the cache rebuilds and succeeds. - The existing plan-builder argv/timeout assertions are updated to cover the new bare-clone, bare-fetch, worktree-add, worktree-prune, and rev-parse FETCH_HEAD plans. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
87516c25ce
|
feat(automations): wire UI to API and auto-start runs from API trigger
Make the Automations area in the web UI functional end-to-end against the real Automation API, and fix the backend so runs created by an automation's API trigger actually start instead of sitting in Submitted forever. Web: - Reveal the Automations nav tab outside demo mode; drop the now-empty demoOnly mechanism. - List page: render via listAutomations (was workflows mock data); wire ellipsis menu to Edit and Delete, with ConfirmDialog + If-Match revision. Move Create Automation into the toolbar, switch the trigger select to a shared FilterButton, hide the redundant page-header title via a new hideTitle handle flag. - Play button on each card fires createAutomationRun with spinner + toast and navigates to the new run. - New automation form: drop the dead Goal panel and hardcoded repository list, post to createAutomation with real triggers. - Edit automation: new /automations/:id/edit route reusing a shared AutomationFormFields component, PUT via replaceAutomation with If-Match. - Show page: rebuild like a run detail page — breadcrumb, title, chips (enabled status, repo+ref, workflow, schedule), Edit + Run actions (Run hits createAutomationRun), and a Runs panel using RunsListView with URL-driven search/sort/pagination/column-picker like the Children sub-tab. Drop the obsolete Definition/Diagram/Runs child routes. Backend (fabro-server): - create_automation_run now calls lifecycle::queue_run_start after the run is persisted, so the run transitions Submitted → Runnable and the scheduler picks it up. Logs a warn and returns the created response if start fails (no worse than the prior always-stuck behavior). - queue_run_start in lifecycle.rs is promoted to pub(super) so sibling handlers can reuse it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8272d8239b
|
feat(model): add Claude Opus 4.8 (#451)
## Summary - Add `claude-opus-4-8` to the built-in Anthropic model catalog with pricing, limits, features, and fast-mode costs. - Move the floating `opus` and `claude-opus` aliases from Opus 4.7 to Opus 4.8 and update the public model table. - Remove/generalize Rust tests that were pinned to specific built-in Opus catalog data. ## Verification - `cargo nextest run -p fabro-model` - `cargo +nightly-2026-04-14 fmt --check --all` - `git diff --check` - `target/debug/fabro --json model test --model opus` (live Anthropic smoke; resolved to `claude-opus-4-8`) |
||
|
|
4cff07373c
|
feat: add server-owned environment store (Task 1 & 2 foundation) (#446)
## Summary
Moves environment definitions out of project/workflow TOML config and
into server-owned files, introducing the `fabro-environment` crate and
enforcing source-aware validation so project/workflow/user configs can
no longer define environment catalogs.
### What changed
**New `fabro-environment` crate** — workspace crate wired into
`fabro-cli` and `fabro-server`. Exposes a `seeded_catalog_layer()` that
CLI commands inject at the call site to fill the environment catalog
that settings resolution requires.
**Config environments are now migration-only** — `defaults.toml` no
longer ships a built-in `[environments.*]` catalog. Instead:
- `SettingsSource` enum tags every parsed layer (ActiveSettings,
Project, Workflow, DirectRun, User).
- `validate_settings_source` rejects `[environments.<id>]` in any source
except `ActiveSettings` with a targeted message: `[environments.<id>] is
now server-managed; move this definition to the server environments
directory`.
- TOML-provided
`run.environment.{image,resources,network,lifecycle,labels,volumes,env}`
overrides are also rejected; only `run.environment.id` survives.
**New migration** (`2026052801_settings_environments_to_server_files`) —
chains after the existing legacy-sandbox migration. Extracts
`[environments.*]` entries from `settings.toml` into sibling
`environments/<id>.toml` files, writes a
`.settings-environments-migration.bak` backup, and fails without
modifying any file if a target already exists.
**Builder API additions** —
`RunSettingsBuilder::load_from_with_catalog`,
`load_default_with_catalog`, `from_toml_with_catalog` let callers inject
a server-side catalog; the bare `from_toml` path now errors if no
catalog is present and a named environment is selected.
`WorkflowSettingsBuilder` test helpers in `src/tests/mod.rs` centralise
catalog injection across all config tests.
**`.fabro/project.toml`** — removed the inline
`[environments.fabro-dev]` block (environment definition now lives
server-side).
### Key design decisions
- CLI offline commands (graph, preflight, validate) use
`seeded_catalog_layer()` as a local stand-in until a running server is
available — matches the pre-existing behaviour without regressing
offline workflows.
- `load_settings_path` no longer runs migrations for non-ActiveSettings
sources, preventing project/workflow files from accidentally triggering
file-system writes.
- The `MigrationReport` type is now the new migration's
`SettingsEnvironmentsMigrationReport` (exposes `contents: String`
instead of a parsed layer), keeping `load.rs` simpler and decoupled from
layer parsing.
### Fabro Details
<details>
<summary>Ran 9 stages in 143m 23s for $105.27</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 11s | – | 0 |
| preflight_lint | 2m 23s | – | 0 |
| implement | 28m 27s | – | 0 |
| simplify_opus | 37m 27s | $53.28 | 0 |
| simplify_gpt | 20m 50s | $12.14 | 0 |
| verify | 6m 3s | – | 0 |
| fixup | 45m 15s | $39.84 | 0 |
| **Total** | **143m 23s** | **$105.27** | **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>
|
||
|
|
0106711170
|
test: cover automation trigger API behavior | ||
|
|
91d11eb04d
|
fix(llm): preserve raw compatible tool arguments (#448)
## Summary Fixes #435. Preserve raw non-JSON tool-call arguments for custom/freeform tools when using the OpenAI-compatible Chat Completions adapter. This keeps `apply_patch` receiving the raw patch text instead of `{}` when LiteLLM/openai-compatible providers emit Codex-style freeform patch calls. Also extends the OpenAI twin so black-box tests can exercise the Chat Completions path with raw tool-call arguments. ## Test Plan - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo nextest run -p fabro-agent --test it openai_compatible_twin_preserves_raw_apply_patch_arguments --run-ignored only` - `cargo nextest run -p fabro-llm` - `cargo nextest run -p fabro-test` |
||
|
|
2e39dfc70e
|
fix(agent): align compaction preserve boundary (#449)
## Summary Follow-up to fabro-sh/fabro#447. This keeps context compaction's effective preserve boundary consistent between summary generation, history mutation, and emitted telemetry so tool-call/result pairs that remain in raw history are not also summarized. The branch also tightens the OpenAI twin support added for this regression: scripted usage is modeled as a single `TokenUsage`, SSE completion payloads reuse the canonical Responses JSON shape, and request validation now treats custom tool-call outputs as tool outputs instead of spreading raw item-type string checks. ## Verification - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo nextest run -p fabro-agent compaction` - `cargo nextest run -p twin-openai` - `FABRO_TEST_MODE=twin cargo nextest run -p fabro-agent --profile e2e --run-ignored only --test it openai_twin_compaction_preserves_tool_call_pairs` - `git diff --check origin/main...HEAD` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
81554581ca
|
fix(agent): preserve tool-call pairs during compaction (#447)
## Summary Fixes OpenAI Responses requests after context compaction by ensuring preserved tool results are not separated from the assistant tool calls that produced them. The previous fixed-size preserved tail could retain a `function_call_output` while dropping the matching `function_call`, which OpenAI rejects as an orphaned tool result. ## Changes - Extends `History::compact` so the preserved range moves backward until every kept tool result has its matching assistant tool call. - Adds a unit invariant test for compacted histories that serialize tool results. - Adds an OpenAI twin integration regression that forces compaction during a tool-use loop. - Teaches the OpenAI twin to validate orphaned `function_call_output` items and script response usage counts for deterministic compaction tests. ## Test Plan - `cargo +nightly-2026-04-14 fmt --check --all` - `git diff --check` - `FABRO_TEST_MODE=twin cargo nextest run -p fabro-agent --test it openai_twin_compaction_preserves_tool_call_pairs --run-ignored all` - `cargo nextest run -p fabro-agent` - `cargo nextest run -p twin-openai` - `cargo nextest run -p fabro-test` - `cargo +nightly-2026-04-14 clippy -p fabro-agent -p fabro-test -p twin-openai --all-targets --no-deps -- -D warnings` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
9ee576690b
|
refactor: remove in-process IP allowlist and introduce WorkerRuntime (#444)
## Summary
This PR does two things: it removes the in-process inbound source-IP
allowlist entirely, and it lays the foundation for pluggable worker
compute backends by introducing a `WorkerRuntime` abstraction.
## IP allowlist removal
The `[server.ip_allowlist]` setting and its GitHub webhook overlay
(`[server.integrations.github.webhooks.ip_allowlist]`) have been removed
from config parsing, the settings API, and the OpenAPI spec. The
`ip_allowlist.rs` module (~600 lines including the `GitHubMetaResolver`,
middleware, and cache logic) is deleted.
**Migration:** Existing `settings.toml` files containing those keys will
now fail to parse as unknown fields. Source-IP restrictions should be
moved to a reverse proxy, firewall, VPN, Tailscale ACL, or cloud ingress
— as documented in the new security guidance.
`build_router_with_options` loses the `ip_allowlist_config:
Arc<IpAllowlistConfig>` parameter and `RouterOptions` loses
`github_webhook_ip_allowlist`. Call sites in tests and the auth harness
are updated accordingly. TCP serving no longer uses
`make_service_with_connect_info` since `ConnectInfo` was only needed for
IP extraction.
## WorkerRuntime abstraction
A new `worker_runtime.rs` module introduces:
- **`WorkerRuntime` trait** — `start`, `request_stop`, `force_stop`,
`is_alive`
- **`WorkerLaunchSpec`** — all inputs needed to describe a worker
process, replacing the former `worker_command` helper
- **`WorkerRef::Local { pid, process_group_id }`** — replaces the
`worker_pid` / `worker_pgid` pair on `ManagedRun`
- **`StartedWorker`** — carries the ref, optional stderr stream, and a
`wait` future
- **`LocalWorkerRuntime`** — the only implementation for now; wraps the
existing subprocess spawn logic
`AppState` stores an `Arc<dyn WorkerRuntime>` and `AppStateConfig`
accepts an optional override in `#[cfg(test)]` for injection. Stop/kill
paths in `server.rs` and `lifecycle.rs` now call
`worker_runtime.request_stop` / `force_stop` / `is_alive` instead of
issuing signals directly.
### Plan Summary
- **Task 1:** New `worker_runtime.rs` with trait, types, and
`LocalWorkerRuntime` impl
- **Task 2:** Wire `Arc<dyn WorkerRuntime>` into `AppState` /
`AppStateConfig` / `TestAppStateBuilder`
- **Task 3:** Replace `worker_pid` / `worker_pgid` on `ManagedRun` with
`worker_ref: Option<WorkerRef>`; build `WorkerLaunchSpec` in
`execute_run_subprocess`
- **Task 4:** Route all stop/kill calls through the runtime
(`terminate_worker_for_deletion`, `shutdown_active_workers`,
`cancel_run` fallback)
- **Task 5:** Fake `RecordingWorkerRuntime` for unit tests; new
cancel-fallback and shutdown tests
### Fabro Details
<details>
<summary>Ran 8 stages in 107m 33s for $21.66</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 10s | – | 0 |
| preflight_lint | 2m 21s | – | 0 |
| implement | 29m 0s | $11.86 | 0 |
| simplify_opus | 11m 9s | $5.94 | 0 |
| simplify_gpt | 53m 13s | $3.85 | 0 |
| verify | 9m 23s | – | 0 |
| **Total** | **107m 33s** | **$21.66** | **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>
|
||
|
|
e3bbe91053
|
Add GET/POST /automations/{id}/runs endpoints (#442)
## Summary Implements the two automation run endpoints from issue #399, backed by a significant refactor of the worker control channel from stdin JSONL to a WebSocket-based pub/sub bus. ## What changed ### New API endpoints (`automations.rs`) - `GET /automations/{id}/runs` — lists cached runs filtered to those linked to the given automation ID, sorted newest-first, with `page[limit]`/`page[offset]` pagination and the standard `{ data, meta }` envelope. - `POST /automations/{id}/runs` — requires `RequiredRunToolActor` auth, checks that the automation exists and has an enabled API trigger (returning 409 with `automation_api_trigger_disabled` otherwise), materializes the run manifest, and delegates to the shared `create_run_from_manifest` helper with a fully-populated `AutomationRef`. ### `enabled_api_trigger()` helper (`fabro-automation`) A new method on `Automation` encapsulates the "automation is enabled **and** has an enabled API trigger" check, keeping the handler clean. ### Worker control channel: stdin JSONL → WebSocket bus The most significant structural change is how the server delivers control messages (answers, cancel, pause/unpause, steer, pair events) to running workers: | Before | After | |---|---| | Server pipes JSONL lines to worker stdin | Server publishes to `WorkerControlBus`; worker connects via WebSocket | | Worker reads stdin on a blocking OS thread | Worker manages a reconnecting WebSocket with ping/pong liveness | | No delivery deduplication | `AppliedWorkerControlDeliveryIds` deduplicates replayed frames | | No reconnect / resume | Worker reconnects with exponential backoff; replays from last applied cursor | The `LocalWorkerControlBus` replaces the old `mpsc` channel and stdin pipe. `RunAnswerTransport::Subprocess` is renamed `Worker` and holds a `run_id` + `Arc<dyn WorkerControlBus>` instead of a channel sender. Worker stdin is now `Stdio::null()`. New control messages `RunPause` / `RunUnpause` are added to the protocol, wired through to `RunControlState`. ### Plan Summary - Add `enabled_api_trigger()` to `Automation`. - Implement `list_automation_runs` and `create_automation_run` handlers; route them under `/automations/{id}/runs`. - Expose `create_run_from_manifest` from the runs handler for reuse. - Add `RequiredRunToolActor` extractor. - Replace stdin JSONL worker control with `WorkerControlBus` + WebSocket reconnect loop in the CLI worker. - Add integration tests for all 409/201 cases, run persistence, listing filters, pagination, and sorting. ### Fabro Details <details> <summary>Ran 8 stages in 51m 5s for $21.34</summary> | Stage | Duration | Cost | Retries | |---|---|---|---| | start | 0s | – | 0 | | toolchain | 1s | – | 0 | | preflight_compile | 2m 8s | – | 0 | | preflight_lint | 2m 23s | – | 0 | | implement | 20m 52s | $13.05 | 0 | | simplify_opus | 11m 5s | $5.70 | 0 | | simplify_gpt | 5m 0s | $2.60 | 0 | | verify | 9m 4s | – | 0 | | **Total** | **51m 5s** | **$21.34** | **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> |
||
|
|
29a9a3f7d6
|
refactor: Remove inbound IP allowlisting (#443)
## Summary
Removes Fabro's in-process inbound source-IP allowlist entirely.
`[server.ip_allowlist]` and
`[server.integrations.github.webhooks.ip_allowlist]` are gone from
config parsing, resolved settings types, the OpenAPI spec, generated API
clients, and the Settings > Security UI. Existing `settings.toml` files
containing those keys now fail as unknown fields — this is a hard
removal with no migration path.
Network source restrictions should be enforced upstream via a reverse
proxy, firewall, VPN, Tailscale ACLs, Kubernetes ingress, or platform
policy.
### What changed
- **Config/types** (`fabro-config`, `fabro-types`): Removed
`ServerIpAllowlistLayer`, `ServerIpAllowlistOverrideLayer`,
`ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`,
`IpAllowEntry`, associated resolver functions, GitHub `/meta` hook-range
parsing, and Unix socket trusted-proxy validation. `ipnet` dropped from
`fabro-types`; kept in `fabro-config` for sandbox CIDR validation.
- **Server runtime** (`fabro-server`): Deleted `ip_allowlist.rs`,
removed `IpAllowlistConfig` parameter from `build_router_with_options`
and `RouterOptions`, removed the global allowlist middleware layer, and
removed `GitHubMetaResolver` startup logic. GitHub webhook HMAC
verification is unchanged.
- **OpenAPI + generated clients**: Removed `ServerIpAllowlistSettings`,
`ServerIpAllowlistOverrideSettings`, `IpAllowEntry`,
`LiteralIpAllowEntry`, `GitHubMetaHooksEntry` schemas; removed
`ip_allowlist` from `ServerNamespace` and `IntegrationWebhooksSettings`;
dropped `IpAllowEntry` re-exports from `fabro-api`.
- **Web UI**: Removed IP allowlist row from Settings > Security; updated
nav description and page copy.
- **Docs/changelog**: Security docs explicitly state Fabro provides no
source-IP filtering and direct operators upstream. Changelog entry dated
2026-05-27 documents the breaking removal and annotates the 2026-04-19
entry where the feature was introduced.
### Also in this diff (unrelated to IP allowlisting)
The worker control stream was migrated from reading newline-delimited
JSON on stdin to a reconnecting WebSocket
(`/api/v1/runs/{id}/worker/control-stream`). This adds
`tokio-tungstenite` to `fabro-cli`/`fabro-server`, introduces
`WorkerControlManagerHandle` with backoff reconnection and deduplication
of replayed delivery IDs, and adds `RunPause`/`RunUnpause` message
handling. A new integration test
(`detached_run_cancel_reaches_worker_over_control_websocket`) exercises
the full cancel path over the WebSocket.
### Key decisions
- **Hard removal via `deny_unknown_fields`**: stale config is
immediately visible as a startup error rather than silently ignored.
- **No stub or default pass-through**: `IpAllowlistConfig::default()` is
gone, not left as a no-op wrapper, to avoid keeping the feature shape
alive.
- **Webhook HMAC boundary unchanged**: source-IP filtering on webhook
routes is removed; cryptographic signature verification remains the
security boundary.
### Fabro Details
<details>
<summary>Ran 9 stages in 59m 53s for $27.24</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 15s | – | 0 |
| preflight_lint | 2m 22s | – | 0 |
| implement | 33m 54s | $22.81 | 0 |
| simplify_opus | 5m 55s | $0.75 | 0 |
| simplify_gpt | 3m 35s | $2.81 | 0 |
| verify | 8m 34s | – | 0 |
| fixup | 2m 24s | $0.87 | 0 |
| **Total** | **59m 53s** | **$27.24** | **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>
|
||
|
|
475b4ab650
|
Replace stdin JSONL control pipe with WebSocket worker control bus (#440)
## Summary
Workers no longer receive control messages over stdin JSONL. A new
`WorkerControlBus` abstraction (backed by `LocalWorkerControlBus` for
local/single-node deployments) publishes `WorkerControlEnvelope`
messages server-side; a worker-initiated WebSocket at `GET
/runs/{id}/worker/control-stream` delivers them with ordered, replayable
delivery frames. The bus API is designed so a Redis Streams backend can
slot in later without touching API handlers or worker message handling.
### Plan Summary
- **Task 1 – Bus contract:** `WorkerControlBus` trait,
`WorkerControlDelivery`, `WorkerControlCursor` (`Start` / `After(id)`),
bus errors.
- **Task 2 – Local backend:** `LocalWorkerControlBus` — in-memory
per-run stream, replay from `Start`, reconnect via `After(id)`, 1
024-message trim bound, cleanup on terminal runs.
- **Task 3 – Server state:** `Arc<dyn WorkerControlBus>` added to
`AppState`; `LocalWorkerControlBus` constructed at startup.
- **Task 4 – Protocol extension:** `WorkerControlMessage::RunPause` /
`RunUnpause`, `WorkerControlDeliveryFrame`, WebSocket liveness constants
(`WORKER_CONTROL_WS_PING_INTERVAL = 15s`,
`WORKER_CONTROL_WS_LIVENESS_TIMEOUT = 45s`), close-reason strings.
- **Task 5 – Worker message handler:** `apply_worker_control_message`
split out; pause/unpause routing; delivery-id dedupe
(`AppliedWorkerControlDeliveryIds`, capacity 2 048).
- **Task 6 – Worker WebSocket client:** `spawn_worker_control_manager` —
HTTP→ws/wss and Unix-socket connection, backoff 100ms→5s,
first-connection gate before `operations::start/resume`, ping/pong
watchdog, fatal loss wired back to `execute`.
- **Task 7 – Server route:** `GET /runs/{id}/worker/control-stream`,
worker-only auth via new `RequireWorkerRunScoped` extractor,
`Start`/`After` cursor dispatch, 410 on invalid cursor, server-side
ping/pong.
- **Task 8 – Stdin removal:** `RunAnswerTransport::Subprocess` renamed
to `Worker { run_id, bus }`; `pump_worker_control_jsonl` deleted; worker
launched with `stdin(Stdio::null())`; pause/unpause transport methods
added.
- **Tasks 9–10 – E2E & verification:** reconnect, invalid-cursor,
cancel-over-WebSocket, and human-interview regression tests; no Redis
dependency added.
### Key design decisions
**`RunAnswerTransport::Subprocess` → `Worker { run_id, bus }`** — all
existing transport methods (`submit`, `cancel_run`, `steer`,
`interrupt`, `pair_*`) now call `bus.publish(run_id, envelope)` instead
of writing to a channel that fed stdin. The match arms are symmetric, so
the diff is mechanical but large.
**First-connection gate** — `execute()` calls
`control_manager.wait_for_first_connection().await?` before
`operations::start` or `operations::resume`. Temporary failures spin
with backoff; a fatal invalid-cursor or request-build failure propagates
as an error before the workflow starts.
**Fatal vs. reconnectable** — HTTP 410 or a WebSocket close with reason
`"invalid_cursor"` is fatal (infrastructure failure, not user
cancellation). Any other close/error triggers the reconnect loop while
the run is non-terminal.
**`AutomationStore::load` made synchronous** — startup load now uses
`std::fs` under a `clippy::disallowed_methods` exception; async
`tokio::fs` is no longer needed for the one-shot directory scan. Invalid
automation files now fail loudly instead of being silently skipped.
**`canRetry` extended to succeeded runs** — `status.kind ===
"succeeded"` is now retryable (non-archived). Tests and API docs updated
to match.
**Default model bumps** — OpenAI default: `gpt-5.4` → `gpt-5.5`; Gemini
default: `gemini-3.1-pro-preview` → `gemini-3.5-flash`.
### Fabro Details
<details>
<summary>Ran 9 stages in 129m 19s for $58.27</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 10s | – | 0 |
| preflight_lint | 2m 23s | – | 0 |
| implement | 73m 48s | $41.53 | 0 |
| simplify_opus | 22m 55s | $11.75 | 0 |
| simplify_gpt | 7m 19s | $2.74 | 0 |
| verify | 8m 51s | – | 0 |
| fixup | 10m 59s | $2.24 | 0 |
| **Total** | **129m 19s** | **$58.27** | **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>
|
||
|
|
ee1502f793
|
Add automation run materialization core and shared run creation helper (#441)
## Summary
Automation-triggered runs need to share the same run creation pipeline
as `POST /runs`. This PR lays the core infrastructure: a
`create_run_from_manifest` helper that the HTTP handler and the upcoming
automation scheduler can both call, plus a `AutomationRunMaterializer`
trait with a production implementation that clones a GitHub repo and
builds a `RunManifest` from it.
### Plan Summary
- Extract the body of `handler/runs.rs::create_run` into a crate-private
`create_run_from_manifest(state, CreateRunFromManifestRequest)` helper;
`POST /runs` calls it with `automation: None`, preserving existing
behavior.
- Add `AutomationRunMaterializeInput/Materialized/Error` types and the
`AutomationRunMaterializer` trait (`automation_materializer.rs`).
- Implement `ProductionAutomationRunMaterializer`: validates
`owner/repo` slug, shallow-clones via `tokio::process::Command` argv
(never shell strings), sets `GIT_TERMINAL_PROMPT=0`, enforces
per-operation timeouts, redacts credentials from error text, resolves
the workflow with `fabro_config::project::WorkflowLocation::resolve`,
and builds a `RunManifest` via `fabro_manifest::build_run_manifest`.
- Add `TestAutomationRunMaterializer` (gated on `test` or
`test-support`) for fake injection in route tests without network
access.
- Wire the materializer override into `AppState` and `AppStateConfig`
behind `#[cfg(any(test, feature = "test-support"))]`; expose via
`TestAppStateBuilder::automation_materializer`.
- Move `async-trait` from `[dev-dependencies]` to `[dependencies]` in
`fabro-server` since the trait is now in production code.
## What changed and why
**`automation_materializer.rs` (new)** — Core of this PR. The
`GitCommandPlan` builder keeps all git invocations as argv slices so
there is no shell injection surface. Credentials are injected
exclusively via `GIT_CONFIG_VALUE_0` (the `extraheader` mechanism),
never embedded in the clone URL, so they cannot appear in run metadata
or error messages. The `redact_git_output` function scrubs the raw
token, the Base64-encoded form, and the full `AUTHORIZATION` header
value from any error string before it surfaces.
**`create_run_from_manifest`** — The extracted helper accepts an
optional `AutomationRef` which is forwarded into
`create_input.automation` so the store can persist automation provenance
on the run. The `POST /runs` code path passes `None`, leaving existing
API behavior identical.
**Test injection** — `TestAutomationRunMaterializer` captures every
`AutomationRunMaterializeInput` it receives and returns a
caller-controlled `Result`, letting route tests assert what inputs the
scheduler would pass without touching GitHub.
```mermaid
flowchart TB
A["POST /runs\n(HTTP handler)"] -->|automation: None| H["create_run_from_manifest"]
S["Automation scheduler\n(future issue)"] -->|automation: Some(ref)| H
H --> DB[(Run store)]
M["AutomationRunMaterializer\n(trait)"] -->|produces RunManifest| S
M -- production --> P["ProductionAutomationRunMaterializer\n(git clone → manifest build)"]
M -- test --> T["TestAutomationRunMaterializer\n(captures input, returns fixture)"]
```
### Fabro Details
<details>
<summary>Ran 8 stages in 72m 56s for $36.55</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 2m 12s | – | 0 |
| preflight_lint | 2m 27s | – | 0 |
| implement | 30m 41s | $23.10 | 0 |
| simplify_opus | 19m 19s | $9.12 | 0 |
| simplify_gpt | 7m 53s | $4.33 | 0 |
| verify | 9m 20s | – | 0 |
| **Total** | **72m 56s** | **$36.55** | **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>
|
||
|
|
a992a7d76b
|
feat(runs): allow retrying succeeded runs
Broaden manual retry eligibility to all unarchived terminal runs while preserving active and archived precondition failures. |
||
|
|
2d78f96107
|
Wire automation store into AppState and expose CRUD REST API (#439)
## Summary
Loads `AutomationStore` into `AppState` at server startup and exposes
five authenticated REST endpoints (`GET/POST /automations`,
`GET/PUT/DELETE /automations/{id}`) backed by the existing
`fabro-automation` crate.
### Plan Summary
- Add `fabro-automation` as a dependency of `fabro-server` and mount
`Arc<AutomationStore>` on `AppState`, computed from a sibling
`automations/` directory next to the active config file.
- Change `AutomationStore::load` from `async` to synchronous (`std::fs`)
so it can run before the Tokio runtime needs to make progress; malformed
files now fail startup instead of being silently skipped.
- Implement `src/server/handler/automations.rs` with shared helpers for
path-ID parsing, `If-Match` (quoted/unquoted) parsing, ETag formatting,
and `AutomationStoreError → ApiError` mapping.
- HTTP semantics: 201 on create, 404 on missing, 409 on duplicate or
stale revision, 422 on domain validation failure, 428 on missing
`If-Match`.
- Update `TestAppStateBuilder` to derive `active_config_path` from the
vault path so each test gets an isolated sibling `automations/`
directory; add `try_build()` to allow startup-failure assertions.
- Update the OpenAPI spec and generated TypeScript client to include
`AutomationListMeta` with a `total` field.
## Key design decisions
**Sync load path.** `AutomationStore::load` is now `fn` (not `async
fn`), using `std::fs`. A `#[expect(clippy::disallowed_methods)]`
annotation explains the rationale: this runs once at startup before the
runtime needs to yield, and avoids requiring a Tokio handle at the call
site in `build_app_state`.
**Fail-fast on malformed files.** Previously, corrupt TOML files were
logged as warnings and skipped. Now any parse or validation error during
load aborts server startup. The old `warn_load_failure` helper is
deleted; tests that relied on skip behaviour are replaced with tests
that assert `Err(AutomationStoreError::Parse { .. })` and
`Err(AutomationStoreError::InvalidFilename { .. })`.
**ETag / If-Match handling.** `parse_required_if_match` strips optional
surrounding quotes before parsing the revision, so both `"<rev>"` and
bare `<rev>` are accepted from clients. Missing `If-Match` on PUT/DELETE
returns **428 Precondition Required**, not 400.
**Test isolation.** `TestAppStateBuilder::build` now derives
`active_config_path` from `vault_path.with_file_name("settings.toml")`
instead of a random temp path, so the sibling `automations/` directory
is predictable and cleaned up with the same temp dir.
### Fabro Details
<details>
<summary>Ran 10 stages in 85m 20s for $33.66</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 19s | – | 0 |
| preflight_lint | 2m 5s | – | 0 |
| fix_lints | 33s | $0.15 | 0 |
| implement | 30m 33s | $17.35 | 0 |
| simplify_opus | 20m 27s | $11.82 | 0 |
| simplify_gpt | 6m 41s | $3.88 | 0 |
| verify | 15m 44s | – | 0 |
| fixup | 6m 8s | $0.46 | 0 |
| **Total** | **85m 20s** | **$33.66** | **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>
|
||
|
|
fa565ceaae
|
chore(model): update provider default models (#437)
Updates the built-in catalog so OpenAI default selection now resolves to `gpt-5.5` and Gemini default selection resolves to `gemini-3.5-flash`. The public model defaults table and catalog assertions were updated to pin the new behavior. Verified with `cargo nextest run -p fabro-model`. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 (Codex) via [Codex](https://openai.com/codex) |
||
|
|
c767db897f
|
Add Automations API contract to OpenAPI spec and update generated clien… (#436)
## Summary
Defines the public Automations REST API contract in the OpenAPI spec,
updates the `RunSandbox` schema to reflect the new sandbox lifecycle
model, adds `fabro-automation` as a dependency to `fabro-api` for type
reuse, and removes the retired `fabro-devcontainer` crate and all
references to it.
## What changed
### Automations API (`fabro-api.yaml`)
Seven new paths under `/api/v1/automations` covering the full CRUD
surface plus run sub-resources:
```
GET/POST /automations
GET/PUT/DELETE /automations/{id}
GET/POST /automations/{id}/runs
```
New schemas: `Automation`, `AutomationTarget`, `AutomationTrigger`
(discriminated oneOf on `type`), `AutomationApiTrigger`,
`AutomationScheduleTrigger`, `CreateAutomationRequest`,
`ReplaceAutomationRequest`, `AutomationListResponse`.
Key contract decisions:
- `AutomationTrigger` uses an OpenAPI discriminator (`propertyName:
type`); unknown discriminator values → HTTP 422, not 400.
- `PUT` and `DELETE` require an `If-Match` header (428 if absent, 409 on
mismatch); `GET` and `PUT` responses carry an `ETag`.
- `POST /automations/{id}/runs` fires the automation's enabled API
trigger; 409 if the automation is disabled or lacks one.
- Run sub-resource responses reuse the existing `Run` and
`PaginatedRunList` schemas.
### `RunSandbox` schema refactor
The sandbox schema is restructured to express the full lifecycle rather
than only the ready state:
| Before | After |
|---|---|
| Flat object with `provider`, `image`, `snapshot`, `runtime` |
Discriminated by `kind`: `planned`, `initializing`, `ready`, `failed` |
| `runtime` was nullable | Moved into `RunSandboxInstance`
(non-nullable); present only when `kind = ready` |
| No failure detail | New `RunSandboxFailure` schema with `error`,
`causes`, `duration_ms` |
`SandboxDetails.sandbox` now references `RunSandboxInstance` (the ready
state), which preserves the existing shape for the details endpoint
while the richer `RunSandbox` type appears on run responses.
### Web UI (`run-sandbox-lifecycle.ts`)
New helper module that bridges the old flat-object sandbox wire shape
and the new lifecycle-keyed shape, with display metadata for each
lifecycle state. Consumers (`RunSummaryPanel`, `TerminalView`,
`RunSandbox` route, `run-detail` header/tabs) updated to route through
these helpers so both old and new wire shapes are handled transparently.
### `fabro-devcontainer` removal
The `fabro-devcontainer` crate is removed from `Cargo.lock`,
`AGENTS.md`, nextest config, and all doc references. Public-facing
changelog entries for devcontainer-specific features are removed or
retitled.
### Plan Summary
- Add Automations CRUD + run sub-resource paths and schemas to the
OpenAPI spec
- Restructure `RunSandbox` schema to model lifecycle states (`planned →
initializing → ready | failed`)
- Add `fabro-automation` dependency to `fabro-api` for domain-type
reuse; add JSON parity round-trip tests
- Regenerate Rust API types and TypeScript client
- Remove `fabro-devcontainer` crate and all references
- Add `run-sandbox-lifecycle.ts` helper module in the web UI and update
all sandbox-state consumers
### Fabro Details
<details>
<summary>Ran 9 stages in 83m 29s for $35.47</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 22s | – | 0 |
| preflight_lint | 2m 40s | – | 0 |
| implement | 45m 36s | $28.67 | 0 |
| simplify_opus | 7m 46s | $2.46 | 0 |
| simplify_gpt | 6m 7s | $3.92 | 0 |
| verify | 12m 8s | – | 0 |
| fixup | 5m 52s | $0.43 | 0 |
| **Total** | **83m 29s** | **$35.47** | **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>
|