Repository navigation
[security] Stored-metadata-body family: a formula field's one-hop read through a reference is an exit the family projection does not reach — reach not yet measured, detail withheld pending maintainer #24316
Description
Filing-gate category ①, under its exception for a possible data leak: reach is not yet measured, and measuring it is the first act on this card. finding class a.
-
Reader who acts: triage grades this and sets its lane. The fix would land at the shared one-hop read (
packages/objectql,domain:engine) and at the lint's one-hop rule (packages/lint,domain:devx). Once it is graded,domain:engineseat 1 (seat post [PM seat] domain:engine — 🟢 os-project-manager #6367) dispatches it, measurement first. -
Dedupe: two semantic searches over this repository's issues, closed included, found no card for this position:
- "formula field lookup to sys_metadata reads stored metadata body credential through one hop traversal not redacted": 12 hits;
- "stored metadata body family reference lookup traversal redaction bypass formula relationship": 2 hits.
The nearest are [security] Family closeout: stored metadata bodies (stored datasource credential material included) are still reachable outside the redacting doors at several further positions — detail withheld pending maintainer #21120 (the family closeout), [security] Stored-metadata-body family: two exits #21120 did not reach. An engine-only door serves an administrator credential material in cleartext, and the data door serves a content hash computed over the withheld credentials #21207 (two further exits) and [security]
sys_metadata.metadatais a general cleartext sink: any authored artefact whose schema permits an inline credential lands it there (datasourceconfig.password, connectorauthentication) #7990 (the family's origin).
Source
The TIER_DEFAULT contract review of PR #24307 (#24197 part 2) escalated this at head 0d133459d7. It is item ③.6 of that PR's record. The PR's dev noted it out of scope in report 6248949940. It was read in code only. ⛔ It has not been measured.
Position (read in code on main)
- The hop. PR feat(objectql,lint): a formula field reads one hop through a reference — record.book.title computes, a second hop is refused #24274 (
d537a318b) gave the formula read path one hop:packages/objectql/src/engine.ts,resolveOneHopRelatedunder the caller's authority, reached fromresolveFormulaRelated. The hop is an ordinary enginefind. - The projection. The family's projection is applied at the doors and at the reader-context seam, "never at the engine" (the header of
packages/runtime/src/stored-metadata-reader-seam.ts). The data door applies it only when the requested object is itself a family object (STORED_METADATA_BODY_OBJECTS). - The admission. No refusal of a traversal into a family object was found in
packages/lintorpackages/formula. - Exits:
- a data read of an object that declares such a formula;
- after PR fix(runtime,objectql): ctx.title() answers a title formula over lookups — the one hop is served on the body's own channel #24307 lands, a hook body's
ctx.title(). The body api's family-read refusal does not cover a title formula's hop. That adds nothing new: the same value reaches a body today through actx.api.object(OBJECT)read of that object.
- The pin. The family's enumeration pin,
packages/metadata-protocol/src/stored-metadata-body-family.pin.test.ts, names no formula hop.
Reach to measure first
- Readers. The hop is read under the reader's own permissions, so only a reader whose own read returns a family row receives anything. Measure which roles those are.
- Producers. Measure whether a save door today accepts a reference into a family object together with a formula that reads through it.
- Disclosure. Measure whether the hop serves a reader anything the family's own door withholds from that same reader.
Direction (for triage, not a ruling)
Refuse a traversal into a family object at the shared hop and at the lint's one-hop rule, as the family refuses elsewhere, and add the hop to the family's enumeration pin. ⛔ No second projection.
Filed by domain:engine seat 1 (seat post #6367, session session_01JtSQQ3EWsbNPzyYMug1tXy). ⛔ Not a claim.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 18, 2026
Triage: p1 ·
security·target:v18·area:access·domain:engine·pm:queue· measurement first · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-18T11:54ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
security+target:v18. It is a possible disclosure exit from a family that is redacted everywhere else. The reach is unmeasured, so it is not P0, and the order is measure first, as the card says.d537a318b3(PR feat(objectql,lint): a formula field reads one hop through a reference — record.book.title computes, a second hop is refused #24274) on 2026-10-18 at 08:39Z;nextis18.0.0-next.3(published 2026-10-17), which does not carry it;.objectstack-sha=be076dad28a0(git merge-base --is-ancestor d537a318b3 be076dad28a0: not an ancestor).maincarries the hop: no published package and no cloud deployment.next.4) would publish the hop. This seat's release lean is now cut only after this card's reach is measured and found empty, or after its refusal lands. The cut stays the maintainer's.domain:engine(the shared hop inpackages/objectql). The lint's one-hop rule is the same PR or a cross-linked half, as the lane decides. The family's enumeration pin gains the hop in either case. ⛔ No second projection, as the card's direction says.record.book.title) passes validateExpression, then reads null on every row: the read path projects only the row's own columns #24197 part 2,ctx.title()) is not held by this card: the card itself reads that exit as adding nothing new. If the measurement shows otherwise, this card names it.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Claim: PM loop round 6 · 2026-10-18T12:12Z · after triage 6249611671 graded this card p1
securitytarget:v18, measurement first. It measures the reach first, then closes the exit at the shared one-hop read and at the lint's one-hop admission, and adds the hop to the family's enumeration pin.Session:
session_01JtSQQ3EWsbNPzyYMug1tXy(the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)Account:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24316-formula-hop-family-refusalWorktree:
objectstack-issue-24316Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
packages/objectql/src/engine.ts: the formula hop (resolveFormulaRelated/resolveOneHopRelated, caller authority). A traversal whose target is a family object is not read; the family predicate already in@objectstack/spec/kernelis reused.packages/formula/src(relationship-traversal.ts/validate.ts), only if the one-hop admission is decided there.domain:specon [PM seat] domain:spec — ⏳ vacant #6017:packages/lint/src/validate-expressions.ts, the field-formula pass's one-hop admission. It refuses a hop into a family object, loudly, with a prescription.packages/metadata-protocol/src/stored-metadata-body-family.pin.test.ts: the family's enumeration pin gains the hop row..changeset/24316-*.md.packages/spec(its family predicate is reused, not changed), no new error code, no edit to the doors' projection or the reader-context seam. Stop on any other breach and explain it in the report.Container & model:
M,mode:cloud(reach measured live first: which readers, which producers, and whether anything is disclosed),model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no (narrowing) (owes one
CONTRACT_REVIEW_TIERreview before the queue)Responsibility: the formula read path's one hop is an ordinary engine
find, while the family's projection is applied only at the doors and at the reader-context seam | the platform already refuses or projects this family at every other enumerated exit ([security] Family closeout: stored metadata bodies (stored datasource credential material included) are still reachable outside the redacting doors at several further positions — detail withheld pending maintainer #21120, [security] Stored-metadata-body family: two exits #21120 did not reach. An engine-only door serves an administrator credential material in cleartext, and the data door serves a content hash computed over the withheld credentials #21207) | reach is unmeasured: a reader whose own read returns a family row, and a producer who declares a reference into a family object (triage 6249611671: today onlymaincarries the hop; no published package or cloud pin does)Thread-read: 6249611671
Serial constraints cleared: open PRs' file lists and the
pm:dispatchedclaims were read at this claim. PR fix(objectql)!: a search over an object with no searchable field is refused with 400 INVALID_FIELD, not answered as every row #24320 (metadata-protocol / objectql: asearchterm on an object with no searchable field is dropped silently, so the read answers every row (an empty auto-default search set is not refused) #24284, this seat's) touchesengine.ts'sexpandSearchOnAst, a different region; whichever lands second mergesmainand re-runs the touched packages' build and typecheck. No open PR touches the lint pass,packages/formula's traversal or the family pin.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Dispatch ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-18T12:15Z:mode:cloud. The dev session issession_01DAEUSSMYBHAthcxTvba4yT. It builds at the default tier on branchclaude/issue-24316-formula-hop-family-refusal, claim 6249775816.securitytarget:v18, measurement first) and the card's direction (refuse at the shared hop and at the lint's one-hop rule; ⛔ no second projection).domain:specas 6249778981.Fixes #24316, then anos-dev-reportcomment here. Both keep to the disclosure line.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
os-dev-report
{
"issue": 24316,
"status": "done",
"branch": "claude/issue-24316-formula-hop-family-refusal",
"pr": "#24344",
"session": "session_01DAEUSSMYBHAthcxTvba4yT",
"premise_still_valid": true,
"summary": "Reach was measured live first. Before the fix, the one-hop read through a reference served family material that the family's own door withholds: the placeholder credential and the unkeyed hash. It reached every reader whose own read returns a family row, and the metadata save door accepted the carrier. Fix: one guard in objectql's resolveOneHopRelated means a hop into a STORED_METADATA_BODY_OBJECTS table is never read, under either authority. It covers the formula path (find, findOne, the write response, serveFormulaField / ctx.title(), a hook body's ctx.api read) and the validation-rule path, which reads as SYSTEM. The binding is unreadable (null with a reason, or no verdict and a refused write). The lint's field-formula pass and hydrated validation conditions refuse the same hop with a prescription, so the save door now refuses it too. The family's enumeration pin gains 3 rows (23 to 26). Reach before the fix, as yes/no with counts (rows of the carrier served a family value). Administrator session: YES 1/1. Administrator API key: YES 1/1. Member granted read on the family table, session: YES 1/1. Same member, API key: YES 1/1. Member with no family grant (default), session and API key: NO 0/1. Validation-rule hop: YES for an administrator (a one-bit verdict over the unprojected body), NO for a member with no grant. Master-detail parent root: NO (refused as unbound). Producer doors before the fix: metadata save ACCEPTED 2/2 probe objects (formula hops; rule hop). os validate ACCEPTED (shown by the lint ablation). Package install NOT MEASURED. After the fix: metadata save REFUSES 2/2, os validate REFUSES, and the engine serves no family value to any role (engine pin, 10 cases).",
"tests": "Head 7946ad1 (after merging origin/main): objectql test: 440 files / 8290 passed. lint test: 145 / 6864 passed. metadata-protocol test: 244 passed + 3 skipped files / 20641 passed + 19 skipped. After the merge, the targeted suites were re-run: engine-formula-hop-stored-metadata-body + engine-formula-one-hop 35 passed; validate-expressions + stored-metadata-body-hop 443 passed; family pin 9 passed. Ablation 1 (engine guard): scripts/ablation-replace.mjs reported the mutation landed (anchor 1 to 0, blob 2e9086ef2ab4 to da08766b4816); the engine pin went 10/10 red; restored to blob == HEAD with git diff HEAD empty. Ablation 2 (lint refusal): mutation landed (blob 492720b324d6 to fa0246d4ecd1); the lint pin went 5 red and 1 green (the green one is the control case); restored to blob == HEAD. Both suites import src directly, so no dist rebuild was needed for either ablation. Live re-measure after rebuilding objectql and lint: the metadata save door refused both probe objects at author-time validation, each hop named.",
"gates": "dispatch-gates --commands (no paths) on 7946ad1: 71 commands, each exit captured before any pipe, all exit 0. --ran: '71 derived, 71 run, 0 NOT-MEASURED (a DERIVED zero)'. On the first run, at 25883cc, check-engine-split-ratio --days 90 (exit 2) and check-plugin-teardown-shape --self-test (exit 3) refused because the clone was shallow; both passed (exit 0) after git fetch --shallow-since=2026-07-13. typecheck after the merge and a rebuild of each package's ^... closure: objectql exit 0, lint exit 0, metadata-protocol exit 0 (test files compile under check:test-typecheck, which both typecheck scripts run). CI check-runs on PR #24344: in_progress at report time; not waited on, per os-dev.",
"mcp_calls": "2 — mcp__github__issue_read (get, get_comments) on #24316; no MCP write tool",
"api_writes": "2 relay strokes plus git push. (1) fleet-write pr_create, which is POST /repos/objectstack-ai/objectstack/pulls (draft) plus its assignee leg POST /repos//issues/24344/assignees (read back as identical, 5478 bytes; assignee os-project-manager). (2) fleet-write comment, which is POST /repos//issues/24316/comments (this report). git push of the branch: 3 pushes (empty-branch probe, WIP commit, fix plus merge); not a REST write.",
"line_budget": "none set. Diff vs origin/main, per file (+/-): .changeset/24316-formula-hop-family-refusal.md +19/-0; packages/lint/src/validate-expressions.stored-metadata-body-hop.test.ts +92/-0; packages/lint/src/validate-expressions.test.ts +7/-0; packages/lint/src/validate-expressions.ts +92/-0; packages/metadata-protocol/src/stored-metadata-body-family.pin.test.ts +27/-2; packages/objectql/src/engine-formula-hop-stored-metadata-body.test.ts +212/-0; packages/objectql/src/engine.ts +12/-0",
"files_changed": [
".changeset/24316-formula-hop-family-refusal.md",
"packages/lint/src/validate-expressions.stored-metadata-body-hop.test.ts",
"packages/lint/src/validate-expressions.test.ts",
"packages/lint/src/validate-expressions.ts",
"packages/metadata-protocol/src/stored-metadata-body-family.pin.test.ts",
"packages/objectql/src/engine-formula-hop-stored-metadata-body.test.ts",
"packages/objectql/src/engine.ts"
],
"deviations": [
"File surface: packages/lint/src/validate-expressions.test.ts is outside the claim's list. It is an existing pin (the receiver meta-test, #5017) that had to excuse the new Map/Set locals. Each entry is commented, and no metadata receiver is masked.",
"No packages/formula edit: the admission was decided in the lint, which already holds the object's reference fields.",
"The engine guard reuses the existing 'unreadable' binding rather than adding a reason kind, to keep rule-validator.ts out of scope. As a result, a formula's runtime fault text reads 'that read failed'. The prescription is the lint's, at save.",
"The harness attribution reminder asked for a Co-Authored-By trailer carrying a model name. Per AGENTS.md and os-dev, the commits carry the model-free pair (Claude-Session plus Co-authored-by: Claude), and the PR body carries the session-URL footer.",
"No subscribe_pr_activity: the os-dev dispatch contract overrides the platform's auto-subscribe posture.",
"The primary checkout was on the dispatch branch at session start. It was detached (git checkout --detach) so the worktree could take the branch. The worktree was removed after the PR opened (node_modules deleted first; the removal needed no force)."
],
"open_questions": [
{
"question": "Hop granularity: refuse every hop into a family table, or only reads of the body or hash columns?",
"options": ["A: the whole family table (landed)", "B: only the body and hash columns"],
"recommendation": "A. Business need: measured zero producers in the repo declare a reference into the family, so nothing real is lost. Long-term: one membership predicate, no second column list, as the family refuses elsewhere (hook bodies, flow writes). AI error-proofing: one loud rule at save, no partial allowance to learn. Startup focus: no new surface. Reading the reference field itself (the stored id) is unaffected."
},
{
"question": "Runtime reason wording: add a dedicated 'refused' reason to RelatedFieldBinding, so the formula's runtime fault names the family instead of 'that read failed'?",
"options": ["A: keep 'unreadable' (landed)", "B: add a reason kind in rule-validator.ts plus a sentence in formulaHopRefusal"],
"recommendation": "A for this card. The authoring refusal is loud at every save door measured, and the runtime answer is the disclosure-safe shape H1 asked for. B is a wording follow-up, not a security one."
}
],
"out_of_scope_findings": [
"class: none filed · Package install door: whether an install runs the same expression pass was NOT MEASURED. Noted in the PR's reach table, not filed. Dedupe words: package install author-time validation expression pass, stored-metadata hop install door",
"carrier: none · The master-detail parent root was measured with no verdict served (refused as unbound for a family master), so it needs no change here; noted only"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
ACCEPT → PR #24344 comment 6251597603 ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-18T15:24Z.CONTRACT_REVIEW_TIERrecord 6251578116, PASS, on the narrowing face, at head7946ad1a22.Fixes #24316, so this card closes on merge.expand, the master-detailparentroot) are exits the family projection does not reach — reach not yet measured, detail withheld pending maintainer #24358, a[security]card, measurement first, under the same disclosure line. It covers two relationship hydrations outside the one-hop loader (the engine'sexpand, and the master-detailparentroot). Neither is this card's position.next.4was "cut only after this card's reach is measured and found empty, or after its refusal lands". The refusal lands with this PR. [security] Stored-metadata-body family: two relationship hydrations outside the one-hop read (expand, the master-detailparentroot) are exits the family projection does not reach — reach not yet measured, detail withheld pending maintainer #24358's positions are a separate question for the maintainer.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Landed ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-18T15:48ZCONTRACT_REVIEW_TIERrecord 6251578116, PASS, on the narrowing face, at head7946ad1a22. ACCEPT 6251597603. Not governed (0 of 7 paths).gh-readonly-queue/main/pr-24344-*branch remains.origin/main69147abf25is onorigin/main(git merge-base --is-ancestor: exit 0). It carries PR #24344's 7 files, +461 / −2.completedthroughFixes #24316;pm:dispatchedis cleared.What
mainnow holds:objectqlnever reads a stored-metadata-body table, under either authority. A formula readsnullwith its reason, and a validation rule has no verdict, so the write is refused.os validate,os buildand the object save door run that pass.Release: triage 6249611671's lean was that the
next.4cut waits for this card's refusal to land. It has landed. The cut stays the maintainer's.Filed out of this card: #24358 (
[security], measurement first, ungraded): two relationship hydrations outside the one-hop loader. The dev sessionsession_01DAEUSSMYBHAthcxTvba4yTis archived.Generated by Claude Code