diff --git a/apps/server/src/telemetry/Identify.test.ts b/apps/server/src/telemetry/Identify.test.ts index ab151821789a..91216c12f919 100644 --- a/apps/server/src/telemetry/Identify.test.ts +++ b/apps/server/src/telemetry/Identify.test.ts @@ -77,6 +77,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 b6c3d0066dff..7c252c442c27 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) { diff --git a/docs/fork/0007-codex-api-key-auth-is-supported.md b/docs/fork/0007-codex-api-key-auth-is-supported.md new file mode 100644 index 000000000000..0040bf477855 --- /dev/null +++ b/docs/fork/0007-codex-api-key-auth-is-supported.md @@ -0,0 +1,27 @@ +# 0007: API-key Codex installs are not reported as broken + +- PR: [TrogonStack/t3code#15](https://github.com/TrogonStack/t3code/pull/15) +- Status: active + +## What you can do now + +- Run Codex authenticated with an API key instead of a ChatGPT account + without a warning and a stack trace on every server start. Telemetry falls + back to the anonymous identifier quietly, the same way it already did when + there is no Codex auth file at all. +- An auth file that really is unreadable or corrupt still warns, so the + warning keeps meaning something. + +## Why + +API keys are a supported way to authenticate Codex, so an install using one +is healthy and should not read as a malfunction in the logs. A warning that +fires on every start of a working setup is worse than no warning, because it +teaches you to scroll past the one that matters. + +## Upstream considerations + +Nothing here is fork-specific, so this belongs upstream as an ordinary bug +fix. Submit it, then delete this entry once it merges. The surface is small +enough that carrying it in the meantime costs nothing at sync time, though it +does sit in a shared file, so a sync must not drop it. diff --git a/docs/fork/README.md b/docs/fork/README.md index d86972f85fdd..c8249d1b7c13 100644 --- a/docs/fork/README.md +++ b/docs/fork/README.md @@ -27,6 +27,7 @@ Each entry uses these sections: ## Ledger -| # | Divergence | PR | Status | -| ---- | ------------------------------------------------------------------------------------- | -------------------------------------------------- | ------ | -| 0003 | [Native subagent threads for Claude orchestrators](./0003-native-subagent-threads.md) | [#3](https://github.com/TrogonStack/t3code/pull/3) | active | +| # | Divergence | PR | Status | +| ---- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ------ | +| 0003 | [Native subagent threads for Claude orchestrators](./0003-native-subagent-threads.md) | [#3](https://github.com/TrogonStack/t3code/pull/3) | active | +| 0007 | [API-key Codex installs are not reported as broken](./0007-codex-api-key-auth-is-supported.md) | [#15](https://github.com/TrogonStack/t3code/pull/15) | active |