Commit graph

23 commits

Author SHA1 Message Date
fabro-sh-0530[bot]
79f89165f6
Wire end-to-end steering for running agents (#209)
## Summary
This makes the advertised mid-run steering path real: users can send
append or interrupt steering messages through the API, CLI, and web UI,
and the worker delivers them to live API-mode agent sessions or buffers
them for the next session. The change adds the control protocol, session
interrupt machinery, workflow hub, server route/OpenAPI/client updates,
and UI feedback needed for the whole path.

### Plan Summary
- Add `SteerKind`/`run.steer` wire protocol and `POST /runs/{id}/steer`
- Deliver steers through subprocess JSONL or the in-process
`SteeringHub`
- Support append and interrupt behavior in agent sessions, with bounded
buffering and events
- Expose steering in the CLI/web UI and surface SSE toasts

## Flow

```mermaid
flowchart TB
  UI["CLI / Web UI"] --> API["POST /runs/{id}/steer"]
  API -->|"subprocess transport"| Control["Worker control JSONL"]
  API -->|"in-process transport"| Hub["SteeringHub"]
  Control --> Hub
  Hub -->|"active API sessions"| Session["SessionControlHandle"]
  Hub -->|"no active session"| Pending["Pending buffer"]
  Pending -->|"first future API session"| Session
  Session --> Agent["Session round loop"]
  Agent --> Events["RunEvent stream"]
  Events --> UI
```

## What changed and why

- Agent sessions now expose a lightweight `SessionControlHandle`, drain
steering at the top of each round, and use a replaceable round
cancellation token for interrupts. LLM waits are cancelled promptly,
while tool execution observes cancellation cooperatively so every
committed `tool_use` still gets a matching `tool_result`.
- `SteeringHub` owns active API session registration, broadcast
delivery, pending buffering, FIFO queue caps, and steering
lifecycle/drop events. A completion coordinator closes the
final-response race without introducing a workflow dependency into the
agent crate.
- The server route replaces the 501 stub, validates run state and
best-effort CLI-only steerability, and forwards through either
subprocess control JSONL or the in-process hub. OpenAPI and generated
clients now include the request type.
- The CLI and web UI can send append or interrupt steers. Run detail and
board views open the new composer, and shared SSE subscriptions now
support per-subscriber event callbacks so invalidation and steering
toasts can coexist on one EventSource.

## Review notes

- Steering actors stay on top-level `RunEvent.actor`; event props only
carry steering kind/drop metadata.
- Buffered steers replay as append messages to the first API session
that registers after an empty-active period. Per-stage targeting remains
out of scope.
- CLI-mode agent stages are still not steerable; the server returns a
best-effort 409 when all active agent stages are CLI-mode, while the
worker hub remains the authoritative safety net.
- No persistence or schema migration is required; active and pending
steering state is in memory.
- New tests focus on protocol round-trips, hub buffering/bounds, session
steering-loop behavior, SSE fanout, and basic server rejection paths.

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:34:16 -04:00
Bryan Helmkamp
4b100d350d
chore: add plan 2026-05-04 16:09:12 -04:00
Bryan Helmkamp
92cfcbde71
chore: update plan 2026-05-04 12:55:19 -04:00
Bryan Helmkamp
e0594a86bd
chore: add plan 2026-05-04 11:12:40 -04:00
Bryan Helmkamp
be0b5829b9
chore: add plan 2026-05-04 11:12:40 -04:00
Bryan Helmkamp
5c60fe8182
chore: plans 2026-05-03 13:10:04 -04:00
Bryan Helmkamp
bfb6bdb25c
fix(cli): bound server readiness probes
Use HTTP health checks with short deadlines for managed server readiness and add finite control-plane request timeouts for CLI/server clients. Keep stream bodies uncapped so SSE attach flows can remain long-lived.
2026-05-02 20:13:18 -04:00
Bryan Helmkamp
ae5ccb5ce2
refactor(workflow): split event module by responsibility
Keep fabro_workflow::event as the public facade while moving event conversion, names, redaction, sink, emitter, stored-field helpers, and StageScope into focused modules. Co-locate the existing event tests with the moved code and update the events strategy docs for the new module layout.
2026-05-02 14:52:10 -04:00
Bryan Helmkamp
2f7aeba417
fix(workflow): preserve exec failure diagnostics
Add bounded redacted exec output tails to failure events while keeping tracing log-safe. Centralize tail projection on ExecResult and thread diagnostics through metadata, setup, devcontainer, and CLI install failures.
2026-05-01 08:45:50 -04:00
Bryan Helmkamp
7e62dae28b
chore: add plans 2026-04-30 15:04:44 -04:00
Bryan Helmkamp
fb51c1485b
docs(plans): add command output streaming and type unification plans
Stage the two 2026-04-30 plan documents: command output streaming with
CAS log storage, and the duplicate-type unification rollup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 08:36:00 -04:00
Bryan Helmkamp
4a4f0f8548
feat(workflow): emit metadata snapshot events
Add typed metadata snapshot events around init, checkpoint, and finalize archive writes so run logs expose durable metadata timing and failures. Include snapshot accounting, CLI rendering with compatibility-notice suppression, and event documentation.
2026-04-29 19:31:13 -04:00
Bryan Helmkamp
bebf472ad2
chore: move testing-strategy doc into docs-internal
The strategy doc lived alone under files-internal/ while every sibling
strategy doc (logging, events, server-secrets) lived under docs-internal/.
Move it next to the others and update plan/spec references.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:38:41 -04:00
Bryan Helmkamp
e6e091fe8e
refactor(server): lock down server secrets 2026-04-23 07:15:27 -04:00
Bryan Helmkamp
537a5125cb
feat(auth): tighten server auth surface with single origin
Implements plan: single origin, drop CLI preflight, gate demo toggle.
Removes loopback client target and CLI auth config preflight endpoint;
adds canonical_origin module on the server; regenerates SPA and TS API
client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:38:31 -04:00
Bryan Helmkamp
5958470a11
plan 2026-04-19 15:18:22 -04:00
Bryan Helmkamp
224673bdf6
docs(install): address sixth review pass on web-install spec
Two fixes:

P0 — Container packaging blocks install mode. The published Dockerfile
bakes /etc/fabro/settings.toml and sets FABRO_CONFIG, which under the
explicit-config carveout means containers would never enter install
mode. v1 must change the Dockerfile: drop the baked settings file,
drop FABRO_CONFIG, set FABRO_STORAGE_DIR=/storage, and persist
~/.fabro across container restarts (recommendation: move FABRO_HOME
into a subdirectory of the /storage volume so one mount covers both
config and data). Spelled out as load-bearing v1 implementation work
under Orchestration config updates. New decision-log row #24.

P2 — Force-foreground decision was not carried through to all sections.
Two leftover references to a `__serve` daemon child contradicted the
"install mode never daemonizes" decision — one in the local-lifecycle
prose, one in the manual smoke test. Updated both to describe the
foreground process exiting cleanly. Decision-log row for #21 also
updated to reflect that the install process IS the operator's `fabro
server start` invocation under foreground mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:50:44 -04:00
Bryan Helmkamp
d34f338e33
docs(install): address fifth review pass on web-install spec
Two fixes:

P1 — Install token surfacing in local daemon mode. Today's
`fabro server start` daemonizes by default; the daemon parent prints
its own summary but the child's stderr (where the install token would
print) is redirected to server.log. Operator wouldn't see the token
without tail-ing the log file. Decision: install mode forces foreground
regardless of how `fabro server start` was invoked; the token then
lands on the operator's terminal directly. Documented as "--foreground
is implicit during install." Daemon path is bypassed entirely for
install mode; restored on the supervisor restart.

P2 — `--no-web` contract hole. The flag is accepted by `server start`
and `server restart` but the spec didn't say what install mode does
with it. Decision: ignore during install with an explicit stderr
warning ("will be respected on next start"); respected after the
supervisor restart. Rejecting would force supervised-deployment
operators to either drop the flag or `docker exec` to run the CLI
wizard, defeating the point. Warning makes the override visible.

Added integration tests for both behaviors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:50:44 -04:00
Bryan Helmkamp
d21d9fa62f
docs(install): address fourth review pass on web-install spec
Three more fixes (all P2):

P2 — Goal wording said "using the same persistence helpers" but the
body explicitly carves out a separate install-mode vault path. Reword
to "same on-disk state, sharing the TOML/env primitives" so the
implementer isn't misled about how much of the CLI path is reused.

P2 — Summary said only `fabro server start` enters install mode but
the process model says start and restart. Reconcile: name both
commands explicitly in the summary.

P2 — Test plan covered the GitHub App `state` rejection path but not
the happy-path roundtrip (POST /install/github/app/manifest → GET
/install/github/app/redirect with stubbed conversion). Add an
integration test that covers the riskiest new path: code-exchange
wiring, session population, redirect-with-token handling, and that
the canonical-URL ordering decision actually flows through to the
manifest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:50:44 -04:00
Bryan Helmkamp
0a576a5f74
docs(install): address third review pass on web-install spec
Three more fixes:

P0 — Local restart UX. Today's `fabro server start` does not stay
around to supervise the `__serve` child it spawns (`start.rs:318`), so
no auto-restart happens locally after `/install/finish` exits. Spec the
two cases honestly: supervised deployments rely on the supervisor;
local laptops show the operator a "run `fabro server start` to launch
your configured server" message after a 30s polling timeout. A built-in
local supervisor is named as a follow-up. Updated the manual-test
section to cover both cases and the orchestration-docs section to
detect supervised vs. local at boot time.

P1 — Auto-start callers must not enter install mode. `connect_server`
→ `connect_api_client_bundle` → `start::ensure_server_running_for_storage`
is used by `run attach`, `server runs`, etc. Add an explicit *Auto-start
callers* subsection and a new decision: only the explicit `fabro server
start` (or `restart`) command enters install mode. Auto-start callers
fail with a clear "configure first" message pointing the operator at
either `fabro server start` or `fabro install`.

P2 — Process-model rationale corrected. The previous draft claimed the
existing dispatch path "would error on missing settings.toml" but that
is false: `user.rs:77` returns defaults, `serve.rs:820-824` falls back
to a Unix socket, and `tests/it/cmd/server_start.rs:111` is a passing
test of `fabro server start` with no config. Reworded to say what is
actually true — without the fork, `fabro server start` cheerfully boots
a non-functional default server, and install mode displaces that
default. Summary line now mentions the explicit-config caveat too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:50:44 -04:00
Bryan Helmkamp
d70bc7a166
docs(install): address second review pass on web-install spec
Five more fixes:

P0 — Reorder wizard: Server config now precedes GitHub. The GitHub
manifest bakes <canonical_url> into redirect_url and callback_urls;
creating the App with a misdetected URL is a real-world side effect we
cannot unmake on github.com.

P0 — `persist_install_outputs` cannot be reused as-is from install
mode. Its vault path goes through `connect_api_client(storage_dir)`,
which calls back into the install-mode server itself (which doesn't
mount /api/v1/*) and would 404. Add an explicit decision: install mode
writes vault secrets directly to disk via Vault::load(...).set(...),
the same pattern persist_github_install_changes already uses. TOML and
env-file helpers remain reusable.

P1 — Bootstrap fork narrowed. Install mode triggers only when no
explicit --config or FABRO_CONFIG was provided AND the default
~/.fabro/settings.toml is absent. A typo in --config must error, not
silently install on top of the wrong target. Matches the asymmetry the
existing config loader already enforces (user.rs:81-112).

P1 — Stop overpromising rollback. The existing helper restores
settings.toml on vault failure but leaves server.env in place (verified
by install.rs:2910). Spec out the actual partial-state semantics for
v1, justify why it's acceptable (env keys are deterministic and
idempotent on retry), and call atomic rollback a deliberate follow-up.

P2 — On-disk layout corrected. Vault path is
<storage_dir>/vaults/default/secrets.json (storage.rs:38), not
<storage_dir>/secrets/.... Added the home-level dev-token file the CLI
also writes (install.rs:1994-1999) so parity is real.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:50:44 -04:00
Bryan Helmkamp
7b9c652548
docs(install): address plan review on web-install spec
Five fixes against the v1 spec:

P0 — Reconcile GitHub App callback flow with the CLI's actual
mechanism: manifest `redirect_url` (not `callback_urls`) carries the
post-creation handoff via browser 302; the install endpoint is renamed
to `/install/github/app/redirect` and authorized by OAuth `state`
because GitHub strips Authorization across redirects.

P1 — Bootstrap fork moves from "precheck inside serve" to the dispatch
layer, since today's `commands::server::dispatch` loads settings before
`serve` is invoked. Spec out the install bootstrap path explicitly,
including skipping the eager dev-token / session-secret creation.

P1 — Clarify that the same `fabro-web` bundle hosts the wizard via a
server-injected `window.__FABRO_MODE__` flag in `index.html` controlling
which router tree mounts at boot. Without this, existing route loaders
that call `/api/v1/auth/*` would throw before the install UI renders.

P2 — Correct the dev-token path to `<storage_dir>/server.dev-token`
(matching `Storage::server_state().dev_token_path()`).

P2 — Resolve the dev-token "never exposed to the client" contradiction:
JWT keys and session secret stay on the server; the dev token is
returned in the `/install/finish` response so the operator can copy it.

P2 — Note that the existing OpenAPI conformance test only covers
`build_router(...)` and would silently miss install drift. Spec the
expansion: split spec iteration by `install` tag, route to the
appropriate router, and verify cross-mounting is rejected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:50:44 -04:00
Bryan Helmkamp
5520785723
docs(install): spec the web-based install wizard
Captures the design for browser-driven first-run configuration as an
alternative to `fabro install`. When `fabro server` boots without
`~/.fabro/settings.toml`, it enters install mode, prints a one-time
token, and serves a wizard from the existing `fabro-web` bundle.
Reaches the same on-disk end state as the CLI, then exits cleanly so
the supervisor restarts into normal mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:50:44 -04:00