Skip to content

feat: support multiple workspaces for the same email (#491)#493

Open
ndycode wants to merge 5 commits into
mainfrom
feat/issue-491-workspace-display
Open

feat: support multiple workspaces for the same email (#491)#493
ndycode wants to merge 5 commits into
mainfrom
feat/issue-491-workspace-display

Conversation

@ndycode
Copy link
Copy Markdown
Owner

@ndycode ndycode commented May 30, 2026

Summary

Closes #491.

A single OpenAI/Google account can belong to multiple ChatGPT workspaces
(personal Plus vs business/team), each a distinct quota pool keyed by its
org_id. This PR makes such workspaces persist, be visible, be switchable, and
be registrable on demand.

Root cause: the login path already captures every workspace from the token
(getAccountIdCandidates) into one account's workspaces[], and
promptAccountSelection lets the user pick which binds. But the strict
AccountMetadataV3Schema (z.object) did not declare workspaces /
currentWorkspaceIndex, so Zod stripped them on every load. Login wrote them to
disk; the next read wiped them. That is the reported "workspaceName always empty
even after login", and it also left list unable to distinguish two same-email
accounts.

Changes

Persistence + labels

  • lib/schemas.ts: add WorkspaceSchema and declare workspaces /
    currentWorkspaceIndex on AccountMetadataV3Schema so workspace tracking
    survives the load round-trip.
  • lib/accounts.ts: surface the active workspace name in formatAccountLabel
    (for example Account 1 ([Personal Plus], user@gmail.com, id:g-AAAA)),
    preserving every existing label format.

Visibility + switching

  • lib/accounts.ts: add formatWorkspaceLines (one line per workspace, active
    marked with *, disabled flagged).
  • lib/codex-manager/commands/status.ts: under status / list, list every
    workspace beneath an account that tracks more than one.
  • lib/codex-manager/commands/workspace.ts (new) workspace <account> [workspace]:
    list an account's workspaces, or set the active one and persist it. Wired into
    the command registry, dispatch, and help.

On-demand registration

  • lib/codex-manager/help.ts + lib/codex-manager.ts: add login --org <org_id>
    (and --org=<id>). It reuses the CODEX_AUTH_ACCOUNT_ID override that every
    login resolver already honors, so a specific workspace can be bound at login
    even when selection would otherwise pick the default org.

Tests

  • schema round-trip preservation and load regression, label disambiguation,
    formatWorkspaceLines, nine workspace-command cases, and --org parsing.

Example

2. Account 2 ([Personal Plus], user@gmail.com, id:g-AAAA) used 1m ago
   workspaces:
     * 1. [Personal Plus] id:g-AAAA (active)
     - 2. [GkTech Business] id:g-BBBB

codex-multi-auth workspace 2 2
Account 2 now using workspace 2: [GkTech Business] (id:g-BBBB).

codex-multi-auth login --org org-BBBB
Binding this login to workspace org id: org-BBBB

Risk notes

  • formatAccountLabel is behavior-preserving when no workspaces are tracked;
    all prior outputs are unchanged.
  • Schema change is additive and optional; no migration required.
  • Nested display renders only when an account tracks more than one workspace.
  • --org does not change the auth resolvers; it sets the existing override env
    var for the invocation, so all three login paths honor it without new wiring.

Verification

  • npm run typecheck - clean
  • npm run lint - clean
  • npm run build - clean
  • npm test - 4041 passed (269 files)

Follow-up to confirm

Whether a real two-workspace login returns both orgs in one token (then the
workspace command alone covers switching) or one at a time (then --org is
the path to add the second). Both are now supported; a sanitized accounts.json
from a real two-workspace account would confirm which path users hit.

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

fixes the root cause of workspace data loss (strict z.object stripping workspaces/currentWorkspaceIndex on every load) and builds the full workspace ux on top: persistent schema fields, label disambiguation for same-email accounts, status/list sub-display, a new workspace command for listing and switching, and login --org for on-demand workspace registration.

  • lib/schemas.ts: adds WorkspaceSchema and declares workspaces/currentWorkspaceIndex on AccountMetadataV3Schema — additive, no migration needed; currentWorkspaceIndex uses z.number() without int/min(0) constraints, which can cause split-brain display if a float or negative lands on disk.
  • lib/codex-manager.ts: --org uses a try/finally to scope CODEX_AUTH_ACCOUNT_ID to the invocation and restore it afterward — correct, but the finally-path restore has no vitest coverage.
  • lib/codex-manager/commands/workspace.ts: new command with full validation, disabled-workspace guard, and already-active no-op; inherits the TOCTOU/EBUSY concerns already flagged in the previous review round.

Confidence Score: 5/5

safe to merge; changes are additive and behavior-preserving for accounts without workspaces

the schema fix is a straightforward additive field declaration; the workspace command and --org flag are well-guarded; the try/finally env-restore pattern in codex-manager.ts is correct; remaining gaps are test coverage and a minor schema constraint

lib/schemas.ts (currentWorkspaceIndex constraint) and lib/codex-manager.ts (--org env-restore test coverage)

Important Files Changed

Filename Overview
lib/schemas.ts adds WorkspaceSchema and declares workspaces/currentWorkspaceIndex on AccountMetadataV3Schema — fixes the root-cause stripping bug; currentWorkspaceIndex uses z.number() without int/min(0) constraints
lib/accounts.ts adds activeWorkspaceName, refactors formatAccountLabel to segment-based building (behavior-preserving), and exports formatWorkspaceLines — logic correct, fallback to workspaces[0] on OOB index is safe
lib/codex-manager.ts adds --org flag with try/finally env-restore, wires workspace command into dispatch — env restore logic is correct but lacks test coverage for the finally path
lib/codex-manager/commands/workspace.ts new workspace command with list/switch logic, disabled-workspace guard, already-active no-op — input validation and save path look correct; TOCTOU and EBUSY coverage already called out in previous threads
lib/codex-manager/commands/status.ts adds workspace sub-list under accounts with more than one workspace; intentionally skips single-workspace accounts per PR spec
lib/codex-manager/help.ts adds --org parsing with --org=value and --org value forms, validates empty/flag-looking values, updates usage string
test/schemas.test.ts adds round-trip preservation test and missing-id rejection test for WorkspaceSchema — covers the root-cause regression directly
test/codex-manager-workspace-command.test.ts nine cases covering list, switch, disabled-workspace reject, no-op, and error paths — no EBUSY/EPERM save-retry test (already noted in previous thread)
test/accounts.test.ts adds label disambiguation, currentWorkspaceIndex tracking, workspace-tag dedup, empty/unnamed workspace, and formatWorkspaceLines tests
test/codex-manager-help.test.ts adds --org parse tests covering both syntax forms and missing-value error — no test for the runAuthLogin env-restore behavior itself

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[codex-multi-auth login --org org-X] --> B[parseAuthLoginArgs]
    B --> C{org present?}
    C -- yes --> D[save prev CODEX_AUTH_ACCOUNT_ID]
    D --> E[set env var to org-X]
    E --> F[runAuthLoginFlow]
    F --> G{success / throw}
    G --> H[finally: restore/delete env var]
    C -- no --> I[runAuthLoginFlow directly]

    J[codex-multi-auth workspace N] --> K[loadAccounts]
    K --> L{workspaces.length > 0?}
    L -- no --> M[log: no tracked workspaces]
    L -- yes --> N{workspace arg?}
    N -- no --> O[formatWorkspaceLines list]
    N -- yes --> P{valid index?}
    P -- no --> Q[error: out of range]
    P -- yes --> R{enabled?}
    R -- no --> S[error: disabled]
    R -- yes --> T{already active?}
    T -- yes --> U[no-op log]
    T -- no --> V[set currentWorkspaceIndex saveAccountsWithRetry]

    W[status / list] --> X[formatAccountLabel with workspaceName]
    X --> Y{workspaces.length > 1?}
    Y -- yes --> Z[formatWorkspaceLines sub-display]
Loading

Fix All in Codex

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
lib/schemas.ts:187
**`currentWorkspaceIndex` accepts floats and negatives from disk**

`z.number()` allows values like `1.5` or `-1` to survive the load round-trip. When a float lands on disk (e.g., via manual editing or a future code path), `formatWorkspaceLines` would show no `*` active marker (since `idx === 1.5` is always false), while `activeWorkspaceName` silently falls back to `workspaces[0]` via the `?? workspaces[0]` guard — split-brain between the label and the workspace list. `z.number().int().min(0)` would catch this on load and prevent the mismatch before it reaches any display path.

### Issue 2 of 2
lib/codex-manager.ts:2803-2811
**`--org` env-restore path has no vitest coverage**

The `try/finally` that deletes or restores `CODEX_AUTH_ACCOUNT_ID` is untested. two cases need coverage: (a) the var is deleted after a successful `--org` run when no prior value existed, and (b) a pre-existing value is restored after the login flow completes or throws. this is especially worth testing on windows, where env mutation across async boundaries can behave unexpectedly. without a test, a regression here would silently bind every subsequent login in the same process to the stale org.

Reviews (4): Last reviewed commit: "fix(login): scope --org override and res..." | Re-trigger Greptile

ndycode added 2 commits May 31, 2026 02:24
Support a single OpenAI/Google account that belongs to multiple ChatGPT
workspaces (personal Plus vs business/team), each a distinct quota pool
keyed by its org_id. The composite accountId+email matching already keeps
such entries separate, but the workspace metadata captured at login did
not survive a reload and was invisible in list/status output.

- schemas: add WorkspaceSchema and declare workspaces and
  currentWorkspaceIndex on AccountMetadataV3Schema. The strict z.object
  stripped these fields on every load, so login-captured workspaces
  vanished after one read/write round-trip. This is the root cause of the
  reported "workspaceName always empty even after login".
- accounts: surface the active workspace name in formatAccountLabel so two
  same-email accounts in different workspaces stay distinguishable, while
  preserving every existing label format.
- tests: schema round-trip preservation plus label disambiguation coverage.
Build on the workspace-persistence fix by making an email's multiple
workspaces visible and selectable, so personal Plus and business/team under
one Google account can be tracked and rotated between independently.

- accounts: add formatWorkspaceLines, one display line per workspace with the
  active one marked and disabled ones flagged.
- status/list: list every workspace beneath an account when it tracks more
  than one, so both stay visible at once.
- new "workspace <account> [workspace]" command: with an account index it
  lists that account's workspaces; with a workspace index it sets the active
  workspace and persists it. Wired into the command registry, dispatch, and
  help.
- tests: formatWorkspaceLines coverage and nine workspace-command cases.
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

📝 Walkthrough

Walkthrough

adds workspace listing and switching. includes a formatter (lib/accounts.ts:1802), a workspace subcommand implementing list/switch logic with index validation and persistence (lib/codex-manager/commands/workspace.ts:25-131), CLI/help integration and --org parsing (lib/codex-manager.ts:3593, lib/codex-manager/help.ts:83-105), and status output wiring (lib/codex-manager/commands/status.ts:246-253). tests added under test/*.

Changes

Workspace Selection Feature

Layer / File(s) Summary
workspace display formatting
lib/accounts.ts, test/accounts.test.ts
formatWorkspaceLines generates readable lines per workspace: marks active with *, non-active with -, annotates disabled status and truncated id, returns [] when no workspaces exist. tests added at test/accounts.test.ts:852-888 for active, disabled, and empty cases.
status output wiring
lib/codex-manager/commands/status.ts
imports formatWorkspaceLines and prints workspace lines for accounts with more than one workspace (lib/codex-manager/commands/status.ts:246-253).
workspace command implementation
lib/codex-manager/commands/workspace.ts, test/codex-manager-workspace-command.test.ts
runWorkspaceCommand parses a required 1-based account index, supports list mode (no workspace arg) and selection mode (workspace index validation, enabled check, no-op when selecting current), updates currentWorkspaceIndex, and persists via saveAccountsWithRetry. tests cover argument validation, listing output, switching and rejection cases (test/codex-manager-workspace-command.test.ts:49-167).
cli wiring and help parsing
lib/codex-manager.ts, lib/codex-manager/help.ts, test/codex-manager-help.test.ts
registers the workspace subcommand and dispatches to runWorkspaceCommand (lib/codex-manager.ts:3593). extends login parsing to support --org and temporarily bind process.env.CODEX_AUTH_ACCOUNT_ID during login (lib/codex-manager.ts:2791-2816, lib/codex-manager/help.ts:83-105). tests for --org parsing added (test/codex-manager-help.test.ts:31-60).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

review notes

  • missing regression tests: lib/accounts.ts:1802 formats indexed lines. add tests verifying numeric index rendering and the fallback "(unnamed)" when workspace.name is empty. current tests at test/accounts.test.ts:852-888 do not assert index formatting or unnamed fallback.
  • persistence failure paths: lib/codex-manager/commands/workspace.ts:115 calls saveAccountsWithRetry. there are no tests for save failures or rollbacks. add tests ensuring failed persist leaves storage consistent or surfaces errors (test/codex-manager-workspace-command.test.ts).
  • concurrency risk: lib/codex-manager/commands/workspace.ts:25-37 and :115 update and persist accounts without locking. concurrent processes could overwrite each other. consider file-level locking or an optimistic merge strategy.
  • windows / utf-8 edge case: id truncation and suffix formatting in lib/codex-manager/commands/workspace.ts:115 must handle multibyte/utf-8 and windows path characters safely. add tests that use wide/unicode ids to validate truncation behavior.
  • error clarity: disabled vs missing workspace use the same exit path in lib/codex-manager/commands/workspace.ts:88. consider distinct messages or exit codes to aid users.

possibly related PRs

  • ndycode/codex-multi-auth#136: touches lib/accounts.ts workspace shapes and may overlap with formatWorkspaceLines changes.
  • ndycode/codex-multi-auth#132: modifies codex auth login CLI parsing and manual-mode handling; intersects with the new --org parsing in lib/codex-manager/help.ts.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title follows conventional commits format with feat type, includes issue reference, and summary is under 72 chars in lowercase imperative.
Linked Issues check ✅ Passed PR fully addresses #491 objectives: persists workspaces via schema fix, surfaces them in label and status listing, provides workspace command for switching, and adds login --org flag for on-demand registration.
Out of Scope Changes check ✅ Passed All changes directly support workspace persistence, display, and switching; no unrelated refactoring or scope creep detected beyond #491 requirements.
Description check ✅ Passed PR description is comprehensive, structured, and addresses all template sections including summary, changes, validation checklist, docs checklist, risk assessment, and additional notes with greptile feedback.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-491-workspace-display
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/issue-491-workspace-display

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/codex-manager.ts`:
- Around line 3563-3569: Add a regression vitest that verifies the new
"workspace" dispatcher path calls runWorkspaceCommand with the forwarded args
and dependency bundle: mock or spy on runWorkspaceCommand and invoke the
dispatcher code path that sets command === "workspace" (simulate input like
"auth workspace ..."), then assert runWorkspaceCommand was called once with the
expected rest argument array and the deps object containing setStoragePath,
loadAccounts, saveAccounts; place the test alongside existing dispatcher tests
and reference lib/codex-manager.ts symbols runWorkspaceCommand and the
dispatcher function used to parse "auth workspace".

In `@lib/codex-manager/commands/status.ts`:
- Around line 246-253: The change to status output modifies workspace rendering;
add/update a vitest that verifies formatWorkspaceLines output is stable by
asserting the active marker, disabled annotation, and indentation are present in
the lines emitted to logInfo for an account with multiple workspaces;
specifically write a test that calls formatWorkspaceLines (or invokes the status
command path that calls it) with sample workspaces including an active and a
disabled workspace and asserts the returned lines contain the expected leading
indentation, active marker string, and "disabled" annotation so future changes
to formatWorkspaceLines or how status.ts logs workspaces will fail the test.

In `@lib/codex-manager/commands/workspace.ts`:
- Around line 125-128: The code accesses target.id.length and
target.id.slice(-6) directly which can throw if id is undefined; update the
idSuffix computation to use optional chaining and a safe default (e.g. const
idSuffix = target.id?.length ? target.id.slice(-6) : "") and adjust the logInfo
call to rely on that idSuffix (referencing target.id, idSuffix, logInfo and the
surrounding workspace formatting like formatWorkspaceLines if present) so
runtime undefined ids are handled without exceptions.

In `@test/codex-manager-workspace-command.test.ts`:
- Around line 98-148: Add a unit test that covers non-numeric workspace indices:
call runWorkspaceCommand with args ["1","xyz"], assert it returns 1, assert
deps.logError was called with "Invalid workspace index. Valid range: 1-2", and
ensure deps.saveAccounts was not called; this mirrors the existing out-of-range
test and verifies the Number.parseInt/validation logic in the workspace
selection code (see runWorkspaceCommand and the parseInt-based validation in
workspace.ts).
- Around line 50-73: Add a unit test that asserts non-numeric account indices
are handled with an explicit error: create a test in
test/codex-manager-workspace-command.test.ts that calls
runWorkspaceCommand(["abc"], deps) (use createDeps()), expects a return code of
1, and expects deps.logError to have been called with "Invalid account index:
abc"; this covers the edge case where Number.parseInt returns NaN and verifies
behavior implemented in workspaces logic (see Number.parseInt check in
lib/codex-manager/commands/workspace.ts and the runWorkspaceCommand function).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0566d4fb-37ae-4abe-9c52-47b644f04dc4

📥 Commits

Reviewing files that changed from the base of the PR and between eaf985a and 4055b92.

📒 Files selected for processing (7)
  • lib/accounts.ts
  • lib/codex-manager.ts
  • lib/codex-manager/commands/status.ts
  • lib/codex-manager/commands/workspace.ts
  • lib/codex-manager/help.ts
  • test/accounts.test.ts
  • test/codex-manager-workspace-command.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx,js,mjs}

📄 CodeRabbit inference engine (AGENTS.md)

Use ESM only ("type": "module"), Node >= 18

Files:

  • lib/codex-manager/help.ts
  • lib/codex-manager/commands/status.ts
  • lib/accounts.ts
  • lib/codex-manager.ts
  • test/accounts.test.ts
  • test/codex-manager-workspace-command.test.ts
  • lib/codex-manager/commands/workspace.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use as any, @ts-ignore, or @ts-expect-error TypeScript assertions

Files:

  • lib/codex-manager/help.ts
  • lib/codex-manager/commands/status.ts
  • lib/accounts.ts
  • lib/codex-manager.ts
  • test/accounts.test.ts
  • test/codex-manager-workspace-command.test.ts
  • lib/codex-manager/commands/workspace.ts
lib/**/*.ts

📄 CodeRabbit inference engine (lib/AGENTS.md)

lib/**/*.ts: All public exports should flow through lib/index.ts or documented package subpaths
Never import from dist/ in source tests or library code
Never suppress type errors

Files:

  • lib/codex-manager/help.ts
  • lib/codex-manager/commands/status.ts
  • lib/accounts.ts
  • lib/codex-manager.ts
  • lib/codex-manager/commands/workspace.ts
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/codex-manager/help.ts
  • lib/codex-manager/commands/status.ts
  • lib/accounts.ts
  • lib/codex-manager.ts
  • lib/codex-manager/commands/workspace.ts
**/lib/accounts.ts

📄 CodeRabbit inference engine (AGENTS.md)

Email dedup is case-insensitive via normalizeEmailKey() (trim + lowercase)

Files:

  • lib/accounts.ts
lib/accounts*.ts

📄 CodeRabbit inference engine (lib/AGENTS.md)

Account health is 0-100 and should be updated through the account manager APIs

Files:

  • lib/accounts.ts
{**/scripts/**/*.js,**/test/**/*.ts}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use bare recursive delete logic in Windows-sensitive scripts/tests without retry handling for transient EBUSY/EPERM/ENOTEMPTY errors

Files:

  • test/accounts.test.ts
  • test/codex-manager-workspace-command.test.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/AGENTS.md)

test/**/*.test.ts: Vitest globals (describe, it, expect) are enabled and should be used without explicit imports
Maintain 80% coverage threshold across statements, branches, functions, and lines
Use removeWithRetry for Windows filesystem cleanup instead of bare fs.rm to handle EBUSY/EPERM/ENOTEMPTY backoff
Use source files in tests, not compiled dist/ files; test the source directly
Do not skip tests without justification; include rationale if a test must be skipped
Relax ESLint rules for test files as specified in eslint.config.js

Files:

  • test/accounts.test.ts
  • test/codex-manager-workspace-command.test.ts
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/accounts.test.ts
  • test/codex-manager-workspace-command.test.ts
🔇 Additional comments (10)
lib/accounts.ts (1)

1809-1830: LGTM!

test/accounts.test.ts (1)

852-887: LGTM!

lib/codex-manager/commands/workspace.ts (4)

1-66: LGTM!


67-86: LGTM!


88-113: LGTM!


115-120: LGTM!

test/codex-manager-workspace-command.test.ts (1)

75-96: LGTM!

lib/codex-manager.ts (1)

91-91: LGTM!

Also applies to: 206-206

lib/codex-manager/commands/status.ts (1)

5-5: LGTM!

lib/codex-manager/help.ts (1)

15-15: LGTM!

Comment thread lib/codex-manager.ts
Comment thread lib/codex-manager/commands/status.ts
Comment thread lib/codex-manager/commands/workspace.ts Outdated
Comment thread test/codex-manager-workspace-command.test.ts
Comment thread test/codex-manager-workspace-command.test.ts
Comment thread lib/codex-manager/commands/workspace.ts Outdated
Comment on lines +48 to +56

describe("runWorkspaceCommand", () => {
it("errors when no accounts are configured", async () => {
const deps = createDeps({ loadAccounts: vi.fn(async () => null) });
const result = await runWorkspaceCommand(["1"], deps);
expect(result).toBe(1);
expect(deps.logError).toHaveBeenCalledWith("No accounts configured.");
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 missing ebusy/eperm save-retry coverage

saveAccountsWithRetry handles EBUSY/EPERM internally, but there's no test that verifies runWorkspaceCommand surfaces an unretryable error correctly. project convention (see unified-settings.test.ts) is to cover the EBUSY retry path for every new save path. worth adding a case that makes saveAccounts throw an EBUSY-coded error and asserts the rejection propagates (or is caught) as expected.

Prompt To Fix With AI
This is a comment left during a code review.
Path: test/codex-manager-workspace-command.test.ts
Line: 48-56

Comment:
**missing ebusy/eperm save-retry coverage**

`saveAccountsWithRetry` handles `EBUSY`/`EPERM` internally, but there's no test that verifies `runWorkspaceCommand` surfaces an unretryable error correctly. project convention (see `unified-settings.test.ts`) is to cover the EBUSY retry path for every new save path. worth adding a case that makes `saveAccounts` throw an `EBUSY`-coded error and asserts the rejection propagates (or is caught) as expected.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex

Comment on lines +122 to +123
account.currentWorkspaceIndex = workspaceIndex;
await saveAccountsWithRetry(storage, deps.saveAccounts);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 concurrent write race (TOCTOU)

this follows the same load-mutate-save pattern as switch/unpin, but worth calling out explicitly: the runtime-rotation-proxy writes to the same accounts file (e.g. disableCurrentWorkspace, lastUsed updates). if the proxy writes between loadAccounts() and saveAccountsWithRetry() here, this save silently overwrites those changes — most critically, a workspace enabled: false written by the proxy could be restored to true. no file-level lock or re-read-before-write guards against this. low probability in practice, but concurrency tests for this path are absent.

Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/codex-manager/commands/workspace.ts
Line: 122-123

Comment:
**concurrent write race (TOCTOU)**

this follows the same load-mutate-save pattern as `switch`/`unpin`, but worth calling out explicitly: the runtime-rotation-proxy writes to the same accounts file (e.g. `disableCurrentWorkspace`, `lastUsed` updates). if the proxy writes between `loadAccounts()` and `saveAccountsWithRetry()` here, this save silently overwrites those changes — most critically, a workspace `enabled: false` written by the proxy could be restored to `true`. no file-level lock or re-read-before-write guards against this. low probability in practice, but concurrency tests for this path are absent.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Let a user register a specific workspace for an email that belongs to more
than one (personal Plus vs business/team). `--org <org_id>` reuses the
CODEX_AUTH_ACCOUNT_ID override that every login resolver already honors, so the
chosen org is bound for that login and a second workspace can be added on
demand instead of always resolving to the default one.

- help: parse --org <id> and --org=<id> (rejects a missing value), document it
  in the login usage line.
- codex-manager: runAuthLogin sets the override env var from --org for the
  invocation, taking precedence over any inherited value.
- tests: --org parsing and missing-value cases.
@ndycode ndycode changed the title feat(workspace): show and switch per-account workspaces (#491) feat: support multiple workspaces for the same email (#491) May 30, 2026
@ndycode ndycode changed the base branch from feat/issue-491-persist-surface-workspaces to main May 30, 2026 18:49
Comment thread lib/codex-manager.ts Outdated
Comment on lines +2792 to +2795
if (loginOptions.org) {
process.env.CODEX_AUTH_ACCOUNT_ID = loginOptions.org;
console.log(`Binding this login to workspace org id: ${loginOptions.org}`);
}
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 CODEX_AUTH_ACCOUNT_ID never restored after --org login

process.env.CODEX_AUTH_ACCOUNT_ID is set here but never deleted or restored, so it persists for the entire process lifetime. resolveAccountSelection reads it on every subsequent call — if runAuthLogin is invoked again in the same process (e.g. from an existing vitest worker that exercises the login path, or any future interactive/menu flow), that second login silently binds to the org from the first call even without --org, causing a token-to-wrong-org mismatch. on Windows, env var mutation can also behave unexpectedly across async boundaries. save the original value before setting it and restore (or delete) it once runAuthLogin returns.

Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/codex-manager.ts
Line: 2792-2795

Comment:
**`CODEX_AUTH_ACCOUNT_ID` never restored after `--org` login**

`process.env.CODEX_AUTH_ACCOUNT_ID` is set here but never deleted or restored, so it persists for the entire process lifetime. `resolveAccountSelection` reads it on every subsequent call — if `runAuthLogin` is invoked again in the same process (e.g. from an existing vitest worker that exercises the login path, or any future interactive/menu flow), that second login silently binds to the `org` from the first call even without `--org`, causing a token-to-wrong-org mismatch. on Windows, env var mutation can also behave unexpectedly across async boundaries. save the original value before setting it and restore (or delete) it once `runAuthLogin` returns.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 7be686a. runAuthLogin is now a thin wrapper that saves the prior CODEX_AUTH_ACCOUNT_ID, sets it only for the org-bound login, and restores or deletes it in a finally, delegating the unchanged flow to runAuthLoginFlow. A later login in the same process or a reused test worker no longer inherits a stale org.

…dices (#491)

Address review feedback on the workspace command.

- guard `target.id` with optional chaining when building the id suffix, matching
  formatWorkspaceLines, so non-conforming on-disk data cannot throw.
- add tests for non-numeric account and workspace indices.
@ndycode
Copy link
Copy Markdown
Owner Author

ndycode commented May 30, 2026

Addressed review feedback in 6fff40b:

Fixed

  • Guarded target.id in workspace.ts with optional chaining (matches formatWorkspaceLines), so non-conforming on-disk data cannot throw. (CodeRabbit, Greptile P2)
  • Added tests for non-numeric account index and non-numeric workspace index. (CodeRabbit)

Intentionally not changed, with reasons

  • Workspace status-render regression test: the rendered contract (active marker, disabled annotation, indent) is already locked by the formatWorkspaceLines unit tests in test/accounts.test.ts; status.ts only forwards those lines to logInfo.
  • Dispatcher regression test for the workspace route: the registration mirrors the existing switch/unpin routes, which are covered by per-command unit tests rather than dispatcher tests; kept consistent with that precedent.
  • TOCTOU on load-mutate-save: matches the established unpin pattern for a manual, interactive CLI mutation; lost-update risk on currentWorkspaceIndex is negligible in that context.
  • EBUSY/EPERM retry coverage: that behavior belongs to the shared saveAccountsWithRetry helper, which is out of scope for this command and exercised elsewhere.

Gates after the change: typecheck, lint, build clean; npm test 4043 passed (269 files).

The --org flag set process.env.CODEX_AUTH_ACCOUNT_ID without restoring it, so it
leaked for the whole process: a later login in the same invocation (menu
re-entry) or a reused test worker would silently bind to the stale org.

Split runAuthLogin into a thin wrapper that saves the prior override, sets it
for the org-bound login, and restores (or deletes) it in a finally, delegating
the unchanged flow to runAuthLoginFlow.
@ndycode
Copy link
Copy Markdown
Owner Author

ndycode commented May 30, 2026

@coderabbitai resolve

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

✅ Actions performed

Comments resolved and changes approved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature] Support registering multiple workspaces for the same email (personal + business/team under one Google account)

1 participant