diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index e791e038..c85f424f 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -187,9 +187,8 @@ $env:OPENAI_API_KEY = "" npx @openai/codex-security scan C:\code\repository ``` -Check or remove the stored sign-in with `npx @openai/codex-security login status` -and `npx @openai/codex-security logout`. Codex Security keeps its sign-in in a -private, stable Codex home at `$CODEX_SECURITY_STATE_DIR/codex-home`, or at +Codex Security keeps its sign-in in a private, stable Codex home at +`$CODEX_SECURITY_STATE_DIR/codex-home`, or at `$CODEX_HOME/state/plugins/codex-security/codex-home` when no state directory is configured. On managed Windows devices, inherited access for `SYSTEM` and local `Administrators` is preserved while protecting the home against future changes @@ -202,7 +201,15 @@ when the dedicated home does not already contain stored credentials. Logging out prevents later scans from automatically reimporting that ambient sign-in until you explicitly log in again. -An environment API key takes precedence over a stored sign-in by default. +If a scan says the stored ChatGPT sign-in could not be refreshed, check it with +`npx @openai/codex-security login status` and retry if it recently changed. +Otherwise replace it with `npx @openai/codex-security logout`, then +`npx @openai/codex-security login`. Codex Security does not automatically clear +the sign-in or change managed login restrictions. + +An environment API key takes precedence for model authentication by default, +but Codex may still need a valid ChatGPT sign-in to load workspace-managed +policies. When both a stored ChatGPT sign-in and an environment API key are available, an interactive scan asks which credential to use. JSON output, dry runs, CI, and other noninteractive scans never prompt and retain automatic API-key diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 5d23633e..820b8bbd 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -5793,6 +5793,23 @@ function scanFailureMessage( // errors can name the organization or project, which must not reach stderr or // the JSON error field. if (isLocalScanFailure(error)) return diagnosticValue(error); + const message = errorMessage(error); + const nativeRefreshRecovery = message.match( + /\b(?:your access token could not be refreshed because you have since logged out or signed in to another account\. Please sign in again\.|your authentication session could not be refreshed automatically\. Please log out and sign in again\.)/iu, + )?.[0]; + if (nativeRefreshRecovery !== undefined) return nativeRefreshRecovery; + if ( + /\byour access token could not be refreshed(?: because your refresh token (?:has expired|was already used|was revoked))?\. Please log out and sign in again\./iu.test( + message, + ) + ) { + return ( + "Codex Security's stored ChatGPT sign-in could not be refreshed. " + + "Codex may still need it to load workspace-managed policies when an API key is selected for model authentication. " + + "If the sign-in recently changed, check 'npx @openai/codex-security login status' and retry. " + + "Otherwise run 'npx @openai/codex-security logout', then 'npx @openai/codex-security login'." + ); + } switch (classifyConnectionFailure(error)) { case "unauthorized": if (authentication?.method === "aws_credentials") { @@ -5803,7 +5820,6 @@ function scanFailureMessage( } return authentication?.method === "api_key" ? `Authentication failed using ${authentication.source}. ` + - "Your ChatGPT sign-in was not used. " + "Retry with '--auth chatgpt' or provide a valid API key." : "Authentication failed using stored ChatGPT credentials. " + "Sign in again with 'codex-security login' or provide a valid API key."; diff --git a/sdk/typescript/tests-ts/cli-authentication.test.ts b/sdk/typescript/tests-ts/cli-authentication.test.ts index e88d3391..0d55872b 100644 --- a/sdk/typescript/tests-ts/cli-authentication.test.ts +++ b/sdk/typescript/tests-ts/cli-authentication.test.ts @@ -878,12 +878,82 @@ describe("CLI authentication", () => { expect(stderr.text()).toContain(expected); expect(stderr.text()).toContain(source); expect(stderr.text()).toContain("--auth chatgpt"); + expect(stderr.text()).not.toContain("ChatGPT sign-in was not used"); expect(stderr.text()).not.toContain("SYNTHETIC_SECRET"); expect(stderr.text()).not.toContain("org-private"); } } }); + test("replaces permanent stored sign-in refresh details with recovery steps", async () => { + for (const auth of ["chatgpt", "api-key"] as const) { + for (const detail of [ + "Your access token could not be refreshed.", + "Your access token could not be refreshed because your refresh token has expired.", + "Your access token could not be refreshed because your refresh token was already used.", + "Your access token could not be refreshed because your refresh token was revoked.", + ]) { + const stdout = capture(); + const stderr = capture(false); + const deps = dependencies({ + environment: { OPENAI_API_KEY: "sk-proj-SYNTHETIC_SECRET_123" }, + onRun: () => { + throw new CodexSecurityError( + `Codex Exec exited with code 1: Error: ${detail} Please log out and sign in again. PRIVATE_UPSTREAM_DETAIL`, + ); + }, + }); + + expect( + await main( + ["scan", ".", "--auth", auth, "--json"], + stdout.stream, + stderr.stream, + deps, + ), + ).toBe(2); + expect(stdout.text()).toBe(""); + expect(stderr.text()).toContain("workspace-managed policies"); + expect(stderr.text()).toContain( + "API key is selected for model authentication", + ); + expect(stderr.text()).toContain( + "npx @openai/codex-security login status", + ); + expect(stderr.text()).toContain( + "npx @openai/codex-security logout', then 'npx @openai/codex-security login", + ); + expect(stderr.text()).not.toContain("provide a valid API key"); + expect(stderr.text()).not.toContain("PRIVATE_UPSTREAM_DETAIL"); + } + } + }); + + test("leaves other sign-in recovery messages unchanged", async () => { + for (const message of [ + "Your access token could not be refreshed because you have since logged out or signed in to another account. Please sign in again.", + "Your authentication session could not be refreshed automatically. Please log out and sign in again.", + ]) { + const stdout = capture(); + const stderr = capture(false); + const deps = dependencies({ + onRun: () => { + throw new CodexSecurityError( + `Codex Exec exited with code 1: ${message} PRIVATE_UPSTREAM_DETAIL`, + ); + }, + }); + + expect( + await main(["scan", "--json"], stdout.stream, stderr.stream, deps), + ).toBe(2); + expect(stdout.text()).toBe(""); + expect(stderr.text()).toContain(`${message}\n`); + expect(stderr.text()).not.toContain("PRIVATE_UPSTREAM_DETAIL"); + expect(stderr.text()).not.toContain("npx @openai/codex-security logout"); + } + }); + test("prints the ChatGPT recovery hint on noninteractive scan output", async () => { const stdout = capture(); const stderr = capture(false);