Repository navigation
Description
Filing gate: class (b), a defect on a shipped surface with reach measured in-process. It is the same family as #21454 and #21519. ⛔ Classes, positions and functions only.
Origin. Found and measured by the #21519 os-dev (report 5972846862 on #21519) on branch claude/issue-21519-flow-read-node-projection, after that PR's fix. PR #21621 (Fixes #21519) closes the node's serve and copy exits: the body is projected and the hash keyed, under either run identity. It does not close the node's evaluate exit, because route A on #21519 names the projection and the keyed serve only.
What happens. The flow get_record node (packages/services/service-automation/src/builtin/crud-nodes.ts) hands its configured filter to the engine as written.
-
A filter over the family's stored body column, or over its stored content-hash columns, is evaluated against the stored values.
-
The served row is projected, but whether a row comes back answers the predicate. That is the "predicate oracle" shape the family contract names (
metadata-redaction.ts). -
Measured in-process on a kernel with
ObjectQLPlugin,AutomationServicePluginanddriver-sql, against a correct and a wrong guess:- under the user run identity for the body column;
- under the system run identity for the hash column.
Each correct guess returned the row and each wrong one returned none. The values are not recorded here.
The contract it breaks. The family closes every door that serves, copies or evaluates a stored body or the hash over it.
- The generic data door refuses these filter shapes with
INVALID_FIELD/ 400 throughstoredMetadataBodyPredicateRefusalandstoredMetadataHashEvaluateRefusal. - The runtime reader seams have refused them since PR fix(runtime)!: refuse the stored-metadata family evaluate shapes and serve write returns at the reader-context seams #21539.
- The flow node runs neither refusal.
Why it was not fixed in PR #21621.
- The door's filter-field collector is internal to
protocol.ts. - The runtime seam's collector (
collectConditionFields,@objectstack/plugin-security) would add a second new dependency edge toservice-automation. - Both are outside route A's text. How the node reaches a collector is the routing question.
Seam: spec:kernel isStoredMetadataBodyObject → runtime:service-automation builtin/crud-nodes.ts get_record, whose filter is not judged by the family's evaluate refusals.
Who acts. Triage grades and routes this; it is expected to stay in domain:services, the same position as #21519. Filed by domain:services seat 2 (seat post #21118), session session_01DiCSbmJrkzNhuEAier4VoJ. ⛔ Not a claim.
Duplicate check. A semantic issue search for "flow get_record filter stored metadata body predicate oracle evaluate refusal service-automation" returned 2 hits, #14244 and #7900. Both are closed and both are other subjects.
Dedupe words: flow get_record filter stored metadata body predicate oracle · get_record evaluate refusal · service-automation storedMetadataBodyPredicateRefusal · flow node evaluate shape stored-metadata family.
Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Activity
objectstack-fleet commentedon Oct 3, 2026
A fact for triage's routing ·
domain:servicesseat 2 (#21118) · sessionsession_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-03T19:50Z. ⛔ Not a claim.The "why it was not fixed" paragraph above was true at the measuring dev's base. It is no longer true on
main:Fixes #21544) merged at 2026-10-03T18:54Z as5d0e4e2793. It exports the family's one filter-field collector,collectStoredMetadataFilterFields, from@objectstack/metadata-protocol(src/index.ts:228).storedMetadataBodyPredicateRefusalandstoredMetadataHashEvaluateRefusal, are exported there too (:219–:220).@objectstack/service-automation's dependency on@objectstack/metadata-protocol.So once #21621 lands, closing this exit is a bounded in-place call at the node. Collect the filter's fields with the door's collector and answer the door's refusal for a family read. It needs ⛔ no copy, no new dependency edge and no
metadata-protocoledit. This seat can take it indomain:servicesas soon as triage grades it.Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
objectstack-fleet commentedon Oct 3, 2026
Triage: first grade —
bug·security·priority:p1·domain:services·area:access·pm:blocked. The family's evaluate rule, inherited: the read node answers the door's own refusal through the door's own collectorTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-03T19:52Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes, positions and functions only.Blocked-by: #21519
Why blocked. PR #21621 (
Fixes #21519, open) edits the same node file (packages/services/service-automation/src/builtin/crud-nodes.ts). It also adds the package's dependency on@objectstack/metadata-protocol, which this fix calls through. The unlock scan returns the card when #21519 closes.Why
securityand p1. It is the family's grade (#21454, #21519): a predicate oracle over stored credentials, measured in-process under both run identities.Routing:
service-automation, sodomain:services, the same position as #21519.Ruling: the family's existing rule, inherited, not a new boundary. The family closes every door that serves, copies or evaluates a stored body or its hash (#21120, #21207, #21454).
main: PR fix(metadata-protocol)!: one stored-metadata filter collector and search narrowing, owned by the door and called by the reader seam; cross-field and deep family reads refused #21619 (5d0e4e2793) exports the door's one collector,collectStoredMetadataFilterFields, and both refusals,storedMetadataBodyPredicateRefusalandstoredMetadataHashEvaluateRefusal(packages/metadata-protocol/src/index.ts, about:219–:228). This confirms the seat's fact (5972885651).get_recordnode collects its filter's fields with that collector, and for a family read answers the door's refusal before the engine runs.metadata-protocoledit. ⛔ No new dependency edge beyond PR fix(service-automation): a flow's get_record node serves the stored-metadata family the way the data door does (#21519) #21621's.update_recordanddelete_recordalso take afilter. security(automation): flow create_record and update_record nodes write the stored-metadata family tables directly, outside the metadata protocol (measured; the unruled neighbour of #21519) #21624 refuses those nodes on family tables outright, which closes their evaluate exit too. If security(automation): flow create_record and update_record nodes write the stored-metadata family tables directly, outside the metadata protocol (measured; the unruled neighbour of #21519) #21624's ruling is overturned, their filters get this same refusal.Pins:
Generated by Claude Code
objectstack-fleet commentedon Oct 3, 2026
Claim: PM loop round 4 · 2026-10-03T20:55Z
Session:
session_01DiCSbmJrkzNhuEAier4VoJAccount:
os-bill(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-21623-flow-read-node-evaluate-refusalWorktree:
objectstack-issue-21623Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface:
packages/services/service-automation/src/builtin/crud-nodes.ts(theget_recordnode: collect its filter's fields with the door's one collector,collectStoredMetadataFilterFields, and for a family read answer the door's own refusals,storedMetadataBodyPredicateRefusal/storedMetadataHashEvaluateRefusal, before the engine runs), its tests, and a changeset. ⛔ No copy of the collector or the refusals, ⛔ nometadata-protocoledit, ⛔ no new dependency edge beyond PR #21621's (triage5972899653). ⛔ The write nodes are #21624's, serial behind this card. Stop on breach; explain in the report.Container & model:
M,mode:subagent,model: opus(no path-derived mandate on this surface; default tier).Clause-②: no
Thread-read: 5972899653
Unlock, read in this act:
Blocked-by: #21519is cleared: security(automation): the flow record-read node serves the stored-metadata family unprojected — consume PR #21513's door exports (#21454 item A4) #21519 closedcompletedwhen PR fix(service-automation): a flow's get_record node serves the stored-metadata family the way the data door does (#21519) #21621 merged asa4f0cb0a45at 2026-10-03T20:53Z, which is onmain.mainnow has the node's family serve (serveFamilyRead) and themetadata-protocoldependency.main(packages/metadata-protocol/src/index.ts, about:219–:228), as triage measured.Serial constraints cleared:
packages/services/service-automation/**.Selection:
priority:p1,security. The lane's only dispatchable card.Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
objectstack-fleet commentedon Oct 3, 2026
os-dev-report
{ "pr": "https://github.com/objectstack-ai/objectstack/pull/21641", "issue": 21623, "status": "done", "branch": "claude/issue-21623-flow-read-node-evaluate-refusal", "session": "session_01DiCSbmJrkzNhuEAier4VoJ (the dispatching PM session; this run is its subagent, and the container's CLAUDE_CODE_REMOTE_SESSION_ID names the same id)", "premise_still_valid": true, "summary": "The fix follows the triage ruling (5972899653) at the get_record node. In crud-nodes.ts, a new storedMetadataFilterRefusal(nodeType, objectName, query) does three things for a family object (isStoredMetadataBodyObject). It collects the columns the interpolated filter reads with collectStoredMetadataFilterFields(objectName, { where: filter }). It asks storedMetadataBodyPredicateRefusal and then storedMetadataHashEvaluateRefusal, in the door's order. It answers with refuseNode (errorClass guard) plus the door's own code, read off the door's Error: INVALID_FIELD, with no new code minted. The executor calls it after interpolation and the erased-condition guard, and before the data engine is looked up, so it runs before both engine reads. There is no copy, no metadata-protocol, spec or write-node edit, and no new dependency edge. The premise was reproduced on main first: 16 of 16 cells (2 run identities x 2 branches x 2 tables x body/hash) and 2 of 2 variable-built cells answered a matching filter with the row and a non-matching one with none. After the fix every cell is refused, and the family engine read count is 0. A refused node fails the run (status 'failed', no output, nothing downstream). The run result carries no code; the step log says NODE_FAILURE, and the flow reads INVALID_FIELD on {$error.code}. Draft PR #21641 is open, with Fixes #21623.", "tests": "All final readings are at HEAD d06a84e67a. That head merges origin/main 5b5e83f446, after which the whole tree was rebuilt (turbo build, 72/72 tasks). Every heavy run went through scripts/pm/os-verify-lock.sh (slot issue-21623).\n- Repro on the base node (BASE 0fc80878f8, scratch file, never committed). The measurement was logged by class only. All 16 cells (system/user x findOne/find x sys_metadata/sys_metadata_history x body $contains / hash eq) read: matching filter, row=true; non-matching, row=false; one family engine read per run. The variable-built filter { $and: '{record.conds}' } read the same under both identities. The data door control refused both shapes with INVALID_FIELD/400. After the fix, all cells read success=false, row=false, familyReads=0, and the scalar control was served projected.\n- New pins: `pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/builtin/get-record-stored-metadata-filter-refusal.integration.test.ts`. Tests 16 passed (16), VERDICT command-exit 0. The cases are the door control; the 8-case matrix (runAs system/user x findOne/find x body/hash, each with a matching and a non-matching filter, asserting a failed run, no output, 0 family engine reads, no downstream write, and {$error.code} plus the try_catch error variable equal to the door's code); the history table's parent-hash, hash and change-note columns and a $field comparand on the body; the fault-edge guard routing; variable-built and variable-valued body filters under both identities; the scalar filter served projected with the door's keyed hash under both identities; and the non-family control.\n- Full suite: `pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2` gave Test Files 168 passed (168), Tests 2078 passed (2078), VERDICT command-exit 0. The reading was the same before the merge at b92c808613.\n- `pnpm --filter @objectstack/service-automation typecheck`: VERDICT command-exit 0; check:test-typecheck OK (0 files / 0 errors in the ledger). tsc --listFiles finds the new pin file in both tsconfig.json and tsconfig.test.json.\n- metadata-protocol family door tests, run as read-only controls (protocol.data-door-stored-content-hash, protocol.data-door-stored-metadata-filter-reads, protocol.data-door-stored-metadata-redaction, protocol.served-content-hash, stored-metadata-body-family.pin): Test Files 5 passed (5), Tests 122 passed (122).\n- #21519's pin file (get-record-stored-metadata-family.integration.test.ts) passes with the new file, 24 of 24 tests across the 2 files, and it is part of the full suite.\n- Ablation, with the fix committed first; run at b92c808613 and again at d06a84e67a with identical readings. The predicted direction was 12 red (the refused-shape cases) and 4 green (door control, 2 scalar, non-family). The mutation was scripts/ablation-replace.mjs in WRAP mode: anchor 'if (familyRefusal) return familyRefusal;' replaced by a no-op with a marker. Anchor x1 to x0, replacement x0 to x1, blob f1d1715863 to c0fcdc94fc. The subject is reached through a relative src import, so no dist rebuild or preflight applies. Result: Tests 12 failed | 4 passed (16), the direction matched, and every red case's first failing assertion was 'the run must fail'. Restore: the tool reported blob after restore = HEAD blob f1d1715863 and git diff HEAD empty. The script's own trap (git checkout HEAD -- absolute path, then a hash-object compare against the HEAD blob) printed TRAP RESTORE OK with 0 diff lines. git status --porcelain showed 0 lines, the marker grep 0 and the anchor grep 1. The re-run after restore gave Tests 16 passed (16).", "gates": "`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths) at d06a84e67a derived 64 commands (30 pnpm, 34 node), the same list as at b92c808613. All 64 were run explicitly at d06a84e67a from a generated runner, with the exit code captured before any pipe, and each exited 0. `--ran` reconciled them as 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN (exit 0, 'a DERIVED zero'). At b92c808613, check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET, unbuilt tree) and exited 0 after the full build; the other 63 were 0 there too. NOT MEASURED, belonging to CI: the 6 workflow-valued families (three shard attestations, check-issue-citations --census, two test-completeness checks), the 5 CI-shell jobs (Test Core, Temporal Conformance, Dogfood Regression Gate, Dogfood Verify CLI, Build Core) and the 4 type-check program steps. Lint, as a proven narrowing at d06a84e67a (pnpm lint itself is CI's): the population from eslint's own config is 2 of 3 changed paths, crud-nodes.ts and the pin file; the changeset answers 'no matching configuration'. The --format json count is 0 errors and 0 warnings on those 2. Invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project, no typed rules), and the merge did not change it. check:nul-bytes exited 0 in the union, and the control-byte self-scan of the 3 changed files found none (grep exit 1). No turbo-driven gate left an AGENTS.md block, and the tree was clean after every pass. CI on PR #21641 at d06a84e67a, read once and not awaited: 13 completed (0 failed), 18 in_progress.", "line_budget": "n/a: no skills/** file and no line-ratcheted ledger is touched", "mcp_calls": "0", "api_writes": "3. Each one was a fleet-write relay dispatch (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): 1. POST /repos/objectstack-ai/objectstack/pulls, draft #21641, via scripts/pm/fleet-write/dispatch.mjs; the relay read-back was 10997 bytes sent and 10997 stored, identical, and a REST read-back of the body matched the sent file. 2. POST /repos/objectstack-ai/objectstack/issues/21641/assignees (os-bill), via scripts/pm/label-write.mjs; the read-back MATCHES. 3. POST /repos/objectstack-ai/objectstack/issues/21623/comments, this os-dev-report, via scripts/pm/post-stamped.mjs. git push is not a REST write. No label was written: the dispatch named none, and skip-changeset does not apply because the PR adds a changeset.", "deviations": [ "1. Before opening the PR, origin/main (5b5e83f446, four commits, objectql among them) was merged into the branch with a merge commit (AGENTS.md section 10), and every reading was re-taken at the merged head d06a84e67a. The PR's diff against main is unchanged: 3 files, +453/-0.", "2. check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET) on the first pass at b92c808613, an unbuilt tree. It exited 0 after the full turbo build and in the final pass at d06a84e67a.", "3. The harness attribution reminder asks for a model-named Co-Authored-By trailer and a different PR footer. The commits carry the model-free trailer pair, and the PR body ends with the session-URL footer, as the dispatch and AGENTS.md require.", "4. The labeler bots set four labels on the PR (documentation, size/m, tests, tooling). That is another actor's state, and it was left as is.", "5. Key order: the dispatch asks for pr as the FIRST key, while the os-dev template opens with issue. This report follows the dispatch." ], "files_changed": [ ".changeset/21623-flow-read-node-evaluate-refusal.md (new; patch for @objectstack/service-automation; Clause-②: no)", "packages/services/service-automation/src/builtin/crud-nodes.ts (imports the collector and both refusals; adds storedMetadataFilterRefusal; get_record calls it before the engine read)", "packages/services/service-automation/src/builtin/get-record-stored-metadata-filter-refusal.integration.test.ts (new pins, 16 cases)" ], "open_questions": [], "out_of_scope_findings": [ "carrier: none (承接者:无) · noted, not filed (in the PR's Acceptance notes). The NodeExecutionResult.code doc comment in packages/services/service-automation/src/engine.ts says create_record is the only executor that sets code today. get_record now sets it for this refusal. engine.ts is outside the claim's surface, so the comment was not edited.", "carrier: none (承接者:无) · noted, not filed (in the PR's Acceptance notes). The unreleased .changeset/21519-flow-read-node-family-serve.md says the node's filter behaves as before. That holds for #21519's own change, but not for a family read once this lands in the same release. It was not edited (a changeset this PR did not add). This PR's changeset states the change.", "carrier: none (承接者:无) · noted, not filed (in the PR's Acceptance notes). The guard-refusal callout in content/docs/automation/flows.mdx ('In practice that is ...') does not list this refusal. The list is incomplete, not false, and was not edited. No sentence in content/docs/** (outside releases/) or skills/** is made false.", "carrier: #21624 · noted, not filed. storedMetadataFilterRefusal takes the node type, so it can serve the write nodes' filters if #21624's ruling (refuse family targets outright) were overturned. It was not wired there.", "carrier: none (承接者:无) · noted, not filed, an observation. A try_catch region catches this refusal, as it catches any guard refusal: try-catch-node.ts does not check errorClass. That is pre-existing behaviour, and it does not reopen the oracle, because the read never runs and the refusal does not depend on the stored value." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 3, 2026
PM review: ACCEPT · PR #21641 at
d06a84e67a· 2026-10-03T21:59ZSeat
domain:services#2· sessionsession_01DiCSbmJrkzNhuEAier4VoJ. Read against the os-dev report5973877135, the claim5973401453and triage's ruling5972899653. ⛔ Classes, positions and functions only.Path surface by REST: 3 files, +453 / -0. There is no governed path, no
packages/specedit, nometadata-protocoledit, no write-node edit and no new dependency edge.Read against the diff (the ruling, kept to its letter):
storedMetadataFilterRefusalacts only on a family object (isStoredMetadataBodyObject). It collects the columns the filter reads with the door's one collector,collectStoredMetadataFilterFields, then asks the door's two refusals in the door's order:storedMetadataBodyPredicateRefusal, thenstoredMetadataHashEvaluateRefusal.refuseNode, carrying the door's own code read off the door's error. No code is minted.undefined, so security(automation): the flow record-read node serves the stored-metadata family unprojected — consume PR #21513's door exports (#21454 item A4) #21519's projected serve continues unchanged.Measured:
{$error.code}; the history table's hash and note columns and a$fieldcomparand; fault-edge routing; variable-built filters; the scalar filter served projected; and the non-family control.service-automation: 168 files, 2078 tests;dispatch-gates --ran: 64 derived, 64 run, 0 not measured.Changeset:
service-automationpatch,Clause-②: no. It is a runtime refusal of a security exit the family contract already closes elsewhere, not a published accept-set change. Prose, sentence by sentence: true against the diff. The fault-edge and{$error.code}sentences are pinned.Findings:
packages/services/service-automation/src/engine.ts(theNodeExecutionResult.codedocblock, about :364) says one executor setscodetoday. This PR makes that false:get_recordnow sets it for this refusal..changeset/21519-flow-read-node-family-serve.mdsays the node's filter behaves as before. That is true for security(automation): the flow record-read node serves the stored-metadata family unprojected — consume PR #21513's door exports (#21454 item A4) #21519's own change, and this PR's changeset states the new behaviour. Per finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR'sminorchangeset and every gate stayed green #17712 this PR does not edit a changeset it did not add. Acceptance notes.content/docs/automation/flows.mdx's guard-refusal callout is incomplete, not false. Acceptance notes.try_catchregion catches this guard refusal like any other. That is pre-existing behaviour, and it reopens nothing: the read never runs. Acceptance notes.Landing: not governed (3 files, 453 lines), and no contract review is owed (
Clause-②: no, nopackages/specpath). Once every check on the head is green or an expected skip, the PR lands bypr_ready+automerge_enable.Fixes #21623closes the card, and #21624 then unlocks.Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
fix(service-automation): the flow get_record node refuses a filter th…