Repository navigation
fix(platform-objects): offer Add Member only to a platform admin - #22064
Conversation
sys_member.add_member targets /auth/organization/add-member, which the ADR-0068 platform-admin gate admits for a platform admin alone; every other caller, org owners and admins included, gets 403 PERMISSION_DENIED. The action was gated on the organization feature only, so the button was offered to every member. Its visible now reads the standing the door reads, current_user.isPlatformAdmin == true (ADR-0068 D4, the ADR-0095 D3 rung), and requiresFeature composes onto it at parse time. - platform-objects: the lowering-matrix row pins the served predicate. - dogfood: on a real showcase boot, with current_user bound the way the console binds it (through extra, from each served session), a platform admin is offered Add Member and an org owner, an admin, a delegated admin and a plain member are not; the door refuses the owner and the member and admits the platform admin. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
One equality over the principals offered the action names every cell in its failure output, instead of stopping at the first principal that disagrees. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…d-member-visibility
📓 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 — 3 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 572b062bc3a2d7cbdbd7df88c372a489ca1324a8 && git checkout 572b062bc3a2d7cbdbd7df88c372a489ca1324a8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5cfd8661c4deef4716d1895883633003a54a9db7 bb5360101575090406643fbbe3e6585ac78d2f8b && git checkout -B drift-repro 5cfd8661c4deef4716d1895883633003a54a9db7 && git merge --no-ff bb5360101575090406643fbbe3e6585ac78d2f8b
node scripts/docs-audit/affected-docs.mjs --json 5cfd8661c4deef4716d1895883633003a54a9db7
|
ACCEPT (seat review) — PR #22064 at head
|
… SSO actions only to a platform admin (objectstack-ai#22068) Fixes objectstack-ai#21903 Clause-②: no ## What changes Thirteen first-party actions are now offered only to a platform administrator. That is the one standing their endpoints admit. - **Before:** each action was offered to every principal who could open the row or list. That covered org owners, org admins, delegated admins and plain members. The endpoint then answered each of them 403, because every one of these doors runs the ADR-0068 platform-admin gate first. - **After:** each action declares `visible: 'current_user.isPlatformAdmin == true'`. That is ruling A-lite's predicate (record 6019378035), the one `sys_member.add_member` landed with in PR objectstack-ai#22064. It is ANDed with any term the action already had, and `requiresFeature` composes onto it at parse time. - **The members:** - `sys_user`: `ban_user`, `unban_user`, `unlock_user`, `create_user`, `set_user_password`, `impersonate_user` and `set_user_manager`; - `sys_oauth_application`: `disable_oauth_application` and `enable_oauth_application`; - `sys_sso_provider`: `register_sso_provider`, `register_saml_provider`, `request_domain_verification` and `verify_domain`. - **The enumeration pin:** `packages/platform-objects/src/platform-admin-affordance-standing.test.ts` imports every `*.object.ts` under `packages/` and judges every action whose target door is platform-admin gated. An action added later fails it until its `visible` carries the standing. The doors and the callers they admit are unchanged. No key, export or parameter is added, and no label changes. That makes `Clause-②: no`, and the changeset is a `@objectstack/platform-objects` patch. **File surface.** This is the surface the claim declared: - the three producers in `packages/platform-objects/src/identity/`; - the lowering-matrix rows in `packages/platform-objects/src/platform-objects.test.ts`; - the new enumeration pin; - `.changeset/21903-platform-admin-affordance-visibility.md`. Two existing tests in the same `identity/` directory evaluate these predicates, so their fixtures were re-judged (see Acceptance notes): `action-predicate-sparse-face.test.ts` and `sys-user-set-manager-action.test.ts`. There is no `packages/spec`, `plugin-auth`, `@objectstack/formula`, `docs/adr` or dogfood edit. ## The census (H1), measured at `56bf27affb` Every first-party action with a `/api/v1/auth/` target lives in `packages/platform-objects/src/identity/`. The gate lines are in `packages/plugins/plugin-auth/src/auth-plugin.ts` unless named otherwise. | Action | Target | Door and its gate | Gated | `visible` served before | `visible` served after | |---|---|---|---|---|---| | `sys_user.ban_user` | `/admin/ban-user` | mount `:2670`, `gateAdmin` `:2672` | yes | `features.admin == true` | `(current_user.isPlatformAdmin == true) && features.admin == true` | | `sys_user.unban_user` | `/admin/unban-user` | mount `:2691`, `gateAdmin` `:2693` | yes | `features.admin == true` | same as `ban_user` | | `sys_user.unlock_user` | `/admin/unlock-user` | mount `:2490`, `judgePlatformAdmin` `:2502` | yes | `features.admin == true` | same as `ban_user` | | `sys_user.create_user` | `/admin/create-user` | mount `:2601`, `gateAdmin` `:2603` | yes | `features.admin == true` | same as `ban_user` | | `sys_user.set_user_password` | `/admin/set-user-password` | mount `:2851`, `gateAdmin` `:2853` | yes | `features.admin == true` | same as `ban_user` | | `sys_user.impersonate_user` | `/admin/impersonate-user` | better-auth endpoint replaced in place (`admin-impersonate-endpoint.ts:198`, caller predicate `:213`, wired at `auth-manager.ts:3609`) | yes | `features.admin == true` | same as `ban_user` | | `sys_user.set_user_manager` | `/admin/set-user-manager` | mount `:2536`, `gateAdmin` `:2538` | yes | `has(record.source) && record.source != "idp_provisioned"` | `current_user.isPlatformAdmin == true && has(record.source) && record.source != "idp_provisioned"` | | `sys_oauth_application.disable_oauth_application` | `/admin/oauth2/toggle-disabled` | mount `:2355`, `judgePlatformAdmin` `:2372` | yes | `(has(record.disabled) && record.disabled != true) && features.oidcProvider != false` | `(current_user.isPlatformAdmin == true && has(record.disabled) && record.disabled != true) && features.oidcProvider != false` | | `sys_oauth_application.enable_oauth_application` | `/admin/oauth2/toggle-disabled` | as above | yes | `(has(record.disabled) && record.disabled == true) && features.oidcProvider != false` | `(current_user.isPlatformAdmin == true && has(record.disabled) && record.disabled == true) && features.oidcProvider != false` | | `sys_sso_provider.register_sso_provider` | `/admin/sso/register` | mount `:2468`, `gateAdmin` `:2470` | yes | none | `current_user.isPlatformAdmin == true` | | `sys_sso_provider.register_saml_provider` | `/admin/sso/register-saml` | mount `:2958`, `gateAdmin` `:2962` | yes | none | `current_user.isPlatformAdmin == true` | | `sys_sso_provider.request_domain_verification` | `/admin/sso/request-domain-verification` | mount `:2983`, `gateAdmin` `:2988` | yes | none | `current_user.isPlatformAdmin == true` | | `sys_sso_provider.verify_domain` | `/admin/sso/verify-domain` | mount `:3002`, `gateAdmin` `:3005` | yes | none | `current_user.isPlatformAdmin == true` | | `sys_member.add_member` (the precedent) | `/organization/add-member` | mount `:2932`, `gateAdmin` `:2934` | yes | unchanged: `(current_user.isPlatformAdmin == true) && features.organization != false` | unchanged | | `sys_oauth_application.create_oauth_application` | `/sys-oauth-application/register` | mount `:3038`, session only | no | `features.oidcProvider != false` | unchanged | | `sys_oauth_application.rotate_client_secret` | `/oauth2/client/rotate-secret` | vendor route: `@better-auth/oauth-provider` 1.7.3 `rotateClientSecretEndpoint` authorizes the client's owner (`client.userId === session.user.id`); plugin-auth configures no `clientPrivileges` | no | `features.oidcProvider != false` | unchanged | | `sys_oauth_application.delete_oauth_application` | `/oauth2/delete-client` | vendor route: `deleteClientEndpoint`, same owner check | no | `features.oidcProvider != false` | unchanged | | `sys_sso_provider.delete_sso_provider` | `/sso/delete-provider` | vendor route: `@better-auth/sso` 1.7.3 `checkProviderAccess` admits the provider's owner or an admin of its organization | no | none | unchanged | Targets are spelled after `/api/v1/auth`. ## The dispatch's hypotheses, measured - **H1: confirmed, with one count corrected.** - The family is the thirteen actions above plus `add_member`. - The body's "`sys_oauth_application`: two actions" is two of the five that object declares. The other three authorize the session or the application's owner, as the table shows. - The body's "`sys_sso_provider`: four actions" is four of five. `delete_sso_provider`'s door is the vendor's provider-access check. - Outside object files, `service-datasource` registers two type-level actions. Their doors ask for the `manage_platform_settings` capability (`admin-routes.ts:272`), and the file rules out an `isPlatformAdmin` arm (`admin-routes.ts:268`). They are not members. - `sys_organization.change_slug` targets `/api/v1/cloud/...`, which another repository serves. Its door is **NOT MEASURED** here. - **H2: confirmed.** - `lowerRequiresFeature` (`packages/spec/src/kernel/public-auth-features.ts:352`) composes `(existing) && gate` at `:417`. - Where an action already had a `visible`, the standing is ANDed in the repo's existing spelling for an authored composed predicate. That is one flat conjunction with the principal term first, as the `sys_user` self-service predicates spell theirs. - Each served result is pinned in the lowering matrix. - **H3: confirmed. No member's door admits a principal below platform admin, apart from the legacy scalar noted below.** - **Offered (before / after):** I evaluated the served predicate from the built `dist` with `celEngine`, binding `current_user` the way the console does. The whole scope goes in as `extra`, with one subject under `current_user`, `user`, `ctx.user` and `os.user`. - At `56bf27affb`, every member was offered to all five principals: platform admin, org owner, org admin, delegated admin and plain member. - At `efd167f5a3`, each member is offered to the platform admin alone. - The four non-members are unchanged. - **Admitted:** I called the built `judgePlatformAdmin` on the same five session shapes. It admits the platform admin and refuses the other four with 403 `PERMISSION_DENIED`. It also refuses a session whose `positions` carry a tenant-written `platform_admin` without the rung. - The impersonate caller predicate (`admin-impersonate-endpoint.ts:213`) refuses the same principals with 403 `YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS`. Its oracle is the posture rung, which `auth-manager.ts:4083` also emits as the session's `isPlatformAdmin`. - On a real showcase boot at this head, `admin-route-nonadmin-refusal.dogfood.test.ts` and `admin-platform-admin-standing.dogfood.test.ts` passed 14 of 14. They cover all thirteen member doors in both directions. - **Page reach:** `sys_sso_provider` also requires `manage_platform_settings` at object level (`sys-sso-provider.object.ts:60`). That is a capability, not the rung, so the predicate is still needed for a holder of the capability who is not a platform admin. - **H4: confirmed. No structural oracle exists without a new export.** - `plugin-auth` exports no mount table. - `auth-route-ledger.ts` is package-internal, and its rows state the gate only in `note` prose. - `VENDOR_ADMIN_PATH_PREFIX` names the namespace, but `plugin-auth` is not a dependency of `platform-objects`. - So the pin uses the narrowest honest form, read off the mounts: - the `/api/v1/auth/admin/` namespace; - minus `/admin/has-permission` (a query that answers every caller) and `/admin/stop-impersonating` (it admits the impersonated session); - plus `/api/v1/auth/organization/add-member`. - **What it misses** (also stated in the test's header): - a gated mount added outside the namespace; - a capability-gated door; - routes another repository serves; - actions not declared on an object file. - **Dogfood pin (c) was not needed.** The door-level check is already pinned by the two dogfood sweeps above, which passed 14 of 14. ## Tests All runs are at the final head `efd167f5a3`. - **Pin (a), the lowering matrix in `platform-objects.test.ts`:** every member has a row with its composed served predicate, beside `add_member`'s row. - `set_user_manager` and the four `sys_sso_provider` rows are new. - The two exact-source assertions on the OAuth toggle pair were updated. - **Pin (b), `platform-admin-affordance-standing.test.ts`:** 5 cases. - The instrument is live: the walk loads more than 50 object files, and the family contains all 14 known members. That is a floor, not the exact set. - The oracle does not include the four non-member targets or the two excluded namespace routes. - Every member's served `visible` carries `current_user.isPlatformAdmin == true`. - No refused principal is offered any member on any candidate row. The refused principals are the four grades and a tenant-written `platform_admin` position without the rung. `current_user` is bound through `extra`. - The platform admin is still offered every member. - **The package:** `pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2` passed 63 files and 1006 tests. On `main` it ran 62 files and 996 tests: this adds 5 pin cases and 5 matrix rows. - **Typecheck:** `pnpm --filter @objectstack/platform-objects typecheck` passed. - The test-layer program (`tsconfig.test.json`) compiles all four touched test files: `--listFiles` counts 1 hit each, and the build program counts 0. - Its only errors are the 3 already ledgered in `src/feature-gate-guard.test.ts`. - **Dogfood, real showcase boot:** these passed on a real showcase boot: - `admin-route-nonadmin-refusal` and `admin-platform-admin-standing`: 14 of 14; - `org-admin-affordance-reach`: 14 of 14. ### Reverse verification, on committed HEAD `efd167f5a3` - **Prediction:** both pins turn red. - **Mutation:** `scripts/ablation-replace.mjs` replaced `set_user_manager`'s predicate with its pre-change text, `has(record.source) && record.source != "idp_provisioned"`. - The anchor count went from 1 to 0, and the file's blob from `0b9e8e738fb4` to `0ac51042a82d`. - An outer `trap … EXIT INT TERM` used absolute paths. - Both pins import the subject from source: a relative import, and the absolute `.object.ts` path. No `dist/` is in their resolution path, so no rebuild was needed. - **Result: red, as predicted, 3 failed of 137.** - (a) The `SysUser.set_user_manager` row expected `current_user.isPlatformAdmin == true && has(record.source) && record.source != "idp_provisioned"` and received the mutated text. - (b) "each member serves a visible that carries the standing term" listed `sys_user.set_user_manager`. - (b) "no principal the gate refuses is offered a member" listed five leaks. The org owner, org admin, delegated admin, plain member and the tenant-written position were each offered `set_user_manager`. - **Restore:** `git checkout HEAD --` on the absolute path. - The blob after the restore and the HEAD blob are both `0b9e8e738fb4911b9a379b2ebc699344f68127bd`. - `git diff HEAD` is empty, and `git status` is clean. - Both `ablation-replace` and the outer script proved this. ## Gates These ran at `efd167f5a3`. `origin/main` was still `56bf27affb` at the last fetch, so there was nothing to merge. - **Full build:** `turbo run build --filter=!@objectstack/docs --concurrency=2` passed 72 of 72 tasks. - **The derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 63 commands (31 pnpm, 32 node). All 63 exited 0. - `--ran`, with each exit code recorded: `63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN` (a derived zero). - The build-dependent ones printed real verdicts. For example, `check:dual-build-cjs-loads` loaded 106 entries across 66 packages, and `check:dts-closure` swept 72 packages. - **The artifact-roster block:** 54 commands outside the total. 51 exited 0. - Three answered NOT WIRED (exit 2) because they need pull-request context: `check-closing-target-claim`, `check-partof-closing-keyword` and `check-single-claim-paths`. - `check-partof-closing-keyword` was rerun with this body as `PR_BODY`. The other two need this PR's number, and CI runs them. - `check-sdui-manifest` passed, but its objectui version comparison is NOT CHECKED: there is no objectui checkout here. - `check:console-injection` ran its self-test only, because there is no console `dist/`. - **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` over the seven touched TypeScript files. - The JSON output counts 7 files, with 0 errors and 0 warnings. - `--print-config` resolves a config for each file, so none is ignored. - Type-aware linting is off. No resolved config carries `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; - the Dogfood Regression Gate beyond the three files above; - Build Core; - Temporal Conformance; - the repo-wide `pnpm lint`. ## Acceptance notes - **The door also admits a legacy `user.role === 'admin'` scalar** (`platform-admin-gate.ts:80-84`, and the impersonate predicate's first half), and this predicate does not read it. This is the boundary PR objectstack-ai#22064 recorded for `add_member`. Nothing in ObjectStack writes that scalar for a platform admin, and re-synthesizing it is vetoed. Noted, not filed. - **Two fixtures were re-judged, because the predicates they evaluate changed:** - `action-predicate-sparse-face.test.ts` bound an org owner through `user:`. Under that binding `@objectstack/formula` re-derives `isPlatformAdmin` from `positions`. - With the standing term in place, the owner would have short-circuited the record half of three predicates to `false`, and the sweep exists to test exactly that half. - Its OAuth per-site verdicts would also have gone red. - It now binds the same principal through `extra`, with the rung, the way the console binds it. The only alternative under `user:` was a `platform_admin` position, the spelling the standing must never be read from. - `sys-user-set-manager-action.test.ts` evaluated its "offered to an admin" case for a principal with no standing. It now binds a platform admin the same way. - **An unmeasured observation about SSO providers, not filed.** - `request_domain_verification` and `verify_domain` pass the platform-admin gate and then delegate to `@better-auth/sso`'s `checkProviderAccess`. So does `delete_sso_provider`'s door, with no gate in front. - That check admits only the provider's owner or an admin of its organization. - By reading, a second platform admin who did not register an org-less provider would be offered these actions and then refused by the vendor. This was not reproduced on a boot. - It is a different principal from this card's family. The standing remains the necessary term. - **The pin's oracle and what it misses** are written in its header. Every exclusion is named, with the principal that the excluded door admits. --- _Generated by [Claude Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #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.organizationfeature alone, so every member of the organization saw it: plain members, admins and owners.POST /api/v1/auth/organization/add-memberthen answered each of them 403PERMISSION_DENIED, because the mount runs the ADR-0068 platform-admin gate before anything else.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 fix(spec): EvalUser.isPlatformAdmin is the ADR-0095 D3 PLATFORM_ADMIN standing, not a deprecated alias #22018EvalUserSchema.isPlatformAdmindescribes 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-objectspatch.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 inpackages/platform-objects/src/platform-objects.test.ts, one dogfood case inpackages/qa/dogfood/test/org-admin-affordance-reach.dogfood.test.ts, and.changeset/21886-add-member-platform-admin-visibility.md. There is nopackages/spec,plugin-author@objectstack/formulaedit.The dispatch's hypotheses, measured at
56c88446ealowerRequiresFeature(packages/spec/src/kernel/public-auth-features.ts:352-419) turns an existing CELvisibleinto(existing) && gate(:415-418). The first-party precedent that carries both keys issys_user.enable_two_factor(sys-user.object.ts:534-535), pinned atplatform-objects.test.tsas(...) && features.twoFactor == true.visibleis exactly(current_user.isPlatformAdmin == true) && features.organization != false. This was read from the parsedSysMemberobject in the matrix test, and from the served/meta/object/sys_memberon a real boot through the dogfood case.EvalUserSchema.isPlatformAdmin(eval-user.zod.ts:225-241) now describes the ADR-0095 D3 standing.current_userthroughextra. At the current objectui pina58626c88d:fieldRules.ts:284passes the scope asextra;ExpressionProvider.tsx:190builds one subject undercurrent_user,user,ctx.userandos.user, withfeaturesbeside it;expressionUser.ts:176forwardsisPlatformAdminfrom the session.grants.posture === 'PLATFORM_ADMIN'(plugin-authauth-manager.ts:4083, emitted at:4107).user:,@objectstack/formulare-derives it frompositions(stdlib.ts:417-430throughcreateEvalUser). Underextra, the bag is merged verbatim (stdlib.ts:462).ownerprincipal is the harness's seeded dev admin, and on this boot its served session carriesisPlatformAdmin: true. It is the platform-admin principal, reused. Nothing on this boot declaresOS_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). ByEvalUserSchema's definition, the rung therefore comes from thesingleposture's unscopedadmin_full_accessgrant. The file had no org owner who is not a platform admin, so the case signs one up and sets its membership toowner, the way the file already sets grades.user:, unchanged.visible.rest,runtime,objectql,services,metadata,metadata-protocol,coreandpluginshave zero non-test hits for an action-visibleevaluation.ExpressionEngine.evaluate/celEngine.evaluatecall 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.current_user.isPlatformAdminfor this predicate, and the door stays the authority.Tests
All runs below are at the final HEAD (
bb53601015) unless marked otherwise.platform-objects: the lowering-matrix rowSysMember#add_membernow expects(current_user.isPlatformAdmin == true) && features.organization != false.pnpm --filter @objectstack/platform-objects exec vitest run: 62 files, 996 passed.typecheckpassed, with 0 TS errors. Its main program does not compileplatform-objects.test.ts.check:test-typecheckcompiles it throughtsconfig.test.json:--listFilescounts 1 hit there and 0 in the main program.Add Member is offered to a platform admin alone — current_user bound as the console binds it — and the door agrees./meta/object/sys_memberand the served/auth/configflags.isPlatformAdmin:truefor the seeded admin,falsefor the second owner, the admin, the delegated admin and the plain member. It also asserts that the second owner carriesorg_owner.celEngine, withcurrent_userbound throughextrathe way the console binds it. Exactly the platform admin is offered the action.PERMISSION_DENIED(envelopeerror.code) and no row is written; the platform admin gets 200success: trueand thesys_memberrow lands.test/org-admin-affordance-reach.dogfood.test.tspassed 14 of 14. The dogfoodtypecheckpassed, and its program compiles the touched file (--listFiles: 1 hit).turbo run build --filter='@objectstack/dogfood^...'passed 63 of 63 after the merge oforigin/main.Reverse verification, on committed HEAD
57b18bf94escripts/ablation-replace.mjsdeleted the linevisible: 'current_user.isPlatformAdmin == true',. The anchor count went from 1 to 0 and the file's blob from9beac68ce1c7tob8792313cac4. An outertrap … EXIT INT TERMused absolute paths.dist/:@objectstack/platform-objectswas rebuilt, thenscripts/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 indist/index.mjs:2219.expected 'features.organization != false' to be '(current_user.isPlatformAdmin == true) && features.organization != false'.platform admin,org owner, not a platform admin,org admin,delegated adminandplain member, against the expectedplatform adminalone. So the owner and member cells (and the admin cells) went red.git checkout HEAD --on the absolute path. The blob after restore and the HEAD blob are both9beac68ce1c7ed2b7143505916818a0a2e39a456, andgit diff HEADis empty. Bothablation-replaceand the outer trap proved this.Gates
These ran locally at
bb53601015, after the merge oforigin/main(5cfd8661c4).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 67 commands for this change set (35 pnpm, 32 node). All 67 ran and exited 0.--ranreconciles them:67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.check:dual-build-cjs-loadsfirst 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-filesandcheck:sourcemap-no-sources-contentwere rerun on that full build too, and all four passed.check-partof-closing-keyword,check-closing-target-claimandcheck-single-claim-paths.check-partof-closing-keywordwas then run with this body asPR_BODYand passed. The other two need this PR's number and CI runs them on it.check-sdui-manifestpassed, but its objectui version comparison is NOT CHECKED, because the container has no objectui checkout.check:console-injectionis NOT MEASURED: there is no consoledist/here, so only its self-test ran. This diff touches neither surface.check:adr-symbol-anchors,check:scripts-symbol-anchors,check:spec-docblock-symbol-anchorsandcheck:adr-anchorsall passed.pnpm exec eslint --no-inline-config --format jsonran over the three touched TypeScript files.--print-configresolves for each of the three, so none is ignored.parserOptions.projectorprojectService, andeslint.config.mjs:327-328states the repo never enables it. So this diff cannot move the verdict on any untouched file.pnpm lint.Acceptance notes
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.singleposture, where the standing comes from the seeded admin's unscoped grant. TheOS_PLATFORM_OWNER_EMAILroute of the walled postures is not exercised by this case.extra. The grade cases above it still bind throughuser:, as the dispatch required. Their predicates read onlycurrent_user.positions, which is identical under either binding.sys_user,sys_oauth_applicationandsys_sso_providershow no standing term invisible— the affordance family's closing card, with an enumeration pin (after #21886's ruling) #21903 (Blocked-by: #21886) remains open and carries the thirteen sibling platform-admin actions and the enumeration pin.Generated by Claude Code