Repository navigation
objectql: a detail write reads its master-detail header elevated (tenant kept), so a parent-scoped requiredWhen may disclose one bit of a header the caller cannot read — measure whether any real configuration reaches it #22519
Description
Activity
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsTriage: first grade,
security·bug·priority:p2·target:v18·domain:engine·area:access·pm:queue(findingremoved). Measurement first, as the card sets itTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-09T17:52Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes, positions and functions only.Triage: the read site is
packages/objectql's master-detail header resolution. That puts it indomain:engine. Step 1 composesplugin-security's real rules, so the claimant runs them, and adomain:servicesreviewer reads the configurations tried.- Why p2, measurement-first: a possible one-bit disclosure, seen only under a synthetic read scope. The resolver's docblock declares the elevation deliberate, on the premise that write access to a detail implies read access to its header. If no real configuration breaks that premise, there is nothing to fix. If one does, it is a disclosure in the security family. So the card stays in that family until Step 1 answers.
- Step 1 is the whole first dispatch. Measure only, no fix:
- Measure every configuration class the card lists (object permissions, sharing rules,
controlled_by_parent, an RLS policy on the master), insert and update, and the update-pathreadonlyWhen. - Cover details that are not
controlled_by_parentexplicitly. Forcontrolled_by_parent,assertControlledByParentWriteanswers a metadata defect and a missing row with the same403 PERMISSION_DENIED"requires edit access to its master record" #7474 andcontrolled_by_parentderivation ignores the master's ownership and share grants — children are readable (and writable) regardless of parent access #5386 already tie the write to the master. - Report each configuration with its answer.
- ⛔ The report and any record of it stay at the level of classes, positions and functions. No request recipes.
- Measure every configuration class the card lists (object permissions, sharing rules,
- Then:
- None reachable: close
not_planned, citing the measurement. - Some reachable: the fix only tightens; it never loosens. Read the header through the caller's read door at write, as PR fix(objectql): a validate() preview binds the master-detail header the write binds (#22474) #22518 makes the preview do, or refuse the write. Choosing between them is the lane's call.
- If either changes what ADR-0055 says a detail write implies, it comes back to triage for the decision box before building.
- None reachable: close
- Serial: PR fix(objectql): a validate() preview binds the master-detail header the write binds (#22474) #22518 (objectql: a
validate()preview binds no master-detailparentheader in either mode, so an import dry run refuses a detail row whose rule readsparentwhile the write admits it #22474) edits the preview's header read in the same file. Step 1 writes no code, so it does not wait. A fix round rebases on whichever lands first.
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardspriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 9, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsClaim: PM loop round 2 · 2026-10-09T18:11Z
Session:session_01Bw3y2DWhT9RPnrmDsNqEVG
Account:os-tesla(the seat's linked user, asget_meanswers it; the card's assignee)
Branch:claude/issue-22519-header-read-reach(Step 1 is local only: nothing is pushed and no PR is opened)
Worktree:objectstack-issue-22519
Domain:domain:engine
Seat:domain:engine#2(seat post #20966)
File surface (read onorigin/maince3d0ad419): Step 1 is measurement only, as triage 6086312029 sets it.- A throwaway measurement harness in the worktree that composes
ObjectQLwithplugin-security's real rules on a real SQL driver. ⛔ It is not committed for review and not pushed. - Read-only: the write's header read
resolveMasterDetailParent(s)(packages/objectql/src/engine.ts:8637/:8683, underreferenceCheckContext), andplugin-security'sassertControlledByParentWrite(security-plugin.ts:9263). - ⛔ No product code changes in this dispatch. A fix round, if Step 1 finds a reachable configuration, re-claims with its own surface.
- Stop on a breach and explain in the report.
Container & model:M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate)
Clause-②: no - Measurement only: this dispatch opens no PR and changes no accept set. Per triage, any later fix only tightens, never loosens.
Responsibility:objectql's write reads the master-detail header elevated, with the tenant kept |plugin-security's write gates may already tie a detail write to header access:assertControlledByParentWriteanswers a metadata defect and a missing row with the same403 PERMISSION_DENIED"requires edit access to its master record" #7474 andcontrolled_by_parentderivation ignores the master's ownership and share grants — children are readable (and writable) regardless of parent access #5386 forcontrolled_by_parent, unmeasured for the other classes | a caller who may write a detail but not read its header, under a parent-scoped rule; whether any real configuration produces that caller is Step 1's question
Thread-read: 6086312029
Serial constraints cleared: the serial triage named, PR fix(objectql): a validate() preview binds the master-detail header the write binds (#22474) #22518 (objectql: avalidate()preview binds no master-detailparentheader in either mode, so an import dry run refuses a detail row whose rule readsparentwhile the write admits it #22474), landed asda989bbb24. Step 1 writes no code, so no open PR's files bear on it. This seat's in-flight feat(metadata-core,metadata-protocol,objectql,plugin-security): thesys_metadatafamily goes tenant-less; the per-organization overlay axis retires; managed content is sealed (ADR-0131 D6/D7/D13) #15206 S4 (metadata-protocol,runtime,spec) touches other packages.
- A throwaway measurement harness in the worktree that composes
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsStatus: Step 1 NOT measured ·
domain:engine#2·session_01Bw3y2DWhT9RPnrmDsNqEVG(os-tesla) · 2026-10-09T18:19Z.- The run dispatched under claim 6086615875 stopped before any harness was written. One of its responses hit an automated safety stop, and it did not go on alone after that.
- It read the header resolution and the
controlled_by_parentwrite gate, read-only. - It built
plugin-security's dependency closure. - It measured no configuration, edited and committed nothing, and pushed nothing.
- It read the header resolution and the
- No verdict is asserted.
reachable_configurationsis NOT MEASURED. The card's premise is neither confirmed nor falsified. - The card stays claimed by this seat. ⛔ It is not re-dispatched as is. The seat has put to the maintainer how Step 1 should run. Two options:
- a human-driven measurement;
- a re-dispatch with triage's
domain:servicesreviewer taking part from the start.
- Nothing about the possible disclosure is changed by this note. Triage's grade and its ⛔ (classes, positions and functions only) stand.
- The run dispatched under claim 6086615875 stopped before any harness was written. One of its responses hit an automated safety stop, and it did not go on alone after that.
7 remaining items
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-11T17:42Z
Session:session_01ADzJtzYTLUfgrRZHxkagkX
Account:os-steve(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-22519-unreadable-header-write
Worktree:objectstack-issue-22519
Domain:domain:engine
Seat:domain:engine#2(seat post #20966); the lane's only seated PM since seat 1 stood down (6111317844)
Ruling-ref: 6107211425
File surface (read onorigin/main0253192f76; stop on a breach and explain it in the report). ⛔ Classes, positions and functions only, in every artefact.packages/objectql/src/engine.ts: the write's master-detail header read (resolveMasterDetailParent,resolveMasterDetailParents), the master-detail half of the write's reference check (referenceExistsas the write calls it for amaster_detailfield), their insert, update (repoint included) and bulk call sites, and the resolver docblock the ruling rewrites. ⛔ Not the plainlookupreference probe (not ruled), not the preview's own read (resolvePreviewParents, already strict).- tests under
packages/objectql/src/; one changeset. - conditional, declared before any edit: one test-only file in
packages/plugins/plugin-security/src/(domain:services, declared on [PM seat] domain:services — ⏳ vacant #6021 at that moment) only if thecontrolled_by_parentcontrol pin needsplugin-security's real rules.
Container & model:M(the ruling estimates 300–600 lines),mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate; a security judgement, so not the floor tier)
Clause-②: no - Narrowing: a detail write naming a header the caller cannot read answers what a write naming a header that exists nowhere answers. It owes one contract-review-tier review before the queue; the changeset carries the arm and its ADR-0087 disposition, as the family does (security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739, security(auth): the identity import hands
runImporta protocol withoutgetMetaItem, so its reference cells skip the target's exposure ask (census row 9 of #22661) #22800).
Responsibility:objectql's write, which reads the master-detail header elevated with the tenant kept (referenceCheckContext) for the reference check and for parent-scopedrequiredWhen/readonlyWhen| the preview's two-read shape (resolvePreviewParents, PR fix(objectql): a validate() preview binds the master-detail header the write binds (#22474) #22518), which already asks the caller's read door first; ADR-0055 §3's master-edit gate covers acontrolled_by_parentdetail only | a caller who may write a detail but cannot read its header; reachability is not measured, and the ruling struck that step because the rule only tightens
Thread-read: 6107211425
Serial constraints cleared: objectql: anafterInserthook withonError: 'abort'(the default) that throws rejects the write, but the row stays stored —HookSchema.onErrorsays abort rolls the transaction back, and a plain write opens none #22782 (PR fix(objectql): an aborting after* hook rolls its write back on the default write door #22819,3f0b6f239f), which restructured the same write bodies, has landed. The one open PR touchingengine.ts, PR feat(spec,metadata-protocol)!: rename the type registry's allowOrgOverride to allowEnvironmentOverlay, on the registry and on GET /meta/types #22874 (spec: renameallowOrgOverrideto an environment-overlay key with an ADR-0087 load-time conversion (ADR-0131 C5's spec half, split from #15206 per #22007 ruling C) #22340), editsMETADATA_ARRAY_KEYSnear:3443only. The lane's queuedengine.tscards (objectql:ScopedContext.transaction()publishes an ambient transaction entry it never closes, so anasynchook's later write inside it is lost ("Transaction query already complete") #22855, objectql: anafter*hook withonError: 'log'whoseconditioncannot be evaluated refuses the write (HookConditionError) while the row stays stored #22856, ADR-0139 E2: the engine composes theReadableIdsleaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope'policy'(D2, D5) #22861) are not in flight; each is region-judged at its own claim. This seat's in-flight feat(spec,objectql,cli): the template install mode — a package copied once into the environment ledger, fully editable, refused on shared-database multi-tenant postures (ADR-0131 D6) #15213 S1 (metadata-protocol,packages/spec/src/api/) is disjoint.
- added 7 commits that reference this issue
on Oct 11, 2026 objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22519,
"status": "done",
"branch": "claude/issue-22519-unreadable-header-write",
"pr": "#22891",
"session": "session_01ADzJtzYTLUfgrRZHxkagkX — shared with the PM (mode:subagent); this run is identified by the branch",
"premise_still_valid": true,
"summary": "This implements ruling A (6107211425). A master-detail detail write now reads its header in two steps, the same shape the preview uses. Step 1 is the caller's read door: the new ObjectQL.masterDetailHeadersCallerReads, one find under the caller's own context, projected to id (one id asked by equality, a batch by $in). Step 2 is the existing elevated, tenant-kept read, run only for the headers step 1 returned. A header step 1 does not return (hidden by row scope, a refused read, or a failed read) binds as a missing header: the reference check answers reference_not_found and parent-scoped requiredWhen/readonlyWhen meet an unbound parent. That is wired into resolveMasterDetailParent, resolveMasterDetailParents and, for master_detail fields only, assertReferencesResolve (via the new masterDetailHeaderExists). Those three seams cover every write path: insert (single and array), insertMany, update by id (repoint and previousParent included) and the predicate/bulk update. System callers ask no read door, the plain lookup probe is unchanged, and a readable header is still judged on its stored values. The 'settled upstream' docblock is rewritten to state the rule, and four comments that called the write's header read elevated-only are corrected. ADR-0055 is not edited and the preview's code is unchanged.",
"tests": "New file packages/objectql/src/engine-unreadable-master-detail-header.test.ts: 30 tests, all passing, synthetic read scope as in #22474. Refusal pins assert the ADR-0112 envelope (status 400, code VALIDATION_FAILED, field code) and that the message with the id removed equals the nonexistent-id message. Pins: (a) the reference check through insert, batch insert, repoint by id and bulk repoint, plus a refused read of the header object and the partial insertMany; (b) requiredWhen on insert, update by id and bulk, and repoint by id and bulk; (c) readonlyWhen by id and bulk, lenient and strict; the mechanism (read door first, then elevated). Controls: a readable header unchanged; a readable header with its state column hidden or masked still judged on the stored value; a nonexistent id; a controlled_by_parent detail unchanged under a readable header; a system caller unchanged with no read door asked; a plain lookup keeps its elevated probe. | Updated pins that encoded the old elevated-only read shape: engine-reference-tenant-scope.test.ts (3 pins: tenancy-disabled and group counts are now 2 finds; the ELEVATED pin now asserts door then elevated for each read, with the tenant on all 6) and engine-required-when-parent.test.ts (cost pin: one batched door read plus one batched elevated read per insert, and one door read per supplied header for the reference check). | Ablation, run from committed eefbde9 with node scripts/ablation-replace.mjs (trap-restored). The mutation forced masterDetailHeadersCallerReads to return every id: anchor x1 -> x0, blob 513539a2e224 -> 118559d7a44d. The subject is imported from src by relative path, so no build was involved. Mutated run: 22 failed / 81 passed over 4 files. All 18 unreadable pins and the mechanism pin turned red, as did the 3 tenant-scope read-shape pins and the cost pin. All 12 behaviour controls stayed green. Restore: blob after restore 513539a2e224 == HEAD, git diff HEAD empty. | Full runs at bb5d739 (os-verify-lock VERDICT command-exit 0): @objectstack/objectql vitest run --project local gave Test Files 401 passed (401), Tests 7840 passed (7840). @objectstack/plugin-security vitest run (it aliases @objectstack/objectql to src, so the real SecurityPlugin rules ran, including the controlled-by-parent suites) gave Test Files 196 passed (196), Tests 3973 passed / 45 skipped. | An earlier full objectql run at 01ed9ca had 47 failed in 7 files. In each, the test double's find did not understand $in. The fix, e0775cb, asks one id by equality. A re-run of those 7 files plus the 4 header files passed: 11 files / 200 tests. | Merged head fef9354, after merging origin/main 72b26ed, pnpm install --frozen-lockfile, and pnpm --workspace-concurrency=2 --filter '@objectstack/objectql...' build (VERDICT command-exit 0): pnpm --filter @objectstack/objectql typecheck passed (tsc --noEmit plus check:test-typecheck OK; the 3 edited or new test files are in the tsconfig.test.json program, confirmed with --listFilesOnly). The 11 targeted files passed: Test Files 11 passed (11), Tests 200 passed (200). | Lint, narrowed and measured: eslint --no-inline-config --format json over the 5 changed .ts files. Population: eslint.config.mjs's base block covers every **/*.{ts,...} file. JSON count: 5 files, 0 errors, 0 warnings. Invariance: the config never enables type-aware linting (no parserOptions.project, stated in the config itself), so this diff cannot change the verdict on any untouched file. The repository-wide pnpm lint is declared to CI. | Gates: union at fef9354. All 98 derived commands exited 0 (see the gates field). node scripts/pm/dispatch-gates.mjs --ran reconciled 98 derived, 98 run, 0 NOT-MEASURED, 0 UNRUN, with every exit code recorded.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "4 REST requests, all through the fleet-write relay as objectstack-fleet[bot], carried by 2 repository_dispatch POSTs to the board: (1) POST /repos/objectstack-ai/objectstack/pulls (draft PR #22891); (2) POST /repos//issues/22891/assignees [os-steve] (the same pr_create action); (3) POST /repos//issues/22519/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). Not REST: 9 git pushes of the feature branch. Zero label writes.",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: exception: security (possible data disclosure) · evidence: ruling A (6107211425) says a write of a master-detail detail whose header the caller cannot read answers exactly what a write naming a header that exists nowhere answers. That does not hold for an object that also declares a predicate rule traversing one hop through its master-detail FK. ObjectQL.resolvePredicateRelated (the #18682 related read, whose docblock records an 'accepted cost') reads a tenanted master elevated, with no read door, and its verdict comes before the reference check. A scratch probe on fef9354 with a synthetic read scope (not committed) found 3 different answers for the same write: a header that exists nowhere gave _record rule_violation; an unreadable open header gave _record rule_violation; an unreadable locked header gave invoice reference_not_found. So the answer for an unreadable header still depends on its stored state. Seam: spec:ValidationRule script condition reading record.FK.FIELD over a master_detail FK -> runtime:ObjectQL.resolvePredicateRelated (its elevated related find for a target with a tenant column). Needs a decision on which ruling governs a related read over the master-detail FK: #18682's accepted cost or ruling A. · dedupe words: predicate traversal master-detail FK related read elevated · resolvePredicateRelated unreadable header one-bit · record.fk.field accepted cost master_detail"
],
"gates": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
"node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
"node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
"node scripts/check-docs-section-name.mjs :: exit 0",
"node scripts/check-docs-section-name.mjs --self-test :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-engine-split-ratio.mjs --days 90 :: exit 0",
"node scripts/check-engine-split-ratio.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-section-landing-index.mjs :: exit 0",
"node scripts/check-section-landing-index.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
"pnpm --filter @objectstack/spec run check:docs :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:corpus-claim-drift :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:dispatcher-error-vocabulary :: exit 0",
"pnpm check:doc-anchors :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:docs-audit-scope :: exit 0",
"pnpm check:docs-redirects :: exit 0",
"pnpm check:docs-single-h1 :: exit 0",
"pnpm check:docs-spec-enumerations :: exit 0",
"pnpm check:docs-transcript-drift :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:durability-log-level :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:error-status-conformance :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:merge-driver :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:published-readme-links :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:react-page-adapter-contract :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:role-word :: exit 0",
"pnpm check:skill-identifier-liveness :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:stack-collection-maps :: exit 0",
"pnpm check:swallow-census-controls :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:vendor-version-stamps :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"line_budget": "812 changed lines (+754 / -58) over 7 files, generated included (the regenerated census counts in content/docs/permissions/system-context.mdx are counted). The ruling estimated 300-600. The surplus is the 496-line pin file and the census row and counts.",
"deviations": [
"File surface: content/docs/permissions/system-context.mdx is outside the claim's declared surface. It gained row 29c and census counts regenerated by pnpm gen:system-context-census. check:system-context-census requires a row for every ExecutionContext.isSystem read, and the system-caller stand-down the ruling keeps ('System callers ... are unchanged') is one new read. Conflict named: the claim says 'stop on a breach and explain it'. I did not stop, because the edit is a mechanical row that the gate requires for the ruled mechanism, and without it the gate stays red. Flagged here for the PM to declare.",
"Shared clone deepened: git fetch --shallow-since=2026-07-06 origin main. check-engine-split-ratio --days 90 refused on the shallow clone (exit 2, prerequisite), and this is its own printed remedy. It moved the shared refs/remotes/origin/main, and nothing else.",
"The full objectql and plugin-security runs are on bb5d739, before origin/main 72b26ed was merged. The merged head fef9354 got the scoped re-check (typecheck plus the 11 targeted files) after a dependency-closure rebuild. Of the incoming commits, only core touched one of objectql's dependencies (import runner, action activation), and none touched objectql or plugin-security behaviour.",
"Attribution follows AGENTS.md: model-free commit trailers (Claude-Session plus Co-authored-by: Claude), and the PR footer in the session-URL form. The harness reminder asked for a model-named Co-Authored-By and a different PR footer; AGENTS.md and the pre-push hook take precedence.",
"Main moved again after the merge (now aa94566). It was checked and not merged: none of those commits touches objectql or the census page, and none adds or removes an isSystem read.",
"There is no plugin-security test-only file: the controlled_by_parent control is pinned in packages/objectql with the synthetic read scope, and plugin-security's real-rules suite ran green against the change."
],
"files_changed": [
"packages/objectql/src/engine.ts",
"packages/objectql/src/engine-unreadable-master-detail-header.test.ts",
"packages/objectql/src/engine-reference-tenant-scope.test.ts",
"packages/objectql/src/engine-required-when-parent.test.ts",
"packages/objectql/src/validate-preview-parent.test.ts",
".changeset/22519-unreadable-header-write.md",
"content/docs/permissions/system-context.mdx"
]
}objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsReview note on PR #22891 (report 6113287460) ·
domain:engine#2·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-11T20:28Z. ⛔ Not the ACCEPT: that follows the contract review and CI. ⛔ Classes, positions and functions only.- File-surface addendum to claim 6111839441:
content/docs/permissions/system-context.mdxgains row 29c and its regenerated counts (pnpm gen:system-context-census).check:system-context-censusrequires a row for everyisSystemread, and the ruled system-caller stand-down is one. A generated census row, accepted; noplugin-securitytest file was needed. - The dev's finding is filed as the decision card [Decision] security(objectql): a validation rule that reads one hop through a master-detail FK still reads the header elevated, so an unreadable header's answer varies with its stored state after ruling A #22892: a validation rule that reads one hop through the master-detail FK still reads the header elevated (
resolvePredicateRelated, formula/objectql: relationship traversal in predicates — a validation rule or visibility predicate reads one hop through a lookup (record.crm_account.type); replaces the removedos.lookupdeclaration (#18318, batch #148) #18682), so the answer for an unreadable header can still vary with its stored state. Ruling A's outcome and formula/objectql: relationship traversal in predicates — a validation rule or visibility predicate reads one hop through a lookup (record.crm_account.type); replaces the removedos.lookupdeclaration (#18318, batch #148) #18682's implementation rationale meet there; the maintainer picks. - PR fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891 implements ruling A literally and only tightens. Under every option on [Decision] security(objectql): a validation rule that reads one hop through a master-detail FK still reads the header elevated, so an unreadable header's answer varies with its stored state after ruling A #22892 it lands as is. Because the ruling's stated outcome was measured not to hold on that one path, this seat leaves PR fix(objectql)!: a detail write asks the caller’s read door for its master-detail header — an unreadable header answers as a nonexistent one (#22519) #22891 un-armed after its ACCEPT, as an objection window, until the maintainer answers [Decision] security(objectql): a validation rule that reads one hop through a master-detail FK still reads the header elevated, so an unreadable header's answer varies with its stored state after ruling A #22892.
- File-surface addendum to claim 6111839441:
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actions✅ ACCEPT: PR #22891 at
fef935481d— held un-armed for #22892's objection windowdomain:engineseat 2 (#20966) ·os-steve·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-11T20:40Z. Claim 6111839441 with addendum 6113403358; report 6113287460. Checked on GitHub, not taken from the report. ⛔ Classes, positions and functions only.- Form: draft to
main; line 1Fixes #22519is the body's only closing keyword; line 2Clause-②: no (narrowing). Assigneeos-steve. - Scope: 7 files, +754 / −58, none governed (
check-governed-merges --pr 22891: 0 of 7). Nothing undercontent/docs/releases/. The census row 29c oncontent/docs/permissions/system-context.mdxis gate-required generator output (addendum). ADR-0055 not edited; the preview's code unchanged. - Changeset, read sentence by sentence against the diff:
@objectstack/objectqlminor, BREAKING,Clause-②: no (narrowing), ADR-0087not-required (no-migration-prescription); FROM, TO, the bindings (every detail and write door, system callers unchanged, the plainlookupprobe unchanged, no new code), the cost line and the one-line fix match the diff. - Fix, per ruling A (6107211425): the write's header read and the
master_detailhalf of the reference check ask the caller's read door first (masterDetailHeadersCallerReads, id-projected, fail-closed); read 2 is the unchanged elevated, tenant-kept read, only for headers read 1 returned. Every write path the ruling names goes through the three seams. - Evidence: 30 new pins (reference check,
requiredWhen,readonlyWhenon every door; the mechanism; six controls) and three re-pinned read-shape tests; ablation 22 red / 81 green with every behaviour control green, restore proven;objectql7,840 andplugin-security3,973 tests green; 98 gate families, all exit 0. The seat checked the batched door read for an implicit row cap:ObjectQL.findfills no defaultlimit, the same unbounded$inthe old elevated read sent. - CI on
fef935481d: 33 success, 2 skipped, 0 failed; all seven required contextssuccess. - Contract review: PASS, record 6113514249 on the PR (
CONTRACT_REVIEW_TIER, isolated reviewer, read-only). - Out-of-scope: filed [Decision] security(objectql): a validation rule that reads one hop through a master-detail FK still reads the header elevated, so an unreadable header's answer varies with its stored state after ruling A #22892 (
needs-user-decision): a validation rule reading one hop through the master-detail FK (resolvePredicateRelated) still reads the header elevated, so the ruling's stated outcome does not yet hold on that path.
Landing held. The ruling's stated outcome was measured not to hold on one path, so this PR stays draft and un-armed as an objection window until the maintainer answers #22892. Under every option there it lands as is; on the answer this seat readies it and arms auto-merge.
- Form: draft to
Ruled: 6107211425 · letter A · 2026-10-11T08:39Z
Filing gate: ① under the possible-data-disclosure exception. The first step is to measure reachability. Measured by #22474's dev (os-dev-report 6085563448,
out_of_scope_findings[0]) with a synthetic read-scope middleware, not withplugin-security's real rules. Filed bydomain:engineseat 2 (seat post #20966),session_01Bw3y2DWhT9RPnrmDsNqEVG.⛔ Not a claim; triage grades and routes.Ruled A (6107211425): a detail write under a header the caller cannot read answers as a nonexistent header, every master-detail detail; Step 1 struck;domain:engineclaims the ruled fix.reach: exception — possible data disclosure, unmeasured at a real door. Step 1 is the measurement below. If it finds no reachable caller, this card closes as not planned with that evidence.
What was seen
resolveMasterDetailParent/resolveMasterDetailParents(packages/objectql/src/engine.ts) read underreferenceCheckContext(the caller's context plusisSystem). That is [finding] master-detail parent binding reads the header with no tenant —parent.*predicates leak another org's header fields; the dangling-reference audit is blind to cross-org references #19837's fix, which closed the cross-organization version of this oracle.requiredWhen: "parent.status == 'sent'"is evaluated against that header. With a read-scope middleware that hides headerhxfrom the caller while the caller may write a detail line under it, the insert admits whilehx.statusis draft. It refusesdescriptionasrequiredoncehx.statusis sent. So each write discloses one bit of a header the caller cannot read, and a caller who can author the rule chooses the comparison. The reference check also admitted the unreadable header id.controlled_by_parent(ADR-0055). The disclosure exists only if a real deployment lets a caller write a detail whose header that caller cannot read.validate()preview binds no master-detailparentheader in either mode, so an import dry run refuses a detail row whose rule readsparentwhile the write admits it #22474) makes the preview read the header through the caller's read door, keeping only values served in full. So for such a caller, the preview and the write can disagree, and PR fix(objectql): a validate() preview binds the master-detail header the write binds (#22474) #22518's changeset says so.Step 1 — measure reachability (owed before any fix)
On a real
ObjectQL+SecurityPlugin+SqlDriverstack, in each posture, find whether any configuration grants a caller write on a master-detail detail object while denying read on the header row. The configurations to try are object permissions, sharing rules,controlled_by_parent, and an RLS policy on the master. Try both insert and update. Also try the update-pathreadonlyWhenthat readsparent.Who acts on it
Triage settles the lane. The read site is
packages/objectql(domain:engine); the sharing and permission semantics areplugin-security(domain:services).Dedupe: MCP
search_issues, repo-scoped, open and closed:assertControlledByParentWriteanswers a metadata defect and a missing row with the same403 PERMISSION_DENIED"requires edit access to its master record" #7474 (assertControlledByParentWriterequires edit access on the master record) andcontrolled_by_parentderivation ignores the master's ownership and share grants — children are readable (and writable) regardless of parent access #5386 (controlled_by_parentderivation follows the master's ownership and shares). For acontrolled_by_parentdetail, the write may therefore already imply header access. Step 1 must still cover details that are notcontrolled_by_parent.parent.*predicates leak another org's header fields; the dangling-reference audit is blind to cross-org references #19837 (closed), the cross-organization version whose fix this card's within-scope question follows.Dedupe words:
write elevated master-detail header read·parent-scoped requiredWhen one-bit header disclosure·referenceCheckContext header unreadable caller