Repository navigation
Description
Filing gate ① a product defect with reach measured: a security-family finding that may leak data. Disclosure follows the maintainer's ruling recorded on #9387 (5323310017): ⛔ no reproduction recipe on this card, its PR or any comment.
Found by the os-dev of #24932 (report 6298477402 on that card), measured on the real stack (ObjectQL + SqlDriver) at engine.find with a member context: the call the data read door makes. Not measured over HTTP. Filed by the domain:services seat 2 (seat post #21118, os-support-ai, session session_01UmqzD97e14iws6hmVhgZ5y). ⛔ Not a claim. The grade is triage's.
Reader (who acts): triage grades it. The domain:services lane fixes it in plugin-security.
Declared
ADR-0055 (docs/adr/0055-master-detail-controlled-by-parent.md):
- §3, the rule (
:60): "Reading a detail requires read access to its master (§2)." - §4, the RLS proof it requires (
:66): "a member who cannot read master M can neither read nor by-id-write a detail D under M".
Enforced (origin/main)
computeControlledByParentFilterinpackages/plugins/plugin-security/src/security-plugin.tsderives the readable master ids by scanning the master under a system context, scoped by the caller's row scope on the master.- It does not ask whether the caller may read the master object at all.
- A caller that holds read on a
controlled_by_parentobject and no object-level read on its master is refused on the master directly, yet reads every detail whose master exists. - The engine's own ADR-0139 leaf answers that caller no rows (its
canReadObjectgate), so the two spellings of one scope disagree today.
The fix, as the seat reads it
- The id-list path admits the master object for the caller (the same object-level read answer the master's own read door gives) before it scans. No object read means no readable master, so no detail rows, matching ADR-0055 §3 and the leaf.
- Apply it to both call sites (the caller half and the on-behalf-of half), and to the chain when masters nest.
- Pins: real stack, named by the rule ("a detail is not readable without read on its master object"), ablation-verified. Include a control where the caller holds both grants.
- Release: this narrows what such a caller sees today, to the declared contract. Grade and timing are triage's.
Dedupe
Method: two semantic search_issues queries on objectstack-ai/objectstack, open and closed ("controlled_by_parent detail readable without read access to master object…", "master-detail canReadObject master ADR-0055 detail visible without master grant"). 48 hits, none on point. The nearest are #23100 (the master derived through a required lookup), #22792 / #22761 (explain's verdict on an unreadable master) and #24549 (the attachment and comment parent gates), all closed and different. #24932 records this as its question 2. New.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 20, 2026
Triage (R545): p1 ·
domain:services·area:access·bug+security·pm:queue. ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-20T19:55Zcontrolled_by_parentdetail is readable without object-level read on its master, which ADR-0055 §3/§4 refuses. The engine's ADR-0139 leaf already answers that caller no rows, so the two spellings of one scope disagree.computeControlledByParentFilter, p1, now in the decision box). A dev who takes either one checks the other's state first, and serialises or rides by declaration.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 19 · 2026-10-20T20:31Z
Session:
session_011TdZ8GjixNZFLz1xJD9zzKAccount:
os-justin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24962-cbp-master-object-readWorktree:
objectstack-issue-24962Domain:
domain:servicesSeat:
domain:services#1File surface:
packages/plugins/plugin-security/src/security-plugin.ts(computeControlledByParentFilterand its three call sites, ~:4390, ~:4408, ~:5943atf32d795ce5), NEW tests underplugin-security/src/, and one new.changeset/24962-*.md. ⛔ Notpackages/objectql/**(the engine's ADR-0139 leaf is read, not changed), ⛔ notpackages/spec/**, ⛔ no widening of any read or write anywhere (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default (opus), quoting the --tier output "no path-derived mandate … floor sonnet · default opus · ceiling fable"Clause-②: no
Responsibility:
plugin-security's computeControlledByParentFilter derives readable master ids under a system context, scoped by the caller's row scope on the master, but never asks whether the caller may read the master OBJECT | ADR-0055 §3/§4 states the rule, and the engine's ADR-0139 leaf already answers that caller no rows | a member holding read on a controlled_by_parent object and no object-level read on its master; measured at engine.find by #24932's dev (report 6298477402)Thread-read: 6300130341
Serial constraints cleared:
#24932 (p1, seat 2's, now needs-user-decision) sits in the same function. It is in the decision box, not in flight, so this work goes first and is declared to seat 2 on #21118 at this claim. If #24932's ruling lands mid-flight, the dev re-reads it. No open PR touches security-plugin.ts (all 18 open PRs' files read at this claim).Disclosure (the maintainer's ruling on #9387,
5323310017): ⛔ no reproduction recipe on this card, the PR or any comment. Measurements go in abstract form, and anything more goes to the seat by private channel.Readings taken on
origin/mainf32d795ce5.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{ "issue": 24962, "status": "done", "branch": "claude/issue-24962-cbp-master-object-read", "pr": "https://github.com/objectstack-ai/objectstack/pull/24985", "session": "session_011TdZ8GjixNZFLz1xJD9zzK — the dispatching PM's session (mode:subagent shares it)", "premise_still_valid": true, "summary": "Premise held on origin/main f32d795ce5. computeControlledByParentFilter scoped the master's rows under a system context and never asked the master's object-level read admission (H1 confirmed). It now asks first, and a principal the master's own read door refuses at the object gate gets the empty master set. The admission is ONE predicate: arms 3 to 6 of canReadObject moved into a private objectReadAdmission(object, sets, delegatorSets) over already-resolved sets. canReadObject asks it after resolving the context; the derivation asks it of the master for the sets it was handed. There is no second copy of the rule. H2 measured: the caller half (:4390), the D10 delegator half (:4408) and getReadFilter (:5943) all compose the derivation, and all three now answer no detail; the recursion applies the rule at every hop. H3 measured: a system context, a super-user grant and an on-behalf-of read whose delegator reads the master keep their answers, and so does every principal the master object admits, row-scoped or not. The by-id write path (§4) shared the gap and is closed through the read door with no write-gate change: a by-id update or delete asks the read door's question first, so it now answers RECORD_NOT_FOUND/404, and a predicate write matches no such detail. Engine comparison: the ADR-0139 leaf asks canReadObject(parent), which now runs the same predicate. Measured: before the change the leaf and the id list disagreed for a principal without object-level master read; after it they agree, and agree for the control too. objectql and spec untouched, no widening. #24932: still needs-user-decision with no ruling at this head, and its contract is unchanged.", "tests": "Head 3dd33f7b78. NEW src/controlled-by-parent-master-object-read.test.ts, real ObjectQL + SqlDriver(better-sqlite3) + real SecurityPlugin, on-behalf-of contexts via resolveAuthzContext/assembleExecutionContext: `pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/controlled-by-parent-master-object-read.test.ts` -> `Tests 23 passed (23)`, VERDICT command-exit 0 (at 3dd33f7b78). Package suite at 438ef2dfde (the only later commit renames that file's labels): `pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2` -> `Test Files 198 passed (198)`, `Tests 4100 passed | 45 skipped (4145)`, VERDICT command-exit 0. Typecheck at 3dd33f7b78: `pnpm --filter @objectstack/plugin-security typecheck` -> `check:test-typecheck: OK`, VERDICT command-exit 0; tsconfig.test.json includes src/**/*. Closure: `pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-security^...' build` VERDICT command-exit 0. ABLATION (fix committed first; each leg through scripts/ablation-replace.mjs WRAP mode: `ok mutation landed: anchor 1 -> 0, blob a774fe4e1ddf -> …`; restored with git checkout HEAD -- ABS_PATH; `ok restored: blob == HEAD (a774fe4e1ddf) and git diff HEAD is empty`, re-proved by the leg script's own hash-object compare). The subject is imported from src by a relative path, so no dist leg is owed. Leg A (guard removed): `Tests 12 failed | 11 passed (23)`. Red: every without-master-read read/scope/chain/on-behalf-of/by-id-write/predicate-write pin. Green: 2 preconditions, 2 insert pins (refused by earlier gates either way) and every control. Leg B (guard skipped on recursive hops): `3 failed | 20 passed`, the two two-hops-down pins plus the on-behalf-of pin (its line read recurses). Leg C (delegator call site handed the caller's sets and context): `1 failed | 22 passed`, the on-behalf-of pin. Direction: red as expected on every leg.", "gates": "Local, at 3dd33f7b78: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` exit 0, 69 commands (first stderr line: tree of objectstack-ai/objectstack at commit 3dd33f7b78). All 69 ran sequentially under os-verify-lock (one runner, PIDs recorded), each `cmd :: exit N` captured before any pipe; all 69 exit 0. `--ran` exit 0: `✓ dispatch-gates --ran: 69 derived famil(ies) accounted for — 69 run, 0 NOT-MEASURED (a DERIVED zero — all 69 recorded an exit code and none of them is 3)`. An earlier run at 438ef2dfde had check:dual-build-cjs-loads and check:i18n at exit 3 (PREREQUISITE NOT MET: dist missing). Their trees were present for the final-head run, and both exited 0 there. `node scripts/check-nul-bytes.mjs` OK. check-platform-checklist not owed (no exported symbol renamed or removed; objectReadAdmission is new and private). PR #24985 check-runs, read once right after creation: `No other open PR may claim the same issue` success; `Packed-tarball smoke (opt-in)` and `18.x upgrade harness (opt-in)` skipped; in_progress: Type Check · debt ledger, Type Check · workspace, Type Check · source gates, Type Check · consumer gates, Governed Surface Queue Guard, Lint · scoped · slot lookup & corpus, Lint · PM dispatch-gates, Lint · repo & tooling gates, Lint · scoped · tooling self-tests, Lint · ESLint, Lint · scoped · query-options erasure, Lint · docs & code guards, Lint · CI, release & engine gates, Check Changeset, The card this PR closes must claim this branch, Check Documentation Links, filter, Check PR Size, Part-of PR must not also close its card, No other open PR may claim the same single-writer path, Flag docs affected by code changes. Test Core and Build Core had not appeared yet. CI convergence is the PM's.", "mcp_calls": "0", "api_writes": "2 relay dispatches, each `POST /repos/objectstack-ai/objectstack/dispatches` from this session and executed by the fleet-write relay as objectstack-fleet[bot]: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24985/assignees [os-justin]. The relay read it back: 6885 bytes sent, 6885 stored, identical, assignee listed. (2) this report = POST /repos/objectstack-ai/objectstack/issues/24962/comments via scripts/pm/post-stamped.mjs. Not REST: `git push` of claude/issue-24962-cbp-master-object-read five times (the empty-branch probe at f32d795ce5, then 8e1a07e512, 01b5f0def9, 438ef2dfde, 3dd33f7b78), all fast-forward.", "deviations": [ "A container restart stopped the run once, before any code. On resume: worktree clean at f32d795ce5, remote branch at the same sha, no process of mine alive, verify lock free. The #24932 state and the newest Claim were re-read and are unchanged.", "The first package-suite run was spelled `pnpm --filter @objectstack/plugin-security test -- --maxWorkers=2`. Through the bare `--` the worker flag was dropped and the run used default workers. The whole package was the intended scope. The re-run at 438ef2dfde, the one cited in tests, uses `exec vitest run --maxWorkers=2`.", "The dispatch put every gate run under os-verify-lock. os-dev.md describes check:* gates as not taking it. The dispatch order was followed: one runner held the lock for 15m04s, then 13m01s, and the lock printed its holder-side starvation warning both times.", "origin/main b811c73696 (3 commits, disjoint) was merged before the PR opened, as AGENTS.md §10 asks. The incoming packages/spec change is liveness data only, and this branch changes nothing under packages/spec, so `pnpm --filter @objectstack/spec build && check:generated` was not re-run. Main has since moved to ba69b168be and that is not merged.", "A measurement harness was written into the worktree and run (packages/plugins/plugin-security/src/zz-measure-24962.test.ts). It was never committed and was deleted before commit. To probe the engine leaf it bound the engine's security-service slot by hand. A copy is in the scratchpad.", "Commit trailers use the model-free pair AGENTS.md prescribes, and the PR body ends with AGENTS.md's session-URL footer, not the harness reminder's spellings.", "The pin labels were reworded once after the first package run, to keep grant-shape wording off a public surface under the #9387 ruling. The pin file and the typecheck were re-run at the new head, and so were all 69 gate families." ], "files_changed": [ "packages/plugins/plugin-security/src/security-plugin.ts (+112/-35)", "packages/plugins/plugin-security/src/controlled-by-parent-master-object-read.test.ts (new, +437)", ".changeset/24962-cbp-master-object-read.md (new, @objectstack/plugin-security patch, Clause-②: no)" ], "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed · one unmeasured source reading on the controlled_by_parent detail WRITE gate's master-edit leg, inside the same security family. Reach was not established: on every by-id write measured here, the read question and the reference check stand ahead of it. Detail is withheld under the #9387 ruling and given to the seat privately · dedupe words withheld by the seat under the #9387 ruling (seat edit, see its next note)" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Seat note ·
domain:servicesseat 1 ·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-20T21:43Z, under the #9387 disclosure ruling (5323310017).6302124402: the dedupe words of its out-of-scope finding were more specific than location-level. Nothing else in the report changed.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
ACCEPT → PR #24985 comment
6302706192·domain:servicesseat 1 ·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-20T22:01ZGenerated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Landed ·
domain:servicesseat 1 ·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-20T22:33Z · ⛔ The #9387 disclosure rule applies.added_to_merge_queue2026-10-20T22:03Z →merged2026-10-20T22:30Zorigin/mainby content9532e27886(one parent,7adfaec2a1), 3 files.security-plugin.tscarries the privateobjectReadAdmission, whichcomputeControlledByParentFilternow asks of the masterState change in this stroke:
pm:dispatchedoff and the assignee cleared. The card closedcompletedon the merge.What changes: a
controlled_by_parentdetail's read scope now requires object-level read on its master (ADR-0055 §3). A by-id write of such a detail answers the read door's not-found (§4). A principal the master admits is unchanged (ACCEPT6302706192).Follow-up: #24986, the write gate's master-edit leg. It is redacted, measure-first, and dispatched by this seat.
Generated by Claude Code