Skip to content

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

Closed
lidge-jun wants to merge 2 commits into
codex/admin-token-empty-noticefrom
codex/admin-token-no-local-prompt
Closed

fix(gui): never ask a local dashboard for an admin token#3492
lidge-jun wants to merge 2 commits into
codex/admin-token-empty-noticefrom
codex/admin-token-no-local-prompt

Conversation

@lidge-jun

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

Copy link
Copy Markdown
Owner

Stacked on #3491 — targets that PR's head branch. Retarget to dev once the parent lands.

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

Closes #3353

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
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 4, 2026 16:53
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Team

Run ID: c4dd55f1-a994-4182-ba76-ee732632310d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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 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:00:05.738003Z 8bbb404 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 added the bug Something isn't working label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed.

Hygiene

Deterministic PR hygiene checks passed.

@github-actions
github-actions Bot marked this pull request as draft September 4, 2026 16:54
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 72 / 80

이 PR은 로컬(루프백) 대시보드 사용자가 관리자 토큰 입력창을 보지 않아야 하는데도 보게 되는 UX 사고(#3353)를 고칩니다. 지금 devgui/src/api.ts resolveTokenAfter401은 부트스트랩이 확실히 거절되면(대개 401) 곧바로 promptForAdminToken을 엽니다. 그런데 루프백 설치는 서버가 HTML에 GUI 세션을 심어 주기 때문에, 그 거절의 진짜 원인은 보통 Host/Origin 불일치입니다. 토큰을 입력해도 고칠 수 없는 문제를 비밀번호 상자로 물어보는 셈이고, 문서(web-dashboard Sign-in)가 이미 “기본 루프백에서는 토큰을 묻지 않는다”고 약속한 것과도 어긋납니다. #3353 사용자는 2.40.0 업그레이드 후 이 창을 보고 설정이 날아간 줄 알았다고 합니다.

고침의 핵심은 “역할(role)이 아니라 바인드(bind)”입니다. standalone이면서 0.0.0.0에 바인드한 운영자는 일부러 대시보드를 노출한 사람이라 토큰을 쳐야 하고, 루프백 hub는 여전히 세션을 스스로 만듭니다. 역할만으로 가드하면 토큰이 필요한 사람을 막아 버립니다. 그래서 서버(src/server/gui-static.ts / index.ts)가 이미 쓰는 isApiAuthRequired 결과를 meta[name="opencodex-management-auth-required"]로 문서에 심고, GUI의 adminTokenPromptAllowed()가 그걸 읽습니다. 태그가 없으면(옛 서버·Vite) 역할로 폴백하되, 허브가 아니면 묻지 않는 안전한 기본값입니다. 묻지 않을 때는 promptCancelled를 세워 실패 파도가 매번 다시 물어보지 않게 하고, SESSION_UNAVAILABLE_EVENT를 쏩니다.

현재 dev917d5dc0d / 2.43.0이고, 이 변경은 admission·CORS·issueGuiSession 의미는 건드리지 않고 “언제 프롬프트를 띄울지”만 고칩니다. #3491 위에 스택되어 있고 #3493이 그 위에 올라갑니다. 테스트가 네 갈래(standalone 무프롬프트+이벤트, 태그 없음, exposed standalone은 프롬프트, loopback hub는 무프롬프트)로 위험한 회귀를 직접 잠급니다. types/config 분할과 무관합니다.

gui/src/api-targets.ts · adminTokenPromptAllowed - 바인드 선언을 읽는 설계가 맞고, 주석이 왜 역할이 틀린지도 분명히 적혀 있습니다.
gui/src/api.ts · resolveTokenAfter401 - 프롬프트 앞에 adminTokenPromptAllowed() 가드와 promptCancelled 설정 순서가 타당합니다.
src/server/gui-static.ts · managementAuthRequired meta - 서버가 이미 계산하는 값을 문서에 드러내는 방식이라 권한 모델이 이중으로 갈라지지 않습니다.
SESSION_UNAVAILABLE_EVENT - 이벤트는 쏘지만, 이 PR diff와 현재 dev GUI 어디에도 프로덕션 리스너가 없습니다. 테스트만 듣습니다. PR 본문이 말하는 “실행 가능한 안내 배너”는 아직 UI에 연결되지 않은 상태입니다. #3493은 문서/다이얼로그 도움말만 추가합니다.
스택 base - codex/admin-token-empty-notice(#3491) 대상입니다. #3491이 먼저 들어가야 dev로 리타깃할 수 있습니다.

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

  • SESSION_UNAVAILABLE_EVENT를 실제로 그리는 셸 배너를 이 PR에 넣을지, 후속 이슈로 둘지(지금은 “안 물어봄”만 보장되고 인앱 안내는 문서에만 있음)
  • #3491과 한 번에 merge-train으로 넣을지, fix(gui): stop the admin-token dialog painting an empty error notice #3491 단독 후 리타깃할지
  • #3353을 이 PR만으로 닫을지, 안내 UI가 붙을 때까지 열어 둘지

너의 추천
#3491이 dev에 들어간 뒤 바로 리타깃해서 병합하세요. 로컬 사용자에게 토큰을 묻는 사고는 우선순위가 높고, 바인드 기준 가드와 테스트가 탄탄합니다. 다만 인앱 “왜 세션이 안 되는지” 배너는 아직 없으니, 병합 코멘트나 짧은 후속 이슈로 셸 리스너를 명시해 두는 편이 좋습니다. #3353은 이 PR로 닫아도 되고, 배너까지 원하면 #3493 문서 앵커와 함께 “부분 해결”로 남겨도 됩니다.

이 댓글은 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: 8bbb404e97

ℹ️ 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
?? (typeof CustomEvent === "function" ? CustomEvent : null);
if (!Ctor) return;
try {
window.dispatchEvent(new Ctor(SESSION_UNAVAILABLE_EVENT, { detail: { 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 Render the session-unavailable event in the dashboard

When a loopback session bootstrap receives a definitive 4xx, this path suppresses all later authentication resolution and dispatches an event, but no production code listens for SESSION_UNAVAILABLE_EVENT (the only production occurrences are the declaration and this emitter). Consequently, the promised actionable Host/Origin notice never appears and the dashboard continues failing with 401 responses without explaining how to recover; add a shell-level listener that renders a localized alert before setting promptCancelled permanently.

Useful? React with 👍 / 👎.

Comment thread src/server/index.ts
undefined,
guiSessionCandidate ?? undefined,
config.runtimeRole ?? "standalone",
isApiAuthRequired(config),

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 Add server coverage for the emitted auth-requirement tag

This new argument controls whether deployed dashboards can recover by entering an admin token, but the added GUI tests manually construct the meta tag and no server-side test asserts that startServer/serveGuiFile emits 1 for non-loopback binds and 0 for loopback binds. A wiring or predicate regression would therefore either lock remote operators out or restore the unwanted local prompt while all new tests remain green; add focused coverage at the server/static-serving boundary.

AGENTS.md reference: src/AGENTS.md:L24-L24

Useful? React with 👍 / 👎.

@github-actions
github-actions Bot marked this pull request as ready for review September 4, 2026 17:12
@lidge-jun
lidge-jun deleted the branch codex/admin-token-empty-notice September 4, 2026 17:16
@lidge-jun lidge-jun closed this Sep 4, 2026
@lidge-jun

Copy link
Copy Markdown
Owner Author

Superseded by #3496. GitHub auto-closed this PR when its stack parent #3491 merged and the base branch codex/admin-token-empty-notice was deleted, and a closed PR cannot be retargeted. #3496 carries the same commits rebased onto dev.

@lidge-jun

Copy link
Copy Markdown
Owner Author

Landed via #3496 at 3e65218

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

@lidge-jun lidge-jun added the landed-via-maintainer Original PR closed after landing via a maintainer merge train label Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working landed-via-maintainer Original PR closed after landing via a maintainer merge train

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant