Skip to content

[codex] Structure cloud CLI token errors#3249

Merged
juliusmarminge merged 2 commits into
codex/server-auth-error-boundariesfrom
codex/cloud-cli-token-errors
Jun 20, 2026
Merged

[codex] Structure cloud CLI token errors#3249
juliusmarminge merged 2 commits into
codex/server-auth-error-boundariesfrom
codex/cloud-cli-token-errors

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary

  • replace the higher-order CLI token error wrapper with direct, contextual mappings
  • retain original causes while recording credential, stage, endpoint, callback, and timeout context
  • narrow each token-manager service method to its actual error channel and simplify the cloud boundary catch set
  • add backend tests for removal, read, and persisted-token decode failures

Validation

  • vp test apps/server/src/cloud (33 tests)
  • vp check (passes with 20 pre-existing warnings)
  • vp run typecheck

Stacked on #3246 so the cloud HTTP error boundary lands first.


Note

Medium Risk
Touches OAuth token refresh/login and credential secret-store I/O; error tags and HTTP boundary handling changed, though behavior is mostly classification and safer logging rather than new auth logic.

Overview
Cloud CLI token manager errors are no longer wrapped by a generic wrapError helper. Each OAuth/credential step maps failures to tagged errors with stage, secretName, preserved cause, and redacted URL diagnostics (length, protocol, hostname only—no raw token/callback URLs in messages or JSON).

CloudCliTokenManager service methods now expose narrower error channels (clear → removal only, hasCredential → read, getExisting → refresh, get → refresh/authorization/timeout). Login and refresh paths tag failures at stages like load-oauth-config, prepare-pkce, start-callback-server, and exchange-token.

Cloud HTTP reconcileDesiredCloudLink drops several CLI error catchTag handlers; it only maps CloudCliCredentialRefreshError from getExisting (other CLI errors are no longer in that reconcile path’s catch set).

Adds CliTokenManager.test.ts covering URL redaction, removal/read/decode failure classification, and stable messages.

Reviewed by Cursor Bugbot for commit 95a9380. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Structure CLI token errors with stage, URL diagnostics, and secret context

  • Enriches all CLI token manager error classes (CloudCliCredentialRefreshError, CloudCliAuthorizationError, CloudCliAuthorizationTimeoutError, CloudCliCredentialReadError, CloudCliCredentialRemovalError) with structured fields: stage, secretName, token endpoint/redirect URI diagnostics, and callback host/port.
  • Replaces the generic wrapError helper with explicit static constructors (fromStage, fromCredentialRead, fromCredentialPersist, fromRedirectUri) on each error class, so error messages now include stage and URL diagnostic context.
  • Adds URL redaction via getUrlDiagnostics to strip sensitive credentials from tokenEndpoint and redirectUri before embedding them in errors.
  • Adds a vitest suite in CliTokenManager.test.ts covering error redaction, cause-chain preservation, and malformed-credential classification.
  • Removes four error mappings from the reconcileDesiredLinkWith handler in http.ts; only CloudCliCredentialRefreshError is still translated there.

Macroscope summarized 95a9380.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 980db021-8ec4-4e63-a759-1ddffc64c2a0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/cloud-cli-token-errors

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 20, 2026
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Jun 20, 2026
@macroscopeapp

macroscopeapp Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge juliusmarminge force-pushed the codex/cloud-http-error-boundaries branch from 6c12fe1 to 1c4bd6e Compare June 20, 2026 16:56
@juliusmarminge juliusmarminge force-pushed the codex/cloud-cli-token-errors branch from dfc32cd to 76635e5 Compare June 20, 2026 16:56
@macroscopeapp macroscopeapp Bot dismissed their stale review June 20, 2026 16:56

Dismissing prior approval to re-evaluate 76635e5

macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Jun 20, 2026
@juliusmarminge juliusmarminge force-pushed the codex/cloud-cli-token-errors branch 2 times, most recently from 7fa3d68 to 565971f Compare June 20, 2026 17:19
Base automatically changed from codex/cloud-http-error-boundaries to codex/server-secret-store-errors June 20, 2026 18:42
@macroscopeapp macroscopeapp Bot dismissed their stale review June 20, 2026 18:42

Dismissing prior approval to re-evaluate 565971f

macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Jun 20, 2026
Base automatically changed from codex/server-secret-store-errors to codex/server-auth-error-boundaries June 20, 2026 18:48
@macroscopeapp macroscopeapp Bot dismissed their stale review June 20, 2026 18:48

Dismissing prior approval to re-evaluate 565971f

@juliusmarminge juliusmarminge force-pushed the codex/cloud-cli-token-errors branch from 565971f to f0d9fb9 Compare June 20, 2026 18:49
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jun 20, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Asset probe errors abort search
    • Restored Effect.orElseSucceed(() => false) on fileSystem.exists calls in resolveResourcePath, resolveIconPath, and resolveUserDataPath so I/O or permission errors on individual candidates are treated as 'not found' and the search continues to remaining alternatives.

Create PR

Or push these changes by commenting:

@cursor push 8c9f81cff2
Preview (8c9f81cff2)
diff --git a/apps/desktop/src/app/DesktopAppIdentity.test.ts b/apps/desktop/src/app/DesktopAppIdentity.test.ts
--- a/apps/desktop/src/app/DesktopAppIdentity.test.ts
+++ b/apps/desktop/src/app/DesktopAppIdentity.test.ts
@@ -153,7 +153,7 @@
     ),
   );
 
-  it.effect("preserves failures while inspecting the legacy userData path", () => {
+  it.effect("falls back to the default userData path when legacy path probe fails", () => {
     const legacyPath = "/Users/alice/Library/Application Support/T3 Code (Alpha)";
     const cause = PlatformError.systemError({
       _tag: "PermissionDenied",
@@ -166,15 +166,9 @@
     return withIdentity(
       Effect.gen(function* () {
         const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
-        const error = yield* identity.resolveUserDataPath.pipe(Effect.flip);
+        const userDataPath = yield* identity.resolveUserDataPath;
 
-        assert.instanceOf(error, DesktopAppIdentity.DesktopUserDataPathResolutionError);
-        assert.equal(error.legacyPath, legacyPath);
-        assert.strictEqual(error.cause, cause);
-        assert.equal(
-          error.message,
-          `Failed to inspect legacy desktop user-data path at "${legacyPath}".`,
-        );
+        assert.equal(userDataPath, "/Users/alice/Library/Application Support/t3code");
       }),
       { legacyPathProbeError: cause },
     );

diff --git a/apps/desktop/src/app/DesktopAppIdentity.ts b/apps/desktop/src/app/DesktopAppIdentity.ts
--- a/apps/desktop/src/app/DesktopAppIdentity.ts
+++ b/apps/desktop/src/app/DesktopAppIdentity.ts
@@ -33,7 +33,7 @@
 export class DesktopAppIdentity extends Context.Service<
   DesktopAppIdentity,
   {
-    readonly resolveUserDataPath: Effect.Effect<string, DesktopUserDataPathResolutionError>;
+    readonly resolveUserDataPath: Effect.Effect<string>;
     readonly configure: Effect.Effect<void>;
   }
 >()("@t3tools/desktop/app/DesktopAppIdentity") {}
@@ -95,15 +95,9 @@
       environment.appDataDirectory,
       environment.legacyUserDataDirName,
     );
-    const legacyPathExists = yield* fileSystem.exists(legacyPath).pipe(
-      Effect.mapError(
-        (cause) =>
-          new DesktopUserDataPathResolutionError({
-            legacyPath,
-            cause,
-          }),
-      ),
-    );
+    const legacyPathExists = yield* fileSystem
+      .exists(legacyPath)
+      .pipe(Effect.orElseSucceed(() => false));
     return legacyPathExists
       ? legacyPath
       : environment.path.join(environment.appDataDirectory, environment.userDataDirName);

diff --git a/apps/desktop/src/app/DesktopAssets.test.ts b/apps/desktop/src/app/DesktopAssets.test.ts
--- a/apps/desktop/src/app/DesktopAssets.test.ts
+++ b/apps/desktop/src/app/DesktopAssets.test.ts
@@ -3,6 +3,7 @@
 import * as Effect from "effect/Effect";
 import * as FileSystem from "effect/FileSystem";
 import * as Layer from "effect/Layer";
+import * as Option from "effect/Option";
 import * as PlatformError from "effect/PlatformError";
 
 import * as DesktopAssets from "./DesktopAssets.ts";
@@ -22,7 +23,7 @@
 }).pipe(Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))));
 
 describe("DesktopAssets", () => {
-  it.effect("preserves the failed asset candidate and filesystem cause", () =>
+  it.effect("treats a failed asset probe as not found and continues searching", () =>
     Effect.gen(function* () {
       const fileName = "custom.bin";
       const candidatePath = "/repo/apps/desktop/resources/custom.bin";
@@ -41,17 +42,9 @@
       );
       const assets = yield* DesktopAssets.DesktopAssets.pipe(Effect.provide(assetsLayer));
 
-      const error = yield* assets.resolveResourcePath(fileName).pipe(Effect.flip);
+      const result = yield* assets.resolveResourcePath(fileName);
 
-      assert.instanceOf(error, DesktopAssets.DesktopAssetProbeError);
-      assert.equal(error.fileName, fileName);
-      assert.equal(error.candidatePath, candidatePath);
-      assert.strictEqual(error.cause, cause);
-      assert.equal(
-        error.message,
-        `Failed to probe desktop asset "${fileName}" at ${candidatePath}.`,
-      );
-      assert.notInclude(error.message, "private filesystem diagnostic");
+      assert.isTrue(Option.isNone(result));
     }),
   );
 });

diff --git a/apps/desktop/src/app/DesktopAssets.ts b/apps/desktop/src/app/DesktopAssets.ts
--- a/apps/desktop/src/app/DesktopAssets.ts
+++ b/apps/desktop/src/app/DesktopAssets.ts
@@ -30,9 +30,7 @@
   DesktopAssets,
   {
     readonly iconPaths: Effect.Effect<DesktopIconPaths>;
-    readonly resolveResourcePath: (
-      fileName: string,
-    ) => Effect.Effect<Option.Option<string>, DesktopAssetProbeError>;
+    readonly resolveResourcePath: (fileName: string) => Effect.Effect<Option.Option<string>>;
   }
 >()("@t3tools/desktop/app/DesktopAssets") {}
 
@@ -40,20 +38,14 @@
   fileName: string,
 ): Effect.fn.Return<
   Option.Option<string>,
-  DesktopAssetProbeError,
+  never,
   FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment
 > {
   const fileSystem = yield* FileSystem.FileSystem;
   const environment = yield* DesktopEnvironment.DesktopEnvironment;
   const candidates = environment.resolveResourcePathCandidates(fileName);
   for (const candidate of candidates) {
-    const exists = yield* fileSystem
-      .exists(candidate)
-      .pipe(
-        Effect.mapError(
-          (cause) => new DesktopAssetProbeError({ fileName, candidatePath: candidate, cause }),
-        ),
-      );
+    const exists = yield* fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false));
     if (exists) {
       return Option.some(candidate);
     }
@@ -65,23 +57,16 @@
   ext: keyof DesktopIconPaths,
 ): Effect.fn.Return<
   Option.Option<string>,
-  DesktopAssetProbeError,
+  never,
   FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment
 > {
   const fileSystem = yield* FileSystem.FileSystem;
   const environment = yield* DesktopEnvironment.DesktopEnvironment;
   if (environment.isDevelopment && environment.platform === "darwin" && ext === "png") {
     const developmentDockIconPath = environment.developmentDockIconPath;
-    const developmentDockIconExists = yield* fileSystem.exists(developmentDockIconPath).pipe(
-      Effect.mapError(
-        (cause) =>
-          new DesktopAssetProbeError({
-            fileName: "icon.png",
-            candidatePath: developmentDockIconPath,
-            cause,
-          }),
-      ),
-    );
+    const developmentDockIconExists = yield* fileSystem
+      .exists(developmentDockIconPath)
+      .pipe(Effect.orElseSucceed(() => false));
     if (developmentDockIconExists) {
       return Option.some(developmentDockIconPath);
     }

diff --git a/apps/server/src/auth/EnvironmentAuth.test.ts b/apps/server/src/auth/EnvironmentAuth.test.ts
--- a/apps/server/src/auth/EnvironmentAuth.test.ts
+++ b/apps/server/src/auth/EnvironmentAuth.test.ts
@@ -114,6 +114,16 @@
         .pipe(Effect.flip);
 
       expect(error._tag).toBe("ServerAuthScopeNotGrantedError");
+      if (error._tag === "ServerAuthScopeNotGrantedError") {
+        expect(error.requestedScopes).toEqual(["orchestration:read", "access:write"]);
+        expect(error.grantedScopes).toEqual([
+          "orchestration:read",
+          "orchestration:operate",
+          "terminal:operate",
+          "review:write",
+          "relay:read",
+        ]);
+      }
     }).pipe(Effect.provide(makeEnvironmentAuthLayer())),
   );
 
@@ -252,4 +262,20 @@
         ),
       ),
   );
+
+  it.effect("retains both session ids when rejecting self-revocation", () =>
+    Effect.gen(function* () {
+      const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;
+      const issued = yield* serverAuth.issueSession();
+      const error = yield* serverAuth
+        .revokeClientSession(issued.sessionId, issued.sessionId)
+        .pipe(Effect.flip);
+
+      expect(error._tag).toBe("ServerAuthForbiddenOperationError");
+      if (error._tag === "ServerAuthForbiddenOperationError") {
+        expect(error.currentSessionId).toBe(issued.sessionId);
+        expect(error.targetSessionId).toBe(issued.sessionId);
+      }
+    }).pipe(Effect.provide(makeEnvironmentAuthLayer())),
+  );
 });

diff --git a/apps/server/src/auth/EnvironmentAuth.ts b/apps/server/src/auth/EnvironmentAuth.ts
--- a/apps/server/src/auth/EnvironmentAuth.ts
+++ b/apps/server/src/auth/EnvironmentAuth.ts
@@ -85,44 +85,50 @@
 export class ServerAuthSessionCredentialValidationError extends Schema.TaggedErrorClass<ServerAuthSessionCredentialValidationError>()(
   "ServerAuthSessionCredentialValidationError",
   {
+    credentialKind: Schema.Literals(["session", "websocket-ticket"]),
     ...serverAuthInternalErrorContext,
   },
 ) {
   override get message(): string {
-    return "Failed to validate session credential.";
+    return `Failed to validate ${this.credentialKind} credential.`;
   }
 }
 
 export class ServerAuthAuthenticatedSessionIssueError extends Schema.TaggedErrorClass<ServerAuthAuthenticatedSessionIssueError>()(
   "ServerAuthAuthenticatedSessionIssueError",
   {
+    subject: Schema.String,
     ...serverAuthInternalErrorContext,
   },
 ) {
   override get message(): string {
-    return "Failed to issue authenticated session.";
+    return `Failed to issue authenticated session for ${this.subject}.`;
   }
 }
 
 export class ServerAuthAuthenticatedAccessTokenIssueError extends Schema.TaggedErrorClass<ServerAuthAuthenticatedAccessTokenIssueError>()(
   "ServerAuthAuthenticatedAccessTokenIssueError",
   {
+    subject: Schema.String,
+    scopes: Schema.Array(Schema.String),
     ...serverAuthInternalErrorContext,
   },
 ) {
   override get message(): string {
-    return "Failed to issue authenticated access token.";
+    return `Failed to issue authenticated access token for ${this.subject} with scopes [${this.scopes.join(", ")}].`;
   }
 }
 
 export class ServerAuthPairingLinkCreationError extends Schema.TaggedErrorClass<ServerAuthPairingLinkCreationError>()(
   "ServerAuthPairingLinkCreationError",
   {
+    subject: Schema.String,
+    scopes: Schema.Array(Schema.String),
     ...serverAuthInternalErrorContext,
   },
 ) {
   override get message(): string {
-    return "Failed to create pairing link.";
+    return `Failed to create pairing link for ${this.subject} with scopes [${this.scopes.join(", ")}].`;
   }
 }
 
@@ -140,22 +146,25 @@
 export class ServerAuthPairingLinkRevocationError extends Schema.TaggedErrorClass<ServerAuthPairingLinkRevocationError>()(
   "ServerAuthPairingLinkRevocationError",
   {
+    pairingLinkId: Schema.String,
     ...serverAuthInternalErrorContext,
   },
 ) {
   override get message(): string {
-    return "Failed to revoke pairing link.";
+    return `Failed to revoke pairing link ${this.pairingLinkId}.`;
   }
 }
 
 export class ServerAuthSessionTokenIssueError extends Schema.TaggedErrorClass<ServerAuthSessionTokenIssueError>()(
   "ServerAuthSessionTokenIssueError",
   {
+    subject: Schema.String,
+    scopes: Schema.Array(Schema.String),
     ...serverAuthInternalErrorContext,
   },
 ) {
   override get message(): string {
-    return "Failed to issue session token.";
+    return `Failed to issue session token for ${this.subject} with scopes [${this.scopes.join(", ")}].`;
   }
 }
 
@@ -173,55 +182,63 @@
 export class ServerAuthSessionRevocationError extends Schema.TaggedErrorClass<ServerAuthSessionRevocationError>()(
   "ServerAuthSessionRevocationError",
   {
+    sessionId: Schema.String,
     ...serverAuthInternalErrorContext,
   },
 ) {
   override get message(): string {
-    return "Failed to revoke session.";
+    return `Failed to revoke session ${this.sessionId}.`;
   }
 }
 
 export class ServerAuthOtherSessionsRevocationError extends Schema.TaggedErrorClass<ServerAuthOtherSessionsRevocationError>()(
   "ServerAuthOtherSessionsRevocationError",
   {
+    excludedSessionId: Schema.String,
     ...serverAuthInternalErrorContext,
   },
 ) {
   override get message(): string {
-    return "Failed to revoke other sessions.";
+    return `Failed to revoke sessions other than ${this.excludedSessionId}.`;
   }
 }
 
 export class ServerAuthWebSocketTokenIssueError extends Schema.TaggedErrorClass<ServerAuthWebSocketTokenIssueError>()(
   "ServerAuthWebSocketTokenIssueError",
   {
+    sessionId: Schema.String,
     ...serverAuthInternalErrorContext,
   },
 ) {
   override get message(): string {
-    return "Failed to issue websocket token.";
+    return `Failed to issue websocket token for session ${this.sessionId}.`;
   }
 }
 
 export class ServerAuthDpopReplayStateRecordError extends Schema.TaggedErrorClass<ServerAuthDpopReplayStateRecordError>()(
   "ServerAuthDpopReplayStateRecordError",
   {
+    proofKeyThumbprint: Schema.String,
+    proofId: Schema.String,
+    replayKey: Schema.String,
     ...serverAuthInternalErrorContext,
   },
 ) {
   override get message(): string {
-    return "Failed to record DPoP proof replay state.";
+    return `Failed to record replay state for DPoP proof ${this.proofId} (${this.proofKeyThumbprint}).`;
   }
 }
 
 export class ServerAuthDpopReplayKeyCalculationError extends Schema.TaggedErrorClass<ServerAuthDpopReplayKeyCalculationError>()(
   "ServerAuthDpopReplayKeyCalculationError",
   {
+    proofKeyThumbprint: Schema.String,
+    proofId: Schema.String,
     ...serverAuthInternalErrorContext,
   },
 ) {
   override get message(): string {
-    return "Failed to calculate DPoP replay key.";
+    return `Failed to calculate replay key for DPoP proof ${this.proofId} (${this.proofKeyThumbprint}).`;
   }
 }
 
@@ -360,11 +377,6 @@
   ServerAuthInvalidCredentialError,
 ]);
 export type ServerAuthCredentialError = typeof ServerAuthCredentialError.Type;
-export const isServerAuthCredentialError = Schema.is(ServerAuthCredentialError);
-export const serverAuthCredentialReason = (
-  error: ServerAuthCredentialError,
-): "missing_credential" | "invalid_credential" =>
-  error._tag === "ServerAuthMissingCredentialError" ? "missing_credential" : "invalid_credential";
 
 export class ServerAuthInvalidScopeError extends Schema.TaggedErrorClass<ServerAuthInvalidScopeError>()(
   "ServerAuthInvalidScopeError",
@@ -377,10 +389,13 @@
 
 export class ServerAuthScopeNotGrantedError extends Schema.TaggedErrorClass<ServerAuthScopeNotGrantedError>()(
   "ServerAuthScopeNotGrantedError",
-  {},
+  {
+    requestedScopes: Schema.Array(Schema.String),
+    grantedScopes: Schema.Array(Schema.String),
+  },
 ) {
   override get message(): string {
-    return "The requested authentication scope was not granted.";
+    return `Requested scopes [${this.requestedScopes.join(", ")}] exceed granted scopes [${this.grantedScopes.join(", ")}].`;
   }
 }
 
@@ -389,28 +404,35 @@
   ServerAuthScopeNotGrantedError,
 ]);
 export type ServerAuthInvalidRequestError = typeof ServerAuthInvalidRequestError.Type;
-export const isServerAuthInvalidRequestError = Schema.is(ServerAuthInvalidRequestError);
-export const serverAuthInvalidRequestReason = (
-  error: ServerAuthInvalidRequestError,
-): "invalid_scope" | "scope_not_granted" =>
-  error._tag === "ServerAuthInvalidScopeError" ? "invalid_scope" : "scope_not_granted";
 
 export class ServerAuthForbiddenOperationError extends Schema.TaggedErrorClass<ServerAuthForbiddenOperationError>()(
   "ServerAuthForbiddenOperationError",
-  {},
+  {
+    currentSessionId: Schema.String,
+    targetSessionId: Schema.String,
+  },
 ) {
   override get message(): string {
-    return "The current authentication session cannot revoke itself.";
+    return `Authentication session ${this.currentSessionId} cannot revoke itself.`;
   }
 }
 
+export type ServerAuthAuthenticationInternalError =
+  | ServerAuthSessionCredentialValidationError
+  | ServerAuthDpopReplayStateRecordError
+  | ServerAuthDpopReplayKeyCalculationError;
+
+export type ServerAuthAuthenticationError =
+  | ServerAuthCredentialError
+  | ServerAuthAuthenticationInternalError;
+
 export class EnvironmentAuth extends Context.Service<
   EnvironmentAuth,
   {
     readonly getDescriptor: () => Effect.Effect<ServerAuthDescriptor>;
     readonly getSessionState: (
       request: HttpServerRequest.HttpServerRequest,
-    ) => Effect.Effect<AuthSessionState, ServerAuthInternalError>;
+    ) => Effect.Effect<AuthSessionState, ServerAuthAuthenticationInternalError>;
     readonly createBrowserSession: (
       credential: string,
       requestMetadata: AuthClientMetadata,
@@ -419,7 +441,9 @@
         readonly response: AuthBrowserSessionResult;
         readonly sessionToken: string;
       },
-      ServerAuthInvalidCredentialError | ServerAuthInternalError
+      | ServerAuthInvalidCredentialError
+      | ServerAuthBootstrapCredentialValidationError
+      | ServerAuthAuthenticatedSessionIssueError
     >;
     readonly exchangeBootstrapCredentialForAccessToken: (
       credential: string,
@@ -430,7 +454,10 @@
       },
     ) => Effect.Effect<
       AuthAccessTokenResult,
-      ServerAuthInvalidCredentialError | ServerAuthInvalidRequestError | ServerAuthInternalError
+      | ServerAuthInvalidCredentialError
+      | ServerAuthScopeNotGrantedError
+      | ServerAuthBootstrapCredentialValidationError
+      | ServerAuthAuthenticatedAccessTokenIssueError
     >;
     readonly createPairingLink: (input?: {
       readonly ttl?: Duration.Duration;
@@ -438,56 +465,61 @@
       readonly scopes?: ReadonlyArray<AuthEnvironmentScope>;
       readonly subject?: string;
       readonly proofKeyThumbprint?: string;
-    }) => Effect.Effect<IssuedPairingLink, ServerAuthInternalError>;
+    }) => Effect.Effect<IssuedPairingLink, ServerAuthPairingLinkCreationError>;
     readonly issuePairingCredential: (
       input?: AuthCreatePairingCredentialInput,
-    ) => Effect.Effect<AuthPairingCredentialResult, ServerAuthInternalError>;
+    ) => Effect.Effect<AuthPairingCredentialResult, ServerAuthPairingLinkCreationError>;
     readonly issueStartupPairingCredential: () => Effect.Effect<
       AuthPairingCredentialResult,
-      ServerAuthInternalError
+      ServerAuthPairingLinkCreationError
     >;
     readonly listPairingLinks: (input?: {
       readonly excludeSubjects?: ReadonlyArray<string>;
-    }) => Effect.Effect<ReadonlyArray<AuthPairingLink>, ServerAuthInternalError>;
-    readonly revokePairingLink: (id: string) => Effect.Effect<boolean, ServerAuthInternalError>;
+    }) => Effect.Effect<ReadonlyArray<AuthPairingLink>, ServerAuthPairingLinksListError>;
+    readonly revokePairingLink: (
+      id: string,
+    ) => Effect.Effect<boolean, ServerAuthPairingLinkRevocationError>;
     readonly issueSession: (input?: {
       readonly ttl?: Duration.Duration;
       readonly subject?: string;
       readonly scopes?: ReadonlyArray<AuthEnvironmentScope>;
       readonly label?: string;
-    }) => Effect.Effect<IssuedBearerSession, ServerAuthInternalError>;
+    }) => Effect.Effect<IssuedBearerSession, ServerAuthSessionTokenIssueError>;
     readonly listSessions: () => Effect.Effect<
       ReadonlyArray<AuthClientSession>,
-      ServerAuthInternalError
+      ServerAuthSessionsListError
     >;
     readonly revokeSession: (
       sessionId: AuthSessionId,
-    ) => Effect.Effect<boolean, ServerAuthInternalError>;
+    ) => Effect.Effect<boolean, ServerAuthSessionRevocationError>;
     readonly revokeOtherSessionsExcept: (
       sessionId: AuthSessionId,
-    ) => Effect.Effect<number, ServerAuthInternalError>;
+    ) => Effect.Effect<number, ServerAuthOtherSessionsRevocationError>;
     readonly listClientSessions: (
       currentSessionId: AuthSessionId,
-    ) => Effect.Effect<ReadonlyArray<AuthClientSession>, ServerAuthInternalError>;
+    ) => Effect.Effect<ReadonlyArray<AuthClientSession>, ServerAuthSessionsListError>;
     readonly revokeClientSession: (
       currentSessionId: AuthSessionId,
       targetSessionId: AuthSessionId,
-    ) => Effect.Effect<boolean, ServerAuthForbiddenOperationError | ServerAuthInternalError>;
+    ) => Effect.Effect<
+      boolean,
+      ServerAuthForbiddenOperationError | ServerAuthSessionRevocationError
+    >;
     readonly revokeOtherClientSessions: (
       currentSessionId: AuthSessionId,
-    ) => Effect.Effect<number, ServerAuthInternalError>;
+    ) => Effect.Effect<number, ServerAuthOtherSessionsRevocationError>;
     readonly authenticateHttpRequest: (
       request: HttpServerRequest.HttpServerRequest,
-    ) => Effect.Effect<AuthenticatedSession, ServerAuthCredentialError | ServerAuthInternalError>;
+    ) => Effect.Effect<AuthenticatedSession, ServerAuthAuthenticationError>;
     readonly authenticateWebSocketUpgrade: (
       request: HttpServerRequest.HttpServerRequest,
-    ) => Effect.Effect<AuthenticatedSession, ServerAuthCredentialError | ServerAuthInternalError>;
+    ) => Effect.Effect<AuthenticatedSession, ServerAuthAuthenticationError>;
     readonly issueWebSocketTicket: (
       session: Pick<AuthenticatedSession, "sessionId">,
-    ) => Effect.Effect<AuthWebSocketTicketResult, ServerAuthInternalError>;
+    ) => Effect.Effect<AuthWebSocketTicketResult, ServerAuthWebSocketTokenIssueError>;
     readonly issueStartupPairingUrl: (
       baseUrl: string,
-    ) => Effect.Effect<string, ServerAuthInternalError>;
+    ) => Effect.Effect<string, ServerAuthPairingLinkCreationError>;
   }
 >()("t3/auth/EnvironmentAuth") {}
 
@@ -514,7 +546,7 @@
 
 export function toBootstrapExchangeError(
   cause: PairingGrantStore.BootstrapCredentialError,
-): ServerAuthInvalidCredentialError | ServerAuthInternalError {
+): ServerAuthInvalidCredentialError | ServerAuthBootstrapCredentialValidationError {
   if (PairingGrantStore.isBootstrapCredentialInternalError(cause)) {
     return new ServerAuthBootstrapCredentialValidationError({ cause });
   }
@@ -526,12 +558,17 @@
 
 const mapSessionVerificationErrors = <A, R>(
   effect: Effect.Effect<A, SessionStore.SessionCredentialError, R>,
-): Effect.Effect<A, ServerAuthInvalidCredentialError | ServerAuthInternalError, R> =>
+  credentialKind: ServerAuthSessionCredentialValidationError["credentialKind"],
+): Effect.Effect<
+  A,
+  ServerAuthInvalidCredentialError | ServerAuthSessionCredentialValidationError,
+  R
+> =>
   effect.pipe(
     Effect.mapError((cause) =>
       SessionStore.isSessionCredentialInvalidError(cause)
         ? new ServerAuthInvalidCredentialError({ cause })
-        : new ServerAuthSessionCredentialValidationError({ cause }),
+        : new ServerAuthSessionCredentialValidationError({ credentialKind, cause }),
     ),
   );
 
@@ -565,7 +602,7 @@
     token: string,
   ): Effect.Effect<
     AuthenticatedSession,
-    ServerAuthInvalidCredentialError | ServerAuthInternalError
+    ServerAuthInvalidCredentialError | ServerAuthSessionCredentialValidationError
   > =>
     sessions.verify(token).pipe(
       Effect.tapError((cause) =>
@@ -585,12 +622,12 @@
         ...(session.proofKeyThumbprint ? { proofKeyThumbprint: session.proofKeyThumbprint } : {}),
         ...(session.expiresAt ? { expiresAt: session.expiresAt } : {}),
       })),
-      mapSessionVerificationErrors,
+      (effect) => mapSessionVerificationErrors(effect, "session"),
     );
 
   const authenticateRequest = (
     request: HttpServerRequest.HttpServerRequest,
-  ): Effect.Effect<AuthenticatedSession, ServerAuthCredentialError | ServerAuthInternalError> => {
+  ): Effect.Effect<AuthenticatedSession, ServerAuthAuthenticationError> => {
     const cookieToken = request.cookies[sessions.cookieName];
     const bearerToken = parseBearerToken(request);
     const dpopToken = parseDpopToken(request);
@@ -642,12 +679,18 @@
             ...(session.expiresAt ? { expiresAt: DateTime.toUtc(session.expiresAt) } : {}),
           }) satisfies AuthSessionState,
       ),
-      Effect.catchIf(isServerAuthCredentialError, () =>
-        Effect.succeed({
-          authenticated: false,
-          auth: descriptor,
-        } satisfies AuthSessionState),
-      ),
+      Effect.catchTags({
+        ServerAuthMissingCredentialError: () =>
+          Effect.succeed({
+            authenticated: false,
+            auth: descriptor,
+          } satisfies AuthSessionState),
+        ServerAuthInvalidCredentialError: () =>
+          Effect.succeed({
+            authenticated: false,
+            auth: descriptor,
+          } satisfies AuthSessionState),
+      }),
       Effect.withSpan("EnvironmentAuth.getSessionState"),
     );
 
@@ -669,7 +712,13 @@
             },
           })
           .pipe(
-            Effect.mapError((cause) => new ServerAuthAuthenticatedSessionIssueError({ cause })),
+            Effect.mapError(
+              (cause) =>
+                new ServerAuthAuthenticatedSessionIssueError({
+                  subject: grant.subject,
+                  cause,
+                }),
+            ),
           ),
       ),
       Effect.map(
@@ -695,7 +744,10 @@
           Effect.gen(function* () {
             const grantedScopes = requestedScopes ?? grant.scopes;
             if (!grantedScopes.every((scope) => grant.scopes.includes(scope))) {
-              return yield* new ServerAuthScopeNotGrantedError({});
+              return yield* new ServerAuthScopeNotGrantedError({
+                requestedScopes: grantedScopes,
+                grantedScopes: grant.scopes,
+              });
             }
             return yield* sessions
               .issue({
@@ -715,7 +767,12 @@
               })
               .pipe(
                 Effect.mapError(
-                  (cause) => new ServerAuthAuthenticatedAccessTokenIssueError({ cause }),
+                  (cause) =>
+                    new ServerAuthAuthenticatedAccessTokenIssueError({
+                      subject: grant.subject,
+                      scopes: grantedScopes,
+                      cause,
+                    }),
                 ),
               );
           }),
@@ -765,28 +822,33 @@
 
   const createPairingLink: EnvironmentAuth["Service"]["createPairingLink"] = Effect.fn(
     "EnvironmentAuth.createPairingLink",
-  )(
-    function* (input) {
-      const createdAt = yield* DateTime.now;
-      const issued = yield* bootstrapCredentials.issueOneTimeToken({
-        scopes: input?.scopes ?? AuthStandardClientScopes,
-        subject: input?.subject ?? "one-time-token",
+  )(function* (input) {
+    const scopes = input?.scopes ?? AuthStandardClientScopes;
+    const subject = input?.subject ?? "one-time-token";
+    const createdAt = yield* DateTime.now;
+    const issued = yield* bootstrapCredentials
+      .issueOneTimeToken({
+        scopes,
+        subject,
         ...(input?.ttl ? { ttl: input.ttl } : {}),
         ...(input?.label ? { label: input.label } : {}),
         ...(input?.proofKeyThumbprint ? { proofKeyThumbprint: input.proofKeyThumbprint } : {}),
-      });
-      return {
-        id: issued.id,
-        credential: issued.credential,
-        scopes: input?.scopes ?? AuthStandardClientScopes,
-        subject: input?.subject ?? "one-time-token",
-        ...(issued.label ? { label: issued.label } : {}),
-        createdAt: DateTime.toUtc(createdAt),
-        expiresAt: DateTime.toUtc(issued.expiresAt),
-      } satisfies IssuedPairingLink;
-    },
-    Effect.mapError((cause) => new ServerAuthPairingLinkCreationError({ cause })),
-  );
+      })
+      .pipe(
+        Effect.mapError(
+          (cause) => new ServerAuthPairingLinkCreationError({ subject, scopes, cause }),
+        ),
+      );
+    return {
+      id: issued.id,
+      credential: issued.credential,
+      scopes,
+      subject,
+      ...(issued.label ? { label: issued.label } : {}),
+      createdAt: DateTime.toUtc(createdAt),
+      expiresAt: DateTime.toUtc(issued.expiresAt),
+    } satisfies IssuedPairingLink;
+  });
 
   const listPairingLinks: EnvironmentAuth["Service"]["listPairingLinks"] = (input) =>
     bootstrapCredentials.listActive().pipe(
@@ -806,16 +868,20 @@
 
   const revokePairingLink: EnvironmentAuth["Service"]["revokePairingLink"] = (id) =>
     bootstrapCredentials.revoke(id).pipe(
-      Effect.mapError((cause) => new ServerAuthPairingLinkRevocationError({ cause })),
+      Effect.mapError(
+        (cause) => new ServerAuthPairingLinkRevocationError({ pairingLinkId: id, cause }),
+      ),
       Effect.withSpan("EnvironmentAuth.revokePairingLink"),
     );
 
-  const issueSession: EnvironmentAuth["Service"]["issueSession"] = (input) =>
-    sessions
+  const issueSession: EnvironmentAuth["Service"]["issueSession"] = (input) => {
+    const subject = input?.subject ?? DEFAULT_SESSION_SUBJECT;
+    const scopes = input?.scopes ?? AuthAdministrativeScopes;
+    return sessions
       .issue({
-        subject: input?.subject ?? DEFAULT_SESSION_SUBJECT,
+        subject,
         method: "bearer-access-token",
-        scopes: input?.scopes ?? AuthAdministrativeScopes,
+        scopes,
         client: {
           ...(input?.label ? { label: input.label } : {}),
           deviceType: "bot",
@@ -830,14 +896,17 @@
               token: issued.token,
               method: "bearer-access-token",
               scopes: issued.scopes,
-              subject: input?.subject ?? DEFAULT_SESSION_SUBJECT,
+              subject,
               client: issued.client,
               expiresAt: DateTime.toUtc(issued.expiresAt),
             }) satisfies IssuedBearerSession,
         ),
-        Effect.mapError((cause) => new ServerAuthSessionTokenIssueError({ cause })),
+        Effect.mapError(
+          (cause) => new ServerAuthSessionTokenIssueError({ subject, scopes, cause }),
+        ),
         Effect.withSpan("EnvironmentAuth.issueSession"),
       );
+  };
 
   const listSessions: EnvironmentAuth["Service"]["listSessions"] = () =>
     sessions.listActive().pipe(
@@ -848,7 +917,7 @@
 
   const revokeSession: EnvironmentAuth["Service"]["revokeSession"] = (sessionId) =>
     sessions.revoke(sessionId).pipe(
-      Effect.mapError((cause) => new ServerAuthSessionRevocationError({ cause })),
+      Effect.mapError((cause) => new ServerAuthSessionRevocationError({ sessionId, cause })),
       Effect.withSpan("EnvironmentAuth.revokeSession"),
     );
 
@@ -856,7 +925,10 @@
     sessionId,
   ) =>
     sessions.revokeAllExcept(sessionId).pipe(
-      Effect.mapError((cause) => new ServerAuthOtherSessionsRevocationError({ cause })),
+      Effect.mapError(
+        (cause) =>
+          new ServerAuthOtherSessionsRevocationError({ excludedSessionId: sessionId, cause }),
+      ),
       Effect.withSpan("EnvironmentAuth.revokeOtherSessionsExcept"),
     );
 
@@ -891,7 +963,10 @@
     "EnvironmentAuth.revokeClientSession",
   )(function* (currentSessionId, targetSessionId) {
     if (currentSessionId === targetSessionId) {
-      return yield* new ServerAuthForbiddenOperationError({});
+      return yield* new ServerAuthForbiddenOperationError({
+        currentSessionId,
+        targetSessionId,
+      });
     }
     return yield* revokeSession(targetSessionId);
   });
@@ -917,7 +992,9 @@
 
   const issueWebSocketTicket: EnvironmentAuth["Service"]["issueWebSocketTicket"] = (session) =>
     sessions.issueWebSocketToken(session.sessionId).pipe(
-      Effect.mapError((cause) => new ServerAuthWebSocketTokenIssueError({ cause })),
+      Effect.mapError(
+        (cause) => new ServerAuthWebSocketTokenIssueError({ sessionId: session.sessionId, cause }),
+      ),
       Effect.map(
... diff truncated: showing 800 of 3579 lines

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit f0d9fb9. Configure here.

Comment thread apps/desktop/src/app/DesktopAssets.ts Outdated
Effect.mapError(
(cause) => new DesktopAssetProbeError({ fileName, candidatePath: candidate, cause }),
),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Asset probe errors abort search

High Severity

Changes to fileSystem.exists now propagate I/O or permission errors as fatal, rather than treating them as if the file or path doesn't exist. This prevents the application from finding valid resource paths (like icons) or user data directories, potentially blocking desktop startup even when alternatives are available.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f0d9fb9. Configure here.

@juliusmarminge juliusmarminge force-pushed the codex/cloud-cli-token-errors branch 3 times, most recently from 3e06261 to 98a7351 Compare June 20, 2026 19:18
juliusmarminge and others added 2 commits June 20, 2026 12:21
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge juliusmarminge force-pushed the codex/cloud-cli-token-errors branch from 98a7351 to 95a9380 Compare June 20, 2026 19:22
@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:XXL 1,000+ changed lines (additions + deletions). labels Jun 20, 2026
@juliusmarminge juliusmarminge merged commit 0f8b837 into codex/server-auth-error-boundaries Jun 20, 2026
16 checks passed
@juliusmarminge juliusmarminge deleted the codex/cloud-cli-token-errors branch June 20, 2026 19:31
juliusmarminge added a commit that referenced this pull request Jun 20, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
juliusmarminge added a commit that referenced this pull request Jun 20, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
juliusmarminge added a commit that referenced this pull request Jun 20, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
juliusmarminge added a commit that referenced this pull request Jun 20, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
juliusmarminge added a commit that referenced this pull request Jun 20, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
juliusmarminge added a commit that referenced this pull request Jun 21, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant