Repository navigation
Security
- Recheck account eligibility on CLI and device credential routes (#428):
/auth/cli/authorize,/auth/deviceand/auth/device/verifynow treat an inactive account as logged out, so it can no longer mint an API key or authorize a device. CLI authorize also enforcesALLOWED_EMAIL_DOMAINS.POST /auth/device/tokenalways reloads the authorizing user and returns403 access_denied(and deletes the code) if that user was deactivated, made read-only, deleted, or falls outside the domain allow-list. - Unify post-login redirect validation (#427): one open-redirect gate for the OAuth callbacks, password login and
/auth/logout?redirect=. Rejects absolute, protocol-relative, backslash and control-character values. Password login now honors theredirectform field (fixes the device flow and deep links on password-auth deployments landing on/) and ignores stale OAuthpost_login_redirectcookies. - Consume device codes atomically with API key issuance (#424): concurrent
/auth/device/tokenpolls could both mint a key (the second replacing the first). The claim and key issuance now happen in one transaction; exactly one poll wins, the rest getinvalid_grant. A key-limit409leaves the code retryable. - Enforce an 8–72 byte password policy (#423): admin create-user returned a 500 for passwords over 72 bytes, and password login accepted anything sharing a valid password's first 72 bytes. Passwords are now validated as 8–72 bytes (never truncated) for admin create-user,
ADMIN_BOOTSTRAP_PASSWORD, and password login. - Valid dummy bcrypt hash for nonexistent password users (#422): the timing-equalization dummy hash was malformed, so unknown emails answered in ~1 ms vs ~250 ms for known ones, enabling account enumeration on
POST /auth/password/login. - Device flow responses no longer cacheable (in #418):
/auth/device/codeand/auth/device/tokennow sendCache-Control: no-storeon success and on every error.
Bug Fixes
- Reject Codex self-parent links across equivalent UUID spellings (#425):
parent_thread_uuidequal tothread_uuidin a different case or spelling (unhyphenated, braced,urn:uuid:) is now rejected with 400. - Make nested Claude subagents findable (#421): depth ≥ 2 subagents are now reachable via a per-level "Launched here (n)" dropdown and a path row; "All subagents" counts every agent file. Also fixes
useTranscriptDatabriefly returning the previous file's lines when switching sources.
Performance
- Stop the analytics recompute loop and cut compute memory ~7x (#416): computes longer than the 5s DB timeout never cached, so every poll recomputed the whole session and could exhaust a 512MB VM. Cache-miss computes are now detached from the request, deduped per session, and cached even if the client leaves. Stale cards are served immediately, with a background refresh when they are more than 2 minutes old. Analytics parsing no longer memoizes agent files or keeps a raw map per line.
- Stream
sync/fileandfiles/download(#417): transcripts are streamed from chunk data with periodic flushes instead of building merged copies. Peak memory for a 15MB transcript drops from ~110MB to ~32MB.line_offsetnow filters by absolute line number (fixing wrong lines when chunks had gaps), and responses are newline-terminated. - Raise analytics agent-file cap from 200 to 500 (#420).
Pricing
- Add OpenAI GPT-6.1 Sol (
gpt-6.1-sol) (#415): $2 / $10 input/output per MTok, cache read $0.10 (0.05x input). Long-context tier not included. - Add Claude Sonnet 5.5 (
sonnet-5-5) (#414): $2 / $10 input/output, cache write $2.50 (5m) / $4 (1h), cache read $0.20.
Configuration
- Smart recap: switch the production OpenAI recap model to
gpt-6-luna(#413):fly.tomland all example configs/docs now referencegpt-6-lunainstead ofgpt-5.6-luna. Self-hosters keep whateverSMART_RECAP_MODELthey set. Existing recaps are not regenerated.
Maintenance
- Backend maintenance (#418): Go 1.26.6 → 1.27.1 (
go.mod, CI, both backend Dockerfiles); bumpsandybalholm/brotli1.2.6,klauspost/compress1.20.1,moby/moby/api1.56.0; Go 1.27 modernizers; minor cleanup. - Frontend dependency bumps and audit fix; dedupe transcript-pane scroll helpers (#419): in-range updates (storybook 10.6.1, react-query 5.104.0, dompurify 3.4.16, react-router-dom 7.18.4, zod 4.6.5, …),
npm auditclean. The four transcript panes now share virtualized-scroll helpers and auseFirstVisibleIndexhook. - Document and test the device-verify limiter's soft capacity bound (#426): docs and tests only; live lockouts are never evicted.
Breaking Changes
- No renamed or removed env vars.
- Passwords must be 8–72 bytes. Admin create-user returns
400 "Password must be at most 72 bytes"for longer passwords, and an over-longADMIN_BOOTSTRAP_PASSWORDfails startup with a clear message (it already failed before, with a raw bcrypt error). Password login rejects passwords over 72 bytes; no existing user is affected, since no stored hash can belong to a longer password. - Inactive or ineligible accounts can no longer obtain CLI credentials.
/auth/cli/authorizeredirects inactive users to/loginand users outsideALLOWED_EMAIL_DOMAINSto/login?error=access_denied.POST /auth/device/tokencan now return403 access_deniedeven with no domain allow-list configured. - CLI impact: no CLI release required. The new device token
403uses the standard RFC 8628access_deniederror code. GET /api/v1/sessions/{id}/sync/filewithline_offset: the response is now newline-terminated and filtered by absolute line number.- Building from source requires Go 1.27+.
DB Migrations
None.
API Changes
No endpoints were added or removed. Behavior changes to existing endpoints:
POST /auth/device/token: documented full response table. New403 {"error": "access_denied"}whenever the authorizing user is no longer eligible (deactivated, read-only, deleted, or outsideALLOWED_EMAIL_DOMAINS), not only when a domain list is configured. Concurrent polls: exactly one200, the rest400 invalid_grant.409 api_key_limit_exceededleaves the code retryable.POST /auth/device/code,POST /auth/device/token: responses (including errors) carryCache-Control: no-store.GET /auth/cli/authorize: inactive accounts are redirected to/login; read-only identities and users outsideALLOWED_EMAIL_DOMAINSare redirected to/login?error=access_deniedand no key is minted.POST /auth/password/login: honors the optionalredirectform field (relative paths only;/auth…stays a backend path, anything else getsFRONTEND_URLprepended). Precedence: pending CLI flow, thenredirect, thenFRONTEND_URL. Ignores and clears thepost_login_redirectcookie. Passwords over 72 bytes redirect to/login?error=Password is too longwithout counting as a failed attempt.GET /auth/{github,google,oidc}/loginredirectparam andGET /auth/logout?redirect=: absolute, protocol-relative (//), backslash, or control-character values are ignored.POST /api/v1/admin/users:passwordmust be 8–72 bytes; over-long passwords return400instead of500.POST /api/v1/sync/chunk(codex_rollout):parent_thread_uuidis compared tothread_uuidas parsed UUIDs, so any spelling of the same UUID returns400.GET /api/v1/sessions/{id}/sync/file:line_offsetreturns lines whose absolute line number is greater than it; every returned line ends in\n. Response is now streamed.GET /api/v1/sessions/{id}/analytics: may serve cached cards with a stale (lower)computed_lineswhile a background refresh runs (refresh only when cards are more than 2 minutes old). First view or a card schema version bump still waits for the compute.validation_error_countis set only on responses that waited for a fresh compute.