Commit graph

2356 commits

Author SHA1 Message Date
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
fabro-releases[bot]
769f078bc8 Bump version to 0.207.0-nightly.6 2026-04-18 21:15:59 +00:00
Bryan Helmkamp
8f48aa8b64
docs(digital-ocean): replace stub with Droplet + compose guide
App Platform has no persistent volumes, so Fabro's /storage directory
rules it out. Documents the Droplet path instead, using the existing
docker-compose.prod.yaml + Caddy setup for automatic TLS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:20:01 -04:00
Bryan Helmkamp
674c859a27
feat(server): emit Content-Security-Policy in Report-Only mode
Build a conservative CSP from an inventory of what the embedded SPA
actually loads today: same-origin scripts/styles, Google Fonts CSS and
font files, data: + blob: for images, blob: for workers, and WASM
(viz-js needs wasm-unsafe-eval for Graphviz rendering).

Inline `<script>` hashes are extracted at server startup from the
embedded index.html, so the theme-bootstrap script doesn't drift from
the policy when the template changes. Tests cover:

  - known-body hash stability
  - whitespace preservation (browsers hash raw bytes between tags)
  - external scripts are skipped (they're covered by script-src 'self')
  - the embedded SPA template actually yields at least one hash
  - the final policy includes the expected directives

Ships as Content-Security-Policy-Report-Only for the initial rollout.
Browsers report violations to DevTools without blocking anything, so
real-world usage surfaces any false positives before we flip to
enforcing. When reports are clean, swap the header name to
Content-Security-Policy in security_headers::apply_csp.

CSP notes:
  - 'unsafe-inline' on style-src is a pragmatic concession for React
    and Tailwind runtime-injected inline styles. Script-src remains
    strict (hash-based).
  - No 'strict-dynamic' — the entry chunks are same-origin and covered
    by 'self'. Can be added later if dynamic script injection
    violations appear.
  - No report endpoint wired up yet. DevTools console is sufficient
    for the tuning phase; add report-to + collector later.
2026-04-18 16:18:56 -04:00
Bryan Helmkamp
484a3dbc39
feat(fly): add fly.toml and full deploy guide
fly.toml points Fly directly at ghcr.io/fabro-sh/fabro:nightly (no
builder step), pins internal_port to 32276 since Fly does not inject
$PORT, declares a Volume mount at /storage, and disables autostop so
the run queue stays live under no HTTP traffic.

Replaces the deploy-fly-io.mdx stub with a CLI-first walkthrough
covering volume creation, secrets, dev token retrieval, and the
single-Machine / single-Volume caveats that apply to Fabro.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:15:13 -04:00
Bryan Helmkamp
36e51237d4
docs(render): replace stub with full deploy guide
Documents the render.yaml blueprint flow end-to-end: one-click deploy,
disk verification, env vars, dev token retrieval, and the same
single-replica / amd64-only caveats as the Railway guide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:08:18 -04:00
Bryan Helmkamp
08cdf9220f
feat(render): add Render blueprint and deploy button
Adds render.yaml using Dockerfile.deploy (prebuilt GHCR image) with a
1 GB persistent disk at /storage and /health healthcheck. README gets
a Deploy to Render button alongside Railway.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:04:35 -04:00
Bryan Helmkamp
8da81fdf9e
fix(release): build musl targets via cargo-zigbuild to fix arm64 SIGSEGV
The shipped aarch64-unknown-linux-musl binary segfaulted at startup on
every arm64 runtime (Apple Silicon, Graviton, Ampere, Docker arm64).
Root cause: a glibc-vs-musl .init_array calling-convention mismatch --
a C static library in the dep graph has an __attribute__((constructor))
that expects (argc, argv, envp) per glibc, but musl on aarch64 calls
it with no args, so register garbage propagates into pointer arithmetic
and faults before main runs.

Switch the musl compile steps to cargo-zigbuild (zig 0.13.0). Zig's
bundled cc + lld produce working static-PIE binaries for both musl
targets, sidestepping Ubuntu musl-tools' -no-pie quirk and the
init_array ordering that triggered the crash. Drop the CARGO_TARGET_*
linker overrides and the musl-tools apt install -- zig handles both.

bin/dev/docker-build.sh mirrors the same toolchain so the local Docker
image build matches CI.

Verified by running fabro version from the resulting arm64 image on
ghcr.io/fabro-sh/dhi-alpine-base:3.23-dev, alpine:3.22, and
debian:stable-slim -- all print the version banner with exit 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:01:45 -04:00
Bryan Helmkamp
13f612b111
feat(server): emit baseline HTTP security headers on every response
fabro-server previously sent no security headers beyond content-type
and cache-control. Add a tower middleware that fills in a conservative
default set on every response, preserving any header the handler
already set so routes can still override.

Always applied:
  - X-Content-Type-Options: nosniff
  - X-Frame-Options: DENY
  - Referrer-Policy: strict-origin-when-cross-origin
  - Cross-Origin-Opener-Policy: same-origin
  - Cross-Origin-Resource-Policy: same-origin
  - Permissions-Policy: (deny sensor/payment/xr APIs)
  - X-Download-Options: noopen
  - X-Permitted-Cross-Domain-Policies: none
  - X-XSS-Protection: 0  (current OWASP guidance — the legacy filter
    has known bypasses; CSP is the proper replacement)
  - Cache-Control: no-store  (default; asset routes keep their own)
  - Pragma: no-cache
  - Vary: Accept-Encoding

Applied only when the request reached an HTTPS edge (direct TLS or
X-Forwarded-Proto: https from a reverse proxy):
  - Strict-Transport-Security: max-age=63072000; includeSubDomains

CSP is deliberately not included — it needs a dedicated audit of the
SPA's script/style/font/connect sources and isn't a drop-in header.
Filed as a separate follow-up.

Tests cover each applied header, non-override behavior against the
static-file cache-control, HSTS gating on X-Forwarded-Proto (including
the chained "https, http" leftmost-wins case), and an integration test
against a live router confirming both API and SPA responses carry the
headers.
2026-04-18 15:59:32 -04:00
Bryan Helmkamp
8e4693ad8c
chore(docker): switch base to DHI Alpine, add cross-arch build support
Runtime image now builds FROM ghcr.io/fabro-sh/dhi-alpine-base (Docker
Hardened Images mirror, Alpine 3.23) instead of alpine:3.22. Same
runtime shape, CVE-minimized base. Changelog updated to reflect the
DHI migration.

bin/dev/docker-build.sh grows --arch {amd64,arm64} and --compile-only
flags so local multi-arch verification works regardless of host arch.
Cargo target cache is now per-arch to prevent arm64/amd64 artifacts
from stomping each other in one shared volume.
2026-04-18 15:42:23 -04:00
Bryan Helmkamp
3ddc71c409
fix(server): tighten SPA fallback to HTML navigations, exclude /api/*
The static-file fallback previously served index.html (25KB of UI
shell) for any unknown non-/api/v1/ GET — including `curl /healthz`,
scripted fetches, and typos under /api/. Two problems:

1. Unregistered paths like /api/v2/foo or /api/healthz bypassed the
   router (which only matched /api/v1/) and fell through to the SPA
   fallback, silently returning HTML for API typos.
2. Non-browser clients got the UI shell back for any misspelled path,
   making deploy healthchecks, load balancer probes, and API clients
   unable to distinguish "route missing" from "server healthy".

Broaden the dispatch guard to route /api/* through the axum Router so
unknown API paths return a clean 404 from the router itself. Gate the
SPA's index.html fallback on `Accept: text/html` so only browser
navigations (which deep-link to client-side routes like /runs/abc123)
get the UI shell; curl/fetch/scripts get 404.

Asset serving is unchanged — favicon.ico, /assets/*, etc. still serve
normally regardless of Accept header; the gate only applies to the
fallback after an asset lookup misses.

Tests: unit coverage for accepts_html + integration tests for the new
404 shape on /setup without Accept and on /api/v2/nonexistent even
with Accept: text/html.
2026-04-18 15:40:47 -04:00
Bryan Helmkamp
d9596f2bc4
feat(deploy): healthcheck on /health, compose uses GHCR image
railway.toml: point Railway's healthcheck at /health. Fabro's server
returns 200 for any unknown path (SPA fallback) so /healthz would have
been a false-positive check that never catches failures. /health is
the real endpoint exposed by fabro-server and documented in the
OpenAPI spec.

docker-compose.yaml: swap `build: Dockerfile` for
`image: ghcr.io/fabro-sh/fabro:nightly`. A fresh clone's `docker
compose up` previously failed because the Dockerfile expects pre-built
binaries under `docker-context/` that only the release workflow
populates. Pulling the published image gives new users a 5-second
boot and mirrors the Railway deployment shape. Pin to linux/amd64
until the arm64 image variant is fixed.
2026-04-18 15:27:35 -04:00
Bryan Helmkamp
184cddf8e0
feat(railway): deploy from prebuilt GHCR image instead of source build
Add Dockerfile.deploy as a thin wrapper that pulls
ghcr.io/fabro-sh/fabro:nightly, and point railway.toml at it. Railway
now skips Rust compilation entirely and deploys in seconds. The
upstream image already configures entrypoint, $PORT-aware CMD, volumes,
and the unprivileged fabro user, so nothing else is needed in the
wrapper.

Update docs/administration/deploy-railway.mdx to reflect the new flow
and call out that amd64 is the supported architecture (arm64 variant
of the image is being handled separately).

Verified locally: `docker build -f Dockerfile.deploy .` succeeds,
`fabro version` prints the expected banner, and PORT override + health
check work.
2026-04-18 15:24:22 -04:00
Bryan Helmkamp
dba41c32f7
ci(release): smoke-test Docker images per-arch before pushing
Today's nightly published an arm64 image that segfaults on any
invocation (`fabro version` → SIGSEGV). The docker job only built and
pushed; the binary was never executed inside the final image layout,
so the broken arm64 manifest reached ghcr.io undetected.

Before the multi-arch push, build each platform single-arch with
load: true and run `fabro version` in the loaded image. A segfault,
missing binary, or broken entrypoint now fails the job instead of
shipping a broken image. The subsequent multi-arch push reuses buildx
cache from the per-platform builds, so the net cost is ~one short
`docker run` per arch.
2026-04-18 15:17:45 -04:00
Bryan Helmkamp
a77c45207f
test: preserve LLVM_PROFILE_FILE across env_clear in CLI tests
Integration tests spawn the fabro binary as a subprocess and call
env_clear() for isolation, which strips LLVM_PROFILE_FILE. Under
cargo-llvm-cov this dropped subprocess coverage into orphaned
default.profraw files in tempdirs instead of the merged profile.

Add a preserve_coverage_env! macro in fabro-test and call it after
each env_clear() in apply_test_isolation, LightweightCli, and the
exec.rs sites. No-op when the env var is unset (normal test runs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:05:32 -04:00
Bryan Helmkamp
e06a47f357
ci(release): bump docker actions to Node.js 24 versions
Node.js 20 actions are deprecated on GitHub Actions runners; updating to
the latest majors silences the warning and keeps the release pipeline
working past the September 2026 removal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:05:32 -04:00
Bryan Helmkamp
b5e17ba082
ci(release): attest SLSA build provenance for binaries and image
Tarballs in each compile matrix and the multi-arch ghcr image now get
Sigstore-signed provenance attestations via GitHub's attest-build-provenance
action. Users can verify with `gh attestation verify` — covered in a new
docs/reference/verifying-releases.mdx.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:05:32 -04:00
fabro-releases[bot]
6ebe609e71 Bump version to 0.207.0-nightly.5 2026-04-18 17:26:06 +00:00
Bryan Helmkamp
977f4b4a77
deps: bump rand 0.9.2 -> 0.9.4
Patches GHSA-cq8v-f236-94qc (RUSTSEC-2026-0097) for direct rand usage.
The transitive rand 0.8.x remains in the lockfile via cookie, sentry,
slatedb, and phf_generator pending upstream bumps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:16:59 -04:00
Bryan Helmkamp
01dfe2a4fe
ci(release): skip tests on aarch64-musl runner
The aarch64-musl binary SIGSEGVs at startup on the ubuntu-24.04-arm
runner (empty stdout/stderr, non-zero exit), so every test that
spawns 'fabro server start' fails. The shipped binary runs natively
on Alpine via the Docker image, so skip the test step here and rely
on x86_64-musl + both gnu targets for test coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:04:14 -04:00
Bryan Helmkamp
c8ee9291d4
ci(release): install pkg-config and libssl-dev on Linux runners
The aarch64 Linux compile job fails on ubuntu-22.04-arm-32-cores
because openssl-sys can't find pkg-config or OpenSSL headers.
build-essential alone doesn't pull them in on this image.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:54:13 -04:00
fabro-releases[bot]
d066592362 Bump version to 0.207.0-nightly.4 2026-04-18 16:47:30 +00:00
Bryan Helmkamp
6c57e17bb3
docs: add Homebrew as a first-class install method
Add Homebrew tab to the Quick Start install tabs and simplify the
agent-driven install.md to detect Homebrew and fall back to the install
script, dropping the gh/tar manual path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:37:15 -04:00
Bryan Helmkamp
dae8b635fc
plans 2026-04-18 12:36:41 -04:00
Bryan Helmkamp
db34213d30
ci: install build-essential on arm64 runners
Release run 24608869678 failed on both aarch64 Linux compiles with
"linker `cc` not found" after the dtolnay/rust-toolchain fix got us
past rustup. The ubuntu-*-arm-32-cores images don't ship build tools
preinstalled (the x86_64 variants do). Add a Linux-only step that
installs build-essential so `cc` is available for cargo's build
scripts, and drop the now-redundant apt-get update from the musl
toolchain step since it runs right after.
2026-04-18 12:36:23 -04:00
Bryan Helmkamp
81be32a1e2
docs(readme): add Homebrew install option
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:36:23 -04:00
Bryan Helmkamp
231faa6cc9
ci(nightly): use client-id for create-github-app-token
v3.1.0+ of actions/create-github-app-token deprecates `app-id` in
favor of the GitHub App's Client ID. Reads from the new
FABRO_RELEASES_APP_CLIENT_ID variable in the nightly environment.
2026-04-18 12:36:23 -04:00
fabro-releases[bot]
0bd72d0b2d Bump version to 0.207.0-nightly.3 2026-04-18 16:24:51 +00:00
Bryan Helmkamp
cce8a22269
ci: install Rust via dtolnay/rust-toolchain on arm64 runners
Release run 24607436574 failed on both aarch64 Linux compiles with
"rustup: command not found" — the ubuntu-*-arm-32-cores runner images
don't ship with rustup preinstalled, while the x86_64 variants do. Our
rust.yml and typescript.yml already use dtolnay/rust-toolchain@stable;
switch release.yml and nightly.yml to the same action so rustup is
bootstrapped regardless of runner image. Targets are passed via the
action's `targets:` input instead of a manual `rustup target add`.
2026-04-18 11:40:05 -04:00
fabro-releases[bot]
da0999b697 Bump version to 0.207.0-nightly.2 2026-04-18 15:05:58 +00:00
Bryan Helmkamp
0c35a998ab
ci(nightly): run Tag nightly on 32-core runner
Matches the faster runner release.yml already uses for release-mode
nextest + build work, cutting Tag nightly wall time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 10:54:22 -04:00
Bryan Helmkamp
bc3dcf3505
plan 2026-04-18 10:51:43 -04:00
Bryan Helmkamp
4fe1e3742c
chore: update gitignore 2026-04-18 10:51:34 -04:00
Bryan Helmkamp
c1554ba2bd
ci(nightly): upgrade create-github-app-token to v3.1.1
v2.2.2 runs on Node.js 20, which GitHub is forcing to Node.js 24 on
June 2, 2026 and removing entirely on September 16, 2026. Bump to
v3.1.1 which runs on Node.js 24. The `app-id`/`private-key` inputs we
use are unchanged (`app-id` is deprecated in favor of `client-id`, but
still accepted).
2026-04-18 10:09:00 -04:00
Bryan Helmkamp
b0349e9873
feat(cli): surface debug build profile in version output
Non-release builds now append the profile to `fabro --version`
(`x.y (sha date debug)`), `fabro version`, and `fabro system info`,
so users can tell a local build apart from a shipped release. The
API's `SystemInfoResponse` gains a `profile` field so the client
can render the server's build profile too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 09:44:54 -04:00
Bryan Helmkamp
65baf1240b
feat(cli): warn on client/server version mismatch in fabro version
When stderr is a TTY and text output is used, print a yellow `warning:`
line on stderr if the server reports a version that differs from the
client. JSON output and non-interactive contexts stay silent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 09:44:54 -04:00
Bryan Helmkamp
0d9f71d1cb
docs: add Apr 17–18 changelog entries and refresh CLI + Railway docs
Document bare `fabro` landing output in the CLI reference and remove
the now-obsolete multi-arch image caveat from the Railway deploy guide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 09:44:54 -04:00
Bryan Helmkamp
8d39f31a62
test(harness): flush workflow store events before returning
Flush the async store logger in workflow test helpers before returning so
callers that reopen the run store immediately do not observe partial state.
This removes the race behind the Linux git checkpoint CI failure.
2026-04-18 09:44:54 -04:00
fabro-releases[bot]
459433baa5 Bump version to 0.207.0-nightly.1 2026-04-18 10:05:08 +00:00
Bryan Helmkamp
fae575c193
fix(server): validate owner/repo path params on GitHub repo lookup
Path<(String, String)> percent-decodes segments, so an authenticated
user could send owner=foo%2F..%2Fuser (decoded to foo/../user). After
reqwest URL normalization this rewrote the GitHub API endpoint and
reissued the server's privileged token against an unintended path.

Reject anything outside [A-Za-z0-9._-] with length caps, plus the
literals "." and "..".
2026-04-18 04:39:47 -04:00
Bryan Helmkamp
f4703d2cb5
fix(release): make release.sh portable across macOS and Linux
The nightly release workflow runs on Linux, but the script was written
with BSD-only idioms and had only ever been executed from a Mac. Every
scheduled nightly had been failing; the last successful nightly tag was
cut manually.

- days_since_2026: replace `date -j -f` (BSD) with a python3 one-liner
- sed -i: use the portable `sed -i.bak` + rm pattern; empty-suffix
  `sed -i ''` is a BSD-ism that breaks GNU sed's arg parsing

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 04:35:05 -04:00
Bryan Helmkamp
7f83a627ee
fix(llm): send Gemini API key via x-goog-api-key header
API keys in query strings leak to access logs, proxies, and request
traces. Move to the header form Google documents as equivalent for both
generateContent and streamGenerateContent endpoints.
2026-04-18 04:32:23 -04:00
Bryan Helmkamp
d793046050
fix(server): harden fabro-demo cookie with HttpOnly and conditional Secure
Only the server reads this cookie (via cookie_and_demo_middleware), so
HttpOnly is safe unconditionally. Secure is gated on https:// web.url to
match the existing session cookie pattern — preserves localhost HTTP dev.
2026-04-18 02:32:01 -04:00
Bryan Helmkamp
0f029d6b02
feat(docker): switch runtime image to Alpine on musl
Now that the release workflow publishes musl binaries, the runtime
image can drop the debian:trixie-slim base for alpine:3.22. The
image shrinks from ~287 MB to ~96 MB (66% smaller) with a smaller
attack surface.

- Dockerfile: alpine:3.22 base, apk packages (ca-certificates git
  tini su-exec), BusyBox adduser/addgroup, tini at /sbin/tini.
- entrypoint.sh: replace runuser with su-exec, Alpine's idiomatic
  drop-privileges helper.
- release.yml docker job: pull the two linux-musl artifacts instead
  of linux-gnu. The docker image and the Alpine install.sh path now
  ship the same binary.
- bin/dev/docker-build.sh: compile fabro-cli for the host's musl
  target in rust:1-bookworm with musl-tools, the matching CC/LINKER
  env vars, and LIBZ_SYS_STATIC=1. Same pattern as CI.

Verified locally on aarch64: Alpine image builds, server binds on
$PORT (default 32276), endpoints return 200, fabro server process
runs as unprivileged UID 1000 under tini.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 02:10:18 -04:00
Bryan Helmkamp
5a18bd459d
perf(ci): use larger runners for release compile matrix
Cuts the release pipeline's critical path (aarch64-apple-darwin) from
~72m to an expected ~35m, with similar wins on the four Linux targets.

- macOS aarch64: macos-15 -> macos-15-xlarge (3 -> 6 vCPU M1)
- Linux x86 gnu/musl: ubuntu-latest/24.04 -> ubuntu-24.04-x86-32-cores
- Linux arm gnu: ubuntu-22.04-arm -> ubuntu-22.04-arm-32-cores
- Linux arm musl: ubuntu-24.04-arm -> ubuntu-24.04-arm-32-cores

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 01:55:31 -04:00
Bryan Helmkamp
cf80fe567a
test(harness): scrub ambient creds from spawned fabro CLI
CLI integration tests spawned the real fabro binary while letting the
parent process's env pass through. The pr_view "no credentials" snapshot
failed in CI because the Nightly workflow's minted GITHUB_TOKEN was
inherited by the child and turned the expected "credentials required"
error into a real GitHub API call (404 / 401). On developer laptops the
same leak occurs whenever gh auth login is active.

Introduce apply_test_isolation(cmd, home) in fabro-test: env_clear() +
re-populate PATH, HOME, NO_COLOR, and the FABRO_* test overrides. Route
TestContext::command(), the internal server bootstrap, and the four
ad-hoc spawners in tests/it/cmd/{attach,render_graph,runner,server_start}
through the same helper so the isolation is systemic instead of
per-callsite. Tests that deliberately need a credential (OPENAI_API_KEY,
GITHUB_APP_PRIVATE_KEY, etc.) continue to set it explicitly on the
returned Command; those survive the clear.

Add a regression test that sets sentinel GITHUB_TOKEN and
ANTHROPIC_API_KEY in the parent, spawns /usr/bin/env through the helper,
and asserts the child sees neither credential while still seeing PATH
and the harness's FABRO_NO_UPGRADE_CHECK override.

Verified: the full workspace (4022 tests) passes with GITHUB_TOKEN and
ANTHROPIC_API_KEY set in the parent, which previously broke the
pr_view_reads_pull_request_from_store_without_pull_request_json
snapshot. cargo fmt and nightly clippy are clean.
2026-04-18 01:55:01 -04:00