Skip to content

Keep machines and sign-ins working when a deployment changes its origin - #725

Merged
lavaman131 merged 1 commit into
mainfrom
previous-origins
Oct 22, 2026
Merged

lavaman131 merged 1 commit into
mainfrom
previous-origins

Conversation

@lavaman131

Copy link
Copy Markdown
Collaborator

Changing APP_ORIGIN currently signs everyone out and makes every paired machine run chopin pair again. Machine GitHub authorizations and browser session credentials are encrypted with the origin as AES-GCM additional data, so after a move they no longer decrypt, and the connector stops with machine-reauthorize. Document links on the old name also stop resolving through MCP.

This adds an optional PREVIOUS_ORIGINS setting: a comma-separated list of origins the deployment moved from. Each one is validated like APP_ORIGIN.

Credentials

  • Machines and Sessions decrypt with APP_ORIGIN first and then with each previous origin. They never try an origin outside those.
  • If a credential only decrypts under a previous origin, it is re-encrypted under APP_ORIGIN as its next credential revision. This goes through the existing compare-and-swap rotate in storage, so storage and migrations are unchanged. When concurrent requests move the same record, one wins and the others re-read what it stored. After one use, nothing depends on the old origin, so PREVIOUS_ORIGINS can be removed.
  • The server logs once per record type (machine, browser session) when it moves a record. The log line contains no identifiers or secrets.

Other places keyed on the origin

Place Change
Origin checks on browser mutations (channels, navigation, tasks, research, experiments and connector routes, logout) Also accept previous origins. This keeps pages still open on the old name working while it is served.
WebSocket admission (/ws, /ws/sidebar, /api/v1/agent) and /mcp Origin check Also accept previous origins.
MCP document locators (locateDocument, also used by remote Planner attach) Document, child-document and legacy /channels/<id> links on a previous origin resolve. Links Chopin writes stay on APP_ORIGIN.
Cookies No change. They are host-only (__Host-), so people sign in once on the new name. A session cookie still sent to the old name, or to a new port on the same host, restores and moves.
OAuth redirect_uri No change. It is still derived only from APP_ORIGIN, never from Host. A sign-in that was in progress across the restart has to start again.
/auth/github/setup on the old name No change needed. It authenticates by cookie, and its relative redirect is sent on to the new name by the proxy.
Pairing links and connector registration No change. Pairing links are built on APP_ORIGIN. Connectors keep calling the old name with their bearer token.

The connector does not change.

Docs and config

  • docs/self-hosting.md adds a "Moving to a new origin" section: add the new callback URL to the GitHub App, keep serving /mcp, /api, /connector*, /auth/github/callback and /auth/github/setup on the old name while redirecting browsers on other paths, then set APP_ORIGIN to the new origin and PREVIOUS_ORIGINS to the old one.
  • docs/authentication.md describes the decrypt and re-encrypt rule.
  • PREVIOUS_ORIGINS is added to .env.example and is late-bound in compose.yaml. An unset value is ignored.

Tests

New tests failed before the change:

  • A machine paired under one origin keeps working after a restart with the new APP_ORIGIN and the old origin in PREVIOUS_ORIGINS. Concurrent uses from two instances all succeed, the grant is stored as revision 2 under the new origin, and the machine still works after PREVIOUS_ORIGINS is removed.
  • A browser session behaves the same way, including concurrent restores.
  • A grant or session stored under an unlisted origin is still refused and left unchanged.
  • The auth routes do the same end to end through /api/session and machines.use.
  • Document, child and legacy links on a previous origin resolve; links on an unlisted origin do not.
  • /mcp and socket admission accept a previous Origin and refuse an unlisted one.
  • PREVIOUS_ORIGINS is validated.

Storage did not change, so no contract tests were added.

Verification

  • bun test: 6733 pass
  • bun run types
  • bun run ci
  • PostgreSQL storage tests: 121 pass
  • E2E: auth lifecycle, machines, connector, build restart, planner picker, experiments and machine choice, 57 pass

No UI changes.

Stored machine GitHub authorizations and browser session credentials are
encrypted with APP_ORIGIN as additional data, so changing it made every
machine run `chopin pair` again and signed everyone out. The optional
PREVIOUS_ORIGINS setting lists the origins a deployment moved from. A
credential that only decrypts under one of them is re-encrypted under
APP_ORIGIN as its next credential revision, through the existing
compare-and-swap rotation, so concurrent moves are safe and later restarts
no longer need the old origin. Origins outside APP_ORIGIN and
PREVIOUS_ORIGINS are still refused.

Origin and WebSocket checks also accept the previous origins, and MCP
document links on them resolve to the same documents, while Chopin keeps
writing links on APP_ORIGIN. Self-hosting documents how to move.

Assistant-model: Claude Opus 5.5 (fast)
Assistant-workflow: inline (fast subagents)
Assistant-verification: bun test (6733 pass), bun run types, bun run ci
Assistant-verification: PostgreSQL storage tests (121 pass)
Assistant-verification: e2e auth, machines, connector, build restart, planner picker, experiments, machine choice (57 pass)
Co-authored-by: Alex Lavaee <alexlavaee@microsoft.com>
@lavaman131 lavaman131 self-assigned this Oct 22, 2026
@lavaman131
lavaman131 merged commit 3649167 into main Oct 22, 2026
24 checks passed
@lavaman131
lavaman131 deleted the previous-origins branch October 22, 2026 22:57
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