mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
293 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3c6a26e8c2
|
Merge pull request #607 from fabro-sh/feat/shared-checkout-parallel
Shared-checkout parallel execution |
||
|
|
3beaddc224
|
Merge remote-tracking branch 'origin/main' into feat/expose-model-controls
# Conflicts: # lib/foundation/fabro-api/src/lib.rs |
||
|
|
f2a8b4e989
|
Merge pull request #616 from fabro-sh/codex/expose-completion-token-usage
Expose detailed completion token usage |
||
|
|
cd706646c6
|
feat: treat resumed in-flight nodes as new stage executions
A node cancelled (or lost to a crash) mid-flight and then resumed now
starts a new stage execution with the next StageId ordinal (work@2)
instead of reusing and clearing the cancelled execution's projection.
The old execution stays immutable with its own events, session, output,
timing, billing, and termination state.
Engine:
- Add a run-scoped StageExecutionTracker on RunServices with per-node
high-water marks. Ordinals are reserved after the StageStart hook
passes on the first attempt (retries reuse the reservation), ensured
at the composite checkpoint pre-step for hook-skips, and reserved in
on_terminal_reached for terminal nodes' synthetic events.
- Keep three concepts distinct: graph visit (max_visits/checkpoints,
unchanged), stage execution ordinal (the @N in StageId), and handler
attempt. The tracker is not checkpointed; the append-only stage event
history is its durable source of truth.
- resume() seeds the allocator from the run projection and computes a
node -> StageId provenance map of executions observed after the
selected checkpoint, threaded through execute_persisted_run,
RunSession, and InitOptions.
Events and projections:
- stage.started, parallel.branch.started, and checkpoint.completed
carry optional graph_visit and resumed_from_stage_id; StageProjection
stores both. Old events deserialize with None and legacy duplicate
stage.started replays keep last-attempt behavior.
- The CheckpointCompleted reducer is envelope-first: diffs and
skipped-stage synthesis attach to the exact execution StageId, an
existing Retrying projection finalizes as Skipped without losing
identity, and historical node_outcomes no longer create or collide
with newer ordinals (node_visits remains a legacy fallback).
Handlers:
- Parallel fan-out reserves child ordinals through the shared tracker,
derives worktree pass{N} from the parent's execution ordinal, and
seeds branch contexts with explicit child stage scopes so branch
lifecycle and nested handler events agree.
- Artifact capture and manager-loop child logs follow the ordinal.
API and UI:
- RunStage documents visit as the execution ordinal and adds optional
graph_visit and resumed_from_stage_id; Rust and TypeScript clients
regenerated.
- The web sidebar lists both executions chronologically; resumed stages
show a "Resumed from" link in the stage detail header and hover
popover, with the graph visit surfaced when it diverges.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
4bd9753217
|
Expose model reasoning effort controls | ||
|
|
142862f342
|
Expose detailed completion token usage | ||
|
|
bb1afae363
|
feat(events): add backward cursor pagination | ||
|
|
673a7064fe
|
Validate completion reasoning effort | ||
|
|
4621149b6e
|
Merge remote-tracking branch 'origin/main' into feat/shared-checkout-parallel | ||
|
|
558c1010f8
|
Regenerate TS client to drop duplicated method from merge
The merge of main kept two copies of testProviderCredentials in the generated models-api.ts; regeneration is authoritative. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
85f3286c66
|
Merge branch 'main' into feat/shared-checkout-parallel | ||
|
|
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> |
||
|
|
84c5468722
|
Merge remote-tracking branch 'origin/main' into fix/cancellation-interrupt-lifecycle
# Conflicts: # lib/components/fabro-agent/src/subagent.rs # lib/components/fabro-agent/tests/it/parity_matrix.rs |
||
|
|
5cf1c7d183
|
Harden cancellation and interrupt lifecycles | ||
|
|
e5f0290229
|
fix(llm): send cache_control breakpoints for Claude via OpenRouter
Anthropic prompt caching is opt-in per request: without explicit ephemeral cache_control breakpoints in the body, no cache writes or reads ever happen. The OpenAI-compatible codec never emitted them, so every run on openrouter Claude models billed the full conversation at the uncached input rate on every turn (0 cache tokens on the billing page, confirmed by OpenRouter's activity portal). - Add a `cache_control_breakpoints` model feature declaring that a route only caches when the request marks the cacheable prefix; set it on the builtin OpenRouter Claude rows. Catalog build rejects the flag without `prompt_cache`. - Teach the Chat Completions wire shape a parts-form content variant so a message can carry the annotation; unmarked messages keep the plain-string form for compatibility with strict servers. - Mark the last system message (covers tools + system upstream) and the second-to-last user turn, counting tool results as user turns — mirroring the anthropic codec's placement so agent loops get incremental cache hits. - Extract the shared placement/opt-out policy into codec::cache and refactor the anthropic codec onto it; anthropic wire snapshots are unchanged. - Honor `provider_options.<name>.auto_cache = false` as an opt-out and consume the control key instead of merging it into the body. - Mirror the new feature through settings (fabro-config), the OpenAPI schema, and the generated TypeScript client. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
65cdf52061
|
feat: make model aliases provider-aware | ||
|
|
0244736b05
|
Resolve run.prepare.steps env and interpolation at the run boundary (#530)
## What
Per-step environment in `run.prepare.steps[].env` was parsed and then
**dropped** before it reached the resolved run settings, so prepare
steps could never see their declared env. This PR carries that env all
the way through to the executor, resolves prepare-step interpolation at
the run boundary, and fixes an argv-quoting bug.
Three things:
1. **Per-step env is carried through.** `RunPrepareSettings` now holds
`steps: Vec<PreparedStep>` (command plus per-step `env`) instead of a
flat `commands: Vec<String>`. The per-step env reaches `exec_command`,
which already accepts per-command env vars, and is merged on top of the
base sandbox environment.
2. **Interpolation resolves at the run boundary.** Prepare-step
`script`/`command` and per-step `env` values are carried in source form
out of the portable config resolve layer (so `fabro validate` stays
portable and never requires env to be set). Their `{{ env.* }}` tokens
resolve in the process that actually runs the steps, via
`RunPrepareSettings::resolve_step_env` — mirroring the existing MCP
transport env resolution. A missing env var is a **hard error**
(fail-closed); there is no fallback to the unresolved literal.
3. **Argv is shell-quoted.** Argv-style prepare steps were assembled
with `join(" ")`, so an argument containing spaces or quotes was
re-split by the shell. They are now shell-quoted per element with the
shared `shell_quote()` helper. `script` steps stay verbatim because they
are raw shell snippets.
## How
- `RunPrepareSettings.commands: Vec<String>` becomes
`RunPrepareSettings.steps: Vec<PreparedStep>` where `PreparedStep {
command, env }`. The server-side `{{ vars.* }}` substitution pass now
walks each step's command and env.
- New `RunPrepareSettings::resolve_step_env(env_lookup)` resolves `{{
env.* }}` in each step's command and env values, returning a hard error
on a missing var (and a loud `Unavailable` error for reserved
`secrets`/`inputs` tokens).
- The run boundary (`fabro_workflow::operations::start`) gains
`runtime_setup_commands`, the prepare-step counterpart to
`runtime_mcp_server`. `LifecycleOptions` now carries `Vec<SetupCommand>`
(command + env), and the initialize phase passes each step's env to
`exec_command`.
- `resolve_prepare` shell-quotes each argv element and carries per-step
env in source form. The stale lint suppression on the resolved fields is
rewritten to describe the deliberate source preservation that now
resolves at the run boundary.
- The shell-quoting helper moves to a shared `fabro_util::shell` module
(backed by `shlex`); `fabro_sandbox::shell_quote` delegates to it so the
config resolve layer and sandbox code share one audited implementation.
- The OpenAPI `RunPrepareSettings` schema and the generated TypeScript
client are updated to the new `steps`/`PreparedStep` shape.
## Testing
- `cargo build --workspace`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo nextest run` for `fabro-util`, `fabro-types`, `fabro-config`,
`fabro-sandbox`, `fabro-api`, `fabro-workflow`, `fabro-server`,
`fabro-cli` (provider keys stripped) — all green.
- `cd lib/packages/fabro-api-client && bun run typecheck` — clean.
New tests cover: per-step env carried through resolution; script/command
+ env resolved at the run boundary; a missing env var is a hard error
(in both the command and a per-step env value); reserved `secrets`
tokens surface as `Unavailable`; argv elements are shell-quoted (an arg
with spaces/quotes is correctly quoted) while a `script` stays verbatim;
and an end-to-end check that per-step env reaches the executed setup
command (with a negative control proving the success is attributable to
the per-step env).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
173968a780
|
feat(server): mcp-servers HTTP API — handlers + AppState wiring (#532)
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
## What
Adds the **mcp-servers HTTP API**: `GET/POST /api/v1/mcp-servers` and
`GET/PUT/DELETE /api/v1/mcp-servers/{id}` on top of the merged
`fabro-mcp-store` foundation and OpenAPI spec.
This includes the AppState wiring needed for the catalog to work end to
end: `McpServerStore` construction from `{active-config-dir}/mcps/`, an
`AppState` accessor, the `fabro-server` dependency, and route
registration for list/create/get/replace/delete handlers.
The API mirrors the automations concurrency pattern with ETags on
read/write responses and required `If-Match` headers for replace/delete.
## Resolved before merge
- **Credential-omitting read model:** read responses now return
`McpServerView` / `McpTransportView`, so stored env/header values are
not exposed by GET/list/create/replace responses. Responses include only
`env_keys` / `header_keys`; persisted values remain available to runtime
execution.
- **Manifest catalog references:** run manifest validation, graph
rendering, preflight, and run creation now resolve server-managed MCP
catalog references such as `[run.agent.mcps.<name>] id = "..."`.
- **Schema strictness:** unknown MCP transport fields are rejected,
aligning the reused Rust domain type with the OpenAPI
`additionalProperties: false` contract.
- **Create response headers:** the `POST /mcp-servers` 201 response now
documents its `ETag` header in OpenAPI.
## Follow-up intentionally left out
Credential-literal validation remains structural only: create/replace
currently accept literal env/header values and persist them for runtime
use. The warn-vs-hard-reject UX is a separate follow-up for the settings
UI; it is not a response-omission issue.
## Testing
Current PR checks are green:
- Rust: format, clippy, generated docs, Linux tests
- TypeScript: build, test, typecheck
Local checks run during the simplify/CI-fix pass:
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --locked --workspace --all-targets
-- -D warnings`
- `cargo nextest run -p fabro-config run_agent_mcps`
- `cargo nextest run -p fabro-mcp-store`
- `cargo nextest run -p fabro-api --test mcp_server_round_trip`
- `cargo build -p fabro-api`
- `cargo nextest run -p fabro-server --features test-support
system_sandbox_provider`
- `cargo nextest run -p fabro-server --features test-support --test it
mcp_servers`
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
a769336c39
|
feat(workflow): support overriding cwd for local sandbox provider (#467)
Some checks failed
Rust / Format (push) Has been cancelled
Rust / Clippy (push) Has been cancelled
Rust / Generated Docs (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
Rust / Test (Linux) (push) Has been cancelled
Rust / Test (macOS) (push) Has been cancelled
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
## Problem The `local` sandbox uses the run's `source_directory` (the CLI's cwd at invocation time) as its working directory and `create_dir_all`s it on the server (`LocalSandbox::initialize` in `fabro-sandbox`). That is correct when the CLI and the server share a host — the agent operates directly on the user's project tree. When the server is **remote** from the CLI — e.g. `fabro serve` running in a container in Kubernetes, driven over HTTP with the `local` sandbox — the client's cwd (e.g. `/Users/alice/project`) does not exist on the server. The sandbox then tries to create that path as the (often unprivileged) server user and fails at init: ``` sandbox.failed provider="local" error="Failed to create working directory" causes=["Permission denied (os error 13)"] ``` and the run dies with `workflow_error` before the agent starts. ## Fix When `source_directory` is absent or does not exist on the server, fall back to a server-writable `workspace` directory under the run's scratch dir instead of recreating the client path. **Same-host behavior is unchanged**: an existing `source_directory` is still used as-is. The selection is extracted into a small pure helper, `local_working_directory(source_directory, run_dir)`, so it can be unit-tested directly. ## Testing - `cargo test -p fabro-workflow local_working_directory` — 3 new tests (existing source dir → used; absent → fallback; present-but-missing-on-server → fallback) - `cargo check -p fabro-workflow` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Thanks for fabro @brynary! --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
bc0bda73a6
|
feat(web): add server-managed Environments CRUD settings UI (#462)
Some checks are pending
Rust / Clippy (push) Waiting to run
Rust / Format (push) Waiting to run
TypeScript / Build (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
## What Adds a CRUD interface for **server-managed Environments** at `/settings/environments`, driven by the `/api/v1/environments` REST API (list / create / retrieve / replace / delete), and reshapes how built-in environments are provisioned and protected. The page lives in the **Workflows** settings nav section (also introduced in this branch), positioned before Variables. ## Why The Environments REST API shipped (#453) but had no UI — environments could only be managed via the API/CLI. This gives operators a web UI alongside Variables and Secrets, and along the way tightens the model: environments are seeded at install time (not silently re-created on every boot), and the `default` fallback is an ordinary, deletable environment. ## Web UI **Pages & component** - `settings-environments.tsx` — list view: provider badge, image/resource summary, row actions (Edit/Delete). **"New environment" is a dropdown** of the enabled sandbox providers; the chosen provider is fixed for the environment's lifetime. - `settings-environments-new.tsx` / `settings-environments-edit.tsx` — create/edit flows; create reads the provider from a query param. - `environment-form.tsx` — shared form, reorganized: - **General** panel (merged identity + image): id, and an **image-source selector** (Image reference *vs* inline Dockerfile) that shows, requires, and sends only the selected, mutually-exclusive source. - **Resources**: CPU / memory / disk as **range sliders** (CPU 1–8, memory 1–16 GB, disk 1–20 GB), each always writing a concrete value. - **Environment variables** key/value editor. - **Advanced** progressive-disclosure section holding **Network** (a single "Block all network access" toggle — allow-all vs block) and **Lifecycle** (preserve / stop-on-terminal / auto-stop). Opens by default when any advanced value is non-default. - The in-form **provider control and the Labels editor were removed** — labels remain API-managed and are round-tripped untouched so UI edits never clear them. **Data layer**: `environmentsApi` client, `queryKeys.environments`, `useEnvironments` / `useEnvironment` SWR hooks. **Nav & routing**: "Environments" item in the Workflows section before Variables; routes registered in `router.tsx`. ## Backend: seed at install, deletable `default` - **Seeding moved to install time.** The server no longer seeds built-ins on startup; `EnvironmentStore::load_or_seed` → `load` (load-only). A new public `seed_environments(dir)` (idempotent, preserves operator edits) is called by both the web installer and the CLI installer. An uninstalled instance therefore has no managed environments, and a run selecting an absent environment fails explicitly (`unknown environment: default`) rather than resurrecting a built-in. - **`default` is no longer protected.** The delete guard and the `Protected` error variant are gone; deleting `default` succeeds (204) and removes the run fallback on purpose — forcing an explicit choice. `local` is unchanged (reserved, in-memory). - **`volumes` removed** from environment settings across the OpenAPI spec, generated Rust + TS clients, config layers, sandbox/server/workflow plumbing, docs, and tests. ## API contract details honored - Edit sends the environment `revision` as `If-Match`; 409 conflicts surface a "changed since you opened it" message. - The REST API accepts inline Dockerfiles only — the form never sends a Dockerfile path. ## Verification - Rust: `cargo build` (touched crates) ✅, `cargo nextest -p fabro-environment` 21/21 ✅, server env unit + `tests/it` integration 2/2 + 15/15 ✅, `clippy` (nightly, touched crates, all targets) clean ✅, `fmt --check` clean ✅. Full `--workspace` suite not run here — worth a CI pass. - Web: `bun run typecheck` ✅, `bun run build` ✅, `environment-form.test.ts` 5/5 ✅. Web suite: 512 pass / 1 unrelated pre-existing `RunDetail` failure. - **Not visually verified in-browser** — the local app is login-gated and automated loads redirect to `/login`; rendering of the form, the New-environment dropdown, and `default` delete should be confirmed in a logged-in session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (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> Co-authored-by: Release Repro <release-repro@example.com> |
||
|
|
23d3644787
|
feat(llm): catalog-estimated completion cost on Response (#494)
Standalone pre-OpenRouter step, pulled forward from the #438 triage (the gateway-refactor plan's "additive feature PR alongside the redo"): completion responses carry a USD cost with provenance. ## What's here **`Response.cost_usd` + `Response.cost_source`** — new optional fields (`skip_serializing_if` keeps the wire shape byte-identical when unset). `CostSource` (`authoritative` | `estimated`) lives in fabro-model's billing vocabulary next to `UsdMicros`/`TokenCounts`, since the API layer reuses it. **`fabro-llm/src/cost.rs`** — `estimate_cost_usd`, a thin wrapper over the existing `Catalog::price_tokens` billing machinery (billing-policy- and speed-aware), ported from #438's prototype with attribution. One fix over the prototype: model aliases and provider names are canonicalized before building the `ModelRef` — `ModelPricing::bill` rejects non-canonical refs, so the original would silently skip cost on alias requests (caught by a new test). **Client-level stamping** — one generic post-decode site instead of #438's ~8 per-adapter sites (which predate the codec refactor): `Client::complete` stamps blocking responses and `Client::stream` stamps `Finish` events, beneath the middleware chain so middleware observes final responses. Codecs stay wire-translation-only — zero wire-snapshot churn — and every registered adapter (including custom `register_provider` ones) gets the same treatment. Stamping never overwrites an existing cost, so future authoritative in-band costs (OpenRouter) take precedence by construction. **API surface** — `cost_usd`/`cost_source` on `CompletionResponse` (OpenAPI spec + handler + regenerated TS client). The streaming endpoint already carries cost implicitly since `Finish` events serialize the `Response` verbatim; this makes the blocking surface match. `CostSource` reuses the canonical fabro-model type via `with_replacement`, with the standard round-trip test pinning type identity and JSON parity. ## Deliberately not here (stays with the OpenRouter redo per the plan's hard rule) - Authoritative `usage.cost` parsing in the `openai_compatible` codec wire structs - Cached-token usage parsing (changes observable usage values) - Per-model `billing_policy` schema field ## Verification - `cargo nextest run --workspace --no-fail-fast`: 6701 passed; only the known 5 pre-existing environment-dependent fabro-workflow failures (identical on main) - All fabro-llm wire snapshots unmodified; new pins: cost estimation unit tests (incl. alias canonicalization), Client stamping tests (blocking, streaming, beneath middleware, no-catalog), fabro-api `CostSource` round-trip - clippy `-D warnings` + pinned-nightly fmt clean; `bun run typecheck` clean in fabro-web Independent of the route-vocabulary work in #493 — branches directly off main. After both land, the OpenRouter redo shrinks to config + typed codec params + authoritative-cost decode. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
d590122531
|
feat: chat-driven workflow builder at /playground (#450)
## Summary
Adds a new `/playground` route where users build a Fabro workflow by
chatting with Ask Fabro on the right while watching a live canvas
re-render on the left. The workflow can be downloaded as a `.fabro.zip`
or — eventually — launched as a real Fabro run; today the "Run for
real" button POSTs to `/api/v1/runs` and redirects to the resulting
`/runs/{id}` page, with a placeholder project/repo/folder picker.
The feature is built as a standalone component subtree under
`apps/fabro-web/app/components/playground/` with no `AppShell` or
`react-router` dependencies, so it can be re-embedded in other contexts
later by passing `chatEndpoint`, `authMode`, and an optional
`realRunRedirect` prop.
## What changed
**Frontend (`apps/fabro-web/`)**
- New `/playground` route + `<Playground>` component tree.
- Live SVG canvas via `@viz-js/viz` with click-to-inspect (read-only
node detail panel), pan, zoom, fit-to-window, and a simulated walk
through the graph driven by a Play button.
- Docked chat sidebar (assistant-ui) wired to the new
`/api/v1/playground/chat` endpoint, with auto-retry on parse failure
and a playground-specific tool-call summary that reads
`Wrote workflow.fabro (N nodes, M edges)`.
- File tabs (`workflow.fabro` / `workflow.toml` / `README.md`),
`.fabro.zip` download via `fflate`, and a "Run for real" toolbar
button that POSTs an inline `RunManifest` to `/api/v1/runs`.
- Draft persists across page refreshes via `localStorage`.
**Backend (`lib/crates/fabro-server/`)**
- New `POST /api/v1/playground/chat` SSE endpoint. Server is stateless
across turns: each request carries the full draft, the server runs
the LLM with a single `write_workflow_file` tool, streams
`StreamEvent` frames back, and lets the client own diffing/animating
the result into the canvas.
- Request-size caps before the LLM call (50 messages, 100 nodes, 200
edges) so a misbehaving or malicious client can't drag multi-MB
transcripts through token billing.
**Spec / wire contract**
- OpenAPI: new `playground/chat` operation + four new schemas
(`CreatePlaygroundChatRequest`, `PlaygroundWorkflowDraft`,
`PlaygroundWorkflowNode`, `PlaygroundWorkflowEdge`).
- `lib/packages/fabro-api-client` not regenerated yet (the playground
uses raw `fetch`); reviewers who want the TS client to pick up the
new types can run `bun run generate` in that package.
## Key design decisions
1. **Single `write_workflow_file` tool, not six per-op tools.** The
first cut exposed `add_node`/`update_node`/`connect`/etc. as
discrete tool calls. The model would routinely add nodes without
wiring them up, leaving the canvas in a broken half-state. Pivoted
to a single tool that takes the full new `workflow.fabro` content;
the browser parses the DOT, diffs it against the local draft, and
animates the resulting reducer ops in. The model only has to "get
the file right", and the canvas still paints node-by-node thanks
to the client-side animator.
2. **Stateless server.** Each chat turn POSTs the full current draft;
nothing is persisted server-side. Keeps the endpoint cheap, makes
refresh-resumption trivial (browser owns the truth), and means the
same endpoint can later sit behind a rate-limited anonymous variant
without growing per-session state.
3. **Standalone component subtree.** `<Playground>` has no
`AppShell`/router/store dependencies. All cross-cutting concerns
flow in as props (`chatEndpoint`, `authMode`, `realRunRedirect`).
This is the structural hook that makes future re-embedding possible
without a refactor.
4. **Chat is the only mutation path.** Click-to-inspect on the canvas
is read-only. Bi-directional canvas editing was explicitly cut from
scope to keep one source of truth for "how the workflow changed."
5. **Inline `RunManifest` instead of temp-dir-then-clone.** The
playground has no project to run against, so the `Run for real`
modal builds a `RunManifest` that carries the full DOT and
`workflow.toml` source inline (`workflows[key].{source, config}`).
`cwd` is pinned to a fixed `/tmp/fabro-playground` constant — no
LLM-controlled segment in a filesystem-looking field.
6. **React effects policy compliance.** All `useEffect` calls in
playground component code go through the existing primitives in
`app/hooks/effects.ts` (`useDocumentEvent`, `useInterval`) or a
purpose-named hook (`useCanvasRender`).
## Still outstanding (planned follow-ups)
- [ ] **Actually kicking off the ad-hoc run.** "Run for real" today
POSTs a manifest with a placeholder project/repo/folder
fieldset. The intent is to reuse the project-picker pattern
being introduced on the in-flight automations branch — once
that pattern lands, the disabled inputs in
`run-for-real-modal.tsx` become the live surface.
- [ ] **Header link to `/playground`.** No nav entry yet; users have
to type the URL directly.
- [ ] **Live SSE-driven canvas overlay** via
`GET /api/v1/runs/{id}/attach` — currently the modal redirects
to the standard run-view page; the "watch it build on the
playground canvas" experience comes when the `stage.*` events
are wired through.
- [ ] **Regenerate `lib/packages/fabro-api-client`** so the new types
ship to TS consumers.
- [ ] **Smoke test:** end-to-end download → unzip →
`fabro run <name>` round-trip.
- [ ] **`scripts/build.ts` dist-symlink bug:** `pruneOldBuilds` can
delete the directory `apps/fabro-web/dist` points at, which
pins the dev server in 503 "build in progress" forever.
Workaround documented; the real fix is a separate PR.
## Test plan
- [ ] `cd apps/fabro-web && bun run test app/components/playground/` —
111 tests pass
- [ ] `cd apps/fabro-web && bun run typecheck` — clean
- [ ] `cargo test -p fabro-server playground` — 6 tests pass
- [ ] Visit `/playground`; the canvas renders the welcome `start → ??? →
exit` ghost.
- [ ] Type "build me a release-notes workflow" in chat; nodes/edges
animate in; ack reads `Wrote workflow.fabro (N nodes, M edges)`.
- [ ] Click a node → inspector panel populates; click empty canvas →
deselects.
- [ ] Click `Simulate`; nodes light up `start → ... → exit` along the
resolved path.
- [ ] Click `Download .fabro`; unzip; `cd <unzipped> && fabro run
<name>` runs locally.
- [ ] Click `Run for real` → modal opens → confirm → POST succeeds →
redirected to `/runs/{id}` → run executes.
- [ ] Refresh the page; the draft persists from localStorage.
- [ ] Click `Start over` → `Yes`; canvas resets to welcome state.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
160f587a1d
|
feat(install): enable only allowed sandbox providers
Add an "Allow local sandboxes" checkbox (checked by default) below the Docker/Daytona choice in the web installer, and stop unconditionally enabling all three providers when generating settings.toml. The wizard now enables only the selected runtime plus local when allowed; the unselected runtime is written as `enabled = false` so the config resolver does not default it back on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fe1d33c041
|
Remove top-level automation enabled master gate (#456)
The top-level `enabled` flag on automations created a confusing
two-level activation model (automation-level + trigger-level). Since
automations are brand new with no existing data to migrate, the master
gate is removed entirely — trigger-level `enabled` is now the sole
activation control.
## What changed
**Domain model (`fabro-automation`):** `enabled` removed from
`Automation`, `AutomationDraft`, `AutomationReplace`, and
`PersistedAutomation`. `enabled_api_trigger()` no longer short-circuits
on the automation flag. The `default_true()` helper is gone. A new test
asserts that TOML with a top-level `enabled` key is rejected (no silent
compatibility path).
**Server handler:** Conflict detail updated from `"automation is
disabled or has no enabled API trigger"` → `"automation has no enabled
API trigger"`. The
`disabled_automation_run_endpoint_returns_conflict_code` test is
deleted; the trigger-disabled and missing-trigger tests remain as the
authoritative inactive-run coverage.
**OpenAPI + generated clients:** `enabled` removed from `Automation`,
`CreateAutomationRequest`, and `ReplaceAutomationRequest` schemas and
from the generated TypeScript interfaces. Trigger-level `enabled` on
`AutomationApiTrigger` and `AutomationScheduleTrigger` is untouched.
**Web UI:** `AutomationFormValues.enabled` and the "Enabled" toggle row
are gone. `isFormValid` no longer requires at least one enabled trigger.
`canRun` in the detail view is now just `apiTrigger?.enabled === true`.
The `StatusChip` component is removed. The automations list uses a new
`apiEnabled` field (derived from `hasEnabledApiTrigger`) to drive
run-button state and tooltip copy. A shared `lib/automation.ts` helper
centralises `findApiTrigger`, `findScheduleTrigger`, and
`hasEnabledApiTrigger` to avoid repeated inline `.find()` calls across
routes.
### Fabro Details
<details>
<summary>Ran 8 stages in 41m 34s for $17.84</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 15s | – | 0 |
| preflight_lint | 2m 25s | – | 0 |
| implement | 13m 0s | $9.25 | 0 |
| simplify_opus | 9m 43s | $6.18 | 0 |
| simplify_gpt | 3m 56s | $2.41 | 0 |
| verify | 9m 17s | – | 0 |
| **Total** | **41m 34s** | **$17.84** | **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>
|
||
|
|
037073d2b2
|
feat: Add Environment REST CRUD API under /api/v1/environments (#453)
## Summary
Adds a server-managed Environment CRUD API at `/api/v1/environments`,
modeled after the existing Automations API and backed by
`EnvironmentStore`. The API manages only server-side environment
definitions in `environments/*.toml`; client-side catalogs (workflow,
project TOML, run inputs) are unaffected.
### Plan Summary
- **OpenAPI contract**: new `Environments` tag, `EnvironmentId` path
parameter, five CRUD paths, list envelope, and REST-specific inline-only
image schema (`EnvironmentApiImageSettings`)
- **Server handler** (`environments.rs`): mirrors `automations.rs` —
auth guard, ETag/If-Match, and `EnvironmentStoreError → ApiError`
mapping
- **Shared handler utilities**: `parse_required_if_match` and
`json_with_etag_response` extracted from `automations.rs` into
`handler/mod.rs` so both modules share them
- **Inline-only Dockerfile enforcement**: `ApiDockerfileSource::Path` is
parsed and immediately rejected with `422`; the file is never read
- **Manifest refresh**:
`refresh_manifest_run_settings_from_environment_catalog()` called after
create, replace, and delete so `/system/info` and default run settings
stay consistent
- **Client regeneration**: TypeScript Axios client regenerated with
`EnvironmentsApi` and new model files; Rust `fabro-api` type aliases
updated
- **Tests**: integration suite in `tests/it/api/environments.rs`
covering all CRUD paths, error cases, and the manifest-refresh
invariant; OpenAPI conformance test verifies generated surfaces
## Key Design Decisions
**Inline-only Dockerfile at the REST boundary.** Allowing `path` sources
over REST would let callers silently read arbitrary server-local files
into the environment catalog. The handler recognizes the `path`
discriminant so it can return a descriptive `422` rather than a generic
parse error, but the payload is discarded via `IgnoredAny` — no disk
access occurs.
**Shared ETag utilities instead of per-handler helpers.** The original
`parse_required_if_match` and ETag header builder in `automations.rs`
were duplicated for environments. They're now generic over any `FromStr`
revision type in `handler/mod.rs`, making future resource handlers
cheaper to add.
**`Environment` response type aliased to domain type.** The
OpenAPI-generated `Environment` response struct is replaced with
`fabro_environment::Environment` via `build.rs` `with_replacement`. A
compile-time function-cast witness in
`fabro-api/tests/environment_round_trip.rs` confirms the alias holds.
Request types (`CreateEnvironmentRequest`, `ReplaceEnvironmentRequest`)
stay API-specific because their image schema differs from the
workflow/settings schema.
**Stale revision → `409`.** Consistent with Automations; `428` is
reserved for missing `If-Match` only.
### Fabro Details
<details>
<summary>Ran 8 stages in 59m 23s for $30.41</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 9s | – | 0 |
| preflight_lint | 2m 25s | – | 0 |
| implement | 25m 43s | $19.79 | 0 |
| simplify_opus | 14m 34s | $6.98 | 0 |
| simplify_gpt | 4m 39s | $3.64 | 0 |
| verify | 9m 14s | – | 0 |
| **Total** | **59m 23s** | **$30.41** | **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>
|
||
|
|
29a9a3f7d6
|
refactor: Remove inbound IP allowlisting (#443)
## Summary
Removes Fabro's in-process inbound source-IP allowlist entirely.
`[server.ip_allowlist]` and
`[server.integrations.github.webhooks.ip_allowlist]` are gone from
config parsing, resolved settings types, the OpenAPI spec, generated API
clients, and the Settings > Security UI. Existing `settings.toml` files
containing those keys now fail as unknown fields — this is a hard
removal with no migration path.
Network source restrictions should be enforced upstream via a reverse
proxy, firewall, VPN, Tailscale ACLs, Kubernetes ingress, or platform
policy.
### What changed
- **Config/types** (`fabro-config`, `fabro-types`): Removed
`ServerIpAllowlistLayer`, `ServerIpAllowlistOverrideLayer`,
`ServerIpAllowlistSettings`, `ServerIpAllowlistOverrideSettings`,
`IpAllowEntry`, associated resolver functions, GitHub `/meta` hook-range
parsing, and Unix socket trusted-proxy validation. `ipnet` dropped from
`fabro-types`; kept in `fabro-config` for sandbox CIDR validation.
- **Server runtime** (`fabro-server`): Deleted `ip_allowlist.rs`,
removed `IpAllowlistConfig` parameter from `build_router_with_options`
and `RouterOptions`, removed the global allowlist middleware layer, and
removed `GitHubMetaResolver` startup logic. GitHub webhook HMAC
verification is unchanged.
- **OpenAPI + generated clients**: Removed `ServerIpAllowlistSettings`,
`ServerIpAllowlistOverrideSettings`, `IpAllowEntry`,
`LiteralIpAllowEntry`, `GitHubMetaHooksEntry` schemas; removed
`ip_allowlist` from `ServerNamespace` and `IntegrationWebhooksSettings`;
dropped `IpAllowEntry` re-exports from `fabro-api`.
- **Web UI**: Removed IP allowlist row from Settings > Security; updated
nav description and page copy.
- **Docs/changelog**: Security docs explicitly state Fabro provides no
source-IP filtering and direct operators upstream. Changelog entry dated
2026-05-27 documents the breaking removal and annotates the 2026-04-19
entry where the feature was introduced.
### Also in this diff (unrelated to IP allowlisting)
The worker control stream was migrated from reading newline-delimited
JSON on stdin to a reconnecting WebSocket
(`/api/v1/runs/{id}/worker/control-stream`). This adds
`tokio-tungstenite` to `fabro-cli`/`fabro-server`, introduces
`WorkerControlManagerHandle` with backoff reconnection and deduplication
of replayed delivery IDs, and adds `RunPause`/`RunUnpause` message
handling. A new integration test
(`detached_run_cancel_reaches_worker_over_control_websocket`) exercises
the full cancel path over the WebSocket.
### Key decisions
- **Hard removal via `deny_unknown_fields`**: stale config is
immediately visible as a startup error rather than silently ignored.
- **No stub or default pass-through**: `IpAllowlistConfig::default()` is
gone, not left as a no-op wrapper, to avoid keeping the feature shape
alive.
- **Webhook HMAC boundary unchanged**: source-IP filtering on webhook
routes is removed; cryptographic signature verification remains the
security boundary.
### Fabro Details
<details>
<summary>Ran 9 stages in 59m 53s for $27.24</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 15s | – | 0 |
| preflight_lint | 2m 22s | – | 0 |
| implement | 33m 54s | $22.81 | 0 |
| simplify_opus | 5m 55s | $0.75 | 0 |
| simplify_gpt | 3m 35s | $2.81 | 0 |
| verify | 8m 34s | – | 0 |
| fixup | 2m 24s | $0.87 | 0 |
| **Total** | **59m 53s** | **$27.24** | **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>
|
||
|
|
a992a7d76b
|
feat(runs): allow retrying succeeded runs
Broaden manual retry eligibility to all unarchived terminal runs while preserving active and archived precondition failures. |
||
|
|
2d78f96107
|
Wire automation store into AppState and expose CRUD REST API (#439)
## Summary
Loads `AutomationStore` into `AppState` at server startup and exposes
five authenticated REST endpoints (`GET/POST /automations`,
`GET/PUT/DELETE /automations/{id}`) backed by the existing
`fabro-automation` crate.
### Plan Summary
- Add `fabro-automation` as a dependency of `fabro-server` and mount
`Arc<AutomationStore>` on `AppState`, computed from a sibling
`automations/` directory next to the active config file.
- Change `AutomationStore::load` from `async` to synchronous (`std::fs`)
so it can run before the Tokio runtime needs to make progress; malformed
files now fail startup instead of being silently skipped.
- Implement `src/server/handler/automations.rs` with shared helpers for
path-ID parsing, `If-Match` (quoted/unquoted) parsing, ETag formatting,
and `AutomationStoreError → ApiError` mapping.
- HTTP semantics: 201 on create, 404 on missing, 409 on duplicate or
stale revision, 422 on domain validation failure, 428 on missing
`If-Match`.
- Update `TestAppStateBuilder` to derive `active_config_path` from the
vault path so each test gets an isolated sibling `automations/`
directory; add `try_build()` to allow startup-failure assertions.
- Update the OpenAPI spec and generated TypeScript client to include
`AutomationListMeta` with a `total` field.
## Key design decisions
**Sync load path.** `AutomationStore::load` is now `fn` (not `async
fn`), using `std::fs`. A `#[expect(clippy::disallowed_methods)]`
annotation explains the rationale: this runs once at startup before the
runtime needs to yield, and avoids requiring a Tokio handle at the call
site in `build_app_state`.
**Fail-fast on malformed files.** Previously, corrupt TOML files were
logged as warnings and skipped. Now any parse or validation error during
load aborts server startup. The old `warn_load_failure` helper is
deleted; tests that relied on skip behaviour are replaced with tests
that assert `Err(AutomationStoreError::Parse { .. })` and
`Err(AutomationStoreError::InvalidFilename { .. })`.
**ETag / If-Match handling.** `parse_required_if_match` strips optional
surrounding quotes before parsing the revision, so both `"<rev>"` and
bare `<rev>` are accepted from clients. Missing `If-Match` on PUT/DELETE
returns **428 Precondition Required**, not 400.
**Test isolation.** `TestAppStateBuilder::build` now derives
`active_config_path` from `vault_path.with_file_name("settings.toml")`
instead of a random temp path, so the sibling `automations/` directory
is predictable and cleaned up with the same temp dir.
### Fabro Details
<details>
<summary>Ran 10 stages in 85m 20s for $33.66</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 19s | – | 0 |
| preflight_lint | 2m 5s | – | 0 |
| fix_lints | 33s | $0.15 | 0 |
| implement | 30m 33s | $17.35 | 0 |
| simplify_opus | 20m 27s | $11.82 | 0 |
| simplify_gpt | 6m 41s | $3.88 | 0 |
| verify | 15m 44s | – | 0 |
| fixup | 6m 8s | $0.46 | 0 |
| **Total** | **85m 20s** | **$33.66** | **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>
|
||
|
|
c767db897f
|
Add Automations API contract to OpenAPI spec and update generated clien… (#436)
## Summary
Defines the public Automations REST API contract in the OpenAPI spec,
updates the `RunSandbox` schema to reflect the new sandbox lifecycle
model, adds `fabro-automation` as a dependency to `fabro-api` for type
reuse, and removes the retired `fabro-devcontainer` crate and all
references to it.
## What changed
### Automations API (`fabro-api.yaml`)
Seven new paths under `/api/v1/automations` covering the full CRUD
surface plus run sub-resources:
```
GET/POST /automations
GET/PUT/DELETE /automations/{id}
GET/POST /automations/{id}/runs
```
New schemas: `Automation`, `AutomationTarget`, `AutomationTrigger`
(discriminated oneOf on `type`), `AutomationApiTrigger`,
`AutomationScheduleTrigger`, `CreateAutomationRequest`,
`ReplaceAutomationRequest`, `AutomationListResponse`.
Key contract decisions:
- `AutomationTrigger` uses an OpenAPI discriminator (`propertyName:
type`); unknown discriminator values → HTTP 422, not 400.
- `PUT` and `DELETE` require an `If-Match` header (428 if absent, 409 on
mismatch); `GET` and `PUT` responses carry an `ETag`.
- `POST /automations/{id}/runs` fires the automation's enabled API
trigger; 409 if the automation is disabled or lacks one.
- Run sub-resource responses reuse the existing `Run` and
`PaginatedRunList` schemas.
### `RunSandbox` schema refactor
The sandbox schema is restructured to express the full lifecycle rather
than only the ready state:
| Before | After |
|---|---|
| Flat object with `provider`, `image`, `snapshot`, `runtime` |
Discriminated by `kind`: `planned`, `initializing`, `ready`, `failed` |
| `runtime` was nullable | Moved into `RunSandboxInstance`
(non-nullable); present only when `kind = ready` |
| No failure detail | New `RunSandboxFailure` schema with `error`,
`causes`, `duration_ms` |
`SandboxDetails.sandbox` now references `RunSandboxInstance` (the ready
state), which preserves the existing shape for the details endpoint
while the richer `RunSandbox` type appears on run responses.
### Web UI (`run-sandbox-lifecycle.ts`)
New helper module that bridges the old flat-object sandbox wire shape
and the new lifecycle-keyed shape, with display metadata for each
lifecycle state. Consumers (`RunSummaryPanel`, `TerminalView`,
`RunSandbox` route, `run-detail` header/tabs) updated to route through
these helpers so both old and new wire shapes are handled transparently.
### `fabro-devcontainer` removal
The `fabro-devcontainer` crate is removed from `Cargo.lock`,
`AGENTS.md`, nextest config, and all doc references. Public-facing
changelog entries for devcontainer-specific features are removed or
retitled.
### Plan Summary
- Add Automations CRUD + run sub-resource paths and schemas to the
OpenAPI spec
- Restructure `RunSandbox` schema to model lifecycle states (`planned →
initializing → ready | failed`)
- Add `fabro-automation` dependency to `fabro-api` for domain-type
reuse; add JSON parity round-trip tests
- Regenerate Rust API types and TypeScript client
- Remove `fabro-devcontainer` crate and all references
- Add `run-sandbox-lifecycle.ts` helper module in the web UI and update
all sandbox-state consumers
### Fabro Details
<details>
<summary>Ran 9 stages in 83m 29s for $35.47</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 22s | – | 0 |
| preflight_lint | 2m 40s | – | 0 |
| implement | 45m 36s | $28.67 | 0 |
| simplify_opus | 7m 46s | $2.46 | 0 |
| simplify_gpt | 6m 7s | $3.92 | 0 |
| verify | 12m 8s | – | 0 |
| fixup | 5m 52s | $0.43 | 0 |
| **Total** | **83m 29s** | **$35.47** | **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>
|
||
|
|
e18772888e
|
Model run sandbox lifecycle explicitly (#431)
## Summary Fixes sandbox state reporting by separating a requested sandbox plan from an initialized sandbox instance. Runs now project sandbox lifecycle as `planned`, `initializing`, `ready`, or `failed`, and live sandbox operations only proceed once a real instance exists. ## Changes - Introduces `RunSandboxPlan`, `RunSandboxInstance`, and lifecycle-backed `RunSandbox` domain types, with serde validation that prevents `ready` sandboxes without an instance. - Updates store projection behavior so sandbox events transition through planned, initializing, ready, and failed states while preserving requested provider/image/snapshot separately from runtime metadata. - Tightens server sandbox handlers so details/files/services/terminal/VNC helpers require an initialized instance and return a clear 404 when the sandbox was never created. - Updates the OpenAPI contract and regenerated clients so `Run.sandbox` exposes lifecycle state while `SandboxDetails.sandbox` contains only initialized instance metadata. - Updates the web UI to render lifecycle state directly from run summaries, hide the Sandbox tab for pure planned sandboxes, and disable sandbox controls until the instance is ready. - Cleans up duplicated lifecycle display/type logic and duplicate server-side sandbox instance loading found during review. | Lifecycle state | Meaning | Live controls | | --- | --- | --- | | `planned` | Sandbox was requested but no provider instance exists | Hidden/disabled | | `initializing` | Provider setup has started | State view only | | `ready` | Runtime instance exists | Enabled | | `failed` | Provider setup failed with error details | State view only | ## Testing - `cargo check --workspace` - `cargo +nightly-2026-04-14 fmt --check --all` - `git diff --check` - `cd apps/fabro-web && bun run typecheck` - `cd apps/fabro-web && bun test app/routes/run-detail.test.ts app/routes/run-sandbox.test.tsx app/components/run-summary-panel.test.tsx` - `cargo nextest run -p fabro-types --test sandbox_model_serde` - `cargo nextest run -p fabro-store run_created_projects_planned_sandbox_lifecycle sandbox_lifecycle_events_update_projected_sandbox_state run_failed_before_sandbox_events_leaves_sandbox_planned` - `cargo nextest run -p fabro-server planned_sandbox_returns_404_from_details_endpoint planned_sandbox_rejects_live_operations failed_sandbox_rejects_live_operations local_sandbox_returns_provider_neutral_details` - `cargo nextest run -p fabro-api --test run_sandbox_round_trip` - `cargo nextest run -p fabro-api --test sandbox_details_round_trip` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
e13de9faaf
|
feat(automation): persist automation refs on runs (#428)
## Summary Adds durable automation metadata to workflow runs so automation-triggered runs can carry their automation and trigger references through creation, stored events, projections, summaries, fork, retry, and API surfaces. This also introduces the new `fabro-automation` crate with typed automation IDs, TOML parsing/validation, revision hashing, and a file-backed automation store. The store avoids overwriting malformed existing TOML files on create and keeps read access from being blocked by mutation disk I/O. ## Changes - Add `AutomationRef` propagation through `RunSpec`, `run.created`, store projections, summaries, fork, retry, and related tests. - Add `fabro-automation` domain/store crate for automation TOML definitions, trigger validation, revisions, create/replace/delete, and load behavior. - Update OpenAPI and regenerated TypeScript client types for `RunSpec.automation` and `AutomationRef.trigger_id`. - Add API/type regression coverage for the new automation fields. - Harden automation store create semantics so skipped malformed files still reserve their path. ## Verification - `cargo nextest run -p fabro-automation` - `cargo +nightly-2026-04-14 clippy -p fabro-automation --all-targets -- -D warnings` - `cargo nextest run -p fabro-api` - `cargo nextest run -p fabro-types run_spec_round_trips_templated_settings run_created_props_round_trip_templated_settings` - `cd lib/packages/fabro-api-client && bun run typecheck` - `cargo +nightly-2026-04-14 fmt --check --all` - `git diff --check` |
||
|
|
ec1b3f2084
|
feat(sandbox): secure daytona snapshot names (#429)
## Summary Secures Daytona custom snapshot creation by removing user-controlled snapshot/image references and replacing them with deterministic names Fabro computes internally. Docker image selection now uses `image.docker`, while Daytona only accepts `image.dockerfile` for custom snapshots and continues to use `daytona-medium` when no Dockerfile is configured. ## Changes - Replaces public `image.ref` config/API shape with Docker-specific `image.docker` across Rust settings, OpenAPI, generated TypeScript client, docs, defaults, examples, and web samples. - Adds Daytona snapshot identity generation using HMAC-SHA256 over a canonical manifest keyed by the Daytona API key, producing `fabro-<uuid>` snapshot names without exposing Dockerfile text or key material. - Routes Daytona custom Dockerfiles, including devcontainer-generated Dockerfiles, through the same computed identity path before calling Daytona snapshot APIs. - Updates sandbox initialization events and store projections so initialized run state can show the resolved image and computed Daytona snapshot after startup. - Updates legacy config migration behavior so Docker image refs map to `image.docker`, while Daytona legacy snapshot names are not preserved. ## Breaking Changes - `image.ref` is no longer accepted in new environment config. - Docker environments should use `image.docker` for image selection. - Daytona environments reject `image.docker`; use `image.dockerfile` to request a custom computed snapshot. ## Verification - `cargo build -p fabro-api` - `cd lib/packages/fabro-api-client && bun run generate` - `cd lib/packages/fabro-api-client && bun run typecheck` - `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` - `ulimit -n 4096 && cargo nextest run --no-fail-fast -p fabro-cli -p fabro-config -p fabro-sandbox -p fabro-workflow -p fabro-store -p fabro-server -p fabro-api` - `cargo insta pending-snapshots` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
b1bd2f522c
|
feat(server): add variables API (#430)
## Summary
Adds a workflow-visible variables store and HTTP API for managing
non-sensitive run variables, then wires those variables into run config
interpolation before run creation, validation, and preflight.
## What Changed
- Adds `/api/v1/variables` CRUD endpoints backed by a JSON variable
store and generated Rust/TypeScript API types.
- Supports `{{ vars.NAME }}` interpolation alongside existing `{{
env.NAME }}` handling for run-owned config fields, including
environment, MCP, hook, artifact, checkpoint, SCM, and notification
settings.
- Reuses canonical `fabro-types` variable DTOs in `fabro-api` and adds
OpenAPI name patterns so clients see the same env-style variable
contract enforced by the server.
- Keeps variable updates store-owned with `update_existing`, avoiding
duplicated not-found/update semantics in the HTTP handler.
- Shares env-style name validation between variables, interpolation
parsing, and vault token names to avoid grammar drift.
Variables are intentionally non-sensitive: list/get responses include
values, unlike vault secrets.
## Validation
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo test -p fabro-types`
- `cargo test -p fabro-variable`
- `cargo test -p fabro-api --test variable_round_trip`
- `cargo test -p fabro-server --features test-support --test it
api::variables`
- `cargo +nightly-2026-04-14 clippy -p fabro-types -p fabro-variable -p
fabro-vault --all-targets -- -D warnings`
- `cargo +nightly-2026-04-14 clippy -p fabro-server --features
test-support --all-targets -- -D warnings`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex/)
|
||
|
|
ab0f94fd82
|
feat(system): report runtime integration status (#416)
## Summary Settings > Integrations now reflects the server's actual integration readiness instead of only static `settings.toml` booleans. This adds `/api/v1/system/integrations` as the runtime source of truth, covering server config, vault credential presence, and Slack Socket Mode connection state. ## What Changed - Added shared `fabro-types` integration status models and reused them from `fabro-api` to avoid duplicate API/domain types. - Added `GET /api/v1/system/integrations` to the OpenAPI spec, Rust server routes, demo routes, and generated TypeScript client. - Reports GitHub and Slack status as `disabled`, `missing_credentials`, `configured`, `connecting`, `connected`, or `error`, with non-secret metadata and missing credential names. - Tracks Slack Socket Mode runtime state from the Slack connection loop and respects explicit `server.integrations.slack.enabled = false` even when vault tokens exist. - Updated the Integrations settings page to read the new runtime endpoint, so a vault-configured Slack setup no longer appears simply as disabled. ## Verification - `cargo build -p fabro-api` - `cargo nextest run -p fabro-api system_integrations` - `cargo nextest run -p fabro-config resolved_server_integrations_are_slack_only_for_chat` - `cargo nextest run -p fabro-slack run_event_loop_notifies_connected_status` - `cargo nextest run -p fabro-server --features test-support --test it get_system_integrations` - `cargo nextest run -p fabro-server` - `cargo +nightly-2026-04-14 fmt --check --all` - `cd apps/fabro-web && bun test app/routes/settings-integrations.test.tsx app/lib/query-keys.test.ts` - `cd apps/fabro-web && bun run typecheck` - `cd apps/fabro-web && bun run build` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
bd72570437
|
Add provider-backed sandbox inventory API and rename SandboxProvider to… (#409)
## Summary
Exposes `GET /api/v1/sandboxes` and `GET /api/v1/sandboxes/{id}`
endpoints that query sandbox inventory directly from configured
providers (Docker, Daytona), independent of run projections. Also
renames the existing `SandboxProvider` enum to `SandboxProviderKind`
throughout the codebase to free the name for the new `SandboxProvider`
trait.
### Plan Summary
- **OpenAPI + types**: New `SandboxInfo`, `SandboxListResponse`,
`SandboxListMeta`, `SandboxProviderLookupError`, and
`SandboxProviderKind` schemas added to the API spec; canonical Rust DTOs
added to `fabro-types`.
- **Provider trait and registry**: `SandboxProvider` trait (`list`,
`get`, `create`, `delete`) and `SandboxProviderRegistry` introduced in
`fabro-sandbox/src/provider.rs`. Registry fans out calls across all
configured providers and implements fail-soft semantics for list and
conflict/unavailable detection for get.
- **Provider implementations**: `DockerSandboxProvider` uses Bollard
label-filtered container listing and per-inspect;
`DaytonaSandboxProvider` uses the SDK with paginated label-filtered
listing. Both verify `sh.fabro.managed=true`.
- **Shared detail mapping**: Docker and Daytona inspect-to-`SandboxInfo`
paths extracted into `docker_info_from_inspect` /
`daytona_info_from_sdk_sandbox` so run-scoped `SandboxDetails` and
inventory `SandboxInfo` share the same normalization logic.
- **Monitoring UI**: `RunsInfo` now exposes `scheduler_slots_used`; the
monitoring panel displays "slots used" instead of the raw active-run
count.
## What changed and why
**`SandboxProvider` → `SandboxProviderKind`** is a mechanical rename
across ~20 call sites so the unqualified name `SandboxProvider` can be
claimed by the new trait without collision.
**Registry lookup semantics** for `get_managed_by_native_id`:
| Outcome | HTTP |
|---|---|
| Exactly one provider matches | `200` |
| All providers succeed, none match | `404` |
| Two or more providers match the same id | `409` |
| No match + at least one provider failed | `502` |
List is always fail-soft: partial results are returned and failing
providers appear in `meta.provider_errors`.
**`DockerFields` / `DaytonaFields` structs** were introduced inside
`details.rs` to hold the shared normalization output. Both
`map_docker_inspect` (run-scoped) and `docker_info_from_inspect`
(inventory) now delegate to `docker_fields_from_inspect`, eliminating
duplicate field-extraction logic. Same pattern for Daytona.
**`futures` moved from optional to unconditional** in
`fabro-sandbox/Cargo.toml` because `join_all` / `try_join_all` are now
used in `provider.rs`, which is not feature-gated.
**`local` provider** intentionally returns an empty list and `None` for
get — it has no provider-managed inventory.
### Fabro Details
<details>
<summary>Ran 8 stages in 102m 54s for $41.81</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 20s | – | 0 |
| implement | 56m 40s | $10.82 | 0 |
| simplify_opus | 27m 50s | $26.24 | 0 |
| simplify_gpt | 5m 2s | $4.76 | 0 |
| verify | 8m 24s | – | 0 |
| **Total** | **102m 54s** | **$41.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>
|
||
|
|
bd837fc0f0
|
Add POST /api/v1/providers/test endpoint (#406)
## Summary
Adds `POST /api/v1/providers/test` so API, CLI, and UI callers can check
LLM provider health without parsing `/health/diagnostics`. The endpoint
tests every configured provider once using the catalog probe model and
returns typed, per-provider results with an aggregate summary — all at
HTTP 200, with provider failures expressed in the body.
This PR also adds `scheduler_slots_used` to `SystemRunCounts` to
distinguish runs occupying concurrency slots from all "active" runs
(e.g. runs blocked waiting for human input count as active but do not
hold a scheduler slot).
### What changed and why
**Provider probe logic** (`diagnostics.rs`)
The inline probe loop inside `check_llm_providers` was extracted into
`test_llm_providers` / `probe_single_provider`, which both the new
endpoint and the existing diagnostics check now share. The extraction
preserves the diagnostics output format: `diagnostic_detail` (a
`#[serde(skip)]` field) carries the richer context string used for the
`LLM Providers` section, while `error_message` carries the redacted,
public-facing error.
Key decisions:
- `ProviderProbeStatus` is `ok | error` only — no `skip`, because v1
only iterates configured providers.
- `model_id` is nullable so auth/registration failures (where no probe
was sent) can be expressed cleanly.
- API key values appearing in upstream error responses are passed
through `redact_string` before being stored in `error_message`.
**Route** (`handler/models.rs`)
`.route("/providers/test", post(test_providers))` added alongside
`/providers`, protected by the same `RequiredUser` extractor.
**`scheduler_slots_used`** (`handler/system.rs`, `server.rs`)
The status predicate (`Starting | Running | Blocked | Paused`) was
already duplicated between the scheduler loop and `get_system_info`.
It's now a named function `counts_toward_scheduler_capacity`, used in
both places and in the new `SystemRunCounts` field. The web UI
monitoring panel was updated to display "slots used" instead of
"active."
**Generated clients**
OpenAPI spec updated; Rust and TypeScript clients regenerated. New
TypeScript types: `ProviderTestList`, `ProviderTestResult`,
`ProviderTestStatus`, `ProviderTestSummary`.
### Plan Summary
- Add `testProviders` OpenAPI operation and `ProviderTestList` /
supporting schemas to `fabro-api.yaml`.
- Extract shared `test_llm_providers` from `check_llm_providers` in
`diagnostics.rs`; keep diagnostics output identical.
- Wire `POST /providers/test` handler in `handler/models.rs`.
- Add `scheduler_slots_used` to `SystemRunCounts` and extract
`counts_toward_scheduler_capacity` predicate.
- Regenerate Rust and TypeScript API clients.
- Add integration tests covering: no providers, successful probe, auth
failure (no upstream call), registration failure, mixed catalog order,
and API key non-leakage.
### Fabro Details
<details>
<summary>Ran 8 stages in 53m 33s for $40.83</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 8s | – | 0 |
| preflight_lint | 2m 19s | – | 0 |
| implement | 23m 56s | $30.73 | 0 |
| simplify_opus | 12m 53s | $5.80 | 0 |
| simplify_gpt | 2m 40s | $4.30 | 0 |
| verify | 9m 5s | – | 0 |
| **Total** | **53m 33s** | **$40.83** | **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>
|
||
|
|
02a87cb650
|
fix(settings): show scheduler slot usage (#404)
## Summary Fixes the Settings Resources concurrency meter so it reports scheduler capacity usage instead of all non-terminal runs. `/api/v1/system/info` now exposes `runs.scheduler_slots_used`, computed from the same status predicate the scheduler uses, while `runs.active` remains unchanged for existing lifecycle semantics. The settings page uses only the new slot count, so pending approval runs and runnable queued runs no longer make the concurrency meter look full. ## Verification - `cargo build -p fabro-api` - `cargo nextest run -p fabro-server --features test-support worker_started_child_run_requires_approval_before_becoming_runnable` - `cargo nextest run -p fabro-server --features test-support scheduler_capacity_counts_only_runs_occupying_slots` - `cargo nextest run -p fabro-server --features test-support get_system_info_returns_runtime_fields` - `cargo nextest run -p fabro-server --features test-support test_app_state_with_options_respects_max_concurrent_runs` - `cargo nextest run -p fabro-server --features test-support openapi_conformance` - `bun test app/routes/settings-monitoring.test.tsx` - `bun run typecheck` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 (context unknown, reasoning unknown) via [Codex](https://openai.com/codex) |
||
|
|
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> |
||
|
|
acf8caa351
|
feat(web): add sortable Size column to runs list
Surfaces the run t-shirt size (XS/S/M/L/XL) in both the main runs list and the Children sub-tab, visible by default. L renders in amber and XL in coral to flag risky and unhealthy runs at a glance. Extracts a shared SizeChip component used by the run header and the table cell, derives Ord on RunSize so the new sort key (server-side ListRuns sort) orders by bucket, and reorders TOGGLEABLE_COLUMNS so the column picker mirrors the visible table order. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2c0416e1c5
|
feat: add server sandbox provider enablement policy (#389)
Operators can now disable individual sandbox providers at the server
level via `[server.sandbox.providers.<provider>]` in `settings.toml`,
without breaking existing deployments that omit the section entirely.
## What changed
**Config layer & resolution** (`fabro-config`, `fabro-types`): new
sparse `ServerSandboxLayer` / `ServerSandboxProvidersLayer` /
`ServerSandboxProviderLayer` structs with `deny_unknown_fields` parse
validation. Resolution defaults every missing level to `enabled = true`.
The resolved `ServerSandboxSettings` / `ServerSandboxProvidersSettings`
/ `ServerSandboxProviderSettings` types live in `fabro-types` and are
shared by all consumers.
**Policy enforcement** (`fabro-server`): three check points enforce the
effective provider (after dry-run Local coercion):
1. `POST /api/v1/runs` — 400 at admission.
2. `POST /api/v1/runs/preflight` — `ok: false` with a `Sandbox Provider
Policy` error check.
3. Launch (`execute_run_in_process` / `execute_run_subprocess`) —
fail-before-execution with a `LaunchFailed` reason.
The dry-run coercion logic was extracted into
`SandboxProvider::effective_for(mode)` on the type itself and reused
across `fabro-server` and `fabro-workflow`.
**Installer** (`fabro-install`): `write_sandbox_settings` now always
writes all three provider policy tables with `enabled = true`, so
generated `settings.toml` files are self-documenting.
**API schema & clients**: `ServerNamespace` gains a required `sandbox`
field in the OpenAPI spec; three new TypeScript model files were
regenerated accordingly.
### Plan Summary
- Task 1: config layer structs → resolved types → resolver helpers →
tests
- Task 2: `effective_sandbox_provider` + `sandbox_provider_policy_error`
helpers; admission, preflight, and launch checks + integration tests
- Task 3: installer writes all three provider entries; install finish
tests updated
- Task 4: OpenAPI schema, `fabro-api` build mappings, TS client
regeneration, docs
### Fabro Details
<details>
<summary>Ran 8 stages in 59m 15s for $45.70</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 5s | – | 0 |
| preflight_lint | 2m 21s | – | 0 |
| implement | 27m 55s | $38.88 | 0 |
| simplify_opus | 14m 20s | $4.93 | 0 |
| simplify_gpt | 3m 14s | $1.88 | 0 |
| verify | 8m 49s | – | 0 |
| **Total** | **59m 15s** | **$45.70** | **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>
|
||
|
|
7d655d7c95
|
feat: expose effective agent tool list via StageProjection.agent_tools (#388)
## Summary
Adds `StageProjection.agent_tools` — a replay-authoritative list of
every tool the model can actually call — so UI and API consumers no
longer have to infer tool availability from `permission_level`. The
field is populated by a new `agent.tools.available` durable event
emitted once per stage session after provider-profile setup, MCP
integration, and access-policy filtering are complete.
### Plan Summary
- **Types (`fabro-types`)** — new `AgentToolSummary`, `AgentToolSource`,
`AgentToolCategory`, `AgentToolsAvailableProps`, and
`EventBody::AgentToolsAvailable`; `agent_tools: Vec<AgentToolSummary>`
added to `StageProjection` with skip-serializing-if-empty semantics.
- **Tool registry (`fabro-agent`)** — `ToolSource::Mcp` gains
`original_name` (no more re-parsing the qualified name downstream);
`ToolDefinitionWithSource::to_agent_tool_summary()` maps to the public
DTO; `Session::effective_tools()` / `agent_tool_summaries()` expose the
filtered list; `tool_category` split into `tool_category` (CLI gate,
defaults `Shell`) and `known_tool_category` (projection, returns `None`
→ `Other` for unknown tools).
- **Projection reducer (`fabro-store`)** — `AgentToolsAvailable`
replaces the stage's `agent_tools`; `AgentToolStarted` flips `invoked =
true` on the matching entry; legacy runs without the event get an empty
list.
- **OpenAPI + generated clients** — `AgentToolSummary`,
`AgentToolSource`, `AgentToolCategory`, `AgentToolsAvailableProps`
schemas added; `StageProjection.agent_tools` field added; `build.rs`
replacements wire them to the `fabro-types` structs.
- **Web sidebar** — new collapsible "Tools" section renders name,
description, source/category badge, and used/available state from
`stage.agent_tools`; `permission_level` is kept as secondary fallback
metadata for legacy stages.
## Key design decisions
- **`agent_tools`, not `tools`** — avoids ambiguity with MCP nested
tools and completion API tool definitions.
- **Dedicated `agent.tools.available` event** — cleaner than overloading
`agent.session.activated`; replacement semantics on replay mean
re-emission works if tool registration ever becomes mutable.
- **`original_name` carried in `ToolSource::Mcp`** — stored by the MCP
integration at registration time so the projection never needs to
re-parse qualified names like `mcp__filesystem__read_file`.
- **`invoked` is projected state, not event state** — the availability
event always emits `false`; replay of `agent.tool.started` flips
matching entries.
- **Parameter schemas omitted** — `AgentToolSummary` carries only
`name`, `description`, `source`, `category`, and `invoked` to keep
payloads small and avoid exposing implementation detail.
- **`AgentToolCategory::Other` for unknown tools** — unlike the CLI
permission gate (which defaults to `Shell` to require approval), the
projection uses `Other` to surface unrecognized MCP/skill tools
accurately.
### Fabro Details
<details>
<summary>Ran 8 stages in 56m 37s for $58.75</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 0s | – | 0 |
| preflight_lint | 2m 15s | – | 0 |
| implement | 22m 54s | $42.64 | 0 |
| simplify_opus | 16m 41s | $10.62 | 0 |
| simplify_gpt | 3m 43s | $5.49 | 0 |
| verify | 8m 33s | – | 0 |
| **Total** | **56m 37s** | **$58.75** | **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>
|
||
|
|
98c26d5370
|
Fold context-window data into agent.message, remove snapshot event (#390)
## Summary
Removes the standalone `agent.context_window.snapshot` event and instead
attaches the context-window projection directly to `agent.message`. This
eliminates the async provider token-count API calls that the old
approach required, and simplifies the event log to a single event type
carrying all post-response agent data.
## What Changed and Why
**Before:** After each LLM turn, the agent emitted a separate
`agent.context_window.snapshot` event — first a local estimate, then
potentially a second one after an async `count_input_tokens` call
resolved (or after response usage arrived). This required fingerprint
deduplication state, a `close_token` to cancel in-flight counts, and
frontend handling for the extra event type.
**After:** The `AgentEvent::AssistantMessage` variant carries an
`Option<StageContextWindowProjection>`. The projection is computed
locally at request-build time and then refined using response token
usage when available (`ResponseUsageScaledBreakdown`), or kept as a
`LocalEstimate` when response usage is absent. No provider API calls are
made.
### Plan Summary
- **Task 1:** Added `context_window:
Option<StageContextWindowProjection>` to `AgentMessageProps` (Rust types
+ OpenAPI), removed `AgentContextWindowSnapshotProps` and
`EventBody::AgentContextWindowSnapshot`.
- **Task 2:** Removed the spawned `count_input_tokens` task,
`close_token`, fingerprint sets, and both snapshot-emit methods from
`Session`. Added `context_window_from_response_usage` to
`context_window.rs`; `BuiltRequest` now holds the local projection
instead of the tool list.
- **Task 3:** Workflow conversion copies `context_window` from
`AgentEvent::AssistantMessage` into `AgentMessageProps`; store reducer
reads it from `AgentMessage` instead of the removed snapshot variant and
stamps `event_seq`.
- **Task 4:** GET endpoint tests updated to seed data via
`agent.message` with embedded context-window; endpoint behavior
unchanged.
- **Task 5:** Frontend constant and tests for
`agent.context_window.snapshot` removed; `agent.message` already
invalidates `stageContextWindow` through existing stage-activity
handling. TypeScript client regenerated with the new `AgentMessageProps`
model.
### Key Design Decisions
- **No provider token-count API calls** during normal execution —
context-window accuracy relies on local estimates scaled by response
usage, which is always available for successful turns.
- **Failed-before-response turns** emit no context-window data
(`context_window: None`), matching the old behavior where a snapshot
would have been emitted but response-usage scaling would never arrive.
- `BuiltRequest` drops the `tools` field (only needed for the
now-removed snapshot emission path); the local projection is computed at
build time and stored directly.
### Fabro Details
<details>
<summary>Ran 8 stages in 60m 3s for $55.78</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 1s | – | 0 |
| preflight_lint | 2m 16s | – | 0 |
| implement | 30m 39s | $44.82 | 0 |
| simplify_opus | 10m 48s | $4.03 | 0 |
| simplify_gpt | 5m 1s | $6.93 | 0 |
| verify | 8m 47s | – | 0 |
| **Total** | **60m 3s** | **$55.78** | **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>
|
||
|
|
c19cedaede
|
Link unconfigured providers to prefilled secret form
On /settings/models, unconfigured providers now offer "Add secret →" alongside "Get API key →", deep-linking to /settings/secrets/new with the expected vault secret name prefilled. Driven by a new `expected_secret_name` field on the Provider API, derived from the first vault credential in the catalog so the suggestion stays in sync with the catalog instead of being hardcoded on the frontend. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
04e169ef79
|
Add POST /api/v1/runs/delete batch delete endpoint (#382)
## Summary
Adds a fail-soft batch delete endpoint (`POST /api/v1/runs/delete`) that
mirrors the existing archive/unarchive batch pattern, processes 1–250
run IDs independently, and returns per-item outcomes with an aggregate
summary. Existing `DELETE /api/v1/runs/{id}` behavior is unchanged.
### Plan Summary
- **OpenAPI-first**: new
`BatchDeleteRunsRequest/Response/Result/Summary` schemas added to the
spec; Rust (`fabro-api`) and TypeScript (`fabro-api-client`) clients
regenerated.
- **Delete internals refactored**: `DeleteRunOutcome` gains `Deleted`
and `AlreadyAbsent` variants (replacing the old `NoContent`);
`delete_run_internal` and its helpers now return `Result<_, ApiError>`
instead of `Result<_, Response>`, enabling both the single-delete
handler and the new batch handler to reuse the same logic.
- **Batch handler**: `batch_delete_runs` in `lifecycle.rs` validates the
request (reusing the generalized `validate_batch_run_ids`), loops over
IDs, and assembles `BatchDeleteRunsResult` items mapping
`ApiError::status()` to outcome strings (`conflict`, `error`).
- **Web helper**: `deleteRuns` added to `run-actions.ts` alongside
`archiveRuns`/`unarchiveRuns`, with the same `as unknown as` cast needed
for the openapi-generator `Set<string>` quirk.
- **Tests**: six new server integration tests cover ordered results,
mixed outcomes without rollback, force deletion, sandbox preservation
handoff, pre-mutation validation rejection, and auth gating.
### Key design decisions
**`POST /runs/delete` not `DELETE /runs`** — JSON request bodies on
`DELETE` are poorly supported by proxies and HTTP clients; the existing
batch lifecycle endpoints already use JSON-body `POST` actions.
**`already_absent` counts as success** — consistent with single-delete
semantics where `204` means "deleted or already absent"; callers doing
cleanup don't need to special-case missing IDs.
**`force` is batch-wide** — callers needing mixed force behavior issue
separate requests; this keeps the request schema simple.
**`SandboxDeleteOutcome` internal enum** — introduced alongside
`DeleteRunOutcome` to cleanly separate the sandbox-layer result
(absent/cleaned/preserved) from the top-level outcome that callers see,
avoiding a leaky intermediate type.
### Fabro Details
<details>
<summary>Ran 8 stages in 41m 52s for $13.37</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 3s | – | 0 |
| preflight_lint | 2m 17s | – | 0 |
| implement | 15m 12s | $8.10 | 0 |
| simplify_opus | 8m 54s | $3.30 | 0 |
| simplify_gpt | 3m 54s | $1.97 | 0 |
| verify | 9m 0s | – | 0 |
| **Total** | **41m 52s** | **$13.37** | **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>
|
||
|
|
846d1e91af
|
Polish stage insights sidebar
- Track which MCP servers the agent invoked. New `invoked: bool` on `McpServerProjection` (OpenAPI + Rust type + generated TS client), set by the projector when an `AgentToolStarted` event has an `mcp__<server>__*` tool_name. UI shows `used/total` in the section header, replaces the tool count with `used` on invoked rows, and dims rows that weren't invoked. Sticky across status re-reads. - Quiet noisy context-window warnings. When the snapshot's total is provider-authoritative (ProviderApiScaledBreakdown or ResponseUsageScaledBreakdown), drop local-estimator warning codes from the snapshot — they imply the user-facing total is wrong when it isn't. Also dedupe by code so a 35-turn conversation with opaque reasoning blocks no longer surfaces 35 copies of the same warning. - Reword the legitimately-local warnings. "opaque provider context estimated from JSON" → "Some content couldn't be precisely tokenized; total is approximate." Same treatment for the media, provider-options, and whole-request local-estimate messages. - Rename the sidebar header from "INSIGHTS" to "AGENT" to better describe what it shows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8ceb246b5a
|
feat: Add batch archive/unarchive API endpoints and update web bulk act… (#380)
## Summary
The web UI previously issued one archive/unarchive HTTP request per
selected run. This PR adds `POST /api/v1/runs/archive` and `POST
/api/v1/runs/unarchive` endpoints that process up to 250 runs in a
single fail-soft, non-transactional request, then wires the web
bulk-action toolbar and board column menu to use them.
### Plan Summary
- **OpenAPI contract** — four new schemas (`BatchRunLifecycleRequest`,
`BatchRunLifecycleResponse`, `BatchRunLifecycleResult`,
`BatchRunLifecycleSummary`) and two new paths; Rust and TypeScript
clients regenerated.
- **Server handlers** — `batch_archive_runs` / `batch_unarchive_runs`
behind `RequiredUser`; full request validation (empty, >250, duplicates,
unparseable IDs) before any mutation; per-item outcome mapping
(`archived`, `already_archived`, `unarchived`, `not_archived`,
`conflict`, `not_found`, `error`).
- **Frontend helpers** — `archiveRuns` / `unarchiveRuns` wrappers in
`run-actions.ts`; single-run helpers unchanged.
- **UI integration** — `BulkActionToolbar` and `ColumnActionsMenu`
replaced `Promise.allSettled` fan-out with one batch call; new
`summarizeBatchLifecycleAction` helper drives toast copy for
all-success, partial, and all-failure cases.
## Key Design Decisions
**Fail-soft `200` for valid batches.** A batch where some items fail is
still a successfully *processed* request; the per-item `ok` flag and
`summary` counts communicate individual outcomes without requiring the
caller to handle HTTP errors for partial failures. Request-level
problems (bad IDs, empty list) still return `400`.
**`RequiredUser` only.** Batch endpoints accept any-run mutations from a
request body, so a run-scoped worker token must not be accepted. This is
enforced at the handler level, separate from existing single-run
lifecycle routes.
**Request validation before any mutation.** Empty list, >250 IDs,
duplicate IDs, and unparseable IDs all return `400` before touching any
run — avoiding partial mutation surprises from invalid input.
**Idempotent outcomes are successes.** `already_archived` (archive of an
already-archived run) and `not_archived` (unarchive of a terminal
non-archived run) both set `ok=true`. This matches the existing
single-run semantics and avoids spurious failures in retry scenarios.
**`ask_fabro_readiness` hoisted out of the per-item loop.** Readiness
resolution involves LLM credential work; it's identical for every run in
the batch, so it's resolved once before the loop and shared via
`&AskFabroReadiness`.
**`uniqueItems: true` / `Set<string>` workaround.** The OpenAPI
generator maps `uniqueItems` arrays to `Set<T>` in TypeScript, but the
HTTP wire format is still a JSON array. The frontend helper casts
through `unknown` to send an array so Axios serializes correctly.
### Fabro Details
<details>
<summary>Ran 8 stages in 47m 48s for $23.53</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 4s | – | 0 |
| preflight_lint | 2m 16s | – | 0 |
| implement | 20m 17s | $15.09 | 0 |
| simplify_opus | 10m 27s | $6.18 | 0 |
| simplify_gpt | 3m 58s | $2.25 | 0 |
| verify | 8m 14s | – | 0 |
| **Total** | **47m 48s** | **$23.53** | **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>
|
||
|
|
39fa73d5e2
|
Add context-window snapshot API for agent stages (#378)
## Summary
Adds a best-effort `GET
/api/v1/runs/{id}/stages/{stageId}/context-window` endpoint that exposes
model-visible input-token usage, broken down by category (system prompt,
tools, MCP tools, skills, memory, conversation, other). The endpoint
degrades gracefully: it returns a stored projection snapshot when the
stage is inactive, and `available: false` when no snapshot has ever been
observed rather than surfacing count gaps as HTTP errors.
### Plan Summary
- **Unit 1** – OpenAPI schemas (`StageContextWindow`,
`StageContextWindowProjection`, breakdown/enum types) and generated Rust
+ TypeScript clients, with `fabro-api` build-time type replacements
pointing at the hand-written `fabro-types` structs.
- **Unit 2** – `ToolSource` enum on `RegisteredTool` (Native / Mcp /
Skill) + `ToolDefinitionWithSource`; new `context_window.rs` builder in
`fabro-agent` that assembles a content-free category breakdown at
request-assembly time; `fabro-llm::token_count` narrow public helpers
(`estimate_message_tokens`, `estimate_tool_definition_tokens`,
`estimate_request_control_tokens`).
- **Unit 3** – `AgentEvent::ContextWindowSnapshot` carries a
`StageContextWindowProjection`; the session emits a local snapshot
immediately, then a provider-scaled replacement (or
response-usage-scaled replacement) asynchronously; fingerprinting
prevents double-counting the same request.
- **Unit 4** – Server endpoint (stubbed routing; full handler targets a
follow-up) returning the latest projected snapshot.
- **Unit 5** – `queryKeys.runs.stageContextWindow`,
`useRunStageContextWindow` hook, and SSE invalidation for
`agent.context_window.snapshot` and all stage-lifecycle events.
### Key design decisions
**Agent-side counting, not server-side.** The exact `fabro_llm::Request`
only exists inside the active agent session. Rather than moving raw
prompt/message content into server-managed state, the session counts the
request it already has and emits content-free projection events. The
HTTP endpoint just reads the latest durable snapshot.
**Hybrid category ownership.** `fabro-agent` owns the category taxonomy
(it sees memory documents, skills, MCP registration, and session
history); `fabro-llm` exposes narrow estimation helpers. Neither crate
leaks the other's concerns.
**Provider count is async and non-blocking.** A spawned task calls
`Client::count_input_tokens(..., PreferProvider)` with a clone of the
request. It is cancelled via `close_token` when the session closes.
Failures produce a warning on the snapshot, not a stage error.
**`available: false` instead of 4xx for known-but-unobserved stages.**
The sidebar needs stable empty states; HTTP errors only mean the run or
stage doesn't exist.
```mermaid
flowchart TB
A[Session::build_request] --> B[build_local_snapshot\nLocalEstimate]
B --> C[emit ContextWindowSnapshot]
C --> D{provider count\nspawned task}
D -- success --> E[scaled_snapshot\nProviderApiScaledBreakdown]
D -- failure --> F[warning appended to local snapshot]
E --> G[emit ContextWindowSnapshot]
G --> H[run_state reducer\nupdates StageProjection.context_window]
F --> H
H --> I[GET context-window endpoint\nreturns projection]
```
**`ToolSource` on every `RegisteredTool`.** All 20+ `make_*_tool` call
sites are updated to set `ToolSource::Native`; MCP tools get
`ToolSource::Mcp { server_name }` at registration time;
`make_use_skill_tool` gets `ToolSource::Skill`. A parallel
`definitions_with_source_for_policy` method preserves existing
`definitions_for_policy` behaviour unchanged.
### Fabro Details
<details>
<summary>Ran 8 stages in 90m 57s for $70.01</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 1m 59s | – | 0 |
| preflight_lint | 2m 11s | – | 0 |
| implement | 45m 14s | $48.47 | 0 |
| simplify_opus | 25m 50s | $18.29 | 0 |
| simplify_gpt | 6m 12s | $3.25 | 0 |
| verify | 8m 59s | – | 0 |
| **Total** | **90m 57s** | **$70.01** | **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: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
def37896cd
|
Expose PermissionLevel on StageProjection and add sortable run columns (#373)
## Summary
Two independent additions landed together: surfacing `PermissionLevel`
on `StageProjection` (the primary goal), and making four previously
unsortable run-list columns (`repo`, `title`, `workflow`, `changes`)
sortable.
## Permission Level on StageProjection
`PermissionLevel` (`read-only | read-write | full`) was already resolved
at session start inside `fabro-agent` but never reached the API. This
wires it through the existing `agent.session.activated` event rather
than introducing a new event.
The data flow:
```mermaid
graph TB
A[SessionOptions.permission_level] -->|set at CLI build_tool_approval| B[Session.permission_level]
B -->|read in api.rs| C[ActivationLeaseOptions.permission_level]
C -->|emitted as| D[Event::AgentSessionActivated.permission_level]
D -->|convert.rs| E[EventBody::AgentSessionActivated.permission_level]
E -->|run_state.rs apply_event| F[StageProjection.permission_level]
F -->|OpenAPI + TS client| G[API consumers]
```
Key decisions:
- **No new event or type.** `PermissionLevel` is reused from
`fabro_types::session` directly; `AgentSessionActivatedProps` gains one
optional field with `skip_serializing_if`, so older persisted events
deserialize cleanly to `None`.
- **`Option<PermissionLevel>` on `StageProjection`** follows the same
pattern as `provider_used` — agent stages populate it, non-agent stages
leave it `None`. No migration required.
- **`AgentSessionActivatedProps` is now a progenitor type replacement**
so the API crate and the canonical type stay in sync (verified by the
new `agent_session_activated_props_round_trip` test).
### Plan Summary
- `fabro-agent` `config.rs` / `session.rs` — store and expose
`permission_level` on `SessionOptions`
- `fabro-types` `run_event/agent.rs` — add field to
`AgentSessionActivatedProps`
- `fabro-types` `run_projection.rs` — add field to `StageProjection`
- `fabro-workflow` `api.rs` / `activation_lease.rs` / `convert.rs` /
`events.rs` — thread the value to the emission site
- `fabro-store` `run_state.rs` — fold into projection on
`AgentSessionActivated`, plus new unit test
- OpenAPI schema, `fabro-api` build.rs, TS client — all
regenerated/updated
## Sortable Run Columns
`repo`, `title`, `workflow`, and `changes` columns were rendered as
plain `<th>` elements with no sort affordance. They now use `SortHeader`
in the frontend, the server-side `RunsSortKey` enum gains the four
variants, and the OpenAPI `ListRunsSortEnum` and TS client enum are
extended to match.
Sort helpers (`run_repo_key`, `run_title_key`, `run_workflow_key`,
`run_changes_total`) normalize to lowercase strings / integer totals and
compose with the existing stable ULID tiebreak.
### Fabro Details
<details>
<summary>Ran 9 stages in 53m 42s for $18.48</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 14s | – | 0 |
| preflight_lint | 2m 29s | – | 0 |
| implement | 20m 37s | $11.87 | 0 |
| simplify_opus | 8m 0s | $3.61 | 0 |
| simplify_gpt | 5m 11s | $2.50 | 0 |
| verify | 4m 48s | – | 0 |
| fixup | 9m 56s | $0.50 | 0 |
| **Total** | **53m 42s** | **$18.48** | **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-bot <fabro-bot@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
eda9e8855e
|
Make runs list Repo, Title, Workflow, and Changes columns server-side sortable
Extend the RunsSort enum and sort_runs() with case-insensitive ordering for repo, title, and workflow names, and total line changes (additions + deletions) for changes. Swap the corresponding `<th>` cells in the runs list view to `<SortHeader>` so every column can toggle asc/desc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |