fix(metadata-protocol, objectql, core): platform store reads and writes carry the explicit system opt-in - #21938
Conversation
…es carry the explicit system opt-in
The metadata protocol's overlay reads, list reads, authoring-gate fold,
audit/commit trail writes and package verbs, SysMetadataRepository's
store methods, ObjectQLPlugin's authored action/hook reads and the
authored-translation read reached the data engine with no principal and
no isSystem, so they rode plugin-security's principal-less hand-off
(ADR-0096 E1). Each engine call now passes context { isSystem: true }
(inside a repository transaction, { ...ctx, isSystem: true }, keeping
the handle). None of the gates the hand-off runs before next() is
scoped to the sys_metadata family, so nothing accepted or refused moves.
Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN
Co-authored-by: Claude <noreply@anthropic.com>
…in on the platform store calls One pin per package: the engine double records the context each call arrives with, and every step asserts it reached the store and that every call carried isSystem: true. metadata-protocol drives the repository write path (draft save, publish, active save, rollback, delete), the overlay reads and the package verbs; objectql and core pin the authored action/hook and translation reads. The new pinned double is recorded in the engine-double-contract coverage ledger (--write). Changeset added. Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude <noreply@anthropic.com>
…incipal-less-producers-engine
…incipal-less-producers-engine
…ler's bound check:objectql-double-limit refuses a new limit-blind find double; the bound is now applied after the filter, by presence. Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude <noreply@anthropic.com>
… on the platform store calls
Seven exact-argument expectations in protocol-meta, the reassign rebind
and the listDrafts WHERE now include context { isSystem: true }. The two
revert/rollback conflict pins identified put's in-transaction read by a
bare `context` key; every repository read now carries one, so they give
the engine a transaction that hands its callback a handle (as
ObjectQL.transaction does) and discriminate on the handle instead.
Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN
Co-authored-by: Claude <noreply@anthropic.com>
…incipal-less-producers-engine
…incipal-less-producers-engine
…incipal-less-producers-engine
📓 Docs Drift CheckThis PR changes 3 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 39 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 0808399c1e87cca260c86c2580a12189b0894a70 && git checkout 0808399c1e87cca260c86c2580a12189b0894a70
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8 58ac17e71fec4921c7cab56958a6f3e2be500758 && git checkout -B drift-repro 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8 && git merge --no-ff 58ac17e71fec4921c7cab56958a6f3e2be500758
node scripts/docs-audit/affected-docs.mjs --json 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8
|
|
CI note from
Generated by Claude Code |
…incipal-less-producers-engine
Fixes #21911
Clause-②: no
This is a slice of #21908: the engine-lane producers of the principal-less hand-off. #21908 stays open, and it builds the deny itself once every producer has a route.
What changed
Every engine call in the card's functions now passes
context: { isSystem: true }. Inside aSysMetadataRepositorytransaction it passes{ ...ctx, isSystem: true }, so the transaction handle still rides along. This is the opt-in that already exists. There is no new API, no export change, and no change to what any door authorizes.findServedOverlayRow(1findOne)overlayLockLayerAt(1find, in its store reader)readActiveOverlayRows/queryByOrg(2find),readFlattenedMetaItems(2find, draft preview)foldStoredCollection(1find). These are the only readsassertRuntimeAuthoringRulesissues.SysMetadataRepository:get1,put6,delete3,promoteDraft1,restoreVersion2,listDrafts1,nextItemVersion1,nextEventSeq1recordMetadataAudit(insert),persistPackageCommitRow(insert),publishPackageDrafts,resolveOverlayPackageBinding,storedFlowBindingAgrees,deletePackage,duplicatePackage(1 read each),reassignOrphanedMetadata(find+update)ObjectQLPlugin.readAuthoredActionRows(3find),readAuthoredHookRows(2find)readAuthoredTranslationLayer(2find)H1 holds: every row sits where the card says on
cab63967. Every engine call in each named function was enumerated with the TypeScript AST, not only the first one: 32 in metadata-protocol, 5 in objectql and 2 in core. Each now carries the opt-in.The six gates: none fires on these calls (Zone 1)
A system context skips the six gates the middleware still runs before the hand-off's
next(). Each one, on thesys_metadatafamily (sys_metadata,_history,_audit,_commit):sys_permission_set.sys_positionandsys_capability.sys_capability.sys_position_permission_set.engine-owned/append-only), butisUserContextWriteneeds auserId. A context with no principal passes it by construction, exactly as a system one does.sys_member.Measured. A local, uncommitted instrument sat at plugin-security's engine middleware. It wrapped each of the six gates, so a throw was recorded per gate and per operation. It also recorded the outcome after the hand-off. After the change it dry-ran the six gates for every moved
isSystemcall, with the flag cleared. The run covered the dogfood suite and a booted showcase dev composition.The instrument was reverted, and
security-plugin.tsequals its HEAD blob (5b4ab280). plugin-security'sdist/was rebuilt clean, andablation-dist-preflight --absentpassed.Before and after, per function (H2)
Principal-less, non-system operations credited to each function. A function is credited when it is the first frame past the engine, its closures and the repository transaction wrapper.
findServedOverlayRowoverlayLockLayerAtqueryByOrg(readActiveOverlayRows)readFlattenedMetaItemsdraft previewfoldStoredCollectionSysMetadataRepository.get/put/deletepromoteDraft/restoreVersion/listDraftsnextItemVersion/nextEventSeqrecordMetadataAudit/persistPackageCommitRowpublishPackageDrafts/resolveOverlayPackageBinding/storedFlowBindingAgreesdeletePackage/duplicatePackage/reassignOrphanedMetadatareadAuthoredActionRows/readAuthoredHookRowsreadAuthoredTranslationLayerAfter the change, the same calls arrive as
isSystemoperations in matching numbers. For example:findServedOverlayRow13,618,queryByOrg2,037,put296,recordMetadataAudit110. The dogfood suite was green on both sides with identical counts: 205 files passed + 1 skipped, 1,590 tests passed + 9 skipped. The boot answered the same statuses on both sides: admin data reads 200, anonymous reads 401. The 2,476 / 204 operations that remain come from the other slices' producers (settings, messaging, storage, auth, webhooks, datasource).Tests
metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts: the engine double records the context of every call. It drives draft save → publish → active save → rollback → delete, the overlay and list reads (each private reader directly, too), and reassign / duplicate / uninstall. Each step asserts that it reached the store and that every call carriedisSystem: true. Inside the transaction the context is exactly{ transaction, isSystem: true }.plugin-authored-actions.test.tsandplugin-authored-hooks.test.tseach gain one case.authored-translation-sync.test.tsgains one case.scripts/ablation-replace.mjs(blob equals HEAD,git diff HEADempty). Each pin resolves its subject fromsrc, so nodistleg was needed. The expected direction was red, and red is what was observed:findServedOverlayRowopt-in dropped: 2 of 3 metadata-protocol cases red.put's history-insert opt-in reverted to{ context: ctx }: 2 of 3 red.readAuthoredHookRows' first read set toisSystem: false: 1 of 11 red.readAuthoredTranslationLayer's first read set toisSystem: false: 1 of 6 red.falsespelling.89ced04af3. The merge to9fd7113eaabrought only two docs pages and oneresttest:local+repo): 376 files, 7,476 tests passed.typecheckfor all three exit 0, including objectql's and core'scheck:test-typecheck.os-verify-lock. The numbers are in the table above.dispatch-gates --commandsat9fd7113eaaderives 76 families. All 76 were run there and exited 0, and--ranreconciles 76 derived, 76 run, 0 NOT-MEASURED.check:engine-split-ratiofirst refused on the shallow clone. It was deepened (--shallow-since=2026-06-30) and re-run.check:dual-build-cjs-loadsfirst needed dists of unbuilt packages and aplugin-auditdeclaration. These were built, and the gate re-ran green.check:objectql-double-limitflagged the new double as limit-blind. The double now applies the caller's bound.pnpm lint):eslint.config.mjs:**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}plus thepackages/**object.eslint --no-inline-config --format jsonover the 13 changed.tsfiles: 13 files, 0 errors, 0 warnings.parserOptions.project, no typed rules), so this diff cannot move any untouched file's verdict.Acceptance notes
SysMetadataRepository.getByHash,list,historyandreplayFromHistorystill reach the engine with no context. No measured run reached them (0 records in either probe). They belong to security(spec, plugin-security): the AI tool contract says a context with no caller runs "RLS-on, sees-nothing", but plugin-security hands a principal-less context straight through, and on a hosted kernel it read and wrote more than a member may #21908's closure census.isSystem: the referential-integrity check on a caller-supplied lookup. On these writes the only lookup it judged wassys_metadata.organization_id, which the repository fills from the door-derived organization. The probe recorded 0 refusals from it (0 downstream failures on the card's functions). The otherisSystemreads on the census page touch none of these four objects, or only change a log line (the tenant-audit warning, the reference-cleanup actor label).listDraftsWHERE. Each now includes the opt-in. Two revert/rollback conflict pins (protocol-commit-history,protocol-writepath-object-ownership) foundput's in-transaction read by a barecontextkey, and every repository read now carries one. Their engine now hands its transaction callback a handle, asObjectQL.transactiondoes, and the pin discriminates on that handle. metadata-protocol's and core's own suites passed unchanged.scripts/engine-double-contract.pinned.jsongains three rows (--write, a grow-only coverage ledger) for the new pin's double. That double is copied from the pinned one inprotocol-publish-drafts-org-scope.test.ts.isSystemcount foroverlayLockLayerAtreads 10. This is not because its reads vanished. The function is notasync, so it does not appear in the async stack the probe filtered system records by. Its principal-less count (the claim) is 0 on both runs.d8657b5c, re-tested after every merge ofmain) test-merges cleanly onto this branch at9fd7113eaa. Its hunks are inanonymousFormIntakeOrgScopeRefusal,saveMetaItemandpromoteDraftForPublish, and none of them is a named function here. All 16 protocol calls keep the opt-in in the merged tree.6003826474).isSystemcensus page needs no edit. Object-literal producers are not elevation reads, and the census gate is green with its counts unchanged.Generated by Claude Code