You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
platform-objects: the Setup Users list has no column that says who holds platform-admin standing, now that the legacy role scalar leaves it (#28105 D1 follow-up) #28793
Ruled: 6566734829 · letter C · 2026-11-02T01:16Z
Ruling-ref: 6566734829
Filing gate: ① the follow-up card the director's ruling on #28105 binds (comment 6557746678, letter D1, maintainer 「同意」 on 2026-11-01 at 12:10Z): "a follow-up card is filed at p3". Filed by domain:server seat 5, the seat executing #28105. Graded p3 by the ruling.
Reach: every Setup admin scanning the Users list for who is a platform admin, the need #27863 stated.
Reader who acts: domain:server (packages/platform-objects for the list view; packages/core owns the predicate).
What is true today (read on origin/maind9c4cdc4e)
Platform-admin standing is decided by hasPlatformAdminStanding (packages/core/src/security/resolve-authz-context.ts:2145): a verified email in OS_PLATFORM_OWNER_EMAIL, or, under the single tenancy posture, an unscoped admin_full_access assignment in sys_user_permission_set.
The Users list gains a column (or an equivalent served mark) derived from real platform-admin standing, read through the same predicate, never a second copy of the rule.
Measure first how a derived, non-stored column is served on a platform list view today (a formula field, a served-beside key, or a computed read) and pick the existing mechanism; say which in the PR.
Blocked-by: #28105 (it removes the role column this card's column replaces).
Dedupe: searched platform admin column users list; only #28105 and #27855. New.
Claim: PM loop round 1 · card thread · 2026-11-01T22:06Z
Session: session_01FEkQ5oc56xChD6Wrc4cGSt (the seat); the card thread is session_01FEkQ5oiX3eWpVYxchQ9C3V
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-7qef1r (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#5
Ruling-ref: 6557746678 (director ruling on #28105, letter D1, which binds this follow-up at p3) and the seat's rulings in the order; pick announced to seats 1-3, seats 1 and 3 confirmed no clash.
File surface: packages/platform-objects/src/identity/ (the sys_user object and its tests), the read seam that serves the derived value (measured by the dev: packages/objectql, packages/metadata-protocol or packages/core/src/security), changesets, any content/docs/** page listing the Users columns. ⛔ No packages/spec/**.
Container & model: M, mode:thread, model and effort as the thread was opened
Clause-②: no (expected; the dev corrects it from the diff if a published shape widens)
Metadata-protocol: none
Responsibility: the code is the sys_user Users list and the read path that can serve a derived value beside it · no column says who holds platform-admin standing, now that the legacy role scalar is gone · reached by every Setup admin opening Users
Thread-read: 6564514014
Premise: holds (read at origin/main1e019240f, which contains PR #28810: all_users has no role column; hasPlatformAdminStanding is per-user, async, fail-closed)
Serial constraints cleared: none needed.
Resume-from: none — fresh from origin/main
State: pm:queue → pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28793.md in the project.
os-dev-report
{
"issue": 28793,
"status": "needs_decision",
"branch": "claude/project-thread-7qef1r",
"pr": null,
"session": "session_01FEkQ5oiX3eWpVYxchQ9C3V",
"premise_still_valid": true,
"summary": "Stopped before code at the ruling 3 exit, on seat 5's instruction (no file surface amendment). Measured at origin/main 1e01924: the premise holds (all_users has no standing column; hasPlatformAdminStanding is per-user, async, fail-closed), but no existing mechanism serves a per-row derived value as a list column without a new pattern. PM assumption 1 is false: a columns entry must name a DECLARED field (lint validate-list-view-field-refs errors; clients echo columns as the projection, which REST answers 400 INVALID_FIELD for an unknown plain column). The only declared field type with no column is formula (driver-sql schema-drift.ts fieldHasColumn), and CEL cannot call the predicate. No production read seam serves a per-row derived key today: the only afterFind hook in production is plugin-audit read-audit (observes, does not shape); plugin-approvals payload-redaction-middleware rewrites a stored column on result rows. Branch carries no commit; nothing pushed.",
"tests": "No repo test added (no code). One scratch measurement, deleted after the run: real ObjectQL + SqlDriver better-sqlite3 in-memory + real SecurityPlugin (fallback member_default), sys_user/sys_member/sys_organization + the security plugin's grant objects, 50 users, Default Organization org_default, admin_full_access grant owned by org_default on usr_000, the same set granted on org_other to usr_002, median of 7 runs on the shared box. Lock VERDICT command-exit 0, Tests 2 passed. Readings: (1) grant-only: the all_users page read (8 columns, limit 50) = 0.99 ms and 1 driver read; hasPlatformAdminStanding over the page = 81.5 ms concurrent / 109.8 ms sequential and 200 driver reads (4 per user); answers: only usr_000 true, usr_002 (grant scoped to a non-default org) false. (2) OS_PLATFORM_OWNER_EMAIL = usr_001's verified email: page 0.66 ms / 1 read; predicate 53.0 ms concurrent / 75.7 ms sequential, 200 reads; answers usr_000 and usr_001 true. So per-row evaluation multiplies the page's reads by about 200x and its latency by about 50-100x on in-memory SQLite; on a networked database every read is a round trip. Not cheap under ruling 4. (3) Engine-level filter on a formula field (sys_member.display_title, system context) = INVALID_FIELD 400 'filters on display_title, a virtual formula field ... no driver materialises'. Not a stock boot: the os dev closure build failed on my own flag (see deviations), so the live probe was NOT MEASURED; it is moot with no code.",
"gates": "NOT MEASURED: no diff, so dispatch-gates --commands derives nothing and no gate applies.",
"dogfood_battery": "n/a: no PR",
"mcp_calls": "3 reads (issue_read get on #28793, issue_read get_comments on #28793 twice); no MCP write",
"api_writes": "1: this os-dev-report comment via scripts/pm/post-stamped.mjs (POST issues/28793/comments through the fleet relay); no git push, no pr_create, no label write",
"deviations": "Ran the closure build as pnpm --filter '@objectstack/example-todo^...' build --concurrency=2: pnpm forwarded the flag into each package's build script, which broke @objectstack/cli's build script (sh syntax error, lock VERDICT command-exit 2). The packages the scratch measurement imports (core, objectql, plugin-security, platform-objects, driver-sql) built; the cli dist did not, so no stock boot was measured. Nothing was committed from that build.",
"open_questions": [
{
"question": "Which mechanism serves platform-admin standing on the Setup Users list, given ruling 3 (no spec edit, existing mechanism) and ruling 4 (bounded cost)?",
"options": [
"A. A platform_admin formula field on sys_user (returnType boolean, constant false expression as the fail-closed default, sortable false, Admin field group so the shipped FLS withholds it from org peers like phone_number) whose value an afterFind hook registered by plugin-auth fills on a user-context read from hasPlatformAdminStanding(ql, row.id). Files: platform-objects sys-user.object.ts + the four translation bundles (en/zh-CN/ja-JP/es-ES, check-i18n-bundles --write needs a full build), plugin-auth new module + auth-plugin.ts registration + tests. Spec edit: no. ui edit: no. Cost: 200 extra driver reads per 50-row page, 53-110 ms on in-memory SQLite vs about 1 ms for the page itself. Filter and sort: refused, not silently empty: engine filter on a formula = INVALID_FIELD 400 (measured), sort refused at the REST door (metadata-protocol protocol-query-validation.ts UNMATERIALIZED_SORT_TYPES) and in the engine (assertOrderByIsMaterializable). Seat 5's objections stand: the declared expression false contradicts the served cell, and every user-context sys_user read that does not name its fields (record page, unprojected reads) pays the per-row cost; system reads skip it (the predicate reads sys_user under system context, so the hook must skip them to avoid recursion).",
"B. A declared non-formula derived or virtual field kind outside spec. Measured: none exists. driver-sql fieldHasColumn answers no column only for formula; summary and autonumber are stored columns. A new kind is a FieldType edit in packages/spec, excluded by ruling 3.",
"C. No column: a served answer to 'who are the platform admins'. The predicate's own header forbids widening the per-user predicate to the population question; the one population enumerator that exists is resolveAdminUserIds inside plugin-auth last-admin-guard.ts (a closure, not exported): it enumerates the in-window admin_full_access grants owned by the Default Organization plus the owner-list users, and confirms through the resolver's predicate. Exporting it as a reader and serving it through a Setup action or route is cheap (one grant read plus the owner-list users, then the predicate for the few candidates), has no metadata contradiction and no per-row cost. Files: plugin-auth (export + door or action handler), platform-objects (action or view declaration), and a route mount if it is a new door (dispatcher-plugin.ts). Spec edit: no for an action; NOT MEASURED whether an existing action result surface renders a user list without a ui edit. Filter and sort: not applicable (a separate answer, not a list column). Risk: the candidate enumeration names the two anchor sources, so a future third anchor needs this reader updated (the admin-standing-surface.ts ledger is the existing pin for that drift).",
"D. A ui-side column fed by a dedicated read: a new door that answers standing for a page of user ids (or the population from C) and a Console grid change for the sys_user all_users view. Files: plugin-auth or rest door + dispatcher mount, ui/ grid. ui edit: yes, which ruling 6 bars unless no served mechanism can be shown. Cost: one extra request per page; server side the same 4 reads per user unless it rides C's population reader. Filter and sort: client-side over one page only.",
"E. Batch the predicate: one grant read for the page's user ids through the same resolver. Measured: no batched form of resolveUserAuthzGrants exists (it is per-user, with request memo, cross-request cache and batch-equivalence goldens keyed per user), so this is new core code in packages/core/src/security plus the same formula-field question as A for the column itself."
],
"recommendation": "C, because it is the only option that needs no spec edit, puts no contradiction into sys_user metadata and keeps the Users list read at one driver read, while answering the need #27863 states (seeing who the platform admins are). If the decision is that it must be a column on the list, A is the only no-spec, no-ui route, and it should land with E's batching later only if the per-page cost is accepted as measured."
}
],
"out_of_scope_findings": [
"carrier: seat 5 · reading, not filed: the seat's note expected a filter on a formula field to evaluate the declared constant and return nobody; measured, the engine refuses it 400 INVALID_FIELD (and the REST door refuses the sort), so option A's filter cost is a refusal, not wrong data",
"carrier: none · reading, not filed: reader population of all_users, read from plugin-security default-permission-sets.ts, not live-measured: member_default grants sys_user select under sys_user_self and sys_user_org_members (id in current_user.org_user_ids), so every org peer can list their organization's users; the Admin field group is withheld from member_default, viewer_readonly and external_default and kept for admin_full_access and organization_admin. Any option that serves standing to org admins tells an organization admin which of their members are platform operators, which is NOT MEASURED as readable to them today; options A and C both need that called out in the ruling"
]
}
The dev's measure-first report (6564729313, status: needs_decision, no commit pushed) shows ruling D1's wording, "the Users list gains a column", has no clean route under this seat's order. A list column must name a declared field; the only column-less declared kind is formula, so a column means a formula field whose declared expression contradicts the cell its hook fills, and per-row standing costs about 200 driver reads per 50-row page (53–110 ms against about 1 ms for the page). No batched resolver and no non-formula derived kind exists without a spec edit.
The seat recommends option C: no column, but a served answer to "who are the platform admins" built from the population enumerator already inside plugin-auth's last-admin-guard.ts. That changes the shape the maintainer approved in D1, so it goes to the maintainer as a decision in the seat-5 thread (options: C no column with a separate answer, recommended; A keep the column with its cost and no filter/sort; park). The privacy reading the dev flagged (serving standing to organization admins names platform operators) rides whichever is chosen.
State: pm:dispatched → needs-user-decision, seat unassigned. The dev thread keeps its worktree for a follow-up order once ruled.
Director seat (第 56 场). Presented to the maintainer in batch #450, item 3. The card came back to the inbox through seat 5's needs-decision note 6564742308 on the dev's measure-first report 6564729313, without the decision-card body (no 一句话问题, no options × workload table, no os-decision-facets block), so this seat's increment completes it. ⛔ 不是裁决。
前提复核(本席实读 origin/main00918c88):packages/core/src/security/resolve-authz-context.ts:2145 exports hasPlatformAdminStanding (per user, async, fail-closed: the OS_PLATFORM_OWNER_EMAIL list, or under the single posture an unscoped admin_full_access grant); PR #28810 (1e019240f9) removed the legacy role column from all_users, so the Users list says nothing about standing today. The dev measured (6564729313): a list column must name a declared field; the only column-less declared kind is formula, whose declared expression would be a constant false that contradicts the cell a hook fills; per-row standing over a 50-row page costs about 200 extra driver reads (53–110 ms against about 1 ms for the page, in-memory SQLite). Premise holds. Re-check: git grep -n "export async function hasPlatformAdminStanding" origin/main -- packages/core/src/security/resolve-authz-context.ts.
一句话问题: 设置里的「用户」列表现在看不出谁是平台管理员(误导人的旧 role 列已按 #28105 D1 移除);D1 当时说「加一列」,但实测加一列要么让每页多付约 200 次数据库读取,要么让元数据里声明一个与实际值相反的常量,所以要重新定:到底是要「每个用户旁边一个标记」,还是要「一张可靠的平台管理员名单」。
Governing text:#28105 ruling 6557746678 D1 ("a follow-up card is filed at p3 … the Users list gains a column"); #9652 veto 5334874978 (nothing is written into sys_user.role); ADR-0068's 2026-10-06 note and ADR-0095 D3 (PLATFORM_ADMIN standing derives from held grants, judged only by hasPlatformAdminStanding, never a second copy of the rule). 协议声明:不改 packages/spec(ruling 3 of the order),Clause-②: no,Metadata-protocol: none。
选项 × 真实代价 × 工作量:
选项
做什么
客户可感知后果
工作量
A
sys_user 加 platform_admin formula 字段(声明常量 false),由 plugin-auth 的 afterFind 钩子在用户态读取时逐行填入真实判定
Ruling: batch #450 item 3 · letter C · maintainer 「同意」 2026-11-02T01:12Z
Director seat (第 56 场; decision card completed and brief-claimed above in 6566203119, presented in the 总监决裁 thread as batch #450 item 3 with the business read-out). The maintainer answered the batch verbatim: 「同意」 = every item's recommendation, so this card's letter is C: no derived column on the Users list; a platform-admin roster answered by the one enumerator.
plugin-auth's existing enumerator (last-admin-guard.ts, resolveAdminUserIds: the admin_full_access grants in the default organisation plus the owner-email list, each confirmed through hasPlatformAdminStanding) is exported and served as a Setup action or route that answers "who holds platform-admin standing", and the Users list page carries one entry to it.
The roster is served only to a caller who holds platform-admin standing themselves; an organisation administrator is refused and learns nothing about platform operators (the privacy point in the dev's report 6564729313).
The exported enumerator is pinned in the admin-standing-surface.ts ledger, so a third source of standing reds the gate instead of silently diverging (ADR-0095 D3, one predicate, never a second copy).
Not taken: A (a column that costs about 200 extra reads per page, cannot filter or sort, and declares false in metadata while showing true), B (a new field type in spec), D (a per-page endpoint plus a ui column, ui first), E (batching the predicate but keeping A's contradiction).
Confidence gap, carried into execution: whether the action's result renders as a user list with no ui change was not measured; if it cannot, the ui part is one small follow-up PR and does not turn the letter.
Workload of the letter:domain:server · one PR (plugin-auth export + action handler + ledger pin, platform-objects action declaration and its four translation packs) · precedent: last-admin-guard.ts already runs the same enumeration; no same-shape landed PR with line counts. Plus, if needed, one small domain:ui PR for the roster rendering.
Execution:domain:server seat 5 re-dispatches on the original worktree with Ruling-ref: naming this comment; the needs-user-decision → pm:queue stroke (p3 stands) is that seat's. Acceptance: a platform administrator reaches the roster from Setup → Users in one step; the roster agrees user by user with hasPlatformAdminStanding; a non-platform-admin caller is refused; the Users list page's request count is unchanged. The Ruled: body line is written by this act.
Claim: PM loop round 10 · card thread · 2026-11-02T13:05Z
Session: session_01ADHkmQKwKFCp1m6Pb6Q8c1 (the seat); the card thread is named in a follow-up claim once it reports its branch
Account: os-steve (the seat's linked user; the card's assignee)
Branch: pending: the card thread's harness-pinned branch, written into a follow-up claim before the dev starts
Worktree: the card thread's own container checkout (the original claude/project-thread-7qef1r is gone from the remote, so the work starts fresh)
Domain: domain:server
Seat: domain:server#5
Ruling-ref: 6566734829 (batch #450 item 3 · letter C · no derived column; a platform-admin roster answered by the one enumerator)
File surface: packages/plugins/plugin-auth/src/ (last-admin-guard.ts: export the enumerator resolveAdminUserIds, plus the roster action or route handler), packages/platform-objects/src/identity/ (the Setup → Users roster action declaration and its four translation packs), packages/core/src/security/admin-standing-surface.ts (the ledger pin for the exported enumerator), their tests, changeset. ⛔ No packages/spec/**, and no sys_user field.
Container & model: M, mode:thread, model: the thread's default judgement tier, effort: high (identity surface)
Clause-②: no
Metadata-protocol: none
Responsibility: the code is plugin-auth's admin-standing enumerator and the Setup Users page · a platform administrator cannot see who else holds platform-admin standing · reached by platform operators
Thread-read: 6566734829
Premise: holds (read at origin/main5780fa49d: resolveAdminUserIds lives in plugin-auth/src/last-admin-guard.ts unexported; sys_user has no platform-admin column)
Serial constraints cleared: no open PR touches last-admin-guard.ts or admin-standing-surface.ts; the dev re-reads open PRs on these paths before opening its own.
Resume-from: none: fresh from origin/main
State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim.
Claim: PM loop round 10 · card thread · branch named · 2026-11-02T13:08Z (supersedes 6577882461, which had the branch pending)
Session: session_01ADHkmQKwKFCp1m6Pb6Q8c1 (the seat); the card thread is session_01ADHkmQKiS3hSX11hKXCKNN
Account: os-steve (the seat's linked user; the card's assignee)
Branch: claude/project-thread-ymgvae (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout (the original claude/project-thread-7qef1r is gone from the remote, so the work starts fresh)
Domain: domain:server
Seat: domain:server#5
Ruling-ref: 6566734829 (batch #450 item 3 · letter C · no derived column; a platform-admin roster answered by the one enumerator)
File surface: packages/plugins/plugin-auth/src/ (last-admin-guard.ts: export the enumerator resolveAdminUserIds, plus the roster action or route handler), packages/platform-objects/src/identity/ (the Setup → Users roster action declaration and its four translation packs), packages/core/src/security/admin-standing-surface.ts (the ledger pin for the exported enumerator), their tests, changeset. ⛔ No packages/spec/**, and no sys_user field.
Container & model: M, mode:thread, model: the thread's default judgement tier, effort: high (identity surface)
Clause-②: no
Metadata-protocol: none
Responsibility: the code is plugin-auth's admin-standing enumerator and the Setup Users page · a platform administrator cannot see who else holds platform-admin standing · reached by platform operators
Thread-read: 6577882461
Premise: holds (read at origin/main5780fa49d: resolveAdminUserIds lives in plugin-auth/src/last-admin-guard.ts unexported; sys_user has no platform-admin column)
Serial constraints cleared: no open PR touches last-admin-guard.ts or admin-standing-surface.ts; the dev re-reads open PRs on these paths before opening its own.
Resume-from: none: fresh from origin/main
State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim.
{
"issue": 28793,
"status": "done",
"branch": "claude/project-thread-ymgvae",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29427",
"session": "session_01ADHkmQKiS3hSX11hKXCKNN",
"premise_still_valid": true,
"summary": "Ruling C implemented. last-admin-guard.ts's standing reads move, text unchanged, into a module-level createStandingReader shared by the guard and the new export resolveAdminUserIds(engine, { standing: 'administrators' | 'platform-admins', maxScan }), where platform-admins skips the organization owner/admin half. POST /api/v1/auth/admin/list-platform-admins (gateAdmin, then the handler confirms the caller via hasPlatformAdminStanding; candidates confirmed one by one through the predicate; no body read) feeds a new sys_user list_toolbar action list_platform_admins whose resultDialog draws the roster as a code-list. Measured: it renders as a list with no ui change. ADMIN_STANDING_PLATFORM_ENUMERATION in core's admin-standing-surface.ts classifies every standing input as a candidate source or not, and plugin-auth's pin requires a sole-source fixture per candidate, so a third source of standing reds a gate.",
"tests": "Head 6b5dd985, after the origin/main merge and a closure rebuild, all under os-verify-lock. plugin-auth test: Test Files 212 passed, Tests 3655 passed / 10 skipped. platform-objects test: 109 files, 1507 passed. core test: 129 files, 2913 passed. typecheck on the 3 packages: exit 0. New: platform-admin-roster.pin.test.ts (12, real ObjectQL + better-sqlite3 + the real hasPlatformAdminStanding), sys-user-platform-admin-roster-action.test.ts (4), 4 classification tests in admin-standing-surface.test.ts. Ablations via ablation-replace, each restored with blob == HEAD: (1) caller check -> if(false): 4 refusal cases red, 'expected 200 to be 403'. (2) membership-half skip removed: the org-owner exclusion test red, while agreement stayed green because the predicate confirmation still drops the org owner. The first attempt was refused by the tool (replacement was a substring of the anchor) and redone. (3) owner-email half disabled: 5 red incl. agreement, expected [usr_grant] to equal [usr_grant, usr_owner]. (4) sys_member dropped from notCandidates: core classification red. (5) third candidate added in core src, core rebuilt, ablation-dist-preflight marker present in 2 dist files: the plugin-auth pin red with its 'no fixture here: teach the enumerator' message. Restore: core rebuilt, --absent marker gone from 14 files, tree clean. UI measurement: a scratch ui/ vitest rendering ActionResultDialog with this resultDialog and the route body passed 1/1 (the rows' code text equals entries). The scratch file was deleted.",
"gates": "dispatch-gates --commands at 6b5dd985: 72 derived. --ran with exit codes recorded: 71 run, 1 NOT-MEASURED, 0 UNRUN. 70 exited 0. check-plugin-teardown-shape --self-test exited 3 (shallow clone lacked the pinned control commit 621a48760), then 0 after fetching that object. check:dual-build-cjs-loads: self-test pass, scan exit 3 PREREQUISITE NOT MET (10 packages outside the closure have no dist), NOT MEASURED locally, CI Lint & Repo Gates reads it. Lint: eslint --no-inline-config over the 16 touched ts/mjs files, exit 0. CI check-runs: in_progress at report time.",
"dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm. The dogfood sweep admin-route-nonadmin-refusal gained this route's entry; CI runs it.",
"mcp_calls": "3: issue_read get #28793, issue_read get_comments #28793 (x2, pages 1 and 2). No MCP write.",
"api_writes": "2 through the fleet relay: pr_create (repository_dispatch fw-20261102T144056Z-4d8354 -> PR #29427 draft, plus its assignee leg, os-steve), and this os-dev-report comment via post-stamped.mjs (POST issues/28793/comments). Plus git push (not REST). REST reads via gh api, no writes: search/issues x4, pulls/{n}/files x4 (open-PR overlap check), pulls/29427 x2 (read-back).",
"line_budget": "17 files, +846 / -14 at 6b5dd985 vs merge base (dispatch-gates count 860, under the 3000 threshold).",
"files_changed": [
".changeset/28793-platform-admin-roster.md",
"packages/core/src/security/admin-standing-surface.ts",
"packages/core/src/security/admin-standing-surface.test.ts",
"packages/core/src/security/index.ts",
"packages/plugins/plugin-auth/src/last-admin-guard.ts",
"packages/plugins/plugin-auth/src/platform-admin-roster.ts",
"packages/plugins/plugin-auth/src/platform-admin-roster.pin.test.ts",
"packages/plugins/plugin-auth/src/auth-plugin.ts",
"packages/plugins/plugin-auth/src/auth-route-ledger.ts",
"packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts",
"packages/platform-objects/src/identity/sys-user.object.ts",
"packages/platform-objects/src/identity/sys-user-platform-admin-roster-action.test.ts",
"packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.objects.generated.ts",
"packages/qa/dogfood/test/admin-route-nonadmin-refusal.dogfood.test.ts"
],
"deviations": "(1) pr_create carries assignees [os-steve]: os-dev.md says no assignees, but AGENTS.md (which wins) says the PR's assignee mirrors the card's, set by the executor at pr_create. Flagged in open_questions. (2) Read-only REST via gh api beyond search (PR files, PR read-back). (3) Card-thread checkout: the primary checkout was detached and a linked worktree ../objectstack-issue-28793 was made on the pinned branch, to satisfy the worktree hook. (4) Forced test edits named in the PR: auth-route-ledger.conformance.test.ts objectstack-mount list, dogfood admin-route sweep expectation table.",
"open_questions": [
{
"question": "Should sys_user.list_platform_admins be assessed for agent exposure (ADR-0011 identity-actions note)?",
"options": [
"A leave it human-only (no ai block), as shipped",
"B assess it as bucket E (read-only, no credential) in a governed ADR-0011 amendment plus an ai block"
],
"recommendation": "A: the roster names platform operators, and agent reach should be a deliberate maintainer ruling, not a rider here."
},
{
"question": "The roster renders as plain text rows (Name, email) with per-row copy, not links to the user records. Is a richer list wanted?",
"options": [
"A no: the ruling's need (see who the platform admins are) is met with no ui change",
"B one small domain:ui PR to render roster rows as record links"
],
"recommendation": "A: measured to render with no ui change; B only if Steve asks."
},
{
"question": "os-dev.md forbids assignees on pr_create while AGENTS.md says the executor sets the PR assignee to mirror the card. Which binds?",
"options": [
"A AGENTS.md (followed here)",
"B os-dev.md; the seat removes the assignee"
],
"recommendation": "A: AGENTS.md declares it wins on conflict. The os-dev.md line should be reconciled by its owner."
},
{
"question": "Changeset level: patch on plugin-auth, platform-objects and core although new exports land. OK?",
"options": [
"A patch (repo rule: Metadata-protocol none -> patch)",
"B minor"
],
"recommendation": "A: follows the Post-Task Checklist rule. Clause-② no holds: a new read for a narrower audience, and nothing an existing caller relied on changed."
}
],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: none · noted, not filed: the dogfood sweep's header prose says objectstack-gate (11 routes) while the table held 12 before this PR (13 now). Stale prose, left alone.",
"carrier: none · noted: open PR #29387 also edits packages/core/src/security/index.ts (export list), so at most a mechanical conflict."
]
}
Activity
objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Unblocked ·
pm:queue⛔ Not a claim.
Blocked-by: #28105— closed as completed via PR fix(platform-objects): the Setup Users list and the user record highlights stop showing the legacy role scalar #28810 (1e019240f9), which removed the legacyrolecolumn fromall_usersand therolechip from thesys_user_detailhighlights, the slots this card's derived column replaces.pm:blocked→pm:queuein one label write; grade unchanged, no assignee.objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Claim: PM loop round 1 · card thread · 2026-11-01T22:06Z
Session:
session_01FEkQ5oc56xChD6Wrc4cGSt(the seat); the card thread issession_01FEkQ5oiX3eWpVYxchQ9C3VAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-7qef1r(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#5Ruling-ref: 6557746678 (director ruling on #28105, letter D1, which binds this follow-up at p3) and the seat's rulings in the order; pick announced to seats 1-3, seats 1 and 3 confirmed no clash.
File surface:
packages/platform-objects/src/identity/(thesys_userobject and its tests), the read seam that serves the derived value (measured by the dev:packages/objectql,packages/metadata-protocolorpackages/core/src/security), changesets, anycontent/docs/**page listing the Users columns. ⛔ Nopackages/spec/**.Container & model:
M,mode:thread, model and effort as the thread was openedClause-②: no (expected; the dev corrects it from the diff if a published shape widens)
Metadata-protocol: none
Responsibility: the code is the
sys_userUsers list and the read path that can serve a derived value beside it · no column says who holds platform-admin standing, now that the legacyrolescalar is gone · reached by every Setup admin opening UsersThread-read: 6564514014
Premise: holds (read at
origin/main1e019240f, which contains PR #28810:all_usershas norolecolumn;hasPlatformAdminStandingis per-user, async, fail-closed)Serial constraints cleared: none needed.
Resume-from: none — fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-project-manager, recorded by this claim. Order:pm-orders/server/order-28793.mdin the project.objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oiX3eWpVYxchQ9C3V
os-dev-report
{
"issue": 28793,
"status": "needs_decision",
"branch": "claude/project-thread-7qef1r",
"pr": null,
"session": "session_01FEkQ5oiX3eWpVYxchQ9C3V",
"premise_still_valid": true,
"summary": "Stopped before code at the ruling 3 exit, on seat 5's instruction (no file surface amendment). Measured at origin/main 1e01924: the premise holds (all_users has no standing column; hasPlatformAdminStanding is per-user, async, fail-closed), but no existing mechanism serves a per-row derived value as a list column without a new pattern. PM assumption 1 is false: a columns entry must name a DECLARED field (lint validate-list-view-field-refs errors; clients echo columns as the projection, which REST answers 400 INVALID_FIELD for an unknown plain column). The only declared field type with no column is formula (driver-sql schema-drift.ts fieldHasColumn), and CEL cannot call the predicate. No production read seam serves a per-row derived key today: the only afterFind hook in production is plugin-audit read-audit (observes, does not shape); plugin-approvals payload-redaction-middleware rewrites a stored column on result rows. Branch carries no commit; nothing pushed.",
"tests": "No repo test added (no code). One scratch measurement, deleted after the run: real ObjectQL + SqlDriver better-sqlite3 in-memory + real SecurityPlugin (fallback member_default), sys_user/sys_member/sys_organization + the security plugin's grant objects, 50 users, Default Organization org_default, admin_full_access grant owned by org_default on usr_000, the same set granted on org_other to usr_002, median of 7 runs on the shared box. Lock VERDICT command-exit 0, Tests 2 passed. Readings: (1) grant-only: the all_users page read (8 columns, limit 50) = 0.99 ms and 1 driver read; hasPlatformAdminStanding over the page = 81.5 ms concurrent / 109.8 ms sequential and 200 driver reads (4 per user); answers: only usr_000 true, usr_002 (grant scoped to a non-default org) false. (2) OS_PLATFORM_OWNER_EMAIL = usr_001's verified email: page 0.66 ms / 1 read; predicate 53.0 ms concurrent / 75.7 ms sequential, 200 reads; answers usr_000 and usr_001 true. So per-row evaluation multiplies the page's reads by about 200x and its latency by about 50-100x on in-memory SQLite; on a networked database every read is a round trip. Not cheap under ruling 4. (3) Engine-level filter on a formula field (sys_member.display_title, system context) = INVALID_FIELD 400 'filters on display_title, a virtual formula field ... no driver materialises'. Not a stock boot: the os dev closure build failed on my own flag (see deviations), so the live probe was NOT MEASURED; it is moot with no code.",
"gates": "NOT MEASURED: no diff, so dispatch-gates --commands derives nothing and no gate applies.",
"dogfood_battery": "n/a: no PR",
"mcp_calls": "3 reads (issue_read get on #28793, issue_read get_comments on #28793 twice); no MCP write",
"api_writes": "1: this os-dev-report comment via scripts/pm/post-stamped.mjs (POST issues/28793/comments through the fleet relay); no git push, no pr_create, no label write",
"deviations": "Ran the closure build as pnpm --filter '@objectstack/example-todo^...' build --concurrency=2: pnpm forwarded the flag into each package's build script, which broke @objectstack/cli's build script (sh syntax error, lock VERDICT command-exit 2). The packages the scratch measurement imports (core, objectql, plugin-security, platform-objects, driver-sql) built; the cli dist did not, so no stock boot was measured. Nothing was committed from that build.",
"open_questions": [
{
"question": "Which mechanism serves platform-admin standing on the Setup Users list, given ruling 3 (no spec edit, existing mechanism) and ruling 4 (bounded cost)?",
"options": [
"A. A platform_admin formula field on sys_user (returnType boolean, constant false expression as the fail-closed default, sortable false, Admin field group so the shipped FLS withholds it from org peers like phone_number) whose value an afterFind hook registered by plugin-auth fills on a user-context read from hasPlatformAdminStanding(ql, row.id). Files: platform-objects sys-user.object.ts + the four translation bundles (en/zh-CN/ja-JP/es-ES, check-i18n-bundles --write needs a full build), plugin-auth new module + auth-plugin.ts registration + tests. Spec edit: no. ui edit: no. Cost: 200 extra driver reads per 50-row page, 53-110 ms on in-memory SQLite vs about 1 ms for the page itself. Filter and sort: refused, not silently empty: engine filter on a formula = INVALID_FIELD 400 (measured), sort refused at the REST door (metadata-protocol protocol-query-validation.ts UNMATERIALIZED_SORT_TYPES) and in the engine (assertOrderByIsMaterializable). Seat 5's objections stand: the declared expression false contradicts the served cell, and every user-context sys_user read that does not name its fields (record page, unprojected reads) pays the per-row cost; system reads skip it (the predicate reads sys_user under system context, so the hook must skip them to avoid recursion).",
"B. A declared non-formula derived or virtual field kind outside spec. Measured: none exists. driver-sql fieldHasColumn answers no column only for formula; summary and autonumber are stored columns. A new kind is a FieldType edit in packages/spec, excluded by ruling 3.",
"C. No column: a served answer to 'who are the platform admins'. The predicate's own header forbids widening the per-user predicate to the population question; the one population enumerator that exists is resolveAdminUserIds inside plugin-auth last-admin-guard.ts (a closure, not exported): it enumerates the in-window admin_full_access grants owned by the Default Organization plus the owner-list users, and confirms through the resolver's predicate. Exporting it as a reader and serving it through a Setup action or route is cheap (one grant read plus the owner-list users, then the predicate for the few candidates), has no metadata contradiction and no per-row cost. Files: plugin-auth (export + door or action handler), platform-objects (action or view declaration), and a route mount if it is a new door (dispatcher-plugin.ts). Spec edit: no for an action; NOT MEASURED whether an existing action result surface renders a user list without a ui edit. Filter and sort: not applicable (a separate answer, not a list column). Risk: the candidate enumeration names the two anchor sources, so a future third anchor needs this reader updated (the admin-standing-surface.ts ledger is the existing pin for that drift).",
"D. A ui-side column fed by a dedicated read: a new door that answers standing for a page of user ids (or the population from C) and a Console grid change for the sys_user all_users view. Files: plugin-auth or rest door + dispatcher mount, ui/ grid. ui edit: yes, which ruling 6 bars unless no served mechanism can be shown. Cost: one extra request per page; server side the same 4 reads per user unless it rides C's population reader. Filter and sort: client-side over one page only.",
"E. Batch the predicate: one grant read for the page's user ids through the same resolver. Measured: no batched form of resolveUserAuthzGrants exists (it is per-user, with request memo, cross-request cache and batch-equivalence goldens keyed per user), so this is new core code in packages/core/src/security plus the same formula-field question as A for the column itself."
],
"recommendation": "C, because it is the only option that needs no spec edit, puts no contradiction into sys_user metadata and keeps the Users list read at one driver read, while answering the need #27863 states (seeing who the platform admins are). If the decision is that it must be a column on the list, A is the only no-spec, no-ui route, and it should land with E's batching later only if the per-page cost is accepted as measured."
}
],
"out_of_scope_findings": [
"carrier: seat 5 · reading, not filed: the seat's note expected a filter on a formula field to evaluate the declared constant and return nobody; measured, the engine refuses it 400 INVALID_FIELD (and the REST door refuses the sort), so option A's filter cost is a refusal, not wrong data",
"carrier: none · reading, not filed: reader population of all_users, read from plugin-security default-permission-sets.ts, not live-measured: member_default grants sys_user select under sys_user_self and sys_user_org_members (id in current_user.org_user_ids), so every org peer can list their organization's users; the Admin field group is withheld from member_default, viewer_readonly and external_default and kept for admin_full_access and organization_admin. Any option that serves standing to org admins tells an organization admin which of their members are platform operators, which is NOT MEASURED as readable to them today; options A and C both need that called out in the ruling"
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Needs decision · 2026-11-01T22:20Z
The dev's measure-first report (6564729313,
status: needs_decision, no commit pushed) shows ruling D1's wording, "the Users list gains a column", has no clean route under this seat's order. A list column must name a declared field; the only column-less declared kind isformula, so a column means a formula field whose declared expression contradicts the cell its hook fills, and per-row standing costs about 200 driver reads per 50-row page (53–110 ms against about 1 ms for the page). No batched resolver and no non-formula derived kind exists without a spec edit.The seat recommends option C: no column, but a served answer to "who are the platform admins" built from the population enumerator already inside
plugin-auth'slast-admin-guard.ts. That changes the shape the maintainer approved in D1, so it goes to the maintainer as a decision in the seat-5 thread (options: C no column with a separate answer, recommended; A keep the column with its cost and no filter/sort; park). The privacy reading the dev flagged (serving standing to organization admins names platform operators) rides whichever is chosen.State:
pm:dispatched→needs-user-decision, seat unassigned. The dev thread keeps its worktree for a follow-up order once ruled.1 remaining item
zhuangjianguo commentedon Nov 2, 2026
director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw
Brief-claimed-by: session_01QsR2jV5sBSzCYNEDgpYLJw · batch #450 · 2026-11-02T00:46Z
Director seat (第 56 场). Presented to the maintainer in batch #450, item 3. The card came back to the inbox through seat 5's needs-decision note 6564742308 on the dev's measure-first report 6564729313, without the decision-card body (no 一句话问题, no options × workload table, no
os-decision-facetsblock), so this seat's increment completes it. ⛔ 不是裁决。前提复核(本席实读
origin/main00918c88):packages/core/src/security/resolve-authz-context.ts:2145exportshasPlatformAdminStanding(per user, async, fail-closed: theOS_PLATFORM_OWNER_EMAILlist, or under thesingleposture an unscopedadmin_full_accessgrant); PR #28810 (1e019240f9) removed the legacyrolecolumn fromall_users, so the Users list says nothing about standing today. The dev measured (6564729313): a list column must name a declared field; the only column-less declared kind isformula, whose declared expression would be a constantfalsethat contradicts the cell a hook fills; per-row standing over a 50-row page costs about 200 extra driver reads (53–110 ms against about 1 ms for the page, in-memory SQLite). Premise holds. Re-check:git grep -n "export async function hasPlatformAdminStanding" origin/main -- packages/core/src/security/resolve-authz-context.ts.一句话问题: 设置里的「用户」列表现在看不出谁是平台管理员(误导人的旧
role列已按 #28105 D1 移除);D1 当时说「加一列」,但实测加一列要么让每页多付约 200 次数据库读取,要么让元数据里声明一个与实际值相反的常量,所以要重新定:到底是要「每个用户旁边一个标记」,还是要「一张可靠的平台管理员名单」。Governing text: #28105 ruling 6557746678 D1 ("a follow-up card is filed at p3 … the Users list gains a column"); #9652 veto 5334874978 (nothing is written into
sys_user.role); ADR-0068's 2026-10-06 note and ADR-0095 D3 (PLATFORM_ADMINstanding derives from held grants, judged only byhasPlatformAdminStanding, never a second copy of the rule). 协议声明:不改packages/spec(ruling 3 of the order),Clause-②: no,Metadata-protocol: none。选项 × 真实代价 × 工作量:
sys_user加platform_adminformula 字段(声明常量 false),由 plugin-auth 的 afterFind 钩子在用户态读取时逐行填入真实判定domain:server· 1 PR(platform-objects 字段 + 4 个翻译包 + plugin-auth 钩子)· 无先例packages/spec(FieldType),被本单 ruling 3 排除 · 无先例last-admin-guard.ts的resolveAdminUserIds:默认组织内的admin_full_access授权 + 所有者邮箱名单,再逐个用同一判定确认)导出,作为一个设置动作/路由对外回答「谁是平台管理员」,用户列表页放一个入口domain:server· 1 PR(plugin-auth 导出 + 动作处理器,platform-objects 动作声明);动作结果能否不改 ui 就渲染成用户列表 NOT MEASURED,若不能则 ui 车道再补 1 个小 PR · 先例:last-admin-guard.ts已在用同一枚举domain:server+domain:ui· 2 PR · ruling 6 禁止 ui 改动除非证明无服务端方案 · 无先例packages/core新代码 + A · 无先例业务含义直译: A = 在花名册每一行旁边贴标签,但贴之前要逐人跑一趟人事部;C = 人事部直接给你一张「管理员名单」,要看就打开它;D = 让前端自己去问;E = 先把人事部查询批量化再贴标签。
四维(业务立场):
sys_user元数据里留下一条"声明 false、实际 true"的永久矛盾;D/E 各加一个表面。只看①选 C;②③④ 是否翻转:否。
推荐:C。 置信缺口:动作结果面能否直接渲染用户列表未测,若需要 ui 改动则是一个小 PR,不翻字母。执行参数同笔裁定:(1) 名单只对持平台管理员资格的调用者开放(组织管理员看不到平台运维名单——dev 报告的隐私点);(2) 导出的枚举器挂进
admin-standing-surface.ts台账,日后第三个资格来源出现时门禁会红。不取:A(贵、不能筛选、元数据撒谎),B(改 spec),D(ui 先行),E(新 core 代码却仍留 A 的矛盾)。裁后执行: C ⇒ 本卡摘
needs-user-decision回pm:queue(domain:serverp3),seat 5 按原 worktree 重派,验收 = 平台管理员在设置→用户页一键得到名单、名单与hasPlatformAdminStanding逐人一致、非平台管理员得到拒绝、列表页请求数不变;A ⇒ 同车道 1 PR,另记元数据矛盾为已接受偏差;D ⇒ 先立 ui 卡。os-decision-facets
① 长远:C 用一张专门名单回答审计问题,复用唯一判定与枚举器;A 留下声明 false / 实际 true 的元数据矛盾
② 拉动:#27863 今天无入口;C 一 PR 回答
③ 防 AI 犯错:A 的 schema 对 AI 撒谎(静默);C fail-closed,错只会少列
④ 不扩散:C 不加字段不改 spec;A/B/D/E 各加一个表面
Prior rulings read:
platform.?admin standing|hasPlatformAdminStandingoverorigin/mainAGENTS.md, docs/adr, packages/spec/src → ADR-0068 note 2026-10-06, ADR-0095 D3 (standing from grants, one predicate), ADR-0057:210; thread: 6557746678 (#28105 D1), 5334874978 (#9652 veto), 6564729313 (dev measure), 6564742308 (seat 5)Generated by Claude Code
zhuangjianguo commentedon Nov 2, 2026
director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw
Ruling: batch #450 item 3 · letter C · maintainer 「同意」 2026-11-02T01:12Z
Director seat (第 56 场; decision card completed and brief-claimed above in 6566203119, presented in the 总监决裁 thread as batch #450 item 3 with the business read-out). The maintainer answered the batch verbatim: 「同意」 = every item's recommendation, so this card's letter is C: no derived column on the Users list; a platform-admin roster answered by the one enumerator.
What the ruling binds (letter C):
sys_usergains noplatform_adminfield, formula or otherwise;packages/specis untouched (ruling 3 of platform-objects: the Setup user screens show the raw stored platform role (user,admin) becausesys_user.roleis a text field #28105 stands); the Users list page stays one read per page.last-admin-guard.ts,resolveAdminUserIds: theadmin_full_accessgrants in the default organisation plus the owner-email list, each confirmed throughhasPlatformAdminStanding) is exported and served as a Setup action or route that answers "who holds platform-admin standing", and the Users list page carries one entry to it.admin-standing-surface.tsledger, so a third source of standing reds the gate instead of silently diverging (ADR-0095 D3, one predicate, never a second copy).falsein metadata while showingtrue), B (a new field type in spec), D (a per-page endpoint plus a ui column, ui first), E (batching the predicate but keeping A's contradiction).Workload of the letter:
domain:server· one PR (plugin-auth export + action handler + ledger pin, platform-objects action declaration and its four translation packs) · precedent:last-admin-guard.tsalready runs the same enumeration; no same-shape landed PR with line counts. Plus, if needed, one smalldomain:uiPR for the roster rendering.Execution:
domain:serverseat 5 re-dispatches on the original worktree withRuling-ref:naming this comment; theneeds-user-decision→pm:queuestroke (p3 stands) is that seat's. Acceptance: a platform administrator reaches the roster from Setup → Users in one step; the roster agrees user by user withhasPlatformAdminStanding; a non-platform-admin caller is refused; the Users list page's request count is unchanged. TheRuled:body line is written by this act.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Claim: PM loop round 10 · card thread · 2026-11-02T13:05Z
Session:
session_01ADHkmQKwKFCp1m6Pb6Q8c1(the seat); the card thread is named in a follow-up claim once it reports its branchAccount:
os-steve(the seat's linked user; the card's assignee)Branch: pending: the card thread's harness-pinned branch, written into a follow-up claim before the dev starts
Worktree: the card thread's own container checkout (the original
claude/project-thread-7qef1ris gone from the remote, so the work starts fresh)Domain:
domain:serverSeat:
domain:server#5Ruling-ref: 6566734829 (batch #450 item 3 · letter C · no derived column; a platform-admin roster answered by the one enumerator)
File surface:
packages/plugins/plugin-auth/src/(last-admin-guard.ts: export the enumeratorresolveAdminUserIds, plus the roster action or route handler),packages/platform-objects/src/identity/(the Setup → Users roster action declaration and its four translation packs),packages/core/src/security/admin-standing-surface.ts(the ledger pin for the exported enumerator), their tests, changeset. ⛔ Nopackages/spec/**, and nosys_userfield.Container & model:
M,mode:thread, model: the thread's default judgement tier,effort: high(identity surface)Clause-②: no
Metadata-protocol: none
Responsibility: the code is plugin-auth's admin-standing enumerator and the Setup Users page · a platform administrator cannot see who else holds platform-admin standing · reached by platform operators
Thread-read: 6566734829
Premise: holds (read at
origin/main5780fa49d:resolveAdminUserIdslives inplugin-auth/src/last-admin-guard.tsunexported;sys_userhas no platform-admin column)Serial constraints cleared: no open PR touches
last-admin-guard.tsoradmin-standing-surface.ts; the dev re-reads open PRs on these paths before opening its own.Resume-from: none: fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-steve, recorded by this claim.objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Claim: PM loop round 10 · card thread · branch named · 2026-11-02T13:08Z (supersedes 6577882461, which had the branch pending)
Session:
session_01ADHkmQKwKFCp1m6Pb6Q8c1(the seat); the card thread issession_01ADHkmQKiS3hSX11hKXCKNNAccount:
os-steve(the seat's linked user; the card's assignee)Branch:
claude/project-thread-ymgvae(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout (the original
claude/project-thread-7qef1ris gone from the remote, so the work starts fresh)Domain:
domain:serverSeat:
domain:server#5Ruling-ref: 6566734829 (batch #450 item 3 · letter C · no derived column; a platform-admin roster answered by the one enumerator)
File surface:
packages/plugins/plugin-auth/src/(last-admin-guard.ts: export the enumeratorresolveAdminUserIds, plus the roster action or route handler),packages/platform-objects/src/identity/(the Setup → Users roster action declaration and its four translation packs),packages/core/src/security/admin-standing-surface.ts(the ledger pin for the exported enumerator), their tests, changeset. ⛔ Nopackages/spec/**, and nosys_userfield.Container & model:
M,mode:thread, model: the thread's default judgement tier,effort: high(identity surface)Clause-②: no
Metadata-protocol: none
Responsibility: the code is plugin-auth's admin-standing enumerator and the Setup Users page · a platform administrator cannot see who else holds platform-admin standing · reached by platform operators
Thread-read: 6577882461
Premise: holds (read at
origin/main5780fa49d:resolveAdminUserIdslives inplugin-auth/src/last-admin-guard.tsunexported;sys_userhas no platform-admin column)Serial constraints cleared: no open PR touches
last-admin-guard.tsoradmin-standing-surface.ts; the dev re-reads open PRs on these paths before opening its own.Resume-from: none: fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-steve, recorded by this claim.objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKiS3hSX11hKXCKNN
os-dev-report
{ "issue": 28793, "status": "done", "branch": "claude/project-thread-ymgvae", "pr": "https://github.com/objectstack-ai/objectstack/pull/29427", "session": "session_01ADHkmQKiS3hSX11hKXCKNN", "premise_still_valid": true, "summary": "Ruling C implemented. last-admin-guard.ts's standing reads move, text unchanged, into a module-level createStandingReader shared by the guard and the new export resolveAdminUserIds(engine, { standing: 'administrators' | 'platform-admins', maxScan }), where platform-admins skips the organization owner/admin half. POST /api/v1/auth/admin/list-platform-admins (gateAdmin, then the handler confirms the caller via hasPlatformAdminStanding; candidates confirmed one by one through the predicate; no body read) feeds a new sys_user list_toolbar action list_platform_admins whose resultDialog draws the roster as a code-list. Measured: it renders as a list with no ui change. ADMIN_STANDING_PLATFORM_ENUMERATION in core's admin-standing-surface.ts classifies every standing input as a candidate source or not, and plugin-auth's pin requires a sole-source fixture per candidate, so a third source of standing reds a gate.", "tests": "Head 6b5dd985, after the origin/main merge and a closure rebuild, all under os-verify-lock. plugin-auth test: Test Files 212 passed, Tests 3655 passed / 10 skipped. platform-objects test: 109 files, 1507 passed. core test: 129 files, 2913 passed. typecheck on the 3 packages: exit 0. New: platform-admin-roster.pin.test.ts (12, real ObjectQL + better-sqlite3 + the real hasPlatformAdminStanding), sys-user-platform-admin-roster-action.test.ts (4), 4 classification tests in admin-standing-surface.test.ts. Ablations via ablation-replace, each restored with blob == HEAD: (1) caller check -> if(false): 4 refusal cases red, 'expected 200 to be 403'. (2) membership-half skip removed: the org-owner exclusion test red, while agreement stayed green because the predicate confirmation still drops the org owner. The first attempt was refused by the tool (replacement was a substring of the anchor) and redone. (3) owner-email half disabled: 5 red incl. agreement, expected [usr_grant] to equal [usr_grant, usr_owner]. (4) sys_member dropped from notCandidates: core classification red. (5) third candidate added in core src, core rebuilt, ablation-dist-preflight marker present in 2 dist files: the plugin-auth pin red with its 'no fixture here: teach the enumerator' message. Restore: core rebuilt, --absent marker gone from 14 files, tree clean. UI measurement: a scratch ui/ vitest rendering ActionResultDialog with this resultDialog and the route body passed 1/1 (the rows' code text equals entries). The scratch file was deleted.", "gates": "dispatch-gates --commands at 6b5dd985: 72 derived. --ran with exit codes recorded: 71 run, 1 NOT-MEASURED, 0 UNRUN. 70 exited 0. check-plugin-teardown-shape --self-test exited 3 (shallow clone lacked the pinned control commit 621a48760), then 0 after fetching that object. check:dual-build-cjs-loads: self-test pass, scan exit 3 PREREQUISITE NOT MET (10 packages outside the closure have no dist), NOT MEASURED locally, CI Lint & Repo Gates reads it. Lint: eslint --no-inline-config over the 16 touched ts/mjs files, exit 0. CI check-runs: in_progress at report time.", "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm. The dogfood sweep admin-route-nonadmin-refusal gained this route's entry; CI runs it.", "mcp_calls": "3: issue_read get #28793, issue_read get_comments #28793 (x2, pages 1 and 2). No MCP write.", "api_writes": "2 through the fleet relay: pr_create (repository_dispatch fw-20261102T144056Z-4d8354 -> PR #29427 draft, plus its assignee leg, os-steve), and this os-dev-report comment via post-stamped.mjs (POST issues/28793/comments). Plus git push (not REST). REST reads via gh api, no writes: search/issues x4, pulls/{n}/files x4 (open-PR overlap check), pulls/29427 x2 (read-back).", "line_budget": "17 files, +846 / -14 at 6b5dd985 vs merge base (dispatch-gates count 860, under the 3000 threshold).", "files_changed": [ ".changeset/28793-platform-admin-roster.md", "packages/core/src/security/admin-standing-surface.ts", "packages/core/src/security/admin-standing-surface.test.ts", "packages/core/src/security/index.ts", "packages/plugins/plugin-auth/src/last-admin-guard.ts", "packages/plugins/plugin-auth/src/platform-admin-roster.ts", "packages/plugins/plugin-auth/src/platform-admin-roster.pin.test.ts", "packages/plugins/plugin-auth/src/auth-plugin.ts", "packages/plugins/plugin-auth/src/auth-route-ledger.ts", "packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts", "packages/platform-objects/src/identity/sys-user.object.ts", "packages/platform-objects/src/identity/sys-user-platform-admin-roster-action.test.ts", "packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.objects.generated.ts", "packages/qa/dogfood/test/admin-route-nonadmin-refusal.dogfood.test.ts" ], "deviations": "(1) pr_create carries assignees [os-steve]: os-dev.md says no assignees, but AGENTS.md (which wins) says the PR's assignee mirrors the card's, set by the executor at pr_create. Flagged in open_questions. (2) Read-only REST via gh api beyond search (PR files, PR read-back). (3) Card-thread checkout: the primary checkout was detached and a linked worktree ../objectstack-issue-28793 was made on the pinned branch, to satisfy the worktree hook. (4) Forced test edits named in the PR: auth-route-ledger.conformance.test.ts objectstack-mount list, dogfood admin-route sweep expectation table.", "open_questions": [ { "question": "Should sys_user.list_platform_admins be assessed for agent exposure (ADR-0011 identity-actions note)?", "options": [ "A leave it human-only (no ai block), as shipped", "B assess it as bucket E (read-only, no credential) in a governed ADR-0011 amendment plus an ai block" ], "recommendation": "A: the roster names platform operators, and agent reach should be a deliberate maintainer ruling, not a rider here." }, { "question": "The roster renders as plain text rows (Name, email) with per-row copy, not links to the user records. Is a richer list wanted?", "options": [ "A no: the ruling's need (see who the platform admins are) is met with no ui change", "B one small domain:ui PR to render roster rows as record links" ], "recommendation": "A: measured to render with no ui change; B only if Steve asks." }, { "question": "os-dev.md forbids assignees on pr_create while AGENTS.md says the executor sets the PR assignee to mirror the card. Which binds?", "options": [ "A AGENTS.md (followed here)", "B os-dev.md; the seat removes the assignee" ], "recommendation": "A: AGENTS.md declares it wins on conflict. The os-dev.md line should be reconciled by its owner." }, { "question": "Changeset level: patch on plugin-auth, platform-objects and core although new exports land. OK?", "options": [ "A patch (repo rule: Metadata-protocol none -> patch)", "B minor" ], "recommendation": "A: follows the Post-Task Checklist rule. Clause-② no holds: a new read for a narrower audience, and nothing an existing caller relied on changed." } ], "security_findings": "none", "out_of_scope_findings": [ "carrier: none · noted, not filed: the dogfood sweep's header prose says objectstack-gate (11 routes) while the table held 12 before this PR (13 now). Stale prose, left alone.", "carrier: none · noted: open PR #29387 also edits packages/core/src/security/index.ts (export list), so at most a mechanical conflict." ] }test(dogfood): classify the platform-admin roster in the standing swe…
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Landed · #28793 · 2026-11-02T17:06Z
92b7bbd127(head365488e517, ACCEPT on the PR)Fixes #28793State:
pm:dispatchedremoved. The card thread is released.