Commit graph

2582 commits

Author SHA1 Message Date
Bryan Helmkamp
55c396d2ff
feat(cli+web): wire archived into listing visibility
The CLI's store-run lookup now passes `include_archived=true` so resolve
and bulk operations (archive, unarchive, rm, inspect, rewind) can still find
archived runs. The web UI's hand-maintained `RunStatus` union and display
map learn `archived` with a gray style so archived runs render correctly.
Default `fabro ps` continues to hide archived via `is_active()`; `-a`
shows everything including archived.
2026-04-19 16:58:55 -04:00
Bryan Helmkamp
c78c1fd17b
feat(server): demo-mode stub for /runs/{id}/files
Replaces the not_implemented placeholder in the demo router with a
demo::list_run_files_stub that returns a small illustrative
three-file diff (modified, added, renamed) matching the real handler's
PaginatedRunFileList wire shape. The stub ignores run_id and state so
demo mode and real mode cannot cross-contaminate (R34).

Unit 10 (frontend rendering paths) will remove the now-obsolete
client-side fallbackFiles fixture when it rewrites run-files.tsx.

Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:57:25 -04:00
Bryan Helmkamp
8bf243fe47
feat(cli): add fabro archive and fabro unarchive commands
Two new top-level commands mirror `fabro rm`'s bulk-by-ID shape: positional
run identifiers, per-ID success/error aggregation, and a final non-zero exit
if any item failed. Calls the new server endpoints from Unit 5. Shared bulk
loop covers both directions and emits structured JSON with an `archived` or
`unarchived` list alongside `errors`. Top-level help snapshot updated.
2026-04-19 16:56:53 -04:00
Bryan Helmkamp
04c14f06d0
feat(server): degraded final_patch fallback for Run Files
Extends the Run Files handler with the patch-only fallback branch.
When the sandbox is unreachable (reconnect failed, provider not
compiled in, or the base revision has been garbage-collected), the
response now:

- Reads RunProjection.final_patch (captured at run end by Unit 2
  for both Success/PartialSuccess and now Failed runs)
- Caps the patch at 5 MiB on a UTF-8 char boundary
- Filters denylisted file sections out via a regex-level `diff --git`
  header scan (no full patch parser; the placeholder line kept so
  clients still render the surrounding context)
- Picks the right degraded_reason: provider_unsupported for Docker-
  provider runs this build can't reconnect to, sandbox_gone for
  terminal runs, sandbox_unreachable for still-running ones
- Populates meta.to_sha from conclusion.final_git_commit_sha and
  meta.total_changed from a `diff --git` header count

When final_patch is absent (old Failed runs, projection write
failures), returns the empty envelope that the UI maps to R4(c).

Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:55:58 -04:00
Bryan Helmkamp
03fc375cdd
feat(server): add archive/unarchive endpoints and include_archived listing
Extends the OpenAPI spec with POST /api/v1/runs/{id}/archive and /unarchive
operations, adds `archived` to the RunStatus enum, and adds an
`include_archived` query param to listRuns. Regenerates the progenitor-built
Rust types and the typescript-axios client. Implements `archive_run` and
`unarchive_run` handlers via `operations::archive/unarchive`, and extends
`list_runs` to filter archived runs unless opted in. Archived runs continue
to map through `api_status_from_workflow` and bypass the board column.
2026-04-19 16:53:33 -04:00
Bryan Helmkamp
62126f07a9
feat(server): real GET /runs/{id}/files handler (sandbox path)
Implements the sandbox branch of the Run Files Changed endpoint. When
a run has a reachable sandbox, the handler:

- Parses the run_id and authenticates via AuthenticatedService
- Rejects any non-default from_sha/to_sha (v1 reserves them)
- Validates SHA format with a 7-40 hex regex before use
- Returns 404 for both missing-run and unauthorized access so
  run-ID enumeration is not possible (IDOR-safe)
- Reconnects to the sandbox via a new try_reconnect_run_sandbox that
  returns Ok(None) for the reconnect-failed case (Unit 6 will insert
  the final_patch fallback there instead of today's empty envelope)
- Enumerates changes via list_changed_files_raw + list_binary_paths,
  batched blob fetching via stream_blob_metadata / stream_blobs
- Applies an inline sensitive-path denylist first (Unit 8 extracts),
  then a 200-file count cap, per-file 256 KiB cap, and 5 MiB
  aggregate cap - truncated entries carry an explicit
  truncation_reason
- Builds a single tracing::info! span at response end with only the
  allowlisted fields (run_id, file_count, bytes_total, duration_ms,
  truncated, binary_count, sensitive_count, symlink_count,
  submodule_count) -- no paths, contents, or git stderr

All calls go through the Unit 4 coalescing primitive, so concurrent
viewers of the same run share one materialization.

Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:52:33 -04:00
Bryan Helmkamp
cd42d9bb8e
chore(spa): refresh embedded bundle for the real-mode UI cleanup
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:52:06 -04:00
Bryan Helmkamp
ce85151b37
feat(server): reject mutations on archived runs with actionable error
Adds an `archived → unarchive first` guard to every mutation entry point
that could otherwise hit an opaque 409 or confusing 404 on an archived run:
start, cancel, pause, unpause, submit_answer, and append_run_event server
handlers; the resume operation; and the rewind CLI command. append_run_event
also rejects client-injected `run.archived` and `run.unarchived` bodies so
lifecycle transitions cannot bypass the operations layer. Worker-emitted
run.completed / run.failed events still flow through as before. Fork reads
from the source's metadata branch only — no source mutation — so no guard
is needed there.
2026-04-19 16:44:23 -04:00
Bryan Helmkamp
f8c560a9a6
refactor(test): promote shared server-lifecycle test helpers into fabro-test
Move wait_for_path, wait_for_log_line, stop_pid, server_log_files, and
isolated_storage_dir out of the three integration test files that duplicated
them and into fabro-test's public surface next to apply_test_isolation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:43:26 -04:00
Bryan Helmkamp
e5370c1d18
feat(server): per-run request coalescing primitive
Adds the concurrency primitive the upcoming GET /runs/{id}/files
handler needs so concurrent viewers of the same run share one
sandbox-git materialization (different runs still materialize in
parallel).

Design notes:
- Materialization runs on a detached tokio::spawn so an abandoned
  caller cannot leave orphan git subprocesses in the sandbox
- tokio::sync::watch is used (not broadcast) so late subscribers that
  arrive after the value is sent still see it via the cached `borrow`
- AssertUnwindSafe().catch_unwind() turns materializer panics into
  500 ApiErrors for every concurrent caller; a subsequent request on
  the same run_id then triggers a fresh materialization (no poisoning)
- ApiError::Clone is derived so the shared Arc<Result<T, ApiError>>
  can fan out cheap copies

The FilesInFlight registry is now a field on AppState; Unit 5 will
consume it from the real handler.

Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:40:39 -04:00
Bryan Helmkamp
a256c14a77
refactor(cli): collapse duplicated bootstrap and settings helpers
- Merge prepare_foreground_server_bootstrap and prepare_server_sink_bootstrap
  into one prepare_server_bootstrap(config, storage, foreground).
- Drop three one-line settings_layer_* passthroughs from user_config; callers
  now use load_settings_with_{storage_dir,config_and_storage_dir} directly.
- Swap underscore-prefixed lock field for #[expect(dead_code, reason=…)] to
  document RAII intent explicitly.
- Remove two narrate-what-it-does comments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:40:33 -04:00
Bryan Helmkamp
34e1d806a6
feat(workflow): add archive/unarchive operations
Centralizes the terminal-only precondition, idempotent behavior, and
event emission for archiving and unarchiving runs. Both operations
return typed outcomes distinguishing a real transition from an idempotent
no-op. `unarchive` reads prior_status from the projection (populated by
the RunArchived apply arm) rather than scanning the event log, so replay
stays pure append-and-apply.
2026-04-19 16:37:19 -04:00
Bryan Helmkamp
8f7afd4bfc
feat(workflow): machine-readable sandbox git helpers for Run Files
Adds sandbox-side helpers the upcoming GET /runs/{id}/files handler
needs to produce structured diff entries without a full unified patch:

- list_changed_files_raw: git diff --raw -z --find-renames=50%,
  returns RawDiffEntry variants (Added/Modified/Deleted/Renamed/
  Symlink/Submodule) with SHA-addressed blob references; paths are
  metadata only and never re-interpolated into shell
- list_binary_paths: git diff --numstat text/binary classifier so
  binary blobs are never piped through cat-file
- stream_blob_metadata / stream_blobs: batched git cat-file
  --batch-check / --batch driven by printf into stdin, avoiding
  per-file RPC storms for 200-file runs
- DiffError discriminates Transient (timeout, process kill) from
  Permanent (bad/invalid revision, unknown object) so the server can
  surface 503 vs fall through to the patch-only fallback

All new invocations use a hardened git prefix (core.hooksPath=/dev/null,
protocol.file.allow=never, core.fsmonitor=false) plus a small env
hardening map (GIT_TERMINAL_PROMPT=0, GIT_EXTERNAL_DIFF cleared) and a
10 s timeout per R32.

Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:35:20 -04:00
Bryan Helmkamp
858e8e1270
fix(cli): unify server state and logging under storage
Route server-owned logs to <storage>/logs/server.log from the start of
tracing, remove legacy home/config ownership paths, and fail fast when
a running legacy daemon is detected instead of silently proceeding.

This also adds the missing sink-resolution, truncate/append,
concurrency, legacy-config, and uninstall regression coverage for the
home/storage cleanup plan.
2026-04-19 16:34:27 -04:00
Bryan Helmkamp
54ddaa2cee
feat(install): redesign web wizard and scope dev token to PAT installs
Redesign the install wizard for clarity:
- swap the sidebar layout for a centered column and a horizontal stepper
- make completed/current stepper entries clickable links
- reorder steps so Server URL precedes LLMs
- use env-var placeholders (ANTHROPIC_API_KEY, etc.) with
  per-provider "Where do I get this?" disclosures
- replace the readonly "Validated username" input with a success pill
- drop the GitHub App name field (GitHub confirms the name anyway)
- re-label the GitHub App option and split review rows by strategy
- add a copy action to the Server URL on the review screen

Scope the dev token to PAT installs:
- only generate the dev token, write its files, and set FABRO_DEV_TOKEN
  inside the GithubInstallState::Token arm
- mark dev_token optional on InstallFinishResponse in the OpenAPI spec
- hide the Development token card on /install/finishing when absent
- add app_install_finish_omits_dev_token_and_does_not_write_it test

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:11:02 -04:00
Bryan Helmkamp
95b101a26f
lint(clippy): disallow blocking std::io and std::net on Tokio paths
Extends the workspace clippy.toml — which already bans std:🧵:sleep,
std:🧵:spawn, and std::process::Command::new on Tokio paths — with:

- disallowed-types: std::io::{Read, Write, BufRead, BufReader, BufWriter}
  and std::net::{TcpStream, TcpListener, UdpSocket}
- disallowed-methods: std::io::{stdin, stdout, stderr}

Non-blocking std::io items (Error, ErrorKind, Result, IsTerminal, Cursor)
remain allowed. std::fs is intentionally deferred.

Annotates ~24 pre-existing sync call sites with #[expect(..., reason = "...")]
matching the established pattern. All annotations describe why blocking I/O
is intentional in that context (sync CLI command, test helper, pre-fork
flush, etc.), so a future conversion to async will surface as an unfulfilled
lint expectation instead of silently drifting.

Fixes one real Tokio-path issue surfaced by the new lint:
fabro-cli's server-start daemon-health poller (try_connect) was a sync fn
called from async execute_daemon; std::net::TcpStream::connect_timeout
blocked a Tokio worker for up to 100ms per poll iteration. Converted to
tokio::net::{TcpStream, UnixStream} with tokio::time::timeout.

One follow-up flagged in-code: fabro-agent/src/cli.rs's JSON event writer
uses std::io::stdout() inside tokio::spawn. Annotated with a FOLLOW-UP
reason pointing at tokio::io::stdout; left unchanged since volume is low
and scope exceeded this pass.

Verified: clippy clean, cargo +nightly fmt --check clean, full nextest
workspace run (4131 passed, 182 skipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:06:02 -04:00
Bryan Helmkamp
beda6b00d8
feat(events): add run.archived and run.unarchived event variants
Adds `RunArchived` and `RunUnarchived` events end-to-end through the engine.
Internal `Event` carries `actor` (and `restored_status` on unarchive); wire
`EventBody` serializes as `run.archived`/`run.unarchived` with typed props.
Projection gains `prior_status: Option<RunStatus>` — `RunArchived` captures
the current status before switching to Archived; `RunUnarchived` applies the
event's `restored_status` payload (authoritative) and clears `prior_status`.
2026-04-19 15:42:39 -04:00
Bryan Helmkamp
296eca568b
feat(workflow): capture final_patch on RunFailed
Previously only Success/PartialSuccess outcomes captured the final
unified-patch string into the run projection. Failed runs left
RunProjection.final_patch empty, which meant the upcoming Files
Changed tab could not degrade to a patch-only view once the sandbox
was gone.

Extend on_run_end to run git diff on Failed too, with a tighter 10 s
timeout (vs 30 s on success) so a pathological workspace doesn't
stall downstream terminal notifications (Slack, SSE, CI). Plumb the
optional field through Event::WorkflowRunFailed, RunFailedProps, and
the projection.

Back-compat: final_patch is serde default-None, so pre-change events
in SlateDB replay cleanly as None. No backfill required; old Failed
runs show R4(c) empty state on the Files tab.

Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:39:17 -04:00
Bryan Helmkamp
ed5e3f1792
feat(types): add archived run status with split terminal/immutable guards
Adds `RunStatus::Archived` variant and splits the overloaded `is_terminal()`
into `is_terminal()` (reached terminal outcome) and `is_immutable()` (cannot
transition outbound). `can_transition_to()` now allows Succeeded|Failed|Dead to
and from Archived, preserving the `* -> Dead` escape hatch. Downstream
exhaustive matches in the CLI and server are updated with conservative Archived
arms; the server's public-enum mapping and board-column placement carry TODOs
for the OpenAPI update in a later unit.
2026-04-19 15:34:39 -04:00
Bryan Helmkamp
f65c7c3fd8
feat(api): add GET /runs/{id}/files spec + RunFilesMeta schema
Reintroduces the endpoint deleted in the April 5 server-only cleanup,
this time targeted at the web UI (not the CLI). Route registered with
not_implemented; real handler lands in Unit 5.

- FileDiff gains optional change_kind, truncated, truncation_reason,
  binary, sensitive fields (all additive, back-compat)
- New RunFilesMeta replaces PaginationMeta on PaginatedRunFileList
  (truncated, total_changed, to_sha, to_sha_committed_at, degraded,
  degraded_reason, patch, files_omitted_by_budget)
- from_sha / to_sha query params reserved for future use (non-default
  values 400 in v1)

Generated TS client picks up the new model; typecheck + openapi
conformance tests pass. No existing consumers of
PaginatedRunFileList['meta'] found in the monorepo.

Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:33:24 -04:00
Bryan Helmkamp
9ddf6c06be
security(server): clamp pagination offset before iterator traversal
CodeQL's rust/uncontrolled-allocation-size alert flagged `paginate_items`
and the models list handler because `PaginationParams.offset: u32` was
cast to `usize` without an upper bound and handed to `Iterator::skip`.
In practice the underlying stores are bounded and `skip` on a Vec
iterator is O(1), so the existing callers couldn't be coerced into
allocating arbitrary memory, but an unbounded `offset` still takes an
unbounded time to walk past and CodeQL had no way to see that.

Clamp `offset` to `MAX_PAGE_OFFSET = 1_000_000` (beyond our largest
expected run count by several orders of magnitude) in both the shared
`paginate_items` helper and the models list handler that rolls its own
pagination. `limit` was already clamped to 100.

Closes code-scanning alert #27.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:17:40 -04:00
Bryan Helmkamp
ec33c6a0ea
security(install): sanitize upstream URLs before issuing HTTP requests
CodeQL's Rust SSRF detector flagged the GitHub-and-provider HTTP calls in
install mode because the `base_url` values flow through `pub` test-only
setters (`with_github_api_base_url`, `with_provider_base_url`) that the
analyzer treats as external entry points. In production these values are
always the hardcoded `DEFAULT_*` constants, so the flagged paths are
unreachable, but the fix also hardens the real request sites.

Route every upstream URL through `parse_install_upstream_url`, which
- parses the URL,
- requires the scheme to be `http` or `https`, and
- requires a host.

Build request endpoints via `install_upstream_endpoint(base, &[segments])`
so each segment is percent-encoded by `url`; a caller cannot inject
extra path components, host overrides, or scheme changes via a path
segment. GitHub's manifest `code` (from the browser callback) is also
checked against the short base64url character set it uses.

Closes code-scanning alerts #28 and #29.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:14:29 -04:00
Bryan Helmkamp
2505cb6d46
Merge remote-tracking branch 'origin/main' into feat/web-install-wizard
# Conflicts:
#	lib/crates/fabro-spa/assets/assets/entry-ez8gc920.js
#	lib/crates/fabro-spa/assets/index.html
#	lib/packages/fabro-api-client/src/.openapi-generator/FILES
#	lib/packages/fabro-api-client/src/models/index.ts
2026-04-19 15:06:12 -04:00
Bryan Helmkamp
d463bb276b
chore(spa): refresh embedded install-wizard bundle
Rebuild the bundled SPA via scripts/refresh-fabro-spa.sh so the Rust server
embeds the current install-wizard sources (OpenAI-compatible removed,
GitHub error banner consolidated into a single effect).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:03:08 -04:00
Bryan Helmkamp
b8af65a9c6
refactor(runs): blocked status canonicalization cleanup (#165)
## Summary

Stacked cleanup of the `canonicalize blocked run status` work (local
commit `d13cdf374`) plus reconciliation with origin's `canonicalize
paginated run list responses` (origin commit `8ab689da7`). Both efforts
ran in parallel and diverged on the column name (`blocked` vs `waiting`)
and on how the board response is shaped — this PR converges them,
keeping `blocked` as the canonical column id while adopting origin's
`column` field on `RunListItem` and `StoreRunSummary` shape.

Also fixes a production-worker regression introduced by the
canonicalization: the worker's start-precondition only accepted
`Submitted | Starting`, so once runs started transitioning through
`Queued` on the way to `Starting`, every subprocess-worker run failed
with `Precondition failed: cannot start run: status is Queued`. That
cascaded into ~90 failing CLI/server integration tests locally.

## Commits

1. `f65843168` refactor(runs): simplify blocked status follow-ups
2. `1492d956c` chore: resolve clippy warnings
3. `676fd9f44` first merge of origin/main
4. `23fc92a2f` **fix(runs): allow Queued status in start precondition**
← the cascade-fix
5. `36b507a83` refactor: simplify pause/unpause + dedupe web status
tables
6. `8d8d27748` refactor(workflow): encapsulate BlockedStateTracker
inside HumanHandler
7. `1c17fda35` second merge of origin/main — resolves waiting vs blocked
8. `4cd3ef7b1` refactor(workflow): Mutex<usize> → AtomicUsize
9. `2e5a58e8a` fix(demo): align run-4 lifecycle status with Blocked
board column

## Test plan

- [x] fmt, clippy, build, doctests all clean
- [x] `cargo nextest run --workspace` — **4092/4092 pass**
- [x] `bun test` — **26/26 pass**, typecheck + production build clean
- [x] Manual CLI repro of the Queued-precondition fix
- [x] Browser smoke test: all 5 columns render with correct
labels/colors, demo run-4 appears in Blocked lane with question text
intact

## Known follow-up (not blocking)

A "paused-while-blocked" run (status `Paused` + `blocked_reason: Some`)
lands in the `running` column because the visible status chooses
`Paused` over `Blocked`. The pending question is not prominent on the
board. Addressing it would require `board_column()` to branch on
`(status, blocked_reason)` rather than just `status` — worth a separate
ticket.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 14:53:46 -04:00
Bryan Helmkamp
87dc7de140
fix(install): clear carried clippy warnings across install code paths
The web-install feature was carrying nine pedantic-tier clippy errors
from its initial commit. Fix them in place:

- \`install.rs\` \`InstallAppState\` switches \`install_token\`,
  \`storage_dir\`, and \`config_path\` from \`Arc<String>/Arc<PathBuf>\` to
  \`Arc<str>/Arc<Path>\` so we stop heap-duplicating buffers.
- Bring \`Infallible\`, \`axum::middleware\`, \`axum::extract::Request\`,
  and \`fabro_types::settings::SettingsLayer\` into scope instead of
  using absolute paths inline.
- Replace \`Duration::from_secs(10 * 60)\` with \`Duration::from_mins(10)\`.
- \`generate_ephemeral_secret\` never returns \`Err\`; drop the \`Result\`.
- \`server/start.rs ensure_storage_server_autostart_allowed\` takes
  \`Option<&OsStr>\` instead of consuming an \`OsString\` it only reads.
- \`server/mod.rs\` storage_dir fallback uses \`map_or_else\` to satisfy
  \`map_unwrap_or\`.

CI now passes \`cargo +nightly-2026-04-14 clippy --workspace
--all-targets -- -D warnings\` cleanly and the 892-test suite still
passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 14:13:10 -04:00
Bryan Helmkamp
0e1d66b137
fix(install): hoist install-shell OnceLock to silence clippy
\`items_after_statements\` flagged the static declaration. Move it to
the top of \`cached_install_mode_shell\` — same behavior, same caching
semantics, one less lint to carry forward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 14:02:27 -04:00
Bryan Helmkamp
9bbd7099fe
chore(fmt): apply nightly rustfmt to server.rs
CI runs nightly rustfmt and flags this untouched for-loop header.
Pre-existing on the branch; clearing it here so the install-wizard
cleanup commits pass fmt --check cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 13:59:30 -04:00
Bryan Helmkamp
2e7a6c95ea
refactor(install): use generated @qltysh/fabro-api-client types
Run \`bun run generate\` inside lib/packages/fabro-api-client to pick up
the new install schemas. Swap install-api.ts from hand-written
interfaces to re-exports from @qltysh/fabro-api-client and drop the
last duplicated type surface for the install wizard.

Keeps the \`installFetch\` wrapper and \`readInstallError\` helper so the
session-storage token handling and our custom error parser stay local
to the wizard. The generated Axios client is available as a future
migration if we decide to drop the wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 13:55:58 -04:00
Bryan Helmkamp
c6ff36d9fb
refactor(install): consolidate shared primitives in fabro-install crate
The `fabro-install` crate was introduced for the web wizard but the CLI
kept its own copies of the same JWT keypair generation, TOML merging,
and GitHub auth settings helpers. Delete the duplicates and route the
CLI through `fabro_install::*`. The CLI keeps a thin
`merge_server_settings` wrapper because it only ever binds TCP and
derives the authority from `--web-url`.

Also tighten `persist_install_outputs_direct` to take its
`PendingSettingsWrite` argument by reference (satisfies
`needless_pass_by_value`) and pull the remaining absolute paths in the
crate's test module into `use` statements, clearing the nightly clippy
warnings that this branch was carrying.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 13:54:47 -04:00
Bryan Helmkamp
acb6b3f9d6
refactor(install): tag GithubAppOwner with discriminated object shape
The install GitHub App manifest shape encoded owner as `"personal"` or
`"org:<slug>"` - a magic string parsed in install-app.tsx, built by
install-api.ts, and reparsed server-side. Replace with a tagged object
`{ kind: "personal" } | { kind: "org", slug }` in the OpenAPI spec, the
progenitor-generated Rust types, and the frontend.

Server-side, the internal `GitHubAppOwner` enum keeps its semantic
shape but gains a `TryFrom<GithubAppOwnerInput>` conversion and emits
the tagged JSON via `as_session_value`.

Frontend drops `buildGithubOwnerValue` in favor of
`buildInstallGithubAppOwner`, and the ready-screen renders the owner
through a small helper instead of string concatenation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 13:48:31 -04:00
Bryan Helmkamp
ad7fdc8d13
refactor(install): return spec-conformant ApiError shape
Install handlers returned `{"error": "..."}` while the OpenAPI paths
referenced the repo-wide `ErrorResponse` schema
(`{"errors":[{status,title,detail}]}`). Funnel the install helper through
`ApiError::into_response`, switch the invalid-token 401 and the
persistence-failure INTERNAL_SERVER_ERROR to the same shape, and update
the TS `readInstallError` helper + test fixtures to read
`body.errors[0].detail`.

The install-finish failure path still carries `leftover_env_keys`
alongside the error envelope so the rollback integration tests retain
their diagnostic field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 13:43:57 -04:00
Bryan Helmkamp
dd4e467bfc
Merge remote-tracking branch 'origin/main'
# Conflicts:
#	lib/crates/fabro-server/tests/it/api/mod.rs
2026-04-19 13:36:54 -04:00
Bryan Helmkamp
3f21644d80
fix(install): cover follow-up edge cases
Harden the remaining install flow regressions and add the missing
coverage for startup dispatch, finish-time shutdown behavior, and
partial-state persistence after vault failures.
2026-04-19 13:32:46 -04:00
Bryan Helmkamp
e8d0f75be9
Merge remote-tracking branch 'origin/main' 2026-04-19 12:46:19 -04:00
Bryan Helmkamp
75f8ed845b
fix(install): harden web wizard against review findings
Tighten the browser-based install flow after correctness and adversarial
review, without changing the external wizard shape.

- Persist the actual bind in server.listen, not the canonical URL
- Reject concurrent /install/finish and rapid GitHub App retries
- Keep the prior GitHub Token strategy until App callback succeeds
- Recover from poisoned install locks instead of propagating panics
- Rollback both settings and vault on failed persistence
- Redirect GitHub callback errors back into the wizard UI
- Validate LLM keys via /models probe instead of a billed generate()
- Reject canonical URLs with trailing slash, path, query, or fragment
- Accept any valid install-token source, not just the first present one
- Redact the install token in structured logs
- Assert install-mode SPA marker injection at startup
- Warn on suspected concurrent operators via UA + X-Forwarded-For
- Add component-level test for the GitHub callback error banner

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:43:00 -04:00
Bryan Helmkamp
1e6543528f
fix(server): satisfy workspace clippy 2026-04-19 12:02:19 -04:00
Bryan Helmkamp
e9daf2db3a
Merge remote-tracking branch 'origin/main' 2026-04-19 11:48:15 -04:00
Bryan Helmkamp
a086a694f1
Merge remote-tracking branch 'origin/main' 2026-04-19 11:48:02 -04:00
Bryan Helmkamp
9dd792c8b8
fix(install): exclude openai-compatible from v1 setup
Restrict the browser install flow to Anthropic, OpenAI, and Gemini,
remove the unused install-time base URL surface, and reject
openai_compatible with a stable 422 response.

Also fix the finishing health poller so it only redirects after the
server comes back healthy outside install mode instead of jumping early
on transient restart failures.
2026-04-19 11:43:38 -04:00
Bryan Helmkamp
ba3e760313
fix(runs): use generated demo status reason parser 2026-04-19 11:38:52 -04:00
Bryan Helmkamp
b5bb134890
fix(runs): bound board enrichment and demo normalization
Paginate board-eligible summaries before enriching them from run state,
add safety caps to paginated web fetches, and make demo run summaries
follow the production title and status-reason normalization rules.
2026-04-19 11:35:30 -04:00
Bryan Helmkamp
ecdfdd82d8
feat(install): add browser-based setup flow
Implement the web-first install experience across the server, CLI, API spec,
web app, and packaged SPA assets.

This also removes test-side process env mutation by pushing env-dependent
decision points behind explicit helpers and test wiring.
2026-04-19 11:20:58 -04:00
Bryan Helmkamp
ec239aaf9c
fix(cli): update install test for listener tls removal 2026-04-19 11:18:34 -04:00
Bryan Helmkamp
6226858648
fix(runs): finish canonical run summary rollout
Complete the /runs and /boards/runs canonicalization work by fixing the
run-detail response shape, preserving lifecycle status separately from board
columns, loading all board pages in the web client, and aligning the shared
status_reason typing.
2026-04-19 11:12:58 -04:00
Bryan Helmkamp
79cd760cd9
refactor(server): dedupe tcp test helpers and drop narrative comment
Collapses duplicated helpers in tests/it/api/tcp.rs introduced with
the TLS-removal test suite (single start_tcp_server, single
wait_for_health), uses ServerState::env_path() in write_test_config,
and replaces the manual SystemTime-based unique-socket path with a
tempdir. Also removes a narrative comment in settings_view that the
module docstring already covers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:04:45 -04:00
Bryan Helmkamp
21240e2d09
fix(server): address TLS removal review follow-ups 2026-04-19 10:48:42 -04:00
Bryan Helmkamp
914778c8a8
refactor(server): remove inbound TLS termination
Remove server-side TLS listener support so Fabro only binds plain TCP
or Unix sockets, and update docs/tests around proxy-terminated HTTPS.
This also drops the removed [server.listen.tls] config shape and the
inbound TLS-specific diagnostics, fixtures, and integration coverage.
2026-04-19 10:43:57 -04:00
Bryan Helmkamp
8ab689da78
feat(runs): canonicalize paginated run list responses
Unify /api/v1/runs and /api/v1/boards/runs around a shared
paginated summary contract with additive convenience fields.

Update the server, demo data, generated clients, CLI pagination,
and web consumers so board views become a thin projection over the
canonical run summary surface.
2026-04-19 10:37:31 -04:00
Bryan Helmkamp
a1ad81430f
refactor(server): store GitHub meta cache under storage root
Keep GitHub /meta cache state under the resolved server storage tree by
adding a storage cache accessor and wiring the resolver to use
<storage_root>/cache.
2026-04-19 09:55:01 -04:00