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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
## 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>
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>
\`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>
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>
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>
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>
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>
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>
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.
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>
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.
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.
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.
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.
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>
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.
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.
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.
Reuse `IpAllowEntry::parse_literal` instead of duplicating `IpNet`
parsing in the resolver, and drop the unreachable defensive branch
in `expand_ip_allow_entries` that called `unwrap_or_default` on a
value that is always `Some` once an entry needs GitHub hooks.
Adds a middleware test covering X-Forwarded-For routing with a
non-zero trusted proxy count, which previously relied on
`extract_client_ip` unit tests alone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Validate the effective GitHub webhook overlay for Unix listeners,
reuse cached GitHub /meta hook ranges when refresh fails, and
propagate webhook allowlist resolution errors during startup instead of
silently skipping the listener.
Introduces a configurable IP allowlist applied to the main API router
and the GitHub webhook listener. Supports CIDR literals plus a
`github_meta_hooks` keyword that resolves live against GitHub's meta
API for the webhooks override. Adds trusted-proxy handling for
X-Forwarded-For, validation that rejects Unix socket listeners without
a trusted proxy count, and deep-merge logic for the new
server.ip_allowlist and per-integration override layers.
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.
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.
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.
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>
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>
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>
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.
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 "..".
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.
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.