Repository navigation
fix(objectql): an in-process engine verb refuses an object name the registry does not resolve (#21516) - #21545
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 5 package(s): 15 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 — 149 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 c84a16078041482977cd8ac13a1e738ec366f917 && git checkout c84a16078041482977cd8ac13a1e738ec366f917
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 901e7cf13abd61afb4990ebc1e3b9cd36cf9b33d 2df18ea792202e43ae85a0d3a7d4a5ff683af9b7 && git checkout -B drift-repro 901e7cf13abd61afb4990ebc1e3b9cd36cf9b33d && git merge --no-ff 2df18ea792202e43ae85a0d3a7d4a5ff683af9b7
node scripts/docs-audit/affected-docs.mjs --json 901e7cf13abd61afb4990ebc1e3b9cd36cf9b33d
|
…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
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 37123511365 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
…resolved-name-refusal
…ver, not the engine The read-only boot composes no auth plugin, so sys_account is not a registered object there, and the engine now refuses a name its registry does not resolve before any driver is asked. The pre-flight inventories the physical table in its legacy shape, so it reads through the driver the engine routes that name to; the missing-table refusal of that read is still the only one recognised as no rows. Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude <noreply@anthropic.com>
…g driver is the probe's refusal Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude <noreply@anthropic.com>
✅ ACCEPT of the merge-queue fix at head
|
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.
Merge-queue fix
os migrate account-issuerreadssys_accountthrough the driver the engine routes that name to. Its read-only boot registers nosys_account, and the engine now refuses an unregistered name. The refusal is not read as absence, because that would report a table full of accounts as a clean pre-flight. #21570's missing-table reading forsys_accountis kept, and every other failure still throws.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 #7929 (the cross-field withholding decision)
changes what it asserts. The table-keyed
captureExpectedReadRefusalsnoisepins keep their subject and reshape their reading (item 3).
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