Skip to content

[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

Closed

Description

@objectstack-fleet

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.

⚠️ Security card: category and position only. No request, field path or value is written here. The filing seat keeps the detail out of GitHub and gives it to the maintainer directly.

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)

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

  1. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p1 · security · target:v18 · area:access · domain:engine · pm:queue · measurement first · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-18T11:54Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.

    ⚠️ This comment adds no request, path or value. It keeps to the card's own disclosure line.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 6 · 2026-10-18T12:12Z · after triage 6249611671 graded this card p1 security target: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 as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24316-formula-hop-family-refusal
    Worktree: objectstack-issue-24316
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface:


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T12:15Z: mode:cloud. The dev session is session_01DAEUSSMYBHAthcxTvba4yT. It builds at the default tier on branch claude/issue-24316-formula-hop-family-refusal, claim 6249775816.

    ⚠️ This comment keeps to the card's disclosure line: category and position only.

    • Carried: triage 6249611671 (p1 security target:v18, measurement first) and the card's direction (refuse at the shared hop and at the lint's one-hop rule; ⛔ no second projection).
    • Measured first: the reach, over HTTP on a live backend. Per reader role, and per producer door, it records whether a family value is served. The report states it as yes or no, with counts.
    • Then the fix: one guard at the engine's one-hop read, so every exit is covered at once, plus a loud author-time refusal in the lint's field-formula pass. The family's enumeration pin gains the hop rows.
    • Cross-lane: the lint pass, declared to domain:spec as 6249778981.
    • Delivery: a draft PR saying Fixes #24316, then an os-dev-report comment here. Both keep to the disclosure line.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  5. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24344 comment 6251597603 · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T15:24Z.

    ⚠️ This comment keeps to the card's disclosure line: category and position only.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T15:48Z

    ⚠️ This comment keeps to the card's disclosure line: category and position only.

    Probe Reading
    Review CONTRACT_REVIEW_TIER record 6251578116, PASS, on the narrowing face, at head 7946ad1a22. ACCEPT 6251597603. Not governed (0 of 7 paths).
    Queue It entered the merge queue at 2026-10-18T15:21Z and left it merged at 2026-10-18T15:46Z. No gh-readonly-queue/main/pr-24344-* branch remains.
    origin/main 69147abf25 is on origin/main (git merge-base --is-ancestor: exit 0). It carries PR #24344's 7 files, +461 / −2.
    Card Closed completed through Fixes #24316; pm:dispatched is cleared.

    What main now holds:

    • The shared one-hop loader in objectql never reads a stored-metadata-body table, under either authority. A formula reads null with its reason, and a validation rule has no verdict, so the write is refused.
    • The lint's field-formula pass and hydrated validation conditions refuse the same hop, with a prescription. os validate, os build and the object save door run that pass.
    • The family's enumeration pin gains three rows (23 → 26).

    Release: triage 6249611671's lean was that the next.4 cut 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 session session_01DAEUSSMYBHAthcxTvba4yT is archived.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:enginepriority:p1High: required for production / M2securitytarget:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions