feat(setup): add one-command agent configuration#168
Open
Menci wants to merge 56 commits into
Open
Conversation
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.
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.
This reverts commit c38e366.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Behavior
The API Keys page now lets users:
curl -fsSL <setup-url>/setup.sh | bashirm <setup-url>/setup.ps1 | iexThe 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:
sudosettings.jsonand Codexconfig.tomlauth.jsonafter backing it upSecurity
Additional changes
0050Verification
pnpm run test— 337 files, 4013 tests passedpnpm run test:agent-setup-installers— 95 passed, 2 host-dependent skipspnpm run lintpnpm run typecheck— all 19 projects passedpnpm run build:webpnpm jiti scripts/generate-agent-setup-assets.ts --checkKnown platform boundaries
File.Replace, andtaskkill /Tpaths were source-reviewed and fixture-tested but not executed on Windows