Commit graph

3504 commits

Author SHA1 Message Date
Bryan Helmkamp
93452001a1
feat(api): require typed PermissionLevel on session create (#300)
## Summary

- `POST /api/v1/sessions` now requires `permissions` as a typed enum
(`read-only` | `read-write` | `full`) instead of accepting an optional
plain string.
- Removes the silent fallback at `sessions.rs:906-911` where unknown
values (e.g. `"readonly"`) were coerced to `read-write` — a real
security footgun: a client trying to lock the agent down would get write
access instead.
- Invalid or missing values are now rejected by axum's `Json` extractor
with `422 Unprocessable Entity`.

## Approach

- New `PermissionLevel` OpenAPI schema (`type: string, enum: [...]`).
- Moves `PermissionLevel` from `fabro_agent::cli` to
`fabro_types::session` so `fabro-api` can `with_replacement` it without
a circular dep. `fabro_agent::cli::PermissionLevel` remains as a `pub
use` re-export so existing call sites keep working.
- `SessionRecord.permissions` becomes required and non-nullable for
coherence — every created session has a concrete level.
- `build_tool_approval` in the server takes `PermissionLevel` directly;
the string-match fallback is deleted.
- CLI's `session_permissions` returns a concrete `PermissionLevel`
(defaults to `read-write` when neither flag nor settings provide one)
and is sent explicitly on every request.

## Scope notes

Confirmed out of scope and not addressed here:
- Mid-session model/permission switching
- Interactive tool approval / HITL

## Breaking change

The `permissions` field is now required on `CreateSessionRequest` and
non-nullable on `SessionRecord`. Existing on-disk session records
persisted with `"permissions": null` will fail to deserialize.
Acceptable per project policy (no migration); local dev users may need
to clear `~/.fabro/storage/sessions/` once.

## Test plan

- [x] `cargo build --workspace`
- [x] `cargo nextest run -p fabro-api` — 125/125 (includes new
`permission_level_round_trip` parity tests)
- [x] `cargo nextest run -p fabro-server` — 554/554 (includes new 422
tests for missing + invalid permissions)
- [x] `cargo nextest run -p fabro-cli` — 892/892
- [x] `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- [x] `cargo +nightly-2026-04-14 fmt --check --all`
- [x] `bun run generate` on `fabro-api-client` — emits typed
`PermissionLevel` union and required field on `CreateSessionRequest`

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:15:20 -04:00
Bryan Helmkamp
581ab41d28
feat(web): add Overview summary panel and promote PR to header pill (#299)
## Summary

Promotes the most useful per-run state into the Overview tab so users no
longer have to tab-hop to read the basics of a run.

- **Adds a horizontal summary panel** above the workflow graph (right
column only — does not span the stages sidebar) with five columns:
**Created by · Changes · Sandbox · Cost · Artifacts**. Quiet-uppercase
labels (`text-[10px] uppercase tracking-[0.08em] text-fg-muted`) over
regular-weight values. Skeleton loaders while queries are in flight; em
dash in muted color for missing/zero data.
- **Promotes the PR chip** out of the meta strip into a
`SECONDARY_BUTTON_CLASS`-style pill next to the Actions menu, visible on
every tab. Pill renders only when a PR exists.
- Lifts `formatBytesAsMemory`, `formatCpuCores`, `formatUsdMicros` to
`lib/format.ts` so the panel can reuse them.
- New `RunSummaryPanel` is split into a smart wrapper (owns the SWR
hooks) + a presentational `RunSummaryPanelView` (prop-driven) for clean
test seams.
- All 7 `Principal` kinds (user / agent / system / slack / webhook /
worker / anonymous) map to glyph + label; user kind uses login-initial
avatar.

## Screenshots

Captured against a real local Fabro server (`fabro server start`) on
demo runs — these only exercise the Created-by column (the other cells
display em dashes because the demo runs have no PR / diff / billing /
artifacts data). The em-dash states **are** the intended empty-state
design.

### Overview tab — full page

![Overview tab](https://files.catbox.moe/2idmv5.png)

### Header + tabs + summary panel close-up

![Header and panel](https://files.catbox.moe/bugbmy.png)

### Summary panel detail

![Summary panel](https://files.catbox.moe/4sbcl0.png)

> The PR pill (mint icon + `#number` next to Actions) is unverified
visually because no demo run on this server has an associated PR — but
the rendering path is the same `SECONDARY_BUTTON_CLASS` markup as the
Actions button and is conditioned on `run.pullRequestUrl && run.number
!= null`. See the [HTML
prototype](https://github.com/fabro-sh/fabro/blob/feat/run-overview-summary-panel/.context/run-overview-options.html)
for the locked design.

## Test plan

- [x] `cd apps/fabro-web && bun run typecheck` clean (only pre-existing
assistant-ui errors)
- [x] `bun test` — +12 new passes, no new failures (387 pass / 5 fail /
2 errors vs baseline 375 / 6 / 3)
- [x] Manual: load `/runs/<id>` against a real server, confirm panel +
em dashes render correctly
- [ ] Manual on a run **with** a PR: verify the pill appears next to
Actions and opens the PR in a new tab
- [ ] Manual on a run **with** rich data (diff / billing / sandbox
resources / artifacts): verify each column populates correctly

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:52:48 -04:00
Bryan Helmkamp
73ebb7d28b
feat(mcp): support run parent relationships (#295)
## Summary

- Add parent metadata (`parent_id`, `children_count`) to Fabro MCP run
summaries, search summaries, and created-run results.
- Allow MCP clients to create child runs, search direct children, and
link or unlink an existing run's parent through the existing run tools.
- Update MCP docs and tool descriptions for the parent-aware
create/search/interact behavior.

## Test Plan

- [x] `cargo +nightly-2026-04-14 fmt --check --all`
- [x] `cargo nextest run -p fabro-mcp-server`
- [x] `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:26:18 -04:00
Bryan Helmkamp
d1fd6d3abf
fix(web): widen Children tab and hide zero count badge
The Children sub-tab was missing `wide: true` on its route handle, so
the Run detail nav narrowed to max-w-5xl only on this tab. Hide the
count badge when the value is zero to reduce visual noise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:18:20 -04:00
Bryan Helmkamp
8d1b5f6c23
feat(mcp): add interrupt action to fabro_run_interact (#296)
## Summary

- Adds a standalone `interrupt` action to the `fabro_run_interact` MCP
tool, closing a parity gap with the HTTP API (`POST
/api/v1/runs/{id}/interrupt`).
- Lets MCP callers cancel an API-mode agent's current LLM round and park
it in `SteeringHub`'s `waiting_for_steer` state without committing to
follow-up text in the same call.
- Dispatches through the existing `Client::interrupt_run`; no client or
server-side changes.

## Why not just use `message` with `interrupt: true`?

Combined interrupt+steer remains the right choice when you want to
redirect the agent. Bare interrupt is for "pause the agent while I
decide what to say next." The variant's doc comment steers callers
toward `message` or `cancel` as the usual options, since a bare
interrupt with no follow-up leaves the run idle indefinitely.

## Test plan

- [x] `cargo nextest run -p fabro-mcp-server` — 13/13 pass, including
new `interrupt_action_requires_only_run_id` unit test
- [x] `cargo nextest run -p fabro-cli -E 'test(/mcp_/)'` — 27/27 pass,
including extended
`mcp_interact_actions_resolve_selector_and_call_expected_endpoints` E2E
(mocks `POST /runs/{id}/interrupt`, asserts the tool hits it)
- [x] `cargo +nightly-2026-04-14 clippy -p fabro-mcp-server -p fabro-cli
--all-targets -- -D warnings` — clean
- [x] `cargo +nightly-2026-04-14 fmt --check` on touched crates — clean

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:12:32 -04:00
Bryan Helmkamp
ac32963538
feat(web): add Children tab to Run detail page (#294)
## Summary

- Surfaces parent/child run relationships in the web UI as a new
**Children** tab between Files Changed and Sandbox on `/runs/:id`.
- Backend exposes a new `children_count` field on the `Run` summary,
computed on read from the existing
`RunProjectionCacheState.children_by_parent` index — accurate without an
extra query.
- Frontend reuses the compact-table `RunRow` from `/runs` (now exported)
so the children list matches the existing list-view at a glance.
- Tab always shows, with a zero state when there are no children.
Refresh button (icon-only, matching the Files Changed pattern)
re-fetches both the list and the parent detail so the count badge
updates with the list.

## Screenshots

Captured live from the running fabro server.

**Populated — `Children · 2` tab active, two succeeded child rows:**

![Children tab
populated](https://raw.githubusercontent.com/fabro-sh/fabro/feature/run-children-tab/.github/assets/children-tab/populated.png)

**Zero state — visiting a run that has no children:**

![Children tab zero
state](https://raw.githubusercontent.com/fabro-sh/fabro/feature/run-children-tab/.github/assets/children-tab/zero-state.png)

## API verification

```sh
# parent
$ curl -s -H "Authorization: Bearer $TOKEN" \
    http://127.0.0.1:32276/api/v1/runs/01KRTKP5DJJ4EV6T7QSB081Z1N \
    | jq '{id, parent_id, children_count}'
{
  "id": "01KRTKP5DJJ4EV6T7QSB081Z1N",
  "parent_id": null,
  "children_count": 2
}

# child
$ curl -s -H "Authorization: Bearer $TOKEN" \
    http://127.0.0.1:32276/api/v1/runs/01KRTKP7VAS2J2AG73GQSAKF4G \
    | jq '{id, parent_id, children_count}'
{
  "id": "01KRTKP7VAS2J2AG73GQSAKF4G",
  "parent_id": "01KRTKP5DJJ4EV6T7QSB081Z1N",
  "children_count": 0
}

# list-by-parent
$ curl -s -H "Authorization: Bearer $TOKEN" \
    "http://127.0.0.1:32276/api/v1/runs?parent_id=01KRTKP5DJJ4EV6T7QSB081Z1N" \
    | jq '{count: (.data | length), has_more: .meta.has_more}'
{ "count": 2, "has_more": false }
```

## What's in each commit

| Commit | What |
| --- | --- |
| `2f5f4296` | `chore(api-client)`: regenerate TS client from current
OpenAPI spec — catches up drift from #292's source-aware diagnostics and
the session/turn shape updates that hadn't been re-run yet. Pure
generator output. |
| `ba16d2b3` | `feat(web)`: the actual Children tab feature. Backend
`children_count` field + cache wiring, new `useChildRuns` SWR hook,
exported `RunRow`/`RUNS_LIST_GRID_TEMPLATE` from `runs.tsx`, new
`run-children.tsx` route, `Run.children_count` on the generated TS type.
|
| `de0c32c9` | `docs`: live UI screenshots for this PR. Safe to revert
before merge if reviewers prefer a screenshot-free repo. |

## Reproducing the screenshots

1. `cargo build -p fabro-cli && ./target/debug/fabro server start`
2. `cd apps/fabro-web && bun run build`
3. ```sh
PARENT=$(./target/debug/fabro run hello --dry-run --detach --sandbox
local --json | jq -r .run_id)
./target/debug/fabro run hello --dry-run --detach --sandbox local
--parent "$PARENT"
./target/debug/fabro run hello --dry-run --detach --sandbox local
--parent "$PARENT"
   ```
4. Open `http://127.0.0.1:<port>/runs/$PARENT/children` (populated) and
a child's children tab (zero state).

## Test plan

- [x] `cargo nextest run -p fabro-store -p fabro-types -p fabro-api -p
fabro-server -p fabro-mcp-server` — 900+ tests pass, including new
`run_summary_includes_children_count` in `fabro-store`
- [x] `cd apps/fabro-web && bun run typecheck` — clean
- [x] `cd apps/fabro-web && bun test` — 383/383 pass
- [x] OpenAPI ↔ Rust parity (the `fabro-api` `run_summary_round_trip`
test covers the new field both directions)
- [x] Manual API verification via curl (above)
- [x] Live UI verification (screenshots above)

## Out of scope (v1)

- Real-time SSE updates of the children list (refresh button covers
this).
- Multi-page pagination UI (shows first page with a "more exist" footer
when `has_more`).
- Parent breadcrumb on the child run page (separate small change).
- Tree/nesting view (flat list only).
- Empty-state CTA.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 10:21:40 -04:00
fabro-releases[bot]
19b84777c7 Bump version to 0.236.0-nightly.0 2026-05-17 09:19:05 +00:00
Bryan Helmkamp
3afc95abdd
test(server): isolate storage in streaming_session_turn_updates_runtime_context test (#293)
## Summary

- Fixes the flaky nightly release-pipeline failure observed on tag
`v0.235.0-nightly.1` ([run
25975240763](https://github.com/fabro-sh/fabro/actions/runs/25975240763/job/76354137058)).
-
`streaming_session_turn_updates_runtime_context_without_copying_prior_history_to_turn`
was using the default test server settings, which means every parallel
test shares the default session storage directory
(`$HOME/.fabro/storage`).
- `session_store::write_json` writes via `fs::write`, which truncates
the file before writing. A concurrent reader from another test's
`AppState` session lookup can observe the empty file mid-write and fail
to deserialize. The deserialization error bubbled up as a `turn.failed`
SSE event carrying `Serialization error: EOF while parsing a value at
line 1 column 0`.
- Apply the same isolation pattern used in `e9387bf62` for
`interrupt_active_session_turn_cancels_runtime_and_persists_interrupted`:
give this test its own storage root under `std::env::temp_dir()`.

## Test plan

- [x] `cargo nextest run -p fabro-server -- streaming_session_turn`
passes locally
- [x] `cargo nextest run -p fabro-server` (full suite, 552 tests) passes
locally
- [x] `cargo +nightly-2026-04-14 clippy -p fabro-server --all-targets --
-D warnings` clean
- [ ] CI green

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 05:06:57 -04:00
fabro-releases[bot]
0f5285209f Bump version to 0.235.0-nightly.1
Some checks failed
Rust / Test (macOS) (push) Has been cancelled
Rust / Format (push) Has been cancelled
Rust / Clippy (push) Has been cancelled
Rust / Generated Docs (push) Has been cancelled
Rust / Test (Linux) (push) Has been cancelled
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
2026-05-16 22:59:47 +00:00
Bryan Helmkamp
2ba04be181
feat(template): add source-aware diagnostics (#292)
## Summary

Template failures from `fabro run` and structural warnings from `fabro
validate` now preserve source provenance through rendering, workflow
transforms, API serialization, and CLI display. Diagnostics can point at
the actual workflow, import, or prompt file with node/attribute context
instead of surfacing MiniJinja's generic `<string>` source.

## What Changed

- Added named MiniJinja render APIs plus miette-aware `TemplateError`
metadata for source names, source text, spans, and labels.
- Reworked workflow template expansion so inline attributes, imported
workflows, and `@prompt` files render with file and owner context.
- Split strict run behavior from structural validate behavior: run-start
still hard-fails on missing inputs, while validate emits source-aware
warnings and continues linting.
- Extended validation diagnostics through Rust structs, OpenAPI, server
DTO mapping, and CLI rendering with optional source path, line, column,
span, and related metadata.
- Added regression coverage across template rendering, workflow
transforms, CLI output, and the server validate endpoint.

## Verification

- `cargo nextest run -p fabro-template`
- `ulimit -n 4096 && cargo nextest run -p fabro-workflow --no-fail-fast`
- `cargo nextest run -p fabro-cli
bare_fabro_with_unbound_inputs_validates_structurally_with_warning
run_rejects_unbound_template_inputs_before_creating_remote_run`
- `cargo nextest run -p fabro-server
validate_endpoint_returns_template_source_coordinates`
- `cargo build -p fabro-api`
- `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: Aleksi Asikainen <1086393+salieri@users.noreply.github.com>
2026-05-16 18:47:37 -04:00
Bryan Helmkamp
e9387bf622
test(server): isolate storage in interrupt_active_session test
The test was failing in CI because parallel tests share the default
session storage directory ($HOME/.fabro/storage). Every AppState build
runs `recover_stale_running_state`, which scans that directory and
re-marks any in-flight Running turn as Interrupted. When another test
built its AppState while this test's turn was running, the recovery
clobbered the turn before the interrupt request arrived, causing a 409
"Turn is already terminal" response.

Give the test its own storage root and wait for `turn.assistant_text_start`
so the agent is committed to the in-flight LLM call before interrupting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:11:10 -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
2b53917759
fix(validate): treat undefined template vars in @file prompts as warnings (#290)
## Summary

`fabro validate` had inconsistent behavior for undefined template
variables depending on whether the prompt was inline or loaded via an
`@file` reference. Inline `{{ inputs.foo }}` produced a warning and
validation passed; the same expression inside a `@file`-imported prompt
produced a hard validation error.

Fixes #286.

## Root cause

Two template-rendering passes with different strictness, applied to
disjoint inputs:

1. **DOT-source pass**
(`lib/crates/fabro-workflow/src/operations/create.rs`) honored
`RenderMode::Structural` for `fabro validate` — undefined variables
downgraded to a `Severity::Warning` diagnostic, then lenient render
finished the job.
2. **Per-attribute pass**
(`lib/crates/fabro-workflow/src/transforms/variable_expansion.rs`)
inside `TemplateTransform` was always strict and had no `RenderMode`
awareness. Because `FileInliningTransform` runs *before*
`TemplateTransform`, expressions inside `@file` content only ever
encountered the strict pass.

## Fix

- Plumb `RenderMode` through `TransformOptions` into
`TemplateTransform`.
- In `RenderMode::Structural`, the transform catches
`TemplateError::UndefinedVariable` per attribute, emits a warning
diagnostic, and falls back to `render_lenient`.
- Diagnostics flow through a new `Transformed.diagnostics` field into
`Validated` alongside lint output.
- Diagnostics now include `node_id` when the undefined variable was
found inside a node attribute, which is more useful than the previous
"at line 1" location.
- `RenderMode` and the shared `template_undefined_variable_diagnostic`
helper moved to `pipeline/types.rs` so the transform layer can reach
them without a circular dep.

Strict mode (`fabro run`, preflight) is unchanged — undefined inputs
still hard-fail before a run is created.

## Behavior

Illustrative output shapes (variable names and line numbers depend on
the fixture):

Inline prompt (unchanged):
```
warning: undefined template variable `inputs.<name>` at line <n> (template_undefined_variable)
Validation: OK
```

`@file`-imported prompt (previously a hard error, now matches inline —
node-attributed instead of line-attributed):
```
warning [node: <id>]: undefined template variable `inputs.<name>` in node `<id>` (template_undefined_variable)
Validation: OK
```

## Test plan

- [x] `cargo nextest run --workspace` — 5773/5773 passing
- [x] `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings` clean
- [x] `cargo +nightly-2026-04-14 fmt --check --all` clean
- [x] New regression test
`bare_fabro_with_unbound_inputs_in_imported_prompt_validates_structurally_with_warning`
in `lib/crates/fabro-cli/tests/it/cmd/validate.rs` against new fixture
`test/templated_unbound_imported/`
- [x] Existing
`bare_fabro_with_unbound_inputs_validates_structurally_with_warning` and
`strict_render_hard_fails_on_unbound_inputs` still pass — verifies
inline structural and run-start strict behavior are both preserved
- [x] Manual reproduction of the exact inputs from the issue now
succeeds with a warning

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

---------

Co-authored-by: Aleksi Asikainen <1086393+salieri@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:31:22 -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
350c029d97
chore: mark generated fabro-api-client as linguist-generated
Stops github-code-quality (CodeQL) from flagging template artifacts in
the openapi-generator output (unused imports, ASI inconsistencies),
and collapses these files in PR diffs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:17:42 -04:00
Bryan Helmkamp
7ac15b28f0
feat(fabro-web): port /chats/new + /chats/:id from prototype (#289)
## Summary

Ports the validated `/chats/new` and `/chats/:id` chat surface from
`docs/superpowers/prototypes/2026-05-16-chats-new/` into
`apps/fabro-web`. Client-side scripted prototype mounted inside the
existing `AppShell`; replaces `/start` as the planned new "kick off
agent work" entry point (but does not delete `/start` in this phase).

- New routes: `/chats/new` (empty-state composer) and `/chats/:chatId`
(active conversation with assistant-ui's `<Thread>`, scripted streaming
replies, markdown + tool-call rendering).
- Drives `@assistant-ui/react` + `@assistant-ui/react-ui` via
`useLocalRuntime` and a custom `ChatModelAdapter` that cycles a 6-entry
scripted reply bank.
- Tailwind v4 cascade fix: assistant-ui CSS is now imported via `@layer
assistant-ui` so v4 utilities cascade above the package's unlayered
scoped preflight. Includes a discovered Bun-specific tweak — see Notable
Deviations below.
- StrictMode-safe first-message handoff: store seeds the user message
into `seedMessages` with a `pendingResponse: true` flag, and
`chats-detail` triggers a single `runtime.thread.startRun({ parentId:
null })` then consumes the flag. Avoids the prototype's
autorespond-lost-stream race under React 19 StrictMode.

The Ask-Fabro right sidebar (also in the prototype) is **out of scope**
for this PR.

Companion spec:
[`docs/superpowers/specs/2026-05-16-chats-new-prototype-design.md`](../tree/chats-new-port/docs/superpowers/specs/2026-05-16-chats-new-prototype-design.md)
Implementation plan:
[`docs/superpowers/plans/2026-05-16-chats-new-fabro-web-port.md`](../tree/chats-new-port/docs/superpowers/plans/2026-05-16-chats-new-fabro-web-port.md)

## Screenshots

Captured from a local debug `fabro server` running this branch's binary,
signed in via GitHub.

### `/chats/new` (empty state)

![chats-new empty
state](https://github.com/fabro-sh/fabro/raw/chats-new-port/docs/superpowers/prototypes/2026-05-16-chats-new/screenshots/chats-new-v4.png)

### `/chats/:chatId` (active conversation)

![chats-detail active
chat](https://github.com/fabro-sh/fabro/raw/chats-new-port/docs/superpowers/prototypes/2026-05-16-chats-new/screenshots/chats-detail-v4.png)

## Files

**New** (under `apps/fabro-web/`):
- `app/lib/chats-types.ts` — `Chat` wrapper + `ChatContentPart`
discriminated union over the API client's `CompletionContentPart`
- `app/lib/chats-script.ts` — 6-entry scripted reply bank
(`CompletionMessage[]`)
- `app/lib/chats-store.tsx` — Context + `useReducer` for chat metadata,
`pendingResponse` flag, scriptIndex
- `app/lib/chats-runtime.ts` — `createScriptedAdapter` +
`toThreadMessages` boundary converter
- `app/lib/test-utils.tsx` — minimal `renderHook` shim (lifts the
duplicated `IS_REACT_ACT_ENVIRONMENT` + dep-warning silencing pattern
out of `install-app.test.tsx`)
-
`app/components/chats/{tool-fallback,composer-chips,custom-composer}.tsx`
- `app/routes/{chats-layout,chats-new,chats-detail}.tsx`
- Tests: `chats-store.test.tsx` (5), `chats-runtime.test.ts` (4),
`chats-router.test.tsx` (3)

**Modified:**
- `package.json` — adds `@assistant-ui/{react,react-ui,react-markdown}`
(pinned exactly to versions verified in the prototype)
- `app/app.css` — `@layer` declaration + assistant-ui CSS imports into
`layer(assistant-ui)` + `.fabro-chat` `--aui-*` variable overrides
mapping to the Fabro palette
- `app/root.tsx` — removed `import "./app.css"` (see Notable Deviations)
- `app/router.tsx` — wires the chats routes under the AppShell tree

## Notable deviations from the plan

Two intentional deviations, both explained in their commit bodies:

1. **`apps/fabro-web/app/root.tsx` no longer imports `./app.css`.**
Bun's CSS bundler (used by `Bun.build` on `entry.tsx`) rejects
spec-valid `@layer name, name;` ordering between `@import` rules, even
though Tailwind's CLI accepts it. The CSS is built standalone by the
Tailwind CLI step in `scripts/build.ts` and linked from
`index.template.html`, so dropping the JS-side import bypasses Bun's
parser without any runtime change. A safety-net comment at the top of
`app.css` warns future engineers against re-adding the import. Commit:
`c37690be9`.
2. **`!` non-null assertions removed** in two places where the verbatim
prototype copy violated the global CLAUDE.md rule banning `!` in
production code: `chats-script.ts` now uses a typed `FALLBACK_REPLY` and
`??` coalescing; `composer-chips.tsx` lifts the first option of each
chip into a `DEFAULT_*` constant. `chats-runtime.test.ts`'s `for await`
drain loops were also replaced with `Array.fromAsync(...)` per the
no-loops-in-tests rule. Commits: `ace6ac6d4`, `652ad97af`.

## Test plan

- [x] `cd apps/fabro-web && bun run typecheck` — clean
- [x] `bun test` — 383 pass / 0 fail (12 new tests for chats)
- [x] `cd apps/fabro-web && bun run build` — succeeds; assistant-ui CSS
bundled into `dist/assets/app.css`
- [x] **Manual browser smoke test** — debug `fabro` binary running this
branch served `/chats/new` and `/chats/seed_email` correctly inside the
real AppShell with GitHub-OAuth auth (screenshots above).

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:10:16 -04:00
Bryan Helmkamp
4f5e3b78f8
refactor: remove compatibility shims (#281)
## Summary
Simplifies the greenfield PR/run schema surface by collapsing alias-only
type shims and removing legacy compatibility paths that kept old wire
shapes and workflow names alive.

## Changes
- Use canonical `Run`, `PullRequestLink`, `PullRequestResponse`,
`BoardColumn`, `WorkflowSettings`, SWR `Key`, and `SteerRunRequest`
names directly across Rust and web code.
- Remove legacy PR/event deserialization compatibility for old PR
records and command output fields, with tests updated to reject stale
wire shapes.
- Drop obsolete workflow aliases for `agent_loop`, `one_shot`,
`codergen_mode`, and `stack.child_dotfile`, then update docs and tests
to the current names.

## Verification
- `git diff --check`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo nextest run -p fabro-types -p fabro-api -p fabro-client -p
fabro-store -p fabro-server -p fabro-workflow -p fabro-cli`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test`

---

[![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-16 15:29:25 -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
2296ba6ea8
feat(runs): add parent run links (#271)
## Summary

Adds orchestration-only parent links between runs without merging them
into fork or rewind lineage. Runs can now be created under a parent,
linked to a different parent, or unlinked through event-sourced
mutations that rebuild summaries and projections from the run event
stream.

## Changes

- Adds optional `parent_id` to run manifests, public run summaries, run
projections, `run.created`, OpenAPI, and the generated TypeScript API
client.
- Adds `PUT /api/v1/runs/{id}/parent` and `DELETE
/api/v1/runs/{id}/parent` for mutable parent links across any run state,
including terminal or archived runs.
- Records `run.parent.linked` and `run.parent.unlinked` events with
actor metadata and previous/current parent IDs.
- Validates parent changes in the API path: parent must exist for new
links, self-parenting is rejected, cycles are rejected, and same-parent
or already-root operations are idempotent no-ops.
- Adds `parent_id` filtering to run listing while preserving dangling
historical parent references after parent deletion.

## Validation

- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo build -p fabro-api`
- `cargo check -p fabro-workflow -p fabro-store -p fabro-server`
- `cargo nextest run -p fabro-types -p fabro-store`
- `cargo nextest run -p fabro-server --features test-support
create_run_can_set_parent_and_list_children
link_relink_and_unlink_parent_are_idempotent
parent_link_validation_rejects_missing_self_and_cycles
deleting_parent_leaves_child_parent_id_as_historical_reference`
- `cargo nextest run -p fabro-api
run_summary_json_matches_openapi_shape`
- `cd lib/packages/fabro-api-client && bun run typecheck`

Known unrelated broad-suite blocker: `cargo nextest run -p fabro-server
--features test-support get_graph_returns_svg` currently returns 500
because the render subprocess emits test-harness output instead of SVG.

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (unknown context, medium reasoning) via
[Codex](https://openai.com/codex)
2026-05-16 13:57:33 -04:00
Bryan Helmkamp
ae55bded81
fix(sandbox): clone Daytona repos under /home/daytona/repos (#285)
## Summary

Daytona's default snapshot runs as the `daytona` user (uid 1001), which
lacks write permission on `/`. With `run.clone.enabled = true`, sandbox
init failed at `fs.create_folder("/repos", ...)` with HTTP 400, before
the first workflow stage could run:

```
sandbox.git.failed error="Failed to create Daytona repos root" causes=["HTTP 400"]
run.failed
```

Root cause: the Daytona provider was using Docker's root-level `/repos`
layout. Docker works because its containers run as root; Daytona's
default sandbox user does not.

**Fix:** move `REPOS_ROOT` for Daytona to `/home/daytona/repos`,
alongside the existing `/home/daytona/workspace`. The path is writable
by the default sandbox user, the symlink layout is unchanged
(`/home/daytona/workspace/<repo>` →
`/home/daytona/repos/<owner>/<repo>`),
and Docker keeps its existing `/repos` path.

**Bonus — better error diagnostics.** A new `wrap_fs_error(operation,
path, error)` helper in the Daytona provider:

- includes the attempted path in the message (was just "Failed to create
  Daytona repos root" with no indication of which path);
- classifies HTTP 400 as a likely permission issue and points at
  snapshot configuration;
- classifies HTTP 401/403 as an API key permissions issue;
- preserves the underlying `DaytonaError` in the source chain
  (per `docs/internal/error-handling-strategy.md` — verified by walking
  `Error::source()` in the regression test).

So if this class of failure recurs (custom snapshot, future path
changes, ...) the user gets:

> Failed to create Daytona repos root '/home/daytona/repos' failed
> (HTTP 400). This usually means the sandbox user lacks write permission
> on the parent directory. If you're using a custom Daytona snapshot,
> ensure the sandbox user can write to '/home/daytona/repos', or use a
> path under the user's home directory (e.g. /home/daytona/...).

instead of:

> Failed to create Daytona repos root
> HTTP 400

## Test plan

- [x] `cargo build --workspace`
- [x] `cargo nextest run -p fabro-sandbox --features daytona` — 142/142
pass
- [x] `cargo nextest run -p fabro-types -p fabro-workflow` — 1365/1365
pass
- [x] New unit test `wrap_fs_error_classifies_http_400_and_403` —
asserts
      top-level message contains path + hint AND walks the source chain
      to prove `DaytonaError::Api { status_code: 400, .. }` is preserved
- [x] `cargo +nightly-2026-04-14 fmt --check --all`
- [x] `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- [x] **Live regression**: `daytona_clone_layout_live_smoke` against the
      default `daytona-medium` snapshot — failed with `Failed to create
      Daytona repos root / HTTP 400` before the change; passes
      end-to-end after (provisions sandbox → clones repo → verifies
      symlink + HEAD match in 2.5s)

## Related

- Closes #284 (thanks @jessmartin for the report, diagnosis, and
proposed fix)

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

Co-authored-by: Jess Martin <27258+jessmartin@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:47:54 -04:00
Bryan Helmkamp
64fd4de393
docs: offer issue-based contribution path alongside PRs
Add a third option for contributors who'd rather not write the code
themselves: file an issue and a maintainer will implement it with
co-author credit on the landing commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:40:23 -04:00
Bryan Helmkamp
66519ee12a
feat(errors): add structured failure diagnostics (#277)
## Summary
- Make `FailureDetail` the canonical rich diagnostic shape for stage and
terminal failures, with terminal `RunFailure` carrying `{ reason, detail
}`.
- Preserve cause chains and move process stdout/stderr diagnostics into
sanitized `exec_output_tail` instead of embedding them in messages or
causes.
- Update ACP error plumbing, CLI/server/store rendering, OpenAPI, and
the generated TypeScript API client for the nested failure contract.

Closes #273

## Test Plan
- `cargo nextest run -p fabro-types -p fabro-core -p fabro-acp -p
fabro-api -p fabro-store -p fabro-server -p fabro-workflow -p fabro-cli
--no-fail-fast -E 'not test(/returns_svg/)' --status-level fail
--final-status-level fail`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `bun run typecheck` in `lib/packages/fabro-api-client`
- `bun run typecheck` in `apps/fabro-web`
2026-05-16 13:25:07 -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
David Bock
b0847180b6
Fix custom provider resolution in exec and model list (#276)
## Summary
- allow `fabro exec` to use configured custom provider IDs from the
resolved LLM catalog
- route direct exec sessions through the same catalog-aware
provider/profile resolution used by workflow runs
- stop `fabro-client::list_models` from rejecting non-built-in provider
filters client-side
- update CLI snapshots and add regression tests for custom-provider exec
and model listing

## Repro
With a configured provider like:

```toml
[llm.providers.bedrock]
adapter = "openai_compatible"
base_url = "https://.../v1"

[cli.exec.model]
provider = "bedrock"
name = "bedrock-claude-sonnet-4-6"
```

these paths diverged:

- `fabro run ... --model bedrock-claude-sonnet-4-6` worked
- `fabro model list` showed `bedrock-*` models
- `fabro exec "..."` failed with `unknown provider: bedrock`
- `fabro model test --provider bedrock` failed with the same client-side
error

## Root cause
There were two separate built-in-only assumptions:

1. `fabro-agent` direct CLI paths parsed provider strings into the
built-in `Provider` enum and built a default catalog, so configured
provider IDs from `settings.toml` were invisible.
2. `fabro-client::list_models()` parsed the optional provider filter
into the same built-in enum before calling the server, so custom
provider filters never reached the API.

## Validation
- `cargo check -p fabro-cli -p fabro-agent -p fabro-client`
- `cargo test -p fabro-agent
resolve_provider_accepts_custom_catalog_provider -- --nocapture`
- `cargo test -p fabro-client list_models_allows_custom_provider_filters
-- --nocapture`
- `cargo test -p fabro-cli
exec_accepts_configured_custom_provider_from_settings -- --nocapture`
- `cargo test -p fabro-cli list_invalid_provider_errors -- --nocapture`
- `cargo test -p fabro-cli help -- --nocapture`

---------

Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-16 10:49:14 -04:00
Bryan Helmkamp
f790a47da4
feat(sandbox): surface provider links and network policy (#275)
## Summary
- Add provider dashboard URL reporting to `SandboxDetails`, including
Daytona dashboard links and a Sandbox tab provider link.
- Extend `SandboxDetails` with required provider-neutral public network
policy for egress and ingress allow/block rules.
- Populate local, Docker, and Daytona network policies from provider
details when Fabro can assert them, otherwise default to explicit
`unknown` policy.
- Update OpenAPI, Rust API replacements, generated TypeScript client
models, server/API tests, and the Sandbox tab `Network` panel.

## Notes
- This reports policy only; it does not probe live connectivity.
- The network model intentionally excludes ports, previews, IP
addresses, DNS, routes, Docker network IDs, and service discovery.
- Older persisted/API JSON still deserializes through the Rust serde
default for `network`.

## Verification
- `cargo build -p fabro-api`
- `cargo nextest run -p fabro-types -p fabro-api -p fabro-sandbox -p
fabro-server sandbox_details`
- `cargo test -p fabro-sandbox details --features docker,daytona`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cd apps/fabro-web && bun test app/routes/run-sandbox.test.tsx`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun run build`
- `cd lib/packages/fabro-api-client && bun run typecheck`
- `git diff --check`

## Post-Deploy Monitoring & Validation
- Log queries/search terms: `Failed to inspect Docker container`,
`Daytona sandbox is not initialized`, `missing runtime metadata`, `GET
/api/v1/runs/*/sandbox`, `Sandbox unavailable`.
- Metrics/dashboards to watch: API 5xx/error rate for `GET
/api/v1/runs/{id}/sandbox`, frontend error reporting for the Sandbox
tab, and provider reconnect/inspect failures.
- Expected healthy signals: Sandbox tab loads with Overview, Resources,
Network, Labels, and Timestamps; Daytona sandboxes show provider links;
local/ambiguous network policy shows `Unknown`; Docker `network_mode =
none` shows `Blocked`.
- Failure signals and rollback trigger: sandbox details deserialization
errors, missing `network` crashes, sustained sandbox endpoint 5xx
increase, or blank Sandbox tab after deploy. Roll back this PR or hide
the Network panel if API/client shape issues appear.
- Validation window and owner: first 24 hours after deploy, release
owner/on-call.

## Compound Engineering
- Implemented with OpenAI Codex CLI on GPT-5.
2026-05-16 10:16:18 -04:00
Bryan Helmkamp
be993cb608
feat(server): expose health check at /api/v1/health (#279)
## Summary

- Mount the existing `/health` handler at `/api/v1/health` so callers
using a uniform `/api/v1` base no longer have to special-case the root
path. The root `/health` route is unchanged and remains the canonical
probe target.
- Add the new path to the OpenAPI spec (`operationId: getApiHealth`,
`Discovery` tag, reusing `HealthResponse`), and regenerate the
TypeScript client so `DiscoveryApi.getApiHealth()` is exposed alongside
`getHealth()`.
- Split the old `moved_routes_not_at_root_of_api_prefix` test into a
focused `api_v1_root_is_not_routed` and a new
`health_responds_at_versioned_path` that asserts `200` +
`{"status":"ok"}` under the versioned prefix.

## Test plan

- [x] `cargo build --workspace` (verifies the OpenAPI spec regenerates
cleanly via `fabro-api` build.rs)
- [x] `cargo nextest run -p fabro-server` (545 tests pass, including
OpenAPI conformance and the new routing assertions)
- [x] `cd lib/packages/fabro-api-client && bun run generate`
(regenerated client exposes `getApiHealth`)
- [ ] Manual: `fabro server start` then `curl -s
http://localhost:<port>/api/v1/health` and `curl -s
http://localhost:<port>/health` both return `{"status":"ok"}`

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 08:37:00 -04:00
Bryan Helmkamp
d52a2ccbe2
feat: add opt-in LiteLLM TOML provider (#269)
## Summary
- Add a disabled built-in `litellm` provider fragment backed by the
OpenAI-compatible adapter and local proxy defaults.
- Document how to enable LiteLLM in `settings.toml`, configure
credentials, and declare explicit LiteLLM-routed models.
- Register the LiteLLM integration page and cross-link it from the model
and settings docs.

## Validation
- `cargo test -p fabro-model`
- `cargo test -p fabro-config`
- `jq empty docs/public/docs.json`
- `rg -n 'aliases = \["openai_compatible",
"openai-compatible"\]|llm\.discovery|FABRO_LITELLM|litellm_api_key_env|x-litellm-'
lib/crates/fabro-model/src/catalog/providers/litellm.toml
docs/public/integrations/litellm.mdx
docs/public/core-concepts/models.mdx
docs/public/reference/user-configuration.mdx` returned no matches

---------

Co-authored-by: Mark Ferraz <mferraz@netwoven.com>
2026-05-16 08:36:31 -04:00
Bryan Helmkamp
9768651b52
feat(model): add opt-in Ollama catalog provider (#268)
## Summary

Adds Ollama as a disabled-by-default built-in catalog provider backed
entirely by provider TOML. Enabling `[llm.providers.ollama] enabled =
true` exposes the bundled `qwen3-coder` sample model through the
existing OpenAI-compatible adapter, while other local Ollama models
still require explicit model blocks until fabro-sh/fabro#267 adds
discovery.

The docs now show the opt-in setting and note that local users can set
`OLLAMA_API_KEY=ollama` for Ollama's OpenAI-compatible endpoint.

## Verification

- `cargo nextest run -p fabro-model`
- `cargo nextest run -p fabro-cli cmd::model`
- `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: roALAB1 <233429779+roALAB1@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 08:18:21 -04:00
fabro-releases[bot]
54c0bb4ef1 Bump version to 0.235.0-nightly.0 2026-05-16 09:45:35 +00:00
Bryan Helmkamp
9027f1cc67
docs(tutorials): clarify what sub-workflows share with the parent run
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
The sub-workflow tutorial implied a child workflow was an "entirely
separate" engine with isolated logs, but the child reuses the parent's
run ID and emits into the same event stream. Reframe the section as
"What's shared and what's isolated" and correct the encapsulation
bullet to scope the isolation to checkpoints and artifacts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:03:53 -04:00
Bryan Helmkamp
ba27bcdd57
docs(github): note GITHUB_TOKEN enables gh CLI in sandbox
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:14:22 -04:00
Bryan Helmkamp
4071908b1d
docs(workflows): document file-based workflow imports
Adds a Defining Workflows page covering the import placeholder
syntax, node ID prefixing, the imported-file contract, default
attribute and class propagation, retry_target remapping, templating
behavior, nested imports, empty-import bypass, and the import_error
validation surface.
2026-05-15 08:02:31 -04:00
fabro-releases[bot]
6f656c8f2b Bump version to 0.234.0-nightly.0 2026-05-15 10:07:14 +00:00
Bryan Helmkamp
32f100cbe7
feat(install): make LLM setup optional in web installer and CLI (#265)
Some checks failed
Rust / Clippy (push) Waiting to run
Rust / Format (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
## Summary

Makes LLM setup explicitly skippable in both the web installer and
`fabro install`, without making omission accidental. A skipped LLM step
lets install complete with zero LLM credentials; later LLM-dependent
workflows keep using the existing provider-not-configured behavior.
`fabro doctor` is intentionally unchanged.

Plan: `docs/superpowers/plans/2026-05-14-optional-llm-install.md`

## Key changes

**Server + API**
- `PUT /install/llm` now accepts `{"providers":[]}` as "LLM step
completed, skipped" — the empty-list rejection is removed; per-provider
validation for non-empty lists is retained.
- OpenAPI: dropped `minItems: 1` from
`InstallLlmProvidersInput.providers`, updated schema descriptions so
empty = skipped and `llm: null` = incomplete. TypeScript client
regenerated.
- `/install/finish` still requires the LLM step to be present, but
tolerates zero credentials — it writes settings, runtime auth secrets,
and GitHub secrets normally and writes no LLM vault entries.

**Web installer**
- New "Skip LLM setup" secondary action on the LLM step (via a
`secondaryAction` prop on `StepPanel`) that records an empty provider
list and advances to GitHub.
- Review screen shows `LLM providers: Skipped` (step completed, empty)
vs `Not configured` (step never completed), via a new
`describeLlmSummary` helper.
- Continue with no API keys still shows the existing validation error —
skipping is only reachable through the explicit skip action.

**CLI**
- Interactive `fabro install` asks "Configure LLM providers now?"
(default yes) before provider selection; declining returns an empty
selection and continues to GitHub.
- Hidden non-interactive `--skip-llm` flag, mutually exclusive with
`--llm-provider` / `--llm-api-key-stdin` / `--llm-api-key-env` via clap
`conflicts_with_all`. Missing LLM flags are still validation errors
unless `--skip-llm` is present. Non-interactive usage text updated with
a skip example.

## Code review

Ran a 12-reviewer `ce:review` pass (correctness, testing,
maintainability, project-standards, agent-native, learnings, security,
api-contract, reliability, adversarial, cli-readiness,
kieran-typescript). No P0/P1 findings; agent-native parity PASS. Applied
fixes in `40a29c591`:
- Re-entrancy guard on `runStepSubmit` so a fast double-click on "Skip
LLM setup" can't fire two requests.
- `validate()` only suggests `--skip-llm` in the missing-provider error
when no credential flag is set (it conflicts with those flags).
- Added tests: all three `--skip-llm` conflict arms, the review screen's
"Not configured" branch, and the skip-button failure path.

One advisory finding left as report-only: an empty `PUT /install/llm`
overwrites previously-saved credentials if a user navigates Back and
clicks Skip — judged acceptable since the button is explicitly labeled
and clicking it is deliberate.

## Testing

- `cargo nextest run -p fabro-server -p fabro-cli -p fabro-install` —
1521 passed
- `cargo build -p fabro-api`, `cargo fmt --check`, `cargo clippy`
(changed crates) — clean
- `bun test` (install-app) — 14 passed; `bun run typecheck` — clean
- New coverage: server accepts empty providers + session shows `llm`
complete with `providers:[]`; finish with skipped LLM persists no LLM
vault credentials but keeps GitHub secrets; web skip button PUTs
`providers:[]` and navigates to GitHub; review renders Skipped / Not
configured; CLI `--skip-llm` requires `--non-interactive`, conflicts
with all credential flags, `validate()` succeeds with `--skip-llm`,
usage text documents `--skip-llm`.

Not added (out of plan scope): an automated test for the interactive
`InstallInputSource` skip branch — `InteractiveInstallInputSource` is
TTY-coupled and has no existing tests; the non-interactive `--skip-llm`
path is fully covered.

## Post-Deploy Monitoring & Validation

This change is install-time only; there is no continuous runtime impact.
Validate during the next install/release smoke:

- **Web installer:** run a fresh browser install, click "Skip LLM setup"
on the LLM step, confirm it advances to GitHub and the review screen
reads `LLM providers: Skipped`. Finish the install and confirm the
server restarts into normal mode with no LLM credentials in the vault
(`secrets.json` has no credential entries) and
GitHub/server/object-store/sandbox settings written normally.
- **CLI:** run `fabro install --non-interactive --skip-llm
--github-strategy token --github-username <user>` and confirm it
completes; run interactive `fabro install` and confirm declining
"Configure LLM providers now?" continues to GitHub.
- **Healthy signals:** install completes (web `/install/finish` → 202;
CLI exits 0), server boots in normal mode, `fabro doctor` runs and
reports no LLM providers configured (expected, unchanged behavior).
- **Failure signals / rollback trigger:** install fails to finish,
server fails to boot after a skipped install, or `/install/finish`
rejects a completed-but-empty LLM step. Rollback = revert this PR;
install behavior returns to requiring at least one LLM provider.
- **Validation window/owner:** next install smoke / release
verification, owned by whoever runs the release.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:34:39 -04:00
Bryan Helmkamp
869b94c0c4
feat(model): add Venice catalog provider (#266)
## Summary

Adds Venice as a catalog-only built-in OpenAI-compatible provider. The
preparatory catalog/test work is already merged in #264, so this PR is
intentionally limited to the provider TOML.

## Changes

- Adds `lib/crates/fabro-model/src/catalog/providers/venice.toml`.
- Registers provider ID `venice` with alias `venice-ai`,
OpenAI-compatible base URL, and `credential:venice` / `VENICE_API_KEY`
credential lookup.
- Adds the two initial Venice-owned models and pricing metadata.

## Verification

- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo nextest run -p fabro-model`
- `cargo nextest run -p fabro-cli cmd::model`
- `cargo nextest run --workspace --status-level slow --profile ci`
- `cargo insta pending-snapshots`

---

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

---------

Co-authored-by: Jesse <606+jesseproudman@users.noreply.github.com>
2026-05-14 21:59:04 -04:00
Bryan Helmkamp
78941a2b84
test(model): prepare catalog-only providers (#264)
## Summary

Prepares the model catalog tests for catalog-only built-in providers so
a future provider can be added with just its catalog TOML.

## Changes

- Replaces the closed-enum round-trip guardrail with a catalog metadata
guardrail, allowing built-in TOML providers that do not have `Provider`
enum variants.
- Makes the all-model `fabro model` CLI tests assert stable table
structure instead of snapshotting every built-in catalog row.
- Renames synthetic custom-provider and missing-provider fixtures away
from provider names that can become real catalog entries.

## Verification

- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo nextest run -p fabro-model -p fabro-auth -p fabro-llm -p
fabro-server -p fabro-workflow -p fabro-config`
- `cargo nextest run -p fabro-cli cmd::model`

---

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

---------

Co-authored-by: Jesse <606+jesseproudman@users.noreply.github.com>
2026-05-14 21:51:18 -04:00
Bryan Helmkamp
e7e4fb5ca1
Add Daytona volume mount passthrough (#263)
## Summary

This adds run-configuration support for mounting existing Daytona
volumes into Fabro-managed Daytona sandboxes.

Concretely, this PR:

- adds `[[run.sandbox.daytona.volumes]]` with `volume_id`, `mount_path`,
and optional `subpath`
- resolves that config through the layer/settings/runtime pipeline
- forwards configured mounts to `daytona_sdk::SandboxBaseParams.volumes`
when creating the sandbox
- documents the configuration surface in the Daytona environment and run
configuration docs

## Motivation

Daytona already supports attaching volumes when a sandbox is created,
but Fabro currently owns that sandbox creation call. That means users
cannot attach a pre-created Daytona volume to a Fabro-managed sandbox
from run config.

The intended use is persistent, provider-owned state such as agent
credentials, caches, datasets, or other files that should survive
ephemeral sandbox lifecycles.

## Scope

This is intentionally a narrow passthrough. Fabro does not create,
delete, list, wait on, or otherwise manage Daytona volume lifecycle.
Users create the volume in Daytona first, then reference its `volume_id`
from Fabro run config.

`volumes` defaults to an empty list in resolved settings for backwards
compatibility with existing serialized settings.

## Testing

- `cargo test -p fabro-server
runtime_daytona_config_preserves_volume_mounts`
- `cargo test -p fabro-config resolves_daytona_volume_mounts`
- `cargo test -p fabro-sandbox --features daytona volume_mounts`
- `cargo test -p fabro-workflow
runtime_daytona_config_preserves_volume_mounts`
- `cargo check -p fabro-server`

---

_Re-opened from #262 (originally by @kimprobably) to land a rustfmt fix
— the original PR came from an org-owned fork, which blocks maintainer
pushes. Branch is now on the base repo. Original commit preserved; one
additional commit fixes rustfmt formatting._

Co-authored-by: Tim Keen <tim@keen.digital>
2026-05-14 10:54:43 -04:00
Bryan Helmkamp
c0fe29390a
feat(sandbox): prepare clone layout for multi-repo runs (#250)
## Summary

- Clone primary GitHub repos into provider-owned `/repos/{owner}/{repo}`
paths for Docker and Daytona sandboxes.
- Keep user/agent execution rooted at the workspace symlink, e.g.
`/workspace/{repo}` or `/home/daytona/workspace/{repo}`.
- Persist optional runtime layout metadata (`workspace_root`,
`repos_root`, `primary_repo_path`, `primary_repo_link`) through events,
projections, OpenAPI, Rust API tests, and the TS client.
- Preserve empty workspace behavior and reconnect from stored
`working_directory` for existing run records.

## Verification

- `cargo nextest run -p fabro-sandbox --features docker,daytona`
- `cargo nextest run -p fabro-workflow`
- `cargo nextest run -p fabro-server`
- `cargo build -p fabro-api`
- `cargo nextest run -p fabro-api run_sandbox_json_matches_openapi_shape
sandbox_details_json_matches_openapi_shape`
- `cd lib/packages/fabro-api-client && bun run typecheck`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `git diff --check`

## Notes

- Added ignored live smoke tests for Docker and Daytona layout
validation; they require real provider credentials/runtime.
2026-05-14 09:38:20 -04:00
Bryan Helmkamp
1cfe9419f5
refactor(llm): simplify catalog cleanup paths (#261)
## Summary

This is a cleanup pass over the configurable LLM provider/catalog work
from issue #210. It addresses reuse, quality, and efficiency findings
from the phase 0-9 review without changing the public provider settings
contract.

Notable changes:

- skip LLM client initialization during run preflight when the workflow
has no LLM nodes
- make preflight provider checks use alias-aware `Client::has_provider`
- resolve `run.model.fallbacks` through the catalog instead of the old
empty-key bridge
- paginate `/models` before cloning returned rows
- share label parsing, provider default-adapter lookup, enum
expected-value formatting, and billing token formatting helpers
- use catalog provider display names for OpenAI-compatible agent
profiles
- align process-env configured-provider discovery with
`EnvCredentialSource`

## Verification

- `cargo check -p fabro-config -p fabro-model -p fabro-auth -p
fabro-agent -p fabro-workflow -p fabro-server -p fabro-cli`
- `cargo nextest run -p fabro-config parse_labels_keeps_key_value_pairs`
- `cargo nextest run -p fabro-workflow resolve_fallback_chain_resolves`
- `cargo nextest run -p fabro-auth configured_providers`
- `cargo nextest run -p fabro-server list_models`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy -p fabro-model -p fabro-auth -p
fabro-workflow -p fabro-server -p fabro-agent -p fabro-config -p
fabro-cli --all-targets -- -D warnings`
- `cd apps/fabro-web && bun test app/routes/run-billing.test.tsx`
- `cd apps/fabro-web && bun run typecheck`
- `git diff --check`

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 09:35:42 -04:00
fabro-releases[bot]
1f531a9b9b Bump version to 0.233.0-nightly.0 2026-05-14 10:01:03 +00:00
Bryan Helmkamp
1b6189ee32
docs(llm): finish configurable provider cleanup (#260)
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
## Summary

Finish phase 9 of the configurable LLM provider/model work by aligning
public docs, release notes, and guardrails with the implementation
already landed in phases 0-8.

- documents settings-driven providers/models, OpenAI-compatible gateway
examples, typed `extra_headers`, model `api_id`, controls, and per-speed
costs
- adds the 2026-05-13 changelog entry and provider string migration note
- updates the internal phase plan ledger to reflect current
implementation status
- adds a workspace policy test blocking direct production
`Catalog::builtin()` usage outside catalog owner/test code
- clarifies `Provider` as a built-in compatibility enum while open-ended
identity is `ProviderId`

## Verification

- `cargo nextest run -p fabro-dev --features dev --test it policy`
- `cargo dev docs check`
- `cargo nextest run -p fabro-model -p fabro-config -p fabro-auth -p
fabro-llm`
- `cargo build --workspace`
- `cargo nextest run --workspace` (5717 passed, 182 skipped, nextest
reported 1 leaky test)
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `git diff --check`
2026-05-13 17:17:54 -04:00
Bryan Helmkamp
a81eb09e78
feat(llm): add catalog controls and speed billing (#249)
## Summary

This PR advances the catalog-driven LLM work from fabro-sh/fabro#210 by
making the resolved model catalog the source of truth for provider
registration, request control validation, and billing identity. Runs now
preserve canonical provider/model/speed identity through pricing and API
responses instead of collapsing billing around provider API aliases or
model IDs alone.

## What Changed

- Register LLM provider adapters from the resolved catalog, including
custom OpenAI-compatible providers and their credential resolution
paths.
- Validate effective model request controls, including run-level
defaults and node overrides, before dispatching LLM requests.
- Add catalog-aware billing lookup that prices canonical `ModelRef`
values, uses base model costs for standard speed, applies per-speed cost
overrides, and returns an unknown estimate instead of silently billing
zero for unsupported combinations.
- Move Anthropic Opus fast-mode pricing into the built-in catalog for
`claude-opus-4-6` and `claude-opus-4-7`.
- Thread the injected catalog and effective speed controls through
workflow billing, including API-mode and CLI-mode handlers.
- Update billing APIs, server aggregation, generated clients, and the
web billing view to expose provider/model/speed billing identity and
keep standard and fast usage in separate rows.

## Notes for Review

Billing lookup intentionally uses canonical catalog model IDs. Provider
`api_id` substitution remains limited to provider request construction,
so aliases can be used on the wire without changing billing identity.
Event conversion paths that do not have catalog access now preserve
token counts with a null dollar estimate rather than falling back to the
bootstrap catalog.

## Verification

- `cargo build -p fabro-api`
- `cd lib/packages/fabro-api-client && bun run generate`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `ulimit -n 4096 && cargo nextest run -p fabro-model -p fabro-workflow
-p fabro-server -p fabro-api -p fabro-cli --no-fail-fast`
- `ulimit -n 4096 && cargo nextest run --workspace --no-fail-fast`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test`
- `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)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:12:16 -04:00
Bryan Helmkamp
6297b200f7
refactor(run): add rich failure contract (#256)
## Summary

Terminal run failures now use a first-class `RunFailure` contract so
downstream consumers receive structured diagnostics instead of flat
`error` / `causes` / `reason` fields. The wire shape keeps concise
public messages, source-chain causes, classification, optional
actor/signature data, and redacted exec output tail in one nested value.

Refs fabro-sh/fabro#198

## What Changed

- Added `fabro_types::RunFailure` and changed `run.failed` to emit
`properties.failure` with `final_git_commit_sha` for failed-run commit
state.
- Replaced `Conclusion.failure_reason` with `Conclusion.failure` while
leaving stage-level `StageCompletion.failure_reason` untouched.
- Updated workflow internals to preserve owned error source chains until
terminal event projection, then convert them into `RunFailure.causes`.
- Updated store, server, CLI, OpenAPI, and generated TypeScript client
consumers to use the nested failure object.
- Added serialization, OpenAPI replacement, projection, and lifecycle
coverage for the new contract.

## Validation

- `cargo nextest run -p fabro-api -p fabro-types -p fabro-workflow -p
fabro-store -p fabro-server -p fabro-cli`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test`
- `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-13 12:32:28 -04:00
Bryan Helmkamp
0e3d0c5c97
feat(manifest): support path-based Daytona Dockerfiles (#258)
## Summary

Supports `dockerfile = { path = "..." }` for Daytona snapshots declared
in `.fabro/project.toml` and workflow-local `workflow.toml`, resolving
each path relative to the TOML file that declared it. Manifest building
now bundles project-level Dockerfiles into the target workflow file
bundle, and server manifest preparation rewrites bundled Dockerfile
paths to inline content before settings reach sandbox creation.

The repo Daytona snapshot config now uses `.fabro/Dockerfile` instead of
embedding the Dockerfile in TOML, preserving the prior Dockerfile
content exactly.

## Testing

- `cargo nextest run -p fabro-manifest
build_manifest_bundles_project_config_daytona_dockerfile_relative_to_project_config`
- `cargo nextest run -p fabro-manifest`
- `cargo nextest run -p fabro-server
prepare_manifest_inlines_project_config_daytona_dockerfile_from_bundle
prepare_manifest_errors_when_project_config_dockerfile_bundle_is_missing`
- `cargo nextest run -p fabro-server`
- `cargo nextest run -p fabro-config`
- `cargo nextest run -p fabro-manifest -p fabro-server -p fabro-config`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`

Optional credentialed Daytona live smoke was not run.

## Post-Deploy Monitoring & Validation

- Log queries/search terms: `missing bundled dockerfile`, `unsupported
dockerfile reference`, `invalid manifest project config path`,
`dockerfile path should have been resolved to inline content before
sandbox creation`, Daytona snapshot creation failures.
- Metrics/dashboards: run submission/preflight failure rate, Daytona
sandbox startup failure rate, snapshot creation failure rate, and run
validation errors for manifests using bundled files.
- Healthy signals: runs using `.fabro/project.toml` with `dockerfile = {
path = "Dockerfile" }` progress past manifest preparation and Daytona
snapshot creation without path-resolution errors.
- Failure signals and rollback trigger: any sustained increase in
manifest preparation failures or Daytona snapshot failures containing
the log terms above; rollback by reverting this PR or temporarily
restoring inline Dockerfile TOML for affected deployments.
- Validation window and owner: first 24 hours after deploy; owner is the
deploying operator/on-call engineer.

![Compound Engineered: Codex CLI /
GPT-5](https://img.shields.io/badge/Compound%20Engineered-Codex%20CLI%20%2F%20GPT--5-blue)
2026-05-13 12:32:15 -04:00
Bryan Helmkamp
4c557a9be9
chore(deps): patch openssl and rmcp Dependabot advisories (#257)
## Summary

Patches all three open Dependabot alerts on `main`:

- **#26 / #28 (openssl, high + medium)** — bump `openssl` 0.10.78 →
0.10.79. Patches `GHSA-xp3w-r5p5-63rr` (UB in `X509Ref::ocsp_responders`
for certs with non-UTF-8 OCSP URLs) and `GHSA-xv59-967r-8726` (heap
buffer overflow in AES key-wrap-with-padding). Lockfile-only.
- **#27 (rmcp, high)** — bump workspace `rmcp` from `1.3` to `1.4`,
which resolves to 1.7.0. Patches `GHSA-89vp-x53w-74fx` (DNS rebinding in
the Streamable HTTP **server** transport). Fabro only uses the
streamable-http **client** transport
(`lib/crates/fabro-mcp/src/client.rs`), so practical exposure was nil —
bumping anyway to stay on a supported, patched line.

Each fix is in its own commit so it can be reverted independently.

## Test plan

- [x] `cargo build --workspace` clean after each bump
- [x] `cargo nextest run -p fabro-mcp -p fabro-mcp-server` — 30/30 pass
on rmcp 1.7
- [ ] CI green

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:31:56 -04:00
Bryan Helmkamp
32970e0a30
fix(graphviz): accept multi-line DOT attribute blocks without commas (#255)
Fixes #179.

## Summary
- `fabro validate`'s DOT parser rejected multi-line node attribute
blocks unless every attribute was comma-separated, forcing long node
definitions onto a single line.
- Per the DOT spec, the separator between attributes is optional —
whitespace (including newlines) alone is sufficient, and `,` or `;` are
both accepted as explicit separators.
- `attr_block` now uses `many0(terminated(attr, opt(',' | ';')))`
instead of `separated_list0(',', attr)`, so all three forms parse
identically.

## Before / after

```dot
// previously rejected — now parses
inspect [
    label="Inspect Code"
    shape=tab
    prompt="@prompts/inspect.md"
    class="heavy"
    reasoning_effort="high"
]
```

## Test plan
- [x] Added regression test `parse_attr_block_multiline_without_commas`
covering the exact form from #179.
- [x] `cargo nextest run -p fabro-graphviz` — 109/109 pass, including
the new test and existing comma-separated multi-line tests.
- [x] `cargo +nightly-2026-04-14 clippy -p fabro-graphviz --all-targets
-- -D warnings` clean.

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

Co-authored-by: Nate Aune <118984+natea@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:29:09 -04:00
David Julia
a591a1ca63
feat(slack): render plan summary + run link in interview messages (re #253, stacked on #252) (#254)
This branch contains both commits:
1. The action_id uniqueness fix from #252 (`fix(slack): make per-button
action_id unique to satisfy Slack's invalid_blocks check`).
2. The new context+link fix (`feat(slack): render context_display and
run link in interview messages`).

The first commit needs to land (or be rebuilt by the maintainer
workflow) before the second is meaningful, because without it
multi-button gates still fail with `invalid_blocks` and the new context
block never reaches Slack. Reviewing #252 first and this issue/PR second
is the cleanest flow.

## What

Two-file change in `lib/crates/fabro-slack/` and
`lib/crates/fabro-server/`. The Slack interview message now includes the
upstream stage's response (`context_display`) and a link back to the
run, so a reviewer in Slack can decide A vs R without opening the web
UI.

## Why

See **#253** for the full repro, screenshots, and threat model. Short
version: today the Slack message contains only the hexagon node's
`label` plus the buttons, which is not enough information to act on.

## Diff shape

- `question_to_blocks` gains a `run_web_url: Option<&str>` argument.
- New helpers:
- `header_section(question, run_web_url)`: bold question text, optional
`stage \`{stage}\`` hint, and an "Open in Fabro" link when the URL is
known.
- `context_section(context_display)`: renders the upstream stage's
response (e.g. plan summary, Dossier URLs) below the header, separated
by a `divider`. Empty context_display is skipped so the message falls
back cleanly to the old two-block shape.
- `escape_slack_controls(text)`: HTML-entity escapes `&`, `<`, `>` in
untrusted strings (question, stage, context_display, answered_blocks
question and answer text). Neutralises LLM-produced payloads like
`<!here>`, `<@U…>`, `<#C…>` while leaving Markdown formatting (`*bold*`,
`_italic_`, `` `code` ``, `~strike~`) intact. Per
https://docs.slack.dev/messaging/formatting-message-text/#escaping.
- `truncate_to_limit`: clamps both the header text and the context block
against Slack's documented 3000-character section text limit, with the
truncation suffix counted against the budget so the result is always
under the cap. Defends against pathological questions or oversized LLM
responses producing `invalid_blocks`.
- `server.rs`: `start_optional_slack_service`'s event subscriber calls
`state.run_web_url(&envelope.event.run_id)` per event and forwards the
result to `SlackService::handle_event`, which threads it into
`question_to_blocks`. Returns `None` (and the link is omitted) when
`server.web.enabled` is `false` or `server.web.url` is unset.

## Tests

10 new in `lib/crates/fabro-slack/src/blocks.rs`:

- `header_includes_run_link_when_url_provided`
- `header_omits_link_when_url_missing`
- `header_shows_stage_when_present`
- `header_truncates_when_inputs_exceed_section_limit`
- `context_display_renders_between_header_and_actions`
- `context_display_truncates_oversized_text_to_fit_slack_budget`
- `empty_context_display_is_skipped`
- `slack_control_chars_in_question_text_are_escaped`
- `slack_control_chars_in_context_display_are_escaped` (covers
`<!here>`/`<@U…>`/`<#C…>` neutralisation and verifies Markdown survives)
- `answered_blocks_escape_slack_control_chars`

84/84 `fabro-slack` tests pass (was 74 after #252). `cargo
+nightly-2026-04-14 fmt --check --all` and `cargo +nightly-2026-04-14
clippy -p fabro-slack -p fabro-server --all-targets -- -D warnings` both
clean.

## Verified end-to-end

Built a patched binary, swapped it for the brew install, triggered a
fresh multi-choice approve gate against a real Slack workspace. The
Slack message now renders with bold "Approve Plan" header, `stage
\`approve\`` hint, "Open in Fabro" link, the upstream plan summary block
(Dossier canonical and version URLs, `tmp-docs/fabro-plan.html` artifact
path, the plan-summary bullets), a divider, and the `[A] Approve` and
`[R] Revise` buttons. Reviewer can act on the gate from Slack alone.

## Latent observation, not in this diff

`lib/crates/fabro-server/src/server.rs::AppState::run_web_url` has a
comment saying it is snapshotted at create-time so attach replays remain
stable when `server.web.url` changes, but the implementation reads
current settings via `server_settings()`/`canonical_origin()`. This
patch is unaffected (per-event call), but the comment looks stale.

## Closes

Closes #253 if you choose to land this directly. Otherwise this PR is
background material for the issue.

---------

Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-13 07:41:54 -04:00
David Julia
69ce83be43
fix(slack): make per-button action_id unique (re #251 ) (#252)
Heads up:
[CONTRIBUTING.md](https://github.com/fabro-sh/fabro/blob/main/CONTRIBUTING.md)
says you don't accept outside PRs, *"Instead of accepting outside pull
requests, we accept bug reports and feature requests as GitHub Issues."*
I filed the canonical bug report as **#251**, and that's where any
actual discussion belongs.

This PR is a courtesy ready-made diff in case it's useful to whoever
supervises the AI workflow that lands this fix. Feel free to close it
without comment; nothing is being asked of you here. I just thought it'd
be useful to have a reference for what I did locally to fix it.

## What

Two-file behaviour fix in `lib/crates/fabro-slack/`: every interview
button on a multi-button gate now gets a Slack-unique `action_id`. Today
they all share `"interview.answer"`, so Slack rejects the
`chat.postMessage` with `invalid_blocks` and
`SlackService::handle_event` silently drops the error.

## Why

Multi-button Slack interview gates never reach Slack. Full reproducer,
MITM-captured `invalid_blocks` response, and root-cause walkthrough are
in **#251**.

## Diff shape

- `blocks.rs`: each button gets a unique suffix.
- `YesNo` / `Confirmation`: `interview.answer.yes` /
`interview.answer.no`
- `MultipleChoice`: `interview.answer.<index>` (index, not raw key, to
dodge Slack's 255-char `action_id` cap and any author-supplied charset
surprises; selected key still rides in the button `value`)
- `interaction.rs`: `parse_interaction` accepts both the legacy
exact-prefix shape (in-flight buttons keep working across upgrade) and
the new suffixed shape, via a pre-computed `ANSWER_ACTION_ID_PREFIX_DOT`
constant so the parse hot path doesn't `format!` on every event.
- Tests: +5 in `interaction.rs` (suffixed yes/no, suffixed multi-choice,
legacy exact prefix, lookalike `interview.answers.yes` rejected,
prefix-sync assertion). Updated the existing block-builder tests to
assert uniqueness instead of the old single constant. One fixture each
in `dispatch.rs` and `connection.rs` updated to the suffixed shape; one
legacy fixture left in each to document backwards compatibility.

74/74 `fabro-slack` tests pass (was 69/69). `cargo +nightly-2026-04-14
fmt --check --all` and `cargo +nightly-2026-04-14 clippy -p fabro-slack
--all-targets -- -D warnings` clean.

## Verified end-to-end

Built a patched `fabro` binary, swapped it for the brew install,
triggered a fresh multi-choice `Approve Plan` gate against a real Slack
workspace, message rendered correctly in the configured channel with two
clickable `[A] Approve` and `[R] Revise` buttons. Before the patch, the
exact same gate produced zero Slack output and only the swallowed
`invalid_blocks` was visible via MITM.

## Suggested follow-up (separate concern, not in this diff)

The silent error swallow in `SlackService::handle_event` (`if let
Ok(posted) = self.client.post_message(...)`) is what hid this bug. Worth
logging at `WARN`. Mentioned in #251 as a separate item.

## Closes

Closes #251 if you choose to land this directly. Otherwise this PR is
just background material for the issue.
2026-05-13 07:32:43 -04:00
Bryan Helmkamp
6480c333ff
docs: accept outside pull requests
Switch the contribution policy from an issue-only model to welcoming
outside PRs. Small fixes go straight to a PR; larger changes start with
an issue or discussion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 07:27:26 -04:00