Uh oh!
There was an error while loading. Please reload this page.
fix(security): stop reporting the platform bucket as a "pre-fix" leftover with a remedy that recreates it - #11629
Conversation
…over with a remedy that recreates it A fresh walled deployment mints 8 organization-less `sys_permission_set` rows from `bootstrapPlatformAdmin` — the fifth seeder, kept outside the per-organization conversion by the 2026-08-20 ruling on #10103. The per-organization pass then warned, once per organization, that "pre-fix organization-less rows are still present" and offered "re-initialize the deployment, or adopt each row by hand". On a deployment hours old nothing was pre-fix, and re-initializing mints exactly those rows again, so the first remedy branch was a loop. The pass now separates the two classes it was conflating and gives each the remedy that fits, carrying a machine-readable `origin` beside the named rows. The seeding itself is byte-identical: the rows stay, unreaped, because PLATFORM_ADMIN is derived from an unscoped grant pointing at the `admin_full_access` row by row id. Part of #11532 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 14 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 0ccc27e7b1a8c329cca71528e81c496752411365 && git checkout 0ccc27e7b1a8c329cca71528e81c496752411365
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3637731e2811d375fe98187bc67f3e8a0b4585ca c83945d19c60b1e1f39aafa6b2b8fe688286229c && git checkout -B drift-repro 3637731e2811d375fe98187bc67f3e8a0b4585ca && git merge --no-ff c83945d19c60b1e1f39aafa6b2b8fe688286229c
node scripts/docs-audit/affected-docs.mjs --json 3637731e2811d375fe98187bc67f3e8a0b4585ca
|
Uh oh!
There was an error while loading. Please reload this page.
Part of #11532
Implements the diagnostic half of the card. The minting half is deliberately
untouched and is explained under "What this PR does NOT do", so the card stays
open after this merges — hence
Part of, not a closing keyword.Premise re-measured on current
mainbefore any code was writtenThe card was dispatched with an explicit precondition: check current
origin/mainfor the symptom, not the issue number. Measured at
2cf5a96cc7, against a realObjectQL+SqlDriver(better-sqlite3), driving the SHIPPED seeders in the bootorder
security-plugin.tsuses (bootstrapPlatformAdmin, then onebootstrapDeclaredPermissionspass per organization),OS_TENANCY_POSTURE=isolated,three organizations:
sys_permission_setrows after a fresh walled bootadmin_full_access,mcp_agent_data_read,mcp_agent_data_write,mcp_agent_restricted,member_default,organization_admin,organization_admin_no_bypass,viewer_readonlymanaged_by = 'platform',package_id = NULL(every row)managed_by = 'package',package_id = com.objectstack.plugin-security— different row idspre-fix organization-less sys_permission_set rows are still present … Remedy: re-initialize the deployment, or adopt each row by handThe symptom is live on current
main. Pinned by identity, not count — thename set and the row ids are asserted, because two offsetting errors can hold a
count constant while the membership inverts.
Nothing here reads the pass's own verdict as evidence. That verdict is the thing
under test and it is self-falsifying, so every assertion reads either rows straight
off knex (past every engine-side projection) or the recorded logger calls.
The two things that did not hold up
bootstrapPlatformAdmin—the fifth seeder, outside the four fix(security,sharing): materialize the RBAC catalog per organization #11121 converted — before the deployment's
first organization existed. An operator on a deployment hours old was told they
were carrying legacy state they never had.
mints exactly those 8 rows again on the next boot, so only the hand-adoption
branch ends — and that one hands a platform-wide bucket to a single tenant.
Related, and repaired here too:
per-organization-catalog.ts's own header assertedthat "a fresh walled deployment never mints an organization-less catalog row once
these seeders run per organization". On the measured boot it mints 8, from a seeder
outside those four.
What changed
per-organization-catalog.ts—warnPreFixOrganizationLessRowsbecomeswarnOrganizationLessRowsand splits its input into the two classes it wasconflating, emitting one line each with the remedy that fits, and carrying a
machine-readable
origin('platform-bucket'/'pre-fix-residue') beside thenamed rows. The
pre-fix-residuemessage and remedy are unchanged, verbatim.The module header's false claim is corrected and now names the ruling it depends on.
bootstrap-declared-permissions.ts— new optionalplatformBucketNames,defaulting to the shipped
defaultPermissionSets. The default is what makes thiscorrect for every shipped composition even if the option is never threaded; the
option exists for a host that overrode
SecurityPluginOptions.defaultPermissionSets.security-plugin.ts— threads that host's actual array, computed from the samebootstrapPermissionSetsthat goes to the platform bootstrap and onto the manifest,so the two can never disagree.
bootstrap-platform-admin.ts— under a walled posture the producer now says whatit wrote: the defaults were seeded without an organization, and each organization's
copies come from the catalog pass. The rig's boot line read
{"seeded":8}withnothing to indicate the rows carried no organization at all, so the operator's first
sight of them was the warning above. The seeding itself is byte-identical.
bootstrap-declared-positions.ts/bootstrap-builtin-positions.ts— importrename only. They declare no platform bucket, because nothing mints an
organization-less
sys_positionrow any more, so every leftover there really ispre-fix residue. Behaviour unchanged.
Membership is decided by name, not by
managed_by, because the question the remedyturns on is "will a re-initialized deployment have this row again?" — true for these
names whatever provenance the current row carries (a pre-#8692 install stores
'admin'on the very same names).
What this PR does NOT do — and why that is a ruling, not an omission
⛔ It does not stop the minting. #10103's maintainer ruling of 2026-08-20
(Q1/Q2, live decision-inbox session) closes that, verbatim:
bootstrap-declared-permissions.ts's own header already records the same boundary.The rows are load-bearing besides:
PLATFORM_ADMINis derived from an unscopedsys_user_permission_setgrant pointing at the organization-lessadmin_full_accessrow by row id (
resolve-authz-context.ts§6b), which is why the ruled Option Crefused a #8617-breadth reap in the first place.
So the loop this PR breaks is the one that was actually breakable: it lived in the
remedy text. Routing the platform bucket through the per-organization pass remains
the maintainer's open call, and it is raised in the dev report rather than filed as a
card, because the ruling says that card is filed only when an onboarding flow wants it.
Verification
All at final commit
c83945d19cunless stated.run before any source edit, on a tree whose only difference from
origin/mainwasthe untracked test file (
git statusshowed exactly that one entry). Predicted inwriting first, then run: cases 1/2/5 green (the premise), cases 3/4 red. Observed
exactly that —
Tests 2 failed | 3 passed (5), failing onAssertionError: expected undefined to be 'platform-bucket'andAssertionError: expected undefined to be 'pre-fix-residue'. The pre-fix code isthe ablation; no mutation was needed, so no restore could silently fail.
pnpm --filter @objectstack/plugin-security typecheck→> tsc --noEmit, exit 0.pnpm --filter @objectstack/plugin-security test -- --maxWorkers=2→Test Files 77 passed (77)/Tests 1470 passed (1470), includingper-organization-catalog.test.tscase 3, which pins the unchanged pre-fix wordingand its
re-initializeremedy.pnpm lint(repo-wideeslint . --no-inline-config) →os-verify-lock: VERDICT command-exit 0 · held the lock 87s. Not narrowed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no hand-written path list), all green at this commit:
check:nul-bytes,check:engine-double-contract,check:where-matcher,check:slot-lookup,check:query-options-erasure,check:type-check-coverage,check:cross-package-test-inputs,check:test-source-alias,check:type-source-resolution,check:published-files,check:i18n(CLI builtfirst — it refuses as
PREREQUISITE NOT METotherwise, which is not measured, notgreen),
check:empty-changeset,check:adr-0087-registration,check:changeset-gate-self-tests,check:objectui-changeset,check:ci-filter-parity,check:plugin-teardown-shape,scripts/docs-audit/check-affected-docs.mjs,scripts/pm/release-rehearsal-clone.mjs --self-test.scripts/engine-double-contract.pinned.jsongains one row. The new suite'sdelegating seam routes
update()through the producer's ownassertEngineUpdateDispatch, so this is the pinned ledger growing by one — notthe shrink-only baseline. Written with the gate's own
node scripts/check-engine-double-contract.mjs --write(
1 added or grown, 0 lost), never by hand.check:type-check-debt: the whole-ledger--re-measureneeds the entireworkspace built and is CI's run. What was measured here instead is the exact quantity
the ratchet compares for the one package this PR touches: the gate's own
remeasureProjectshape was reproduced (package tsconfig with the**/*.test.tsexclusion dropped) and
tsc --noEmitreported 11 errors — equal to the recorded@objectstack/plugin-security: { errors: 11 }, with 0 of them in the new test file.The ratchet cannot drift upward from this change. The other ledger entries were not
re-measured here; no path in this diff reaches them.
Heavy steps were serialized through
scripts/pm/os-verify-lock.sh; every verdict aboveis read from the gate's or the lock's own printed line, never from
$?after a pipe.Generated by Claude Code
Generated by Claude Code