Skip to content

feat(setup): add one-command agent configuration#168

Open
Menci wants to merge 56 commits into
mainfrom
worktree-agent-setup
Open

feat(setup): add one-command agent configuration#168
Menci wants to merge 56 commits into
mainfrom
worktree-agent-setup

Conversation

@Menci

@Menci Menci commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • replace the manual Claude Code and Codex snippets with a synchronized Agent Setup card
  • add five-minute setup leases and public shell/PowerShell installer endpoints
  • install missing CLIs and surgically update existing Claude Code and Codex configuration
  • preserve all addressable chat models while ranking native model families first

Behavior

The API Keys page now lets users:

  • select an existing Floway API key
  • enable Claude Code and/or Codex
  • configure optional models and reasoning effort
  • enable Claude Code gateway model discovery
  • copy either:
    • curl -fsSL <setup-url>/setup.sh | bash
    • irm <setup-url>/setup.ps1 | iex

The setup URLs remain stable while the page is open. Their five-minute lease is renewed every minute, and form changes update the configuration served by the same URLs.

The installers:

  • retain existing CLI installations without upgrading or compatibility-gating them
  • install missing CLIs through official user-local installers without sudo
  • back up affected configuration files
  • surgically merge Claude Code settings.json and Codex config.toml
  • replace Codex auth.json after backing it up
  • roll back each agent independently on failure
  • verify configuration without making model inference requests

Security

  • setup tokens are 256-bit random bearer credentials
  • API keys do not appear in setup URLs or copied commands
  • script responses are non-cacheable and non-CORS
  • setup tokens are redacted from application logs and internal-error responses
  • installer downloads, jq bootstrap, file permissions, timeouts, process-tree cleanup, atomic replacement, and rollback paths are validated
  • shell and PowerShell values are encoded as language-native literals rather than interpolated unsafely

Additional changes

  • add shared SQL/in-memory Agent Setup repositories and migration 0050
  • make the Codex model catalog deterministic, native-first, and reasoning-aware
  • document the new post-boot flow and installer verification command

Verification

  • pnpm run test — 337 files, 4013 tests passed
  • pnpm run test:agent-setup-installers — 95 passed, 2 host-dependent skips
  • pnpm run lint
  • pnpm run typecheck — all 19 projects passed
  • pnpm run build:web
  • pnpm jiti scripts/generate-agent-setup-assets.ts --check
  • Node runtime verification of setup creation, synchronization, GET/HEAD scripts, security headers, heartbeat renewal, expiry rotation, malformed requests, supersession, and installer execution

Known platform boundaries

  • native Windows PowerShell 5.1, DACL, File.Replace, and taskkill /T paths were source-reviewed and fixture-tested but not executed on Windows
  • two Bash Codex install-from-absent cases were skipped because the verification host already had Codex installed
  • the Vue card was covered by component/composable tests; runtime verification drove the Node HTTP and installer surfaces rather than a headless browser

Menci added 30 commits July 11, 2026 04:28
Add the per-user agent_setup lease table and its repository contract so
later route and dashboard work has shared persistence to build on.

The lease carries a random token (embedded in the public setup-script
URL), the serialized configuration, an optimistic-concurrency revision,
the dashboard's public origin, and a Unix-ms expiry. Three conditional
writes model the protocol: replaceForUser (POST) acquires/replaces a
lease and bumps the revision; updateConfiguration (PUT) is a
single-statement CAS on (user, token, revision) that bumps the revision
and renews expiry, rotating the token only if the lease had already
expired; renewLease (heartbeat) extends expiry, leaving configuration
and revision untouched, and likewise rotates only an expired token. The
token-mismatch check precedes the revision check, so a superseded tab is
distinguished from a stale-revision edit.

public_base_url is stored because a Node deployment behind a
TLS-terminating reverse proxy cannot recover the external scheme from
the inbound request URL; the authenticated client sends
window.location.origin at lease creation and it is preserved across
renewal.

Both SqlRepo (single-statement CAS via UPDATE ... RETURNING) and
InMemoryRepo implement the contract; the shared repo test exercises both
backends. No foreign key on user_id: ownership is validated in route
logic, matching the rest of the migration corpus.
Add the canonical Agent Setup configuration model and the safe
assignment-prefix renderer that later routes and installers build on.

configuration.ts owns the persisted shape as a strict, recursive Zod
schema: opaque optional model/effort slots are non-empty, NUL-free, and
nullable (null = "no override"; the installer removes the managed key),
while Claude Code's effort is the closed low|medium|high|xhigh enum and
Codex's reasoning effort stays an open upstream-owned string.
defaultAgentSetupConfiguration selects the first selectable key, enables
both agents, enables Claude model discovery, and leaves every model/effort
override null — creating a lease needs no model catalog; model defaults are
a dashboard presentation concern. A zero-key list raises a typed error so
the route can steer the user to create a key. applyClaudeContextSuffix is
the family-agnostic, pure `[1m]` rule (append at persist time when the
advertised window reaches one million and the suffix is absent).

render.ts renders POSIX and PowerShell prefixes entirely through dedicated
single-quote encoders — never interpolation — so any value carrying quotes,
whitespace, newlines, or metacharacters stays contained; NUL is rejected
before rendering. Each prefix leads with a trace-suppressing directive
(set +x / Set-PSDebug -Off) before the API key assignment and ends with a
newline so the fixed installer body concatenates cleanly. The server
assembles the parseable Codex identity token from the origin host, so the
installers do no JWT/base64url work.

schemas.ts gains the create (publicBaseUrl), update (token, configuration,
expectedRevision), and heartbeat (token) bodies for the Task 3 routes;
strict http(s)-origin validation of publicBaseUrl stays in route logic.
Wire the Agent Setup lease control plane and the public setup-script
endpoints on top of the Task 1 repo and Task 2 render/schema APIs.

routes.ts adds the per-user control surface — POST acquires or replaces a
lease, PUT edits the configuration under the repo's optimistic-concurrency
CAS, and POST /heartbeat renews the lease without touching the revision —
plus the unauthenticated GET/HEAD script endpoints. POST validates the
browser-supplied publicBaseUrl down to a bare http(s) origin (no
credentials, path, query, or fragment) and stores its canonical origin so a
Node deployment behind a TLS-terminating proxy never trusts a forwarded
host/proto; responses carry origin-relative script URLs. First use selects
the first active key, enables both agents and Claude discovery, and leaves
every model/effort override null; reopening restores the saved
configuration only while its key stays selectable, otherwise falls back to
the default. A zero-key account gets a typed no-selectable-key response.
Tokens are 32 CSPRNG bytes as 43-char unpadded base64url, minted per write
and retried only on the unique-token-index message so unrelated DB failures
still surface. Serving revalidates lease, expiry, user, key ownership, and
configuration on every request and collapses every failure to one
indistinguishable 404; HEAD runs the same checks but stops before rendering
the API-key-bearing body.

script-assets.generated.ts embeds the two canonical placeholder scripts as
runtime-neutral string constants, generated deterministically by
scripts/generate-agent-setup-assets.ts (with --check for drift) so Wrangler
and Node share one source without a ?raw loader or filesystem access. A
byte-drift test ties the checked-in constants back to the canonical files.

middleware/request-path.ts is the single classifier + redactor shared by
auth (bypass), the CORS layer (bypass), the new request logger, and the
internal-error response, so the exact public matcher (GET/HEAD + 43-char
token + fixed filename) and the token scrubbing can never drift between
layers. A forced failure on a script route leaks the token in neither the
logs nor the 500 body.
Make the codex online catalog deterministic, native-first, all-chat
visible, and reasoning-aware.

Ordering: classify each surfaced chat id against the bundled catalog as
exact (whole-id equality to a bundled gpt-* slug, keeps bundled
priority), variant (non-exact leaf-first segment match to a gpt-* slug),
or unrelated (non-GPT bundled match or no match). Variants sort by
(matched priority, public id) into a band strictly above the max native
priority; unrelated ids sort by public id into a band above the
variants. Emit in (priority, slug) order so array shape is deterministic
regardless of enumeration order. Family never excludes an id.

Every surfaced entry forces visibility 'list' and supported_in_api true
so a hidden bundled source cannot leak onto a registry-addressable
model. When the registry declares effort tiers, force
supports_reasoning_summaries true and forward the supported/default
effort values.

Add focused tests for input-order determinism, each match class,
visibility forcing, and reasoning-summary derivation.
Add the pure model-selection helpers and the instance-local Agent Setup
state machine the Task 6 page mounts on.

agent-setup-models.ts keeps the whole addressable chat catalog and uses
family matching only to stable-partition native ids first (Claude for the
Claude picker; gpt-/codex- for the Codex picker), deduping by id with the
first occurrence winning. Model ids stay opaque; the picker shows the raw
id while a Claude selection persists the `[1m]` context-window form only
when the chosen model advertises a one-million-token window. A restored id
missing from the catalog survives as an unavailable-current option instead
of silently resetting, and the empty sentinel maps to the config's null.
Codex effort suggestions come only from the model's advertised supported
list; any non-empty input is retained, blank clears to null.

useAgentSetup drives one lease per mounted page: a single create POST that
carries window.location.origin, then a serialized mutation pump that never
overlaps a configuration PUT with a heartbeat. Edits are debounced (400ms)
and coalesced; a local draft generation, tracked separately from the
server's configuration revision, gates copy — dirty immediately, clean only
once the current generation is confirmed. A stale ok response still adopts
the freshest token/revision/expiry/URLs without rewinding a newer draft; a
revision conflict adopts server state when no newer edit exists and
otherwise resubmits the latest draft against the new revision; a superseded
409 is terminal. Heartbeats renew every 60s while visible (15s retry, 20s
per-request timeout), visibility-hidden pauses scheduling with immediate
reconciliation on resume, and dispose() clears every timer and ignores late
responses. Backend 409 discriminants are read structurally from
GlobalError.raw, never by matching an English message.

useModels gains useAddressableModelsStore (aliases + include_unlisted) so
the page offers every chat id a downstream agent could address.
Preserve opaque Codex effort input byte-for-byte and tighten native-family
ranking to Claude's claude- ids and Codex's gpt-5/codex- ids without
filtering any chat models.

Make the composable own form mutation tracking through a synchronous deep
watch, so direct v-model-style edits immediately dirty the lease, disable
copy, and schedule persistence. Cancel stale debounce timers before
revision-conflict resubmission and visibility-resume reconciliation to
prevent duplicate PUTs.

Classify retries explicitly: transport errors, 408, 429, and 5xx retry;
permanent 4xx failures remain visible without a loop; machine-readable 409
revision/superseded outcomes retain their dedicated handling. Move request
timeouts and AbortControllers into the composable instance so dispose
aborts an active create/save/heartbeat and releases every timer.

Add regression coverage for near-miss family ids, opaque effort bytes,
direct draft mutation, duplicate-PUT prevention, retryable and permanent
HTTP outcomes, all active-request abort paths, and zero timers after
disposal.
Give create, save, and heartbeat failures separate ownership so a successful
lease renewal cannot hide a rejected configuration write. The public error
projection prioritizes the unresolved form failure; successful and clean
conflict saves clear only that save channel.

Track the generation captured by the active PUT. An explicit save call for
that same generation is now an idempotent flush rather than a second
400ms timer, while a genuinely newer form generation still queues behind
the in-flight request.

Add fake-timer regressions for save-error ownership across heartbeat
success, later save recovery, in-flight save idempotence, and the exact
lease-expiry copy boundary.
Replace the temporary-plan attribution on Agent Setup family ranking with
the client-tool compatibility rationale and official references: Claude
Code's gateway model-discovery contract and pinned Codex picker/catalog
source. Clarify that these constants rank the picker only and never filter
Floway's addressable chat catalog.
Replace the manual Claude Code / Codex snippet block on the API Keys page
with an Agent Setup card driven by the Task 5 lease composable. The card
owns a single useAgentSetup instance, binds every control straight to the
autosaving draft, and offers a copyable shell + PowerShell one-liner built
from the lease's relative script URLs prefixed with the page origin.

- AgentSetupCard: API-key picker, per-agent enable switches, optional
  Claude model/Sonnet/Haiku/effort selects and discovery switch, and a
  free-form Codex model select + effort combobox seeded with upstream
  suggestions. Model pickers reuse the Task 5 helpers (all chat ids,
  native-first, [1m] suffix baked in, unavailable-restored option kept).
  Reka Select rejects an empty option value, so the null override binds to
  a NUL-prefixed sentinel the configuration schema can never accept as a
  real id. Disabled agents keep their values; superseded shows a reload
  terminal; no selectable key shows a create-a-key empty state.
- AgentSetupCommand: persistent copy button (Code copyable=false + own
  Button) that rechecks its gate on click, catches clipboard rejection,
  and announces success/failure through an aria-live status.
- Keys page: loads the addressable-models store, passes key id/name and
  models into the card, and drops the snippet-era row selection. KeysTable
  loses its selectedId/select contract; row copy stays.
- Delete CliSnippet.vue.
Give each command copy button an instance-specific accessible name and connect every visible Agent Setup field label to its rendered Select trigger or Combobox input. Select and Combobox now expose an optional typed id prop that lands on the actual focusable control. Replace the Claude effort setter cast with an exact runtime guard that rejects impossible values.
Give the Claude Code, Codex, and gateway model discovery switches explicit accessible names. Assert against each rendered role=switch element by its exact aria-label instead of only counting unnamed controls.
Implement the Claude Code path in both fixed installer bodies (POSIX Bash
3.2+ and PowerShell 5.1+/7) plus a common status-aggregation and an isolated
integration harness.

Common framework: safe modes/umask, a private working directory with trap
cleanup, and independent per-agent status so one agent's failure never rolls
back or skips the other while any selected-agent failure exits non-zero.
Errexit is deliberately not used in POSIX because Bash disables it inside the
if-guards that drive that aggregation; control flow uses explicit checks and
per-agent rollback.

Claude: discover via PATH plus known official user-local locations (warn on
multiples, never upgrade), install the official user-local build only when
absent (download validated as a real script before execution), then surgically
merge only the managed keys into ${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json
with a real JSON parser, preserving unrelated values, staging in-dir, validating,
backing up, atomically replacing at mode 0600, and rolling back on failure.
Verification issues no inference: reparse settings, print raw --version, reach
the authenticated /v1/models, and run doctor when the subcommand exists.

JSON parsing requires jq: prefer PATH, else download the pinned official
jq-1.8.2 build for Darwin/Linux x64/arm64 into the private dir and verify its
hard-coded SHA-256 (digests confirmed against the release checksums file and
the Sigstore build attestation); an unsupported platform or unavailable jq
fails before any mutation. The API key never reaches argv: jq reads it from the
environment and curl from a mode-0600 config file; doctor/error output is
redacted.

Codex remains a failing placeholder for the next task. Harness runs the served
prefix+body in throwaway HOME/CLAUDE_CONFIG_DIR/PATH against fake CLIs, a fake
installer, and a local model directory; 27 pass / 1 skip, with POSIX under Bash
3.2 and PowerShell under pwsh, and the generated asset regenerated with drift
proven.
Use Windows File.Replace for atomic existing-target updates, including Windows
PowerShell 5.1 where IsWindows is unavailable, while retaining same-directory
Move-Item for new files and Unix.

Reject present non-object env values before backup or mutation on both script
variants. Keep the API key out of global process environments, scope it only to
jq/awk calls, clear ambient exports, and exercise installer/CLI subprocesses
against sentinel leaks.

Bound jq, installer, model-directory, version, and doctor operations. Bash 3.2
uses a portable TERM/KILL watchdog when timeout is unavailable; PowerShell uses
redirected Process children with async output draining and enforced deadlines.
Validate locally served installer content in both languages, accepting scripts
while rejecting uppercase HTML block pages.

Extend the isolated harness from 27 to 36 passing Claude checks and regenerate
the embedded script assets.
Protect the PowerShell stage before writing secret JSON, protect backups and
restored settings, and tighten an existing Windows target before File.Replace
so a permissive historical DACL cannot survive atomic replacement.

Complete bounded execution across installers, raw version checks, doctor
capability probes, and doctor runs. The Bash 3.2 fallback now returns an
explicit timeout status, reaps its processes, closes watchdog descriptors, and
is exercised with timeout tools removed from PATH. Only explicit unknown
command output skips doctor; timeouts and unexpected failures roll back.

Name the shell contract as Bash 3.2+, extend installer harness coverage to 44
passing Claude checks, and regenerate the served script assets.
Launch Bash fallback commands in a dedicated process group and signal the full
group with bounded TERM-to-KILL escalation. Exercise the native macOS path in a
truly isolated PATH, record a three-level installer descendant, and prove it is
dead after timeout.

Make PowerShell tree termination explicit across platforms: Unix PowerShell 7
uses Kill(true), while Windows PowerShell 5.1 uses checked taskkill /T /F. Check
non-Windows chmod failures before secret writes and clean transient backups.

Use a .NET Framework-compatible backup timestamp and classify doctor absence
only when diagnostics identify the doctor command itself. Extend the harness to
47 passing Claude checks and regenerate embedded installer assets.
Replace the failing Codex placeholder in both fixed installers with a real,
transactional Codex setup, and extend the isolated installer harness to cover
it end to end.

Discovery mirrors Claude: PATH winner is authoritative, official user-local
locations are also consulted, multiple installs warn, and an existing CLI is
never upgraded or version-gated. A missing CLI installs via the official
user-local installer (POSIX chatgpt.com/codex/install.sh with
CODEX_NON_INTERACTIVE; Windows install.ps1), reusing the shared HTML guard,
bounded download/exec, and process-tree timeout helpers.

config.toml is written ONLY by driving the installed Codex over
`app-server --listen stdio://` (JSON-RPC 2.0, newline-delimited): initialize ->
initialized -> config/batchWrite, demuxing unrelated notifications and matching
by id, with no TOML text editing or version branching. The batch sets the fixed
Floway provider leaves (model_provider, model_providers.floway.{name,base_url,
wire_api,supports_websockets}, chatgpt_base_url, features.apps,
cli_auth_credentials_store) and clears/sets the opaque model and
model_reasoning_effort via JSON null/verbatim string. A batch status of ok or
okOverridden confirms the base config; okOverridden is surfaced with its
non-secret layer metadata only. Bash uses two private FIFOs on fixed fds with a
job-control process group and a per-read deadline; PowerShell uses a redirected
System.Diagnostics.Process with async stderr draining and bounded reads. The
API key is never part of any app-server request.

Auth is staged transactionally: both config.toml and auth.json are backed up
first (recording absence), then a minimal ChatGPT-mode auth.json is written
(server-rendered id token, in-memory API key as access_token, noop refresh,
fresh RFC3339 last_refresh) under owner-only permissions and atomically
replaced. Any config, auth, or verification failure restores both backups or
removes newly created files; a freshly installed CLI is never uninstalled.
Verification reparses the auth (id token / access-token equality, never
printed), prints the raw `codex --version`, and reaches the authenticated
/azure-api.codex/models directory without inference, confirming a selected
model is in the catalog. `codex doctor` is intentionally not a gate: its
overall status fails on unrelated environment checks even when config and auth
are healthy.

The harness gains a fake Codex app-server (handshake, ordering, notifications,
ok/okOverridden/error/malformed/delay/premature-EOF/large-stderr modes, stdin
kept open until response), exact batch-edit and null-clear assertions,
CODEX_HOME handling, auth shape/timestamp checks, backup+rollback and
independent-agent outcomes, secret-in-output and no-inference guards, and a real
pinned Codex 0.144.1 app-server smoke test that self-skips when the pinned CLI
is absent. The two install-from-absent cases self-skip on a host that already
has a system Codex at a known location.
Make Codex auth timestamps locale-independent by formatting UTC with
InvariantCulture and literal RFC3339 separators, including a runtime regression
under a culture whose time separator is a dot.

Keep model-directory transport/auth, JSON parsing, catalog-shape, and selected-
model-missing failures distinct without surfacing the in-process authorization
header. Add assertions that every failure class remains secret-free.

Scope CODEX_NON_INTERACTIVE to the installer invocation with try/finally,
restoring either the caller's exact prior value or absence before app-server and
version verification. The fake installer now requires the temporary true value,
while the fake Codex process rejects leaked or incorrectly restored values.

Lock the Windows Codex auth permission ordering in the harness: protect the
stage before writing secrets, harden an existing target before File.Replace,
and re-protect a restored target after rollback.
Require the real app-server smoke guard to parse the complete
`codex-cli <semver>` output and compare exactly against 0.144.1, so 0.144.10 or
extra output cannot exercise an unverified wire protocol.

Make every Windows Codex auth ACL source-order assertion first prove that each
function and operation marker exists before comparing indices. This prevents a
missing marker (-1) from accidentally satisfying an ordering relation.

Exercise valid-JSON invalid model catalogs with a non-array models value. The
PowerShell verifier now requires the models field to be an array, emits the
fixed safe shape error, restores both config and auth, and remains secret-free.

Rewrite the installer-harness header to describe the current Claude and Codex
system, full behavior coverage, real pinned smoke, and only host-prerequisite
skips.
Rewrite the README post-boot flow so it describes the API Keys -> Agent
Setup card instead of hand-copying a CLI snippet into agent config. The
new prose covers selecting the API key the setup link carries, toggling
Claude Code / Codex and tuning model, reasoning effort, Sonnet/Haiku
aliases, and gateway model discovery, waiting for the autosave spinner,
and copying the curl|bash or irm|iex command. It states the ~5-minute
lease behavior, that only a missing CLI is installed via the official
user-local installer without sudo (existing installs are never
upgraded), that config is merged surgically with per-file backups, that
the Codex step replaces the current CODEX_HOME/auth.json ChatGPT login
after a timestamped backup, and that verification never issues an
inference request. Upstream Codex/Claude credential-import steps stay
untouched and separate.

Document the Agent Setup installer verification surface in AGENTS.md: the
checked-in setup.sh/.ps1 location, the generate-agent-setup-assets --check
drift gate, and the test:agent-setup-installers harness (not part of the
vitest run).
Distinguish the surgical Claude settings and Codex TOML merges from the
Codex auth replacement, which preserves the previous ChatGPT login only
in a backup. Describe the setup URL as stable under a renewed five-minute
lease, and assign each model and reasoning control to its owning agent.
Address the final whole-branch review findings for Agent Setup.

- internalErrorResponse now returns and logs a fully opaque internal
  error for the exact public setup GET/HEAD routes. Path redaction alone
  was insufficient: a thrown error can carry the served API key, the
  lease token, or any other secret in its message/stack, and
  console.error would have written the same to the log. All other routes
  keep the full stack trace, and a near-miss script-shaped path is still
  run through the shared redactor.

- Document the updateConfiguration CAS precedence at the repo interface
  and in both backends: when a matching token is both expired and edited
  against a stale revision, the revision conflict wins and nothing
  rotates; the client's rebased retry against the still-expired lease is
  the write that rotates the token. Add a repo test exercising the
  conflict-then-retry rotation across memory and SQL.

- Add route coverage: a soft-deleted own API key collapses the public
  GET to the generic 404; a PUT selecting an unavailable (soft-deleted)
  or foreign key returns the 400 contract with no token/key leak.

- Add an app-level CORS invariant: OPTIONS on a script path is answered
  as a preflight (default ACAO) that never resolves the lease and cannot
  act as an existence oracle, while the real GET stays no-CORS. The auth
  exemption remains GET/HEAD only.

- Capture the expected stack in the forced-DB-error test so it no longer
  clutters test output.
…d create

Two dashboard Agent Setup robustness fixes from the whole-branch review.

Lost-ack conflict: because a fresh lease rotates the token and supersedes
every other tab, a revision conflict on a token we still own can only be a
lost ack — an earlier PUT of ours committed but its response never arrived,
so the server advanced under one of our own past configurations. The old
reconcile overwrote the local draft with the server's config whenever no edit
occurred during the request, silently discarding the user's newer intent.
reconcileRevisionConflict now takes the attempted configuration, always adopts
the server's lease metadata/revision, and confirms the generation only when
the server already holds exactly what we attempted; otherwise it keeps the
current draft and resubmits against the freshly adopted revision.

Failed initial create: a create error left the card stuck on an endless
"Preparing setup…" spinner. The composable now exposes retryCreate (aborts any
lingering request, clears the error, posts exactly one more create, and guards
against a stale aborted attempt writing state), and the card renders the create
error with an explicit Retry action before the spinner branch.
…kup cleanup

Three installer-script robustness fixes from the whole-branch review, plus
runtime harness coverage; the embedded script assets are regenerated.

Bash signal handling: EXIT now owns cleanup on its own, and INT/TERM only
translate the signal into the conventional exit status (130/143) and let that
exit fire the EXIT trap. Previously a trapped INT/TERM ran cleanup inline and
then let the interrupted script resume into the next agent's configuration —
an interrupt during Claude would delete the working directory and still go on
to mutate Codex. A new harness test signals a mid-Claude-install run and
asserts the process exits 130/143, never reaches the Codex phase, leaves no
Codex mutation, and cleans the working directory.

POSIX rollback: the Claude and Codex restore paths no longer swallow a failed
restore with `|| true`. A failed restore-from-backup rename (or a failed
remove of a run-created file) now leaves the backup untouched, prints a
prominent path-specific warning telling the operator how to recover by hand,
and returns non-zero so the failure aggregates. Codex handles its two files
independently so one failure does not abandon the other. Harness tests inject
a restore rename failure and assert the warning, the preserved backup, and
that the file was not silently reported as restored.

PowerShell Codex auth backup: if ACL hardening fails after the backup copy,
the unprotected copy of the ChatGPT login is now removed before rethrowing
instead of being left readable on disk; the untouched original is preserved.
A harness test forces the protection failure and asserts the backup is gone,
the original is unchanged, and no secret leaks.
The rendered setup script carries a live API key, so it must never be cached
by any hop. cache-control:no-store covers HTTP/1.1, but HTTP/1.0 proxies and
clients that ignore Cache-Control could still retain it. Add Pragma:no-cache
and Expires:0 to the script response headers and assert all three on the
GET and HEAD paths.
The gateway's applyClaudeContextSuffix helper (plus its ModelContextLimits
type and ONE_MILLION_CONTEXT_TOKENS constant) had no runtime caller — the
browser's agent-setup-models helper is the single place the [1m] suffix is
applied, at model selection, and the gateway treats the persisted id as
opaque and renders it verbatim. Remove the dead helper and its tests, and
rewrite the browser helper's now-stale "mirrors the gateway's persist-time
rule" comment to state the actual ownership. The official model-config
reference URL is preserved.

In setup.ps1 the model-directory comment sat above the generic
Invoke-FlowayProcess helper it did not describe. Move it above
Test-FlowayModelDirectory, and give Invoke-FlowayProcess its own comment
covering redirected stdio, the deadline, process-tree termination on
timeout, and the returned exit code + combined output. Regenerate the
embedded script asset.
The Agent Setup lease persisted a browser-supplied public_base_url and
echoed it into the rendered installer. That stored origin was redundant
with what the request itself carries, and forced the create POST to ship
window.location.origin as a validated body.

Drop the column and every publicBaseUrl slot across the migration, repo
types, SQL/memory backends, and control-plane schema. The create POST now
takes no JSON body. The public GET script handler resolves the Floway
base origin from the serving request via a new runtime-neutral
getRequestOrigin: the request URL origin is authoritative on Cloudflare,
while on Node a single clean http|https X-Forwarded-Proto value overrides
the scheme (bundled docker/nginx.conf terminates TLS and forwards plain
HTTP with the public Host) — the forwarded host is never trusted, and a
comma-chained or invalid proto falls back to the request scheme. HEAD
still skips rendering. Origin-relative script URLs and the dashboard's
window.location.origin concatenation are unchanged.
…ginx

The bundled nginx set `X-Forwarded-Proto $scheme` unconditionally, and
$scheme is `http` because the container only `listen 80`. An operator who
terminates TLS in their own proxy in front of it had the incoming
`X-Forwarded-Proto: https` clobbered to `http`, so getRequestOrigin
rendered an `http://` setup-script origin even behind HTTPS.

Forward the header through an http-context `map` that passes only a clean
single `http`/`https` token and otherwise falls back to `$scheme`. This
never forwards a comma-chained or arbitrary value across the trusted
bundled-proxy boundary, and mirrors exactly the validation getRequestOrigin
applies on Node. Host handling is unchanged; a forwarded host is never used.

The runtime-info comment falsely claimed the bundled nginx terminates TLS;
it is a plain HTTP reverse proxy. Rewrite it and the mirroring routes test
comment to describe TLS terminated in an operator's outer proxy or a custom
proxy pointed straight at Node, with nginx preserving a validated proto or
its own scheme. Add a source-coherence test asserting the map and
proxy_set_header use the validated variable. Verified with `nginx -t`.
The public setup script needs the gateway's own origin (ANTHROPIC_BASE_URL,
the Codex provider base_url, and the placeholder Codex identity token's
host-derived email). The previous approach derived that origin server-side
from the serving request — trusting an X-Forwarded-Proto forwarded across the
bundled nginx plus a getRequestOrigin runtime helper — which coupled the
gateway to its deployment topology and forced it to know its own public URL.

The dashboard already knows the exact origin the operator reached it on
(window.location.origin), so it now injects that into the shell that executes
the fetched installer body; the gateway learns nothing about its own origin.

- Revert the backend-origin design entirely: docker/nginx.conf, the
  getRequestOrigin helper, and its tests/coherence test are restored to their
  base state; routes.ts no longer imports or calls any origin helper.
- render.ts drops FLOWAY_BASE_URL/$FlowayBaseUrl and the server-side
  renderCodexIdentityToken; the rendered prefix is now {apiKey, configuration}.
- The copyable commands lead with the origin and reference it exactly once:
  Bash `export FLOWAY_BASE_URL='<origin>'; curl -fsSL "$FLOWAY_BASE_URL/..." | bash`
  (the piped bash inherits the exported variable); PowerShell
  `$FlowayBaseUrl = '<origin>'; irm "$FlowayBaseUrl/..." | iex` (iex runs in the
  same runspace). The origin is emitted through browser-side POSIX / PowerShell
  single-quote literal encoders.
- The fixed installers require FLOWAY_BASE_URL/$FlowayBaseUrl (validated as a
  non-empty http(s) origin before any mutation), keep it out of third-party
  child environments (`export -n` on Bash; in-process only on PowerShell), and
  assemble the Codex identity token locally (jq @base64 -> base64url on Bash,
  Convert.ToBase64String on PowerShell) — byte-identical across both and to the
  previous server output.

Tests cover prefix absence, the exact quoted commands, the real Bash/pwsh
pipeline scoping via a local probe fixture, the missing/invalid-origin guards
failing before any mutation, and the locally-derived identity token's host
claim.
Menci added 26 commits July 12, 2026 01:45
The gateway origin now reaches the setup scripts solely through the
dashboard's one-line command, and each installer assembles its own
ChatGPT identity token locally; no backend state, handler, or serve-time
step supplies either. Rewrite the four comments that still described the
abandoned server-rendered design — the acquisition-body schema note, the
create-POST test note, and the Codex auth-staging comment in both
installers — and regenerate script-assets.generated.ts to match.
…r callers

The Agent Setup web feature had one component fanning out to five files each
serving only it: shell-literal fed only agent-setup-command, and
agent-setup-command, agent-setup-models, useAgentSetup and AgentSetupCommand
were each imported by the Card alone. The fragmentation added files, exported
symbols, and narration comments without any reuse.

- Delete shell-literal.ts and agent-setup-command.ts. The two setup-command
  builders were single-quote-escaping wrappers used once each by the Card; they
  are now two inline template-literal computeds in AgentSetupCard.vue, keeping
  the single-quoted-literal escaping (the real defense-in-depth for injecting an
  origin into a shell) and dropping the NUL-assertion ceremony that guarded an
  input window.location.origin cannot produce. The Card test already asserts the
  full rendered sh/ps1 commands, so coverage is unchanged.
- Keep agent-setup-models.ts as the one substantial, vendor-reference-heavy pure
  helper, but internalize applyClaudeContextSuffix (a single internal caller)
  and un-export the internal AgentFamily type. Its direct [1m] tests fold into
  buildModelOptions cases that exercise the same rule end to end.
- Keep AgentSetupCommand.vue: it holds per-instance copy/flash state and renders
  twice, so inlining would duplicate stateful markup. Keep useAgentSetup.ts
  intact — its comments are concurrency invariants, not narration.
- Drop the trivial copyDisabled computed (inline !canCopy) and trim restated
  header comments; rewrite one test comment that narrated task history.

Net: 6 production + 6 test files -> 4 + 4; exported lib/composable symbols
16 -> 10.
Construct the two copyable probe commands directly in the integration harness after their single-consumer production helpers moved into AgentSetupCard. Reuse the gateway PowerShell literal encoder already exercised by the harness instead of retaining a production export for tests. Clarify that URL origin syntax excludes quotes while keeping shell literal escaping independent of that invariant.
Inline one-shot hashing, installation, model-directory, and path helpers. Share CLI discovery and rollback primitives while retaining transactional, process-tree, full-duplex, checksum, and secret-handling boundaries. Condense duplicated narration and keep compatibility and vendor-reference comments.
…m narration

Final-state cleanup of the Agent Setup backend after prior reviews:

- Merge the one-consumer request-logger middleware directly into app.ts as
  an inline completion-log middleware; delete request-logger.ts. Token
  redaction stays in the shared middleware/request-path helper (no
  duplication) and log output/redaction are unchanged.
- render.ts: inline the two single-caller assignment builders into their
  prefix renderers and make shellLiteral/powerShellLiteral local — neither
  has a production consumer. render_test.ts drops the two encoder-only
  describe blocks and folds single-quote escaping / NUL / Unicode coverage
  into the prefix-renderer tests.
- routes.ts / configuration.ts / schemas.ts / repo types+sql+memory /
  auth.ts / internal-error-response.ts: delete obvious module/shape/list-
  ordering narration, keeping only the security boundary, token-collision
  retry, no-store, 404-collapse, HEAD-secret, and CAS-precedence invariants.
  The CAS precedence invariant now lives once on the repo interface rather
  than being re-narrated in each impl.

configuration.ts stays the shared parser boundary (schema + type + default +
error) for repo JSON, render, routes, and control-plane schemas; merging it
away would split the type from its schema without removing a file.
Keep the PowerShell literal encoder local to the integration harness after the renderer made it private, and assert rollback ACL ordering through the shared restore primitive plus the Codex auth protection flag.
…ckage

Move the Agent Setup domain — configuration schema, language-native prefix
rendering, canonical installers, generated assets, wire schemas, token
generation, the persistence contract, and the route factories — into a new
pure package that depends only on hono/zod/@hono/zod-validator and knows
nothing of databases, gateway auth/CORS/logging, or apps/web.

The route surface is split into dependency-injected factories:
createAgentSetupPublicRoutes seals every failure so a host can mount the
API-key-bearing GET/HEAD scripts ahead of its middleware without a path
bypass, and createAgentSetupControlRoutes takes getUserId /
listSelectableApiKeyIds callbacks and an AgentSetupRepository. Token is the
lease identity; the contract expresses multi-row (multi-page) leases with an
insert that sweeps only expired siblings, a revision-checked config update
that never rotates the token, and an expiry-only renewal.

Installer harness and asset generator move under the package; domain route
tests run against an in-memory fake repository.
…store for multi-page semantics

Gateway now owns only the three permitted integration seams: app.ts mounts
the package's public GET/HEAD script routes structurally ahead of the logger,
CORS, and auth middleware (no path-match bypass); control-plane/agent-setup.ts
injects the repository and authenticated user into the package's control-route
factory behind auth; and the repo layer implements the package's
AgentSetupRepository contract plus migration 0050.

Rewrite the lease model: token is the primary key, so a user holds many
concurrent independent leases. POST inserts a new revision-1 row and an AFTER
INSERT trigger sweeps only that user's already-expired rows; PUT is a
revision-checked config write that never rotates the token; heartbeat extends
expiry only, never touching updated_at or the revision. A swept token yields a
terminal 'missing'. SQL translates a token PK collision into the package's
typed AgentSetupTokenCollisionError.

Delete middleware/request-path.ts and its test, and strip Agent Setup
awareness from auth, the request logger, and internal-error-response —
the public routes seal their own failures and never reach those layers.

Regenerate migration 0050 in place; the feature is not yet deployed.
…n semantics

Each dashboard page owns an independent lease keyed by its own token; pages no
longer supersede one another. Rename the terminal state from `superseded` to
`terminated`: a lease ends only when the server no longer recognizes this
page's token (it expired and was swept, reported as `missing`). Heartbeat and
PUT never rotate the token — renewal only extends expiry in place — so the
lost-ack reconciliation reasoning is stated in terms of this page being the
sole writer of its token. The terminal card message now names an expired /
invalid link instead of a takeover by another tab or device.

Debounce, serialized pump, lost-ack, and revision-conflict behavior per token
are unchanged.
…in tests

Read the injected user id through one env cast inside the generic control-route
factory (Hono narrows a validated route's handler context to a massaged env),
and regenerate the assets header for the new package paths. Fix the domain
test harness to issue real PUT / heartbeat verbs, seed schema-valid stored
configurations, and expect a sealed 500 (not a rejection) for an unrelated
insert failure.
…hrowing synchronously

The AgentSetupRepository contract returns a Promise; the in-memory
implementation now returns a rejected promise on a token collision so it
matches the SQL backend's behavior under the repo parity test.
Consume malformed filenames, trailing segments, and unsupported methods beneath token-shaped setup paths inside the public route mount. This keeps a still-live URL credential out of downstream access logs while preserving structural middleware isolation.
Make the package-level verification command run its Vitest project directly, matching the rest of the workspace and preventing filtered test invocations from silently doing nothing.
Require the exact 43-character token shape on the public catchall so authenticated heartbeat and control paths continue into the host middleware chain. Keep malformed paths beneath actual setup credentials contained.
Assert that unsupported methods and CORS preflights on credential-bearing script URLs are contained before the generic middleware chain, return non-cacheable 404 responses, and never resolve or log the token.
Give setup.sh and setup.ps1 one canonical output contract, modeled on
InitWin's compact terminal UX: a blank line then `┌─ <name>` opens a phase,
`│  ` continues its body, `│  · ` marks a step, and a closing Summary phase
lists each agent as `<label>  [configured|failed|skipped]`. Both installers
print the identical text so an equivalent configuration yields the same
stdout line sequence.

Route every user-visible line through a small helper layer instead of
scattered printf/Write-Host. Informational, progress, and success lines go to
stdout; warnings, errors, rollback notices, and captured tool output go to
stderr. Color (title/phase cyan, step dark-cyan, success green, warning
yellow, error red, captured/skipped gray) is emitted only for an interactive
terminal with NO_COLOR unset, probed per stream so a redirected capture stays
escape-free. Bash gates ANSI on per-stream TTY detection; PowerShell rides the
host via Write-Host -ForegroundColor for stdout (no escapes when captured) and
colors [Console]::Error with ANSI only for an interactive stderr.

Remove the PowerShell "setup failed: ..." double wrapper: each failure now
prints exactly one primary error at its detection site and unwinds through a
'floway-handled' marker, with a cause-specific rollback notice where Bash
emits one. Align the app-server/timeout/doctor phrasing across both shells and
add the PowerShell manual-recovery warnings (naming the preserved backup and
the action) that Bash already had when a restore or remove fails.

Security behavior, config wire output, transaction independence, and exit
codes are unchanged. The harness gains an output-contract section: normalized
Bash/PowerShell happy-path parity, stdout/stderr split, no-ANSI-when-captured,
forced-color assertions via a test-only FLOWAY_INSTALLER_TEST_FORCE_COLOR hook,
warning/failure structure, and PowerShell rollback-restore guidance via a
test-only FLOWAY_INSTALLER_TEST_FAIL_RESTORE hook.
_emit_line wrapped even empty-color lines in a trailing reset, so an
interactive/forced-color run emitted a stray escape on default-color
detail lines. Skip the color wrap when no color is given.
Match Bash ANSI intensity to PowerShell ConsoleColor shades and distinguish dark-cyan steps from cyan phases. Color interactive PowerShell stderr through Console.ForegroundColor so Windows PowerShell 5.1 does not depend on VT mode, while retaining a test-only redirected ANSI path. Align version and doctor timeout messages, avoid misleading recovery guidance after a successful restore, and normalize captured-output trailing lines.
Update the Windows auth ordering contract for the safer rollback design: backup files are protected before mutation, and restore moves that protected inode back without a fallible post-move chmod that could produce false recovery guidance.
Keep the one-second process deadline but extend the fixture's natural runtime and assertion margin. This still proves tree termination happens far before natural completion without conflating PowerShell startup variance under a loaded harness with timeout correctness.
Apply the same natural-runtime separation to the PowerShell version timeout case so process startup variance cannot consume a threshold intended to prove the one-second child deadline.
Launch only the downloaded Codex PowerShell installer with the process-scoped  argument documented by OpenAI. The override is confined to the child process, does not persist policy changes, and leaves the Claude installer invocation unchanged. Pin the upstream README reference and assert the child command line in the harness.
Drive the HTTP-served install.ps1 path rather than the local hook when asserting the official execution-policy argument, so the test observes the child process whose invocation carries the documented override.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant