Skip to content

test(oauth): check OAuth scopes against a live server - #1128

Open
EhabY wants to merge 1 commit into
mainfrom
fix/oauth-scopes
Open

EhabY wants to merge 1 commit into
mainfrom
fix/oauth-scopes

Conversation

@EhabY

@EhabY EhabY commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Part of VSC-24. Stacked on #1138, which changes the scopes the extension requests. This PR checks them against a live server.

Why

Released servers ignore requested scopes, so unit tests can't show the extension requests enough. Missing scopes show up only against a server that enforces them, and sometimes only as an empty list or a closed socket. This suite runs every request the extension makes against such a server, using a real OAuth token.

Size

+1204/−47 in 15 files:

  • test/scopes/: +940 (probes, deployment setup, test, compose file).
  • test/unit/oauth/scopeProbes.test.ts: +141.
  • CI, tooling and docs: +81/−12 (workflow, Vitest project, --project !scopes exclusions, CONTRIBUTING.md, AGENTS.md).
  • src/oauth: +42/−35. hasRequiredScopes moves unchanged from a private method in sessionManager.ts to utils.ts, so the suite can check the granted scopes with it. constants.ts gains a pointer to scopeConsumers for why each scope is requested.

Tests

  • test/unit/oauth/scopeProbes.test.ts (every PR, ~2s): uses the TypeScript checker to find every CoderApi request or stream method that src/ references, including through destructuring, Pick<CoderApi, …> and structural interfaces. It fails if a method has no probe, or if a probe or exemption is stale.
  • test/scopes/ (pnpm test:scopes, ~85s):
    • Signs a member in through the extension's own OAuth code (discovery, dynamic client registration, scope negotiation, token exchange). It checks that the server granted exactly the requested scopes and that hasRequiredScopes accepts them.
    • Runs every probe with that token. coder start and the one-build update run through the extension's startWorkspace and updateWorkspace, using the CLI the server serves. coder start runs on an outdated workspace with automatic updates on, so the CLI dry-runs first.
    • scopeConsumers names, for each requested scope, a probe that fails without it and the error it fails with. Each of those probes is run with a token missing that scope, so a scope nothing needs can't be added unnoticed.
    • List probes require results, since the server silently drops rows the token can't read. Streams must deliver a non-ping message where the server authorizes per message.
    • Known gaps assert their exact error, so a server change shows up as a failure.
    • Setup refuses a deployment that already has users, and enables DCR, which is off by default from 2.38.
  • .github/workflows/oauth-scopes.yaml: runs when code the probes run or import changes, against a coder-preview image pinned by digest, plus nightly against latest. pnpm test and the CI unit job leave the suite out.
docker compose -f test/scopes/compose.yaml up -d --wait
CODER_SCOPES_TEST_URL=http://localhost:7080 pnpm test:scopes

Each scope #1138 requests, and a probe that fails without it:

Probe Without the scope Scope
getAuthenticatedUser GET /users/me: 404 user:read
getWorkspaceByOwnerAndName GET /users/member/workspace/own: 404 user:read
getWorkspaceByOwnerAndName (shared) GET /users/admin/workspace/shared: 404 composites (organization_member:read)
stopWorkspace POST …/builds: 403 You do not have permission to stop this workspace coder:workspaces.operate
startWorkspace POST …/builds: 400 Failed to fetch workspace owner user:read
coder start dry-run: 404 workspace:create
coder ssh coordinate GET …/coordinate: 404 coder:workspaces.access
watchInboxNotifications GET /notifications/inbox/watch: 403 inbox_notification:read

Known gaps


🤖 Generated with Claude Code

@linear-code

linear-code Bot commented Sep 29, 2026

Copy link
Copy Markdown

VSC-24

@EhabY

EhabY commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Chat: Review posted | View chat
Requested: 2026-10-06 12:50 UTC by @EhabY

Review history
  • R2 (2026-10-06): 12 reviewers, 11 Nit, 2 P1, 8 P2, 18 P3, 14 P4, REQUEST_CHANGES. Review
  • R3 (2026-10-06): 9 reviewers, 12 Nit, 2 P1, 8 P2, 19 P3, 24 P4, COMMENT. Review

deep-review v0.13.0 | Round 3 | c9eff9e..d0009b8

Last posted: Round 3, 65 findings (2 P1, 8 P2, 19 P3, 24 P4, 12 Nit), COMMENT. Review

Finding inventory

Finding inventory: PR #1128

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P2 Author fixed (427ac73) test/scopes/probes.ts:102 postWorkspaceBuild exemption is wrong: chained on_success update build (2.36+) is unprobed R1 Netero Yes
CRF-2 P2 Author fixed (427ac73) test/scopes/probes.ts:39 getWorkspaces probe never runs the shared_with_user filter query R1 Netero Yes
CRF-3 Nit Author fixed (427ac73) test/scopes/oauthScopes.test.ts:1 New test/scopes/ dir missing from AGENTS.md test layout R1 Netero Yes
CRF-4 P1 Author fixed (427ac73) test/scopes/probes.ts:62 coder start on a shared workspace still fails with the new scopes; REST probe reports it fixed R1 Pariston Yes
CRF-5 P1 Author fixed (427ac73) test/scopes/oauthScopes.test.ts:9 Suite reads CODER_URL, set in every Coder workspace; pnpm test hits the host deployment R1 Knov P1, Hisoka P2, Bisky P4 Yes
CRF-6 P2 Author fixed (427ac73) test/scopes/probes.ts:89 No probe for /coordinate (SSH) or usage POST; suite green without coder:workspaces.access R1 Kite, Mafuuu, Bisky, Mafu-san, Knov P2, Hisoka P3 Yes
CRF-7 P3 Author fixed (d0009b8) test/scopes/oauthScopes.test.ts:15 Suite proves scopes sufficient, never that each is needed (no leave-one-out) R1 Bisky Yes
CRF-8 P2 Author contested; panel closed R2 (2/2 accept) src/oauth/constants.ts:14 No probe needs user:read_personal R1 Mafu-san P2, Bisky P3 Yes
CRF-9 P2 Author fixed (427ac73) src/oauth/constants.ts:6 Scope change invalidates every stored OAuth session on all server versions; description says nothing changes R1 Mafuuu, Mafu-san P2, Pariston Note Yes
CRF-10 P3 Author fixed (427ac73) CHANGELOG.md:7 No CHANGELOG entry for wider consent and forced re-login R1 Leorio Yes
CRF-11 P2 Author fixed (427ac73) test/scopes/probes.ts:180 receives() resolves on watch-ws ping, so watchWorkspace passes when updates are auth errors R1 Komugi Yes
CRF-12 P3 Author fixed (427ac73) test/scopes/compose.yaml:15 PR and push runs use coder-preview:latest; results depend on the day R1 Komugi Yes
CRF-13 P3 Author fixed (427ac73) test/scopes/compose.yaml:16 Port published on all interfaces with committed owner password R1 Kurapika, Hisoka, Knov Yes
CRF-14 P4 Author fixed (427ac73) test/scopes/deployment.ts:63 Doc says never writes to a real server; guard only rejects servers with users R1 Kurapika Yes
CRF-15 P2 Author fixed (427ac73) test/scopes/probes.ts:80 Description says inbox gap is fixed by coder/coder#30176; PR is open and extension would still need the scope R1 Mafu-san P2, Leorio P4 Yes
CRF-16 P3 Author fixed (427ac73) test/scopes/probes.ts:78 knownGaps instruction to move probe is wrong for inbox after server fix R1 Leorio Yes
CRF-17 P3 Author fixed (427ac73) test/scopes/probes.ts:219 Stream failure message drops status and can be empty R1 Leorio Yes
CRF-18 P3 Author fixed (427ac73) src/oauth/constants.ts:7 Comment does not say composites replace low-level scopes or why R1 Leorio Yes
CRF-19 P4 Author fixed (427ac73) test/scopes/deployment.ts:71 Fresh-deployment error does not say how to reset R1 Leorio Yes
CRF-20 P4 Author fixed (427ac73) test/scopes/probes.ts:122 "Expected at least one item" omits likely cause R1 Leorio Yes
CRF-21 P4 Author fixed (427ac73) test/scopes/probes.ts:148 Restore error in finally replaces the scoped build's error R1 Mafuuu Yes
CRF-22 P3 Author fixed (427ac73) test/scopes/deployment.ts:102 Suite uses createToken, not an OAuth grant; OAuth scope path untested R1 Bisky, Hisoka Yes
CRF-23 P3 Author fixed (427ac73) test/unit/oauth/scopeProbes.test.ts:37 Metatest misses methods reached by destructuring R1 Hisoka Yes
CRF-24 P3 Author contested; panel closed R2 (1/1 accept) src/oauth/constants.ts:11 workspace:create needed only for coder start dry-run; REST start could avoid it R1 Knov Yes
CRF-25 P3 Author fixed (427ac73) test/scopes/probes.ts:30 "One probe per method" and CONTRIBUTING "every method" are false R1 Gon P2/P3 Yes
CRF-26 Nit Author fixed (427ac73) test/scopes/probes.ts:199 openStream returns a URL, not a stream R1 Gon P3 Yes
CRF-27 Nit Author fixed (427ac73) test/scopes/probes.ts:98 TASKS_API value is a label, not a reason R1 Gon P3 Yes
CRF-28 Nit Author fixed (427ac73) test/scopes/probes.ts:116 Seven doc comments restate the code below them R1 Gon P2 Yes
CRF-29 Nit Author fixed (427ac73) test/scopes/probes.ts:120 nonEmpty reads as predicate but throws R1 Gon Yes
CRF-30 Nit Author fixed (427ac73) test/scopes/deployment.ts:128 waitForBuild helper shares name with CoderApi.waitForBuild R1 Gon Yes
CRF-31 Nit Author fixed (427ac73) test/scopes/deployment.ts:209 Tar block size 512 repeated; offsets unlabeled R1 Gon P4 Yes
CRF-32 Nit Author fixed (427ac73) test/unit/oauth/scopeProbes.test.ts:103 Unnamed 60_000 hook timeout R1 Gon P4 Yes
CRF-33 P4 Author fixed (427ac73) test/scopes/deployment.ts:187 Use Api.uploadFile instead of raw files POST R1 Ging-TS Yes
CRF-34 P4 Author fixed (427ac73) test/unit/oauth/scopeProbes.test.ts:2 TS6 compiler API use not recorded in pnpm-workspace pin comment R1 Ging-TS Note Yes
CRF-35 OOS Out of scope (body); tracked in coder/coder#30423 coderd GET /organizations/{org}/members/{user} Returns 404 for scoped tokens even for own membership R1 Pariston No
CRF-36 OOS Out of scope (body); author fixed in PR (427ac73) src/oauth/sessionManager.ts:482 revokeTokens skips revocation when scope check fails R1 Pariston, Mafuuu No
CRF-37 OOS Out of scope (body); author fixed in PR (427ac73) src/oauth/sessionManager.ts:102 getStoredTokens doc says invalid tokens are cleared; they are not R1 Mafuuu, Leorio, Mafu-san No
CRF-38 OOS Out of scope (body); author fixed in PR (427ac73) src/oauth/sessionManager.ts:264 coder:all does not satisfy hasRequiredScopes R1 Kite No
CRF-39 OOS Out of scope (body); author fixed in PR (427ac73) src/inbox.ts:44 Inbox socket closes and reconnects on each notification on scope-enforcing servers R1 Mafuuu No
CRF-40 OOS Out of scope (body) coderd /users/me, scopes_catalog.go user:read is deployment-wide; organization_member:read not requestable R1 Kurapika No
CRF-41 OOS Out of scope (body) coderd templateVersionExternalAuth coder start on templates with external auth needs user:read_personal, untested anywhere R1 Mafu-san No
CRF-42 Note Note (body) src/oauth/constants.ts:8 Composite contents are server-defined; widening is undetected by the suite R1 Kurapika No
CRF-43 Note Dropped by orchestrator (verification only, no action) test/scopes/deployment.ts:144 waitForJob ordering is correct R1 Komugi No
CRF-44 P3 Author contested; panel closed R3 (1/1 accept); code now in #1138 src/oauth/utils.ts:62 Sessions from before the server offered inbox_notification:read stay valid without it; inbox keeps failing R2 Netero Yes
CRF-45 P4 Author fixed (d0009b8) .github/workflows/oauth-scopes.yaml:10 paths filter omits files the probes import (featureSet, core, util, package.json, lockfile, vitest config) R2 Netero Yes
CRF-46 P2 Author fixed (d0009b8) test/scopes/probes.ts:236 CLI probes inherit CODER_SESSION_TOKEN, which overrides the OAuth session file; coder start probes 401 in Coder workspaces R2 Mafu-san P2, Komugi P2, Bisky P3 Yes
CRF-47 P3 Author fixed (d0009b8) test/scopes/probes.ts:98 Usage POST needs workspace update (not read, as the CRF-6 reply says) and has no probe R2 Razor P3, Mafuuu P4 Yes
CRF-48 P3 Author fixed (c9eff9e, moved to #1138; partial per author) src/oauth/utils.ts:101 Forced re-login on scope-ignoring servers is avoidable: stored scope there is the request list, grant is coder:all R2 Pariston Yes
CRF-49 P3 Author contested; panel closed R3 (1/1 accept) test/scopes/deployment.ts:186 awaitFollowUpBuild hangs to the test timeout when no chained build is queued; workspace not restored R2 Hisoka Yes
CRF-50 P3 Author fixed (d0009b8) test/scopes/probes.ts:99 Real-CLI coder start probe never takes the dry-run path; workspace:create still backed only by hand-written POST R2 Knov Yes
CRF-51 P4 Author contested; panel closed R3 (2/2 accept) test/scopes/oauthScopes.test.ts:26 Grant test requires optional scope, so the suite fails on servers that do not offer it (release/2.38) R2 Hisoka P4, Leorio P4 Yes
CRF-52 P4 Author fixed (d0009b8) test/scopes/probes.ts:313 receives starts trigger before the handshake completes; inbox notification can be missed R2 Komugi Yes
CRF-53 P4 Author fixed (d0009b8) test/scopes/probes.ts:339 "Server refused the stream" prefix used when the server was never reached R2 Leorio Yes
CRF-54 P4 Author fixed (d0009b8) test/scopes/probes.ts:267 Refused /coordinate reports only route and status, no method or server reason R2 Leorio Yes
CRF-55 P4 Author fixed (d0009b8) test/scopes/probes.ts:110 knownGaps doc assumes a scope will exist and omits cliProbes as destination R2 Leorio Yes
CRF-56 P4 Author fixed (d0009b8) test/unit/oauth/scopeProbes.test.ts:128 Metatest failure message omits unprobedMethods option and running test:scopes R2 Leorio Yes
CRF-57 P4 Author fixed (c9eff9e, moved to #1138; DCR part tracked in #1140) CHANGELOG.md:20 CHANGELOG does not name the inbox scope and claims OAuth works where DCR is off by default (2.38); description Follow-up untracked R2 Leorio Yes
CRF-59 P3 Author fixed (c9eff9e, moved to #1138) src/oauth/sessionManager.ts:103 getStoredTokens doc describes readStoredTokens behavior; readStoredTokens doc only points back R2 Gon P2 Yes
CRF-60 P3 Author fixed (c9eff9e, moved to #1138) src/oauth/constants.ts:5 DEFAULT_OAUTH_SCOPES doc omits that adding a scope signs every OAuth user out R2 Gon P2 Yes
CRF-61 Nit Author fixed (d0009b8, c9eff9e) test/scopes/probes.ts:146 Thirteen comments restate code or another comment, three of them CRF-28 leftovers R2 Gon P2 Yes
CRF-62 Nit Author fixed (c9eff9e, moved to #1138) src/oauth/sessionManager.ts:120 readStoredTokens vs getStoredTokens names do not say which checks scopes R2 Gon Yes
CRF-63 Nit Author fixed (d0009b8) test/scopes/deployment.ts:54 Deployment.cli holds a path; rename cliPath R2 Gon Yes
CRF-64 P3 Dropped by orchestrator (disproved: coderd tokens.go:302-341 deletes the previous same-app key on code exchange, cascading to its refresh token) src/oauth/sessionManager.ts:458 Forced re-login overwrites pre-upgrade tokens without revoking them R2 Kurapika No
CRF-65 OOS Out of scope (body); tracked in #1140 src/api/workspace.ts:82 runCliCommand spawns the CLI with the full host env; CODER_SESSION_TOKEN overrides the stored session R2 Bisky, Mafu-san, Komugi No
CRF-66 OOS Out of scope (body); tracked in #1140 PR description / OAuth discovery DCR off by default on 2.38: extension offers OAuth and sign-in fails at registration R2 Mafu-san No
CRF-67 OOS Out of scope (body); author fixed in #1138 src/oauth/authorizer.ts:216 "Server may still accept them" is wrong for scope-enforcing servers R2 Razor No
CRF-68 OOS Out of scope (body); tracked in coder/coder#30423 coderd workspaces.go:1801 Shared use-role members lack workspace update, so usage posts on shared workspaces fail R2 Razor No
CRF-69 OOS Out of scope (body) src/oauth/constants.ts:6 DEFAULT_OAUTH_SCOPES name reads as overridable; it is the required list R2 Gon No
CRF-70 OOS Out of scope (body); author fixed in #1138 src/oauth/sessionManager.ts:44 StoredTokens doc says used by getStoredTokens; readStoredTokens builds it now R2 Gon No
CRF-71 P4 Open .github/workflows/oauth-scopes.yaml:8 CRF-45 fix incomplete: paths filter still omits src/headers.ts, src/logging, src/error, test/mocks and others the probes import R3 Netero Yes
CRF-72 P4 Open test/scopes/oauthScopes.test.ts:73 Leave-one-out test passes on any error; consumer errors are not pinned R3 Netero Yes
CRF-73 P4 Open test/scopes/oauthScopes.test.ts:34 Grant test accepts coder:all or wildcards, so a server granting everything passes R3 Pariston Yes
CRF-74 P4 Open test/scopes/probes.ts:116 knownGaps lost the changed-error instruction; gap and description do not link coder/coder#30423 R3 Leorio Yes
CRF-75 P4 Open test/scopes/oauthScopes.test.ts:57 "names a consumer" failure does not say to edit scopeConsumers R3 Leorio Yes
CRF-76 P4 Open test/scopes/deployment.ts:232 Shortened awaitJobSuccess doc ("even if it already had") lost its meaning R3 Leorio Yes
CRF-77 P4 Open test/scopes/deployment.ts:372 tarFile returns Buffer, forcing an unexplained Uint8Array copy; use Buffer R3 Ging-TS Yes
CRF-78 P3 Open test/scopes/deployment.ts:171 promoteNewTemplateVersion outdates every workspace on the template; versionId doc is false afterwards R3 Gon P2/P3 Yes
CRF-79 P4 Open test/scopes/deployment.ts:65 TEMPLATE comment says only the coder provider; terraform_data is also used, reason undocumented R3 Gon P3 Yes
CRF-80 P4 Open test/scopes/deployment.ts:168 withScopes Deployment keeps OAuth grantedScope and token doc says OAuth token R3 Gon Yes
CRF-81 Nit Open test/scopes/probes.ts:131 scopeConsumers.probe is a name; knownGaps.probe is a function R3 Gon Yes
CRF-82 P4 Open test/scopes/probes.ts:100 cliProbes doc omits netcheck and support bundle, which the extension also runs R3 Pariston Note, Kite Note Yes
CRF-83 Nit Dropped by orchestrator (reverses the CRF-26 rename the review asked for; doc states the side effect) test/scopes/probes.ts:351 Rename streamUrl to openAndClose R3 Gon No
CRF-84 OOS Out of scope (body) src/websocket/reconnectingWebSocket.ts:379 connect adds its message listener after open, dropping a message sent right after the handshake R3 Mafuuu No

Contested and acknowledged

CRF-7 (P2, test/scopes/oauthScopes.test.ts:15) - no leave-one-out test

  • Finding: Suite proves sufficiency, not necessity; proposed a leave-one-out test per scope.
  • Author defense: Ran it once; every scope except user:read_personal fails some probe when dropped. Not kept because it quadrupled runtime.
  • Panel re-raised (R2): Bisky, the agent that raised it, re-raised at P3: the check needs one consumer probe per scope, not full reruns. Measured consumer probes total about 4 s. Proposed a scopeConsumers map (probe name or reason per scope) plus one test per scope and an assertion that the keys equal DEFAULT_OAUTH_SCOPES.

CRF-8 (P2, src/oauth/constants.ts:14) - no probe needs user:read_personal

  • Finding: Add a probe that fails without the scope, or drop it.
  • Author defense: coder start reads external auth links with ActionReadPersonal once a link exists; the test deployment cannot create one. Comment and description now say so.
  • Panel closure (R2, 2/2): Mafu-san and Bisky, the agents that raised it, accepted. Both traced coder/coder templateVersionExternalAuthForUser: dbauthz authorizes only after a link row is fetched, so the scope matters only for linked providers, which need an external IdP the compose deployment lacks.

CRF-24 (P3, src/oauth/constants.ts:11) - workspace:create only for the dry-run

  • Finding: REST start pinning the active version would let the list drop workspace:create.
  • Author defense: Keeps the CLI path because that is how the extension starts workspaces; the comment names the dry-run.
  • Panel closure (R2, 1/1): Knov, the agent that raised it, accepted: the token already carries SSH (code execution in every reachable workspace), so create adds only resource use; the CHANGELOG tells users the consent screen asks to create workspaces.

CRF-44 (P3, src/oauth/utils.ts:62) - pre-upgrade sessions lack the optional inbox scope

  • Finding: Sessions created before the server offered inbox_notification:read pass hasRequiredScopes without it, so inbox keeps failing until the next sign-in.
  • Author defense: Declined. Those sessions gain the scope at their next sign-in, as the OPTIONAL_OAUTH_SCOPES doc now says; detecting it needs a metadata fetch on every session check. Code moved to fix(oauth): request the scopes the extension and CLI actually need #1138.
  • Panel closure (R3, 1/1): Netero, the agent that raised it, accepted for this PR: inbox is best-effort and the c9eff9e doc states the behavior. Netero noted the "metadata fetch on every check" part is inaccurate, since login already has scopes_supported and could record the offered optional scopes; that choice belongs to fix(oauth): request the scopes the extension and CLI actually need #1138.

CRF-49 (P3, test/scopes/deployment.ts:186) - awaitFollowUpBuild can hang

  • Finding: watch-ws never closes on its own, so if no chained start is queued the loop waits to the 120 s timeout and the workspace is not restored.
  • Author defense: Declined. The server authorizes the follow-up start when it queues it, so a refusal fails the POST with 403 instead of hanging; reading the workspace once would race the asynchronous orchestrator.
  • Panel closure (R3, 1/1): Hisoka, the agent that raised it, accepted after tracing coderd: postWorkspaceBuild inserts the orchestration row in the build transaction and returns 403 on an authorization failure, and the orchestrator builds the child as system, so scopes cannot reach the hang path. A remaining non-scope failure (invalid parameters, dormant workspace) would still hang to the timeout.

CRF-51 (P4, test/scopes/oauthScopes.test.ts:26) - grant test requires the optional scope

  • Finding: The suite fails on servers that do not offer inbox_notification:read (release/2.38) even though the extension correctly omits it.
  • Author defense: Declined. The suite targets servers that offer the scope; on 2.38 the failure is accurate because inbox cannot work there.
  • Panel closure (R3, 2/2): Hisoka and Leorio, the agents that raised it, accepted. On 2.38 every failure names inbox_notification:read, the suite does not claim to support such images (CONTRIBUTING.md:258), and the not-offered path is unit-tested in authorizer.test.ts.

Law analysis

  • Round 2. Effective LOC: +1178 -70 (20 files). Head: 427ac73.
  • Verdict: Split (horizontal). Enforcement: Mandatory.
  • PR 1: fix(oauth): request the scopes the extension and CLI actually need: all src/oauth changes (scope list, optional scope negotiation, revocation without scope check, coder:all and hasRequiredScopes move), their unit tests, CHANGELOG. About 126 production / 73 test lines.
  • PR 2: test(oauth): check OAuth scopes against a live server: test/scopes/, scopeProbes.test.ts, vitest project, package.json and test-electron.sh exclusions, workflow, pnpm-workspace comment, CONTRIBUTING, AGENTS. About 980 lines. Merges after PR 1.
  • Constraint: revocation change must not merge after the scope list change.

Round log

Round 1

Full panel (round 1). Netero: 2 P2, 1 Nit, no P0, so panel proceeded. Law not run (729 effective additions). Panel: ging-ts, kurapika (auth, OAuth tokens), pariston, mafuuu, bisky, komugi, hisoka, gon, leorio, mafu-san, knov, wildcard kite. Multi-domain (auth scopes plus CI and test infra), size 12. Result: 2 P1, 9 P2, 11 P3, 6 P4, 8 Nit; 7 out of scope; 1 body note. Gon's comment-restatement P2s consolidated into one Nit (CRF-28): keep-argument was that redundant comments drift, but they are accurate today and cause no behavior risk. Gon naming P3s downgraded to Nit per the Nit definition. CRF-25 downgraded from P2 to P3: keep-argument was that it misleads contributors, but the metatest enforces the real contract. Reviewed against 7ff5019..329435a.

Round 2

Churn guard PROCEED: 31 addressed, 3 contested (CRF-7, CRF-8, CRF-24). Classified as a restructure round (files added to the PR; new production code in authorizer.ts, sessionManager.ts, utils.ts). Law ran (1178 effective additions, never run before): Split, Mandatory. Per the Law decision gate the panel was skipped, so the contested findings and the round-2 auth code were not panel-reviewed. Netero: 1 P3, 1 P4 new, both verified against the code; Netero also verified fixes for CRF-1, CRF-2, CRF-3, CRF-23 and CRF-36/37/38 with mutation checks. Round 1 post receipt was written by hand after the CLI got a 502 from GitHub although the review was created (review 5391464079). Reviewed against f852fdd..427ac73.
The Law gate says to skip the panel, but deep-review post rejected both REQUEST_CHANGES ("requires panel review") and COMMENT ("panel reviewers required" after round 1), so a full restructure panel ran: ging-ts, kurapika, pariston, mafuuu, bisky, gon, leorio, hisoka, komugi, wildcard razor (size 10), plus dispute check-in agents mafu-san and knov (not counted). Bisky, Mafu-san and Knov reused their round 1 agents. Panel result: 1 P2, 7 P3, 7 P4, 3 Nit new; CRF-7 re-raised at P3; CRF-8 and CRF-24 closed. Kurapika's P3 (CRF-64) was dropped after checking coderd tokens.go. Gon's 13 comment-restatement P2s were consolidated into CRF-61 as in round 1. Gon's doc P2s CRF-59 and CRF-60 were downgraded to P3: the keep-argument was that a misplaced doc could lead to swapping readStoredTokens back, but a unit test now fails on that swap.

Round 3

PR split after round 2: #1128 now holds only the live suite, metatest, CI and docs, stacked on #1138 (c9eff9e). Churn guard PROCEED: 17 addressed, 3 contested. Restructure round (goal changed, src/oauth files left the diff). Law not rerun (effective additions 1123, down from 1178). Netero (reused agent): 2 P4 new; accepted the CRF-44 defense (1/1), which is now in #1138. Full panel: ging-ts, pariston, mafuuu, bisky, gon, leorio, komugi, wildcard kite (size 8), plus hisoka for the disputes. Hisoka, Leorio and Bisky reused their prior agents. CRF-49 closed 1/1, CRF-51 closed 2/2, CRF-7 fix verified live by Bisky. Several reviewers ran the live suite: 39/39 passed. New: 1 P3, 9 P4, 1 Nit; CRF-83 dropped; 1 out of scope. Gon's P2 on the promoteNewTemplateVersion doc was merged with the versionId doc P3 into CRF-78 at P3: the keep-argument was that the hidden side effect can mislead a later probe, but no current probe relies on the stale value (Hisoka checked). Gon's TEMPLATE comment P3 was downgraded to P4 (CRF-79): the keep-argument was that the agent attachment is load-bearing, but Gon's reason for it is unverified. Reviewed against c9eff9e..d0009b8.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR replaces the OAuth scope list with the two coder:workspaces.* composites plus workspace:create, user:read and user:read_personal, and adds a live-server scope suite and a source-scan metatest. Findings: 2 P1, 9 P2, 11 P3, 6 P4, 8 Nit.

Out of scope (needs a ticket or explicit acceptance by a human):

  • coder/coder GET /organizations/{org}/members/{user}: returns 404 for scoped tokens even for the caller's own membership (500 with organization:read), so coder start and coder update on another user's workspace fail for every scoped token.
  • src/oauth/sessionManager.ts:482: revokeTokens reads tokens through the scope check, so logging out with a pre-upgrade session leaves its refresh token valid on the server. This PR makes every stored session hit that path.
  • src/oauth/sessionManager.ts:102: the getStoredTokens doc says invalid tokens are cleared; the function returns undefined and leaves them in storage.
  • src/oauth/sessionManager.ts:264: hasRequiredScopes accepts coder:* as a wildcard but not coder:all, so a session granted coder:all is rejected.
  • src/inbox.ts:44: on scope-enforcing servers the inbox socket closes on the first notification and reconnects, so out-of-disk and out-of-memory notifications are never shown (server side: coder/coder#30176).
  • coder/coder /users/me and scopes_catalog.go: reading your own user needs user:read, which reads every visible user, and organization_member:read is only grantable through composites.
  • coder/coder templateVersionExternalAuth: coder start on a template with external auth reads external auth links, which need user:read_personal; no scoped-token test in either repository covers it.

Notes:

  • src/oauth/constants.ts:8: the server defines what the two composites contain, so the extension's grant widens whenever the server adds actions to them, and the suite only detects missing permissions.

CHANGELOG.md:7

P3 [CRF-10] The PR widens the consent screen (workspace:create, user:read) and forces existing OAuth users to sign in again, but adds no CHANGELOG entry. (Leorio)

Recent fix PRs (#1129, #1115, #1110, #1092) each add one. Add an Unreleased "Fixed" entry that names both user-visible effects.

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread test/scopes/probes.ts
Comment thread test/scopes/oauthScopes.test.ts Outdated
Comment thread test/scopes/probes.ts
Comment thread test/scopes/oauthScopes.test.ts Outdated
Comment thread src/oauth/constants.ts
Comment thread test/scopes/deployment.ts Outdated
Comment thread test/unit/oauth/scopeProbes.test.ts
Comment thread test/scopes/probes.ts Outdated
Comment thread test/scopes/probes.ts
Comment thread test/scopes/oauthScopes.test.ts
@EhabY
EhabY force-pushed the fix/oauth-scopes branch 4 times, most recently from d5e8ad0 to 427ac73 Compare October 6, 2026 11:29
@EhabY

EhabY commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

CRF-10: added an Unreleased "Fixed" entry covering the wider consent screen and the one-time re-authentication.

Out of scope, fixed here anyway: logout now revokes tokens with outdated scopes, coder:all is accepted, and the getStoredTokens doc is corrected. The inbox gap is closed by requesting inbox_notification:read when the server offers it. Still open: the shared coder start owner lookup (server side, tracked as a known gap).

@EhabY

EhabY commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit addresses 31 of the 34 round 1 findings, and it also fixes revocation for outdated sessions, accepts coder:all, and requests inbox_notification:read when the server offers it. This round: the request to split the PR, 1 P2, 8 P3, 9 P4 and 3 Nit, of which 1 P3 re-raises a disputed finding. Of the three disputed findings, the user:read_personal and workspace:create defenses are accepted, and the leave-one-out check is re-raised at P3 in its thread.

Please split the PR. The diff is now +1178 -70 across 20 files, and most of the roughly 126 lines of new src/oauth code arrived this round beside about 500 lines of harness rewrite:

  1. fix(oauth): request the scopes the extension and CLI actually need: every src/oauth change (scope list, OPTIONAL_OAUTH_SCOPES, revocation through readStoredTokens, coder:all and the hasRequiredScopes move) with its unit tests and the CHANGELOG lines.
  2. test(oauth): check OAuth scopes against a live server: test/scopes/, scopeProbes.test.ts, the vitest project, the --project !scopes exclusions, the workflow, the pnpm-workspace.yaml comment, CONTRIBUTING.md and AGENTS.md. It imports PR 1's exports and merges after it.
    The revocation change must not merge after the scope list change, or logging out with a pre-upgrade session skips /oauth2/revoke.

Out of scope (needs a ticket or explicit acceptance by a human):

  • coder/coder GET /organizations/{org}/members/{user}: returns 404 for scoped tokens, so coder start on another user's workspace fails for every scoped token (tracked here as the coder start (shared) known gap).
  • src/api/workspace.ts:82: runCliCommand spawns the CLI with the extension host's full environment, so a CODER_SESSION_TOKEN there overrides the stored session, and coder start/coder update fail with 401 or run as another user.
  • OAuth on 2.38 with dynamic client registration off (the default): discovery still succeeds, so the extension offers OAuth and sign-in then fails at registration. The PR description calls this a follow-up without a ticket.
  • src/oauth/authorizer.ts:216: the warning "Server may still accept them" is wrong on scope-enforcing servers, which reject the whole authorize request over an unknown name.
  • coder/coder coderd/workspaces.go:1801: a member a workspace is shared with under the use role lacks workspace update, so coder ssh usage posts on shared workspaces fail whatever the token's scopes.
  • src/oauth/constants.ts:6: DEFAULT_OAUTH_SCOPES is the required list that hasRequiredScopes enforces, but "default" reads as overridable.
  • src/oauth/sessionManager.ts:44: the StoredTokens doc says it is used by getStoredTokens; readStoredTokens builds it now.

src/oauth/utils.ts:101

P3 [CRF-48] The forced sign-in on servers that ignore scopes is avoidable: there the stored scope is the extension's own request list, while the server granted coder:all. (Pariston)

coder/coder release/2.37 records every authorization code as coder:all (authorize.go:278) and returns no scope, so this line stores the old DEFAULT_OAUTH_SCOPES, which the new hasRequiredScopes rejects (verified), and the user signs in again to get a token with the same permissions. A stored session with the old list cannot come from a scope-enforcing server, because the old extension's login fails at GET /users/me there, and release/2.38 returns scope on every exchange and refresh. Store a missing server scope as unrestricted instead of the request list, and accept the exact legacy list in getStoredTokens, so neither this change nor later edits to DEFAULT_OAUTH_SCOPES sign out every user of a server that ignores scopes.

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread test/scopes/probes.ts
Comment thread test/scopes/deployment.ts
Comment thread test/scopes/probes.ts Outdated
Comment thread test/scopes/oauthScopes.test.ts Outdated
Comment thread test/scopes/probes.ts Outdated
Comment thread test/scopes/probes.ts Outdated
Comment thread src/oauth/sessionManager.ts Outdated
Comment thread test/scopes/deployment.ts Outdated
Comment thread src/oauth/utils.ts
Comment thread .github/workflows/oauth-scopes.yaml Outdated
@EhabY
EhabY force-pushed the fix/oauth-scopes branch from 427ac73 to d0009b8 Compare October 6, 2026 12:33
@EhabY EhabY changed the title fix(oauth): request the scopes the extension and CLI actually need test(oauth): check OAuth scopes against a live server Oct 6, 2026
@EhabY
EhabY changed the base branch from main to fix/oauth-request-scopes October 6, 2026 12:33
@EhabY

EhabY commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

Split as suggested. #1138 has every src/oauth change, with its unit tests and the CHANGELOG. This PR is stacked on it and holds only the suite.

CRF-48: partly done. A missing server scope is now stored as coder:all, so later scope edits do not sign out users of servers that ignore scopes. Sessions stored with the old list still sign in again once, which the CHANGELOG states.

Out of scope: fixed the StoredTokens doc and the "Server may still accept them" warning (#1138). The runCliCommand environment, the DCR default on 2.38, and the use role lacking update are follow-ups.

@EhabY

EhabY commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-ups filed:

@EhabY

EhabY commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the split, this PR holds only the live scope suite, the metatest, CI and docs, stacked on #1138. Every round 2 finding is addressed or settled. The three disputed ones are accepted by the reviewers who raised them: the hang in awaitFollowUpBuild, the grant test on servers without the optional scope, and pre-upgrade sessions lacking the inbox scope (now in #1138). Reviewers ran the suite against the pinned image several times, and all 39 tests passed each time. New this round: 1 P3, 9 P4 and 1 Nit, none of which changes what the suite checks except the two leave-one-out and grant-test assertions.

Out of scope (needs a ticket or explicit acceptance by a human):

  • src/websocket/reconnectingWebSocket.ts:379: connect adds its message listener only after await this.#socketFactory() resolves, so a message the server sends right after the handshake is dropped (20 of 20 in a ws reproduction). The agent metadata watcher may then show nothing until the next metadata message after each connect.

🤖 This review was automatically generated with Coder Agents.

Comment thread .github/workflows/oauth-scopes.yaml
Comment thread test/scopes/oauthScopes.test.ts Outdated
Comment thread test/scopes/oauthScopes.test.ts Outdated
Comment thread test/scopes/probes.ts Outdated
Comment thread test/scopes/oauthScopes.test.ts Outdated
Comment thread test/scopes/deployment.ts Outdated
Comment thread test/scopes/deployment.ts Outdated
Comment thread test/scopes/deployment.ts Outdated
Comment thread test/scopes/probes.ts Outdated
Comment thread test/scopes/probes.ts Outdated
@EhabY
EhabY force-pushed the fix/oauth-scopes branch 2 times, most recently from 37af2eb to e3334c4 Compare October 7, 2026 08:45
@EhabY

EhabY commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Out of scope (round 3): filed #1141 for the dropped first message. A plain ws client that listens only after an awaited open drops it 20 of 20 times.

@EhabY
EhabY force-pushed the fix/oauth-scopes branch from e3334c4 to 6e5a414 Compare October 7, 2026 11:26
@EhabY
EhabY force-pushed the fix/oauth-scopes branch from 6e5a414 to ed8e384 Compare October 8, 2026 18:30
Base automatically changed from fix/oauth-request-scopes to main October 9, 2026 20:55
Add `pnpm test:scopes`, which signs in through the extension's OAuth flow
against a throwaway deployment (test/scopes/compose.yaml) and runs a probe
for every Coder API method and CLI request the extension makes with the
granted token. It also checks that each requested scope is needed by a
named probe, and asserts the exact error of known gaps.

`scopeProbes.test.ts` runs on every PR and uses the TypeScript checker to
find every CoderApi method src/ references, failing when one has no probe.

The workflow runs the suite on relevant changes against a pinned
coder-preview image, and nightly against `latest`. `pnpm test` and the CI
unit job leave it out.
@EhabY
EhabY force-pushed the fix/oauth-scopes branch from ed8e384 to c92c0ec Compare October 9, 2026 20:56

This branch has not been deployed

No deployments
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.

Shared workspaces: scoped tokens can't run coder start, and use-role users can't report usage

1 participant