Skip to content

feat(gui): main-card Re-login with device code (#3898) - #4441

Merged
lidge-jun merged 6 commits into
devfrom
codex/260912-native-main-reauth-ui
Sep 13, 2026
Merged

lidge-jun merged 6 commits into
devfrom
codex/260912-native-main-reauth-ui

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

  • Refs Headless hub: WebUI cannot reauth native main (deviceauth is pool-only) #3898. Follow-up to feat(codex): native-main device reauth for headless hubs (#3898) #4433 (the API/CLI layer): the Codex Auth main card finally gets its missing control — Re-login with device code — so a headless hub operator can recover a WHAM-revoked native __main__ grant from the dashboard.
  • New use-main-device-reauth hook drives ONLY the dedicated /api/codex-auth/main/reauth-device namespace (empty-body start, immediate poll until the URL/code arrive, flowId-owned polling, late responses ignored, unmount cancel). The pool AddCodexAccountModal/openReauth path is deliberately untouched: /api/codex-auth/login rejects __main__ and would write the wrong credential store.
  • DTO hygiene: the hook reads only flowId/status/verificationUrl/deviceCode and the closed failure-code set; the verification URL is allowlisted to https://auth.openai.com/codex/device; the human code must match a short alphanumeric shape; nothing persists to browser storage.
  • Main card: expired state now shows the CTA, the verification URL + human code + pending status + cancel while the flow runs, a succeeded badge on completion (which refreshes the account list), and an actionable failure badge with the failure code — no auto-retry. Pool refresh pauses while the flow is active, same as the add modal.
  • i18n: 7 new codexAuth.* keys plus a revised mainTokenExpired (no longer claims App login is the only path) in all 9 shipped locales. Structure claim in structure/gui-and-management-api.md.

Stack (merge bottom-up):

# PR Layer Review focus
4 (top) paginated history recovery user-data safety
3 this PR main-card Re-login UI ← you are here hook + card + i18n
2 #4433 native-main reauth API/CLI auth boundary
1 #4427 WS stage instrumentation log schema

Depends on #4433. Review this PR's diff only.

Verification

  • bun test tests/gui/main-device-reauth-ui.test.ts tests/test-layout.test.ts tests/test-layout-tooling.test.ts → 20 pass / 0 fail (fresh run at d111f8d; receipt .codexclaw/evidence/01a09616-38e6-72e0-b5bf-99eb10ce58a6/test-receipt.json). Source contracts: dedicated namespace only, no pool login route, no token fields, all-locale copy.
  • bun run structure:check → SSOT checks passed. Syntax parse (Bun.Transpiler) OK on all new/changed GUI files.
  • Local product suite / build / typecheck / install: NOT RUN (standing rule); gui dependencies are not installed in this worktree. The happy-dom suite gui/tests/main-device-reauth.test.tsx (CTA start, pending URL/code + cancel ownership, hook empty-body POST + poll + dedicated-route-only) runs in hosted exact-head CI, which is the integration evidence for this PR.
  • Screenshot: to be attached from hosted CI built artifacts once available (repo gate for GUI PRs); the change is one CTA plus a status block on the existing main card, reusing current card styles.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed. (structure doc; i18n all locales)
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. (dedicated namespace; URL allowlist; no token fields; independent adversarial review of the design folded)

@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 13, 2026 01:03
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-13T01:08:38.988335Z d111f8d PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the enhancement New feature or request label Sep 13, 2026
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • UI screenshot required.

What to do

  • Add a screenshot of the UI change to the PR description.

This pull request was already a draft. Its draft status will be preserved after every issue above is resolved.

@github-actions
github-actions Bot marked this pull request as draft September 13, 2026 01:04
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 72 / 80

이 PR은 이슈 #3898의 L3입니다. L2(#4433)가 만든 전용 표면 POST/GET/DELETE /api/codex-auth/main/reauth-device 위에, Codex Auth 메인 카드에 디바이스 코드로 다시 로그인 버튼을 붙입니다. 헤드리스 hub(Docker/k3s, Codex App 없음)에서 native __main__이 WHAM으로 죽어도, 대시보드에서 풀 로그인 경로를 타지 않고 메인 슬롯만 살릴 수 있게 하려는 조각입니다.

현재 dev(HEAD dcd13b435, package 2.53.0)의 메인 카드는 showReauth일 때 codexAuth.mainTokenExpired 문구만 보여 줍니다. "Codex 앱 로그인으로 다시 로그인하세요"라고만 말하고, 디바이스 코드 CTA는 없습니다. 풀 쪽 AddCodexAccountModal/openReauth/api/codex-auth/login을 쓰는데, 그 라우트는 __main__을 거절하고 풀 credential store에 씁니다. 그래서 이 PR이 풀 모달을 재사용하지 않고 gui/src/components/use-main-device-reauth.ts 훅으로 전용 네임스페이스만 호출하는 선택은 맞습니다.

훅 설계는 L2 폴링 계약을 잘 따릅니다. 시작 POST는 빈 body(키가 있으면 400), 시작 응답에는 URL/코드가 비어 있을 수 있어서 바로 GET 폴링으로 채웁니다. flowId 소유 폴링, 늦은 응답 무시, 언마운트 시 DELETE cancel, verification URL은 https://auth.openai.com/codex/device allowlist, deviceCode는 짧은 영숫자 형태만 받습니다. 토큰 필드는 DTO에서 읽지 않습니다. 카드는 idle/failed에서 CTA, pending/committing에서 URL·코드·취소, 성공 배지 후 load()로 계정 목록을 갱신합니다. 플로우가 살아 있는 동안 풀 refresh를 멈추는 것도 add 모달과 같습니다. i18n 9개 로케일에 키 7개 + mainTokenExpired 문구 수정, structure 클레임, happy-dom/소스 계약 테스트까지 범위가 한 층에 잘 묶여 있습니다.

베이스는 codex/260912-native-main-reauth-api(#4433)이고, 그 아래는 #4427입니다. dev에 단독으로 올릴 수 있는 PR이 아닙니다. enforce-target도 stacked로 인식했고, 지금은 UI 스크린샷 없음으로 quality gate가 빨갛습니다. PR 본문도 hosted CI 아티팩트 스크린샷을 나중에 붙인다고 적어 두었습니다. GUI PR 레포 게이트이므로 머지 전에 반드시 채워야 합니다. react-doctor도 실패로 찍혀 있으니 로그의 경고 내용을 한 번 확인하세요.

라인 136 - codex-account-pool-main-card.tsx CTA는 phase === "idle" || phase === "failed"일 때만 나옵니다. 그런데 use-main-device-reauth.tscancel()과 서버 cancelled 응답은 phase: "cancelled"로 둡니다. 취소 직후 CTA가 사라져서, 카드가 다시 마운트되기 전에는 재시작할 수 없습니다. cancelled를 idle처럼 취급하거나, cancel 성공 후 idle로 되돌리세요.

라인 68 - CodexAccountPool.tsx에서 useMainDeviceReauth(apiBase, () => { void load(); })로 매 렌더마다 새 onCompleted를 넘깁니다. 지금은 클릭 시에만 start가 돌아가서 당장 깨지지는 않지만, 훅 deps에 onCompleted가 들어가 있어 start identity가 흔들립니다. useCallback으로 load를 감싸거나 훅 안에서 ref로 최신 콜백을 잡는 편이 안전합니다.

라인 155-156 - verification URL을 <span> 평문으로만 찍습니다. 라벨은 mainReauthOpen(열기)인데 클릭 가능한 <a href=...>(allowlist 통과분만)가 아닙니다. 헤드리스 운영자가 다른 기기 브라우저로 열 때 복사 실수가 나기 쉽습니다. allowlist를 통과한 URL만 href로 연결하거나, 복사 버튼을 두는 쪽이 의도에 더 가깝습니다.

라인 248-249 - allowedVerificationUrlstartsWith(DEVICE_VERIFICATION_URL)입니다. 호스트는 이미 고정이라 큰 위험은 아니지만, 쿼리/프래그먼트/추가 path를 어디까지 허용할지 한 줄로 고정해 두면 L2 DTO와 문서가 맞습니다. 지금은 테스트가 exact URL만 검증합니다.

경로 gui/tests/main-device-reauth.test.tsx / tests/gui/main-device-reauth-ui.test.ts - CTA·폴링·전용 라우트·로케일 키 계약은 잘 잡혀 있습니다. 로컬 product/gui install은 standing rule로 안 돌렸고, happy-dom 스위트는 hosted exact-head CI가 실증거입니다. 그 CI가 초록인지와 스크린샷 첨부 여부가 이 PR의 머지 게이트입니다.

메인테이너의 판단이 필요한 지점

너의 추천
#4433이 머지(또는 같은 스택으로 함께 랜딩)된 뒤에 이 PR을 이어서 머지하세요. 머지 전에 (1) cancel → 다시 CTA가 보이게 cancelled/idle 정리, (2) UI 스크린샷 첨부해 enforce-target 통과, (3) hosted happy-dom/CI 초록 확인을 하세요. 풀 login/AddCodexAccountModal 경로는 건드리지 말고 전용 네임스페이스만 유지하세요.

이 댓글은 grok-bot이 작성했습니다

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d111f8d63b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +170 to +172
useEffect(() => {
return () => {
unmountedRef.current = true;

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 Badge Reset the liveness flag during effect setup

The dashboard mounts under React.StrictMode (gui/src/main.tsx), whose development effect replay runs this cleanup and then sets the effect up again; because setup never restores unmountedRef.current to false, every subsequent start exits at the first polling guard after the POST and leaves the card stuck in starting while the server flow continues. The same permanent disablement occurs when apiBase changes in production, so set the flag to false in the effect setup before returning this cleanup.

Useful? React with 👍 / 👎.

Comment on lines +92 to +95
try {
await fetch(`${apiBase}/api/codex-auth/main/reauth-device?flowId=${encodeURIComponent(flowId)}`, { method: "DELETE" });
} catch { /* best-effort: the flow expires on its own */ }
setState({ phase: "cancelled" });

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 Badge Honor the DELETE result before reporting cancellation

When the DELETE fails or races with credential publication, this code still unconditionally reports cancelled. The management API can explicitly return succeeded after publication (cancelMainDeviceReauth documents that publication wins), while a transport failure can leave the server flow active, so the operator may be told an authentication operation stopped even though it continues or already updated auth.json; inspect the HTTP/DTO result and refresh on success or expose a cancellation failure instead.

AGENTS.md reference: gui/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

Comment on lines +214 to +216
{mainReauth.state.verificationUrl && (
<span>{t("codexAuth.mainReauthOpen")}: {mainReauth.state.verificationUrl}</span>
)}

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 Badge Make the device-page affordance operable

When the verification URL arrives, the UI labels it “Open” but renders it as plain text, so neither pointer nor keyboard users can activate it and there is no copy control. This makes the new device-login flow depend on manually selecting a long URL; render an accessible link/button or reuse the existing LoginUrlBlock pattern.

AGENTS.md reference: gui/AGENTS.md:L33-L33

Useful? React with 👍 / 👎.

…ng flow [skip ci]

Both device-reauth GUI tests failed on this branch before the merge. The first
clicked button.codex-auth-action-btn, but the always-rendered pause control
ships the same class and renders first, so the click hit pause and starts
stayed 0; select by label the way the cancel test already does. The second
awaited start() to completion while the mock always answers pending, and
start() owns the flow until a terminal status, so it timed out at 5s; drive it
and wait for the first poll to land instead. Product code is unchanged.
… ci]

oxlint's react-compiler pass lowers a for statement and asserts its init is a
variable declaration, so the empty init in for (;;) raised "Invariant: Expected
a variable declaration" and blamed the enclosing hook. GUI lint runs in hosted
CI, so this failed the gate. The loop body already returns on abort, unmount or
a replaced flow id, and nothing follows the loop, so keying the loop on the same
abort signal preserves behavior exactly.
Base automatically changed from codex/260912-native-main-reauth-api to dev September 13, 2026 03:51
@lidge-jun
lidge-jun merged commit 0c4f962 into dev Sep 13, 2026
4 of 5 checks passed
@lidge-jun
lidge-jun deleted the codex/260912-native-main-reauth-ui branch September 13, 2026 03:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant