Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,8 @@ $env:OPENAI_API_KEY = "<your-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
Expand All @@ -164,7 +163,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
Expand Down
13 changes: 12 additions & 1 deletion sdk/typescript/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5740,6 +5740,18 @@ 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);
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(
errorMessage(error),
)
Comment on lines +5744 to +5746

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Sanitize every recognized refresh-error variant

When either alternate refresh message covered by the new test (because you have since logged out... or authentication session could not be refreshed automatically) arrives with trailing upstream detail, this regex misses it, classification remains unknown, and diagnosticValue emits the entire wrapper to stderr. That permits organization, project, or credential-bearing suffixes to escape through the same boundary this branch sanitizes for the four permanent variants; preserve the native recovery advice by extracting/replacing the complete upstream error rather than passing the wrapper through.

AGENTS.md reference: sdk/typescript/AGENTS.md:L8-L12

Useful? React with 👍 / 👎.

) {
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") {
Expand All @@ -5750,7 +5762,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.";
Expand Down
67 changes: 67 additions & 0 deletions sdk/typescript/tests-ts/cli-authentication.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -878,12 +878,79 @@ 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(message);
},
});

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("npx @openai/codex-security logout");
}
});

test("prints the ChatGPT recovery hint on noninteractive scan output", async () => {
const stdout = capture();
const stderr = capture(false);
Expand Down
Loading