Skip to content

fix(service-automation): a flow's get_record node serves the stored-metadata family the way the data door does (#21519) - #21621

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21519-flow-read-node-projection
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21519-flow-read-node-projection

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21519
Clause-②: no

What this changes

A flow's get_record node 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' and runAs: 'user') and on both node branches (one row through findOne, a row list through find when limit is above 1).

Triage's route A, as ruled on the card:

  • @objectstack/service-automation takes a dependency on @objectstack/metadata-protocol.
  • The node consumes the door functions that metadata-protocol exports: storedMetadataBodyProjection, redactStoredMetadataRows, serveStoredMetadataHashColumnRows and ephemeralStoredHashDigest.
  • No copy of the serve, no @objectstack/spec edit, no new kernel service, no metadata-protocol source edit.

Files: crud-nodes.ts, its new pin file, package.json and pnpm-lock.yaml (the new dependency; the lockfile was regenerated by pnpm 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 new serveFamilyRead wraps the node's two engine reads:

  • A read of any other object runs and returns exactly as before, by reference.
  • A family read is judged by the family's own predicate, isStoredMetadataBodyObject from @objectstack/spec/kernel, the one the door functions use. No second list of family objects is kept.
  • A family read gets the door's projection. When a fields projection 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. storedHashDigestOf reads the data engine's getKeyedDigest accessor 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-scoped ephemeralStoredHashDigest. 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 an ObjectQLPlugin composition, the data service the node uses is the engine the door wraps, because the plugin registers one instance as both objectql and data. 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_record reads 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-sql on better-sqlite3 in memory, and AutomationServicePlugin. 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.

  • Data door control: both tables are served with the body projected and the hash keyed, unchanged.
  • Four identity and branch cases (system or user, findOne or find): the run output and the written record carry no stored credential and no stored hash. The served body equals the door's projection and keeps the row's non-credential configuration. The served hash equals the door's keyed hash.
  • History table: projected and keyed the same way, with the parent hash kept null.
  • Body-only projection: a projection naming the body without the type column is served projected, with exactly the columns named.
  • Crypto provider registered on the engine: the node serves the provider's keyed digest, which equals the door's.
  • Ordinary object control: read and copied exactly as stored.

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.

  • Mutation leg. scripts/ablation-replace.mjs replaced the family gate in serveFamilyRead with 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 from HEAD on exit. The subject is reached through the test's relative source import, not through dist/, 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.
  • Restore leg. The tool reported that the blob equals HEAD and that git diff HEAD is empty. The trap confirmed the blob again, and git status --porcelain was 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 under scripts/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-typecheck is OK with 0 files in the ledger.
  • metadata-protocol family door tests, read-only consumers (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.
  • The built dist of 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 at feac30ac58. All 75 were run at that head, and each exited 0. --ran reconciled 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.
  • On the previous head, check:test-source-alias was red: the new pin file imported @objectstack/metadata-protocol through dist/. The prescribed fix is in this PR: one anchored alias to source in packages/services/service-automation/vitest.config.ts. After that, the suite and the pins were run again at feac30ac58.
  • On the previous head, check:dual-build-cjs-loads first answered PREREQUISITE NOT MET because the tree was not built, so that run measured nothing. At feac30ac58, with the tree built, it measured 106 entries across 66 packages, and all of them load.

Lint (a proven narrowing; pnpm lint itself is CI's):

  • Population, from eslint's own config: 3 of the 6 changed paths are linted (crud-nodes.ts, the pin file and vitest.config.ts). The changeset, package.json and the lockfile answer "no matching configuration".
  • Count, from --format json at feac30ac58: 0 errors and 0 warnings on the 3 linted files.
  • Invariance: eslint.config.mjs enables no type-aware linting and no cross-file import rule (no-restricted-imports is per-file), so this diff cannot move the verdict on an untouched file.

Dependency closure. The closure of @objectstack/metadata-protocol was 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-cycles and check:turbo-task-graph are green).

Slim compositions. Every production composition in this repository that runs flows already loaded metadata-protocol through ObjectQLPlugin: @objectstack/cli depends on it directly, and @objectstack/verify reaches it through @objectstack/objectql and @objectstack/runtime. The engine-only @objectstack/objectql/core entry 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 own LiteKernel suites 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/** (outside releases/) and skills/** 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)

  • The digest selector (the engine's keyed digest, else the ephemeral key) now exists in three places: the door's private method, the runtime reader seam and this node. They agree today and are pinned by hash equality. Consolidating them would need a metadata-protocol export, which is outside this claim. Carrier: none.
  • The family enumeration pin in metadata-protocol (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.
  • Not measured, read-only inference: a record-change flow bound to a family table would receive the stored row as its trigger record. The record-change trigger does not consult the family predicate.
  • Not measured: a host that loaded metadata-protocol through both its ESM and its CJS build would hold two ephemeral key instances. The runtime reader seam carries the same hazard.

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

claude added 4 commits October 3, 2026 18:47
…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>
@github-actions github-actions Bot added the size/m label Oct 3, 2026
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-automation, touching 4 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/services/service-automation/package.json, packages/services/service-automation/vitest.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/system-context.mdx (via registerCrudNodes (symbol, a top-level function))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/services/service-automation/package.json, packages/services/service-automation/vitest.config.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 045b946256d988653fdca185c7fd33d6d86bd78d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from da51e97ee687be5a88b740db2cd899f0b6c9a61c — the merge of head feac30ac588cbca3d25f86e649f337b2d7dd271b into base 045b946256d988653fdca185c7fd33d6d86bd78d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 045b946256d988653fdca185c7fd33d6d86bd78d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 20:18
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 20:18
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit a4f0cb0 Oct 3, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21519-flow-read-node-projection branch October 3, 2026 20:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security(automation): the flow record-read node serves the stored-metadata family unprojected — consume PR #21513's door exports (#21454 item A4)

2 participants