Commit graph

2138 commits

Author SHA1 Message Date
Fabro
99d3e7bf4e fabro(01KY7YH7RYCJ1BDVTTP96ZA4HV): implement (succeeded)
Fabro-Run: 01KY7YH7RYCJ1BDVTTP96ZA4HV
Fabro-Completed: 5
Fabro-Checkpoint: 378f2a7374

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-23 18:52:40 +00: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
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
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
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
thewoolleyman
7e97fba7f6 feat(workflow): keep push credentials fresh across long ACP turns
GitHub App installation tokens expire ~60 min after minting. On a long run,
the origin token baked into the sandbox clone at clone time is stale by the
time a late ACP node (e.g. the PR node) runs `git push`, causing an
`Invalid username or token` failure.

Two host-driven mechanisms, both using the existing
`Sandbox::refresh_push_credentials()` (re-mint + `git remote set-url origin`)
over the exec channel — no new inbound surface:

1. Turn-entry re-mint at each ACP node entry, so a push early in the turn uses
   a fresh token.
2. A background refresh-ahead loop, scoped to the turn via a drop-guard, that
   re-mints every 45 min so a single push-bearing turn that itself exceeds the
   TTL stays fresh. A normal sub-interval turn never ticks; a failed/timed-out
   tick retries sooner so a transient error cannot leave a longer-than-interval
   expired-token window.

Both refresh calls are timeout-bounded (30s) so a stalled GitHub API cannot
hang node entry. FABRO_PUSH_CRED_REFRESH_AHEAD (default on; falsy = empty/0/
false/off/no, case-insensitive) disables the whole feature — turn-entry and
loop — for operators who manage `origin` themselves;
FABRO_PUSH_CRED_REFRESH_INTERVAL_SECONDS overrides the interval (0 disables
just the loop). Both are added to the worker env allowlist.

refresh_push_credentials now returns RefreshOutcome (Refreshed vs Skipped) so
callers log accurately: Refreshed only when a GitHub App installation token was
actually re-minted; a static PAT or pre-minted Installation token (nothing to
re-mint) short-circuits to Skipped before the set-url exec.

Known follow-ups documented in-code: (a) resumed runs reconnect without App
creds, so refresh no-ops until they are threaded through the reconnect path;
(b) no freshness check on the per-entry mint; (c) the background set-url can
contend with the agent's own git on .git/config.lock; (d) parallel ACP branches
each run their own loop; (e) the refresh lives in the ACP handler only though
the stale-origin problem is stage-agnostic (native/command stages are not
covered); (f) refresh failures are logged via tracing but not surfaced as a
RunNotice event.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 02:47:31 +02:00
Bryan Helmkamp
18d4628291
Add OpenAI GPT-5.6 models (#566)
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
## Summary

- add GPT-5.6 Sol, Terra, and Luna to the built-in OpenAI model catalog
with current capabilities, limits, aliases, and pricing
- route all three models through the OpenAI Responses API and keep the
Codex-safe 272K context policy
- make GPT-5.6 Sol the OpenAI default and update the model docs and Ask
Fabro API contract

## Testing

- cargo nextest run -p fabro-model
- cargo nextest run -p fabro-llm builtin_catalog_route_equivalence_table
- cargo nextest run -p fabro-server list_models
- cargo nextest run -p fabro-server --features test-support
run_responses_include_ask_fabro_affordance
- cargo +nightly-2026-04-14 fmt --check --all
- cargo +nightly-2026-04-14 clippy -p fabro-model -p fabro-llm
--all-targets -- -D warnings
- git diff --check
2026-07-10 10:46:30 -04:00
fabro-sh-fabro[bot]
96372c5a3c
Demote server.integrations.slack.default_channel to a plain string (#565)
The `server.integrations.slack.default_channel` field was typed
`Option<InterpString>` but was never documented as interpolable — every
doc example uses a plain channel name like `#releases`. It resolved only
env vars, only once at server startup, and that capability was inherited
from a uniform schema-staging design, not a deliberate feature. This
brings it in line with every other server-scope config field, which were
already demoted to plain literals under the project rule that
interpolation belongs to fields resolved with run context.

## What changed

- **Type** (`fabro-types`, `fabro-config` layers):
`Option<InterpString>` → `Option<String>` in `SlackIntegrationSettings`
and `SlackIntegrationLayer`.
- **Demotion warning** (`resolve/server.rs`): calls
`warn_if_demoted_template` at resolve time with the field path
`server.integrations.slack.default_channel`, matching the pattern used
for earlier server-field demotions. A value still containing a `{{ env.*
}}`-shaped token is stored verbatim and triggers a startup warning — no
resolution, no error.
- **Startup wiring** (`server.rs`): the `value.resolve(process_env_var)`
call and its error mapping are deleted; the literal string is passed
directly to `SlackService::new`, which already accepts `Option<String>`.
- **System status handler** (`handler/system.rs`): removed the
now-unnecessary `display_interp` helper that called `resolve_or_source`;
the field is cloned directly into the metadata map.
- **Wire shape**: unchanged. `InterpString` serialized as its raw source
string, so stored/wire JSON is identical before and after. The OpenAPI
spec is untouched.

## What is not changing

Per-run Slack channels — `run.notifications.<route>.slack.channel` and
`run.interviews.slack.channel` — remain `InterpString` with variable
substitution at run creation. Those are the intended interpolating
surface and are correct as-is.

## Migration signal

Anyone who placed a `{{ env.NAME }}` token in
`server.integrations.slack.default_channel` (only possible during ~3
months of nightly builds) will see a startup warning naming the field.
The value is treated as a literal; no data is lost and startup does not
fail.

## Interview-prompt routing observation (step 4)

The interview-prompt posting path checks `run.interviews.slack.channel`
first and falls back to the server default only when the run-scope field
is absent — the preference already exists. No routing change is needed
or made here.


### Fabro Details

<details>
<summary>Ran 8 stages in 41m 23s for $10.10</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 41s | – | 0 |
| preflight_lint | 2m 42s | – | 0 |
| implement | 0s | – | 0 |
| simplify_fable | 27m 43s | $10.10 | 0 |
| simplify_gpt | 0s | – | 0 |
| verify | 7m 44s | – | 0 |
| **Total** | **41m 23s** | **$10.10** | **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-10 08:29:54 -04:00
fabro-sh-fabro[bot]
9daca83b35
Migrate provider extra_headers from typed refs to interpolation strings (#564)
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
## Summary

Provider `extra_headers` previously required values to be typed TOML
tables (`{ env = "X" }`, `{ literal = "Y" }`, `{ vault = "Z" }`). This
PR migrates them to the project's standard interpolation string format:
plain text for literals, `{{ env.NAME }}` tokens for environment
variables, and `{{ secrets.NAME }}` tokens for vault secrets. This
brings `extra_headers` in line with the rest of the interpolation system
and unlocks mixed-segment values like `Bearer {{ secrets.GATEWAY_TOKEN
}}`.

### What changed and why

**Config authoring surface (`fabro-config`):**
`ProviderSettings.extra_headers` changes from `Option<HashMap<String,
HeaderValueRef>>` to `Option<HashMap<String, InterpString>>`. The
`Combine` impl and all re-exports are updated accordingly.

**Catalog layer (`fabro-model`):**
`ProviderCatalogSettings.extra_headers` and
`CatalogProvider.extra_headers` become `HashMap<String, String>` — raw
interpolation source strings. This is required by the crate dependency
direction: `fabro-types` (which owns `InterpString`) depends on
`fabro-model`, so `fabro-model` cannot hold `InterpString` without
creating a cycle. The source string is re-parsed and resolved in
`fabro-auth` at credential-build time.

**Credential resolution (`fabro-auth`):** Both `CredentialResolver`
(vault-backed) and `EnvCredentialSource` (env-only) are rewritten to
parse each header source string as an `InterpString` and resolve it with
a `ResolveCtx` scoped to `env` + `secrets` only. A new
`resolve_extra_headers` helper is shared between the two paths. Vault
resolution uses `vault_token_lookup`, which wraps `vault_get_token` and
maps any non-Token vault entry to `None` — so file and OAuth vault
entries fail closed rather than resolving incorrectly. `vars.*` and
`inputs.*` tokens are not in scope and produce `Unavailable` errors
automatically.

**New error variant:** `ResolveError::Interpolation { provider, source
}` surfaces header resolution failures as diagnosable auth issues. The
inner `source` (an `InterpResolveError`) names only the token namespace
and name — never a resolved value.

**`{ literal = "..." }` guardrail removed:** `HeaderValueRef`
deliberately rejected bare string header values to discourage pasting
credentials. `InterpString` accepts any string. This is an intentional
change; the mitigation is documentation — use `{{ secrets.NAME }}` for
credential-shaped values, not bare literals.

**Redactor registration gap (noted, not fixed here):** Secrets resolved
into provider headers at the credential boundary do not flow through the
run boundary's exact-match redaction registry. Exposure is low (headers
are host-side and outbound-only, never logged), but a follow-up should
thread a registering lookup through `VaultCredentialSource`. A code
comment at the resolution site marks the gap.

### Breaking change

Existing `extra_headers` config using `{ env = "X" }`, `{ literal = "Y"
}`, or `{ vault = "Z" }` table syntax **will fail to parse** after this
change. Users must migrate to the token form: plain strings for
literals, `{{ env.X }}` for env vars, `{{ secrets.X }}` for vault
secrets. A changelog entry is included.

### Plan Summary

- Update `ProviderSettings.extra_headers` → `InterpString` in
`fabro-config`
- Collapse authoring `InterpString` → source `String` in
`provider_settings_to_catalog` (allowlisted `as_source()` call)
- Delete `HeaderValueRef` and its serde/display/parse machinery from
`fabro-model`
- Rewrite both auth resolution paths to use `InterpString::parse +
resolve_with`; add `Interpolation` error variant
- Add `vault_token_lookup` helper for token-only fail-closed vault
resolution
- Update test TOML in `fabro-llm`, builtin catalog comment in
`openrouter.toml`, and all hand-written + generated docs


### Fabro Details

<details>
<summary>Ran 8 stages in 108m 43s for $34.44</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 5m 33s | – | 0 |
| preflight_lint | 5m 59s | – | 0 |
| implement | 43m 23s | $17.34 | 0 |
| simplify_fable | 32m 49s | $13.09 | 0 |
| simplify_gpt | 6m 25s | $4.01 | 0 |
| verify | 13m 58s | – | 0 |
| **Total** | **108m 43s** | **$34.44** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-8; }
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD. Be sure to use the rust-style-guide skill to help you follow this repo's Rust style conventions.", model="gpt-55", reasoning_effort="xhigh"]
    simplify_fable    [label="Simplify (Fable)", prompt="@prompts/simplify.md", model="claude-fable-5", reasoning_effort="xhigh"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, timeout="1800s", script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=succeeded"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=succeeded"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=succeeded"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_fable -> simplify_gpt -> verify
    verify -> exit  [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-07-09 16:47:35 -04:00
fabro-sh-fabro[bot]
d5dcd11793
Remove unused provenance tracking from config interpolation (#562)
The `Resolved` / `Provenance` types in `fabro-types` interp tracked
which env vars and secrets contributed to a resolved value, but no
production code ever read `.provenance` — every caller immediately
discarded it with `.map(|r| r.value)`. The redaction design this
metadata anticipated was superseded by per-run exact-value registration
(`fabro_redact::SecretRedactor`); origin-tagging on resolved strings
can't reach the surfaces where secrets actually leak (subprocess output,
diffs, tool output), so it added no coverage.

This PR removes the dead scaffolding with zero behavior change:

- `resolve` / `resolve_with` now return `Result<String, ResolveError>`
directly; `Resolved` and `Provenance` are deleted along with the
name-accumulation logic inside `resolve_with`.
- All call sites drop the now-unnecessary `.map(|r| r.value)` unwrap.
- Provenance assertions in tests are removed; all value/error assertions
are preserved.
- The module doc is updated to describe the actual model: secret values
are intended to be registered into a per-run exact-value redactor at
resolution time; sensitivity is not tracked on resolved strings.
- A comment on `ResolvedRunGoal` / `ResolvedGoalSource` (an unrelated
run-metadata concept sharing the word "provenance") is rephrased to
avoid confusion with the deleted type.

`Provenance` no longer appears in `fabro-types/src/settings/mod.rs`
exports. The unrelated `RunClientProvenance` / `RunServerProvenance`
run-spec types are untouched.


### Fabro Details

<details>
<summary>Ran 8 stages in 39m 23s for $7.58</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 17s | – | 0 |
| preflight_lint | 2m 33s | – | 0 |
| implement | 11m 6s | $4.23 | 0 |
| simplify_fable | 8m 27s | $1.60 | 0 |
| simplify_gpt | 5m 58s | $1.75 | 0 |
| verify | 8m 36s | – | 0 |
| **Total** | **39m 23s** | **$7.58** | **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-09 10:48:02 -04:00