Skip to content

feat: webui codex OAuth device-flow login with multi-account support - #686

Merged
delete-cloud merged 7 commits into
mainfrom
feat/webui-codex-oauth
Jul 31, 2026
Merged

delete-cloud merged 7 commits into
mainfrom
feat/webui-codex-oauth

Conversation

@delete-cloud

@delete-cloud delete-cloud commented Jul 31, 2026 •

Copy link
Copy Markdown
Owner

设计

Spec: docs/superpowers/specs/2026-07-31-webui-codex-oauth-design.md(ADR-lite,关键决策记录在此):

  • 只做 codex device flow(原生支持 headless)
  • 多账号 = 复合 provider key:codex + codex:<label> 并存,存储层零迁移
  • 账号路由:建会话 provider 可传 codex:<label>,插件按完整 key 取记录/刷新;未连接的 key 建会话即 400
  • 持久化:CODING_AGENT_OAUTH_AUTH_PATH env 覆盖,SRE 侧指到数据 PVC(后续 SRE PR)
  • 断开 = 只删本地记录,不做远端 revoke(CLI 有 --revoke)
  • 多并发登录流程:flow_id 独立管理,可查可取消,无互斥

端点

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 预设)。

质量

  • 评审一轮:1 P2(轮询吞错无限重试,已修)9 P3
  • 服务端 67 + 前端 100 测试全绿;契约文档同步

Summary by CodeRabbit

  • 新功能

    • 支持通过设备授权方式登录 Codex,并管理多个已连接账号。
    • 设置页面新增账号查看、添加、恢复、取消和断开功能。
    • 新建会话时可选择指定的 Codex 账号。
    • 支持登录流程状态查询、过期处理及错误提示。
    • 支持自定义 OAuth 凭据存储位置。
  • 文档

    • 补充 OAuth 登录、多账号管理及会话账号选择的 API 说明。
  • 测试

    • 覆盖登录流程、账号管理、会话路由及前端交互场景。

- 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.
@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Codex OAuth multi-account flow

Layer / File(s) Summary
Provider routing and OAuth storage
coding-agent/src/coding_agent/oauth/*, coding-agent/src/coding_agent/plugins/llm_provider.py, coding-agent/src/coding_agent/server/schemas.py, coding-agent/tests/coding_agent/*
Provider validation accepts codex:<label>. OAuth storage resolves CODING_AGENT_OAUTH_AUTH_PATH. Token acquisition and refresh use the selected provider key.
OAuth flow manager and HTTP API
coding-agent/src/coding_agent/server/oauth_flows.py, coding-agent/src/coding_agent/server/http_server.py, coding-agent/src/coding_agent/server/schemas.py, coding-agent/tests/ui/test_oauth_flows.py, coding-agent/docs/web-client-api-contract.md
The server manages concurrent device flows, TTL, polling, token exchange, account labeling, persistence, cancellation, listing, and local deletion. The HTTP API exposes flow and account operations and validates disconnected providers.
WebUI account management and selection
coding-agent/webui/app/src/lib/*, coding-agent/webui/app/src/components/*, coding-agent/webui/app/src/App.tsx
Settings adds account management with flow recovery, polling, cancellation, retry, and disconnection. Header adds dynamic Codex providers and static model presets. Client methods normalize OAuth responses and encode provider keys.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.40% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the PR's main change: Web UI Codex OAuth device-flow login with multi-account support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/webui-codex-oauth

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (5)
coding-agent/src/coding_agent/server/oauth_flows.py (1)

231-239: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Pending flows are never pruned.

_prune removes only flows in _TERMINAL_STATES. A flow that stays pending past expires_at remains in _flows forever. If the background poll task hangs or the process keeps receiving start requests, the registry grows without bound. Consider marking a pending flow past expires_at as expired and 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 win

Clear CODING_AGENT_OAUTH_AUTH_PATH in these tests.

default_auth_path() now prefers the CODING_AGENT_OAUTH_AUTH_PATH environment variable over DEFAULT_AUTH_PATH. If that variable is set in the test environment, patching DEFAULT_AUTH_PATH has no effect, and OAuthStore() reads the deployment auth file instead of tmp_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 value

Reduce poll effect churn and prune the failure map.

The effect depends on the whole flows array. 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 delete pollFailures entries 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_id references in the handlers to flowId, 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 value

Handle a rejected clipboard write.

navigator.clipboard.writeText rejects 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 value

Keep literal completion in CodexFlowState.

| string widens CodexFlowState to string, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 180d03d and c8fa2ef.

📒 Files selected for processing (20)
  • coding-agent/docs/superpowers/specs/2026-07-31-webui-codex-oauth-design.md
  • coding-agent/docs/web-client-api-contract.md
  • coding-agent/src/coding_agent/oauth/cli.py
  • coding-agent/src/coding_agent/oauth/store.py
  • coding-agent/src/coding_agent/plugins/llm_provider.py
  • coding-agent/src/coding_agent/server/http_server.py
  • coding-agent/src/coding_agent/server/oauth_flows.py
  • coding-agent/src/coding_agent/server/schemas.py
  • coding-agent/tests/coding_agent/plugins/test_llm_provider.py
  • coding-agent/tests/coding_agent/test_codex_oauth.py
  • coding-agent/tests/ui/test_oauth_flows.py
  • coding-agent/webui/app/src/App.tsx
  • coding-agent/webui/app/src/components/CodexAccountsCard.test.tsx
  • coding-agent/webui/app/src/components/CodexAccountsCard.tsx
  • coding-agent/webui/app/src/components/Header.test.tsx
  • coding-agent/webui/app/src/components/Header.tsx
  • coding-agent/webui/app/src/components/SettingsPanel.test.tsx
  • coding-agent/webui/app/src/components/SettingsPanel.tsx
  • coding-agent/webui/app/src/lib/api.ts
  • coding-agent/webui/app/src/lib/types.ts

Comment thread coding-agent/src/coding_agent/server/http_server.py
Comment thread coding-agent/src/coding_agent/server/oauth_flows.py
Comment thread coding-agent/webui/app/src/components/Header.tsx
@delete-cloud
delete-cloud merged commit 34c0dc5 into main Jul 31, 2026
2 checks passed
@delete-cloud
delete-cloud deleted the feat/webui-codex-oauth branch July 31, 2026 18:20
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.

1 participant