fix(objectql): an in-process engine verb refuses an object name the registry does not resolve (#21516) - #21545
objectstack-fleet[bot] wants to merge 15 commits into
Conversation
…all-through Records every object name the engine's resolver hands to the driver without a registry entry, with its caller frames, into the file named by OS_TEST_UNRESOLVED_CENSUS. Reverted before the refusal lands. Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
The census it measured is recorded in the pull request. Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
… does not resolve resolveObjectName no longer hands an unresolved name to the driver as a raw table name. It throws the data door's own OBJECT_NOT_FOUND 404, built by one factory in @objectstack/core that the door's object-existence gate now calls too. judgeFilter keeps judging the filter for such a name (it reads nothing). Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
…e engine's refusal as the not-provisioned case The engine now refuses an unresolved name, so three best-effort platform probes that read a system table by a constant name no longer reach the driver when that object is not registered in a lean/bare composition: ObjectQL.probeInstallOrganizations (sys_organization), SeedLoaderService.resolveSoleOrganizationId (sys_organization) and SysMetadataRepository's history counters (sys_metadata_history). Each now recognises OBJECT_NOT_FOUND attributed to its own object as the same benign "not provisioned here" case it already recognises for a missing table — a path a body cannot reach, never the resolver's old raw-table fall-through. Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 144 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 4de02f7e9cdae2e127379cf9205941ae71e0d812 && git checkout 4de02f7e9cdae2e127379cf9205941ae71e0d812
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1ca1eb09727d7769bc605428ad1e6fb62d743f49 6752a29827847c463c90f5171d0ee9f7b083e5d8 && git checkout -B drift-repro 1ca1eb09727d7769bc605428ad1e6fb62d743f49 && git merge --no-ff 6752a29827847c463c90f5171d0ee9f7b083e5d8
node scripts/docs-audit/affected-docs.mjs --json 1ca1eb09727d7769bc605428ad1e6fb62d743f49
|
…resolved-name-refusal
… family they write through Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
Deliberate probes of an unregistered name now assert the refusal (the data door's OBJECT_NOT_FOUND envelope, nothing reaching the driver); harnesses where the fall-through was incidental register the objects they write through. The #3770 case-B pin keeps the door's 404 and flips its engine assertion. Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
…s refusal An unregistered organization object (and a view's unregistered probe object) is no longer read through the driver: the engine refuses the name first, so the declared refusal no longer occurs. The capture stays declared and each pin now asserts nothing was withheld, so a returning read turns it red. The channel-asymmetry pin registers its probe object (unprovisioned) so it still measures a real driver refusal. Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
… readers resolve The engine now refuses an object name its registry does not hold, so partial compositions register what a deployment's plugins register: the authz resolver's read set (left unprovisioned, so it still reads "no grants"), the settings service's secret and audit objects, and the approvals fixture's two expected-absent probes (unprovisioned, so its withheld-refusal pin is unchanged). Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
…l reads The engine now refuses an object name its registry does not hold, so the real-engine import/export and classification harnesses register the family after their DDL; an unprovisioned store still answers the driver's own "no such table", which those pins classify. Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
…r answer The control's driver lines existed only because the hooks read objects the boot never declared through the engine's raw-table fall-through; the engine now refuses those names before any driver. Each probe read now records its answer, and the control asserts OBJECT_NOT_FOUND and no driver line. Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
… engine refuses Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
… refusal; type a mock The record-change org-probe pin asserts the absent organization object is quiet by construction; the conformance stack registers the authz resolver's read set (unprovisioned) that its stubbed auth service never did; the engine.test expand mock types its parameter (test-typecheck ledger). Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
… not resolve; changesets An action body through REST /actions reading an out-of-band table by its unregistered name now answers 404 OBJECT_NOT_FOUND for an administrator and a member, with nothing of the table in the answer; the same body on a registered name is served (the control); the data door's own 404 is the reference. Changesets: core minor (new objectNotFoundError export), objectql minor BREAKING narrowing with its ADR-0087 disposition, metadata-protocol patch, spec patch (contract docblock). Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
…resolved-name-refusal
Fixes #21516
Clause-②: yes (narrowing)
An in-process engine verb now refuses an object name the schema registry does
not resolve with the data door's own
OBJECT_NOT_FOUND(404), instead ofhanding that name to the driver as a raw table name. One name space for the
in-process verbs and the generic data door (triage ruling). The engine's accept
set narrows; no surface is widened. The
yeshalf of the clause line is the onenew export,
objectNotFoundError, in@objectstack/core.What changed
packages/core(newobjectNotFoundError). One factory for theOBJECT_NOT_FOUND/ 404 envelope, besiderecordNotFoundErrorand for thesame ADR-0076 D2 reason (the engine closure cannot import the package where
the door's envelope was written). Both doors now build the refusal here.
packages/metadata-protocol(the door).assertObjectRegisteredraisesthat shared factory: same wire status, same code.
packages/objectql(the engine).resolveObjectNamethrowsobjectNotFoundErrorfor a name the registry does not resolve, rather thanreturning it as a physical table name. Every in-process verb
(
find,findOne,count,aggregate,insert,insertMany,update,delete,validate) resolves through it, so all refuse uniformly: nospelling allow-list, no per-caller marker.
judgeFilterkeeps judging thefilter for an unresolved name (it reads nothing and reaches no driver), as its
contract states.
known system object and were already fail-soft on a missing table now treat
the engine's refusal (attributed to their own object) as the same "not
provisioned in this composition" case. A body cannot reach these paths:
ObjectQL.probeInstallOrganizations(registry-presence guard),SeedLoaderService.resolveSoleOrganizationIdandSysMetadataRepository'shistory counters (refusal recognised by
codeandobject).packages/spec. TheIObjectQLEngine.judgeFilterdocblock states thatexecution refuses an unknown object before admission (comment only; it ships
in the built type declarations).
Why (classes, doors, roles, codes only)
An action body invoked through the actions door could name a protected member
of the stored-metadata family by a spelling the registry does not resolve and
receive its stored content, whether a member or an administrator invoked it.
The in-process verb handed that name to the driver as a raw table name, and
every name-keyed in-process guard (PR #21513's reader seam among them) was
addressed by the registered name only. The generic data door answers
OBJECT_NOT_FOUNDfor the same name. The engine now answers the same, so thetwo doors share one name space and no name-keyed guard can be stepped around by
naming its target some other way.
Census: does any legitimate platform reader rely on the raw-table fall-through?
Instrumented the resolver's fall-through and ran the
objectql,metadata-protocolandruntimesuites, plus a full boot, seed and door driveof four example apps (crm, showcase, todo, multi-package). The instrument was
reverted in the branch; the net diff carries none of it.
Every in-repo caller that passes a possibly-unresolved name, by function:
ObjectQL.probeInstallOrganizationsSeedLoaderService.resolveSoleOrganizationIdSysMetadataRepositoryhistory countersObjectQL.cascadeDeleteRelations/planCascadeAtomicity/referenceExiststry/catchConclusion: no production or example reader relies on the fall-through.
Fixture triage (the test-only fallout, per the seat's answer)
Every test that encoded the raw-table fall-through, by disposition. No ADR text
is edited, and no pin ruled under the noise-discipline decision (card 7929)
changes what it asserts.
refusal (
code+status, and where the test watched the driver, thatthe driver saw nothing).
objectql:engine-20822-no-field-map-type-blind-lowering,query-expression-conformance,engine.test,engine-undeclared-update-field,engine-undeclared-field-preflight,engine-temporal-comparand-door,engine-aggregate-filter/-having/-reference-verdict,engine-summary-recompute-context,registry-field-type-refused-at-door, theglobal-search-*pins,engine-judge-filter,engine-organization-probe-outage.protocol-unregistered-object.test.ts, case B of the card 3770 gate: thedoor's 404 assertion is unchanged; the engine assertion turns from
"serves the row" to "refuses
OBJECT_NOT_FOUND/ 404"; header item ②gains one sentence naming [finding] [security] An in-process engine verb passes an object name the registry does not resolve to the driver as a raw table name, so a sandboxed body reads a protected table by a name the data door refuses #21516.
registry-gate-wiring: the premise reads ground truth at thedriver (host code's declared internal path), then asserts the engine
refuses the same name.
platform reader resolves (registered after boot or DDL, so nothing new is
provisioned and every outage/absence subject keeps its meaning).
objectqlmetadata-write harnesses (delete, save, publish-meta,publish-package-drafts, protocol-derived-provenance,
protocol-save-meta-repo-path, protocol-picklist,
protocol-publish-canonical-fold): the stored-metadata family.
rest(14 harness files): the stored-metadata family, after DDL.plugin-security(4 files) and thehttp-conformancestack: the authzresolver's read set, unprovisioned, so the missing-table answer is still
what they measure.
plugin-approvalsstatus-mirror cascade: the delegation object and theorg object, unprovisioned.
service-settings: the secret andsetting-audit objects.
unregistered org object is now refused before any driver, so a pin that read
"the probe reached the driver and was withheld" now reads "the probe reached
no driver" (
tablesSeen()equal to empty wheresilentChannels()wasread).
runtime(about 17 files) andtrigger-record-change.expected-read-refusal-noise.channel-asymmetry.test.tsregisters its probeobject, unprovisioned, so the real driver refusal is still what its two
channels measure.
cliserved-boot control (schema-migrate.host-composition): eachhook's probe read is witnessed by its recorded
OBJECT_NOT_FOUNDanswer, notby a driver line; the SQL driver suppresses that line for its own deferred
set, so the line never was the subject.
engine.test.ts: one mock parameter typed (name: string), the@objectstack/objectql#typecheckred of the earlier heads.New pin: the measured public door
packages/runtime/src/unresolved-object-name.actions-door.pin.test.tsbootsthe plugin set
bootStackuses and drives REST/actions. The target is atable that exists and holds a sentinel row, created out of band at the driver
and registered nowhere (the class the card measured, naming no protected
table). For an administrator and a member, the action body's read answers
404 OBJECT_NOT_FOUNDand the sentinel appears nowhere in the answer. Control:the same body shape on a registered name is served. Reference: the generic data
door's answer for the same name is the same 404. PR #21513's reader-seam pins
stay green.
Ablation (one-shot; nothing left in the tree)
Mutation leg,
scripts/ablation-replace.mjsonengine.ts: the refusal inresolveObjectNamereplaced by the old raw-table return plus a marker branch(marker on disk 1, refusal on disk 0); rebuilt;
ablation-dist-preflight:marker present in 4 built files. Pins under mutation:
protocol-unregistered-object,engine-20822-...,query-expression-conformance,engine-judge-filter):5 failed | 245 passed (250)2 failed | 4 passed (6)(both role cases)Restore leg:
git checkout HEAD -- engine.ts; blob equals the HEAD blobf5793bff919d,git diff HEADempty, porcelain clean; rebuilt; marker absentfrom all 14 built files. Pins restored:
250 passed (250),6 passed (6).engine.tsis byte-identical on the final head (the later merge ofmaincarried no
objectqlsource).Verification on the final head
6752a29827(merge oforigin/mainat1ca1eb0972)objectqlfull suite:367 files, 7384 passed.metadata-protocolfull suite:206 passed, 3 skipped files; 3187 passed, 19 skipped.runtimefull suite:317 files; 5176 passed, 19 skipped.service-analyticsfull suite:175 files; 4152 passed, 253 skipped.cliunit tier:252 files, 3685 passed; integration tier, the three filesthis branch or the merged
maintouched:36 passed.mcp:35 files, 389 passed; dogfood (registry-gate-wiring+ the twofiles
mainadded):16 passed; themain-added example,metadataandcorefiles: green.plugin-security,plugin-approvals,service-settings,http-conformance,trigger-record-changetest tasks: turbo38/38(5 test tasks run, 33 cached builds).
63/63turbo tasks.bab0903840): typecheck of every touched package76/76tasks;restrepo project177 passed; the 14restharness files552 passed, 21 skipped.6752a29827: every check green except "Part-of PR must not also closeits card", which read the earlier body; this body is its input.
Acceptance notes
internal callers unaffected") is narrowed at the engine: the door's gate is
unchanged and the engine now gives the same answer. ADR-0053's type-blind
lowering is kept for a registered object with no field map, and removed for an
unregistered name (the bypass itself). No ADR text is edited here.
@objectstack/coreminor (Clause-②: yes, the new export);@objectstack/objectqlminor (Clause-②: no (narrowing), with its ADR-0087disposition);
@objectstack/metadata-protocolpatch;@objectstack/specpatch (docblock).
packages/cli/test/refusal-renders-once.e2e.test.ts(added onmain) sitsin neither of the cli package's two vitest projects and was not run here;
it drives refusal rendering of
os init/os compile, which this changedoes not reach.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3