diff --git a/apps/server/src/telemetry/Identify.test.ts b/apps/server/src/telemetry/Identify.test.ts index 92d3223267d7..a8c107b57389 100644 --- a/apps/server/src/telemetry/Identify.test.ts +++ b/apps/server/src/telemetry/Identify.test.ts @@ -57,6 +57,49 @@ it.layer(NodeServices.layer)("telemetry identity", (it) => { ), ); + it.effect("falls back quietly when Codex authenticates with an API key", () => { + const logs: CapturedLog[] = []; + const logger = makeCaptureLogger(logs); + + return Effect.gen(function* () { + const config = yield* ServerConfig.ServerConfig; + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const homeDirectory = path.join(config.baseDir, "home"); + const codexAuthPath = path.join(homeDirectory, ".codex", "auth.json"); + const anonymousId = "api-key-fallback-anonymous-id"; + const privateApiKey = "sk-private-openai-api-key"; + + yield* fileSystem.makeDirectory(path.dirname(codexAuthPath), { recursive: true }); + yield* fileSystem.writeFileString( + codexAuthPath, + `{"auth_mode":"apikey","OPENAI_API_KEY":"${privateApiKey}"}`, + ); + yield* fileSystem.writeFileString(config.anonymousIdPath, anonymousId); + + const identifier = yield* Identify.getTelemetryIdentifierForHome(homeDirectory); + + assert.equal(identifier, sha256(anonymousId)); + assert.isUndefined(findIdentityLog(logs, "codex", "TelemetryIdentityDecodeError")); + assert.isUndefined(findIdentityLog(logs, "codex", "TelemetryIdentityReadError")); + const allLogs = logs + .map((log) => + [String(log.message), ...Object.values(log.annotations).map(String)].join("\n"), + ) + .join("\n"); + assert.notInclude(allLogs, privateApiKey); + }).pipe( + Effect.provide( + Layer.merge( + ServerConfig.layerTest(process.cwd(), { + prefix: "t3-telemetry-identify-apikey-", + }), + Logger.layer([logger], { mergeWithExisting: false }), + ), + ), + ); + }); + it.effect("logs structured decode context and falls back from malformed Codex auth", () => { const logs: CapturedLog[] = []; const logger = makeCaptureLogger(logs); diff --git a/apps/server/src/telemetry/Identify.ts b/apps/server/src/telemetry/Identify.ts index d68812c17cc6..1a658a1b5043 100644 --- a/apps/server/src/telemetry/Identify.ts +++ b/apps/server/src/telemetry/Identify.ts @@ -10,10 +10,17 @@ import * as Schema from "effect/Schema"; import * as ServerConfig from "../config.ts"; +/** + * Codex omits `tokens` entirely when the install authenticates with an API key + * rather than a ChatGPT account, so an absent `tokens` is a supported install + * and not a malformed file. + */ const CodexAuthJsonSchema = Schema.Struct({ - tokens: Schema.Struct({ - account_id: Schema.String, - }), + tokens: Schema.optional( + Schema.Struct({ + account_id: Schema.String, + }), + ), }); const ClaudeJsonSchema = Schema.Struct({ @@ -183,7 +190,9 @@ const getCodexAccountId = Effect.fn("TelemetryIdentity.getCodexAccountId")(funct ), ); - return Option.some(authJson.tokens.account_id); + return authJson.tokens === undefined + ? Option.none() + : Option.some(authJson.tokens.account_id); }); const getClaudeUserId = Effect.fn("TelemetryIdentity.getClaudeUserId")(function* ( @@ -250,6 +259,9 @@ const upsertAnonymousId = Effect.gen(function* () { * 1. ~/.codex/auth.json tokens.account_id * 2. ~/.claude.json userID * 3. ~/.t3/telemetry/anonymous-id + * + * A missing file or an API-key-only Codex auth.json falls through quietly. Only + * unreadable or malformed files warn. */ export const getTelemetryIdentifierForHome = Effect.fn("getTelemetryIdentifierForHome")( function* (homeDirectory: string) {