Repository navigation
Description
Parent
Part of #154 (remote agent sessions). Phase 4. Design doc docs/designs/remote-workrooms.md: OQ20 (decided 2026-09-27, PR #248) and Phase 4 "The credential broker is a new component". Research: docs/designs/oq20-remote-git-credentials.md (PR #248).
The plan below was changed by an eng review, a CEO review, a design review and an eng review of the design (2026-09-27/28). Where the original sections differ, "Amended plan" at the end wins.
Where it lives: Codaset (decided 2026-09-27, owner)
The broker is built into Codaset. The code lands there under joelmoss/codaset#40, and this issue holds the specification and tracks it from the Workroom side.
- The stack fits. Codaset is Rails 8.1 on Postgres, with Redis and Sidekiq. Refresh tokens can use Active Record Encryption (
encrypts), and the App's client secret and private key can go in Rails credentials. - It is already deployed at
codaset.dev. Only/workroom,/upand a placeholder home page are live there; the rest of the app sits behindif ENV.key?('WIP')inconfig/routes.rb. - The broker's routes join the live ones outside the
WIPgate, and sit outside Codaset's session-cookieAuthenticationconcern: the Mac and the VMs authenticate by signing requests with their own keys, not with a browser session. - Workroom users become Codaset users (decided 2026-09-27, owner). Signing in with GitHub creates a Codaset
User(with its personal organisation), or links to an existing one by the GitHub account's verified primary email. An unverified email never links. The GitHub identity is stored on its own table, keyed by GitHub user ID.
What to build (as filed)
The server half of OQ20. The broker holds each user's GitHub access and mints short-lived, one-repository tokens for remote workrooms. The Mac and the remote VMs never hold a long-lived GitHub credential.
- One Workroom GitHub App, asking for repository permissions only, and the account permission to read email addresses. Leave "Expire user authorization tokens" on.
- Sign-in through the App's browser flow on
codaset.dev; the broker keeps the user token and refresh token, encrypted, and refreshes them server-side. The Mac registers the public half of a device key and signs every later request with it (client side: Credential broker clients: Mac sign-in, agent enrolment, and the git credential helper #251). - Enrolment codes bound to one workroom and one repository; a VM redeems one by registering its own key, bound to that workroom's ID.
- Minting installation tokens narrowed to one repository.
- The authorization check: the user must be able to push to the repository.
- The base clone token: a read-only installation token the Mac asks for while awake.
- Revocation: cancelling a grant stops minting; revoking a device key stops that Mac.
Amended plan (2026-09-28)
Accounts and sign-in
- Sign-in is a loopback redirect with PKCE (ephemeral
127.0.0.1port, 60 s single-use code), not the device flow. A new email creates a Codaset user; an existing account links only after an emailed code (a stale GitHub email must not take over an account); an account already linked to another GitHub user is refused. - The browser ends on a codaset.dev result page that says "Workroom is signed in as @login" only after the Mac confirms it stored its key: a server-side sign-in attempt (Solid Cache, 10 min, bound to a per-browser nonce cookie) moves pending → redeemed → completed, and the Mac sends a signed "complete" request after storing credentials. Nothing personal travels in URLs.
- Web sign-in (same GitHub flow, no loopback) creates a Codaset session for the codaset.dev pages below.
Signed requests
- DPoP-shaped ES256 JWT per request (
typ,jwk,htm,htu,iat,jti);iatwithin 60 s, astale_proof401 carriesDatefor one skew-corrected retry;jtikept 125 s in Solid Cache and the replay store fails closed.
Grants and tokens
- Everything is keyed by repository ID. Grant creation runs the App-authenticated installation lookup first (a user token can't see a private repo outside the install), then the user's push check by ID. Whether the App covers a repository is decided by a metadata-read token request with
repository_ids: [id](revoked at once); only GitHub's "not accessible to the parent installation" 422 means not covered. - Tokens carry
contents: writeandworkflows: write. - Re-check at every mint: a dead user token (401 on the current token, or
bad_refresh_token) suspends all of that user's grants until they sign in again;push: falsewithdraws the grant; a 404 checks coverage (App removed or repository deselected → suspended withapp_not_installed, recovers after reinstall; still covered → withdrawn); SAML 403, 5xx, 429 and timeouts keep the last answer. Orgs with IP allow lists get a typedip_allow_listerror and are unsupported for now. - User-token refresh is single-flight under a row lock; a suspension job carries a token generation and does nothing if the user has signed in since.
- Issued tokens are stored (encrypted) and revoked: cancel, withdrawal, suspension and Mac removal mark tokens for revocation in the same transaction; a job revokes them (204 or 401 counts as done); each broker request re-queues lost revocations. "Remove this Mac and cut off its workrooms" cancels every grant that Mac created.
- Base-clone token: installation lookup, then a user read check, then a read-only token (not stored; can outlive a Mac removal by up to 1 h).
Pages on codaset.dev (styles: Codaset's app style for account pages, the /workroom page style for the install page)
- Workroom access at
/workroom/account: signed-in Macs (remove: "Sign this Mac out" or "Sign out and cut off GitHub access", with live workroom counts; the workroom machines keep running), and "Check a repository" (ready / you requested this / not available, 30 checks an hour per user). /install/{owner}: public, static page for org admins, with GitHub's install link and a "Copy link" for writers.- Per-cause sign-in error pages, an account-link code page, and a signed-out entry page.
Operations
- Rate limits in Solid Cache; structured logs; an append-only
broker_eventsaudit table (90+ days); a runbook. No feature flag: rollback reverts the deploy, then runs a task from the pre-revert commit that revokes every live token.
Acceptance criteria
- A user signs in from the Mac (loopback + PKCE), and the broker refreshes their token across the 8-hour expiry with no client involved.
- A first GitHub sign-in creates a Codaset user with a personal organisation; an existing account links only after its emailed code; an account linked to another GitHub user is refused.
- The result page says "signed in" only after the Mac's confirmation; every sign-in failure has its own page and one next step.
- A VM that redeemed an enrolment code gets a token that can push to its one repository (including workflow files) and cannot reach any other.
- An enrolment code cannot be redeemed twice, or after it expires, or for a different workroom or repository.
- Grants and mints follow the repository ID through a rename; a new repository under the old name gets nothing.
- A user without push access is refused even where the App is installed; a missing install answers
app_not_installedwith the install page link. - Re-check outcomes follow the amended rules (suspend on a dead token, withdraw on no push, coverage check on 404, keep the last answer on transient failures).
- Cancelling a grant stops minting at once and revokes its unexpired tokens, even if the first revocation job is lost.
- Refresh tokens, issued tokens, the client secret and the private key are stored encrypted and never logged.
- A read-only base-clone token cannot push.
- The broker and its pages answer at
codaset.devwithWIPunset; the rest of the app stays behind it (route-set test).
Blocked by
None.
Filed by Claude Code at the owner's request, 2026-09-27. Hosting and accounts sections updated at the owner's request, 2026-09-27. Amended plan added by Claude Code after the 2026-09-27/28 reviews.
Activity
docs(remote-workrooms): record the #250 review outcomes in OQ20
docs(remote-workrooms): record the #250 review outcomes in OQ20
docs(remote-workrooms): record the #250 review outcomes in OQ20
joelmoss commentedon Oct 2, 2026
🤖 Left by Claude Code
Built in joelmoss/codaset#43 (merged 2026-10-01). Closing. The design doc's OQ20 records it as built.
docs(remote-workrooms): bring the status up to 2026-10-02