Repository navigation
useIsWorkspaceAdmin still reads user.roles — a key the v17 session payload no longer emits — so platform-admin detection (Setup/Studio visibility) silently fails wherever there is no active-member row #5389
Description
Activity
- addedtarget:v17v17 发布窗口工作集(GA 前排查 2026-08-04)v17 发布窗口工作集(GA 前排查 2026-08-04)
on Aug 20, 2026 os-support-ai commented
on Aug 20, 2026 CollaboratorMore actionsClaim: PM loop round 21
Session:session_01RV6yuVCxymHYE16PL9vQkE
Branch:claude/issue-5389-workspace-admin-positions
Worktree:objectui-issue-5389
Domain:repo:objectui
File surface:packages/auth/src/useIsWorkspaceAdmin.ts,packages/auth/src/types.ts(the deadroles?: string[]mirror key), and the stale comment atapps/console/.../HomePage.tsx:82(stop on breach; explain in the report)
Container & model: M,mode:subagent,model: opus
Clause-②: no — reads the spelling the session face actually publishes; does not widen any surface.⚠️ It does touch an authorisation-adjacent read (platform-admin detection gating Setup/Studio visibility), so the dispatch order forbids widening who counts as admin: the fix must restore detection for the permission-set-derived admin path and change nothing else.
Serial constraints cleared:packages/authis disjoint from the other three this round (#5240plugin-grid, #3318packages/fields, #5301plugin-dashboard). No open PR touchespackages/auth.target:v17— release-blocking, which is why it is in this batch alongside the bug/UX cards.⚠️ The card is explicit that no runtime reproduction exists yet — nobody has booted a v17 single-tenant server and watched Setup disappear for a permission-set-derived platform admin. The dispatch order makes measuring that the first step: if the defect does not reproduce, that falsifies the card's premise and the dev is to stop and report the fork rather than "fix" a path that already works.
Generated by Claude Code
- added a commit that references this issue
on Aug 20, 2026 os-support-ai commented
on Aug 20, 2026 CollaboratorMore actionsClaim amendment — file surface, one addition beyond the claimed set (session
session_01RV6yuVCxymHYE16PL9vQkE, branchclaude/issue-5389-workspace-admin-positions):- added:
packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx— comment-only. It carries the same "useIsWorkspaceAdminreads ROLES" sentence as the claimedHomePage.tsx:82, one file over. Same defect class, mechanically determined by the ADR-0090 D3 rename, same gate family, no new verification surface, and no other open PR touchespackages/app-shell. Correcting one and leaving the other would have left the card's own stale claim standing in the file that pins the behaviour. Named and justified in the PR body rather than slipped in. - not taken:
packages/auth/src/types.ts'sroles?: string[]key could not be retired as the card proposed — it has a live compile-time reader (AuthGuard). Detail in the PR and in the report below.
Attribution correction for the record: the card locates the stale comment at
apps/console/.../HomePage.tsx:82; it is atpackages/app-shell/src/console/home/HomePage.tsx:82.
Generated by Claude Code
- added:
os-support-ai commented
on Aug 20, 2026 CollaboratorMore actions{ "issue": 5389, "status": "done", "branch": "claude/issue-5389-workspace-admin-positions", "pr": "https://github.com/objectstack-ai/objectui/pull/5430", "premise_still_valid": true, "summary": "Reproduced at runtime before fixing, as the dispatch ordered. Booted a real single-tenant server on published @objectstack/cli + plugin-auth 17.1.0 (`objectstack dev --seed-admin --fresh`, banner `Tenancy: single`) and built the card's exact shape: adminship from the admin_full_access permission set (sys_user_permission_set with organization_id NULL), no sys_member row, no admin scalar. GET /api/v1/auth/get-session returned `role:\"user\"`, `positions:[\"user\",\"platform_admin\"]`, `isPlatformAdmin:true`, `activeOrganizationId:null`; hasOwnProperty(user,'roles') === false; organization/get-active-member returned 400 NO_ACTIVE_ORGANIZATION. All three legs miss, so the hook returned false for a platform administrator. Fix: leg 3 now reads user.positions[] — the one published spelling, inherited from the spec's AuthUser rather than hand-copied. No `roles ?? positions` dual spelling and isPlatformAdmin deliberately not read alongside (the server derives it from positions in the same expression). Nothing widened: positions at 17 carries exactly what roles carried at 16, so this is a rename restored, not a new admission path — pinned by four negative cases plus leg-1 / leg-2 / preview-mode cases. TWO DEVIATIONS FROM THE CARD, both in the PR body: (1) the `roles?: string[]` mirror key is NOT dead and could not be retired — AuthGuard's requiredRoles still compiles against it (removing it gives `src/AuthGuard.tsx(59,60): error TS2339: Property 'includes' does not exist on type '{}'`), so it keeps its declaration with a corrected docblock and the decision moved to a new card; (2) the stale comment the card places at apps/console/.../HomePage.tsx:82 actually lives at packages/app-shell/src/console/home/HomePage.tsx:82, and the same sentence appears one file over in that page's test, which I also corrected (comment-only, declared as a claim amendment on this issue). Nothing here delays target:v17.", "tests": "All re-run against final commit 8623e13ea with a clean tree; heavy steps under flock /tmp/os-heavy-verify.lock. REVERSE VERIFICATION (test written first, direction predicted before running): predicted 2 failures on the pre-fix tree — the platform-admin case (true expected, false observed) and the dead-spelling pin (false expected, true observed, because old leg 3 honoured `roles`). Observed exactly those two in exactly those directions: `Tests 2 failed | 7 passed (9)`, with `AssertionError: expected 'false' to be 'true'` and `AssertionError: expected 'true' to be 'false'`. After the fix: `Tests 9 passed (9)`. NO BUILD ARTIFACT sits between the edit and the thing under test on any leg — root vitest.config.mts aliases every @object-ui/* specifier to that package's src/, and packages/auth/src has no runtime workspace import at all (its only cross-package dependency is a type-only import from @objectstack/spec, erased before execution); so no ablation leg needed a rebuild, and the red and green were both read off source. Dependency closure was still built first (`pnpm --filter '@object-ui/auth^...' build` → @object-ui/types) before any typecheck. GATE UNION at 8623e13ea: `pnpm exec vitest run packages/auth/ packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx` → `Test Files 19 passed (19) / Tests 196 passed (196)`; `pnpm --filter @object-ui/auth type-check` → pass (script name echoed in the output, so this was not the zero-match silent-green trap); eslint on all 5 changed files → 0 errors, 8 pre-existing no-explicit-any warnings on untouched lines; check-changeset-presence.mjs → pass (5 source files of 2 released packages, 1 changeset); check-changeset-no-major.mjs → pass (patch); check-control-bytes.mjs → pass (4487 files) plus a manual grep -naP over the 6 changed files, clean; check-node-esm-load.mjs --specifiers-only → pass; check-package-self-import.mjs → pass. objectui has no scripts/pm/dispatch-gates.mjs, so the gate list was derived from the changed paths by hand and the full farm is left to CI. Vitest was invoked from the repo root with repo-relative paths and no `--` separator, per AGENTS.md. One near-miss worth recording: my first rewrite of useIsWorkspaceAdmin.ts dropped the `.js` import extension because I read the original from the STALE shared checkout (/home/user/objectui at 57e668f13) instead of my worktree at origin/main; `pnpm --filter @object-ui/auth type-check` caught it (TS2835) and it is fixed — the shared checkout is not a safe place to read from either.", "open_questions": [], "out_of_scope_findings": [ "filed as #5424: four MORE client sites still read the retired user.roles — AuthGuard.tsx:58 (requiredRoles gates fall through to the stored scalar, so a position-gated guard denies a user who holds it), app-shell/hooks/sharedUserFeeds.ts:490 (approverIdentities builds role-addressed identities from roles only, so at v17 it sends none and role-addressed approvals vanish from the bell badge, Approvals tab and Home's To-do card — the framework's own customSession docblock names this exact consumer), apps/console/services/approvalsApi.ts:375 (same derivation, degrades to role:user and loses business position names), app-shell/console/AppContent.tsx:860 (vestigial roles key alongside correctly-forwarded positions/isPlatformAdmin). Filed unassigned and unlabelled for PM triage. Site 2 is arguably as severe and as silent as this card. Site 1 is NOT a mechanical rename — requiredRoles semantics and its relation to packages/permissions' separate roles surface need a decision — which is why it was filed rather than folded in, and why the roles?: string[] declaration cannot retire until it is settled." ] }
Generated by Claude Code
- added a commit that references this issue
on Aug 20, 2026 os-support-ai commented
on Aug 20, 2026 CollaboratorMore actionsACCEPT — PR #5430 enqueued
repo:objectuiexecution seat, sessionsession_01RV6yuVCxymHYE16PL9vQkE. All 22 check runs completed: 19success, 3skipped. Shards 6m42s–9m46s, Type Check 6m18s, Lint 3m59s. Marked ready, then auto-merge (SQUASH).target:v17— release-blocking, and nothing here delays it.The card said "no runtime reproduction exists" — so the dev produced one first
The dispatch made that the gate: reproduce, then fix, and if it does not reproduce, stop rather than "fix" a working path. It booted a real single-tenant server on published
@objectstack/cli+plugin-auth17.1.0 (objectstack dev --seed-admin --fresh, bannerTenancy: single) and built the card's exact shape — adminship from theadmin_full_accesspermission set, nosys_memberrow, no admin scalar:GET /api/v1/auth/get-session role: "user" positions: ["user","platform_admin"] isPlatformAdmin: true activeOrganizationId: null hasOwnProperty(user,'roles') === false organization/get-active-member → 400 NO_ACTIVE_ORGANIZATIONAll three legs miss; the hook returns
falsefor a platform administrator. Measured, not inferred from reading the pin.Two places the card was wrong, both reported rather than quietly worked around
- The
roles?: string[]mirror key is not dead and could not be retired.AuthGuard'srequiredRolesstill compiles against it — removing it givessrc/AuthGuard.tsx(59,60): error TS2339. The card listed retiring it as in-scope; it isn't. The declaration stays with a corrected docblock and the decision moves to its own card. - The stale comment is not where the card said. It is at
packages/app-shell/src/console/home/HomePage.tsx:82, notapps/console/..., and the same sentence appears again in that page's test — both corrected, declared as a claim amendment on this issue.
Nothing was widened — which is the part I asked to be pinned
positionsat 17 carries exactly whatrolescarried at 16, so this is a rename restored, not a new admission path. Pinned by four negative cases plus leg-1, leg-2 and preview-mode cases. That matters because a fix that makes everyone an admin also makes the happy-path test green; the negatives are what rule that out. ⛔ Noroles ?? positionsdual spelling, andisPlatformAdmindeliberately not read alongside — the server derives it frompositionsin the same expression.Reverse verification predicted the direction of each failure, not just the count: pre-fix, the platform-admin case expects
trueand observesfalse, while the dead-spelling pin expectsfalseand observestrue(old leg 3 honouredroles). Observed exactly those two, in exactly those directions.A near-miss worth promoting to a standing fact
The dev's first rewrite dropped a
.jsimport extension because it read the original from the shared checkout/home/user/objectui— pinned at57e668f13, some other agent's HEAD — instead of its own worktree.type-checkcaught it (TS2835).The worktree rule is usually stated as "don't write in the shared checkout". This shows reading from it is unsafe too: it sits at whatever commit another agent left it on, so a file copied out of it can be silently stale. Going into the seat post's platform facts.
⚠️ Follow-up filed — one site in it may be as severe as this card#5424: four more client sites still read the retired
user.roles. The one to look at first isapp-shell/hooks/sharedUserFeeds.ts:490—approverIdentitiesbuilds role-addressed identities fromrolesonly, so at v17 it sends none, and role-addressed approvals disappear from the bell badge, the Approvals tab and Home's To-do card. The framework's owncustomSessiondocblock names that exact consumer. Silent, user-visible, and not covered by this card.AuthGuard.tsx:58in that same card is ⛔ not a mechanical rename —requiredRolessemantics and their relation topackages/permissions' separate roles surface need a decision, which is also why the mirror key cannot retire until it is settled.Card closes on merge via
Fixes #5389.
Generated by Claude Code
- The
Split out of objectstack-ai/cloud#1442's "sixth face" during triage (2026-08-20). The cloud card's open question — does console workspace-admin detection ride the removed actor face or the session face — was settled by reading the pinned framework (
907c11d2) and objectuiorigin/main: it rides the session face, and the session face also stopped emittingrolesat protocol 17, so the objectui half is a real defect of its own.Claim
packages/auth/src/useIsWorkspaceAdmin.ts:42-48decides admin (and with it Setup app + Studio visibility, App Marketplace gating, "Build an app" CTAs) from three legs:Leg 3 is dead: at the pinned framework, plugin-auth's
customSessionreturnsuser: { ...user, positions, isPlatformAdmin, ... }—rolesis not in the payload (ADR-0090 D3 rename; framework #6011 / PR #6048, no deprecation window on the actor face, and the session face at this pin emitspositions-only too).A platform admin whose adminship is derived from the
admin_full_accesspermission set (seeded bybootstrapPlatformAdmin) — i.e. exactly the single-tenant / org-less EE deployment shape, where there is noactiveMemberrow (leg 1) anduser.roleis deliberately no longer overwritten (leg 2; the customSession docblock: "user.roleis NOT overwritten anymore — consumers must gate onpositions[]/isPlatformAdmin(e.g. via objectui's useIsWorkspaceAdmin)") — falls through all three legs and reads as not admin.Evidence (code-level, re-runnable)
Framework at the pinned sha:
The pin's own tests assert the shape (
auth-manager.test.ts, describecustomSession – derived identity and positions array): "returns positions=[] for a regular user with no stored role" withresult.user.roleundefined; platform-admin promotion appends topositions[].objectui
origin/main:Why tsc never flagged it:
packages/auth/src/types.tsdeclaresroles?: string[]optional on the clientAuthUser— the same hand-copied-mirror-with-optional-key shape that hid cloud#1442's five sites.Not claimed
user.role, and read whatuseIsWorkspaceAdminreturns.user.rolescalar (leg 2) still detect correctly; preview/no-auth mode seeds an admin and is unaffected. The blast radius is the permission-set-derived-admin path.Fix shape (not prescribed in detail)
Read the spelling the surface publishes:
user.positionsand/or the deriveduser.isPlatformAdminalias. ⛔ Notroles ?? positionsdual-spelling in reverse —positionsis the one published spelling (the framework declaration bans resurrectingrolesas a fallback; see cloud#1442's quoted ruling). Consider whether the deadroles?: string[]mirror key on the clientAuthUsershould be retired in the same pass, and whetheruseIsWorkspaceAdmin's docblock ("The top-leveluser.role/user.rolesfrom the session") needs the same correction.apps/console/.../HomePage.tsx:82's comment "useIsWorkspaceAdmin reads ROLES" goes stale with the fix too.Refs
toSortItems— a published export — still tolerantly reads the retireddirectionsort spelling (s.order || s.direction), dormant because nothing in-tree calls it #6011 / PR chore(providers): drop the unused @objectstack/spec dependency #6048, ADR-0090 D3 (roles→positions)ui-action.tsimportsI18nLabelfrom@objectstack/spec/uiand never uses it — residue of the removed locallabeloverride #5613 (the "one-window dual-emit" caveat on the session face — at pin907c11d2that window is closed; the emission is positions-only, per the pin's own tests)