Repository navigation
fix(runtime)!: callData asks each nested-relation condition's target its exposure before the engine reads it (#22823) - #22893
Conversation
…its exposure before the engine reads it The query fallback (no protocol service) and the aggregate arm hand a caller's where to the ObjectQL engine, which lowers a nested-relation condition by reading the related object as the caller. Each condition's target is now asked the gate the addressed object is asked, for list, through the shared relationConditionSites walk, and an unserved target refuses the call with the data door's 404 OBJECT_API_DISABLED / 405 OBJECT_API_METHOD_NOT_ALLOWED. Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
…or runtime's exposure codes, changesets The census pin names callData as the third door that reaches the engine's relation lowering through the shared walk, held by its own package's pin. The error-code ledger lists OBJECT_API_DISABLED and OBJECT_API_METHOD_NOT_ALLOWED under @objectstack/runtime, the provenance check:error-code-provenance requires of an emitting package. Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
… import as an unjudged protocol Since #22800 the identity import's run protocol carries the declaration reader, so the parenthetical stated a false example. Only that parenthetical is reworded; the rule it illustrates is unchanged. A deliberate correction of a pending note this PR did not add. Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
…ddressed object and the relation targets One read of the execution context's system flag, so the system-context census row for callData's exposure bypass keeps describing one decision. Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 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 b2722e362a4ced348e9ae1df0867de932492af7e && git checkout b2722e362a4ced348e9ae1df0867de932492af7e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2e11d9006cdc9548dbb1f3883687fdbe2bfe21f7 bd79104296c7cb6b81710c9312d5f92e102df6af && git checkout -B drift-repro 2e11d9006cdc9548dbb1f3883687fdbe2bfe21f7 && git merge --no-ff bd79104296c7cb6b81710c9312d5f92e102df6af
node scripts/docs-audit/affected-docs.mjs --json 2e11d9006cdc9548dbb1f3883687fdbe2bfe21f7
|
|
DELIBERATE CORRECTION —
|
Contract reviewServed-tier: Read-only, on the card (#22823: body, release 6110112147, claim 6112207746, os-dev-report 6113489644, file-surface addendum 6113535816), the PR (body, the seat's DELIBERATE CORRECTION note 6113547162, the docs-drift comment, 7 files, 4 commits), the net diff against Check-runs on the head, at the final read: 26 success, 3 skipped ( ① Derived judgmentsAccept-set change the diff implies: one narrowing, at two arms of one function,
Tests, read against the diff: 13 cases in the new runtime file; the four refused classes ( ② Semver level
③ Boundary flags
The DELIBERATE CORRECTION (the seat's PR note 6113547162; PR body section "Deliberate correction"; carried from #22800's ACCEPT 6111874171).
The correction is confirmed. Per the seat's note, Advisory, no action: the docs-drift comment lists nine hand-written pages naming Implemented-by: VERDICT: PASS Adopted by |
Fixes #22823
Clause-②: no (narrowing)
This is census row 10 of #22661. The parent card stays open; the seat that owns it closes it.
What changed
callData(packages/runtime/src/action-execution.ts) hands a caller'swhereto the ObjectQL engine at two positions. The engine lowers a nested-relation condition by reading the related object as the caller (ObjectQL.lowerRelationConditions), while the exposure gate at the top ofcallDatajudges only the addressed object.query/findfallback. It runs when no protocol service resolves. This is the card's position.aggregatearm. It never takes the protocol service, so it is the engine's door on every assembly. The HTTP MCPaggregate_recordstool reaches it throughdomains/mcp.ts. This is a bounded in-place fix, explained below.At both positions, the new
refuseUnservedRelationTargetsnow asks each condition's target the same gatecallDataapplies to an addressed object (checkApiExposure), forlist, before the engine is called. The conditions are found by the sharedrelationConditionSitesfrom@objectstack/core. There is no second rule and no second walker.A target the gate does not serve refuses the call with the data door's own envelope, built field for field like
refuseUnservedRelationTarget:apiEnabled: falseanswers404 OBJECT_API_DISABLED.listanswers405 OBJECT_API_METHOD_NOT_ALLOWED, withallowed.object(the target) andparam(where.LOOKUP).So one
callDatagives one answer whether or not the deployment registered the protocol slot. The same rule already holds for the not-found and delete bodies in this function.The system bypass is decided once, by the same flag read for the addressed object and the relation targets (
exposureGated). That keeps the system-context census row forcallDatadescribing one decision, and its declared counts unchanged.Landing points beyond the declared file surface (read this)
packages/spec/src/api/error-code-ledger.zod.ts. Two value-only rows listOBJECT_API_DISABLED/OBJECT_API_METHOD_NOT_ALLOWEDunder@objectstack/runtime, plus their own@objectstack/specpatch changeset.check:error-code-provenancerequires every stamping package to list the codes it stamps.ablation-replace), the gate answersFAIL — 2 stamp site(s) of a registered code with no provenance row, namingaction-execution.ts. With the rows present it is green.22737-spec-ledger-metadata-protocol-exposure-codes.mdand the analytics one before it.REGISTERED_ERROR_CODESis unchanged, and no generated artifact moves.aggregatearm of the same function. This is the role file's bounded in-place exemption. All four conditions hold:wherehanded to the engine after only the addressed object is judged);listquestion ofaggregate'swhere);Measurements
Reach of the
queryfallback (the card's first step). Read at75b6d6a048: no shipped composition reaches it.ObjectQLPluginconstruction underpackages/andexamples/keeps the built-in protocol:cliserve.ts,runtimestandalone-stack.ts,plugin-dev, theverifyharness and theobjectqlkernel-factory.ts.registerProtocol: falseappears only in tests, plus one test-fixture string incli.examples/embed-objectqlboots no kernel and no dispatcher.registerProtocol: falsewith noMetadataProtocolPlugin.plugin.step2.test.tspins that it boots protocol-free.On a real engine (ObjectQL plus
InMemoryDriver, a member context, no protocol service), measured with a one-off test file that was deleted afterwards:apiEnabled: falsetargetqueryfallbackqueryfallback404 OBJECT_API_DISABLED,where.hiddenaggregateaggregate404 OBJECT_API_DISABLEDA
get-only target under$oranswered 1 row with the guard removed, and405 OBJECT_API_METHOD_NOT_ALLOWEDwith this PR.Tests (HEAD
bd79104296)The new file
packages/runtime/src/action-execution-calldata-relation-target-exposure.test.tshas 13 cases:queryfallback, each at the top level and under$or, assertingcode,status,object,paramandallowed, and thatfindis never called;aggregate, asserting thataggregateis never called;listwhitelist, noenableblock) reach the engine withwhereunchanged on both arms;findDatagets the query verbatim and the engine is not called here.Ablation, each leg through
node scripts/ablation-replace.mjs, which wraps the test command and restores afterwards. Each leg reported the anchor count going from 1 to 0, a blob change, and a restore with the blob equal to HEAD and an emptygit diff HEAD:query fallbackrefusal cases went red. The other 23 cases in the run stayed green: this file's 9 others (theaggregatepins, the controls and the protocol pin) and the 14 cases ofaction-execution-calldata-query.test.ts.aggregatecall was replaced. The 4aggregate armcases went red; the other 23 stayed green.bd79104296, with the blob at HEAD079f932376ebbefore and after. The first measurement, before the single-bypass refactor, gave the same split.src(an in-package relative import), so nodistwas involved.Package runs:
pnpm --filter @objectstack/runtime typecheck: exit 0.@objectstack/runtimelocal project: 353 files, 5093 passed, 19 skipped, exit 0.@objectstack/runtimerepo project: 3 files, 751 passed.@objectstack/corecensus pinsecond-object-read-exposure.pin.test.ts: 4 passed.relation-condition-sites.test.ts: 2 passed.@objectstack/specerror-code-ledger.test.ts: 21 passed.@objectstack/spec typecheckand@objectstack/core typecheck: exit 0.Census row 10. The pin's
CALLERStable is keyed on files that callreferenceTargetOf, andcallDatadoes not call it. So row 10 is named in the docblock, as rows 8 and 9 are, and therelation-condition-sites.tsrow now names all three doors that call the walk. The pin is green.Gates
All 90 commands were run after the last commit, at HEAD
bd79104296. That is the list re-derived on the real diff: the dispatch list's 66 plus 24specand repo gates.dispatch-gates --ranreports "90 derived, 90 run, 0 NOT-MEASURED", derived from the recorded exit codes. Per-command codes are in the report on the card.check-empty-changesetexits 1, red by design (below).check:error-code-provenance,check:dispatcher-error-vocabulary,check:error-status-conformance,check:system-context-censusandcheck:nul-bytes.check:dual-build-cjs-loadsansweredPREREQUISITE NOT MET(exit 3) on the first pass, before every package'sdistexisted. It was green on the final pass.Deliberate correction:
check-empty-changesetis red by designThe second commit,
9e9aa0b7af, rewords one parenthetical in the pending.changeset/22739-import-reference-target-exposure.md("Bindings", first bullet).runImporta protocol withoutgetMetaItem, so its reference cells skip the target's exposure ask (census row 9 of #22661) #22800 (PR fix(plugin-auth): the identity import asks each lookup target its declared exposure before matching a cell (#22800) #22866,f9ebb7dd31) made that example false: the identity import's run protocol carries the reader.runImporta protocol withoutgetMetaItem, so its reference cells skip the target's exposure ask (census row 9 of #22661) #22800)". No other line of that file moves.This edits a pending note this PR did not add, so
check-empty-changesetstays red as a DELIBERATE CORRECTION. The seat confirms it with a same-head contract record.Acceptance notes
relationConditionSites(packages/core/src/utils/relation-condition-sites.ts) still names two doors and says it lives in core "because the two doors share no other package". A third door now calls it. Comment only; it does not affect behaviour. It is noted here rather than edited.callDatareads the metadata service only. The new target ask reads the engine registry first, then the metadata service, which is the schema the engine's lowering reads and the read the write mouths in this file already take. In a composition whose metadata slot does not carry the engine's objects, the addressed gate falls open while the target ask still bites. Not measured on a real composition.callData's existing single bypass decision, and the bridge does the same.origin/main.git merge-treeagainst72b26ed4f5..40bc51f3f9is clean. The only shared file is the error-code ledger, where feat(metadata-protocol,spec): importUnmanagedPackage, the unmanaged install mode's copy-once import (ADR-0131 D6, stage S1) #22879 edited themetadata-protocolblock and this PR edits theruntimeblock.Generated by Claude Code