Repository navigation
fix(spec): EvalUser.isPlatformAdmin is the ADR-0095 D3 PLATFORM_ADMIN standing, not a deprecated alias - #22018
Conversation
…ORM_ADMIN standing The key is the predicate platform-operator gates read (ADR-0068 D4) and the session emits it from the posture rung, yet its JSDoc and published description still called it a deprecated alias derived from positions. Lift the mark and describe what it reports. Description and JSDoc only: the schema shape, optionality and createEvalUser are unchanged. Adds a pin on the published JSON Schema description. Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
…0068 D2/D4 notes - ADR-0068: one dated note under D2 and one under D4 naming ADR-0095 D3 and EvalUserSchema.isPlatformAdmin as the superseding text. The original wording is not rewritten. - permission-metadata.mdx: the "derived, deprecated alias" aside now describes the standing and says gates read the key, never the array. - authentication.mdx: the admin user-management routes are gated on isPlatformAdmin with the legacy better-auth scalar as fallback. The platform_admin position is not one of the gate's signals. - Changeset for @objectstack/spec (patch). Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
Output of `pnpm --filter @objectstack/spec check:generated --fix` (gen:docs only; the other 14 artifacts were already current). Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7b4251fd4c4dfd2e4c409455413cd82c28445236 && git checkout 7b4251fd4c4dfd2e4c409455413cd82c28445236
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1fb274e61cfff12ede54963d779acdfd079be318 39076f8471870248d8f3febad7645c211a80d221 && git checkout -B drift-repro 1fb274e61cfff12ede54963d779acdfd079be318 && git merge --no-ff 39076f8471870248d8f3febad7645c211a80d221
node scripts/docs-audit/affected-docs.mjs --json 1fb274e61cfff12ede54963d779acdfd079be318
|
Contract reviewServed-tier: Inputs read: card #22012 (body and both comments, the claim 6019998508 and the os-dev-report 6021314784), the ruling 6019378035 on #21886 in full, PR #22018 (body, 7-file list, net diff against ① Derived judgmentsAccept set and public surface of
The factual claims the new
Governed rule text,
Hand-written docs:
Rules and conventions:
② Semver level
Clause-②: no ③ Boundary flags
Dev deviations declared: a self-killed vitest launch that never ran; one fixture commit fetched at depth 1 into the shared object store; a Check-runs on the head as read: 22 Implemented-by: VERDICT: PASS Generated by Claude Code |
|
CI note from the owning
Generated by Claude Code |
维护者速读
改了什么
为什么改
风险与代价(含回滚)
席位意见
你要做的
Generated by Claude Code |
…ectstack-ai#22064) Fixes objectstack-ai#21886 Clause-②: no ## What changes `sys_member.add_member` ("Add Member" on an organization's member list) is now offered only to a platform administrator. That is the one standing its endpoint admits. - **Before:** the action was gated on the `organization` feature alone, so every member of the organization saw it: plain members, admins and owners. `POST /api/v1/auth/organization/add-member` then answered each of them 403 `PERMISSION_DENIED`, because the mount runs the ADR-0068 platform-admin gate before anything else. - **After:** the action declares `visible: 'current_user.isPlatformAdmin == true'`, the predicate the maintainer's ruling A-lite fixed (record 6019378035). ADR-0068 D4 names that predicate, and since PR objectstack-ai#22018 `EvalUserSchema.isPlatformAdmin` describes it as the PLATFORM_ADMIN standing of ADR-0095 D3. `requiresFeature: 'organization'` composes onto it at parse time, so the served predicate is `(current_user.isPlatformAdmin == true) && features.organization != false`. The door and the callers it admits are unchanged. No key, export or parameter is added, and the label is unchanged. That makes `Clause-②: no`, and the changeset is a `@objectstack/platform-objects` patch. **File surface, as the claim declared it.** The producer is `packages/platform-objects/src/identity/sys-member.object.ts`. The other files are the lowering-matrix pin in `packages/platform-objects/src/platform-objects.test.ts`, one dogfood case in `packages/qa/dogfood/test/org-admin-affordance-reach.dogfood.test.ts`, and `.changeset/21886-add-member-platform-admin-visibility.md`. There is no `packages/spec`, `plugin-auth` or `@objectstack/formula` edit. ## The dispatch's hypotheses, measured at `56c88446ea` - **H1: confirmed. The two keys compose with AND, the authored term first.** `lowerRequiresFeature` (`packages/spec/src/kernel/public-auth-features.ts:352-419`) turns an existing CEL `visible` into `(existing) && gate` (`:415-418`). The first-party precedent that carries both keys is `sys_user.enable_two_factor` (`sys-user.object.ts:534-535`), pinned at `platform-objects.test.ts` as `(...) && features.twoFactor == true`. - **H2: confirmed.** The served `visible` is exactly `(current_user.isPlatformAdmin == true) && features.organization != false`. This was read from the parsed `SysMember` object in the matrix test, and from the served `/meta/object/sys_member` on a real boot through the dogfood case. `EvalUserSchema.isPlatformAdmin` (`eval-user.zod.ts:225-241`) now describes the ADR-0095 D3 standing. - **H3: confirmed, with one correction.** - The console binds `current_user` through `extra`. At the current objectui pin `a58626c88d`: - `fieldRules.ts:284` passes the scope as `extra`; - `ExpressionProvider.tsx:190` builds one subject under `current_user`, `user`, `ctx.user` and `os.user`, with `features` beside it; - `expressionUser.ts:176` forwards `isPlatformAdmin` from the session. - The session value is `grants.posture === 'PLATFORM_ADMIN'` (plugin-auth `auth-manager.ts:4083`, emitted at `:4107`). - Under `user:`, `@objectstack/formula` re-derives it from `positions` (`stdlib.ts:417-430` through `createEvalUser`). Under `extra`, the bag is merged verbatim (`stdlib.ts:462`). - **The correction:** the file's existing `owner` principal is the harness's seeded dev admin, and on this boot its served session carries `isPlatformAdmin: true`. It is the platform-admin principal, reused. Nothing on this boot declares `OS_PLATFORM_OWNER_EMAIL`: it is unset in the environment, absent from the dogfood package and the showcase config, and the harness sets it only under a walled posture (`packages/verify/src/harness.ts:481-497`). By `EvalUserSchema`'s definition, the rung therefore comes from the `single` posture's unscoped `admin_full_access` grant. The file had no org owner who is not a platform admin, so the case signs one up and sets its membership to `owner`, the way the file already sets grades. - The existing cases still bind through `user:`, unchanged. - **H4: confirmed. No server path evaluates an action `visible`.** - `rest`, `runtime`, `objectql`, `services`, `metadata`, `metadata-protocol`, `core` and `plugins` have zero non-test hits for an action-`visible` evaluation. - The eight `ExpressionEngine.evaluate` / `celEngine.evaluate` call sites in package sources evaluate other things: formula fields and defaults (`objectql/src/engine.ts:2263`, `:6190`), hook conditions (`hook-wrappers.ts:309`), approval conditions, share-link gates and automation steps. - So nothing on the server binds `current_user.isPlatformAdmin` for this predicate, and the door stays the authority. ## Tests All runs below are at the final HEAD (`bb53601015`) unless marked otherwise. - **Pin (a), `platform-objects`:** the lowering-matrix row `SysMember#add_member` now expects `(current_user.isPlatformAdmin == true) && features.organization != false`. - `pnpm --filter @objectstack/platform-objects exec vitest run`: 62 files, 996 passed. - The package `typecheck` passed, with 0 TS errors. Its main program does not compile `platform-objects.test.ts`. `check:test-typecheck` compiles it through `tsconfig.test.json`: `--listFiles` counts 1 hit there and 0 in the main program. - **Pin (b), one dogfood case on a real showcase boot:** `Add Member is offered to a platform admin alone — current_user bound as the console binds it — and the door agrees`. - It reads each principal's served session, its own served `/meta/object/sys_member` and the served `/auth/config` flags. - It asserts each session's `isPlatformAdmin`: `true` for the seeded admin, `false` for the second owner, the admin, the delegated admin and the plain member. It also asserts that the second owner carries `org_owner`. - It evaluates the served predicate with `celEngine`, with `current_user` bound through `extra` the way the console binds it. Exactly the platform admin is offered the action. - It then probes the door on the same boot: the owner and the plain member get 403 `PERMISSION_DENIED` (envelope `error.code`) and no row is written; the platform admin gets 200 `success: true` and the `sys_member` row lands. - Result: `test/org-admin-affordance-reach.dogfood.test.ts` passed 14 of 14. The dogfood `typecheck` passed, and its program compiles the touched file (`--listFiles`: 1 hit). - **Builds:** `turbo run build --filter='@objectstack/dogfood^...'` passed 63 of 63 after the merge of `origin/main`. ### Reverse verification, on committed HEAD `57b18bf94e` - **Mutation:** `scripts/ablation-replace.mjs` deleted the line `visible: 'current_user.isPlatformAdmin == true',`. The anchor count went from 1 to 0 and the file's blob from `9beac68ce1c7` to `b8792313cac4`. An outer `trap … EXIT INT TERM` used absolute paths. - **Proof the mutation reached `dist/`:** `@objectstack/platform-objects` was rebuilt, then `scripts/ablation-dist-preflight.mjs @objectstack/platform-objects 'visible: "current_user.isPlatformAdmin == true"' --absent --source-marker=…` reported the marker absent from all 66 built files. Before the mutation, the same spelling was present in `dist/index.mjs:2219`. - **(a) turned red, 1 failed of 127:** `expected 'features.organization != false' to be '(current_user.isPlatformAdmin == true) && features.organization != false'`. - **(b) turned red, 1 failed of 14:** the set offered "Add Member" was received as `platform admin`, `org owner, not a platform admin`, `org admin`, `delegated admin` and `plain member`, against the expected `platform admin` alone. So the owner and member cells (and the admin cells) went red. - **Direction:** the run turned red, as predicted. - **Restore:** `git checkout HEAD --` on the absolute path. The blob after restore and the HEAD blob are both `9beac68ce1c7ed2b7143505916818a0a2e39a456`, and `git diff HEAD` is empty. Both `ablation-replace` and the outer trap proved this. - **After the restore:** the package was rebuilt, and the preflight in present mode found the marker in 6 built files on a clean tree. (a) passed 127 of 127 and (b) passed 14 of 14. ## Gates These ran locally at `bb53601015`, after the merge of `origin/main` (`5cfd8661c4`). - **The derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 67 commands for this change set (35 pnpm, 32 node). All 67 ran and exited 0. `--ran` reconciles them: `67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN`. - `check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET (exit 3), because the build had covered only the dogfood closure. It was rerun after a full workspace build (`turbo run build`, 72 of 72) and loaded 106 require entry points across 66 packages. - `check:dts-closure`, `check:lean-entry-closure`, `check:published-files` and `check:sourcemap-no-sources-content` were rerun on that full build too, and all four passed. - **The artifact-roster block:** the derivation prints 53 more commands outside its total, and all 53 ran. - 50 exited 0. - Three answered NOT WIRED / NOT MEASURED (exit 2) because they need pull-request context: `check-partof-closing-keyword`, `check-closing-target-claim` and `check-single-claim-paths`. `check-partof-closing-keyword` was then run with this body as `PR_BODY` and passed. The other two need this PR's number and CI runs them on it. - `check-sdui-manifest` passed, but its objectui version comparison is NOT CHECKED, because the container has no objectui checkout. - `check:console-injection` is NOT MEASURED: there is no console `dist/` here, so only its self-test ran. This diff touches neither surface. - **The four symbol-anchor sweeps:** `check:adr-symbol-anchors`, `check:scripts-symbol-anchors`, `check:spec-docblock-symbol-anchors` and `check:adr-anchors` all passed. - **Lint, as a proven narrowing:** `pnpm exec eslint --no-inline-config --format json` ran over the three touched TypeScript files. - The population is read from eslint's own config: `--print-config` resolves for each of the three, so none is ignored. - The JSON output counts 3 files, with 0 errors and 0 warnings. - The narrowing cannot hide a verdict elsewhere. Type-aware linting is off: the resolved configs carry no `parserOptions.project` or `projectService`, and `eslint.config.mjs:327-328` states the repo never enables it. So this diff cannot move the verdict on any untouched file. - **Left to CI, as a declared narrowing:** the type-check lanes, Test Core, Dogfood Regression Gate, Build Core and the repo-wide `pnpm lint`. ## Acceptance notes - **The door also admits a legacy `user.role === 'admin'` scalar** (`platform-admin-gate.ts:80-84`), and this predicate does not read it. A deployment that still carries that pre-ADR-0068-D2 scalar on a user who lacks the posture rung would be admitted by the door, but would not be offered the button. The gate's own header says nothing in ObjectStack writes that scalar for a platform admin, and that re-synthesizing it is vetoed. The predicate is the one the ruling fixed, so this is noted, not filed. - **Only one platform-admin route is exercised here.** The dogfood boot uses the `single` posture, where the standing comes from the seeded admin's unscoped grant. The `OS_PLATFORM_OWNER_EMAIL` route of the walled postures is not exercised by this case. - **Only the new case binds through `extra`.** The grade cases above it still bind through `user:`, as the dispatch required. Their predicates read only `current_user.positions`, which is identical under either binding. - **Out of this PR:** objectstack-ai#21903 (`Blocked-by: objectstack-ai#21886`) remains open and carries the thirteen sibling platform-admin actions and the enumeration pin. --- _Generated by [Claude Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #22012
Clause-②: no
What this changes
This executes the maintainer's ruling A-lite on #21886 (comment 6019378035): the protocol text changes and the mechanism does not.
EvalUserSchema.isPlatformAdminloses its stale "Deprecated, derived from positions" mark. It is now described as thePLATFORM_ADMINstanding of ADR-0095 D3, and ADR-0068 gets a dated note under D2 and under D4.packages/spec/src/identity/eval-user.zod.ts: only the JSDoc and the.describe()ofisPlatformAdminchange. The key is thePLATFORM_ADMINstanding of ADR-0095 D3, resolved per request: from the declared administrator list (OS_PLATFORM_OWNER_EMAIL) under every tenancy posture, or from an unscopedadmin_full_accessgrant undersingle. It is the predicate platform-operator gates read (ADR-0068 D4). The text says the resolver projectsplatform_adminintopositionsfrom the same grant, and that gates read the key, never the array. The schema shape, optionality, default andcreateEvalUserare unchanged.content/docs/references/identity/eval-user.mdxchanges by one row. This is the output ofcheck:generated --fix;gen:docswas the only stale artifact out of 15.docs/adr/0068-unified-user-context-and-built-in-identity-roles.md: one dated note under D2 and one under D4. They name ADR-0095 D3 andEvalUserSchema(symbol-anchored) as the superseding text. The original wording is not rewritten (git diffon the file: 4 additions, 0 deletions).content/docs/permissions/permission-metadata.mdx: the "derived, deprecated alias" aside now describes the standing and says gates read the key, never the array.content/docs/permissions/authentication.mdx: the old sentence listed "theplatform_adminposition" among the admin routes' gate signals. That was false: the gate inplatform-admin-gate.ts(isPlatformAdminUser) readsisPlatformAdminplus the legacy better-auth scalar, and its header says the positions leg was removed. The sentence now says so.Also in this PR: a pin,
packages/spec/src/identity/eval-user.test.ts, and.changeset/22012-isplatformadmin-standing.md(@objectstack/specpatch).⛔ Not taken, by the ruling:
createEvalUsertaking the rung as input; any@objectstack/formulabuildScopechange; a new authorable key; treating'platform_admin' in current_user.positionsas a standing read; anyvisibleedit on any action. That last item belongs to #21886 and #21903, which this PR leaves open.Premise check (on
origin/main1bc6ca1d)packages/plugins/plugin-auth/src/platform-admin-gate.ts#isPlatformAdminUserreadsu.isPlatformAdmin === true, withrole === 'admin'as the legacy fallback. It does not readpositions.customSessioncallback inauth-manager.tsemitsisPlatformAdmin: grants.posture === 'PLATFORM_ADMIN', never from the array. Zero non-testcreateEvalUsercalls exist inplugin-auth.resolve-authz-context.tsderives the standing at §6b (the legacy grant, retired on walled postures) and §6b-config (OS_PLATFORM_OWNER_EMAIL). It projectsplatform_adminintopositionsat §6c from the samehasPlatformAdminGrant, andhasPlatformAdminStandingreads the rung.sys_position.nameandsys_user_position.positionrefuse the reserved built-in names (plugin-security/src/objects/reserved-identity-names.ts).The premise holds.
Readings of the dispatch hypotheses
eval-user.zod.tslines 225 and 226.grep -ci deprecaton the file gives 2 before, 0 after. That file is identical onf7b8a593and1bc6ca1d.createEvalUserdocblock unchanged. "isPlatformAdmin is always derived from positions" is what the factory computes (positions.includes(BUILTIN_IDENTITY_PLATFORM_ADMIN)), the ruling keeps that computation, and the new description does not contradict it.check:generatedbefore--fix: 14 of 15 current, 1 stale (check:docs).json-schema.manifest/**andspec-changes.jsondid not move.check:authorable-surfaceis green, and theidentity/EvalUser:isPlatformAdminrow is unchanged.git diff 1bc6ca1d HEADoverpackages/spec/authorable-surface*,authorable-defaultsandlivenessis 0 lines, so no liveness row moved.content/withoutreferences/andreleases/, plusapps/docs,skills/and package READMEs). It has 2 hits forisPlatformAdmin, exactly the card's two. Only 1 of them carries deprecated or alias wording.1bc6ca1d(isPlatformAdminwithin 80 characters of deprecat or alias, CHANGELOGs and releases excluded) gives 16 lines:plugin-auth(code comments and tests), outside this card. See Acceptance notes.Tests and gates (final head
39076f84)pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/identity/eval-user.test.tsgives 3 passed. It asserts that the published JSON Schema description names thePLATFORM_ADMINstanding, ADR-0095 D3 and ADR-0068 D4, carries no deprecation word and nodeprecatedflag, and equals the.describe()at the point of use.35709496withscripts/ablation-replace.mjs): the old describe text was put back on disk. The anchor count went 1 to 0, the replacement count went 0 to 1, and the blob went73dc1f88to565e7b3e. The pin went red as predicted: 2 failed (standing named; no deprecation word) and 1 passed (consistency). The tool proved the restore: the blob equals HEAD73dc1f88andgit diff HEADis empty. The test imports./eval-user.zodrelatively, so it readssrcand nodistrebuild was involved.pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2gives 620 files, 18488 passed, 1 todo.pnpm --filter @objectstack/spec typecheckexits 0. That coverstsc --noEmit,check:scripts-typecheckandcheck:test-typecheck, and the last of these compiles the new test file.pnpm --filter @objectstack/spec buildexits 0.packages/spechas no workspace dependencies, so its dependency closure is empty.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 116 commands from 7 paths against merge base1bc6ca1d. All 116 were run, and--ranwith recorded exit codes reports "116 run, 0 NOT-MEASURED (a DERIVED zero)".check-plugin-teardown-shape --self-test,lint check:doc-formula-expressions,lint check:doc-security-posture,spec check:skill-examples,check:docs-transcript-drift,check:dual-build-cjs-loadsandcheck:lean-entry-closure.f7b8a593, mostly the changeset families, which apply now that the changeset exists. All 14 are run and green.pnpm lintbelongs to CI):eslint --no-inline-config --format jsonon the 2 changed TS files reports 2 files, 0 errors and 0 warnings.eslint.config.mjsitself:files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'], so the md and mdx files are outside it.parserOptions.project, no typed rules), so this diff cannot move the verdict on any untouched file.Acceptance notes
createEvalUserdocblock ineval-user.zod.tslists "the customSession bridge" among the factory's users. It is not one: the session reads the rung, andplugin-authhas 0 non-test calls to the factory. This predates this PR and is unchanged here, by the claim's file surface. Who picks it up: nobody named.plugin-authstill call the key a "derived alias". The ADR-0068 D2 paragraph of thecustomSessioncomment inauth-manager.tsand theisPlatformAdminUserdocblock inplatform-admin-gate.tssay it, and the first even notes that D2's wording predates D4 rows. Two tests do as well: their titles and comments say "derived isPlatformAdmin alias". The code is right; only the word is stale after this ruling. Who picks it up: nobody named.维护者速读(草稿)
改了什么
EvalUser.isPlatformAdmin(当前用户是否为平台管理员)的说明去掉了“已弃用、由 positions 派生”的标记,改为说明它就是 ADR-0095 D3 定义的平台管理员身份(PLATFORM_ADMIN 档位)。该身份按请求判定:部署声明的管理员名单OS_PLATFORM_OWNER_EMAIL,在 single 模式下另含无范围的admin_full_access授权。为什么改
sys_member.add_memberis offered to every organization member, owners and admins included, but its door admits only a platform admin #21886(add_member仅平台管理员可见)因此被卡住。风险与代价(含回滚)
createEvalUser都不变,已有写法照常可用。席位意见
你要做的
docs/adr/**属于 Tier H 受管路径,本 PR 需要你批准后由 spec 席位落地。落地后 [finding]sys_member.add_memberis offered to every organization member, owners and admins included, but its door admits only a platform admin #21886 解锁。Generated by Claude Code