- rename resolve_route catalog-instance test to describe its actual
id-based resolution assertion
- use EnvVars::OPENAI_API_KEY instead of a raw string in the automation
scheduler test fixture
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merging main brought in billing tests that construct ModelRef with String
model ids and an integration test that pins an OpenRouter run via the
backend's provider id. The ModelRef sites now use ModelId conversions.
The integration test also exposed a real regression: resolve_provider_context
ignored the persisted run provider whenever the model selector resolved
globally, re-routing pinned OpenRouter runs to a higher-priority provider for
nodes without explicit model/provider attrs. Request-time routing now treats
the run's selected provider as a pin with custom-model passthrough, matching
transform-time selection semantics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The brew_command helper inherits the developer's environment (unlike
context.command(), which env_clears), so an exported FORCE_COLOR or
CLICOLOR_FORCE overrides the NO_COLOR=1 the harness sets and the CLI
renders ANSI codes into snapshot output, failing
upgrade_brew_install_refuses_and_prints_brew_command and
upgrade_brew_install_rejects_version_flag on any machine with
FORCE_COLOR exported.
Remove FORCE_COLOR, CLICOLOR_FORCE, and CLICOLOR from the spawned
command's env, and add the FORCE_COLOR constant to EnvVars.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consolidate duplicated resolution logic introduced by the provider-aware
model alias work:
- Add Catalog::resolve_selection (with SelectedModel and ready_provider)
as the single home for the select -> passthrough -> default-fallback
policy, replacing six hand-rolled copies across fabro-server,
fabro-workflow, and fabro-llm.
- Move legacy [models] row resolution into a shared LegacyModelIndex and
LegacyModelError in fabro-model; delete fabro-config's parallel
implementation and its LlmNormalizationError enum, plus the now-unused
builtin_* catalog helpers.
- Drop redundant client.resolve_request calls (and their full-request
clones) from the completions and playground handlers.
- Remove the redundant resolve_provider_context round-trip in
resolve_start_llm and make resolve_run_model return a ProviderId
instead of a never-None Option.
- Replace the "<default model>" sentinel selector with a dedicated
ModelSelectionError::NoDefaultModel variant.
- Add a CatalogRoute trait so provider adapters call
self.api_model_id(...) instead of threading catalog/provider args.
- Delete the unused FromStr impl for ModelId; dedupe the CLI's
id-or-alias predicate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A branch node reachable from multiple parallel nodes previously had its
warning and fix hint name an arbitrary first parent. Collect all unique
parallel parents (sorted) and render the full list in both.
Addresses review feedback on #595.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add two lint rules so the graph format stops silently accepting
attributes that nothing reads:
- inert_attribute: handler-specific attributes (script, language,
duration, join_policy, max_parallel, output_schema, prompt) placed on
node types that never read them. Attributes read by several handlers
(timeout), resolved for every node (fidelity, retry_policy), or
injectable via model stylesheets (model, reasoning_effort, ...) are
deliberately excluded.
- parallel_branch_inert_attribute: fidelity/thread_id on parallel
branch nodes and fork->branch edges. Branch dispatch bypasses the
fidelity lifecycle, so these are dead letters today; the warning
points at the parallel node, where fidelity does take effect.
Also reconcile the loop_restart docs with actual executor behavior:
taking a loop_restart edge restarts from the target with a fresh empty
context (visit counts preserved), on success as well as failure; the
transient_infra guard applies only to failure crossings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assert ParallelBranchStarted seeds started_at (the live-timer half of
the fix) and cover the failed-status fold to a Failed terminal state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the billed_model_usage_from_llm_with_cost wrapper with a
with_reported_cost method on BilledModelUsage and BilledTokenCounts, and
centralize the optional-cost fold as UsdMicros::accumulate so fabro-agent
and fabro-workflow share one implementation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add Poolside as a built-in OpenAI-compatible provider and expose Laguna S 2.1 and XS 2.1 both directly and through OpenRouter. Include vault/env credential registration, secret redaction, live coverage, catalog tests, and user documentation.
Consolidate duplicated logic from the SQLite runs read model review:
- Derive the status sort CASE and board-column filter from a new
RunStatusKind::board_rank(), replacing three hand-maintained copies
of the status/column mapping; add a test upserting every status
variant so the migration CHECK can't silently drift
- Share RunSize bucket thresholds between from_total_usd_micros and
the generated size-sort CASE via RunSize::BUCKET_MAX_USD_MICROS
- Resolve run selectors from a lean identity query instead of
decoding every stored summary per request
- Delete the RunsSortKey/RunsSortDirection adapter enums; the store
sort enums now carry the wire serde names
- Consolidate the workflow display-name fallback chain into
WorkflowRef::display_name() (store, CLI, run lookup)
- Share pagination clamping and the paginated list envelope across
handlers
- Reconcile now skips rows whose source seq is unchanged and
batch-deletes stale rows; drop the two indexes no query can use
- Hold the summary store OnceLock cell in RunDatabaseInner instead of
a snapshot so late attachment reaches already-open writers
- Misc: expect() on COUNT(*) sign, %err logging, shared wall-time
helper, shared SQLite test fixture, dead billing fallback removed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consolidate the legacy-import helpers (backup path naming, RFC 3339
timestamp parsing, import report) into fabro-db and reuse them from the
vault, automation, variable, and environment stores. Add
SecretStore::open_snapshot to collapse the repeated
open/snapshot/into_vault chain. Let automation trigger canonicalization
live solely in normalize_replace, replace its redundant second full
validation with a targeted manual-id collision check, single-source the
automation SELECT projection, and gate list_automation_runs on a
lightweight existence query.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Delete the dead test-only Vault-based env-secrets migration and point
the startup migration tests at the production migrate_to_store path
over a real SQLite-backed SecretStore
- Extract shared legacy-import helpers (timestamped backup rename,
is_toml_file) into fabro_db::legacy and parse_rfc3339_utc into
fabro-db, replacing four per-crate copies
- Take one secrets snapshot in migrate_to_store instead of per-name
queries
- Share one bind order between the MCP store INSERT and UPDATE
statements
- Return SecretEntry directly from entry_from_row
- Unify the environment/MCP store blocking loaders into a generic
load_store_blocking helper
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The automations legacy import declares REMOVAL_DEADLINE per
docs/internal/migrations-strategy.md; the secrets JSON import predates it
and never did. Add the same constant and log field so the temporary
migration's lifespan is visible in code and in startup logs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review pass over the secrets-to-SQLite migration:
- Add SecretStore::open() consolidating the connect/migrate/import-legacy
sequence repeated at five call sites; fabro-agent and fabro-cli drop
their fabro-db dependency
- Restore process-env LLM credential lookup in the standalone CLI/agent
sources via SqlVaultCredentialSource::new (regression: vault_only
dropped the env fallback that VaultCredentialSource::new provided)
- Fix five install tests that still asserted against the legacy
secrets.json, which the importer renames to .bak
- Make AppStateConfig.preloaded_vault required, deleting the fallback
that re-read the already-renamed legacy file; drop the now-unused
vault_path field and demote load_startup_vault to test-only
- Skip the snapshot clones and CAS retry in resolve() when the vault
holds no OAuth secrets (per-request hot path)
- Remove dead persist_with_secret_store, the VaultSecretWrite alias,
the secret_type_string one-liner (now SecretType::as_str), the
impossible RowCountOverflow error, and duplicated row parsing
- Run check_crypto concurrently with the other diagnostics checks
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Detect applied migrations via sqlx's Migrate trait
(ensure_migrations_table + list_applied_migrations) instead of
hand-querying the _sqlx_migrations bookkeeping table, so the check
cannot drift from what Migrator::run actually applies.
- Write the snapshot to a staging file and rename it into place, so a
failure mid-copy never leaves a partial file at the snapshot path.
- Derive the database path from the pool's connect options instead of
storing a duplicate copy on Database.
- Drop the invented "fabro.sqlite3" fallback filename from
pre_migration_snapshot_path; append the suffix to the path directly.
- Deduplicate the snapshot-inspection blocks in the test behind small
connect_read_only/table_exists helpers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A binary downgrade after new SQLite migrations have been applied fails
sqlx's startup validation ("migration was previously applied but is
missing in the resolved migrations") and previously left the operator
with no rollback artifact: the shared database had no backup, so
recovering meant hand-editing _sqlx_migrations and dropping tables.
Database::migrate now writes a consistent single-file snapshot to
<db>.pre-migration.bak (via VACUUM INTO, mode 0600) before applying any
migration the database has not seen. Rollback is: stop the server,
replace the database file with the snapshot, delete -wal/-shm siblings,
start the previous binary. Fresh databases and no-op migrates skip the
snapshot, so the file always preserves the state from immediately before
the most recent schema change. A snapshot failure fails the migration:
no rollback artifact, no schema change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Parallel branches run in spawned tasks that bypass the engine's
StageStarted/StageCompleted lifecycle, so a branch stage was created
Running by its first branch-scoped event and never reached a terminal
state. On a successful run nothing swept it (only RunFailed does), so
the fan-out rows spun forever with a `--` duration even after the run
and its fan-in finished.
Fold ParallelBranchStarted/ParallelBranchCompleted in the projection:
seed started_at for the live timer, then set the terminal state and
wall-time from the branch's own completion event.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The exact-value secret registry (`SecretRedactor`) was built as
infrastructure ahead of its wiring, but the wiring was never merged —
the team settled on content-based redaction (entropy analysis + gitleaks
patterns) as the sole mechanism. The type had zero consumers outside its
own crate. This PR removes it and corrects two doc comments that
described the abandoned design as pending.
**What changed:**
1. `fabro-redact/src/secret_registry.rs` deleted in full (~217 lines),
with its `mod` declaration and `pub use` re-export removed from
`lib.rs`. `Region`, `redact_string`, `redact_json_value`,
`DisplaySafeUrl`, and everything else in the crate are untouched.
2. The `resolve_extra_headers` doc in `fabro-auth` no longer promises
future exact-match registration. It now honestly states that low-entropy
header values not shaped like credentials are not caught by
content-based redaction.
3. The `InterpString` module doc in `fabro-types` no longer describes a
pending per-run registry. It states the real architecture: resolved
secret values are plain strings, and redaction is content-based applied
at output serialization.
**Known limitation (pre-existing, not introduced here):** a declared
secret whose value is a low-entropy ordinary word (e.g. an environment
name) is not caught by content-based detection. This was the gap
`SecretRedactor` was meant to fill; it is an accepted trade-off, not a
regression from this PR.
### Fabro Details
<details>
<summary>Ran 8 stages in 26m 21s for $2.27</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 34s | – | 0 |
| preflight_lint | 2m 45s | – | 0 |
| implement | 0s | – | 0 |
| simplify_fable | 9m 44s | $2.27 | 0 |
| simplify_gpt | 0s | – | 0 |
| verify | 10m 50s | – | 0 |
| **Total** | **26m 21s** | **$2.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-8; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD. Be sure to use the rust-style-guide skill to help you follow this repo's Rust style conventions.", model="gpt-55", reasoning_effort="xhigh"]
simplify_fable [label="Simplify (Fable)", prompt="@prompts/simplify.md", model="claude-fable-5", reasoning_effort="xhigh"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, timeout="1800s", script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_fable -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>