Commit graph

76 commits

Author SHA1 Message Date
Bryan Helmkamp
29e408aa18
Merge origin/main into feat/sandbox-bash-contract 2026-07-24 22:42:28 -04:00
Release Repro
5d0617f547
fix(agent): harden compaction reasoning budgets
Model default reasoning explicitly at the provider-route level so always-reasoning endpoints without effort controls receive summary headroom. Cap all summary requests at model output limits and bound retained visible summaries to the original allowance. Reuse builtin catalog fixtures and named budget constants in tests, and document the new model setting.
2026-07-24 21:58:23 -04:00
Bryan Helmkamp
3606ba6a0f
feat(sandbox): standardize command execution on non-login Bash
Fabro advertised Bash while its three backends implemented three
different contracts: Daytona evaluated commands through `sh`, and
Docker's streaming, stdio, and setup paths used a login shell. Bash-only
syntax silently misbehaved depending on provider and code path, and
login profiles could change PATH and command behavior per image.

Make `bash -c` the enforced interpreter for every command string the
Unix sandbox API accepts, on every production backend and through both
buffered and streaming execution. This selects the interpreter only —
no `errexit`, no `pipefail`, no login mode — so `false | true` still
succeeds and a workflow that wants other semantics writes them into its
own command.

Local resolves `bash` through the worker's PATH (NixOS has no
/bin/bash) and reuses that one executable across all three command
paths. Docker and Daytona require /bin/bash with no `sh` fallback.

Fresh initialization and resume/start now verify Bash through a shared
marker-validating probe before reporting the sandbox usable, so a
missing or non-Bash interpreter fails at the lifecycle boundary with
provider-specific remediation instead of on the first command. The
probe also rejects Bash in POSIX mode, which an image whose `bash` is
really `sh` would otherwise pass.

Sandbox MCP scripts and the detached launch wrapper move under the same
contract; host-side stdio MCP scripts, hooks, and interactive terminals
are separate executors and keep their existing `sh` behavior.

The `shell` tool's name and JSON schema are unchanged across providers;
only its prose now identifies `command` as Bash source.

BREAKING CHANGE: sandbox commands no longer load login-shell profiles,
so environment set in /etc/profile.d/*.sh, ~/.bash_profile, or
nvm/rbenv/sdkman initializers is gone. Move those exports into the
Dockerfile's ENV or the Daytona snapshot image.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 21:26:05 -04:00
Bryan Helmkamp
85f3286c66
Merge branch 'main' into feat/shared-checkout-parallel 2026-07-24 06:29:57 -04:00
Bryan Helmkamp
0a39ba9e06
Shared-checkout parallel execution (recovered from run 01KY7YH7RYCJ1BDVTTP96ZA4HV)
Cumulative implement + simplify_fable diff recovered from the run's meta
branch (fabro/meta/01KY7YH7RYCJ1BDVTTP96ZA4HV, stage 006 diff.patch).
The run validated this tree clean: cargo nextest (7,007 passed), clippy,
fmt, TS client regen + typecheck, web tests (679 passed), docs check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 06:19:11 -04:00
Bryan Helmkamp
cdf1dce4f8
fix(agent): remove dead execution limits 2026-07-23 19:36:29 -04:00
Bryan Helmkamp
7bdee5b494
Merge remote-tracking branch 'origin/main' into pr-599
# Conflicts:
#	docs/public/agents/outputs.mdx
#	docs/public/reference/dot-language.mdx
2026-07-23 17:24:33 -04:00
Bryan Helmkamp
b558af070f
Merge pull request #601 from fabro-sh/fabro/run/01KY7Y01REECZ24XXTMBZ3PPV9
Provider-scoped model catalog: `(provider, model slug)` as stable ident…
2026-07-23 17:14:51 -04:00
Fabro
b3e085dc45 Merge remote-tracking branch 'origin/main' into fabro/run/01KY7Y01REECZ24XXTMBZ3PPV9 2026-07-23 20:29:21 +00:00
Bryan Helmkamp
886371e474
refactor(workflow): clean up last-file routing fallback review nits
Document the contract of read_last_file_routing_json (terminal JSON
extraction only; routing validation happens downstream), extract a
shared sandbox_with_file test helper, and drop the misleading
"standalone" wording from the fallback docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:47:17 -04:00
Bryan Helmkamp
02437fb18a
fix(workflow): constrain last-file routing fallback 2026-07-23 15:28:19 -04:00
Fabro
44df6d6378 Merge remote-tracking branch 'origin/main' into fabro/run/01KY7WQ92JWT90307EBQY6P2HV 2026-07-23 18:52:01 +00:00
Fabro
ad15da7461 fabro(01KY7WQ92JWT90307EBQY6P2HV): simplify_fable (succeeded)
Fabro-Run: 01KY7WQ92JWT90307EBQY6P2HV
Fabro-Completed: 6

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-23 17:48:44 +00:00
Fabro
1a2bd7966d fabro(01KY7Y01REECZ24XXTMBZ3PPV9): implement (succeeded)
Fabro-Run: 01KY7Y01REECZ24XXTMBZ3PPV9
Fabro-Completed: 5
Fabro-Checkpoint: 1f2ff54692

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-23 17:41:29 +00:00
Fabro
f68187bfc6 fabro(01KY7WQ92JWT90307EBQY6P2HV): implement (succeeded)
Fabro-Run: 01KY7WQ92JWT90307EBQY6P2HV
Fabro-Completed: 5

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-23 17:16:41 +00: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
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
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
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
Ryan Neal
d5b2220ed3
feat(llm): Amazon Bedrock provider — Converse codec, SigV4 + API-key auth (#459)
Adds **Amazon Bedrock** as an opt-in built-in provider, over Bedrock's
unified **Converse / ConverseStream** API. One codec serves every
Converse-capable family — Claude, Amazon Nova, Meta Llama, Mistral,
DeepSeek, Moonshot Kimi, Z.AI GLM, MiniMax, NVIDIA Nemotron, and OpenAI
gpt-oss — because AWS translates the envelope to each model's native
dialect server-side. Auth is either **AWS SigV4** (the default
credential chain — env / profile / IMDS / IRSA / SSO, resolved per
request so sessions refresh) or a **Bedrock API key**
(`AWS_BEARER_TOKEN_BEDROCK`, bearer). Disabled by default (the Ollama /
OpenRouter opt-in pattern).

This is the redo of #459's original Claude-only `InvokeModel` adapter,
rebuilt on the gateway-refactor seams (#481–#497). @depopry's SigV4
signer, AWS event-stream frame decoder, `BedrockAuth`, the `aws_sigv4`
credential grammar, `AdapterKind::Bedrock`, region-from-base_url, and
the lean-deps decision are preserved and authored by him on the first
two commits; the per-family `BedrockCodec` trait he wrote turned out to
be the crate-wide `Codec` seam in miniature, so the refactor promoted
exactly that shape. The original Claude-only description is preserved in
a comment below.

## What's here

- **`AdapterKind::Bedrock` × `CodecKind::BedrockConverse`** on the
route, plus the `aws_sigv4` credential source (no static secret — the
adapter signs at request time; `fabro-auth` stays AWS-free).
*(@depopry)*
- **SigV4 signer + AWS event-stream `FrameDecoder`** on the lean AWS
stack (no `aws-sdk-bedrockruntime`; transport stays on `fabro-http`).
Re-targeted at Converse's direct-JSON stream frames; the signer resolves
credentials per request. *(@depopry)*
- **`bedrock_converse` codec** — Converse envelope (`system[]`, typed
content blocks, `inferenceConfig`, `toolConfig`), prompt caching via
`cachePoint`, thinking-signature round-trip through `reasoningContent`,
usage mapped onto the disjoint `TokenCounts` buckets,
`provider_options.bedrock` passthrough. Plus the adapter shell and an
event-stream byte loop beside the transport's shared SSE loop.
- **Catalog**: `bedrock.toml` (Claude incl. Fable 5, Nova 2, Llama 4,
Mistral, DeepSeek, Kimi, GLM, MiniMax, Nemotron, gpt-oss — cross-region
inference-profile ids, per-model `billing_policy` so Claude bills
Anthropic-style) and a companion **`bedrock-openai`** provider for
GPT-5.5/5.4 over the `bedrock-mantle` Responses endpoint (pure config
over the existing `openai_responses` codec, zero new code).
- Secrets registry (`AWS_BEARER_TOKEN_BEDROCK`), gitleaks rules for both
Bedrock key formats, the `docs/integrations/bedrock` guide, and live e2e
tests.

## Live verification (confirmed end-to-end against a real AWS account)

Verified on a real Bedrock account (us-east-2, SigV4 + bearer):

- **SigV4 + Converse** — multiple families (Claude, Nova, DeepSeek, …)
via the full settings → catalog → route → adapter → codec path.
- **ConverseStream** — streaming deltas through the workflow engine.
- **Multi-turn tool use** — agent loop with tool calls round-tripping
(no-arg tools included).
- **Multi-model routing** — Claude + DeepSeek pinned in one run through
the single Converse codec.
- **mantle Responses** — `openai.gpt-5.5` answered via the
`bedrock-openai` provider (bearer auth).

The exercise caught and fixed several issues that unit tests (static
creds, mocked transports) could not — see the follow-up commits below.

## Follow-up fixes from live testing (commits on top of the foundation)

1. **Worker AWS env** — the workflow worker scrubs its env to an
allowlist, so SigV4 (which re-resolves from the ambient chain per
request) couldn't work through `fabro run`. The AWS credential-chain
inputs now cross into the worker.
2. **Vault bearer key** — Bedrock was the only key-based provider
missing a `vault:` credential ref, so `fabro secret set
AWS_BEARER_TOKEN_BEDROCK` silently didn't feed it. Now resolves env →
vault → SigV4.
3. **Converse tool-encoding hardening** — a no-arg tool call's
`toolUse.input` is now a `{}` object (Bedrock rejects null), and every
tool `inputSchema` gets a top-level `type: "object"` (strict families
like DeepSeek reject a typeless schema Claude tolerates).
4. **Nova output cap** — `amazon.nova-2-lite` max_output 65536 → 65535
(Bedrock's per-request limit).

Earlier fixes already folded into the foundation commits: the
`aws-config` sleep-impl (default chain panicked) and AWS error-body
decoding (top-level `message`/`Message`/`__type` → proper messages
instead of "Unknown error").

## Manual testing & setup

See `docs/integrations/bedrock` — now documents the non-obvious account
setup that live testing surfaced: the per-Region Anthropic use-case
approval, `aws-marketplace:Subscribe` for third-party models, the Fable
5 / Mythos-class data-sharing opt-in, and the bearer-vs-SigV4 precedence
override for running Converse + mantle side by side.

## Open decision / discussion

- **Model-id naming** — Bedrock rows use dotted ids mirroring Bedrock's
native inference-profile ids (`us.anthropic.claude-sonnet-4-6`,
`openai.gpt-5.5`), which also makes them the wire `api_id`. Third scheme
alongside bare ids and OpenRouter's `vendor/model` slashes. No collision
risk (enforced at catalog build). Open to a uniform scheme if preferred.
- **`BEDROCK_API_KEY` alias** — see the comment thread; the AWS console
hands some users `export BEDROCK_API_KEY=` while the SDK-standard var is
`AWS_BEARER_TOKEN_BEDROCK`. Question of whether to accept both.

## Deferred (named follow-ups)

- **`qwen.qwen3-coder-next`** — omitted pending a verified Bedrock
model/inference-profile id (its fabro id isn't a valid Bedrock
identifier; needs an explicit `api_id`). Re-add once confirmed via `aws
bedrock list-inference-profiles`.
- **Claude Mythos 5** — Anthropic-Messages-only on `bedrock-mantle`
(limited preview).
- **Converse structured output** (`response_format` rejected with a
clear error).
- **`reasoning_effort` on Converse rows** via
`additionalModelRequestFields` (the `bedrock-openai` GPT rows already
accept effort levels).
- **CountTokens** route (`count_input_tokens` returns `None`).

## Verification

`cargo nextest run --workspace`: green except the pre-existing
environment-dependent fabro-workflow failures (identical on main).
clippy `-D warnings` + pinned-nightly fmt clean. Codec unit tests +
adapter httpmock tests + frame-decoder/signer locks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Scott Werner <scott@sublayer.com>
Co-authored-by: Scott Werner <stwerner@vt.edu>
2026-06-16 11:46:49 -04:00
Scott Werner
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>
2026-06-12 14:39:09 -04:00
Bryan Helmkamp
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>
2026-06-10 14:01:56 -04:00
fabro-sh-0530[bot]
7b7a2c9044
Add fabro variable CLI namespace for server-managed variables (#434)
## Summary

Exposes the existing variables API through a new `fabro variable` CLI
namespace (`list`, `get`, `set`, `rm`), following the same patterns as
`fabro secret`. Variables are intentionally readable — `list` and `get`
show stored values — while `fabro secret` remains write-only. This PR
also ships a significant set of accompanying changes: a refactored
sandbox lifecycle model in the web UI, removal of the
`fabro-devcontainer` crate, and a new `RunSandbox` OpenAPI schema that
models the full planned → initializing → ready/failed lifecycle.

## What Changed

### CLI (`fabro variable`)
- New `fabro variable` namespace with `list` (aliased `ls`), `get`,
`set`, and `rm` subcommands, dispatched through the same
`ServerTargetArgs` pattern as `fabro secret`.
- `fabro-client` gains five new wrapper methods (`list_variables`,
`get_variable`, `create_variable`, `update_variable`, `delete_variable`)
over the generated OpenAPI client.
- `set` is an upsert; `--value-stdin` accepts empty input after
newline-trimming (unlike the secrets equivalent).
- CLI reference docs (`docs/public/reference/cli.mdx`) regenerated;
`docs/public/workflows/variables.mdx` gains a short section explaining
`{{ vars.NAME }}` interpolation and the variables-vs-secrets security
boundary.

### Sandbox lifecycle model (web)
- New `RunSandbox` OpenAPI shape splits the old flat object into `kind`
(planned/initializing/ready/failed) + `plan` + optional `instance` +
optional `failure`.
- `apps/fabro-web/app/lib/run-sandbox-lifecycle.ts` centralises
lifecycle helpers (`sandboxLifecycleKind`, `sandboxInstance`,
`sandboxRuntime`, `sandboxIsReady`, `sandboxTabVisible`,
`SANDBOX_LIFECYCLE_DISPLAY`).
- Run summary panel and sandbox route now show lifecycle state
(Initializing / Failed with causes / Not created) before or instead of
the fully-loaded `SandboxDetails`.
- The sandbox details query is skipped entirely until `sandboxIsReady`
returns true, preventing unnecessary 404 fetches for planned/failed
sandboxes.
- `runHasSandbox` in `tabs-shell.tsx` delegates to `sandboxTabVisible`,
hiding the Sandbox tab for `planned` state and showing it for
`initializing`/`ready`/`failed`.
- Legacy flat sandbox shape (no `kind`) is handled via
backwards-compatible shims in the new helpers.

### `fabro-devcontainer` removal
- The `fabro-devcontainer` crate has been removed from `Cargo.lock` and
all dependent crates.
- References to devcontainer in internal plans, docs, changelog entries,
and event schemas have been cleaned up or reworded to reflect that the
feature is no longer present.

### Plan summary
- **Unit 1:** `fabro-client` variable wrappers
- **Unit 2:** CLI args, dispatch, and `commands/variable/mod.rs`
- **Unit 3:** `list`, `get`, `set`, `rm` behavior modules
- **Unit 4:** Test harness helpers and integration tests
- **Unit 5:** Regenerated CLI docs + `variables.mdx` update


### Fabro Details

<details>
<summary>Ran 8 stages in 58m 50s for $23.81</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 9s | – | 0 |
| preflight_lint | 2m 21s | – | 0 |
| implement | 31m 34s | $18.44 | 0 |
| simplify_opus | 8m 48s | $2.60 | 0 |
| simplify_gpt | 4m 1s | $2.77 | 0 |
| verify | 9m 17s | – | 0 |
| **Total** | **58m 50s** | **$23.81** | **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>
2026-05-27 13:57:25 -04:00
Bryan Helmkamp
70b9e9a1ab
docs: sync product docs with runtime changes 2026-05-26 21:55:17 -04:00
fabro-sh-0530[bot]
e8f0aceee8
refactor: rationalize server secret scopes (vault-only for optional int… (#401)
## Summary

Separates Fabro server secrets into two explicit scopes: **bootstrap**
secrets that come from process env or `server.env`, and **optional
integration** secrets that come exclusively from the vault. This makes
secret resolution simple and predictable, and removes all `process env →
server.env` fallback paths for optional integrations such as GitHub App,
Slack, Daytona, Brave Search, and LLM provider keys.

## What changed

**New `ToolSecrets` struct in `fabro-agent`** — Brave Search API key is
now passed explicitly through `SessionOptions.tool_secrets` rather than
read from process env inside the tool. The standalone CLI reads the key
at the CLI boundary (with an explicit
`#[expect(clippy::disallowed_methods)]` annotation); the server will
read it from the vault. The error message changes from
`"BRAVE_SEARCH_API_KEY environment variable is not set"` to
`"BRAVE_SEARCH_API_KEY is not configured"`.

**`VaultCredentialSource::vault_only` constructor in `fabro-auth`** —
Adds a constructor that passes `|_| None` as the env lookup, ensuring
the server LLM credential source never resolves provider keys from
process env.

**GitHub App secrets move to vault in install flows** — Both the CLI
`fabro install github` path and the browser install finish handler now
write `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_CLIENT_SECRET`, and
`GITHUB_APP_WEBHOOK_SECRET` to the vault instead of `server.env`.
Switching strategies removes stale secrets from the other strategy's
storage location. The `vault_set` field type changes from `Vec<(String,
String)>` to `Vec<VaultSecretWrite>` to carry per-secret type metadata
(file vs. token).

**`fabro-vault` gains a `fabro-static` dependency** — Needed so the
vault crate can reference canonical env-var names from the shared
registry without a cycle.

**`GH_TOKEN` fallback removed** — `GITHUB_TOKEN` is now read from the
vault only; the changelog and `server-configuration.mdx` note drops
mention of `GH_TOKEN` as an accepted fallback.

**Version bump** — Workspace crates promoted from `0.244.0-nightly.0` to
`0.244.0`.

**Docs** — Internal strategy doc, public admin docs (Docker, Railway,
server-configuration, security, troubleshooting), and integration docs
(GitHub, Slack, Daytona, Brave Search, LiteLLM, tools reference, models)
all updated to reflect vault-only optional secrets and direct users to
`fabro secret set` rather than process env or `server.env`.

### Plan Summary

- **Task 1** (secret registry) — not yet present in this diff;
classification lives in the places that consume it.
- **Task 3–6** (vault-only lookups for GitHub, Slack, Daytona, LLM) —
implemented via `vault_only` constructor, `tool_secrets` threading, and
install-path changes.
- **Task 7** (Brave Search explicit injection) — `ToolSecrets`,
`register_core_tools` wiring, CLI boundary read.
- **Task 8** (install persistence) — GitHub App secrets written to
vault; token strategy writes `GITHUB_TOKEN` to vault and clears app
vault keys; app strategy clears `GITHUB_TOKEN` vault key.
- **Task 9** (docs) — all public and internal docs updated.


### Fabro Details

<details>
<summary>Ran 0 stages in 155m 26s for $60.85</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **155m 26s** | **$60.85** | **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>
2026-05-25 17:26:01 -04:00
Bryan Helmkamp
2c851d5a41
Merge remote-tracking branch 'origin/main' 2026-05-25 15:50:43 -04:00
fabro-sh-0530[bot]
b15d8b8476
feat: Add approve/deny run controls to MCP and CLI (#400)
## Summary

Exposes the existing `POST /api/v1/runs/{id}/approve` and `POST
/api/v1/runs/{id}/deny` REST endpoints through the `fabro_run_interact`
MCP tool and two new top-level CLI commands (`fabro approve`, `fabro
deny`). Workflow agents are explicitly blocked from using these actions
— approval remains a human/user operation.

## What changed

**Client & tool backend** (`fabro-client`, `fabro-tool`): Added
`approve_run` and `deny_run` to `Client` and the `FabroToolBackend`
trait, implemented in `ClientBackend`. `deny_run` passes a
`DenyRunRequest` body; absent, blank, or whitespace-only reasons are
normalised to `None`.

**`fabro_run_interact` MCP tool**: Added `Approve` and `Deny` variants
to `RunInteractAction` / `ValidatedInteractAction`, and an optional
`reason` parameter (only valid for `deny`; validated and trimmed on
input). Both actions return `{ "summary": … }` using the existing shape.
The tool description is updated to list the new actions.

**Workflow-agent guard** (`fabro-workflow`): Before dispatching
`fabro_run_interact`, the handler checks
`validated.action.requires_user()`. If the action is `approve` or
`deny`, it returns an immediate `ToolError` without ever reaching the
backend, keeping the guard explicit and independent of server auth.

**CLI** (`fabro-cli`): Extracted the archive/unarchive batch loop into a
shared `run_resolved_run_batch` helper in `commands/runs/mod.rs`, then
implemented `approval.rs` using the same helper. Both commands follow
the same batch contract as archive: attempt all runs, collect per-run
errors, exit non-zero if any fail, and emit `{ "approved"/"denied": […],
"errors": […] }` in JSON mode.

**Server auth regression** (`fabro-server`): Extended
`run_tools_worker_cannot_call_user_only_non_mcp_routes` to cover `POST
/runs/{id}/deny` alongside the existing `approve` and `timeline` checks.

**Docs** (`mcp.mdx`, `cli.mdx`): Updated the `fabro_run_interact` table
entry and added approve/deny examples, plus reference sections for the
two new CLI commands.

### Plan Summary

- Add `approve_run` / `deny_run` to `Client` and `FabroToolBackend`
- Extend `fabro_run_interact` with `approve`, `deny`, and optional
`reason`
- Block workflow-agent self-approval with an early `ToolError`
- Refactor archive batch loop into shared `run_resolved_run_batch`
helper
- Add `fabro approve` and `fabro deny` CLI commands reusing that helper
- Add integration tests for CLI commands, MCP tool, and server auth
guard


### Fabro Details

<details>
<summary>Ran 9 stages in 63m 57s for $42.33</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 17s | – | 0 |
| implement | 28m 16s | $32.57 | 0 |
| simplify_opus | 10m 59s | $4.24 | 0 |
| simplify_gpt | 6m 13s | $3.96 | 0 |
| verify | 10m 51s | – | 0 |
| fixup | 2m 29s | $1.55 | 0 |
| **Total** | **63m 57s** | **$42.33** | **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>
2026-05-25 15:49:57 -04:00
Bryan Helmkamp
879969cf54
docs: add child runs guide
Document child-run orchestration as a first-class execution concept and link the related MCP, UI, and API surfaces back to it.
2026-05-25 15:43:53 -04:00
Bryan Helmkamp
2a2b410802
feat: remove demo-mode toggle button and endpoint
Demo mode remains available via the X-Fabro-Demo header or the
fabro-demo=1 cookie set manually in browser devtools, but the UI
button and the POST /api/v1/demo/toggle endpoint are gone. The
fixture machinery and the auth/me demoMode flag (used by the SPA to
render Automations and the /start landing) are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:35:37 -04:00
Bryan Helmkamp
47d8d6365b
docs: refresh generated CLI reference 2026-05-25 11:13:01 -04:00
Bryan Helmkamp
3ebea2413b
docs: clarify structured output fallbacks 2026-05-25 10:56:53 -04:00
Bryan Helmkamp
245052db38
feat(cli): allow rendering invalid graphs
Keep graph validation diagnostics visible, but let users opt into rendering DOT workflows that fail semantic validation with --allow-invalid.
2026-05-25 10:19:32 -04:00
Bryan Helmkamp
4ad9827baf
Remove retired OpenAI catalog models 2026-05-24 11:40:35 -04:00
fabro-sh-0530[bot]
3fb4b5bc1b
Add output_schema validation with same-context repair for agent and pro… (#374)
## Summary

Adds `output_schema` and `output_retries` node attributes that validate
structured LLM output and perform corrective repair turns inside the
same conversation context before failing the node. Also adds sortable
columns (Repo, Title, Workflow, Changes) to the runs list view and hides
the pager when the result set is small.

### Plan Summary

- **Task 1**: `Node::output_schema()` / `Node::output_retries()`
accessors in `fabro-types`, with `@`-prefix file-reference support in
static validation and file inlining.
- **Task 2**: New `handler/structured_output.rs` module —
`OutputSchemaKind` (Routing / JsonSchema), balanced JSON scanning,
validation, repair-message generation, `apply_validated_output`, and
`exhausted_failure_outcome`.
- **Task 3**: `extract_status_fields` moved to `structured_output.rs`;
agent routing fallback chain (response → `status.json` → last file
touched) preserved and delegated to `validate_agent_output_sources`.
- **Task 4/5**: `one_shot` (prompt) and `run` (agent) both loop over LLM
calls, appending the prior assistant response and a corrective user turn
on validation failure, up to `output_retries` times.
- **Task 6**: ACP backend rejects `output_schema` immediately with a
clear error before launching any process.
- **Task 7**: `outputs.mdx` and `dot-language.mdx` updated with
attribute docs, repair semantics, and `output.{node_id}` context key.

## What changed and why

```mermaid
TB
  graph

  A[Node attrs\noutput_schema / output_retries] --> B[structured_output.rs\nparse / validate / repair]
  B --> C{OutputSchemaKind}
  C -->|Routing| D[validate routing fields\n→ outcome routing]
  C -->|JsonSchema| E[jsonschema validator\n→ context_updates.output.node_id]
  B --> F[exhausted_failure_outcome\nterminal, non-retryable]

  G[prompt handler\none_shot loop] --> B
  H[agent handler\nrun loop + session.process_input] --> B
  I[ACP backend] -->|output_schema present| J[Validation error\nno process launched]
```

**`output_schema="routing"`** tightens existing loose routing
extraction: malformed fields now fail validation and trigger a repair
turn rather than being silently ignored. The fallback priority (response
text → `status.json` → last file touched) is preserved but only for the
`NoJsonObject`/`NoRelevantJsonObject` error kinds that allow it.

**Custom schemas** (`@path` inlined to JSON Schema) validate the last
JSON object in the response against a precompiled
`jsonschema::Validator`. On success, the parsed value is stored at
`output.{node_id}` in `context_updates` for downstream nodes.

**Repair loop** — prompt nodes keep the prior assistant response in the
message list and append a corrective user message; agent API sessions
call `session.process_input` on the live session. Both paths aggregate
token usage across all turns. Exhausting `output_retries` returns a
terminal `OutputSchemaValidation` error (non-retryable, deterministic
failure category) that does not consume `max_retries`.

**ACP guardrail** rejects `output_schema` before spawning any
subprocess, with a clear `"output_schema is not supported with
backend=\"acp\" in this release"` message.

The `one_shot` refactor also extracted `complete_one_shot_request` and
`OneShotCompletion` to separate fallback-chain logic from the repair
loop, removing duplication.


### Fabro Details

<details>
<summary>Ran 9 stages in 74m 9s for $31.92</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 16s | – | 0 |
| preflight_lint | 2m 28s | – | 0 |
| implement | 24m 59s | $17.96 | 0 |
| simplify_opus | 16m 36s | $9.95 | 0 |
| simplify_gpt | 3m 20s | $1.75 | 0 |
| verify | 6m 29s | – | 0 |
| fixup | 17m 13s | $2.26 | 0 |
| **Total** | **74m 9s** | **$31.92** | **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 && ! 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: Fabro <fabro@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-23 19:45:16 -04:00
fabro-sh-0530[bot]
f73f2a53f3
Replace queued with pending/runnable and add approval flow (web + API s… (#371)
## Summary

Replaces the single `queued` pre-execution state with explicit `pending`
and `runnable` states, and wires approve/deny actions for
parent-generated child runs that require human approval before they can
execute. This diff covers the web UI and OpenAPI spec layers of that
change.

## What changed

**Run status model**
- `queued` is removed from all TypeScript types, display maps, column
routing, and tests.
- `pending` (awaiting approval) and `runnable` (eligible for the
scheduler) replace it as distinct board columns and `RunStatus` variants
with their own labels and colors (`runnable` gets cyan; `pending` stays
muted).

**Approval actions**
- New `approveRun` / `denyRun` API calls in `run-actions.ts` invoke the
new `POST /runs/{id}/approve` and `POST /runs/{id}/deny` endpoints.
- `canApprove` predicate requires both `status.kind === "pending"` and
`lifecycle.approval?.state === "pending"` — a run whose status is
pending but has no approval record does not expose the action.
- `useApproveRun` / `useDenyRun` mutations in `mutations.ts` follow the
same pattern as `useCancelRun`.
- `ActionsMenu` in `run-detail.tsx` gains Approve (lifecycle group) and
Deny (destructive group) menu items.

**Board and event plumbing**
- `columnForStatus` now routes `pending → pending column` and `runnable
→ runnable column`; `submitted` stays in the pending column.
- `BOARD_STATUS_EVENTS` and `RUN_SUMMARY_EVENTS` replace `run.queued`
with `run.start_requested`, `run.pending`, `run.approved`, `run.denied`,
and `run.runnable`.
- The `pending` column is hidden when empty (same behaviour the old
`queued` column had).

**Waterfall phases (`run-phases.ts`)**
- `queued` phase is removed; `pending` and `runnable` phases are added
in order.
- The submitted phase closes at `run.start_requested` rather than
`run.queued`.
- Each phase derives its timestamps from its own event rather than a
single `firstTs` lookup, making multi-phase pre-execution timelines
accurate.

**OpenAPI spec**
- `POST /api/v1/runs/{id}/approve` and `POST /api/v1/runs/{id}/deny`
endpoints added with 200/404/409 responses.
- `startRun` description updated to describe the pending/runnable
branching behaviour.
- `cancelRun` description updated to reference `pending`/`runnable`
instead of `queued`.

### Plan Summary

- **Task 3** (OpenAPI schema additions for approve/deny endpoints) —
complete in this diff.
- **Task 6** (Web UI surfaces: board columns, run-detail actions,
waterfall phases, event subscriptions) — complete in this diff.
- **Task 7** (doc cleanup: references to `queued` replaced in plans,
brainstorms, and QA docs) — complete in this diff.


### Fabro Details

<details>
<summary>Ran 9 stages in 127m 37s for $104.98</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 2m 15s | – | 0 |
| preflight_lint | 2m 29s | – | 0 |
| implement | 92m 10s | $91.53 | 0 |
| simplify_opus | 18m 35s | $10.65 | 0 |
| simplify_gpt | 7m 36s | $2.81 | 0 |
| verify | 3m 42s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **127m 37s** | **$104.98** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 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 clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]

    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 -> fmt   [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: fabro <fabro@anthropic.com>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-23 15:34:33 -04:00
fabro-sh-0530[bot]
7f84ac5e3f
Replace run-scoped sandbox config with named environments (#360)
## Summary

Replaces the `[run.sandbox]` configuration surface with a named,
provider-explicit environment catalog. Runs now select an environment by
slug (`[run.environment] id = "..."`) rather than configuring a sandbox
inline. Fabro resolves the catalog through normal settings precedence,
applies sparse run-level overrides, and creates a concrete sandbox from
the resolved environment.

This is a clean break — no `[run.sandbox]` compatibility layer.

### Plan Summary

- **New config shape:** Top-level `[environments.<slug>]` catalog valid
in `settings.toml`, `.fabro/project.toml`, and `workflow.toml`. Runs
reference a slug via `[run.environment] id = "..."` with optional sparse
overrides under `[run.environment.*]`.
- **Unified environment fields:** `provider`, `image` (ref +
dockerfile), `resources` (cpu/memory/disk), `network` (mode + allow
CIDRs), `lifecycle` (preserve/stop_on_terminal/auto_stop), `labels`,
`volumes`, `env` — replacing the previous split between `[run.sandbox]`,
`[run.sandbox.docker]`, `[run.sandbox.daytona]`, and
`[run.sandbox.daytona.snapshot]`.
- **OpenAPI schema update:** `RunSandboxSettings`, `DockerSettings`,
`DaytonaSettings`, and `DaytonaNetworkLayer` replaced with
`RunEnvironmentSettings`, `EnvironmentSettings`, `EnvironmentProvider`,
`EnvironmentImageSettings`, `EnvironmentResourcesSettings`,
`EnvironmentNetworkSettings`, `EnvironmentLifecycleSettings`, and
`EnvironmentVolumeSettings`.
- **CLI flag rename:** `--sandbox <provider>` → `--environment <slug>`
on `run`, `create`, `preflight`, and `server start/restart`.
- **Provider capability model:** Hard errors for security properties a
provider cannot enforce (local with blocked/CIDR networking; docker with
CIDR allow-lists). Warnings for unsupported resource limits, volumes,
labels, auto-stop, and Docker Dockerfiles.
- **Docs and internal code updated** throughout: `.fabro/project.toml`,
workflow configs, all public docs, CLI args, manifest builders, and the
runner's GitHub credentials check.

### Provider mapping

| Environment field | Local | Docker | Daytona |
|---|---|---|---|
| `image.ref` | Ignored | Docker image | Snapshot name |
| `image.dockerfile` | Ignored | Warning; ignored | Snapshot Dockerfile
(requires `image.ref`) |
| `resources.cpu/memory/disk` | Warning; ignored | cpu_quota / memory
limit / warning | Snapshot sizing |
| `network.mode = block` | **Error** | `network_mode = none` | Daytona
block |
| `network.mode = cidr_allow_list` | **Error** | **Error** | Daytona
CIDR allow-list |
| `labels` | Warning; ignored | Warning; ignored | Daytona labels |
| `volumes` | Warning; ignored | Warning; ignored | Daytona volume
mounts |
| `lifecycle.auto_stop` | Warning; ignored | Warning; ignored | Daytona
auto-stop interval |
| `env` | Process env overlay | Container env | Sandbox env |


### Fabro Details

<details>
<summary>Ran 11 stages in 217m 39s for $129.86</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 4m 7s | – | 0 |
| preflight_lint | 4m 9s | – | 0 |
| fix_lints | 3m 46s | $1.06 | 0 |
| implement | 76m 6s | $57.39 | 0 |
| simplify_opus | 71m 50s | $38.17 | 0 |
| simplify_gpt | 8m 27s | $2.24 | 0 |
| verify | 6m 10s | – | 0 |
| fixup | 42m 1s | $31.00 | 0 |
| fmt | 3s | – | 0 |
| **Total** | **217m 39s** | **$129.86** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 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 clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]

    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 -> fmt   [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Bryan Helmkamp <bhelmkamp@users.noreply.github.com>
2026-05-23 13:03:21 -04:00
Bryan Helmkamp
eb4891b1b0
refactor(agent): simplify reviewed changes
Use raw sandbox reads for memory and skills, keep line-numbered reads focused on display, and share retry-delay handling across agent and LLM code.

Trim task tool descriptions, bound multi-file read concurrency, restore Docker's text read path, and add the reviewed implementation plan docs.
2026-05-22 21:51:45 -04:00
Bryan Helmkamp
4190e13a20
Collapsible run stage sidebar (#352)
## What

Makes the run-detail stage sidebar (shown on the Overview and Stages
tabs) collapsible with a slide animation.

- A toggle button slides the panel between full width (`w-56`) and an
icon-only rail (`w-12`), animating `width` over 300ms with the same
easing as the Ask Fabro panel.
- When collapsed, **stage status icons stay visible** — green check /
red X / spinning teal for running — so run progress is still scannable
at a glance. Workflow links (Graph Source, Run Logs, etc.) collapse to
icons too so they remain reachable.
- Labels and durations become `sr-only` with `title` tooltips for hover.
- The open/closed choice persists to `localStorage`
(`fabro:stage-sidebar-collapsed`), carrying across the Overview and
Stages tabs and reloads.

## Layout

- The collapse toggle is inline with the `STAGES` heading row (or
`WORKFLOW` when a run has no stages yet), so it doesn't push the stage
list down.
- The stage sidebar's top padding on the Stages tab was reduced (`pt-6`
→ `pt-3`) so the heading aligns with the adjacent content column and
sits closer to the tab nav.

## Notes

Self-contained in `StageSidebar` — `run-overview.tsx` and
`run-stages.tsx` render it inside flex layouts that already track its
width, so the slide works in both with no parent changes (aside from the
padding tweak).

Verified: `tsc` typecheck passes; `stage-sidebar` lib tests pass
(10/10).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (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>
2026-05-22 12:20:29 -04:00
fabro-sh-0530[bot]
f5f921aa3c
feat: add [run.agent] fabro_tools opt-in for worker run tools (#348)
## Summary

Workflow agents previously always received the `agent:run_tools` JWT
scope and had Fabro run tools registered unconditionally. This PR makes
Fabro run tool access an explicit per-run opt-in via `[run.agent]
fabro_tools = true`, defaulting to `false`.

## What changed

**Config layer** (`fabro-types`, `fabro-config`): `RunAgentSettings`
gains a `fabro_tools: bool` field (serialized with `#[serde(default)]`
for backward compatibility). The corresponding `RunAgentLayer` gets an
optional `fabro_tools: Option<bool>` that resolves to `false` when
absent. Layer merging follows the existing `Combine` macro semantics, so
a workflow-level `false` overrides a user-level `true`.

**Server** (`fabro-server`): `worker_command` gains an
`agent_fabro_tools_enabled: bool` parameter. The server reads
`run_state.spec.settings.run.agent.fabro_tools` from the stored run
before spawning `__run-worker`, then issues the worker JWT with either
`["run:worker"]` or `["run:worker", "agent:run_tools"]` accordingly.
`WorkerScopeSet::run_worker()` loses its `#[cfg(test)]` gate so it's
available in production paths.

**CLI worker** (`fabro-cli`): `FabroRunToolServices` construction is now
gated on `run_spec.settings.run.agent.fabro_tools` rather than being
unconditional. The resolved run spec already carries the setting, so no
env-var parsing is needed in the runner.

**Tests**: The single monolithic
`worker_command_always_sets_worker_token_env` test is replaced by two
focused tests — one confirming the default scope is `["run:worker"]`
only, and one confirming the opt-in scope includes `agent:run_tools`.
Shared assertion logic is extracted into
`assert_worker_command_passes_token_only_by_env` and
`worker_token_claims` helpers. Config resolver tests cover default,
explicit true/false, and layer-override behavior.

**Docs**: `[run.agent]` description and reference tables are updated; a
new `run-configuration.mdx` section explains the opt-in semantics before
the existing `[run.agent.mcps]` section.

### Plan Summary

- 1. Add `fabro_tools` to resolved and layered run config, with resolver
tests.
- 2. Gate worker JWT scope and pass setting from stored run state into
`worker_command`.
- 3. Gate CLI `FabroRunToolServices` construction on the resolved
setting.
- 4. Update docs generator sample and public reference/execution docs.
- 5. Full verification pass (nextest, fmt, clippy).


### Fabro Details

<details>
<summary>Ran 9 stages in 54m 4s for $15.21</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 4s | – | 0 |
| preflight_lint | 2m 18s | – | 0 |
| implement | 28m 5s | $10.58 | 0 |
| simplify_opus | 12m 4s | $2.86 | 0 |
| simplify_gpt | 5m 19s | $1.77 | 0 |
| verify | 3m 37s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **54m 4s** | **$15.21** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
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."]
    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="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 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 clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]

    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 -> fmt   [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: fabro-agent <agent@fabro.local>
2026-05-22 09:41:27 -04:00
Bryan Helmkamp
86b1fbef7f
feat(api): bind sessions to runs (#336)
## Summary

Ask Fabro sessions are now run-bound instead of standalone. Sessions are
created under their owning run, then accessed by flat session ID routes,
with durable state projected from the run event stream rather than a
separate session store.

## Changes

- Move session creation/listing to `POST/GET /api/v1/runs/{id}/sessions`
while keeping flat session reads, turns, interrupts, and event streams
under `/api/v1/sessions/{id}/...`.
- Add typed `run.session.*` events, ULID-backed session/turn IDs,
read-only default permissions, and a rebuildable SlateDB `session_id ->
run_id` index.
- Remove the old file-backed session store and wire the server, runtime,
Rust client, generated API crates, and TypeScript client around run
event projections.
- Replace the old top-level CLI session command with `fabro run ask` for
chatting with a run.
- Regenerate the TypeScript API client; this also catches up existing
generated models for Pair/run event detail schemas already present in
the OpenAPI spec.

## Validation

- `cargo build -p fabro-api -p fabro-client -p fabro-server -p
fabro-cli`
- `cargo nextest run -p fabro-server --features test-support -E
'test(run_bound_session_is_created_as_run_event_and_resolves_by_flat_id)
| test(sessions_are_listed_only_under_their_owning_run)'`
- `cargo nextest run -p fabro-store
projection_rebuilds_runtime_context_from_run_events`
- `cargo +nightly-2026-04-14 clippy -p fabro-api -p fabro-client -p
fabro-store -p fabro-server -p fabro-cli --all-targets -- -D warnings`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cd lib/packages/fabro-api-client && bun run typecheck && cd
../../../apps/fabro-web && bun run typecheck`
- `git diff --check`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
Generated with GPT-5 via [Codex](https://openai.com/codex)
2026-05-21 10:53:41 -04:00
Bryan Helmkamp
c28a102af8
docs: sync public docs to recent changes
Document Daytona Dockerfile path refs, static template includes, Slack review context, skipped LLM setup, and template validation behavior.
2026-05-18 14:42:54 -04:00
Bryan Helmkamp
29b7cc0de0
feat(workflow): enforce strict api/acp backends (#307)
## Summary

This PR makes agent execution a strict two-backend contract: API-backed
stages use Fabro-owned model/provider auth, while ACP-backed stages
launch a user-supplied stdio process that owns its own auth and tools.
That removes the legacy CLI backend and prevents ACP execution from
accidentally resolving or forwarding provider credentials.

## Changes

- Replaces the old `api`/`cli`/`acp` backend model with `AgentBackend {
api, acp }`, with `backend=\"cli\"` rejected and migrated toward
explicit ACP process configuration.
- Splits ACP process configuration into `acp.command` for shell command
strings and `acp.config` for JSON stdio configs, while rejecting legacy
`acp_command`.
- Restricts ACP to `agent` nodes and rejects API-only attributes such as
`model`, `provider`, `reasoning_effort`, `max_tokens`, and `speed` on
ACP nodes.
- Deletes the workflow CLI runtime, CLI credential resolver surface, CLI
live smoke tests, and `agent.cli.*` event handling.
- Updates ACP events and projections to report process identity
(`command`, optional `config_name`) rather than provider/model metadata.
- Updates import/stylesheet propagation, CLI workflow smoke coverage,
server steering tests, and web model extraction for the new
event/backend contract.

## Validation

- `cargo check -p fabro-auth -p fabro-acp -p fabro-workflow -p fabro-cli
--all-targets`
- `cargo nextest run -p fabro-auth -p fabro-acp -p fabro-validate -p
fabro-store -p fabro-workflow --lib`
- `cargo nextest run -p fabro-acp`
- `cargo nextest run -p fabro-cli --test it
workflow::acp::acp_backend_workflow`
- `cargo nextest run -p fabro-workflow --test it
codergen_without_backend_simulated`
- `cargo nextest run -p fabro-workflow --test it
import_e2e_through_engine`
- `cargo nextest run -p fabro-workflow --test it stylesheet_application`
- `cargo nextest run -p fabro-server
steer_with_active_acp_stage_returns_non_steerable_conflict`
- `cargo nextest run -p fabro-server
active_acp_stage_marker_clears_on_terminal_paths`
- `cargo nextest run -p fabro-types
agent_backend_accepts_only_api_and_acp`
- `cd apps/fabro-web && bun test app/routes/run-stages.test.ts`
- `cd apps/fabro-web && bun run typecheck`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)

---------

Co-authored-by: Peter Bell <4843+PeterBell@users.noreply.github.com>
2026-05-18 13:20:56 -04:00
Bryan Helmkamp
cd74013d06
refactor(auth): split credential sources and vault schemas (#306)
## Summary

Compared with `origin/main`, this PR splits credential storage and
credential references into explicit types. Vault secrets now distinguish
`token`, `oauth`, and `file` payloads, while runtime/model configuration
points to credentials through explicit `env:<NAME>` and `vault:<NAME>`
source refs.

## Changes

- Replaces the old `environment`/`credential` secret schema vocabulary
with `token`/`oauth`/`file` across OpenAPI, Rust API tests, generated
TypeScript models, CLI/docs references, and the changelog.
- Updates auth resolution, refresh, provider strategies, workflow LLM
handling, server diagnostics, install flows, run manifests, and secret
handlers to consume typed vault entries and explicit credential sources.
- Updates provider catalog TOMLs and config parsing so provider auth and
extra headers use `vault` refs instead of ambiguous `credential` refs.
- Updates CLI install/login/run/secret paths and integration tests to
write and read the new credential shapes.
- Removes the temporary legacy vault migration and empty-vault fallback,
then centralizes provider vault secret-name lookup and Codex API
credential shaping.

## Verification

- `cargo +nightly-2026-04-14 fmt --all`
- `cargo +nightly-2026-04-14 clippy -p fabro-auth -p fabro-model -p
fabro-config -p fabro-vault -p fabro-server -p fabro-cli --all-targets
-- -D warnings`
- `ulimit -n 4096 && cargo nextest run -p fabro-auth -p fabro-model -p
fabro-config -p fabro-vault -p fabro-server -p fabro-cli` (`1938`
passed, `35` skipped)
2026-05-18 11:07:42 -04:00
Bryan Helmkamp
302e2445b4
refactor(model): move provider facts into catalog (#298)
## Summary

Moves provider-specific facts out of `AdapterKind` metadata and into
provider catalog data, leaving adapters responsible for runtime protocol
behavior. This makes providers that share an adapter mostly TOML-driven
while still surfacing adapter construction failures during readiness
checks.

## What Changed

- Provider TOML now owns auth mode, API-key/header policy, billing
policy, agent profile, base URLs/env overrides, extra headers, and probe
markers.
- Auth, install, config, diagnostics, and server flows resolve provider
credentials from catalog auth config, including API-key, header-only,
and no-auth providers.
- LLM client registration now reports adapter construction failures,
validates final adapter requests before HTTP dispatch, and preserves
custom primary auth headers.
- Billing and docs now use provider-owned billing policy instead of
adapter metadata, and the old adapter metadata surface is removed.

## Reviewer Notes

OpenAI-compatible `base_url` validation now happens during
adapter/client registration rather than catalog build. That keeps
catalog parsing adapter-agnostic while still letting readiness and model
listing reflect providers that cannot register.

## Verification

- `cargo check -p fabro-model -p fabro-auth -p fabro-llm -p fabro-server
-p fabro-cli`
- `cargo nextest run -p fabro-llm -- adapter_registry`
- `cargo nextest run -p fabro-model -- catalog`
- `cargo nextest run -p fabro-auth -- api_key`
- `cargo nextest run -p fabro-server -- install`
- `cargo +nightly-2026-04-14 fmt --check --all`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:59:08 -04:00
Bryan Helmkamp
b9497517a5
feat(llm): support agent profile overrides (#291)
## Summary

Adds catalog-level `agent_profile` overrides so custom providers and
individual models can choose Anthropic, OpenAI, or Gemini agent behavior
independently from their adapter default. The effective precedence is
model override, then provider override, then adapter metadata.

## What Changed

- Added typed provider/model `agent_profile` settings in `fabro-config`
and `fabro-model`, with serde/strum support for `anthropic`, `openai`,
and `gemini`.
- Centralized effective profile resolution in the catalog, including
provider alias canonicalization and a guard against unrelated model
overrides leaking across providers.
- Updated run startup, API sessions, CLI/ACP backends, prompt
project-memory discovery, and standalone agent startup to use the
resolved catalog profile.
- Documented provider-level and model-level `agent_profile`
configuration in the public model and user configuration docs.

No OpenAPI or model-list response shape changes are included.

## Validation

- `cargo nextest run -p fabro-model -p fabro-config -p fabro-workflow -p
fabro-agent` passed: 1826 passed, 125 skipped.
- `cargo +nightly-2026-04-14 fmt --check --all` passed.
- `cargo +nightly-2026-04-14 clippy -p fabro-model -p fabro-config -p
fabro-workflow -p fabro-agent --all-targets -- -D warnings` passed.
- `git diff --check` passed.
- `cargo nextest list -p fabro-dev` confirmed there is no docs-options
reference test target to run.

## Post-Deploy Monitoring & Validation

Watch workflow and agent-session logs for provider/model resolution
errors, unexpected project-memory file selection, or CLI/ACP launch
command mismatches on custom catalog providers. Healthy signal: custom
provider/model runs start normally and use the intended profile-specific
behavior. Failure trigger: repeated `Provider ... is not configured`
errors, missing expected project memory, or profile-specific agent
startup failures after configuring `agent_profile`. Mitigation is to
remove the override from config or revert this PR. Validation window:
first deploy cycle after merge; owner: release/on-call engineer.

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
2026-05-16 17:40:59 -04:00
Bryan Helmkamp
56c7f627c4
feat(session): add server-backed agent sessions (#278)
## Summary

Adds the first server-backed Fabro agent session slice: persistent
session records, durable turn/event storage, HTTP session APIs, SSE turn
streaming, generated clients, and a new `fabro session -p <prompt>` CLI
path.

## What Changed

- Adds shared session IDs, records, statuses, event envelopes, and
message DTOs in `fabro-types`, with OpenAPI replacements in `fabro-api`.
- Renames the agent runtime transcript item from `Turn` to `Message` and
adds conversion between runtime history and persisted `SessionMessage`
records.
- Introduces a file-backed `SessionStore` for session metadata, turns,
full transcripts, and append-only events under local storage.
- Wires server session routes for create/list/read/update/delete, turn
submission, event replay, interrupt requests, and session-scoped tools.
- Implements streamed turn execution with durable events persisted
before SSE broadcast, active-turn conflict handling, local same-machine
`working_dir` validation, and noninteractive permission denials.
- Adds `fabro-client` helpers and the `fabro session -p` command, plus
regenerated TypeScript API client files.

## Notes

V1 intentionally keeps session execution local to same-machine server
targets. Remote clone-backed session sandboxes, interactive REPL/TUI
behavior, warm session pooling, and real tool discovery for
`/sessions/{id}/tools` remain follow-up work.

## Verification

- `cargo build --workspace`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo test -p fabro-store session_store_contract_tests --lib`
- `cargo test -p fabro-agent
history::tests::session_message_roundtrip_preserves_runtime_history
--lib`
- `cargo test -p fabro-server 'session_' --lib`
- `cargo test -p fabro-server --features test-support --test it
openapi_conformance -- --nocapture`
- `cargo test -p fabro-cli --test it cmd::session:: -- --nocapture`
- `cd lib/packages/fabro-api-client && bun run typecheck`
- `git diff --check`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
2026-05-16 17:25:05 -04:00
Bryan Helmkamp
0f1cf4da5c
feat(cli): wire run parent commands (#288)
## Summary
Add CLI support for run parent relationships now that the server API can
store them. This lets users create child runs, filter children, inspect
parent metadata, and link or unlink parents without dropping to raw API
calls.

## What Changed
- Added top-level `fabro parent link` and `fabro parent unlink` commands
with selector resolution, text output, and JSON summaries.
- Added `--parent` to `fabro run`, `fabro create`, and `fabro ps`;
create/run send `parent_id` in manifests and `ps` uses server-side
parent filtering.
- Surfaced `parent_id` in `ps --json` and `inspect`, with a conditional
`PARENT` column for unfiltered tables.
- Extended `fabro-client` parent-link APIs and
`list_store_runs(parent_id)`.

## Test Plan
- `cargo nextest run -p fabro-cli`
- `cargo nextest run -p fabro-client`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy -p fabro-cli -p fabro-client
--all-targets -- -D warnings`
- `cargo insta pending-snapshots`
- `git diff --check`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
Generated with GPT-5 via [Codex](https://openai.com/codex)
2026-05-16 15:15:28 -04:00
Bryan Helmkamp
87950295bd
refactor(llm): split provider identity from adapters (#280)
## Summary

This PR separates provider identity from adapter behavior across the LLM
stack. Provider IDs now represent catalog rows and provider metadata,
while adapter/profile routing owns protocol behavior for Anthropic,
OpenAI, Gemini, and OpenAI-compatible providers.

## Changes

- Replace the shared `fabro_model::Provider` enum with open-ended
`ProviderId` catalog identity and typed `AdapterKind` metadata.
- Route auth, CLI, ACP, workflow, memory selection, profile
construction, and LLM client registration through catalog provider rows
instead of provider-ID fallbacks.
- Move API-key URL/header/env metadata into provider catalog/auth flows
and require configured provider rows for credential-backed clients.
- Simplify billing to `algorithm`-tagged OpenAI, Anthropic, and Gemini
shapes; OpenAI-compatible adapters bill through the OpenAI algorithm.
- Remove greenfield compatibility paths for old provider aliases, legacy
provider-tagged billing JSON, and the `openai_compatible`
pseudo-provider env fallback.
- Update fixtures and tests to exercise catalog-driven
Kimi/Zai/Minimax/Inception/custom OpenAI-compatible routing.

## Validation

- `cargo test --no-run -p fabro-model -p fabro-auth -p fabro-agent -p
fabro-workflow -p fabro-server -p fabro-llm -p fabro-api -p fabro-cli -p
fabro-store -p fabro-static`
- `cargo nextest run -p fabro-model -p fabro-auth -p fabro-agent -p
fabro-workflow -p fabro-server --no-fail-fast`
- `cargo nextest run -p fabro-llm -p fabro-api -p fabro-cli -p
fabro-store -p fabro-static --no-fail-fast`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `git diff --check`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
2026-05-16 13:13:41 -04:00
Bryan Helmkamp
d09e6cde33
feat(pr): support GitHub pull request associations (#270)
## Summary

Adds event-sourced pull request association management for runs while
preserving Fabro-created PR creation. A run can now store a current
GitHub PR association, replace it by linking another GitHub PR URL, and
remove it through an unlink event.

## What Changed

- Added `pull_request.linked` and `pull_request.unlinked` events,
projection replay support, and optional PR metadata fields in shared
pull request records.
- Added API, server, and client support for `PUT
/runs/{id}/pull_request` and `DELETE /runs/{id}/pull_request`; linking
accepts GitHub PR URLs, infers owner/repo/number, and captures live
GitHub title and branch metadata when available.
- Added `fabro pr link` and `fabro pr unlink`, updated `fabro pr view`,
and kept create/merge/close behavior guarded to GitHub PRs with usable
coordinates.
- Updated web UI rendering and internal event docs so stored PR links
display cleanly when live GitHub details are unavailable.

## Testing

- `cargo +nightly-2026-04-14 fmt --check --all`
- `git diff --check`
- `cargo build -p fabro-api`
- `cargo nextest run -p fabro-types -p fabro-store -p fabro-server -p
fabro-cli`
- `bun run typecheck` in `lib/packages/fabro-api-client`
- `bun run typecheck` in `apps/fabro-web`
- `bun test` in `apps/fabro-web`

Refs https://github.com/fabro-sh/fabro/issues/235

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)

---------

Co-authored-by: Haroldo Olivieri <6575718+haroldolivieri@users.noreply.github.com>
2026-05-16 12:47:27 -04:00