diff --git a/packages/adapters/codex-local/src/server/quota.ts b/packages/adapters/codex-local/src/server/quota.ts index b51f6646..7bc771e4 100644 --- a/packages/adapters/codex-local/src/server/quota.ts +++ b/packages/adapters/codex-local/src/server/quota.ts @@ -107,8 +107,8 @@ function parsePlanAndEmailFromToken(idToken: string | null, accessToken: string return { email: null, planType: null }; } -export async function readCodexAuthInfo(): Promise { - const authPath = path.join(codexHomeDir(), "auth.json"); +export async function readCodexAuthInfo(codexHome?: string): Promise { + const authPath = path.join(codexHome ?? codexHomeDir(), "auth.json"); let raw: string; try { raw = await fs.readFile(authPath, "utf8"); diff --git a/packages/adapters/codex-local/src/server/test.ts b/packages/adapters/codex-local/src/server/test.ts index 292e53ee..64af601b 100644 --- a/packages/adapters/codex-local/src/server/test.ts +++ b/packages/adapters/codex-local/src/server/test.ts @@ -15,6 +15,7 @@ import { } from "@paperclipai/adapter-utils/server-utils"; import path from "node:path"; import { parseCodexJsonl } from "./parse.js"; +import { codexHomeDir, readCodexAuthInfo } from "./quota.js"; function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] { if (checks.some((check) => check.level === "error")) return "fail"; @@ -108,12 +109,23 @@ export async function testEnvironment( detail: `Detected in ${source}.`, }); } else { - checks.push({ - code: "codex_openai_api_key_missing", - level: "warn", - message: "OPENAI_API_KEY is not set. Codex runs may fail until authentication is configured.", - hint: "Set OPENAI_API_KEY in adapter env, shell environment, or Codex auth configuration.", - }); + const codexHome = isNonEmpty(env.CODEX_HOME) ? env.CODEX_HOME : undefined; + const codexAuth = await readCodexAuthInfo(codexHome).catch(() => null); + if (codexAuth) { + checks.push({ + code: "codex_native_auth_present", + level: "info", + message: "Codex is authenticated via its own auth configuration.", + detail: codexAuth.email ? `Logged in as ${codexAuth.email}.` : `Credentials found in ${path.join(codexHome ?? codexHomeDir(), "auth.json")}.`, + }); + } else { + checks.push({ + code: "codex_openai_api_key_missing", + level: "warn", + message: "OPENAI_API_KEY is not set. Codex runs may fail until authentication is configured.", + hint: "Set OPENAI_API_KEY in adapter env, shell environment, or run `codex auth` to log in.", + }); + } } const canRunProbe = diff --git a/server/src/__tests__/codex-local-adapter-environment.test.ts b/server/src/__tests__/codex-local-adapter-environment.test.ts index a9201c98..ba92a224 100644 --- a/server/src/__tests__/codex-local-adapter-environment.test.ts +++ b/server/src/__tests__/codex-local-adapter-environment.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; @@ -7,6 +7,12 @@ import { testEnvironment } from "@paperclipai/adapter-codex-local/server"; const itWindows = process.platform === "win32" ? it : it.skip; describe("codex_local environment diagnostics", () => { + beforeEach(() => { + vi.stubEnv("OPENAI_API_KEY", ""); + }); + afterEach(() => { + vi.unstubAllEnvs(); + }); it("creates a missing working directory when cwd is absolute", async () => { const cwd = path.join( os.tmpdir(), @@ -32,6 +38,67 @@ describe("codex_local environment diagnostics", () => { await fs.rm(path.dirname(cwd), { recursive: true, force: true }); }); + it("emits codex_native_auth_present when ~/.codex/auth.json exists and OPENAI_API_KEY is unset", async () => { + const root = path.join( + os.tmpdir(), + `paperclip-codex-auth-${Date.now()}-${Math.random().toString(16).slice(2)}`, + ); + const codexHome = path.join(root, ".codex"); + const cwd = path.join(root, "workspace"); + + try { + await fs.mkdir(codexHome, { recursive: true }); + await fs.writeFile( + path.join(codexHome, "auth.json"), + JSON.stringify({ accessToken: "fake-token", accountId: "acct-1" }), + ); + + const result = await testEnvironment({ + companyId: "company-1", + adapterType: "codex_local", + config: { + command: process.execPath, + cwd, + env: { CODEX_HOME: codexHome }, + }, + }); + + expect(result.checks.some((check) => check.code === "codex_native_auth_present")).toBe(true); + expect(result.checks.some((check) => check.code === "codex_openai_api_key_missing")).toBe(false); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it("emits codex_openai_api_key_missing when neither env var nor native auth exists", async () => { + const root = path.join( + os.tmpdir(), + `paperclip-codex-noauth-${Date.now()}-${Math.random().toString(16).slice(2)}`, + ); + const codexHome = path.join(root, ".codex"); + const cwd = path.join(root, "workspace"); + + try { + await fs.mkdir(codexHome, { recursive: true }); + // No auth.json written + + const result = await testEnvironment({ + companyId: "company-1", + adapterType: "codex_local", + config: { + command: process.execPath, + cwd, + env: { CODEX_HOME: codexHome }, + }, + }); + + expect(result.checks.some((check) => check.code === "codex_openai_api_key_missing")).toBe(true); + expect(result.checks.some((check) => check.code === "codex_native_auth_present")).toBe(false); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + itWindows("runs the hello probe when Codex is available via a Windows .cmd wrapper", async () => { const root = path.join( os.tmpdir(),