Uh oh!
There was an error while loading. Please reload this page.
perf(security): batch the curated capability existence read and equality-gate the reconcile - #11537
Conversation
…the reconcile (#11451) `bootstrapSystemCapabilities` issued one `SELECT ... WHERE name = ? LIMIT 1` per curated definition and then an `UPDATE` that fired whether or not `label`/`description` differed. The curated half now costs ONE batched `$in` read, and the reconcile is equality-gated for both halves. The #8470 predicate (`managed_by: 'platform'` + `organization_id: null`) travels INSIDE the batched query rather than filtering its answer: post-#8461 a name can carry a row per organization, so the wide question returns an unbounded set against a page capped at one row per name, and a truncated page reads as "absent", which inserts. The derived half keeps its per-item read - its question is cross-organization by construction and its counters derive from the lowest-id row installation- wide. Filed as #11520 rather than decided here. 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): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 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 f9dc2320aaa5d1172956ab1a231b88c9d3183088 && git checkout f9dc2320aaa5d1172956ab1a231b88c9d3183088
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cd932772d3ea155573e1c9f6a7feaa187721be9b 968f91714cd92039e34e0c71f43e2627acc9a14d && git checkout -B drift-repro cd932772d3ea155573e1c9f6a7feaa187721be9b && git merge --no-ff 968f91714cd92039e34e0c71f43e2627acc9a14d
node scripts/docs-audit/affected-docs.mjs --json cd932772d3ea155573e1c9f6a7feaa187721be9b
|
os-sam
commented
Aug 24, 2026
Seat review ( |
Uh oh!
There was an error while loading. Please reload this page.
Part of #11451
bootstrapSystemCapabilitiesbuilt its definition set in memory and then issued a per-itemSELECT … WHERE name = ? LIMIT 1for each one, followed by anUPDATEthat fired whether or notlabel/descriptiondiffered. The curated half's read is now ONE batched$in; the reconcile is equality-gated for both halves.The derived half's existence read is not batched. That is a decision, not an omission, and the reason is below — filed as #11520, which is a sub-issue of #11451 and remains open, so this PR deliberately does not carry a closing keyword for the card.
The design choice the card asked to be made deliberately
Written down before the code (the reasoning is preserved in the module header of
bootstrap-system-capabilities.ts, not just here).The card's three options conflate two independent questions: how to batch a predicated read (options 1 vs 2), and which halves to batch (option 3). This PR takes option 2's mechanism —
seed-name-lookup.tsgains an optional equality predicate carried alongside the$in— applied to the curated half only.Why not option 1 (batch wide, filter in memory)
Both harms are measured, not argued, against the double in
bootstrap-system-capabilities.test.tsthat deliberately models three shipped-driver behaviours (limitorders byidascending per #4363,nullisIS NULL,insertenforces(COALESCE(organization_id, '__global__'), name)):buildExistingByNameresolves that name toaaa_org_jia—managed_by: 'admin'— not the platform's row. Reconciling it isbootstrapSystemCapabilitiesreconciles an arbitrary row when a curated capability name exists in more than one organization —find(..., limit: 1)has no ORDER BY, so the platform's own row can be left unseeded #8470 exactly: an organization's authored copy overwritten every boot while the platform's own row is never addressed.limit: names.length, and post-fix(platform-objects,plugin-security,driver-sql): scope sys_user_preference and sys_capability uniqueness per organization (#8323) #8461 a name can carry a row per organization, so 10 rows match a request for 8 names and the two highest-idrows fall off. Two curated names then readabsentwhile their platform rows are present in the same fixture — andabsentsends the curated half to its insert branch, where the unique key refuses the write and the seeder reports ablockedCuratedcollision, every boot, on a healthy install.ExistingByNameIndex.get()returns ONE row chosen by arrival order, so filtering onmanaged_by/organization_idneeds an all-rows accessor — which hands every caller its own spelling of "which row is mine", the shape The Layer-0 tenant wall's strict equality annihilates the driver's platform bucket: #2734's fix is defeated on every walled read, and the org-less RBAC catalog reads ZERO for every principal #10103 repaired.Keeping the predicate in the query makes the result a provable singleton per name, which is the property
sys-capability.object.ts's own index comment names: "the seeder now scopes its curated lookup tomanaged_by: 'platform'+organization_id: null, i.e. exactly the bucket this key part keeps a singleton". That is what makes the page cap exact rather than merely usually-large-enough.Why not option 3's reasoning, even though the derived half is untouched
Not because it is the smaller half — it is the half that grows. Its lookup is cross-organization by construction, and
derivedRowIsOurs,skippedAuthoredand #8751'splatformStampedInOrgare all computed from the lowest-idrow installation-wide. Narrowing it to the platform bucket answers a different question: it stops counting an organization's platform-stamped row whenever our own bucket row also exists, and it starts seeding the bucket in the case #8552 ruled must be left alone ("adopting or backfilling it was rejected"). Batching it unnarrowed needs an unbounded read. Both are decisions above this card. #11520 carries the full analysis and the options so the next attempt does not re-derive it.Worth recording: on a stock installation the derived set is EMPTY — the platform's own permission sets grant only names already in
KNOWN_CAPABILITIES, and package-declared names are excluded viamaterializedCapabilityNames. It grows only for an app whosedefaultPermissionSetsgrant capability strings that nothing declares.Fence: every existing caller of
seed-name-lookup.tsis unaffected, by measurementThe widening is additive and opt-in:
...(equals ?? {})spreads nothing when no predicate is given, so a caller that passes none emits the exact key set it emitted before — not those keys plusundefined-valued ones, whichtoEqualwould have quietly accepted. The three pre-existing pins that assertql.wheres[0]equals{ name: { $in: … } }(permission sets, positions, declared capabilities) are green, and a new test pins the key set explicitly (Object.keys(…)→['name']) so theundefined-key loophole is closed rather than assumed. All four call sites' suites pass: 189 tests across 6 files.The predicate also rides the per-item degradation read. A fallback that dropped it would ask a wider question than the batched read it stands in for — and for this caller, wider is not "slower but the same", it is a different row.
Fence: the #8470 narrowing is still LOAD-BEARING, not merely still present
Ablation, the way #11470 proved its
customized: falseflip.CURATED_LOOKUPrelaxed to{}, confirmed on disk in both directions (injected form present ×1, removed predicate body ×0), restored byte-identically afterwards (git hash-object=382ee3d6…= the HEAD blob;git statusclean). The suites resolve the seeder through a relative import, so no rebuild is interposed between the edit and the run.Predicted in writing before running (RED, more failures) and observed exactly that — 85 passed unmutated, 9 failed ablated:
[#8470]tests, including "reconciles the platform row and only the platform row (adverse — the org row sorts FIRST)" and "is unaffected by the NUMBER of organizations holding the name";#11451 … › the seeder itself is unharmed by the fixture that breaks the unpredicated read;#11451 … › the batched read carries the #8470 predicate IN the query, with no other keys.The two tests that call
buildExistingByNamedirectly use a literal in the test file rather than the seeder's constant, and stayed green — as intended, since they measure the module, not the seeder's wiring.Fence: no round-trip pin was moved
bootstrap-seed-round-trips.test.tsgains a new#11451describe; every pre-existing pin is untouched. The new block states the derived half's residue as1 + derivedreads rather than hiding it, so a later card that batches it moves these numbers deliberately.⭐ Pins identity, not only counts: the rebuild is asserted to re-read and re-write the same row ids (no offsetting "one dropped, one inserted" behind a constant), and a drifted curated row's
UPDATEis asserted to land on that row's id.Behaviour changes worth review
CapabilitySeedResultgainsunchangedandunreadable. Reporting "wrote nothing because nothing differed" separately from "wrote nothing because the writes stopped working" is what stops the round-trip count being satisfiable by an implementation that quietly stopped reconciling.blockedCuratedcollision for each, describing a blocking row nobody ever saw.What was NOT measured
bootstrap-curve.mjsrig lives inobjectstack-ai/cloudand its axes are permission sets / positions / objects — not this one. Nothing here measures wall time, and a test that did would measure the machine it ran on. The only defensible claim is the round-trip count and the identity of the rows each leg touches, both pinned in-repo.Also not measured: real-driver behaviour. Every number above comes from in-memory doubles. The truncation finding is a property of the double's modelling of
limit+ORDER BY id, which its header documents as measured againstSqlDriveron better-sqlite3 — I did not re-run that measurement.Verification (all at
968f91714c, the head of this branch)tsc --noEmitfor@objectstack/plugin-security—TSC_EXIT=0vitest runover the fourbuildExistingByNamecallers' suites plus both capability suites —Test Files 6 passed (6) · Tests 189 passed (189)pnpm lint(repo-wideeslint . --no-inline-config) — exit 0, no findings. The whole farm, not a narrowed run.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(the script confirmed the--repoassertion holds against this checkout's remote). All green, quoting each gate's own verdict line:check:engine-double-contract—OK — 390 pinned, 133 in the DEBT ledger, 2 exemptcheck:test-source-alias—OK — 72 packages with tests scannedcheck:cross-package-test-inputs—OK: 14 package(s) read outside themselves, all declaredcheck:query-options-erasure—✓ ratchet holds: 67 unswept non-test site(s) … none newcheck:slot-lookup—✓ ratchet holds: 107 unswept site(s) … none newcheck:type-check-coverage—OK — 65/78 workspace packages type-checkedcheck:changeset-gate-self-tests,check:objectui-changeset,check:published-files,check:type-source-resolution,check:ci-filter-parity,check:plugin-teardown-shape,check:nul-bytes, ADR-0087 / no-major / empty-changeset — all exit 0check:type-check-debtneeds the whole workspace built, so its ledger entry for this package was reproduced directly instead:tsc --noEmitwith the**/*.test.tsexclusion lifted reports 11 raw errors, exactly whatTEST_DEBT['@objectstack/plugin-security']records, and none of the 11 diagnostics name a file this PR touches. The probe tsconfig was removed and the tree verified clean. CI runs the ledger-wide invocation.Release-notes input is the changeset (
.changeset/batch-curated-capability-existence-read.md); nocontent/docs/releases/**edit, and nopackages/spec/**edit was needed.Filed, not fixed here
buildExistingByName's UNSCOPED page cap truncates once a name can have more than one row — and a truncated page reads as "absent", which inserts #11518 — the same page cap truncates for the two callers already reading unscoped onmain(bootstrapDeclaredCapabilities,permission-set-projection). This PR sidesteps it with a predicate that makes its own page exact; it does nothing for those two. Out of scope here and not addressed by this branch.bootstrapSystemCapabilitiesis still per-item, and batching it is a ruling, not a refactor — it trades away #8751'splatformStampedInOrgsignal or reverses part of #8552 #11520 — the derived half, as above.Generated by Claude Code