Skip to content

feat(admin)!: sign read keys in to a read-only dashboard - #541

Merged
amondnet merged 11 commits into
mainfrom
amondnet/ui-frame
Sep 14, 2026
Merged

amondnet merged 11 commits into
mainfrom
amondnet/ui-frame

Conversation

@amondnet

@amondnet amondnet commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Two changes that belong together: the admin surface learns that a browser session has a tier, and the design record fixes the frontend stack the dashboard grows into next.

A read key can now use the dashboard. SessionStore records the privilege of the credential that minted a session, and authenticate reads it back instead of returning Write for every cookie. That removes the reason login_submit had to refuse read_keys outright — while a session carried full access unconditionally, minting one from a read key would have silently escalated it. A read-key operator previously had curl and no dashboard at all.

Enforcement does not move: every mutation still goes through require_write, and all seven mutation routes were re-checked to still carry that gate. OIDC keeps minting write sessions, because nothing in its allow lists distinguishes a read-only identity.

One consequence is a revocation caveat, not a permission one (see Notes). The cookie carries no permission the read key did not already have through the header slot, but it carries its own lifetime: sessions are validated against the in-memory store alone, so a rotated read key's cookie keeps reading until session_ttl_secs. That is #100's existing gap widened from write-tier credentials to both tiers, and it is documented in m9-admin-surface.md and in the BREAKING CHANGE: footer.

The dashboard renders from the tier. GET /admin/api/session carries access, and the bundle stops offering actions that would come back 403: a read session keeps every table it can read and loses the two add-account forms, the row actions, and the actions column those lived in — dropped rather than left standing, since a header row one cell wider than its body rows is a rendering bug. It also gets a sentence saying why, because a section that merely lost its buttons reads as broken. useCanWrite compares against write rather than against read, so an unrecognized value reads as read-only rather than unlocking the page.

Decision 6 in admin-ui-delivery.md fixes what goes inside Resolution 2's React + Vite: Base UI for primitives, TanStack Router landing with the /admin/* deep links Resolution 6 already promised, and Tailwind v4 + shadcn/ui conditional on html.rs generating its stylesheet instead of hand-keeping a copy of index.css. Cloudflare Kumo is rejected with reasons. It also records what the stack does not decide — monitoring is blocked on storage.md, and access-permission management is display-only until the same decision, since a UI cannot mint a key the config refuses as a literal.

Milestone / spec

  • docs/m9-admin-surface.md — the access-tier section (read / write) and the emergency-rotation limitation
  • docs/admin-ui-delivery.md — Decision 5 (session bootstrap) and the new Decision 6

Checklist

  • cargo build passes
  • cargo test passes (new behavior is covered; tests run without network/loopback where possible)
  • cargo clippy --all-targets -- -D warnings clean
  • cargo fmt --all --check clean
  • Source files stay under 500 lines — src/admin/mod.rs (2193) and src/config.rs (9265) were already over; this change adds 41 and 4 lines respectively and splits neither
  • English only; matches surrounding style
  • Frozen spec in docs/ updated if this change deviates from it
  • User-facing docs updated for behavior/config/endpoint/CLI/provider/model changes
  • Any new GitHub Action is pinned to a full commit SHA — none added

Notes for reviewers

The credential path is the thing to look at closely. The safety argument is that the tier now reaches the session and require_write reads it, so please check the audit rather than taking it on faith: Authenticated::Session is constructed in exactly one place (src/admin/mod.rs), nothing infers write from authok.kind, and all seven mutation routes (4 Claude + 3 Codex) call require_write. The spend surface is unaffected — it authenticates header credentials only, through its own authenticate in src/gateway/spend/api.rs.

An adversarial review (GPT-6 Astra) raised one finding, and it stands: read-key sessions outlive revocation of the key that minted them. authenticate validates against the session store without rechecking the originating credential, and AppState::refreshed carries the store across reloads, so overwriting a ${file:}-backed read key and reloading stops its header credential while its cookie reads on for up to session_ttl_secs (default 1h). This is not a gap this PR invents — m9-admin-surface.md already documented it and #100 already tracks the fix — but it previously applied only to credentials that could log in, which read keys could not. What survives is read-only, and strictly less than the full access a write-tier session already carried across the same window. The response here is to name it (rotation note + BREAKING CHANGE: footer) rather than to bind sessions to a credential generation in this PR: fixing it for read keys while leaving the worse write-tier case unfixed would be the odd half-state, and #100 is where it belongs for both.

Both test halves are load-bearing. The read assertions alone would also pass against a dashboard that refused every cookie, so each property is pinned from both directions, and the UI harness defaults to a write session so the 57 pre-existing tests keep asserting on the page they were written against. Both suites were checked falsifiable by mutation: re-hardcoding access: AdminAccess::Write fails the Rust test with an assertion (not a compile error), and making useCanWrite return true fails 3 of the 5 new UI tests.

Docs were swept column-wise, not by sample. The read_keys login claim lived in the config reference, the endpoints reference and the remote-provisioning guide, each in en/ko/ja/zh-cn, plus m9-admin-surface.md and the src/config.rs doc comment. The READMEs needed no change — "read-tier serves the GETs" is still true of the spend row they carry it on. site builds clean (161 pages) and nimbus-docs lint reports 94 files clean.

Still open for a reviewer to weigh in on: whether an operator wants a read-only dashboard reachable at all, given the revocation caveat above. The tier is opt-in by config — a deployment with no read_keys sees no behavior change at all — but a deployment that hands read keys out widely is the one that should read the rotation note before upgrading.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a read-only tier for admin sessions, allowing read-only keys to sign in to the dashboard with restricted privileges. The GET /admin/api/session endpoint now returns the session's access tier (read or write), which the frontend uses to conditionally render write affordances. Server-side mutations continue to enforce write permissions, returning a 403 Forbidden for read-only sessions. Documentation and tests have been updated globally to reflect this change. The review feedback suggests adding a defensive fallback to 'read' access in the frontend session initialization to ensure type safety and prevent unauthorized write access if the tier is undefined.

Comment thread ui/src/App.tsx Outdated
@codecov

codecov Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 83 untouched benchmarks


Comparing amondnet/ui-frame (5f8b467) with main (6e2b455)

Open in CodSpeed

Sessions recorded no privilege, so `authenticate` returned `Write` for every
cookie and `login_submit` had to refuse `read_keys` outright: while a session
carried full access unconditionally, minting one from a read key would have
escalated it. That left read-key operators with curl and no dashboard at all.

`SessionStore` now records the tier the minting credential carried and
`authenticate` reads it back, so the login form accepts a read key and mints a
read session. Every mutation still goes through `require_write` -- that gate,
not the dashboard's good manners, is what makes the session read-only, and all
seven mutation routes were re-checked to still carry it. OIDC keeps minting
write sessions: nothing in its allow lists distinguishes a read-only identity,
so deriving one there would invent a policy the config cannot express.

`GET /admin/api/session` now carries `access`, which the dashboard renders its
write affordances from. It is a display signal only.

The read-key test's login half changes with the behavior it covers, from "a
read key is refused a cookie" to the stronger property that replaces it: the
read session reaches a mutation with a valid cookie, a matching CSRF token and
a same-origin request, and is still refused.

BREAKING CHANGE: `POST /admin/login` answers `303` with a session cookie for a
`[server.admin] read_keys` credential where it previously answered `401`. The
session is read-tier and every mutation still answers `403`, so the cookie
carries no permission the read key did not already have through the header
slot -- but it carries its own lifetime. Browser sessions are validated against
the in-memory session store alone, so rotating a compromised read key now stops
its header credential at the next reload while its cookie goes on reading the
admin surface until `session_ttl_secs` (default 1h) elapses; restart rather
than reload to drop it. A deployment that treated "read keys cannot open a
browser session" as a revocation property no longer has it. Dropping stale
sessions on reload is tracked in #100, which this widens from write-tier
credentials to both tiers.
…n tier

`GET /admin/api/session` now reports whether the session may mutate, so the
bundle no longer offers actions that will come back `403`. A read session keeps
every table it can read and loses the two add-account forms, the row actions,
and the actions column those buttons lived in -- dropped rather than left
standing, since a header row one cell wider than its body rows is a rendering
bug, not a cosmetic one. It also gets a sentence saying why, because a section
that merely lost its buttons reads as a broken page.

`useCanWrite` is the single place the tier is interpreted, and it compares
against `write` rather than against `read`: an unrecognized value then reads as
read-only instead of unlocking the page. This hides affordances and protects
nothing -- `require_write` on the server is the enforcement.

The suite asserts both tiers against the same fixtures. A read-only assertion
alone would pass just as well against a dashboard that had lost its write
affordances for everyone, so each property is pinned from both directions; the
harness accordingly defaults to a write session, which is the page every suite
written before the tier existed was asserting against.
…sion

Two surfaces asserted what the previous commits changed, in every locale, and
both are now false: `read_keys` "cannot sign in: `POST /admin/login` rejects it
with `401`", and `/admin/api/session` returns "the two per-session values". The
sweep was column-wise rather than by sample -- the claim lives in the config
reference, the endpoints reference and the remote-provisioning guide, each in
en/ko/ja/zh-cn, plus `m9-admin-surface.md` and the `read_keys` doc comment in
`src/config.rs`. The READMEs needed no change: "read-tier serves the GETs" is
still true of the spend row they carry it on.

`admin-ui-delivery.md` gains Decision 6, which fixes the stack above
Resolution 2's React + Vite: Base UI for primitives, TanStack Router landing
with the deep links Resolution 6 already promised, and Tailwind v4 + shadcn/ui
conditional on `html.rs` generating its stylesheet instead of hand-keeping a
copy of `index.css`. Cloudflare Kumo is rejected with its reasons. Two
constraints bound the whole table and both are already in this document -- the
bundle ships inside the binary, and the shell's CSP has no `'unsafe-inline'`
for styles, which refuses a CSS-in-JS runtime outright.

It also records what the stack does *not* decide, because naming one would
otherwise obscure it: monitoring is blocked on `storage.md` (every observable
is a point-in-time value and `metrics.rs` emits to sinks, not a scrape
endpoint), and access-permission management is display-only until the same
decision, since a UI cannot mint a key that the config refuses as a literal.
Opening the login form to `read_keys` put read-tier credentials inside a gap
the rotation note only described for admin tokens: sessions are validated
against the in-memory store alone, so a rotated key's cookie keeps reading
until `session_ttl_secs`. The note's closing sentence was already true in the
abstract and vacuous for read keys, since a read key could not hold a session
at all -- it is now load-bearing for them, and a reader deciding whether a read
key is safe to hand out widely needs it spelled out.

What survives is read-only and strictly less than the full access a write-tier
session already carried across the same window, so this widens #100's scope
rather than adding a defect it does not already track.

Found by an adversarial review of this branch.
@greptile-apps

greptile-apps Bot commented Sep 12, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

The PR is not safe to merge without addressing the widened credential-revocation gap, because a removed read key can retain administrative read access through an existing cookie.

Fix All in Claude CodeFindings

  1. P1 Security Read sessions survive revocation
Fix with agent prompt
### Issue 1
src/admin/mod.rs:203-206
Allowing read credentials to mint independent sessions means rotating or removing a compromised read key no longer revokes all of its access. Cookie authentication checks only the process-lifetime session store, which survives configuration reloads, so the former key holder can continue reading account metadata, usage, pool state, and upstream status until the session expires. Because `session_ttl_secs` has no upper bound, this exposure can last much longer than the one-hour default. Sessions should be invalidated when their originating credential is revoked or bound to a credential generation.

**How this was verified:** A read key’s tier is copied into a new session, and cookie authentication later checks only that session’s expiry without consulting the current admin keyring.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

  • Read and write sessions retain distinct server-enforced privileges.
  • The React dashboard centralizes tier interpretation in useCanWrite.
  • Read-only tables remain available while account-management controls are removed.
  • The change widens the existing session-revocation gap to read credentials.

Diagram

sequenceDiagram
    participant O as Operator
    participant A as Admin auth
    participant S as Session store
    participant API as Admin API

    O->>A: POST /admin/login with read key
    A->>A: "Resolve access = read"
    A->>S: Create cookie session(read, expiry)
    S-->>O: Session cookie
    O->>API: "GET /admin/api/* with cookie"
    API->>S: Lookup session
    S-->>API: "access = read"
    API-->>O: Read-only admin data
    O->>A: Rotate/remove read key and reload
    Note over A,S: Current keyring changes, session store survives
    O->>API: "GET /admin/api/* with old cookie"
    API->>S: Lookup checks only expiry
    S-->>API: "access = read"
    API-->>O: Data remains accessible until session expiry
Loading

Reviews (1) · Last reviewed commit: "docs(admin): name read keys in the emerg..."

Comment thread src/admin/mod.rs

@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: a995ffc07d

ℹ️ 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 src/admin/mod.rs

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 28 files

Architecture diagram
sequenceDiagram
    participant Browser
    participant AdminAPI as Admin API Server
    participant SessionStore as SessionStore
    participant Auth as AdminAuth
    participant Config as Config
    participant Dashboard as Dashboard UI

    Note over Browser,Dashboard: Admin Dashboard Session Flow with Access Tiers

    Browser->>AdminAPI: POST /admin/login (token)
    AdminAPI->>Auth: login_access(token)
    Auth->>Config: authenticate_value(token)
    Config-->>Auth: Option<AdminAccess> (Read or Write)
    Auth-->>AdminAPI: access tier
    alt Valid credential
        AdminAPI->>SessionStore: create(ttl, access)
        SessionStore-->>AdminAPI: (sid, csrf)
        AdminAPI-->>Browser: 303 + Set-Cookie (shunt_admin_session=sid)
    else Invalid token
        AdminAPI-->>Browser: 401
    end

    Note over Browser,Dashboard: Session Bootstrap - Dashboard Render

    Browser->>AdminAPI: GET /admin/api/session (cookie)
    AdminAPI->>SessionStore: lookup(sid)
    alt Valid session
        SessionStore-->>AdminAPI: SessionAuth { csrf, access }
        AdminAPI-->>Browser: { csrf, expiry_buffer_ms, access }
        Browser->>Dashboard: Render with access tier
        alt access = write
            Dashboard->>Dashboard: Show add forms, row actions, actions column
        else access = read
            Dashboard->>Dashboard: Hide add forms, row actions, actions column
            Dashboard->>Dashboard: Show read-only notice message
        end
    else Invalid/expired
        AdminAPI-->>Browser: 401
    end

    Note over Browser,AdminAPI: Mutation Attempt (enforcement stays server-side)

    Browser->>AdminAPI: POST /admin/api/accounts/claude (cookie + CSRF + same-origin)
    AdminAPI->>SessionStore: lookup(sid)
    SessionStore-->>AdminAPI: SessionAuth { csrf, access }
    AdminAPI->>AdminAPI: require_write(access)
    alt access = write
        AdminAPI-->>Browser: 200 Success
    else access = read
        AdminAPI-->>Browser: 403 Forbidden
    end

    Note over Browser,AdminAPI: OIDC Login (always mints write session)

    Browser->>AdminAPI: GET /admin/oidc/callback (OIDC authorize)
    AdminAPI->>AdminAPI: Validate OIDC allowlist
    AdminAPI->>SessionStore: create(ttl, AdminAccess::Write)
    SessionStore-->>AdminAPI: (sid, csrf)
    AdminAPI-->>Browser: 303 + Set-Cookie (write-tier session)
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/admin/mod.rs
Comment thread docs/m9-admin-surface.md
Comment thread src/admin/mod.rs
`readJson` casts the response body rather than validating it, so the `access`
field reaching `Session` is asserted to be an `AdminAccess`, never checked. The
declared type says `'read' | 'write'`; a response without the field puts
`undefined` behind it. Defaulting to the *lower* tier is what keeps that
mismatch harmless, and the direction is the whole point -- the page then hides
its write affordances rather than unlocking them.

This changes no behavior today: `useCanWrite` compares against `'write'`, so
`undefined` already read as read-only, and an unrecognized value still does
without needing a default. What it buys is the runtime value matching its
declared type, so a future guard written as `access !== 'read'` -- the natural
spelling, and the one that would silently escalate -- starts from a value that
is always one of the two.

Reported by gemini-code-assist on #541.
…ated

Opening `POST /admin/login` to `read_keys` falsified a claim that was mirrored
across six places, and the previous commits updated only the published docs and
the `m9` tier bullet. The source-level contracts still said the opposite, which
is the worse half: a maintainer reads the Rustdoc, not the site.

- `src/admin/mod.rs` module doc claimed the read tier is refused "on every
  mutation, the login form included".
- The `session_bootstrap` Rustdoc still called its response "the two
  per-session values"; it has returned three since `access` was added.
- `m9-admin-surface.md` asserted in three further places that login needs a
  write-tier credential and that "a session therefore always carries write
  access".
- `m4-inbound-auth.md` and `src/auth/slots.rs` carry the same sentence about
  non-header accept channels, and both named a write-tier form credential via
  `authenticate_login_token` -- a function this branch renamed to
  `login_access`. Neither bot flagged those two; they turned up only on a
  column-wise sweep for the claim rather than for the files under review.

The emergency-rotation limitation also gains the bound Greptile supplied:
`session_ttl_secs` is a bare `u64` with a default and `Config::validate` does
not range-check it, so the window a rotated read key's cookie survives is
whatever the deployment set, not the 1h default.

Reported by chatgpt-codex-connector, cubic-dev-ai and greptile-apps on #541.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 5 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread docs/m4-inbound-auth.md

@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: 0ed3a96077

ℹ️ 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 src/auth/slots.rs
The header-slot enumeration describes `cookie` as an accept slot to justify
stripping it on every forward, and every copy called what it accepts a
**write-tier** `shunt_admin_session`. A `read_keys` login now mints a read-tier
session, so the qualifier is false wherever it appears.

The strip itself is unchanged and still correct -- widening what `session_cookie`
accepts cannot break `strip ⊇ accept` when the whole header is removed. This is
the enumeration's accuracy, not its behavior.

Both bots pointed at one instance each; grepping for the *term* rather than for
the files under review found six, in two languages of contract:

- `docs/m4-inbound-auth.md` (the `cookie`-strip paragraph, which contradicted
  the accept-slot table two rows above after the previous commit corrected it)
- `src/auth/slots.rs` twice -- the module enumeration and the `RESERVED_SLOTS`
  rationale -- and the comment on `no_forward_site_relays_the_admin_session_cookie`
- `site/.../guides/inbound-codex-endpoint.md` and `guides/shared-gateway.mdx`,
  each with its ko/ja/zh-cn copies, since the claim is user-facing there

Reported by cubic-dev-ai and chatgpt-codex-connector on #541.

@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: d2e62f6a1b

ℹ️ 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 ui/src/components/ClaudeAccounts.tsx
The remote-provisioning guide walks an operator through the Claude and Codex
tables and says every row offers **Re-login**, and that `imported` rows also
carry **Refresh**. A read-tier session renders neither -- `useCanWrite` gates
the row-action cell, its header, and both add-account forms -- so the guide
described a page a `read_keys` operator does not see.

Both passages now say so, and the table walkthrough closes by naming what a
read session gets instead: the same tables and the same Status column, minus
the row actions and the add forms, because `require_write` refuses those
mutations server-side anyway.

ko/ja/zh-cn carry the same two passages and are updated with it, per AGENTS.md.

Reported by chatgpt-codex-connector on #541.

@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: 95486a8f7a

ℹ️ 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 src/admin/mod.rs
Comment thread src/admin/mod.rs
`POST /admin/login` is a navigation form POST, so it carries no `x-csrf-token`
and had no same-origin guard -- unlike `logout` three functions below, whose
comment already spells out why that combination needs one. `SameSite=Strict`
decides whether the browser *sends* an existing session cookie; it does not stop
the browser storing the `Set-Cookie` a cross-site form submission gets back. So
anyone holding a valid credential could submit that form from their own page and
replace a visitor's session with one of their choosing.

That asymmetry cost nothing while only write keys could sign in: a write-key
holder replacing an operator's cookie gains nothing they do not already have.
Opening the form to `read_keys` gave it a point. A read key goes to someone
deliberately granted less privilege, and this was its one lever against a write
operator -- silently downgrading that dashboard to read-only, with every write
failing `403` until they signed in again.

The guard runs before the rate limiter, so a cross-site flood cannot spend the
operator's login budget. Scripted logins are unaffected: `same_origin` returns
`true` when neither `Sec-Fetch-Site` nor `Origin` is present.

The test asserts both tiers -- the guard is about the request's origin, not
about privilege, and one that refused only read keys would pass a read-only
test. Verified falsifiable: removing the guard fails it on the cross-origin
assertion rather than on a compile error.

Also fixes the README capability row, which listed only `tokens_env`,
`tokens_file` and `write_keys` as bringing the dashboard up. A `read_keys`-only
`[server.admin]` now brings it up read-only, so the row said a supported
configuration was unavailable. Updated in all four READMEs.

Reported by chatgpt-codex-connector on #541.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 11 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/admin/mod.rs

@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: 606c146f6f

ℹ️ 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 ui/src/App.tsx
`access` became a third value `GET /admin/api/session` returns, but the
component's header comment still described the response as the CSRF token and
the refresh buffer. It now names all three and what each is for, including that
a `read_keys` login means a cookie no longer implies write.

`ui/README.md` already listed the three; this was the source-level copy.

Reported by chatgpt-codex-connector on #541.

@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: b4b9e63f7b

ℹ️ 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 src/admin/mod.rs
Conflict in `tests/admin_surface.rs`, in the read-key login test both sides
changed. Resolved by keeping this branch's assertions and adopting main's
mechanism:

- main (#542) replaced raw `std::env::set_var`/`remove_var` with the
  `common::EnvVars` RAII guard, which restores on drop, and added
  `tests/env_lock_coverage.rs` — a structural gate that fails on any remaining
  raw call. The test header merged to the guard cleanly; the two trailing
  `remove_var` calls inside the conflicted block are dropped as the guard now
  covers them. They were cleanup, not preconditions, so nothing is asserted less
  (the distinction #542's own review round turned on).
- main's side ended with a plain "the write key does log in" check. This
  branch's write half supersedes it: it asserts the same login plus the tier the
  session reports and that the same mutation is *not* refused, which is what
  keeps the read-half assertions non-vacuous.
- This branch's cross-origin login assertions are kept in full.

Verified on the merged tree: 2582 passed, 0 failed; fmt, clippy, site build
(161 pages), nimbus-docs lint (94 files), UI typecheck and 74 UI tests all green.
@sonarqubecloud

Copy link
Copy Markdown

@amondnet
amondnet merged commit 363434b into main Sep 14, 2026
15 checks passed
@amondnet
amondnet deleted the amondnet/ui-frame branch September 14, 2026 01:24
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