fix(runtime): a sandboxed body or an action handler reading the stored-metadata tables is served the data door's form (#21454) - #21513
Conversation
…-metadata-body family (wip) Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-authored-by: Claude <noreply@anthropic.com>
…condition, refusal codes (wip) Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-authored-by: Claude <noreply@anthropic.com>
…control and both contexts (wip) Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-authored-by: Claude <noreply@anthropic.com>
…rve for the in-process reader contexts Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-authored-by: Claude <noreply@anthropic.com>
…text seams Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-authored-by: Claude <noreply@anthropic.com>
… the test typecheck Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-authored-by: Claude <noreply@anthropic.com>
…e engine predicate, pinned in the double ledger Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-authored-by: Claude <noreply@anthropic.com>
… its contract type Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 14 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 — 31 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 ef064c15277ee33057fc686f55fe9bef78eaffd9 && git checkout ef064c15277ee33057fc686f55fe9bef78eaffd9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f9a8eb889ead684cb3f35f4ee013e634513ad1af c69c33396dc50ea173464a5d09cc4e7f731b8d9c && git checkout -B drift-repro f9a8eb889ead684cb3f35f4ee013e634513ad1af && git merge --no-ff c69c33396dc50ea173464a5d09cc4e7f731b8d9c
node scripts/docs-audit/affected-docs.mjs --json f9a8eb889ead684cb3f35f4ee013e634513ad1af
|
…inor under Clause-② yes Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #21454 (body and all 11 comments, rulings ① Derived judgments
② Semver level
③ Boundary flagsDev report
Escalated, unanswered on the card:
Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #21454
Clause-②: yes
The stored-metadata-body family serves a stored metadata body only as its type's read projection, and the stored content hash over it only in keyed form, at every door. Step 1 of this card measured three in-process reader contexts serving the two stored-metadata tables as stored, body and hash alike. The action contexts run elevated, so a member who invokes the action receives the same answer as an administrator. Triage ruled disposition A: apply the family's projection and keyed serve at the reader-context seams, consume the shared functions, and leave the engine alone. This PR does that for every context the measurement found. It is
Part of, notFixes: the flow reader that triage folded into this card was measured and is reached, but its fix lies outside this claim's surface (see "Not closed by this PR").Disclosure discipline: classes, doors, roles, codes and forms only.
How
packages/runtime/src/stored-metadata-reader-seam.ts(new). One serve, built only from the generic data door's own functions:@objectstack/spec/kernel;serveStoredMetadataReadserves one read.serveStoredMetadataReadsThroughserves a scoped API:find,findOneandaggregateon a family object, through every context the API can derive (sudo(),withRunAs(...), atransaction(...)callback's context and the contextbeginTransaction()returns). It is idempotent, so a hash is never keyed twice. Writes,countand every other object pass through unchanged.sandbox/body-runner.ts,buildSandboxApi. Both body faces (hook and action) get theirctx.apihere, every fallback included. That API is now served through the seam, so a hook body that copies what it read can only copy the projected form.action-execution.ts. Two call sites are served through the seam:buildActionApi, thectx.apian action body and a host code handler both receive, andbuildActionEngineFacade'sfind. Both action doors (REST/actionsand MCPrun_action) build these two, so both doors are covered.packages/metadata-protocol/src/index.ts. Exports the data door's four stored-row serve functions and the digest type, so the runtime consumes them rather than copying them. The exports are additive and change no behaviour.scripts/engine-double-contract.pinned.json. One new pinned row: the seam unit test's scoped-API double routesfindOnethrough the engine's predicate. Written by the gate's own--write.A declared in-place widening. The handler's
ctx.api(③ in the table below) was not one of the two contexts the ruling named. It is the same scoped context object an action body receives (buildActionApi), and it was measured reaching (administrator and member: stored body, stored hash). The fix is the same call, in a file this claim already holds. All four in-place conditions hold, so it lands here and the pins cover it.Readings: before and after
ctx.api.object(...)ctx.api.transaction(...)PERMISSION_DENIED/metactx.engine.findctx.api.object(...)PERMISSION_DENIEDFor every row marked "after", the pins assert four things:
Tests
packages/runtime/src/stored-metadata-reader-contexts.pin.test.tsis step 1's probe with its context cases flipped from RECORD to ASSERT. It boots thebootStackplugin set, in order, once inbeforeAll(never inside a case) and runs 15 cases.packages/runtime/src/stored-metadata-reader-seam.test.tsholds 8 unit cases for the routes around the seam that the composition does not exercise one by one. Both files are green at532c96d70f(15/15, 8/8).532c96d70f,pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2answered: Test Files 311 passed (311), Tests 4393 passed, 19 skipped.--project repoat265f1c1f32: Test Files 3 passed, Tests 751 passed.265f1c1f32,pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2answered: Test Files 205 passed, 3 skipped; Tests 3092 passed, 19 skipped. After that sha the package's only change came in through the main merge, and it did not touch this package.532c96d70f,pnpm --filter @objectstack/runtime typecheck(tsc --noEmitpluscheck:test-typecheck, OK, ledger unchanged) andpnpm --filter @objectstack/metadata-protocol typecheckwere both green.@objectstack/runtimeload, and both builds of@objectstack/metadata-protocolexport the four functions.Reverse verification (forms only)
The fix was committed first, at
265f1c1f32. Then:ctx.api, the engine facade'sfind) were replaced throughscripts/ablation-replace.mjs. Each anchor went from 1 to 0 and each blob changed. The runtime was rebuilt, andscripts/ablation-dist-preflight.mjsfound the mutation marker in both dist bundles. The pins then answered 10 failed, 13 passed. Every context case failed with "the stored credential reached the answer". The precondition, the three controls, the member hook refusal and the 8 seam unit cases stayed green.git checkout HEAD --on the two paths. Both blobs equal HEAD,git diff HEADis empty and the porcelain is clean. After a rebuild, the preflight with--absentwas clean and the pins answered 23/23 green.buildActionApi's malformed-envelope fallback is the one seam call no case exercises, so it was not mutated.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackwas derived with no paths at532c96d70fand gave 72 lines. All 72 were run at that head, each exited 0, and--ranreconciled them: 72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN.check:dual-build-cjs-loadsanswered PREREQUISITE NOT MET on an earlier head. It was re-run once the tree's packages were built, and it measured 106 entry points across 66 packages.check:engine-double-contractasked for the new pinned row above.check:slot-lookupasked for the pins' handler plugin to read the engine slot through its contract type. Both are fixed in this PR.pnpm lint(the repo-wideeslint .) was not run locally; CI runs it.--format jsonat532c96d70f: 6 files, 0 errors, 0 warnings.eslint.config.mjsenables no type-aware linting (noparserOptions.project, no typed rules) and carries no cross-file import rules. Its custom plugins and baselines are per file, so this diff cannot move the verdict on any untouched file.Not closed by this PR (measured; reported on the card for routing)
service-automationtier, on an in-process engine with the automation service, with the row seeded in its stored form. No security plugin was loaded, so the run identities are not role-gated there.@objectstack/service-automation, which does not depend on@objectstack/metadata-protocol. Consuming it there needs either that dependency edge or the keyed serve moved beside the body primitives in@objectstack/spec/kernel. Both are outside this claim's surface, and the second ispackages/spec.count, answers whether a guess matches (measured, member invoking). The data door refuses these shapes. The ruled disposition changes only what a read serves, and so does this PR.OBJECT_NOT_FOUND), and the in-process verbs do not.ctx.api, and this PR does not project it, because a hook's input is also its write-back channel.Acceptance notes
ScopedRepo.execute()hands a code handler a raw scoped context that no seam wraps. Its own census found zero callers. That is engine lane: noted, not filed..changeset/21454-reader-context-family-serve.md, patch for@objectstack/runtimeand@objectstack/metadata-protocol.Generated by Claude Code