Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9b3d7a6
fix(security,correctness): Phase 1 audit remediation
ndycode May 31, 2026
dc7141f
test(sandbox): redirect HOME/CODEX_HOME to a per-run temp dir (tests-…
ndycode May 31, 2026
8a79e06
fix(observability,resilience): Phase 2 audit remediation
ndycode May 31, 2026
431af87
fix(prompts): harden GitHub prompt fetch path (Phase 2c)
ndycode May 31, 2026
8574309
feat(ui): honor NO_COLOR / FORCE_COLOR / TTY for color output (ui-04)
ndycode May 31, 2026
6d3d6ff
fix(runtime): wire the routing mutex into the proxy (accounts-01/08)
ndycode May 31, 2026
244bc56
docs: fix version/override drift + guard it (docs-supplychain-03/04)
ndycode May 31, 2026
5472bdc
feat(recovery): quarantine + surface corrupt session files (recovery-10)
ndycode May 31, 2026
d1da64c
fix(config): retry transient FS locks on the config load path (config…
ndycode May 31, 2026
f7c4bc8
refactor(storage): unify fs-retry code sets (storage-07)
ndycode May 31, 2026
1abef7c
fix(prompts): SHA-256 cache integrity + atomic cache writes (prompts-…
ndycode May 31, 2026
e09d516
fix(recovery): guard sort, validate mutate paths, honest strip result
ndycode Jun 1, 2026
1652239
fix(quota): align capability key + add quota-window staleness escape
ndycode Jun 1, 2026
bf504ef
fix(ui): color-bleed, display-width alignment, glyph-mode bar (ui-01/…
ndycode Jun 1, 2026
e8cd946
fix(settings): single-source the refresh-interval bounds (settings-hu…
ndycode Jun 1, 2026
cb7e38c
fix(request): log deprecation/sunset headers on the error path too (r…
ndycode Jun 1, 2026
8a82f12
test(ci): deterministic property seed + coverage gate on PRs
ndycode Jun 1, 2026
95e9467
feat(cli): add --json to status/list (cli-manager-03)
ndycode Jun 1, 2026
e85ed32
fix(chatgpt-import): guard planOcChatgptSync against load/preview thr…
ndycode Jun 1, 2026
fca3eb5
fix(config): make load precedence symmetric with save (config-02)
ndycode Jun 1, 2026
c1b85c0
fix(local-bridge): allow auth to an auth-enabled runtime proxy (runti…
ndycode Jun 1, 2026
ada3e14
fix(scripts): wire the preuninstall lifecycle hook (install-scripts-02)
ndycode Jun 1, 2026
718f899
fix(scripts): detect unlisted vendor files in provenance check (insta…
ndycode Jun 1, 2026
6e04749
fix(review): address greptile + coderabbit findings on audit PR #499
ndycode Jun 1, 2026
7fc3e03
fix(review): second-pass review findings + deferred items (PR #499)
ndycode Jun 1, 2026
39e97da
fix(review): deterministic paths test + Vitest 4 pool config (PR #499)
ndycode Jun 1, 2026
f44f2b3
fix(security): close 5 audit findings from adversarial review (round 4)
ndycode Jun 1, 2026
8705de5
fix(recovery,prompts): address round-4 CodeRabbit findings (round 5)
ndycode Jun 1, 2026
6c864fc
test(rotation,status): add coverage requested in round-4 review (roun…
ndycode Jun 1, 2026
7810156
fix: harden the deferred audit items (round 7)
ndycode Jun 1, 2026
14e1140
fix: resolve round-7 CodeRabbit findings (round 8)
ndycode Jun 1, 2026
ef5bcb6
fix: resolve round-8 CodeRabbit findings (round 9)
ndycode Jun 2, 2026
19ca67b
fix: resolve round-9 CodeRabbit findings (round 10)
ndycode Jun 2, 2026
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
6 changes: 4 additions & 2 deletions .github/workflows/pr-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ jobs:
- name: Run ESLint
run: npm run lint

- name: Run tests
run: npm test
- name: Run tests with coverage
# tests-ci-05: run coverage on PRs so the 80% threshold gates the PR,
# not only the post-merge push-to-main run in ci.yml.
run: npm run coverage

- name: Build
run: npm run build
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Generated: 2026-04-25
Commit: a87e005
Branch: main
Package version: 2.0.1
Package version: 2.1.13-beta.2

## OVERVIEW

Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ The following are not treated as vulnerabilities in this repository:

Security override rationale (`package.json` -> `overrides`):

- `hono`: pinned to `4.12.14` to keep builds out of the vulnerable `4.12.0-4.12.1` range reported in `GHSA-xh87-mx6m-69f3` (authentication bypass advisory).
- `hono`: pinned to `4.12.18` to keep builds out of the vulnerable `4.12.0-4.12.1` range reported in `GHSA-xh87-mx6m-69f3` (authentication bypass advisory).
- `rollup`: pinned to `^4.59.0` to keep the Vite and Vitest transitive graph above the vulnerable `<4.59.0` range surfaced by `npm audit`.

Before release and after dependency changes:
Expand Down
33 changes: 32 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,44 @@ export default [
"@typescript-eslint/require-await": "warn",

// General best practices
"no-console": "off", // Allow console for CLI tool
// request-10: guard lib internals against stray console output that should
// go through the structured logger (which masks tokens/emails). The genuine
// CLI/UI output surface (commands, help, the CLI entrypoints, the injectable
// device-auth log sink) is re-allowed in the override block below, so this
// only fires on NEW leaks in non-CLI library code.
"no-console": "error",
"prefer-const": "error",
"no-var": "error",
"eqeqeq": ["error", "always"],
"no-duplicate-imports": "error",
},
},
{
// CLI / UI / human-output surface: console IS the intended output channel
// here (the tool prints to stdout/stderr for the user), so `no-console` stays
// off. Keep this list tight — library internals must use the logger.
files: [
"index.ts",
"lib/cli.ts",
"lib/codex-manager.ts",
"lib/codex-manager/**/*.ts",
"lib/auth/device-auth.ts",
],
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: "./tsconfig.json",
},
},
plugins: {
"@typescript-eslint": tseslint,
},
rules: {
"no-console": "off",
},
},
{
files: ["scripts/**/*.js", "scripts/**/*.mjs"],
languageOptions: {
Expand Down
33 changes: 32 additions & 1 deletion lib/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
getAccountIdentityKey,
getRuntimeAccountIdentityKey,
} from "./storage/identity.js";
import { getCircuitBreaker, resetAllCircuitBreakers } from "./circuit-breaker.js";
import { getCircuitBreaker, resetAllCircuitBreakers, removeCircuitBreaker } from "./circuit-breaker.js";
import {
getStoragePathState,
runWithStoragePathState,
Expand Down Expand Up @@ -1459,6 +1459,37 @@ export class AccountManager {
}

this.accounts.splice(idx, 1);
// Clear identity-keyed tracker + circuit state for the removed account so a
// later re-add of the same identity does not inherit stale health/token
// penalties or an open circuit (accounts-02). Done before the numeric-range
// clear below, which handles the index-shift of the *remaining* accounts.
//
// Tracker state is WRITTEN under getRuntimeTrackerKey (the pinned
// _runtimeTrackerKey), which is intentionally STABLE across later identity
// enrichment (see getRuntimeTrackerKey / updateFromAuth). The recomputed
// getRuntimeAccountIdentityKey can DIFFER from that stable key when an
// account was first tracked under an older key shape (e.g. "email:foo" or a
// numeric index) and then gained accountId/email fields. Clearing only the
// recomputed key would leave the real (stable) entries behind, so a re-add
// inherits stale penalties. Clear the stable tracker key first (required),
// then also clear the recomputed identity key when it differs to defensively
// cover any state written under the post-enrichment shape.
const removedTrackerKey = getRuntimeTrackerKey(account);
const healthTracker = getHealthTracker();
const tokenTracker = getTokenTracker();
healthTracker.clearAccountKey(removedTrackerKey);
tokenTracker.clearAccountKey(removedTrackerKey);
const removedIdentityKey = getRuntimeAccountIdentityKey(account);
if (
removedIdentityKey !== undefined &&
removedIdentityKey !== removedTrackerKey
) {
healthTracker.clearAccountKey(removedIdentityKey);
tokenTracker.clearAccountKey(removedIdentityKey);
}
if (typeof account.circuitKeyId === "string" && account.circuitKeyId) {
removeCircuitBreaker(account.circuitKeyId);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Clear numeric-keyed tracker state in the shifted range. After reindex,
// any refresh-only account that moved from N to N-1 must not inherit the
// stale health/token entries that used to belong to the old numeric slot.
Expand Down
25 changes: 25 additions & 0 deletions lib/auth/org-override.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Resolve the effective account-id override for a login, with the documented
* precedence: an explicit `login --org <id>` argument wins over the ambient
* CODEX_AUTH_ACCOUNT_ID env var, for that call only.
*
* This lives in its own internal module (not exported from the CLI entrypoint)
* so the concurrency contract — the launcher must NOT mutate process.env for the
* duration of a login, which raced on re-entry / reused test workers — can be
* unit-tested without widening the public surface of lib/codex-manager.ts.
*
* A blank/whitespace explicit org is treated as absent so an empty `--org ""`
* does not suppress the env fallback.
*
* @param explicitOrg - the value passed to `login --org`, if any
* @param env - environment to read CODEX_AUTH_ACCOUNT_ID from (injectable for tests)
* @returns the trimmed effective override, or null when neither source provides one
*/
export function resolveOrgOverride(
explicitOrg?: string,
env: NodeJS.ProcessEnv = process.env,
): string | null {
const explicit = explicitOrg?.trim();
const override = (explicit || env.CODEX_AUTH_ACCOUNT_ID || "").trim();
return override.length > 0 ? override : null;
}
9 changes: 9 additions & 0 deletions lib/circuit-breaker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,12 @@ export function resetAllCircuitBreakers(): void {
export function clearCircuitBreakers(): void {
circuitBreakers.clear();
}

/**
* Remove a single circuit breaker by key. Used when an account is removed so a
* later re-add of the same identity starts with a fresh (closed) circuit rather
* than inheriting an open one (accounts-02).
*/
export function removeCircuitBreaker(key: string): void {
circuitBreakers.delete(key);
}
71 changes: 21 additions & 50 deletions lib/codex-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
REDIRECT_URI,
} from "./auth/auth.js";
import { runDeviceAuthFlow } from "./auth/device-auth.js";
import { resolveOrgOverride } from "./auth/org-override.js";
import {
copyTextToClipboard,
isBrowserLaunchSuppressed,
Expand All @@ -37,6 +38,7 @@ import {
runBestCommand,
} from "./codex-manager/commands/best.js";
import { runAccountCommand } from "./codex-manager/commands/account.js";
import { ACCOUNT_MANAGER_COMMANDS } from "./codex-manager/account-manager-commands.js";
import { runBudgetCommand } from "./codex-manager/commands/budget.js";
import { runBridgeCommand } from "./codex-manager/commands/bridge.js";
import { runCheckCommand } from "./codex-manager/commands/check.js";
Expand Down Expand Up @@ -201,36 +203,6 @@ type TokenSuccessWithAccount = TokenSuccess & {
};
type PromptTone = "accent" | "success" | "warning" | "danger" | "muted";
const log = createLogger("codex-manager");
const ACCOUNT_MANAGER_COMMANDS = new Set([
"login",
"list",
"status",
"switch",
"unpin",
"workspace",
"best",
"check",
"features",
"usage",
"verify-flagged",
"verify",
"forecast",
"report",
"fix",
"doctor",
"uninstall",
"account",
"budget",
"bridge",
"integrations",
"models",
"monitor",
"rotation",
"why-selected",
"config",
"init-config",
"debug",
]);

interface ModelInspection {
requested: string;
Expand Down Expand Up @@ -1286,10 +1258,20 @@ async function syncCodexCliActiveSelectionIfDrifted(
}
}

/**
* Resolve the account-id selection for freshly-minted tokens.
*
* The org-override precedence (explicit `login --org` wins over the ambient
* CODEX_AUTH_ACCOUNT_ID env, for this call only) lives in the internal
* lib/auth/org-override.ts module so it can be unit-tested without exporting this
* CLI-internal function. Threading the org as a parameter avoids mutating
* process.env for the duration of a login, which raced on concurrent re-entry.
*/
function resolveAccountSelection(
tokens: TokenSuccess,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
orgOverride?: string,
): TokenSuccessWithAccount {
const override = (process.env.CODEX_AUTH_ACCOUNT_ID ?? "").trim();
const override = resolveOrgOverride(orgOverride);
if (override) {
return {
...tokens,
Expand Down Expand Up @@ -2790,25 +2772,13 @@ async function runAuthLogin(args: string[]): Promise<number> {
const loginOptions = parsedArgs.options;
// `--org <id>` binds this login to a specific workspace/org so the same
// email's personal vs business/team workspace can be registered on demand
// (issue #491). It reuses the CODEX_AUTH_ACCOUNT_ID override that every login
// resolver already honors. Scope it to this invocation and restore the prior
// value in a finally so a later login in the same process (menu re-entry, a
// reused test worker) is never silently bound to a stale org.
if (!loginOptions.org) {
return runAuthLoginFlow(loginOptions);
}
const previousAccountIdOverride = process.env.CODEX_AUTH_ACCOUNT_ID;
process.env.CODEX_AUTH_ACCOUNT_ID = loginOptions.org;
console.log(`Binding this login to workspace org id: ${loginOptions.org}`);
try {
return await runAuthLoginFlow(loginOptions);
} finally {
if (previousAccountIdOverride === undefined) {
delete process.env.CODEX_AUTH_ACCOUNT_ID;
} else {
process.env.CODEX_AUTH_ACCOUNT_ID = previousAccountIdOverride;
}
// (issue #491). The org is threaded explicitly into resolveAccountSelection
// (no process.env mutation), so concurrent re-entry (menu re-entry, a reused
// test worker) can never bind a login to a stale org via a shared global.
if (loginOptions.org) {
console.log(`Binding this login to workspace org id: ${loginOptions.org}`);
}
return runAuthLoginFlow(loginOptions);
}

async function runAuthLoginFlow(
Expand Down Expand Up @@ -3191,7 +3161,7 @@ async function runAuthLoginFlow(
return 1;
}

const resolved = resolveAccountSelection(tokenResult);
const resolved = resolveAccountSelection(tokenResult, loginOptions.org);
await persistAccountPool([resolved], false);
await syncSelectionToCodex(resolved);

Expand Down Expand Up @@ -3573,6 +3543,7 @@ export async function runCodexMultiAuthCli(rawArgs: string[]): Promise<number> {
.catch(() => null),
loadAppHelperStatus: readAppRuntimeHelperAccountSignal,
loadQuotaCache,
json: rest.includes("--json") || rest.includes("-j"),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
}
if (command === "switch") {
Expand Down
42 changes: 42 additions & 0 deletions lib/codex-manager/account-manager-commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Canonical set of subcommands routed to the account-manager dispatcher.
*
* Kept in a small internal module (rather than exported from the CLI entrypoint
* lib/codex-manager.ts) so both the dispatcher and the wrapper-routing alignment
* test (test/codex-routing.test.ts) consume the SAME source of truth — the test
* can assert AUTH_SUBCOMMANDS ⊇ ACCOUNT_MANAGER_COMMANDS without re-exporting a
* test-only implementation detail through the public CLI surface (cli-manager-01
* /02). This is an internal module, not part of the published package API.
*
* @internal
*/
export const ACCOUNT_MANAGER_COMMANDS = new Set([
"login",
"list",
"status",
"switch",
"unpin",
"workspace",
"best",
"check",
"features",
"usage",
"verify-flagged",
"verify",
"forecast",
"report",
"fix",
"doctor",
"uninstall",
"account",
"budget",
"bridge",
"integrations",
"models",
"monitor",
"rotation",
"why-selected",
"config",
"init-config",
"debug",
]);
Loading