Repository navigation
feat: webui codex OAuth device-flow login with multi-account support - #686
Conversation
- SettingsPanel: new CodexAccountsCard listing connected accounts (label/email/plan badge) with confirm+disconnect, add-account device flow (label input, verification link, click-to-copy user code, 3s polling until authorized/error/expired, retry), in-flight flow cancel, pending-flow recovery on reload, and a not-supported note when the server lacks the /oauth/* endpoints (404). - Header: append connected codex:<label> accounts to the provider dropdown (static list on fetch failure); codex providers get gpt-5.5/5.4/5.2 datalist presets instead of a live model fetch. - lib/api.ts: 6 oauth client methods with defensive payload parsing; lib/types.ts: CodexFlow/CodexFlowStart/OAuthAccount types.
- oauth/store.py: default_auth_path() with CODING_AGENT_OAUTH_AUTH_PATH env override; OAuthStore() and oauth CLI resolve through it - server/oauth_flows.py: in-memory flow registry + background poll tasks (pending/authorized/error/expired/cancelled, 10min TTL); label from explicit request or id_token (email first, account-id short code, -2/-3 suffix on conflict); local exception_error_message helper (stands in for unmerged #685) - server/http_server.py: POST /oauth/codex/start, GET /oauth/codex/flows{,/{id}}, POST /oauth/codex/flows/{id}/cancel, GET /oauth/accounts, DELETE /oauth/accounts/{key} (local delete, no remote revoke); 400 at session create for unconnected codex:<label> - plugins/llm_provider.py: codex:<label> routing — StoreBackedCodex- TokenSource parameterized by provider key, refresh writes back same key - server/schemas.py: CreateSessionRequest.provider accepts ProviderName literals plus codex:<label> (^[a-z0-9][a-z0-9-]{0,30}$) - tests: tests/ui/test_oauth_flows.py (mocked CodexOAuthClient), plugin codex:* routing tests, store env-path tests - docs: web-client-api-contract.md gains the oauth endpoints section
Review P2: the flow poll catch swallowed every error, so a flow the server forgot (restart cleared the in-memory registry, TTL prune) or a failing auth path was retried every 3s until page close. Now a 404 marks the flow expired with a server-lost note, and other errors give up after 5 consecutive failures. Expired rows render flow.error when present.
📝 WalkthroughWalkthroughThis change adds Codex OAuth device-flow APIs, persistent multi-account routing, labeled session providers, and WebUI account management. It also adds flow recovery, cancellation, account deletion, dynamic provider selection, OAuth path overrides, and comprehensive server and frontend tests. ChangesCodex OAuth multi-account flow
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant CodexAccountsCard
participant AgentClient
participant HTTPServer
participant CodexOAuthFlowManager
participant OAuthStore
User->>CodexAccountsCard: Add Codex account
CodexAccountsCard->>AgentClient: startCodexOAuth(label)
AgentClient->>HTTPServer: POST /oauth/codex/start
HTTPServer->>CodexOAuthFlowManager: start device flow
CodexOAuthFlowManager-->>HTTPServer: return verification data
HTTPServer-->>AgentClient: return flow start response
AgentClient-->>CodexAccountsCard: display verification code
CodexAccountsCard->>AgentClient: poll flow status
AgentClient->>HTTPServer: GET /oauth/codex/flows/{flow_id}
CodexOAuthFlowManager->>OAuthStore: persist authorized account
HTTPServer-->>AgentClient: return authorized flow
AgentClient-->>CodexAccountsCard: refresh account list
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
coding-agent/src/coding_agent/server/oauth_flows.py (1)
231-239: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winPending flows are never pruned.
_pruneremoves only flows in_TERMINAL_STATES. A flow that stayspendingpastexpires_atremains in_flowsforever. If the background poll task hangs or the process keeps receiving start requests, the registry grows without bound. Consider marking a pending flow pastexpires_atasexpiredand cancelling its task during the prune.♻️ Proposed change
def _prune(self) -> None: now = datetime.now(UTC) + for flow in self._flows.values(): + if flow.state == "pending" and now > flow.expires_at: + flow.state = "expired" + if flow.task is not None: + flow.task.cancel() stale = [ flow_id for flow_id, flow in self._flows.items() if flow.state in _TERMINAL_STATES and now > flow.expires_at ]🤖 Prompt for 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. In `@coding-agent/src/coding_agent/server/oauth_flows.py` around lines 231 - 239, Update OAuthFlowRegistry._prune to handle pending flows whose expires_at has passed: mark each such flow as expired, cancel its associated task, and remove it from _flows while preserving the existing terminal-flow pruning behavior.coding-agent/tests/coding_agent/plugins/test_llm_provider.py (1)
90-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winClear
CODING_AGENT_OAUTH_AUTH_PATHin these tests.
default_auth_path()now prefers theCODING_AGENT_OAUTH_AUTH_PATHenvironment variable overDEFAULT_AUTH_PATH. If that variable is set in the test environment, patchingDEFAULT_AUTH_PATHhas no effect, andOAuthStore()reads the deployment auth file instead oftmp_path. Delete the variable to keep the tests isolated.♻️ Proposed change
def test_codex_label_provider_requires_connected_account( self, monkeypatch, tmp_path ): import coding_agent.oauth.store as store_module + monkeypatch.delenv("CODING_AGENT_OAUTH_AUTH_PATH", raising=False) monkeypatch.setattr(store_module, "DEFAULT_AUTH_PATH", tmp_path / "auth.json")auth_path = tmp_path / "auth.json" + monkeypatch.delenv("CODING_AGENT_OAUTH_AUTH_PATH", raising=False) monkeypatch.setattr(store_module, "DEFAULT_AUTH_PATH", auth_path)🤖 Prompt for 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. In `@coding-agent/tests/coding_agent/plugins/test_llm_provider.py` around lines 90 - 127, Clear the CODING_AGENT_OAUTH_AUTH_PATH environment variable in both Codex provider tests before patching DEFAULT_AUTH_PATH, using the existing monkeypatch fixture. Keep the temporary auth path setup and provider assertions unchanged so OAuthStore reads only from tmp_path.coding-agent/webui/app/src/components/CodexAccountsCard.tsx (2)
63-114: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueReduce poll effect churn and prune the failure map.
The effect depends on the whole
flowsarray. Each poll response builds a new array, so the interval is cleared and recreated on every tick and on every unrelated flow update. Keying the effect on the pending flow ids keeps one stable interval. Also deletepollFailuresentries for flows that are no longer pending, because dismissed and retried flow ids stay in the map for the life of the card.♻️ Proposed refactor
- useEffect(() => { - if (unsupported) return; - const pending = flows.filter((f) => f.state === "pending"); - if (pending.length === 0) return; + const pendingIds = flows + .filter((f) => f.state === "pending") + .map((f) => f.flow_id) + .join(","); + + useEffect(() => { + if (unsupported) return; + const pending = pendingIds ? pendingIds.split(",") : []; + if (pending.length === 0) return; + for (const id of [...pollFailures.current.keys()]) { + if (!pending.includes(id)) pollFailures.current.delete(id); + } const timer = setInterval(() => { - for (const flow of pending) { + for (const flowId of pending) { client - .getCodexFlow(flow.flow_id) + .getCodexFlow(flowId)Update the remaining
flow.flow_idreferences in the handlers toflowId, and change the dependency list to[client, pendingIds, pollMs, unsupported, refreshAccounts].🤖 Prompt for 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. In `@coding-agent/webui/app/src/components/CodexAccountsCard.tsx` around lines 63 - 114, Update the polling useEffect to derive a stable pendingIds value from pending flow IDs and use it for interval iteration, rather than depending on the entire flows array; update handler references from flow.flow_id to flowId. Prune pollFailures entries for IDs no longer pending, and change the dependency list to [client, pendingIds, pollMs, unsupported, refreshAccounts] so unrelated flow updates do not recreate the interval.
165-170: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueHandle a rejected clipboard write.
navigator.clipboard.writeTextrejects in an insecure context or when the permission is denied. The promise has no.catch, so the rejection is unhandled and the user sees no feedback.🛡️ Proposed fix
const copyCode = (flowId: string, code: string) => { - void navigator.clipboard?.writeText(code).then(() => { - setCopiedFlow(flowId); - setTimeout(() => setCopiedFlow((cur) => (cur === flowId ? null : cur)), 1500); - }); + void navigator.clipboard + ?.writeText(code) + .then(() => { + setCopiedFlow(flowId); + setTimeout(() => setCopiedFlow((cur) => (cur === flowId ? null : cur)), 1500); + }) + .catch((e) => setNotice({ kind: "error", text: `copy failed: ${msg(e)}` })); };🤖 Prompt for 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. In `@coding-agent/webui/app/src/components/CodexAccountsCard.tsx` around lines 165 - 170, Update the copyCode function to handle rejected navigator.clipboard.writeText promises by adding failure handling that prevents an unhandled rejection and provides appropriate user feedback; keep setCopiedFlow updates restricted to successful writes.coding-agent/webui/app/src/lib/types.ts (1)
267-273: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueKeep literal completion in
CodexFlowState.
| stringwidensCodexFlowStatetostring, so editors stop suggesting"pending","authorized","error","expired", and"cancelled". Use a non-reducing string wildcard if unknown server states must be accepted.♻️ Proposed refactor
export type CodexFlowState = | "pending" | "authorized" | "error" | "expired" | "cancelled" - | string; + // eslint-disable-next-line `@typescript-eslint/ban-types` + | (string & {});🤖 Prompt for 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. In `@coding-agent/webui/app/src/lib/types.ts` around lines 267 - 273, Update the CodexFlowState type to preserve editor suggestions for the listed literal states while still accepting unknown server values. Replace the widening | string member with a non-reducing string wildcard type pattern, keeping the existing completion literals unchanged.
🤖 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 `@coding-agent/docs/superpowers/specs/2026-07-31-webui-codex-oauth-design.md`:
- Around line 37-40: Escape the pipe characters inside the response cells for
the GET flow-status and GET accounts rows so Markdown keeps the union types and
provider values within their table columns. Preserve the documented endpoint
paths and response structures unchanged.
In `@coding-agent/src/coding_agent/server/http_server.py`:
- Around line 7529-7535: Update the deletion log in the OAuth account deletion
handler around codex_oauth_flow_manager.delete_account to avoid including
provider_key or any derived user identifier; log only a generic account-deleted
event while preserving the existing deletion response and 404 behavior.
In `@coding-agent/src/coding_agent/server/oauth_flows.py`:
- Around line 212-229: Update the authorization flow around _allocate_label and
set_provider so label allocation and provider-record persistence occur within a
single OAuthStore.update read-modify-write transaction. Perform the conflict
check and candidate selection against the transaction’s current providers, store
the record under the chosen codex label, and assign flow.account_label
consistently, eliminating the separate _store.load followed by
_store.set_provider race.
In `@coding-agent/webui/app/src/components/Header.tsx`:
- Around line 64-81: Lift OAuth account state and a refreshOAuthAccounts
callback into App.tsx, pass them to Header, SettingsPanel, and
CodexAccountsCard, and invoke the refresh after successful authorization and
disconnect. In Header.tsx, replace the local mount-only listOAuthAccounts fetch
with the lifted account list so selectors update immediately. In
SettingsPanel.tsx, extend providerOptions with connected codex:<label> keys;
update coding-agent/webui/app/src/components/Header.tsx:64-81 and
coding-agent/webui/app/src/components/SettingsPanel.tsx:79-81 accordingly.
---
Nitpick comments:
In `@coding-agent/src/coding_agent/server/oauth_flows.py`:
- Around line 231-239: Update OAuthFlowRegistry._prune to handle pending flows
whose expires_at has passed: mark each such flow as expired, cancel its
associated task, and remove it from _flows while preserving the existing
terminal-flow pruning behavior.
In `@coding-agent/tests/coding_agent/plugins/test_llm_provider.py`:
- Around line 90-127: Clear the CODING_AGENT_OAUTH_AUTH_PATH environment
variable in both Codex provider tests before patching DEFAULT_AUTH_PATH, using
the existing monkeypatch fixture. Keep the temporary auth path setup and
provider assertions unchanged so OAuthStore reads only from tmp_path.
In `@coding-agent/webui/app/src/components/CodexAccountsCard.tsx`:
- Around line 63-114: Update the polling useEffect to derive a stable pendingIds
value from pending flow IDs and use it for interval iteration, rather than
depending on the entire flows array; update handler references from flow.flow_id
to flowId. Prune pollFailures entries for IDs no longer pending, and change the
dependency list to [client, pendingIds, pollMs, unsupported, refreshAccounts] so
unrelated flow updates do not recreate the interval.
- Around line 165-170: Update the copyCode function to handle rejected
navigator.clipboard.writeText promises by adding failure handling that prevents
an unhandled rejection and provides appropriate user feedback; keep
setCopiedFlow updates restricted to successful writes.
In `@coding-agent/webui/app/src/lib/types.ts`:
- Around line 267-273: Update the CodexFlowState type to preserve editor
suggestions for the listed literal states while still accepting unknown server
values. Replace the widening | string member with a non-reducing string wildcard
type pattern, keeping the existing completion literals unchanged.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e1a2eae6-6ab3-401e-a3b3-9520dbcb4840
📒 Files selected for processing (20)
coding-agent/docs/superpowers/specs/2026-07-31-webui-codex-oauth-design.mdcoding-agent/docs/web-client-api-contract.mdcoding-agent/src/coding_agent/oauth/cli.pycoding-agent/src/coding_agent/oauth/store.pycoding-agent/src/coding_agent/plugins/llm_provider.pycoding-agent/src/coding_agent/server/http_server.pycoding-agent/src/coding_agent/server/oauth_flows.pycoding-agent/src/coding_agent/server/schemas.pycoding-agent/tests/coding_agent/plugins/test_llm_provider.pycoding-agent/tests/coding_agent/test_codex_oauth.pycoding-agent/tests/ui/test_oauth_flows.pycoding-agent/webui/app/src/App.tsxcoding-agent/webui/app/src/components/CodexAccountsCard.test.tsxcoding-agent/webui/app/src/components/CodexAccountsCard.tsxcoding-agent/webui/app/src/components/Header.test.tsxcoding-agent/webui/app/src/components/Header.tsxcoding-agent/webui/app/src/components/SettingsPanel.test.tsxcoding-agent/webui/app/src/components/SettingsPanel.tsxcoding-agent/webui/app/src/lib/api.tscoding-agent/webui/app/src/lib/types.ts
设计
Spec:
docs/superpowers/specs/2026-07-31-webui-codex-oauth-design.md(ADR-lite,关键决策记录在此):codex+codex:<label>并存,存储层零迁移codex:<label>,插件按完整 key 取记录/刷新;未连接的 key 建会话即 400CODING_AGENT_OAUTH_AUTH_PATHenv 覆盖,SRE 侧指到数据 PVC(后续 SRE PR)--revoke)端点
POST /oauth/codex/start、GET /oauth/codex/flows[/{id}]、POST .../cancel、GET /oauth/accounts、DELETE /oauth/accounts/{key},契约文档已同步。前端
Settings 面板 Codex 账号卡片(列表/添加/断开/轮询恢复/404 降级),Header provider 下拉动态列出已连接账号(codex 系用 gpt-5.5/5.4/5.2 预设)。
质量
Summary by CodeRabbit
新功能
文档
测试