Skip to content

fix(gui): never ask a local dashboard for an admin token - #3496

Merged
lidge-jun merged 2 commits into
devfrom
codex/admin-token-no-local-prompt
Sep 4, 2026
Merged

fix(gui): never ask a local dashboard for an admin token#3496
lidge-jun merged 2 commits into
devfrom
codex/admin-token-no-local-prompt

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Successor to #3492, which GitHub auto-closed when its stack parent #3491 merged and that branch was deleted. Same commits, rebased onto dev.

Summary

A plain local user should never see the admin-token dialog, and today they can. That is the UX disaster this PR fixes.

A loopback install mints its own GUI session into the served document, so the dashboard never needs a typed credential. But resolveTokenAfter401 treated any definitive bootstrap refusal as "ask the human for a token". On loopback the only ways to get that refusal are a Host or Origin mismatch — a misconfiguration the admin token cannot repair. The user is handed a password box they cannot fill, and filling it would not have helped, because the token is not what was refused.

It also contradicted the shipped contract. The Sign-in guide already promises "on the default loopback bind the dashboard never asks for a token". #3353 is a user who met the prompt after upgrading to 2.40.0, assumed config loss, and had to have an LLM read the codebase to find out what the box wanted.

Now the prompt only appears when the deployment actually requires a typed credential; otherwise the dashboard emits an actionable notice naming the real cause. promptCancelled is set alongside it so a failing fan-out does not re-enter resolution on every request, and storeSession already clears that flag, so recovery is automatic once the address is corrected.

The predicate is the bind, not the topology. runtimeRole is the obvious signal and it is wrong in both directions:

  • standalone + hostname: "0.0.0.0" is an operator who deliberately exposed the dashboard and must type the token — proven by tests/server-management-auth.test.ts, "a non-loopback binding never issues a GUI session from a forged loopback Host".
  • A hub on loopback still mints its own session.

Gating on role === "hub" would have hidden the prompt from exactly the operator who needs it. So the server now states isApiAuthRequired — the same predicate it gates the mint with — beside the role it already emits, and the GUI reads that. A document without the tag falls back to the role, so a hub dashboard keeps working against a server that predates it.

No server admission or credential semantics change: issueGuiSession, requireManagementAuth, and the CORS resolvers are untouched apart from stating a value they already compute.

before and after

Verification

  • bun run typecheck — clean
  • bun run lint:gui — clean
  • cd gui && bun test — 1366 pass, 0 fail (220 files)
  • bun test tests/gui-static.test.ts tests/server-management-auth.test.ts tests/server-auth.test.ts — 143 pass, 0 fail
  • bun run privacy:scan — passed

Four new cases in gui/tests/api-auth-deadline.test.ts: a standalone loopback dashboard is never prompted and emits the notice once; an absent tag behaves the same; an exposed standalone bind is still prompted; a loopback hub is not. The existing prompt-fallback tests in api-auth-deadline and api-auth-memory now declare the non-loopback shape their own comments already described.

Checklist

  • Focused tests cover the change
  • Docs updated where user-facing behavior changed (the sibling PR adds the guide anchor)
  • No credential, token, or request-body logging introduced
  • Targets dev

Closes #3353

Summary by CodeRabbit

  • Bug Fixes
    • Improved authentication recovery based on deployment configuration and runtime type.
    • Prevented unnecessary administrator-token prompts in standalone or unauthenticated environments.
    • Added a non-blocking session-unavailable notification when recovery cannot proceed.
    • Preserved token prompts for deployments where management authentication is required.

jun added 2 commits September 5, 2026 02:16
A plain loopback install mints its own GUI session into the served document, so a
refused bootstrap there is a Host/Origin misconfiguration — not a missing
credential. The dashboard answered it with a password box the user could not fill
and that would not have helped if they could: the token is not what was refused.

That also contradicted the shipped contract. The Sign-in guide already promises
"on the default loopback bind the dashboard never asks for a token", and #3353 is
a user who hit the prompt after an upgrade, assumed config loss, and had to have
an LLM read the source to learn what the box wanted.

`resolveTokenAfter401` now prompts only when the deployment actually requires a
typed credential, and reports an actionable notice otherwise. `promptCancelled`
is set alongside it so a failing fan-out does not re-enter resolution on every
request; `storeSession` already clears that flag, so recovery stays automatic
once the address is corrected.

The predicate is the bind, not the topology. `runtimeRole` was the obvious
signal and it is wrong in both directions: `standalone` + `hostname: "0.0.0.0"`
is an operator who deliberately exposed the dashboard and MUST type the token
(tests/server-management-auth.test.ts, "a non-loopback binding never issues a GUI
session from a forged loopback Host"), while a `hub` on loopback still mints its
own session. Gating on `role === "hub"` would have hidden the prompt from exactly
the person who needs it. So the server states `isApiAuthRequired` — the same
predicate it gates the mint with — beside the role it already emits, and the GUI
reads that. A document without the tag falls back to the role, so a hub dashboard
still works against a server that predates it.

Four cases are covered: a standalone loopback dashboard is never prompted and
emits the notice once, an absent tag behaves the same, an exposed standalone bind
is still prompted, and a loopback hub is not. The existing prompt-fallback tests
in api-auth-deadline and api-auth-memory now declare the non-loopback shape their
own comments already described.

Closes #3353
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 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-04T17:24:32.015982Z 6da1269 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

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Sep 4, 2026
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Team

Run ID: 72c8ccb2-4a12-4643-a9d3-cd24fbcb5c69

📥 Commits

Reviewing files that changed from the base of the PR and between 85e4211 and 6da1269.

⛔ Files ignored due to path filters (1)
  • devlog/_plan/260905_admin_token_local_ux/assets/local-session-notice-before-after.png is excluded by !**/*.png
📒 Files selected for processing (6)
  • gui/src/api-targets.ts
  • gui/src/api.ts
  • gui/tests/api-auth-deadline.test.ts
  • gui/tests/api-auth-memory.test.ts
  • src/server/gui-static.ts
  • src/server/index.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.


📝 Walkthrough

Walkthrough

The server now declares management-auth requirements in GUI metadata. The GUI uses that metadata and runtime role to control admin-token prompts. When prompting is unavailable, it emits a session-unavailable event. Tests cover suppression, fallback, prompting, and existing authentication flows.

Changes

Management authentication prompt gating

Layer / File(s) Summary
Server authentication metadata
src/server/gui-static.ts, src/server/index.ts
GUI bootstrap responses can include opencodex-management-auth-required with "1" or "0". The main fetch handler passes isApiAuthRequired(config) to serveGuiFile.
Client prompt gating and session notification
gui/src/api-targets.ts, gui/src/api.ts
adminTokenPromptAllowed() evaluates management-auth metadata and falls back to the hub runtime role. Session recovery skips the prompt when it is not allowed and dispatches SESSION_UNAVAILABLE_EVENT through a guarded browser-event path.
Authentication-flow coverage
gui/tests/api-auth-deadline.test.ts, gui/tests/api-auth-memory.test.ts
Tests cover standalone prompt suppression, missing-role fallback, explicit metadata, session-unavailable events, prompt cancellation, concurrent authentication, and existing prompted-token behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 6da12

The dashboard now avoids requesting an admin token when local session bootstrap cannot be restored and instead reports an actionable session notice, while deployments requiring management authentication retain token prompting. The covered behavior is ready to merge.

Sequence Diagram(s)

sequenceDiagram
  participant Server
  participant GUI
  participant Shell
  Server->>GUI: Serve runtime-role and management-auth metadata
  GUI->>GUI: Evaluate adminTokenPromptAllowed()
  GUI-->>Shell: Dispatch SESSION_UNAVAILABLE_EVENT when prompting is unavailable
Loading

Suggested reviewers: wibias

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The implementation addresses local dashboard authentication behavior in gui/src/api-targets.ts, gui/src/api.ts, src/server/gui-static.ts, and src/server/index.ts. However, linked issue #3353 requires … Add the requested dashboard user guide that explains how to generate, configure, and persist OPENCODEX_ADMIN_AUTH_TOKEN. If this PR is intended only to fix local-session behavior, link it to a separate issue and remove the claim that it clo…
Out of Scope Changes check ⚠️ Warning The code changes implement authentication-flow behavior and session-unavailable handling, but linked issue #3353 requests documentation for OPENCODEX_ADMIN_AUTH_TOKEN. The supplied evidence does not s… Add the requested documentation and provide explicit scope justification for the authentication code changes, or move those code changes to a separate issue/PR and keep this PR focused on the user guide.
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 6 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: preventing local dashboards from requesting an admin token.
Full details: Linked Issues check

Explanation

The implementation addresses local dashboard authentication behavior in gui/src/api-targets.ts, gui/src/api.ts, src/server/gui-static.ts, and src/server/index.ts. However, linked issue #3353 requires a user guide for setting OPENCODEX_ADMIN_AUTH_TOKEN, and the supplied changes contain no documentation or guide update. The requirement is therefore unmet.

Resolution

Add the requested dashboard user guide that explains how to generate, configure, and persist OPENCODEX_ADMIN_AUTH_TOKEN. If this PR is intended only to fix local-session behavior, link it to a separate issue and remove the claim that it closes #3353.

Full details: Out of Scope Changes check

Explanation

The code changes implement authentication-flow behavior and session-unavailable handling, but linked issue #3353 requests documentation for OPENCODEX_ADMIN_AUTH_TOKEN. The supplied evidence does not show that these implementation changes are required by that issue. The changes are outside the linked issue scope, although they address the reported user-experience problem.

  • Fix all pre-merge checks with AI
✨ 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 codex/admin-token-no-local-prompt

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.

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

현재 헤드에는 사용자에게 보이는 안내가 실제로 없습니다.

쉽게 말하면 gui/src/api.ts가 opencodex:session-unavailable 이벤트를 보내기만 하고, gui/src 전체에서 그 이벤트를 받아 배너나 토스트를 그리는 production listener는 0개입니다. 검색 결과 이 이름은 api.ts와 테스트에만 존재합니다. 그래서 로컬 bootstrap이 401을 반환하면 토큰 창은 사라지지만, PR 설명의 "actionable notice naming the real cause"는 화면에 나타나지 않고 사용자는 설명 없는 실패만 보게 됩니다.

SESSION_UNAVAILABLE_EVENT를 실제 앱 shell에서 구독해 Host/Origin 주소 문제와 해결 방법을 표시하고, DOM 수준 회귀 테스트로 이벤트 발생이 아니라 안내 렌더링까지 증명해 주세요. 중복 이벤트가 안내를 쌓지 않고, 정상 session 복구 후 안내가 사라지는 것도 함께 확인해야 합니다. 그 뒤 exact-head CI가 통과하면 다시 보겠습니다.

@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: 6da1269f87

ℹ️ 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 thread gui/src/api.ts
Comment on lines +283 to +285
if (!adminTokenPromptAllowed()) {
state.promptCancelled = true;
reportSessionUnavailable(plane);

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 Badge Restore shared-plane recovery for connected dashboards

When a connected client's shared hub session expires or renewal is refused, the document was served by src/client/machine-listener.ts with role client and no management-auth meta tag, so this page-global check returns false even though the shared target is a non-loopback hub. The shared runtime is then permanently marked promptCancelled, while App.tsx never resets sharedSessionReady; consequently the pairing form remains hidden and all shared /api/* requests continue returning 401 until the page is reloaded. Make the decision target-specific or have the shared-plane event transition the dashboard back to its pairing flow.

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

Useful? React with 👍 / 👎.

Comment thread gui/src/api.ts
Comment on lines +52 to +54
try {
window.dispatchEvent(new Ctor(SESSION_UNAVAILABLE_EVENT, { detail: { plane } }));
} catch { /* a shell that cannot receive the notice must not break the fetch path */ }

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 Render the session-unavailable event

For the standalone Host/Origin-mismatch path introduced here, this event is the only replacement for the removed admin-token prompt, but no production module in the commit subscribes to SESSION_UNAVAILABLE_EVENT or its literal event name. Thus the advertised actionable notice is never displayed; users only see unrelated page-level load failures and still receive no explanation of how to correct the dashboard address. Add a shell listener that renders a translated, accessible notice and cover the rendered result rather than merely asserting that an event fired.

AGENTS.md reference: gui/AGENTS.md:L14-L18

Useful? React with 👍 / 👎.

@Ingwannu

Ingwannu commented Sep 4, 2026

Copy link
Copy Markdown
Owner

추가로 Codex가 잡은 connected-dashboard 회귀도 현재 헤드에서 확인했습니다. machine-listener는 role=client만 넣고 management-auth meta는 넣지 않으므로 shared plane 401도 page-global adminTokenPromptAllowed()에서 false가 됩니다. 이때 shared runtime의 promptCancelled만 고정되고 App.tsx의 sharedSessionReady는 false로 전환되지 않아 페어링 폼이 다시 나타나지 않습니다. 결과적으로 새로고침 전까지 shared API가 계속 401입니다.

따라서 앞서 요청한 notice renderer만 추가해서는 부족합니다. 판단을 API plane별로 나누고, shared plane 세션 만료/거부는 기존 pairing 상태로 되돌려야 합니다. sharedSessionReady 전환, 페어링 폼 재노출, 재페어링 후 자동 복구까지 DOM 통합 테스트로 고정해 주세요.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 70 / 80

이 PR은 로컬(loopback) 대시보드가 관리자 토큰 입력창을 띄우면 안 되는데, 지금은 띄울 수 있는 UX 사고를 고칩니다. 지금 dev HEAD(85e42117c, 방금 들어온 #3491)는 그 대화상자가 열릴 때 빈 빨간 에러 박스를 그리던 CSS 문제만 먼저 막았습니다. 이번 변경은 한 단계 더 가서, 애초에 로컬 사용자에게 그 창을 보여주지 않게 합니다.

기본 설치는 서버가 HTML 문서에 GUI 세션을 직접 넣어 줍니다. 그래서 loopback에서 부트스트랩이 거절되면 원인은 “토큰이 없어서”가 아니라 Host/Origin 설정이 어긋난 경우가 대부분입니다. 그런데 gui/src/api.tsresolveTokenAfter401은 거절만 확정되면 무조건 promptForAdminToken으로 넘어갔습니다. 사용자는 채울 수 없는 비밀번호 칸을 보고, 채워도 거절 원인은 그대로입니다. #3353 보고자가 2.40.0 업그레이드 뒤 설정이 날아간 줄 알고 LLM에게 코드베이스를 읽혀야 했던 바로 그 장면입니다. 가이드(docs-site Sign-in)는 이미 “기본 loopback에서는 토큰을 묻지 않는다”고 약속해 두었는데, 실제 GUI 동작이 그 약속을 깨고 있었습니다.

고친 방식의 핵심은 “역할(role)이 아니라 바인드(bind)”를 묻는 것입니다. standalone + hostname: "0.0.0.0"은 일부러 대시보드를 밖에 연 운영자라서 반드시 토큰을 쳐야 하고(tests/server-management-auth.test.ts의 forged loopback Host 케이스), 반대로 loopback hub는 여전히 세션을 스스로 발급합니다. role === "hub"로 가리면 정작 토큰이 필요한 사람만 못 보게 됩니다. 그래서 서버가 이미 쓰던 isApiAuthRequired(config) 결과를 opencodex-management-auth-required 메타로 HTML에 심고(src/server/gui-static.ts, src/server/index.ts), GUI의 adminTokenPromptAllowed()가 그걸 읽습니다. 태그가 없으면(구서버·Vite) 예전처럼 role fallback이라 hub만 프롬프트를 허용합니다.

프롬프트를 막은 뒤에는 promptCancelled를 켜고 opencodex:session-unavailable 커스텀 이벤트를 한 번 쏩니다. 팬아웃 401이 매 요청마다 해석으로 다시 들어가지 않게 하려는 장치이고, storeSession이 그 플래그를 지우므로 주소를 고치면 자동 회복 경로도 그대로입니다. 테스트 네 개가 행렬을 잘 박아 두었습니다. standalone은 프롬프트 0 + 이벤트 1, 태그 없음도 프롬프트 0, 노출 standalone은 프롬프트 1, loopback hub는 프롬프트 0. 기존 prompt-fallback 테스트들도 non-loopback 메타를 선언하도록 맞춰 두었습니다. #3492가 #3491 머지·브랜치 삭제로 자동 닫힌 뒤 같은 커밋을 dev에 다시 올린 successor이고, 설명용 형제 PR은 #3493(베이스가 이 브랜치)입니다.

다만 PR 본문이 말하는 “원인 이름을 붙인 actionable notice를 대시보드가 그린다”는 코드 범위와 어긋납니다. 이 diff는 이벤트만 dispatch하고, gui/src/App.tsx나 다른 셸에 SESSION_UNAVAILABLE_EVENT 리스너·문구 UI가 없습니다. before/after PNG는 plan 자산으로만 들어가 있습니다. 그래서 로컬 사용자의 체감은 “이상한 비밀번호 창이 안 뜨는 것”까지는 확실하고, “왜 안 되는지 화면에 적혀 있는 것”은 아직 이 PR만으로는 보장되지 않습니다. CI도 test 3/4가 fail로 남아 있어(다른 shard는 pass) 머지 전에 원인 확인이 필요합니다. #3353을 이 PR만으로 Closes 하면, 원격 바인드에서 토큰이 뭔지 모르는 쪽 가이드(#3493)는 이슈 닫힘과 함께 끊길 수 있습니다.

라인 - gui/src/api.ts reportSessionUnavailable / SESSION_UNAVAILABLE_EVENT - 이벤트는 쏘지만 셸 리스너가 이 PR에 없어, 본문이 약속한 “원인 안내 notice”는 실제 화면에 안 나옵니다. 리스너를 이 PR에 넣거나, 문구를 “프롬프트만 막고 이벤트는 후속”으로 고쳐야 합니다.
라인 - gui/src/api.ts resolveTokenAfter401 주석의 “non-hub deployment” - 실제 가드는 adminTokenPromptAllowed()(바인드 메타)인데 주석만 role 언어로 남아 헷갈립니다.
경로/심볼 - src/client/machine-listener.ts serveGuiFile(..., "client") - 새 managementAuthRequired 인자를 안 넘깁니다. 지금은 optional + client fallback(프롬프트 없음)이라 깨지지는 않지만, client HTML에도 0 메타를 심을지 한 줄로 정해 두면 구서버 호환 규칙과 맞춰집니다.
경로/심볼 - tests/gui-static.test.ts / serveGuiFile 호출부 - 새 메타 문자열이 HTML에 실제로 삽입되는지 서버 쪽 단언이 없습니다. GUI 단위 테스트만으로 계약의 한쪽만 잠깁니다.
경로/심볼 - CI test 3/4 - 이 PR 관련 shard가 fail입니다. 로컬 bun test 그린 것과 별개로 Actions 실패 원인을 보고 재실행·수정이 필요합니다.
경로/심볼 - Closes #3353 vs #3493 - #3353은 “토큰이 뭔지 알려 달라”는 enhancement이기도 해서, 로컬 오탐 수정(#3496)과 원격 안내(#3493)를 한 이슈에 묶을지 메인테이너가 나눠야 합니다.

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

  • session-unavailable notice UI를 이 PR에 반드시 넣을지, feat(gui,docs): explain the admin token where it is asked for #3493(또는 후속)로 미룰지
  • #3353을 #3496만으로 닫을지, 가이드가 있는 #3493까지 연 뒤 닫을지
  • test 3/4 fail이 이 변경 원인인지 flake인지 — 원인 확인 전 머지 여부
  • machine-listener client 문서에 management-auth 메타를 명시적으로 0으로 심을지

너의 추천
CI test 3/4 실패 원인을 먼저 확인하고, notice UI를 이 PR에 최소 리스너(원인 한 줄 + #3493 가이드 링크)로 넣거나 본문 약속을 축소한 뒤 dev에 머지하세요. 이어서 스택된 #3493을 retarget·머지하고, #3353은 두 PR이 함께 커버한다고 보는 편이 안전합니다. types/config 분할에 무효화되는 PR은 아닙니다.

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

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants