Commit graph

2143 commits

Author SHA1 Message Date
Bryan Helmkamp
96102854d8
chore: address review feedback
- 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>
2026-07-23 13:13:45 -04:00
Bryan Helmkamp
831b750664
fix: honor the run's provider pin during request-time routing
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>
2026-07-23 13:13:45 -04:00
Bryan Helmkamp
32d1b1b339
Merge remote-tracking branch 'origin/main' into provider-aware-model-aliases 2026-07-23 12:50:22 -04:00
Bryan Helmkamp
1874497056
Merge pull request #596 from fabro-sh/test-strip-force-color
fix(test): strip inherited color-forcing env vars in brew upgrade tests
2026-07-23 12:16:01 -04:00
Bryan Helmkamp
d213e23dc6
Merge pull request #595 from fabro-sh/validate-inert-attributes
feat(validate): warn on inert node and edge attributes
2026-07-23 12:12:26 -04:00
Bryan Helmkamp
14317ccaf7
fix: preserve legacy model selector compatibility 2026-07-23 12:12:09 -04:00
Bryan Helmkamp
f02ee6aaed
fix(validate): handle custom types and inherited threads 2026-07-23 11:19:44 -04:00
Bryan Helmkamp
edbd25f67c
fix(test): strip inherited color-forcing env vars in brew upgrade tests
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>
2026-07-23 11:19:01 -04:00
Bryan Helmkamp
05e6507095
refactor: unify model selection and legacy-model normalization
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>
2026-07-23 11:07:47 -04:00
Bryan Helmkamp
3af4335098
fix(validate): name every parallel parent in branch diagnostics
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>
2026-07-23 11:04:14 -04:00
Bryan Helmkamp
0748d0c6b8
feat(validate): warn on inert node and edge attributes
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>
2026-07-23 10:48:18 -04:00
Bryan Helmkamp
65cdf52061
feat: make model aliases provider-aware 2026-07-23 10:12:25 -04:00
Bryan Helmkamp
7b52b3b6ba
Merge pull request #584 from andremw/fix/parallel-branch-stage-stuck-running
Finalize parallel branch stages instead of leaving them Running
2026-07-23 09:45:49 -04:00
Bryan Helmkamp
580ee156b2
Merge pull request #594 from fabro-sh/preserve-provider-costs
Preserve provider-reported workflow costs
2026-07-23 08:10:18 -04:00
Bryan Helmkamp
08cef66ec8
Saturate billing cost accumulation 2026-07-23 07:53:01 -04:00
Bryan Helmkamp
5b0cfd4050
Tighten parallel branch projection tests
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>
2026-07-23 07:37:34 -04:00
Bryan Helmkamp
4d2712b8db
Consolidate provider-reported cost handling into fabro-model
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>
2026-07-23 07:21:17 -04:00
Bryan Helmkamp
7f25689fb6
Preserve provider-reported workflow costs 2026-07-23 07:08:04 -04:00
Bryan Helmkamp
f8ed856959
Add regression test for authoritative provider billing 2026-07-23 06:59:48 -04:00
Bryan Helmkamp
e6c92aeb27
Add current GPT and Claude models to OpenRouter catalog 2026-07-23 06:50:26 -04:00
Bryan Helmkamp
b39e3767a3
Fix legacy token billing projection 2026-07-22 21:37:13 -04:00
Bryan Helmkamp
d629816c3b
Merge pull request #589 from fabro-sh/codex/glm-5-2-support
Add GLM 5.2 direct and OpenRouter support
2026-07-22 19:47:21 -04:00
Bryan Helmkamp
3fd4b44d6e
Rename reasoning stream snapshot 2026-07-22 19:29:16 -04:00
Bryan Helmkamp
911f26f8e5
Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Bryan Helmkamp <19+brynary@users.noreply.github.com>
2026-07-22 19:14:54 -04:00
Bryan Helmkamp
8f47275d84
feat(llm): add Poolside Laguna models
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.
2026-07-22 18:49:16 -04:00
Bryan Helmkamp
8a243085d0
Merge remote-tracking branch 'origin/main' into codex/glm-5-2-support
# Conflicts:
#	lib/crates/fabro-llm/tests/integration.rs
#	lib/crates/fabro-model/src/catalog.rs
2026-07-22 18:37:15 -04:00
Bryan Helmkamp
04f55666f9
Merge pull request #588 from fabro-sh/codex/openrouter-kimi-k3
Add Kimi K3 via OpenRouter
2026-07-22 18:29:47 -04:00
Bryan Helmkamp
1511c84f48
Test GLM 5.2 through Z.AI 2026-07-22 17:26:37 -04:00
Bryan Helmkamp
d8613fa052
Merge pull request #568 from thewoolleyman/push-credential-refresh-ahead
feat(workflow): keep push credentials fresh across long ACP turns
2026-07-22 17:20:40 -04:00
Bryan Helmkamp
c67cfef141
Add GLM 5.2 model support 2026-07-22 17:19:16 -04:00
Bryan Helmkamp
129c893c86
Add Kimi K3 via OpenRouter 2026-07-22 16:59:25 -04:00
Bryan Helmkamp
03b1b790d1
Merge pull request #587 from fabro-sh/codex/kimi-k3-openai-compat
Add Kimi K3 support and fix OpenAI-compatible tools
2026-07-22 16:45:46 -04:00
Bryan Helmkamp
5d345cf393
Fix OpenAI-compatible parity fixture 2026-07-22 16:37:32 -04:00
Bryan Helmkamp
ca11cac25d
Add Kimi K3 catalog support 2026-07-22 16:23:40 -04:00
Bryan Helmkamp
a96f90bbd8
Fix OpenAI-compatible agent tools 2026-07-22 15:31:17 -04:00
Bryan Helmkamp
49c30aa4a6
Let repo init use the server default environment 2026-07-22 14:44:43 -04:00
Bryan Helmkamp
2843b33d92 Simplify runs read model: single-source mappings, leaner queries
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>
2026-07-22 13:55:22 -04:00
Bryan Helmkamp
22844300af Add SQLite runs read model 2026-07-22 13:55:22 -04:00
Bryan Helmkamp
6ec4c9a13d Simplify SQLite stores after review pass
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>
2026-07-22 13:37:31 -04:00
Bryan Helmkamp
c12264c16d Migrate automations to SQLite 2026-07-22 13:32:35 -04:00
Bryan Helmkamp
431399826d Simplify SQLite stores after review
- 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>
2026-07-22 13:11:10 -04:00
Bryan Helmkamp
ec3933d5de Move MCP servers to SQLite storage 2026-07-22 13:10:01 -04:00
Scott Werner
a2c2781a6e Declare a removal deadline on the legacy secrets JSON import
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>
2026-07-22 12:47:37 -04:00
Bryan Helmkamp
f2cbc016ee Clean up SQLite secrets migration and fix CLI env credential regression
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>
2026-07-22 12:45:38 -04:00
Bryan Helmkamp
6860852c9c Simplify SQLite secret storage 2026-07-22 12:45:38 -04:00
Bryan Helmkamp
d325a2c1e6 Migrate secrets storage to SQLite 2026-07-22 12:44:20 -04:00
Scott Werner
f97ac8df3c Simplify pre-migration snapshot internals
- 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>
2026-07-22 12:08:25 -04:00
Scott Werner
f91c71b8e0 Snapshot fabro.sqlite3 before applying new migrations
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>
2026-07-22 08:58:33 -04:00
andremw
c623bf7432 Finalize parallel branch stages instead of leaving them Running
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>
2026-07-22 17:54:42 +09:30
fabro-sh-fabro[bot]
eccbed80b7
Remove unused SecretRedactor registry and update stale comments (#574)
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
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>
2026-07-12 10:37:46 -04:00