mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
2038 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ccbc62ea5d
|
fix(ci): restore Rust checks | ||
|
|
e952bb4c7f
|
fix(config): disable Slack unless configured
Require an explicit server.integrations.slack table before Slack reports enabled or starts from vault tokens. |
||
|
|
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> |
||
|
|
efbd2d02e0
|
chore(server): switch CSP to report-only while tuning
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> |
||
|
|
b3528a982e
|
chore: update CSP | ||
|
|
3e881e9938
|
fix(server): allow required CSP handoffs
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. |
||
|
|
b5de404354
|
ci: lock Cargo resolution and fix cancellation flake (#461)
Some checks failed
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) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
## 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>
|
||
|
|
d7a00d52d0
|
fix(automation): honor schedule trigger enabled state
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
Remove the stale top-level automation enabled gate from schedule filtering now that automations only carry trigger-level enabled flags. |
||
|
|
17cae07e5a
|
Add scheduled automation executor with in-memory cron planner (#457)
## Summary
Automation schedule triggers now fire automatically at their UTC cron
times. A new long-lived server task (`spawn_automation_scheduler`) owns
an in-memory planner that tracks one cursor per enabled schedule trigger
and creates/starts a normal Fabro run whenever a cursor comes due —
reusing the same materialization and run-creation path as API-triggered
runs.
### Plan Summary
- **Planner** (`AutomationSchedulePlanner`) — pure in-memory, no
persistent state. Reconciles cursors from the live automation list on
every tick; advances each due cursor _before_ spawning the fire task so
a failed materialization never hot-loops.
- **Executor loop** (`spawn_automation_scheduler`) — wakes on the
nearest cursor due time (capped at 30 s), on automation store mutations
(via `Notify`), or on shutdown. Spawns one Tokio task per due trigger so
slow materializations don't block other triggers.
- **Run firing** (`fire_scheduled_automation_run`) — materializes, calls
`create_run_from_manifest` with `Principal::System { Engine }`, then
calls `queue_run_start`. Warnings on any failure; next attempt waits for
the next cron occurrence.
- **Wiring** — `automation_scheduler_notify: Notify` added to
`AppState`; `create_automation`, `replace_automation`, and
`delete_automation` handlers call `notify_automation_scheduler()` so the
planner reacts immediately to changes.
- **Visibility widening** — `handler/lifecycle.rs` (`queue_run_start`)
and `handler/mod.rs` (`lifecycle`) promoted from `pub(super)` to `pub(in
crate::server)` so the scheduler (a sibling of `handler`) can call the
same start path.
- **Shared cron parser** — `parse_schedule_expression` extracted to
`fabro-automation` and re-exported so both validation and the scheduler
use the same parser configuration (no seconds, no year).
### Key design decisions
| Decision | Rationale |
|---|---|
| Cursor advances before fire task spawns | Guarantees at-most-one
attempt per occurrence even if materialization panics |
| No backfill on startup | Matches locked spec; `next_occurrence(expr,
now)` always starts from the current time |
| `Principal::System { Engine }` for actor | Avoids adding new public
enum variants or OpenAPI surface |
| `automation_temp_root()` extracted to `AppState` | Removes duplicated
`Storage::new(…).scratch_dir().join("automations")` from the automations
handler |
| Tests use `run_due_schedules_once` helper | Drives the planner
directly with fixed `DateTime<Utc>` values; no wall-clock sleeps in
tests |
### Fabro Details
<details>
<summary>Ran 9 stages in 60m 12s for $22.11</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 9s | – | 0 |
| preflight_lint | 2m 26s | – | 0 |
| implement | 20m 14s | $12.44 | 0 |
| simplify_opus | 12m 29s | $6.34 | 0 |
| simplify_gpt | 3m 53s | $2.79 | 0 |
| verify | 11m 5s | – | 0 |
| fixup | 6m 46s | $0.54 | 0 |
| **Total** | **60m 12s** | **$22.11** | **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>
|
||
|
|
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>
|
||
|
|
7c73f7ac02
|
fix(server): inline dockerfiles defined in the [environments.*] catalog
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>
|
||
|
|
ac66f6c1d6
|
Merge remote-tracking branch 'origin/main' into fix-center-size-column
# Conflicts: # lib/crates/fabro-server/src/automation_materializer.rs # lib/crates/fabro-server/src/server.rs |
||
|
|
fee245d788
|
fix(web): make plural /automations/:id the canonical detail route
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> |
||
|
|
7f9b31074c
|
perf(server): cache bare GitHub clones for automation materialization
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> |
||
|
|
87516c25ce
|
feat(automations): wire UI to API and auto-start runs from API trigger
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> |
||
|
|
8272d8239b
|
feat(model): add Claude Opus 4.8 (#451)
## 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`) |
||
|
|
4cff07373c
|
feat: add server-owned environment store (Task 1 & 2 foundation) (#446)
## Summary
Moves environment definitions out of project/workflow TOML config and
into server-owned files, introducing the `fabro-environment` crate and
enforcing source-aware validation so project/workflow/user configs can
no longer define environment catalogs.
### What changed
**New `fabro-environment` crate** — workspace crate wired into
`fabro-cli` and `fabro-server`. Exposes a `seeded_catalog_layer()` that
CLI commands inject at the call site to fill the environment catalog
that settings resolution requires.
**Config environments are now migration-only** — `defaults.toml` no
longer ships a built-in `[environments.*]` catalog. Instead:
- `SettingsSource` enum tags every parsed layer (ActiveSettings,
Project, Workflow, DirectRun, User).
- `validate_settings_source` rejects `[environments.<id>]` in any source
except `ActiveSettings` with a targeted message: `[environments.<id>] is
now server-managed; move this definition to the server environments
directory`.
- TOML-provided
`run.environment.{image,resources,network,lifecycle,labels,volumes,env}`
overrides are also rejected; only `run.environment.id` survives.
**New migration** (`2026052801_settings_environments_to_server_files`) —
chains after the existing legacy-sandbox migration. Extracts
`[environments.*]` entries from `settings.toml` into sibling
`environments/<id>.toml` files, writes a
`.settings-environments-migration.bak` backup, and fails without
modifying any file if a target already exists.
**Builder API additions** —
`RunSettingsBuilder::load_from_with_catalog`,
`load_default_with_catalog`, `from_toml_with_catalog` let callers inject
a server-side catalog; the bare `from_toml` path now errors if no
catalog is present and a named environment is selected.
`WorkflowSettingsBuilder` test helpers in `src/tests/mod.rs` centralise
catalog injection across all config tests.
**`.fabro/project.toml`** — removed the inline
`[environments.fabro-dev]` block (environment definition now lives
server-side).
### Key design decisions
- CLI offline commands (graph, preflight, validate) use
`seeded_catalog_layer()` as a local stand-in until a running server is
available — matches the pre-existing behaviour without regressing
offline workflows.
- `load_settings_path` no longer runs migrations for non-ActiveSettings
sources, preventing project/workflow files from accidentally triggering
file-system writes.
- The `MigrationReport` type is now the new migration's
`SettingsEnvironmentsMigrationReport` (exposes `contents: String`
instead of a parsed layer), keeping `load.rs` simpler and decoupled from
layer parsing.
### Fabro Details
<details>
<summary>Ran 9 stages in 143m 23s for $105.27</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 11s | – | 0 |
| preflight_lint | 2m 23s | – | 0 |
| implement | 28m 27s | – | 0 |
| simplify_opus | 37m 27s | $53.28 | 0 |
| simplify_gpt | 20m 50s | $12.14 | 0 |
| verify | 6m 3s | – | 0 |
| fixup | 45m 15s | $39.84 | 0 |
| **Total** | **143m 23s** | **$105.27** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-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>
|
||
|
|
0106711170
|
test: cover automation trigger API behavior | ||
|
|
91d11eb04d
|
fix(llm): preserve raw compatible tool arguments (#448)
## 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` |
||
|
|
2e39dfc70e
|
fix(agent): align compaction preserve boundary (#449)
## 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) |
||
|
|
81554581ca
|
fix(agent): preserve tool-call pairs during compaction (#447)
## 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) |
||
|
|
9ee576690b
|
refactor: remove in-process IP allowlist and introduce WorkerRuntime (#444)
## Summary
This PR does two things: it removes the in-process inbound source-IP
allowlist entirely, and it lays the foundation for pluggable worker
compute backends by introducing a `WorkerRuntime` abstraction.
## IP allowlist removal
The `[server.ip_allowlist]` setting and its GitHub webhook overlay
(`[server.integrations.github.webhooks.ip_allowlist]`) have been removed
from config parsing, the settings API, and the OpenAPI spec. The
`ip_allowlist.rs` module (~600 lines including the `GitHubMetaResolver`,
middleware, and cache logic) is deleted.
**Migration:** Existing `settings.toml` files containing those keys will
now fail to parse as unknown fields. Source-IP restrictions should be
moved to a reverse proxy, firewall, VPN, Tailscale ACL, or cloud ingress
— as documented in the new security guidance.
`build_router_with_options` loses the `ip_allowlist_config:
Arc<IpAllowlistConfig>` parameter and `RouterOptions` loses
`github_webhook_ip_allowlist`. Call sites in tests and the auth harness
are updated accordingly. TCP serving no longer uses
`make_service_with_connect_info` since `ConnectInfo` was only needed for
IP extraction.
## WorkerRuntime abstraction
A new `worker_runtime.rs` module introduces:
- **`WorkerRuntime` trait** — `start`, `request_stop`, `force_stop`,
`is_alive`
- **`WorkerLaunchSpec`** — all inputs needed to describe a worker
process, replacing the former `worker_command` helper
- **`WorkerRef::Local { pid, process_group_id }`** — replaces the
`worker_pid` / `worker_pgid` pair on `ManagedRun`
- **`StartedWorker`** — carries the ref, optional stderr stream, and a
`wait` future
- **`LocalWorkerRuntime`** — the only implementation for now; wraps the
existing subprocess spawn logic
`AppState` stores an `Arc<dyn WorkerRuntime>` and `AppStateConfig`
accepts an optional override in `#[cfg(test)]` for injection. Stop/kill
paths in `server.rs` and `lifecycle.rs` now call
`worker_runtime.request_stop` / `force_stop` / `is_alive` instead of
issuing signals directly.
### Plan Summary
- **Task 1:** New `worker_runtime.rs` with trait, types, and
`LocalWorkerRuntime` impl
- **Task 2:** Wire `Arc<dyn WorkerRuntime>` into `AppState` /
`AppStateConfig` / `TestAppStateBuilder`
- **Task 3:** Replace `worker_pid` / `worker_pgid` on `ManagedRun` with
`worker_ref: Option<WorkerRef>`; build `WorkerLaunchSpec` in
`execute_run_subprocess`
- **Task 4:** Route all stop/kill calls through the runtime
(`terminate_worker_for_deletion`, `shutdown_active_workers`,
`cancel_run` fallback)
- **Task 5:** Fake `RecordingWorkerRuntime` for unit tests; new
cancel-fallback and shutdown tests
### Fabro Details
<details>
<summary>Ran 8 stages in 107m 33s for $21.66</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 10s | – | 0 |
| preflight_lint | 2m 21s | – | 0 |
| implement | 29m 0s | $11.86 | 0 |
| simplify_opus | 11m 9s | $5.94 | 0 |
| simplify_gpt | 53m 13s | $3.85 | 0 |
| verify | 9m 23s | – | 0 |
| **Total** | **107m 33s** | **$21.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>
|
||
|
|
e3bbe91053
|
Add GET/POST /automations/{id}/runs endpoints (#442)
## Summary Implements the two automation run endpoints from issue #399, backed by a significant refactor of the worker control channel from stdin JSONL to a WebSocket-based pub/sub bus. ## What changed ### New API endpoints (`automations.rs`) - `GET /automations/{id}/runs` — lists cached runs filtered to those linked to the given automation ID, sorted newest-first, with `page[limit]`/`page[offset]` pagination and the standard `{ data, meta }` envelope. - `POST /automations/{id}/runs` — requires `RequiredRunToolActor` auth, checks that the automation exists and has an enabled API trigger (returning 409 with `automation_api_trigger_disabled` otherwise), materializes the run manifest, and delegates to the shared `create_run_from_manifest` helper with a fully-populated `AutomationRef`. ### `enabled_api_trigger()` helper (`fabro-automation`) A new method on `Automation` encapsulates the "automation is enabled **and** has an enabled API trigger" check, keeping the handler clean. ### Worker control channel: stdin JSONL → WebSocket bus The most significant structural change is how the server delivers control messages (answers, cancel, pause/unpause, steer, pair events) to running workers: | Before | After | |---|---| | Server pipes JSONL lines to worker stdin | Server publishes to `WorkerControlBus`; worker connects via WebSocket | | Worker reads stdin on a blocking OS thread | Worker manages a reconnecting WebSocket with ping/pong liveness | | No delivery deduplication | `AppliedWorkerControlDeliveryIds` deduplicates replayed frames | | No reconnect / resume | Worker reconnects with exponential backoff; replays from last applied cursor | The `LocalWorkerControlBus` replaces the old `mpsc` channel and stdin pipe. `RunAnswerTransport::Subprocess` is renamed `Worker` and holds a `run_id` + `Arc<dyn WorkerControlBus>` instead of a channel sender. Worker stdin is now `Stdio::null()`. New control messages `RunPause` / `RunUnpause` are added to the protocol, wired through to `RunControlState`. ### Plan Summary - Add `enabled_api_trigger()` to `Automation`. - Implement `list_automation_runs` and `create_automation_run` handlers; route them under `/automations/{id}/runs`. - Expose `create_run_from_manifest` from the runs handler for reuse. - Add `RequiredRunToolActor` extractor. - Replace stdin JSONL worker control with `WorkerControlBus` + WebSocket reconnect loop in the CLI worker. - Add integration tests for all 409/201 cases, run persistence, listing filters, pagination, and sorting. ### Fabro Details <details> <summary>Ran 8 stages in 51m 5s for $21.34</summary> | Stage | Duration | Cost | Retries | |---|---|---|---| | start | 0s | – | 0 | | toolchain | 1s | – | 0 | | preflight_compile | 2m 8s | – | 0 | | preflight_lint | 2m 23s | – | 0 | | implement | 20m 52s | $13.05 | 0 | | simplify_opus | 11m 5s | $5.70 | 0 | | simplify_gpt | 5m 0s | $2.60 | 0 | | verify | 9m 4s | – | 0 | | **Total** | **51m 5s** | **$21.34** | **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>
|
||
|
|
475b4ab650
|
Replace stdin JSONL control pipe with WebSocket worker control bus (#440)
## Summary
Workers no longer receive control messages over stdin JSONL. A new
`WorkerControlBus` abstraction (backed by `LocalWorkerControlBus` for
local/single-node deployments) publishes `WorkerControlEnvelope`
messages server-side; a worker-initiated WebSocket at `GET
/runs/{id}/worker/control-stream` delivers them with ordered, replayable
delivery frames. The bus API is designed so a Redis Streams backend can
slot in later without touching API handlers or worker message handling.
### Plan Summary
- **Task 1 – Bus contract:** `WorkerControlBus` trait,
`WorkerControlDelivery`, `WorkerControlCursor` (`Start` / `After(id)`),
bus errors.
- **Task 2 – Local backend:** `LocalWorkerControlBus` — in-memory
per-run stream, replay from `Start`, reconnect via `After(id)`, 1
024-message trim bound, cleanup on terminal runs.
- **Task 3 – Server state:** `Arc<dyn WorkerControlBus>` added to
`AppState`; `LocalWorkerControlBus` constructed at startup.
- **Task 4 – Protocol extension:** `WorkerControlMessage::RunPause` /
`RunUnpause`, `WorkerControlDeliveryFrame`, WebSocket liveness constants
(`WORKER_CONTROL_WS_PING_INTERVAL = 15s`,
`WORKER_CONTROL_WS_LIVENESS_TIMEOUT = 45s`), close-reason strings.
- **Task 5 – Worker message handler:** `apply_worker_control_message`
split out; pause/unpause routing; delivery-id dedupe
(`AppliedWorkerControlDeliveryIds`, capacity 2 048).
- **Task 6 – Worker WebSocket client:** `spawn_worker_control_manager` —
HTTP→ws/wss and Unix-socket connection, backoff 100ms→5s,
first-connection gate before `operations::start/resume`, ping/pong
watchdog, fatal loss wired back to `execute`.
- **Task 7 – Server route:** `GET /runs/{id}/worker/control-stream`,
worker-only auth via new `RequireWorkerRunScoped` extractor,
`Start`/`After` cursor dispatch, 410 on invalid cursor, server-side
ping/pong.
- **Task 8 – Stdin removal:** `RunAnswerTransport::Subprocess` renamed
to `Worker { run_id, bus }`; `pump_worker_control_jsonl` deleted; worker
launched with `stdin(Stdio::null())`; pause/unpause transport methods
added.
- **Tasks 9–10 – E2E & verification:** reconnect, invalid-cursor,
cancel-over-WebSocket, and human-interview regression tests; no Redis
dependency added.
### Key design decisions
**`RunAnswerTransport::Subprocess` → `Worker { run_id, bus }`** — all
existing transport methods (`submit`, `cancel_run`, `steer`,
`interrupt`, `pair_*`) now call `bus.publish(run_id, envelope)` instead
of writing to a channel that fed stdin. The match arms are symmetric, so
the diff is mechanical but large.
**First-connection gate** — `execute()` calls
`control_manager.wait_for_first_connection().await?` before
`operations::start` or `operations::resume`. Temporary failures spin
with backoff; a fatal invalid-cursor or request-build failure propagates
as an error before the workflow starts.
**Fatal vs. reconnectable** — HTTP 410 or a WebSocket close with reason
`"invalid_cursor"` is fatal (infrastructure failure, not user
cancellation). Any other close/error triggers the reconnect loop while
the run is non-terminal.
**`AutomationStore::load` made synchronous** — startup load now uses
`std::fs` under a `clippy::disallowed_methods` exception; async
`tokio::fs` is no longer needed for the one-shot directory scan. Invalid
automation files now fail loudly instead of being silently skipped.
**`canRetry` extended to succeeded runs** — `status.kind ===
"succeeded"` is now retryable (non-archived). Tests and API docs updated
to match.
**Default model bumps** — OpenAI default: `gpt-5.4` → `gpt-5.5`; Gemini
default: `gemini-3.1-pro-preview` → `gemini-3.5-flash`.
### Fabro Details
<details>
<summary>Ran 9 stages in 129m 19s for $58.27</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 10s | – | 0 |
| preflight_lint | 2m 23s | – | 0 |
| implement | 73m 48s | $41.53 | 0 |
| simplify_opus | 22m 55s | $11.75 | 0 |
| simplify_gpt | 7m 19s | $2.74 | 0 |
| verify | 8m 51s | – | 0 |
| fixup | 10m 59s | $2.24 | 0 |
| **Total** | **129m 19s** | **$58.27** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-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>
|
||
|
|
ee1502f793
|
Add automation run materialization core and shared run creation helper (#441)
## 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>
|
||
|
|
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>
|
||
|
|
fa565ceaae
|
chore(model): update provider default models (#437)
Updates the built-in catalog so OpenAI default selection now resolves to `gpt-5.5` and Gemini default selection resolves to `gemini-3.5-flash`. The public model defaults table and catalog assertions were updated to pin the new behavior. Verified with `cargo nextest run -p fabro-model`. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 (Codex) via [Codex](https://openai.com/codex) |
||
|
|
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>
|
||
|
|
7b7a2c9044
|
Add fabro variable CLI namespace for server-managed variables (#434)
## Summary
Exposes the existing variables API through a new `fabro variable` CLI
namespace (`list`, `get`, `set`, `rm`), following the same patterns as
`fabro secret`. Variables are intentionally readable — `list` and `get`
show stored values — while `fabro secret` remains write-only. This PR
also ships a significant set of accompanying changes: a refactored
sandbox lifecycle model in the web UI, removal of the
`fabro-devcontainer` crate, and a new `RunSandbox` OpenAPI schema that
models the full planned → initializing → ready/failed lifecycle.
## What Changed
### CLI (`fabro variable`)
- New `fabro variable` namespace with `list` (aliased `ls`), `get`,
`set`, and `rm` subcommands, dispatched through the same
`ServerTargetArgs` pattern as `fabro secret`.
- `fabro-client` gains five new wrapper methods (`list_variables`,
`get_variable`, `create_variable`, `update_variable`, `delete_variable`)
over the generated OpenAPI client.
- `set` is an upsert; `--value-stdin` accepts empty input after
newline-trimming (unlike the secrets equivalent).
- CLI reference docs (`docs/public/reference/cli.mdx`) regenerated;
`docs/public/workflows/variables.mdx` gains a short section explaining
`{{ vars.NAME }}` interpolation and the variables-vs-secrets security
boundary.
### Sandbox lifecycle model (web)
- New `RunSandbox` OpenAPI shape splits the old flat object into `kind`
(planned/initializing/ready/failed) + `plan` + optional `instance` +
optional `failure`.
- `apps/fabro-web/app/lib/run-sandbox-lifecycle.ts` centralises
lifecycle helpers (`sandboxLifecycleKind`, `sandboxInstance`,
`sandboxRuntime`, `sandboxIsReady`, `sandboxTabVisible`,
`SANDBOX_LIFECYCLE_DISPLAY`).
- Run summary panel and sandbox route now show lifecycle state
(Initializing / Failed with causes / Not created) before or instead of
the fully-loaded `SandboxDetails`.
- The sandbox details query is skipped entirely until `sandboxIsReady`
returns true, preventing unnecessary 404 fetches for planned/failed
sandboxes.
- `runHasSandbox` in `tabs-shell.tsx` delegates to `sandboxTabVisible`,
hiding the Sandbox tab for `planned` state and showing it for
`initializing`/`ready`/`failed`.
- Legacy flat sandbox shape (no `kind`) is handled via
backwards-compatible shims in the new helpers.
### `fabro-devcontainer` removal
- The `fabro-devcontainer` crate has been removed from `Cargo.lock` and
all dependent crates.
- References to devcontainer in internal plans, docs, changelog entries,
and event schemas have been cleaned up or reworded to reflect that the
feature is no longer present.
### Plan summary
- **Unit 1:** `fabro-client` variable wrappers
- **Unit 2:** CLI args, dispatch, and `commands/variable/mod.rs`
- **Unit 3:** `list`, `get`, `set`, `rm` behavior modules
- **Unit 4:** Test harness helpers and integration tests
- **Unit 5:** Regenerated CLI docs + `variables.mdx` update
### Fabro Details
<details>
<summary>Ran 8 stages in 58m 50s for $23.81</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 9s | – | 0 |
| preflight_lint | 2m 21s | – | 0 |
| implement | 31m 34s | $18.44 | 0 |
| simplify_opus | 8m 48s | $2.60 | 0 |
| simplify_gpt | 4m 1s | $2.77 | 0 |
| verify | 9m 17s | – | 0 |
| **Total** | **58m 50s** | **$23.81** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
|
||
|
|
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) |
||
|
|
352b7c5de4
|
refactor: remove devcontainer support (#433)
## Summary Remove devcontainer support from the product surface and codebase: the parser crate, workflow bridge, lifecycle execution path, typed events, CLI progress rendering, generated client field, and public/internal documentation references are all gone. ## What Changed - Deleted the dedicated parser crate and removed its Cargo dependencies and lockfile entries. - Removed workflow initialization paths that resolved repository devcontainer metadata, applied Daytona snapshots from it, merged environment variables from it, or ran its lifecycle commands. - Removed the typed event variants and CLI progress handlers for the retired lifecycle events while leaving shared unknown-event handling intact. - Cleaned the generated TypeScript client and tracked docs so repository search has no remaining devcontainer references outside git history. ## Verification - `cargo +nightly-2026-04-14 fmt --all` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo build --workspace` - `cargo nextest run -p fabro-types` - `cargo nextest run -p fabro-workflow` - `cargo nextest run -p fabro-cli run_progress` - `cd lib/packages/fabro-api-client && bun run generate && bun run typecheck` - `cargo metadata --no-deps --format-version 1 | rg -i "fabro-devcontainer|devcontainer"` - `rg -n -i "devcontainer|dev container|dev-container|dev_container|fabro-devcontainer|\\.devcontainer" . --glob '!target/**' --glob '!.worktrees/**'` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
cf124be413
|
fix(types): finish image.ref → image.docker rename in env var substitution
Commit
|
||
|
|
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/)
|
||
|
|
5d6cd48e9e
|
Replace vague expect/panic messages with invariant-explaining messages (#422)
Production code must not panic without a clear explanation of *why* the
failure is impossible. This PR upgrades panic-adjacent messages across
the codebase to meet that standard, and converts two genuine runtime
panics into proper error handling.
## What changed
**Invariant-explaining `expect` messages** — all existing `expect("short
label")` calls that guarded hard-coded literals, just-inserted map
entries, just-pushed Vec elements, or hard-coded regex/template strings
now carry a sentence explaining *why* the None/Err path cannot be
reached (e.g. `"node was just inserted by ensure_node, so get_mut cannot
return None"`). No behavior changes.
**`assert_eq!` → `panic!` with justification** in `strategy.rs` — the
bare assert is replaced with an explicit `panic!` whose message names
every existing call site that enforces the `CodexDevice ↔ OpenAI`
invariant, making future regressions easier to diagnose.
**Genuine runtime errors converted to `Result`** — `select_backend` /
`select_backend_for_gh_command` in the upgrade command previously called
`.expect()` on `http_client()`, which can fail due to TLS or environment
issues. Both functions now return `Result<Backend>` and propagate the
error to the CLI boundary.
**Signal handler panics degraded to warnings** in `serve.rs` —
`ctrl_c()` and `unix::signal()` failures no longer panic the server;
instead they log a warning and park the future, allowing the server to
keep running without graceful-shutdown support rather than crashing on
startup.
**Telemetry thread spawn failure** in `fabro-telemetry` — instead of
panicking, a failure to spawn the background thread logs a debug message
and silently disables telemetry, which is the correct degradation for an
optional observability feature.
**OS RNG `expect` messages** — three sites (`random_secret`,
`random_auth_code`, `generate_dev_token`) now explain that a failure
means the system RNG is broken and the security of the generated value
would be compromised, justifying the panic boundary.
### Fabro Details
<details>
<summary>Ran 3 stages in 45m 16s for $11.65</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| work | 34m 22s | $9.00 | 0 |
| audit | 10m 35s | $2.65 | 0 |
| **Total** | **45m 16s** | **$11.65** | **0** |
</details>
<details>
<summary>Ran <code>Goal.fabro</code> (4 nodes and 5 edges)</summary>
```dot
digraph Goal {
graph [
goal="Complete the user-provided goal",
rankdir=LR,
max_node_visits=30
]
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
work [
label="Work",
thread_id="goal",
fidelity="full",
max_visits=12,
prompt="@prompts/continue.md"
]
audit [
label="Completion Audit",
thread_id="goal",
fidelity="full",
goal_gate=true,
retry_target="work",
output_schema="routing",
output_retries=2,
max_visits=12,
prompt="@prompts/audit.md"
]
start -> work -> audit
audit -> exit [label="Done", condition="outcome=succeeded"]
audit -> work [label="Continue", condition="outcome=failed || preferred_label=Continue"]
audit -> work [label="No clear verdict"]
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
5eb3eb1a23
|
fix(server): allow GitHub avatars in CSP img-src
Recent CSP enforcement blocked avatars.githubusercontent.com images used by run cards in the web UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ab68fc27d1
|
fix(workflow): preserve usage across session compaction (#420)
## Summary Fixes shared-thread workflow stages that compact their session before routing/audit bookkeeping finishes. The workflow backend now records token usage from each `Session::process_input` call as it happens, instead of slicing assistant turns out of the final session history after the session may have been compacted or replaced. ## Changes - Track per-input token usage inside `fabro-agent::Session` alongside the existing timing data. - Use the recorded per-input usage in the workflow LLM backend for initial prompts, retry-after-compaction prompts, and schema repair prompts. - Keep the invariant panic message for inconsistent session history explicit with `expect(...)`. - Add a black-box workflow integration test that drives a shared-thread audit through pre-routing compaction and asserts the audit still succeeds. ## Verification - `ulimit -n 4096 && cargo nextest run --workspace` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` - `cd apps/fabro-web && bun test --isolate` - `cd apps/fabro-web && bun run typecheck` - `cd lib/packages/fabro-api-client && bun run typecheck` - After rebasing onto current `origin/main`: `ulimit -n 4096 && cargo nextest run -p fabro-workflow --test it integration::shared_thread_compaction_before_routing_audit_succeeds` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
535cbda355
|
fix(server): enforce content security policy (#421)
## Summary Enforces Fabro's CSP by switching from `Content-Security-Policy-Report-Only` to `Content-Security-Policy` while preserving the SPA sources we know are required. The policy now hashes the install-mode inline bootstrap and allows `ws:`/`wss:` connections so terminal WebSockets do not regress under enforcement. The security headers integration test now asserts enforced CSP behavior, and the public security docs now describe the default headers Fabro emits. ## Verification - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo test -p fabro-server csp::tests --lib` - `cargo test -p fabro-server security_headers::tests --lib` - `cargo test -p fabro-server --features test-support --test it security_headers_are_applied_to_all_responses` - Browser QA against an enforced local server: login, runs list, settings, and automation diagram rendered with no CSP console violations or page errors. - Live listener on `127.0.0.1:32276` restarted and verified to emit `content-security-policy` with no report-only header. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) |
||
|
|
38695d7e89
|
fix(server): normalize default ports in terminal origin check (#417)
## Summary The terminal WebSocket origin check (`origin_allowed` in `handler/sandbox.rs`) rejected browser requests when the `Host` header omitted the default port for the scheme. Result: clicking the **Terminal** tab on `/runs/<id>/sandbox` returned **403 Forbidden** and the UI showed "Terminal WebSocket connection failed." Other tabs (Services, Filesystem, VNC) worked because their WebSockets either don't traverse the server (VNC connects directly to Daytona's signed preview URL) or aren't WebSocket upgrades. ## Root cause Browsers send `Origin: https://example.com` and `Host: example.com` (no `:443`) on default HTTPS. The previous logic always constructed the origin authority *with* the default port, then string-compared against the raw `Host` header: ```rust let origin_authority = match origin_url.port_or_known_default() { Some(port) => format!("{origin_host}:{port}"), None => origin_host.to_string(), }; origin_authority.eq_ignore_ascii_case(host) ``` So `"example.com:443"` got compared against `"example.com"` and never matched. Every browser-driven WS upgrade to a default-port HTTPS deployment failed. ## Fix Parse the `Host` header through the origin's scheme into another `Url`, then compare `host_str()` and `port_or_known_default()` on both sides. This normalizes default ports symmetrically. ```rust let Ok(host_url) = url::Url::parse(&format!("{}://{host}", origin_url.scheme())) else { return false; }; origin_url.host_str() == host_url.host_str() && origin_url.port_or_known_default() == host_url.port_or_known_default() ``` Reproduced in a production deployment of the nightly image behind Caddy doing TLS termination on a public IP. Before the fix the terminal WS handshake returned 403 every time; with the fix the handshake completes and the terminal session attaches. ## Tests Added four new cases alongside the existing two: - `origin_validation_allows_default_https_port_omitted_from_host` — the bug case (browser-style `Origin: https://host` + `Host: host`). - `origin_validation_allows_default_http_port_omitted_from_host` — same for plain HTTP. - `origin_validation_allows_explicit_default_port_in_host` — `Host: example.com:443` still matches `Origin: https://example.com`. - `origin_validation_rejects_scheme_mismatch_on_default_port` — `Origin: http://example.com` + `Host: example.com:443` is still rejected (different effective ports). All six `origin_validation_*` tests pass; the full `fabro-server` suite stays green (679/679). ## Test plan - [x] `cargo nextest run -p fabro-server origin_validation` — 6 passed - [x] `cargo nextest run -p fabro-server` — 679 passed - [x] `cargo +nightly-2026-04-14 fmt --check --all` - [x] `cargo +nightly-2026-04-14 clippy -p fabro-server --all-targets -- -D warnings` - [x] Manual: terminal tab in the SPA against a TLS-terminated default-port deployment 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
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) |
||
|
|
01892185ff
|
Replace bare unwrap() with documented expect() across production runtim… (#415)
Audit and remediation pass enforcing the project's
no-panic-in-production policy. Every `unwrap()` on a mutex/RwLock in
reachable runtime code is replaced with `expect()` carrying a message
that explains *why* the lock cannot be poisoned (no code panics while
holding it). Bare `unreachable!()` and `panic!()` calls are updated with
messages that name the invariant being asserted. One genuine bug is
fixed in the process.
## What changed
**`unwrap()` → `expect()` on locks** (`fabro-core`, `fabro-oauth`,
`fabro-util`, `fabro-workflow/*`, `fabro-server`): Every
`Mutex`/`RwLock` `.unwrap()` in production paths now carries the
standard justification pattern: `"<name> mutex/RwLock should not be
poisoned: no code panics while holding this lock"`.
**`unreachable!()` and `panic!()` message quality**: Bare
`unreachable!()` calls in `subagent.rs`, `wait.rs`, `condition.rs`,
`event/convert.rs`, and `server.rs` now name the structural invariant
(e.g. "outer match arm already verified…"). The `panic!` in `tools.rs`
now includes the offending name and the expected format, making it
actionable.
**`sha_newtype` / `short_sha_newtype` in `run_files.rs` — actual bug
fix**: These helpers previously called `unwrap_or_else(|e| panic!(…))`
on git output, meaning a malformed SHA from a real git subprocess would
panic in a request handler. They now return `Result<T, ApiError>` and
propagate errors to callers, which in turn propagate with `?`. This is
the only change that alters observable behavior under failure.
**Demo-only panics in `fabro-server/src/demo/mod.rs`**: Panic messages
updated to clarify that these paths operate on hardcoded compile-time
constants, so the panic is a programming-error guard rather than a
runtime failure guard.
## Design note
The lock-poisoning `expect` messages all follow a single template so
reviewers can quickly verify the claim: if you ever add code that can
panic inside a lock guard scope, the message becomes a lie and that must
be caught in review. The uniformity is intentional.
### Fabro Details
<details>
<summary>Ran 0 stages in 64m 54s for $14.28</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **64m 54s** | **$14.28** | **0** |
</details>
<details>
<summary>Ran <code>Goal.fabro</code> (4 nodes and 5 edges)</summary>
```dot
digraph Goal {
graph [
goal="Complete the user-provided goal",
rankdir=LR,
max_node_visits=30
]
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
work [
label="Work",
thread_id="goal",
fidelity="full",
max_visits=12,
prompt="@prompts/continue.md"
]
audit [
label="Completion Audit",
thread_id="goal",
fidelity="full",
goal_gate=true,
retry_target="work",
output_schema="routing",
output_retries=2,
max_visits=12,
prompt="@prompts/audit.md"
]
start -> work -> audit
audit -> exit [label="Done", condition="outcome=succeeded"]
audit -> work [label="Continue", condition="outcome=failed || preferred_label=Continue"]
audit -> work [label="No clear verdict"]
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|
|
6a94970b21
|
fix(model): retire GPT-5.2 and GPT-5.3 catalog entries (#412)
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
## Summary Retires the built-in OpenAI catalog rows for GPT-5.2 and GPT-5.3-era models while preserving compatibility through aliases on the closest remaining replacements. `gpt-5.2`, `gpt5`, `gpt-5.3-codex`, and `codex` now resolve through `gpt-5.4`; `gpt-5.3-codex-spark` and `codex-spark` now resolve through `gpt-5.4-mini`. The Rust tests that pinned individual declarative catalog rows were removed so future catalog updates stay data-only. ## Verification - `cargo nextest run -p fabro-model` - `cargo nextest run -p fabro-server list_models` - `cargo +nightly-2026-04-14 fmt --check --all` --- [](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>
|
||
|
|
37f1d26d4c
|
Fix stage inference/tool timing reporting (was always zero) (#408)
## Summary Every `stage.completed` and `run.completed/failed` event has reported `inference_time_ms: 0, tool_time_ms: 0` since timing fields were wired up in #343. Two independent bugs caused this: handlers never populated `Outcome.timing`, and engine-failure terminal paths discarded the rolled-up conclusion entirely. ## What changed and why ### Bug 1 — Handlers never populated `Outcome.timing` **`fabro-agent/session.rs`**: Added `SessionInputTiming { inference: Duration, tool: Duration }` accumulators to `Session`. `run_single_input` now takes a `&mut SessionInputTiming` and records elapsed time at every exit point of the `'streamattempts` loop (stream open, retry, cancel, error, normal completion) plus a `tool_start` / `tool_elapsed` wrap around `execute_tool_calls`. The per-input total is exposed via `session.last_input_timing()` after `process_input_with_runtime` returns, even on error. **`CodergenResult::Text`**: Added a `timing: StageTiming` field. All backends now populate it: - `AgentApiBackend::run` accumulates `session.last_input_timing()` across inputs and any structured-output repair turns (repair turns now use `process_input_with_runtime` instead of `process_input` so timing is captured there too). - `AgentApiBackend::one_shot` wraps `complete_one_shot_request` with `Instant`/`elapsed` across repair iterations; all time is attributed to inference. - `AgentAcpBackend::run` uses `result.duration_ms` attributed entirely to inference (ACP is opaque about the split). **`AgentHandler`, `PromptHandler`, `FanInHandler`, `CommandHandler`**: Each now sets `outcome.timing = Some(timing)` from the backend result before returning. `CommandHandler` attributes `result.duration_ms` to tool time (`StageTiming::active_only(0, duration_ms)`). The failure branches (structured-output exhausted retries) also carry timing forward so no timing is lost on partial success. **`StageTiming::active_only`**: New constructor added to `fabro-types` for the handler→executor hop where wall time is ignored (executor's own stopwatch is authoritative for wall). ### Bug 2 — Engine-failure paths discarded the conclusion **`start.rs`**: Introduced `emit_workflow_run_failed` as a shared helper that calls `build_conclusion_from_store` (which already does the full per-stage rollup) and uses `conclusion.timing` and `conclusion.billing` when emitting `WorkflowRunFailed`, instead of `RunTiming::wall_only(...)` and `None`. All three terminal failure paths now go through this helper: - `persist_terminal_engine_failure` — main `VisitLimitExceeded`/engine-error path - `DetachedRunBootstrapGuard::drop` — takes `RunStoreHandle` as a new field (cloned in at arm time) - `DetachedRunCompletionGuard::drop` — same - `persist_detached_failure` — now accepts `&RunStoreHandle` and delegates to `emit_workflow_run_failed` ### Refactoring `test_usage` helper was duplicated across `billing_rollup` and `event/convert` test modules; both now import from `crate::test_support`. `scheduler_capacity` predicate (`counts_toward_scheduler_capacity`) was extracted from the inline closure in `spawn_scheduler` and reused in the `GET /system/info` handler for the new `scheduler_slots_used` field — a pre-existing separate fix included in this changeset. ### Plan Summary - **A1** — `SessionInputTiming` accumulators in `Session`; `last_input_timing()` getter - **A2** — `CodergenResult::Text { timing }` field; all three backends populate it - **A3** — All four active-work handlers (`agent`, `prompt`, `fan_in`, `command`) set `outcome.timing` - **B1** — `persist_terminal_engine_failure` uses conclusion's rolled-up timing + billing - **B2** — Both drop guards and `persist_detached_failure` also use `emit_workflow_run_failed` ### Fabro Details <details> <summary>Ran 8 stages in 76m 38s for $39.81</summary> | Stage | Duration | Cost | Retries | |---|---|---|---| | start | 0s | – | 0 | | toolchain | 1s | – | 0 | | preflight_compile | 2m 3s | – | 0 | | preflight_lint | 2m 18s | – | 0 | | implement | 35m 53s | $17.74 | 0 | | simplify_opus | 22m 51s | $17.18 | 0 | | simplify_gpt | 4m 0s | $4.89 | 0 | | verify | 8m 59s | – | 0 | | **Total** | **76m 38s** | **$39.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) |