The automations page collapsed loading, no-automations, and no-search-
match into a single branch that always rendered `No automations match
"{query}"`. With an empty query that read `No automations match ""`,
which also flashed during the initial fetch and when the trigger filter
(not the search) excluded everything.
Split into loading / error / true-empty / no-match states using the
shared EmptyState/ErrorState/LoadingState components. The true-empty
state is now a "Create your first automation" panel with a primary CTA,
and the search/filter toolbar is hidden when there is nothing to filter.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Emit the policy via Content-Security-Policy-Report-Only instead of the
enforcing header so browsers log violations without blocking resources
while we debug remaining CSP issues. The policy string is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Permit GitHub App manifest form posts and signed HTTPS VNC preview iframes while keeping the rest of the SPA CSP locked down. Mirror the policy in the split-web Caddy config.
Group Variables and Secrets under a new "Workflows" nav section between
General and Administration, and move Security under Administration next to
Server.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Enforce Cargo lockfile use across CI and release automation so jobs fail
on stale `Cargo.lock` state instead of resolving dependencies
implicitly. This adds `--locked` to Rust CI, release builds/tests,
nightly release tagging, the TypeScript workflow's embedded Rust build,
and helper-owned Cargo calls in `fabro-dev`.
This also fixes the Linux CI flake exposed by the PR: canceling a
durably blocked in-process run could take the abort path while the
workflow was still unwinding a human-input gate, causing
`run.failed(cancelled)` to be followed by `run.unblocked`. That invalid
event order broke projection rebuilds and made `GET /runs/{id}` return
404. Cancellation now uses the durable lifecycle status when selecting
the in-process blocked-run path, so the pending interview is cancelled
before the terminal event is emitted.
The release command's intentional `cargo update --workspace` step is
unchanged, because that step updates `Cargo.lock` after bumping the
workspace version.
## Testing
- `cargo nextest run --locked -p fabro-dev --features dev -E
'test(dry_run_computes_stable_version_from_date) |
test(dry_run_prints_equivalent_build_commands)'`
- `cargo --locked dev release --dry-run --skip-tests --release-date
2026-01-01`
- `cargo --locked dev docs check`
- `cargo nextest run --locked -p fabro-server --features test-support
cancel_durably_blocked_in_process_run_cancels_pending_interview_without_abort_signal
--status-level fail --final-status-level fail --show-progress none`
- `cargo nextest run --locked -p fabro-server --features test-support
--test it scenario::lifecycle --profile ci --status-level fail
--final-status-level fail --show-progress none --no-fail-fast`
- Linux Docker stress reproduction: `cargo nextest run --locked -p
fabro-server --features test-support --test it
scenario::lifecycle::full_http_lifecycle_cancel --profile ci
--stress-count 200 --status-level fail --final-status-level fail
--show-progress none`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --locked -p fabro-dev --features dev
--all-targets -- -D warnings`
- `cargo +nightly-2026-04-14 clippy --locked -p fabro-server --features
test-support --all-targets -- -D warnings`
- `git diff --check`
Full `cargo nextest run --locked -p fabro-dev --features dev` currently
has two unrelated policy-test failures:
`policy::catalog_builtin_references_stay_in_allowlist` and
`policy::workflow_template_rendering_call_sites_stay_in_allowlist`.
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
---------
Co-authored-by: Release Repro <release-repro@example.com>
The slug cannot be changed after creation, so showing it as a read-only
row on the edit page added noise without value. Keep the editable slug
input on the create page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The search input used text-sm (20px line-height) while the filter
buttons use text-xs (16px), both with py-2, making the input 4px
taller. Trim the input to py-1.5 so it matches the buttons' 34px
height without resizing the shared filter button components.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The manifest bundler collects Dockerfile path references from both the
named-environment catalog and [run.environment], but the server-side
resolver only inlined [run.environment.image]. A Dockerfile declared
under [environments.<slug>.image] therefore reached the Daytona provider
as an un-inlined Path and tripped its guard ("dockerfile path should have
been resolved to inline content before sandbox creation"), so no run
could use a catalog-defined Dockerfile environment.
Walk layer.environments alongside run.environment when resolving manifest
dockerfiles, mirroring the bundler. Add a regression test proving a
catalog dockerfile path is inlined.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Runs were falling back to the built-in `default` environment (a bare
daytona-medium snapshot with no Rust toolchain), so every Rust stage in
the smoke workflow failed with exit 127 (cargo/rustc not found). The old
[run.sandbox] config that built a custom snapshot was dropped in the
move to named environments (#360) and never ported.
Add a `fabro-dev` named environment that builds the Daytona snapshot from
.fabro/Dockerfile (Rust + nightly-2026-04-14 + cargo-nextest + bun), with
8 CPU / 16GB RAM, and select it via [run.environment].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the toolbar on /runs?view=list so the runs section under
/automations/:id supports the same client-side filters.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cargo.lock churn from the merge with origin/main; the workspace bumped
fabro-environment's version but Cargo.lock still pointed at the
0.246.0-nightly.0 entry until a build refreshed it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The list card linked to the singular /automation/:id, which mismatched
the rest of the new automations CRUD surface (/automations,
/automations/new, /automations/:id/edit). Switch the card link and the
slug-preview text on the create form to the plural form, and mount
/automations/:id in the router alongside the existing singular route
(kept as a back-compat alias for any older bookmarks).
Drive-by: fold two adjacent `use super::*` imports into one and reflow
a long `if let` line in the automations handler (linter cleanup; no
behavior change).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace Sonner's default richColors palette with a Fabro-themed
FabroToaster: dark panel surface, accent-colored Heroicons type icons
(coral error, mint success, teal info, amber warning), and a themed
close button so persistent error toasts can be dismissed. Extract the
shared config out of the two duplicated <Toaster> mount points.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Materializing an automation run cloned the full repo fresh into a
tempdir on every click — 5–15s of git activity on the HTTP request
thread, paid in full for every run, then thrown away.
Add a per-`(owner, repo)` bare-clone cache under
`<Storage::cache_dir>/automation-repos/<owner>/<repo>.git`, and replace
the per-call clone+fetch+checkout dance with:
1. `KeyedMutex` lock on `(owner, repo)` so concurrent calls serialize
per repo and parallelize across repos.
2. If the bare clone is missing, `git clone --bare --depth 1`. Otherwise
`git worktree prune` to clean up any admin entries leaked by previous
`TempDir` drops.
3. `git fetch --depth 1 origin <ref>` against the bare clone.
4. `git rev-parse FETCH_HEAD` for the SHA.
5. `git worktree add --detach --force <temp>/repo FETCH_HEAD` into the
per-call scratch dir, then build the manifest as today.
First run for a repo still pays the clone cost. Every subsequent run
for any ref or automation against that repo pays only the fetch delta
plus a near-free worktree add (~100–500ms).
Corruption recovery: if the bare clone's `HEAD` file is missing or
zero-length after a failure, the cache wipes the directory and retries
once before surfacing `CloneFailed` as before. Auth and network errors
do not trigger a wipe.
Promote `fabro_store::KeyedMutex` and its guard to `pub` so the
server can reuse the existing primitive instead of duplicating it.
Tests:
- `bare_clone_reused_across_calls` seeds a local upstream, runs
`prepare_worktree` twice, and asserts the bare clone's `objects/`
tree is identical before and after the second call (i.e., no
re-clone).
- `bare_clone_recovers_from_corruption` truncates `HEAD` between
calls and asserts the cache rebuilds and succeeds.
- The existing plan-builder argv/timeout assertions are updated to
cover the new bare-clone, bare-fetch, worktree-add, worktree-prune,
and rev-parse FETCH_HEAD plans.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make the Automations area in the web UI functional end-to-end against the
real Automation API, and fix the backend so runs created by an automation's
API trigger actually start instead of sitting in Submitted forever.
Web:
- Reveal the Automations nav tab outside demo mode; drop the now-empty
demoOnly mechanism.
- List page: render via listAutomations (was workflows mock data); wire
ellipsis menu to Edit and Delete, with ConfirmDialog + If-Match revision.
Move Create Automation into the toolbar, switch the trigger select to a
shared FilterButton, hide the redundant page-header title via a new
hideTitle handle flag.
- Play button on each card fires createAutomationRun with spinner + toast
and navigates to the new run.
- New automation form: drop the dead Goal panel and hardcoded repository
list, post to createAutomation with real triggers.
- Edit automation: new /automations/:id/edit route reusing a shared
AutomationFormFields component, PUT via replaceAutomation with If-Match.
- Show page: rebuild like a run detail page — breadcrumb, title, chips
(enabled status, repo+ref, workflow, schedule), Edit + Run actions
(Run hits createAutomationRun), and a Runs panel using RunsListView
with URL-driven search/sort/pagination/column-picker like the Children
sub-tab. Drop the obsolete Definition/Diagram/Runs child routes.
Backend (fabro-server):
- create_automation_run now calls lifecycle::queue_run_start after the
run is persisted, so the run transitions Submitted → Runnable and the
scheduler picks it up. Logs a warn and returns the created response if
start fails (no worse than the prior always-stuck behavior).
- queue_run_start in lifecycle.rs is promoted to pub(super) so sibling
handlers can reuse it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
- Add `claude-opus-4-8` to the built-in Anthropic model catalog with
pricing, limits, features, and fast-mode costs.
- Move the floating `opus` and `claude-opus` aliases from Opus 4.7 to
Opus 4.8 and update the public model table.
- Remove/generalize Rust tests that were pinned to specific built-in
Opus catalog data.
## Verification
- `cargo nextest run -p fabro-model`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `git diff --check`
- `target/debug/fabro --json model test --model opus` (live Anthropic
smoke; resolved to `claude-opus-4-8`)
## Summary
Fixes#435.
Preserve raw non-JSON tool-call arguments for custom/freeform tools when
using the OpenAI-compatible Chat Completions adapter. This keeps
`apply_patch` receiving the raw patch text instead of `{}` when
LiteLLM/openai-compatible providers emit Codex-style freeform patch
calls.
Also extends the OpenAI twin so black-box tests can exercise the Chat
Completions path with raw tool-call arguments.
## Test Plan
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo nextest run -p fabro-agent --test it
openai_compatible_twin_preserves_raw_apply_patch_arguments --run-ignored
only`
- `cargo nextest run -p fabro-llm`
- `cargo nextest run -p fabro-test`
## Summary
Follow-up to fabro-sh/fabro#447. This keeps context compaction's
effective preserve boundary consistent between summary generation,
history mutation, and emitted telemetry so tool-call/result pairs that
remain in raw history are not also summarized.
The branch also tightens the OpenAI twin support added for this
regression: scripted usage is modeled as a single `TokenUsage`, SSE
completion payloads reuse the canonical Responses JSON shape, and
request validation now treats custom tool-call outputs as tool outputs
instead of spreading raw item-type string checks.
## Verification
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo nextest run -p fabro-agent compaction`
- `cargo nextest run -p twin-openai`
- `FABRO_TEST_MODE=twin cargo nextest run -p fabro-agent --profile e2e
--run-ignored only --test it
openai_twin_compaction_preserves_tool_call_pairs`
- `git diff --check origin/main...HEAD`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
## Summary
Fixes OpenAI Responses requests after context compaction by ensuring
preserved tool results are not separated from the assistant tool calls
that produced them. The previous fixed-size preserved tail could retain
a `function_call_output` while dropping the matching `function_call`,
which OpenAI rejects as an orphaned tool result.
## Changes
- Extends `History::compact` so the preserved range moves backward until
every kept tool result has its matching assistant tool call.
- Adds a unit invariant test for compacted histories that serialize tool
results.
- Adds an OpenAI twin integration regression that forces compaction
during a tool-use loop.
- Teaches the OpenAI twin to validate orphaned `function_call_output`
items and script response usage counts for deterministic compaction
tests.
## Test Plan
- `cargo +nightly-2026-04-14 fmt --check --all`
- `git diff --check`
- `FABRO_TEST_MODE=twin cargo nextest run -p fabro-agent --test it
openai_twin_compaction_preserves_tool_call_pairs --run-ignored all`
- `cargo nextest run -p fabro-agent`
- `cargo nextest run -p twin-openai`
- `cargo nextest run -p fabro-test`
- `cargo +nightly-2026-04-14 clippy -p fabro-agent -p fabro-test -p
twin-openai --all-targets --no-deps -- -D warnings`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
Drop flex-1 from the columns row when the landing empty state is
showing so the row sizes to the column headers and the empty state
sits directly beneath them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Adds a standalone Docker Compose proof that runs the Fabro Rust API, a
Caddy static SPA server, and a Caddy edge proxy as separate services.
This demonstrates split web asset serving while keeping `/api/*`,
`/auth/*`, and `/health` same-origin with the API server.
## Changes
- Adds `docker-compose.split-web.yaml` with private `fabro-api` and
`fabro-web` services behind an exposed `edge` proxy on port 8080.
- Adds Caddy edge routing that sends `/api/*`, `/auth/*`, and `/health`
to Rust, while everything else goes to the static web service.
- Adds a static Caddy config for `apps/fabro-web/dist` with SPA
fallback, source-map blocking, security headers, immutable asset
caching, and `X-Fabro-PoC-Upstream` route-proof headers.
- Adds PoC server settings and a README with build, run, and validation
commands.
## Verification
- `cargo dev docker-build --tag fabro-sh/fabro:split-web-poc`
- `docker compose -f docker-compose.split-web.yaml up -d`
- `docker compose -f docker-compose.split-web.yaml ps`
- `curl` checks for `/runs`, `/assets/app.css`, `/assets/app.css.map`,
`/api/v1/health`, `/api/v1/auth/config`, `/auth/login/dev-token`,
`/api/v1/auth/me`, and `/api/v1/attach`
- Browser login flow via `browser-use`: loaded `/login`, submitted the
dev token, and landed on the authenticated Runs screen
- `docker compose -f docker-compose.split-web.yaml config`
- `caddy validate` for both Caddyfiles
- `git diff --check`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
## Summary
Automation-triggered runs need to share the same run creation pipeline
as `POST /runs`. This PR lays the core infrastructure: a
`create_run_from_manifest` helper that the HTTP handler and the upcoming
automation scheduler can both call, plus a `AutomationRunMaterializer`
trait with a production implementation that clones a GitHub repo and
builds a `RunManifest` from it.
### Plan Summary
- Extract the body of `handler/runs.rs::create_run` into a crate-private
`create_run_from_manifest(state, CreateRunFromManifestRequest)` helper;
`POST /runs` calls it with `automation: None`, preserving existing
behavior.
- Add `AutomationRunMaterializeInput/Materialized/Error` types and the
`AutomationRunMaterializer` trait (`automation_materializer.rs`).
- Implement `ProductionAutomationRunMaterializer`: validates
`owner/repo` slug, shallow-clones via `tokio::process::Command` argv
(never shell strings), sets `GIT_TERMINAL_PROMPT=0`, enforces
per-operation timeouts, redacts credentials from error text, resolves
the workflow with `fabro_config::project::WorkflowLocation::resolve`,
and builds a `RunManifest` via `fabro_manifest::build_run_manifest`.
- Add `TestAutomationRunMaterializer` (gated on `test` or
`test-support`) for fake injection in route tests without network
access.
- Wire the materializer override into `AppState` and `AppStateConfig`
behind `#[cfg(any(test, feature = "test-support"))]`; expose via
`TestAppStateBuilder::automation_materializer`.
- Move `async-trait` from `[dev-dependencies]` to `[dependencies]` in
`fabro-server` since the trait is now in production code.
## What changed and why
**`automation_materializer.rs` (new)** — Core of this PR. The
`GitCommandPlan` builder keeps all git invocations as argv slices so
there is no shell injection surface. Credentials are injected
exclusively via `GIT_CONFIG_VALUE_0` (the `extraheader` mechanism),
never embedded in the clone URL, so they cannot appear in run metadata
or error messages. The `redact_git_output` function scrubs the raw
token, the Base64-encoded form, and the full `AUTHORIZATION` header
value from any error string before it surfaces.
**`create_run_from_manifest`** — The extracted helper accepts an
optional `AutomationRef` which is forwarded into
`create_input.automation` so the store can persist automation provenance
on the run. The `POST /runs` code path passes `None`, leaving existing
API behavior identical.
**Test injection** — `TestAutomationRunMaterializer` captures every
`AutomationRunMaterializeInput` it receives and returns a
caller-controlled `Result`, letting route tests assert what inputs the
scheduler would pass without touching GitHub.
```mermaid
flowchart TB
A["POST /runs\n(HTTP handler)"] -->|automation: None| H["create_run_from_manifest"]
S["Automation scheduler\n(future issue)"] -->|automation: Some(ref)| H
H --> DB[(Run store)]
M["AutomationRunMaterializer\n(trait)"] -->|produces RunManifest| S
M -- production --> P["ProductionAutomationRunMaterializer\n(git clone → manifest build)"]
M -- test --> T["TestAutomationRunMaterializer\n(captures input, returns fixture)"]
```
### Fabro Details
<details>
<summary>Ran 8 stages in 72m 56s for $36.55</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 2s | – | 0 |
| preflight_compile | 2m 12s | – | 0 |
| preflight_lint | 2m 27s | – | 0 |
| implement | 30m 41s | $23.10 | 0 |
| simplify_opus | 19m 19s | $9.12 | 0 |
| simplify_gpt | 7m 53s | $4.33 | 0 |
| verify | 9m 20s | – | 0 |
| **Total** | **72m 56s** | **$36.55** | **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>