Commit graph

5137 commits

Author SHA1 Message Date
Bryan Helmkamp
b589b581ff
docs(plan): mark Run Files tab plan partially completed
Captures what landed in this session (Units 1-10, 13) vs what's
deferred (Units 11-12) so follow-up work can pick up from a clean
baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:06:44 -04:00
Bryan Helmkamp
731a8cc6e0
feat(web): rewrite Run Files tab, surface it in navigation
Rewrites apps/fabro-web/app/routes/run-files.tsx to consume the real
PaginatedRunFileList response and removes the fallbackFiles fixture
and the Steer subsystem. The new component:

- Loads via apiJsonOrNull, so a 404/501 (dev without the route)
  renders the empty state instead of the root error boundary
- Branches on meta.degraded + meta.patch to render PatchDiff with a
  DegradedBanner whose copy reflects degraded_reason
- Renders per-entry placeholders for sensitive, binary, symlink/
  submodule, and truncated files with the priority order
  sensitive > binary > symlink/submodule > truncated -- security
  flags never get hidden behind a lesser placeholder
- Renders one MultiFileDiff per regular entry
- Uses role="region" + aria-label on each file row

Also unhides the Files Changed tab in run-detail.tsx by flipping
broken: true -> false. Adds missing final_patch: None to the runner
RunFailed test fixtures to match the lifecycle change from Unit 2.

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 17:06:06 -04:00
Bryan Helmkamp
74cd7fe434
fix(settings): drop inner max-w wrapper so content aligns with shell header
The page wrapped its body in mx-auto max-w-4xl, which centered the
description and JSON inside the shell's max-w-5xl column. The
shell's "Settings" header used the outer 5xl bounds, so everything
below it shifted right. Let the page inherit the shell's width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:04:59 -04:00
Bryan Helmkamp
4e4937c714
docs(settings): tighten copy — point at settings.toml instead of the CLI
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:04:04 -04:00
Bryan Helmkamp
81903b9ba4
chore(spa): refresh embedded bundle for Settings nav item
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:03:28 -04:00
Bryan Helmkamp
647da0dddf
feat(app-shell): promote Settings to a top-level nav item
Previously Settings was reachable only via direct URL or logout menu.
Add it to the nav (visible in both demo and real modes) and drop the
now-redundant in-page title so the shell header supplies the heading
instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:03:27 -04:00
Bryan Helmkamp
9d1c0d98c7
fix: migrate blocking fs calls off Tokio paths
Phase 1 of the std::fs lint initiative. Refactors blocking std::fs entry
points that ran inside async contexts. Caller chains either converted to
async (using tokio::fs) or wrapped in tokio::task::spawn_blocking where
sync callers were already natural (Command builders, flock semantics).

HIGH (per-request async hot paths):
  - fabro-sandbox local.rs: wrap recursive std::fs::read_dir traversal in
    spawn_blocking. Fixes /api/runs/{id}/files stalling workers under
    concurrent or deep listings.
  - fabro-server static_files.rs: convert serve/serve_install/serve_with_mode
    and the static-asset load chain to async; use tokio::fs::read for the
    debug-only disk fallback. Cascades through install.rs build_install_router
    (now async) and ~17 test call sites.

LOW (async but not per-request):
  - fabro-workflow artifact.rs: sync_artifacts_to_env, offload_large_values
    → tokio::fs::read_to_string.
  - fabro-workflow artifact_snapshot.rs: compute_artifact_info → async +
    tokio::fs::read.
  - fabro-server ip_allowlist.rs: load_cache and store_cache → async +
    tokio::fs::{read,write,create_dir_all}.
  - fabro-server server.rs: wrap worker_command invocation in spawn_blocking
    at the async boundary in execute_run_subprocess; keep the sync
    worker_command + current_server_target signatures intact.
  - fabro-cli server/start.rs: wrap the OpenOptions::open call in
    acquire_lock in spawn_blocking; file-lock semantics require a real
    std::fs::File, and the flock polling loop stays async with time::sleep.

Deferred:
  - fabro-llm load_file_as_base64 (file:// attachment loader): 7 call sites
    across 4 providers, each inside sync translators. Left for Phase 3
    annotation with a FOLLOW-UP marker; file:// URLs are rare in practice.

Verified: workspace builds, 4131 tests pass, 182 skipped. The lint that
enforces this discipline lands in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:00:56 -04:00
Bryan Helmkamp
420e375134
build(web): upgrade @pierre/diffs to 1.1.15
Bumps @pierre/diffs from 1.0.11 to 1.1.15 to pick up the Virtualizer
component and renderHeaderPrefix/renderCustomHeader hooks the Run
Files tab relies on for large-diff performance. 1.0 -> 1.1 merged
MouseEventManager/LineSelectionManager into InteractionManager but
the public React components (MultiFileDiff, PatchDiff, FileDiff,
File) keep their existing shape, so no consumer changes are needed
yet -- Unit 10 exercises the new features.

Pins an exact version (1.1.15) rather than a caret range so bun
doesn't resolve up to 1.1.16, which was published today and would
trip the "no packages younger than 24 h" rule in the user-global
policy.

The redundant apps/fabro-web/bun.lock is removed; bun workspaces
resolve against the root bun.lock and the per-app lockfile was
drifting from it. Embedded SPA bundle (lib/crates/fabro-spa/assets/)
is refreshed to match the new build output.

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 17:00:50 -04:00
Bryan Helmkamp
bdf645a774
docs: changelog entry and plan status=completed for archived runs 2026-04-19 16:59:46 -04:00
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
86f0140ee0
docs(logging): add prohibited-fields section
Extends the Fabro logging strategy with an explicit prohibited-fields
table covering the Run Files Changed endpoint's sensitive surface:
diff_contents, per-changed-file file_path values, raw git_stderr,
and credential-ish strings. Each entry pairs the prohibition with a
concrete cardinality-bounded alternative, so future handlers have a
precedent to follow rather than rediscovering the rule.

The Run Files handler (Unit 5) already emits exactly the allowlisted
field set (run_id, file_count, bytes_total, duration_ms, truncated,
binary_count, sensitive_count, symlink_count, submodule_count); this
change makes the policy enforceable for other endpoints.

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:58:20 -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
174c62e2f7
style(runs): tighten stages, graph toolbar, billing, and stage sidebar
run-stages:
- replace the full-width tinted System/Assistant cards with a subtler
  left-accent bar and header so dense streams read cleanly
- unify the Running/Timed out/exit/duration indicators under a shared
  StatusPill, and bump stdout/stderr labels from 10px to 11px
- raise the selected stage header from text-sm font-medium to
  text-base font-semibold
- command output preformatted text is now text-sm on mobile

run-overview and run-graph:
- extract the floating direction/fit/zoom controls into a single
  GraphToolbar capsule with internal dividers, shared between both
  graph views
- drop the translucent canvas in favor of solid bg-panel-alt
- wrap the "no workflow graph" message in a proper empty-state panel

run-billing:
- tfoot now uses bg-overlay so totals read heavier than the body
- table headers gain font-medium and a readable fg-3 instead of
  fg-muted
- "By model" heading is a real section heading, not an eyebrow
- add an empty state when a run has no billing yet

stage-sidebar:
- cancelled stages now use NoSymbolIcon so they don't look identical
  to failed stages at a glance

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:51:55 -04:00
Bryan Helmkamp
9b5b84da64
style(runs): polish list surfaces, toggles, and accessibility
- run card: solid bg-panel at rest instead of bg-panel/80 (opacity
  shift on hover was backwards)
- column header: mb-3 to match inter-card gap
- lifecycle tag: 10px → 11px (below readable threshold for uppercase)
- additions/deletions: tabular-nums so large counts don't jitter
- view toggle: add a bg-overlay active state so the active view is
  not purely a hue shift; wrap in role="group" with aria-pressed on
  each button
- search + repo select: add aria-label and name attributes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:46:27 -04:00
Bryan Helmkamp
476b20176e
style(app-shell): tighten header and nav surface treatment
- drop the semi-transparent bg-panel/50 on the top nav so the page
  background stops bleeding through
- simplify the header separator to after:border-b with a single
  bottom inset
- raise the page title from text-lg/6 to text-xl for room to breathe
- add isolate to the root so Headless UI portals don't fight the
  header's stacking context

Leaves the demo-mode beaker toggle visible in real mode as a
follow-up; gating it cleanly needs a new feature flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:44:37 -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
baa5eef835
feat(settings): add page headers to /settings and /runs/:id/settings
Both pages previously dumped raw JSON with no context. Add a heading
and a one-line description so a user landing from the nav understands
what they're looking at and how to edit it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:44:03 -04:00
Bryan Helmkamp
2ff71d74bb
refactor(web): align auth surfaces with the install wizard
Extract the wizard's INPUT_CLASS, PRIMARY_BUTTON_CLASS,
SECONDARY_BUTTON_CLASS, and ErrorMessage into
apps/fabro-web/app/components/ui.tsx so auth-login, setup, and the
install wizard share one source of truth.

- auth-login: raise the heading to text-2xl, swap white-on-teal for
  navy-on-teal, replace the bordered dev-token input with the outline
  pattern, use the ErrorMessage pill for invalid tokens, associate the
  input with a label, and shrink the GitHub mark to size-4 per the
  icons guideline
- setup: replace the nested bg-overlay cards with a numbered <ol>
  matching the wizard's welcome layout, raise the heading, switch the
  primary button to navy-on-teal
- install-app: re-import the shared primitives instead of holding
  local duplicates

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:43:41 -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
43b30f386a
refactor(run-detail): remove dead Open PR, Terminal, and Files tabs
- drop the unwired "Open PR" button; restore when RunPullRequest gains a url field
- drop the Terminal <Menu> block; both entries were non-functional
  and the Web Terminal link pointed at a hardcoded Daytona dev URL
- drop the "Files Changed" tab; its loader hit a non-existent path and
  the tab was already hidden behind a broken flag
- promote the remaining Preview button to primary teal styling so the
  action bar has a clear primary

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:40:50 -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
b849738a5b
plans 2026-04-19 15:18:25 -04:00
Bryan Helmkamp
5958470a11
plan 2026-04-19 15:18:22 -04:00
Bryan Helmkamp
a1c816724b
plans 2026-04-19 15:18:21 -04:00
Bryan Helmkamp
5543e276a0
Merge pull request #166 from fabro-sh/feat/web-install-wizard
feat(install): browser-based install wizard
2026-04-19 15:18:08 -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
908f078cec
chore(docker): add --tag to docker-build.sh, document it in AGENTS.md
Lets a smoke-test harness pick its own image tag without racing the default
fabro:latest, and points future agent sessions at bin/dev/docker-build.sh
so they don't hand-roll a throwaway Dockerfile.

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
20e4ef32ae
refactor(install): collapse overlapping React state into discriminated unions
Two wins here. First, the `session`/`loadingSession`/`sessionError`
triple is replaced with a single `SessionState` discriminated union, so
the component can switch on `.status` instead of juggling three
correlated flags. Second, the seven flat `useState` calls for the
GitHub step are grouped into `githubStrategy` + `tokenForm` + `appForm`,
with `appForm.owner` typed as the generated `InstallGithubAppOwner`
tagged object. Invalid states like "token flow but org slug set" simply
stop existing.

\`buildInstallGithubAppOwner\` is deleted (unused) — form handlers build
the tagged object in place, which is small enough to stay readable.

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