fix(service-automation): a flow's get_record node serves the stored-metadata family the way the data door does (#21519) - #21621
Conversation
…or the stored-metadata family serve Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…etadata family the way the data door does A get_record read of the stored-metadata tables now projects the stored body and keys the content hash through the data door's own functions from @objectstack/metadata-protocol, under the data engine's crypto provider or the process-scoped ephemeral key, so the run output and any record the flow writes from it carry the door's form under either run identity. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…ead node's family serve Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…rce for the package's tests check:test-source-alias refuses a new unaliased artifact import; the family-serve pins and their data door control are judged against the source in the checkout. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 6 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 da51e97ee687be5a88b740db2cd899f0b6c9a61c && git checkout da51e97ee687be5a88b740db2cd899f0b6c9a61c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 045b946256d988653fdca185c7fd33d6d86bd78d feac30ac588cbca3d25f86e649f337b2d7dd271b && git checkout -B drift-repro 045b946256d988653fdca185c7fd33d6d86bd78d && git merge --no-ff feac30ac588cbca3d25f86e649f337b2d7dd271b
node scripts/docs-audit/affected-docs.mjs --json 045b946256d988653fdca185c7fd33d6d86bd78d
|
Fixes #21519
Clause-②: no
What this changes
A flow's
get_recordnode now serves the stored-metadata family (the current metadata table and its version history: the stored body column and the stored content-hash columns) the way the generic data door serves it. The body is served as its type's read projection, and the hash is served in keyed form under the door's own key. This holds under both run identities (runAs: 'system'andrunAs: 'user') and on both node branches (one row throughfindOne, a row list throughfindwhenlimitis above 1).Triage's route A, as ruled on the card:
@objectstack/service-automationtakes a dependency on@objectstack/metadata-protocol.storedMetadataBodyProjection,redactStoredMetadataRows,serveStoredMetadataHashColumnRowsandephemeralStoredHashDigest.@objectstack/specedit, no new kernel service, no metadata-protocol source edit.Files:
crud-nodes.ts, its new pin file,package.jsonandpnpm-lock.yaml(the new dependency; the lockfile was regenerated bypnpm install),vitest.config.ts(one source alias, see Tests and gates), and the changeset.Position. The change is in
packages/services/service-automation/src/builtin/crud-nodes.ts. A newserveFamilyReadwraps the node's two engine reads:isStoredMetadataBodyObjectfrom@objectstack/spec/kernel, the one the door functions use. No second list of family objects is kept.fieldsprojection names the body without the type column, the type is read beside it and dropped again. The answer is then redacted and served keyed.One key.
storedHashDigestOfreads the data engine'sgetKeyedDigestaccessor when the node runs. That is the registered crypto provider's keyed digest. While no provider is registered, it falls back to metadata-protocol's process-scopedephemeralStoredHashDigest. These are the same two sources, in the same order, that the data door reads (ObjectStackProtocolImplementation.storedHashDigest) and that the runtime reader seam reads. In anObjectQLPlugincomposition, thedataservice the node uses is the engine the door wraps, because the plugin registers one instance as bothobjectqlanddata. The pins below assert, for each case, that the hash the node serves equals the hash the door serves for the same row.Measured first, on the base, by class only
On the base tree, with the dependency edge added and the node unchanged, a flow whose
get_recordreads a family row put both the stored credential and the stored content hash into two places: the run's declared output, and an ordinary record the same flow wrote from what it read. That held under both identities and on both branches, 4 of 4 combinations for each of the two exits. The new pin file failed 7 of its 8 cases at the time (the data door control case was added after this run).After the fix, the same measurement reports no stored credential and no stored hash in either exit, in all 4 combinations.
Pins
New file:
packages/services/service-automation/src/builtin/get-record-stored-metadata-family.integration.test.ts. It boots the real stack the package's other integration tests use:ObjectKernel,ObjectQLPlugin,driver-sqlon better-sqlite3 in memory, andAutomationServicePlugin. The stored row is written with its canonical hash, the shape the save door stores. Every case is judged against the data door's own answer for the same row (findData), not against a fixed shape.Reverse verification (ablation)
The fix was committed first. The ablation was run at the fix's head and again at
feac30ac58, after the source alias, with the same readings each time. The predicted direction was red for the 7 node cases and green for both controls.scripts/ablation-replace.mjsreplaced the family gate inserveFamilyReadwith a plain read, which removes the serve call. The anchor went 1 to 0, the marker went 0 to 1, and the blob changed. A trap also restored fromHEADon exit. The subject is reached through the test's relative source import, not throughdist/, so no rebuild or dist preflight was needed. Result: 7 failed, 2 passed. Both controls, the data door and the ordinary object, stayed green. The direction matched the prediction.git diff HEADis empty. The trap confirmed the blob again, andgit status --porcelainwas empty. The marker count went back to 0 and the anchor count to 1. The re-run gave 9 passed (9).Tests and gates
Every reading below was taken at
feac30ac58, the head this PR was opened at. Each heavy command ran underscripts/pm/os-verify-lock.sh.pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2: Test Files 167 passed (167), Tests 2062 passed (2062).pnpm --filter @objectstack/service-automation typecheck: exit 0.check:test-typecheckis OK with 0 files in the ledger.protocol.data-door-stored-content-hash,protocol.data-door-stored-metadata-redaction,stored-metadata-body-family.pin,protocol.served-content-hash): 4 files, 67 tests passed.distof service-automation loads under both ESM import and CJS require.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no paths) derived 75 commands atfeac30ac58. All 75 were run at that head, and each exited 0.--ranreconciled them: 75 derived, 75 run, 0 NOT-MEASURED, 0 UNRUN. The derivation has no runnable dogfood or boot gate: the dogfood shard-attestation families take their values from the workflow, so they are NOT MEASURED locally and belong to CI.check:test-source-aliaswas red: the new pin file imported@objectstack/metadata-protocolthroughdist/. The prescribed fix is in this PR: one anchored alias to source inpackages/services/service-automation/vitest.config.ts. After that, the suite and the pins were run again atfeac30ac58.check:dual-build-cjs-loadsfirst answered PREREQUISITE NOT MET because the tree was not built, so that run measured nothing. Atfeac30ac58, with the tree built, it measured 106 entries across 66 packages, and all of them load.Lint (a proven narrowing;
pnpm lintitself is CI's):crud-nodes.ts, the pin file andvitest.config.ts). The changeset,package.jsonand the lockfile answer "no matching configuration".--format jsonatfeac30ac58: 0 errors and 0 warnings on the 3 linted files.eslint.config.mjsenables no type-aware linting and no cross-file import rule (no-restricted-importsis per-file), so this diff cannot move the verdict on an untouched file.Dependency closure. The closure of
@objectstack/metadata-protocolwas measured from the workspace manifests: production plus peer plus optional dependencies give 9 packages, and adding dev dependencies gives 13. Neither contains@objectstack/service-automation, so the new edge closes no cycle (check:workspace-manifest-cyclesandcheck:turbo-task-graphare green).Slim compositions. Every production composition in this repository that runs flows already loaded metadata-protocol through
ObjectQLPlugin:@objectstack/clidepends on it directly, and@objectstack/verifyreaches it through@objectstack/objectqland@objectstack/runtime. The engine-only@objectstack/objectql/coreentry has no kernel, so it has no flow runtime that this edge could join. A kernel composition that runs flows without the metadata protocol plugin still boots: this package's ownLiteKernelsuites with a fake data engine do exactly that, and they are in the 167 green files above. They load the module, and the node uses only its pure functions and its process key.Docs
content/docs/**(outsidereleases/) andskills/**were searched for the flow record-read node and for stored-metadata reads. No sentence states the node's served form, so none is made false and no doc edit is needed.Acceptance notes (observations, not filed)
stored-metadata-body-family.pin.test.ts, its surface list) names neither the runtime reader contexts nor this node. It is outside this claim's file surface (no metadata-protocol edit). Carrier: none.Two same-family exits at this node's neighbours were measured and are reported to the seat in the report comment on the card. They are not changed here: the node's evaluate shapes, and the write nodes aimed at a family table.
Generated by Claude Code