You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
objectql: a file field's metadata follows the parent-derived verdict the download door applies — hydrate a field-owned file for a reader refused sys_file read (ruling B on #22624) #22637
Filing gate: ③ a task the maintainer directed. The maintainer ruled B on decision card #22624, verbatim 「同意」 (director record 6094990917 on #22624; pointer 6095034976 on #22593). The ruling names domain:engine seat 2 as the filer. Filed by that seat (seat post #20966) · session_01Bw3y2DWhT9RPnrmDsNqEVG. ⛔ Not a claim; triage grades it.
Reader:domain:engine (packages/objectql).
The ruling, verbatim
A file field's metadata follows the parent-derived verdict the download door already applies (ADR-0104 D3, buildFileReadAuthorizer): when the caller's sys_file read is refused, resolveFileReferences still hydrates a file whose ref_object / ref_id name the record being read (the delegate consulted where the owner object declares a fileAccessDelegate); a referenced id that does not point at that record keeps PR #22620's refused marker; direct sys_file queries and public_read are unchanged.
⛔ No change to sys_file's own read rule, to direct sys_file queries, or to public_read.
Where the verdict lives (service-storage's buildFileReadAuthorizer) and how the engine reaches it without a second copy is the claimant's measured choice. ⛔ No second predicate. That is a cross-lane path (domain:services) if service-storage changes.
Pins (from the ruling)
A field-owned file on the record being read hydrates in full for a reader refused sys_file read.
A referenced id that is not field-owned by that record keeps the refused marker.
An owner object that declares a fileAccessDelegate gets the delegate's verdict.
public_read is unchanged.
Ablation: remove the parent-derived arm, and the first pin returns the refused marker.
Clause-②: yes (a widening of what a refused reader is served). It owes one contract-review-tier review before the queue.
Serial: PR #22620 (#22593) adds the refused arm this card extends, in the same function. It lands first; this card starts on top of it.
Lane:packages/objectql (resolveFileReferences), so domain:engine. If service-storage changes, the claim declares it under the cross-domain exception path.
Why p2, with security: it widens what a refused reader is served, and the maintainer ruled it (「同意」, 6094990917). The boundary moves exactly as ruled, and no further:
only a field-owned file on the record being read;
by the download door's own verdict (ADR-0104 D3, buildFileReadAuthorizer), ⛔ never a second predicate;
sys_file's own read rule, direct sys_file queries and public_read are unchanged.
Before the queue:Clause-②: yes owes the contract-review-tier review, as the card says.
Claim: PM loop round 3 (serial) · 2026-10-10T13:07Z
Session: session_01Bw3y2DWhT9RPnrmDsNqEVG
Account: os-tesla (the seat's linked user, as get_me answers it; the card's assignee)
Branch: claude/issue-22637-file-metadata-parent-derived
Worktree: objectstack-issue-22637
Domain: domain:engine
Seat: domain:engine#2 (seat post #20966)
Serial: the lane runs one card at a time by the maintainer's order (6095179065 on #20966). #15206 stage S5 (PR #22628) waits only on an authorized approval (Tier H; quick-read 6097800896), so it is not in flight.
File surface (read on origin/main243dd3c625):
For each id whose sys_file row's ref_object / ref_id name the record being read, the engine hydrates the metadata by the download door's own verdict. Today that verdict is "the caller can read that record", or the owner object's declared fileAccessDelegate.
Every other id keeps { id, metadataRefused: true }.
Tests beside it, and a changeset.
Conditional:
packages/services/service-storage (buildFileReadAuthorizer, storage-service-plugin.ts:1210), only if the one verdict must move or be exposed so the engine reaches it without a second copy. That is a domain:services path, declared on [PM seat] domain:services — ⏳ vacant #6021 before any edit.
⛔ Not: sys_file's own read rule, direct sys_file queries, or public_read. ⛔ No second predicate.
Stop on a breach and explain in the report.
Container & model: M, mode:subagent, model: default (dispatch-gates --tier: no path-derived mandate)
Clause-②: yes
os-dev-report
{
"issue": 22637,
"status": "done",
"branch": "claude/issue-22637-file-metadata-parent-derived",
"pr": "#22697",
"session": "session_01Bw3y2DWhT9RPnrmDsNqEVG (shared with the PM; this run is identified by its branch)",
"head": "9931bb55d3",
"premise_still_valid": true,
"summary": "Ruling B is executed. When a caller's sys_file read is refused, ObjectQL.resolveFileReferences now makes one sudo-shaped sys_file read of the held ids whose ref_object / ref_id name a record in the result. It asks the download door's own field-owned verdict once per read, for those owner ids, and hydrates the allowed files exactly as a sys_file reader would see them. Every other id keeps { id, metadataRefused: true }. The verdict is not copied into the engine (ObjectQL holds no service registry, so it cannot reach a fileAccessDelegate itself). service-storage moves the field-owned arm of buildFileReadAuthorizer (the :1237-:1265 range the ruling cites) into one module-private batched function, readableFieldOwners. The door calls it with its one owner id, with a byte-identical query and fail-closed handling. StorageServicePlugin.start() registers the same function on the engine, beside registerHeldFileResolver, through the new seam registerFieldOwnedFileReadAuthorizer (type FieldOwnedFileReadAuthorizer, exported). Is the ownership match a second predicate? By the ruling's own words it is the scope condition, not an access verdict; allow and deny, including the delegate, come from the door's function, reached without a copy. Both cross-lane paths were declared by the PM before any edit: service-storage on #6021 (comment 6097907456) and two docs passages on #6023 (comment 6098008938). The two docs passages that became false are corrected (prose only). Draft PR #22697. All 35 check-runs on head 9931bb5 concluded: 33 success and 2 skipped (Console Pin Gate and Packed-tarball smoke, both opt-in or path-scheduled). All seven required contexts are success.",
"tests": "Premise, measured first in a scratch plugin-security test (never committed, deleted). The stack was real ObjectQL (src) + real SecurityPlugin + real StorageServicePlugin + SqlDriver; the reader had record read and no sys_file read. With engine.ts and storage-service-plugin.ts at 243dd3c (blobs 3177422431 / 0246a5bfe4), a field-owned committed file read { id, metadataRefused: true } and the direct sys_file query got PERMISSION_DENIED/403. At the branch the same file read { id, name, size, mimeType, url }, a copied id stayed refused, and the direct query stayed 403. Both files were restored from HEAD (blob == HEAD, git diff HEAD empty, porcelain clean). At head 9931bb5 (origin/main merged): objectql vitest --project local, 394 files / 7753 tests passed; --project repo, 1 / 5 passed. service-storage vitest: 51 files / 846 tests passed, with the door's existing tests unedited. plugin-security file-field-hydration-refused.test.ts: 4 passed. typecheck exit 0 for objectql (test layer 40 files / 234 errors / 65 pinned, unchanged) and for service-storage (test layer clean). New pins: objectql engine-file-hydrate-parent-derived.test.ts (16) and service-storage field-owned-file-hydration-door-agreement.test.ts (9), which asks the door and the record read about one real ObjectQL + SqlDriver fixture with the plugin booted. Ablation at e6e7fbe, through ablation-replace WRAP: the anchor 'const owned = await this.readParentDerivedFiles(objectName, records, uniqueIds, execCtx);' (1 to 0) was replaced by the same call over records.slice(0, 0) plus a debug marker, and the blob went 6a9455a5d3 to bb576869bc. objectql (source): 10 failed / 6 passed, and the first pin received { id: 'f_owned_1aa', metadataRefused: true }. service-storage (objectql via dist, rebuilt; ablation-dist-preflight found the marker in 4 built files): 4 failed / 5 passed, and the field-owned pin received the marker. Restore: blob == HEAD, git diff HEAD empty; after a rebuild, preflight --absent passed (absent from all 14 built files, tree clean) and the files were green again (16/16, 9/9). The merge changed neither ablated file. Lint, a narrowed run: eslint --no-inline-config --format json over the 5 changed .ts files reports 5 files, 0 errors, 0 warnings. The population is eslint.config.mjs (the config pnpm lint runs), and it has no parserOptions.project, so no type-aware linting can move an untouched file's verdict.",
"gates": {
"head": "9931bb55d3",
"derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths): 102 commands; all run after a full turbo build; --ran reconciles 102 derived, 102 run, 0 NOT-MEASURED",
"results": [
"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-tenant-audit-census.mjs :: exit 0",
"node scripts/check-tenant-audit-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:i18n :: exit 0",
"pnpm check:i18n-stale-fill :: 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"
]
},
"check_runs": {
"head": "9931bb55d3",
"total": 35,
"conclusions": {
"Test Core": "success",
"TypeScript Type Check": "success",
"Dogfood Regression Gate": "success",
"Console Pin Gate": "skipped",
"Test Core (2/6)": "success",
"Test Core (3/6)": "success",
"Dogfood Verify CLI": "success",
"Dogfood Regression Gate (1/3)": "success",
"Auto Label": "success",
"Test Core (5/6)": "success",
"Dogfood Regression Gate (3/3)": "success",
"Test Core (4/6)": "success",
"Test Core (1/6)": "success",
"Temporal Conformance (live PG + MySQL)": "success",
"Test Core (6/6)": "success",
"Dogfood Regression Gate (2/3)": "success",
"Build Core": "success",
"Build Docs": "success",
"Packed-tarball smoke (opt-in)": "skipped",
"filter": "success",
"Type Check · workspace": "success",
"Type Check · source gates": "success",
"Type Check · debt ledger": "success",
"Lint & Repo Gates": "success",
"Part-of PR must not also close its card": "success",
"Check Changeset": "success",
"No other open PR may claim the same issue": "success",
"No other open PR may claim the same single-writer path": "success",
"Flag docs affected by code changes": "success",
"Type Check · consumer gates": "success",
"Check PR Size": "success",
"The card this PR closes must claim this branch": "success",
"Governed Surface Queue Guard": "success",
"Spec property liveness": "success",
"Check Documentation Links": "success"
}
},
"files_changed": [
".changeset/22637-file-field-metadata-parent-derived.md",
"content/docs/permissions/attachments-access.mdx",
"content/docs/protocol/objectql/types.mdx",
"packages/objectql/src/engine-file-hydrate-parent-derived.test.ts",
"packages/objectql/src/engine.ts",
"packages/objectql/src/index.ts",
"packages/services/service-storage/src/field-owned-file-hydration-door-agreement.test.ts",
"packages/services/service-storage/src/storage-service-plugin.ts"
],
"deviations": [
"Cross-lane edits beyond packages/objectql. service-storage (2 paths) was declared on #6021 (comment 6097907456) and the two docs passages on #6023 (comment 6098008938), both by the PM at my mid-run request and before any edit. Both declarations were read back before editing.",
"Two first-pass gate reds were caused by my own new test code and are fixed: check:query-options-erasure went from 236 to 240 test sites and is back to 236; check:slot-lookup flagged getService typed any. A scratch measurement file briefly reddened check:cross-package-test-inputs, which is green after its deletion. The full gate list was re-run on the final head.",
"origin/main was merged into the branch (9931bb5) before pr_create, per AGENTS 10. Three later main commits touch none of these packages and were not merged.",
"The order's wait-for-CI clause was honoured with foreground polling of the check-runs REST read, no background watcher."
],
"mcp_calls": "0",
"api_writes": "3 REST writes, each through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft, #22697); label-write --assign os-tesla, POST /repos//issues/22697/assignees; post-stamped, POST /repos//issues/22637/comments (this report). git push is not counted.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed: packages/spec FileRefusedValueSchema TSDoc ('when the reader is refused the sys_file read that resolves it') and fileAccessDelegate TSDoc / .describe() ('authorizes downloads') are now incomplete rather than false. The marker now also means the parent-derived verdict did not allow, and the delegate is also consulted for a refused reader's metadata. They are left unchanged to avoid gen:schema / gen:docs and are named in the PR's Acceptance notes. Dedupe words: FileRefusedValueSchema TSDoc parent-derived · fileAccessDelegate describe metadata hydration.",
"carrier: none · noted, not filed: the full composition (real SecurityPlugin + StorageServicePlugin + REST) has no committed pin; it was measured once in a scratch run. A dogfood pin would sit in packages/qa (domain:cli). This is in the PR's Acceptance notes.",
"carrier: none · noted, not filed: by the ruling, hydration is narrower than the door. A file's uploader without sys_file read, reading a delegate-governed record whose delegate refuses them, sees the marker while the door serves them the bytes (the door's owner arm). This is in the PR's Acceptance notes."
]
}
ACCEPT (seat review): PR #22697 at head 9931bb55d3. A reader refused sys_file read is served the metadata of a file the record being read owns, by the download door's own field-owned verdict (ruling B on #22624)
domain:engine#2 · session_01Bw3y2DWhT9RPnrmDsNqEVG (os-tesla), claim 6097826006 · 2026-10-10T15:32Z. Read against GitHub and the branch, not the report (os-dev-report 6098964328).
Contract review at CONTRACT_REVIEW_TIER: 6099114132, PASS on this head. It is owed because of the Clause-②: yes line.
Shape.
Draft, base main. Line 1 is Fixes #22637 and line 2 is Clause-②: yes, matching the claim.
8 files, +1,251/−65 = 1,316 changed lines. NOT governed, and under the human-merge threshold.
The head merges clean with origin/main (git merge-tree).
It asks the registered field-owned verdict once per read, for those owners, and hydrates the files it allows.
Every other id keeps { id, metadataRefused: true }.
With no verdict registered, the marker is unchanged.
The verdict is not copied.service-storage extracts the field-owned arm of buildFileReadAuthorizer into the module-private readableFieldOwners, with the delegate arm and the fail-closed handling.
The download door calls it with its one owner id. The record found the door's verdict byte-for-byte unchanged on every path against the merge-base source.
start() registers it through the new seam registerFieldOwnedFileReadAuthorizer (exported type FieldOwnedFileReadAuthorizer), beside registerHeldFileResolver.
Unchanged:sys_file's own read rule, direct sys_file queries (pinned 403) and public_read (pinned).
Pins and ablation.
engine-file-hydrate-parent-derived.test.ts (16) covers field-owned, copied id, attachment-only, delegate deny and allow, multiple, and mixed arrays.
field-owned-file-hydration-door-agreement.test.ts (9) asks the door and the record read the same question on one real ObjectQL + SqlDriver fixture.
Ablating the parent-derived arm turns both files red, and the restore is blob-equal.
Docs: two prose passages corrected. Changesets:objectql and service-storage, both minor.
CI on 9931bb55d3. 35 runs: 33 success, 2 skipped (Console Pin Gate and Packed-tarball smoke, roster skips), 0 failure. All seven required contexts are success. The Vercel status is success.
Boundary.
The uploader edge. An uploader without sys_file read, on a record whose delegate refuses them, sees the marker while the door serves them the bytes through its owner arm. That is within the ruling, which cites the field-owned arm only.
Landed: PR #22697 → 1bb1da1bcb through the merge queue, at 2026-10-10T15:58Z. domain:engine#2 · session_01Bw3y2DWhT9RPnrmDsNqEVG · 2026-10-10T15:59Z.
Content on origin/main: all 8 files are blob-identical to the reviewed head 9931bb55d3.
Among them: the engine's parent-derived arm and seam, service-storage's readableFieldOwners with its registration, both new pin files, the two docs passages and the changeset.
1bb1da1bcb is an ancestor of origin/main.
Card.Fixes #22637 closed it as completed. pm:dispatched is removed in this act.
Records: ACCEPT 6099135363 and contract review 6099114132 (PASS).
Activity
objectstack-fleet commentedon Oct 10, 2026
Triage: first grade,
enhancement·security·priority:p2·domain:engine·area:files·pm:blockedon #22593. It executes ruling B on #22624Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T07:55Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: #22593
packages/objectql(resolveFileReferences), sodomain:engine. Ifservice-storagechanges, the claim declares it under the cross-domain exception path.security: it widens what a refused reader is served, and the maintainer ruled it (「同意」,6094990917). The boundary moves exactly as ruled, and no further:buildFileReadAuthorizer), ⛔ never a second predicate;sys_file's own read rule, directsys_filequeries andpublic_readare unchanged.Clause-②: yesowes the contract-review-tier review, as the card says.objectstack-fleet commentedon Oct 10, 2026
Claim: PM loop round 3 (serial) · 2026-10-10T13:07Z
Session:
session_01Bw3y2DWhT9RPnrmDsNqEVGAccount:
os-tesla(the seat's linked user, asget_meanswers it; the card's assignee)Branch:
claude/issue-22637-file-metadata-parent-derivedWorktree:
objectstack-issue-22637Domain:
domain:engineSeat:
domain:engine#2(seat post #20966)Serial: the lane runs one card at a time by the maintainer's order (6095179065 on #20966). #15206 stage S5 (PR #22628) waits only on an authorized approval (Tier H; quick-read 6097800896), so it is not in flight.
File surface (read on
origin/main243dd3c625):packages/objectql/src/engine.ts,resolveFileReferences(about:11954). It changes on the refused arm that PR fix(objectql): a refused sys_file read marks the file field refused instead of reading as no file #22620 added (isReadRefusal, about:12040).sys_filerow'sref_object/ref_idname the record being read, the engine hydrates the metadata by the download door's own verdict. Today that verdict is "the caller can read that record", or the owner object's declaredfileAccessDelegate.{ id, metadataRefused: true }.packages/services/service-storage(buildFileReadAuthorizer,storage-service-plugin.ts:1210), only if the one verdict must move or be exposed so the engine reaches it without a second copy. That is adomain:servicespath, declared on [PM seat] domain:services — ⏳ vacant #6021 before any edit.packages/speconly if a declared member changes. That is adomain:specpath, declared on [PM seat] domain:spec — 🟢 os-warren · session_019G5ZnRrTMiwcHG6iZM19zf #6017 before any edit.sys_file's own read rule, directsys_filequeries, orpublic_read. ⛔ No second predicate.Container & model:
M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate)Clause-②: yes
Responsibility: n/a — not a defect card
Thread-read: 6095345451
Serial constraints cleared: at 2026-10-10T13:07Z, 9 open PRs read by
filename. None touchespackages/objectql/src/engine.tsorservice-storage'sstorage-service-plugin.ts/storage-routes.ts. This seat's PR feat(metadata-protocol,objectql,rest,spec)!: every metadata read is environment → code; legacy organization rows and sealed overlays are reported at boot, not served (#15206 S5) #22628 (S5) touchesobjectqlonly inplugin.tsand in protocol tests.objectstack-fleet commentedon Oct 10, 2026
os-dev-report
{
"issue": 22637,
"status": "done",
"branch": "claude/issue-22637-file-metadata-parent-derived",
"pr": "#22697",
"session": "session_01Bw3y2DWhT9RPnrmDsNqEVG (shared with the PM; this run is identified by its branch)",
"head": "9931bb55d3",
"premise_still_valid": true,
"summary": "Ruling B is executed. When a caller's sys_file read is refused, ObjectQL.resolveFileReferences now makes one sudo-shaped sys_file read of the held ids whose ref_object / ref_id name a record in the result. It asks the download door's own field-owned verdict once per read, for those owner ids, and hydrates the allowed files exactly as a sys_file reader would see them. Every other id keeps { id, metadataRefused: true }. The verdict is not copied into the engine (ObjectQL holds no service registry, so it cannot reach a fileAccessDelegate itself). service-storage moves the field-owned arm of buildFileReadAuthorizer (the :1237-:1265 range the ruling cites) into one module-private batched function, readableFieldOwners. The door calls it with its one owner id, with a byte-identical query and fail-closed handling. StorageServicePlugin.start() registers the same function on the engine, beside registerHeldFileResolver, through the new seam registerFieldOwnedFileReadAuthorizer (type FieldOwnedFileReadAuthorizer, exported). Is the ownership match a second predicate? By the ruling's own words it is the scope condition, not an access verdict; allow and deny, including the delegate, come from the door's function, reached without a copy. Both cross-lane paths were declared by the PM before any edit: service-storage on #6021 (comment 6097907456) and two docs passages on #6023 (comment 6098008938). The two docs passages that became false are corrected (prose only). Draft PR #22697. All 35 check-runs on head 9931bb5 concluded: 33 success and 2 skipped (Console Pin Gate and Packed-tarball smoke, both opt-in or path-scheduled). All seven required contexts are success.",
"tests": "Premise, measured first in a scratch plugin-security test (never committed, deleted). The stack was real ObjectQL (src) + real SecurityPlugin + real StorageServicePlugin + SqlDriver; the reader had record read and no sys_file read. With engine.ts and storage-service-plugin.ts at 243dd3c (blobs 3177422431 / 0246a5bfe4), a field-owned committed file read { id, metadataRefused: true } and the direct sys_file query got PERMISSION_DENIED/403. At the branch the same file read { id, name, size, mimeType, url }, a copied id stayed refused, and the direct query stayed 403. Both files were restored from HEAD (blob == HEAD, git diff HEAD empty, porcelain clean). At head 9931bb5 (origin/main merged): objectql vitest --project local, 394 files / 7753 tests passed; --project repo, 1 / 5 passed. service-storage vitest: 51 files / 846 tests passed, with the door's existing tests unedited. plugin-security file-field-hydration-refused.test.ts: 4 passed. typecheck exit 0 for objectql (test layer 40 files / 234 errors / 65 pinned, unchanged) and for service-storage (test layer clean). New pins: objectql engine-file-hydrate-parent-derived.test.ts (16) and service-storage field-owned-file-hydration-door-agreement.test.ts (9), which asks the door and the record read about one real ObjectQL + SqlDriver fixture with the plugin booted. Ablation at e6e7fbe, through ablation-replace WRAP: the anchor 'const owned = await this.readParentDerivedFiles(objectName, records, uniqueIds, execCtx);' (1 to 0) was replaced by the same call over records.slice(0, 0) plus a debug marker, and the blob went 6a9455a5d3 to bb576869bc. objectql (source): 10 failed / 6 passed, and the first pin received { id: 'f_owned_1aa', metadataRefused: true }. service-storage (objectql via dist, rebuilt; ablation-dist-preflight found the marker in 4 built files): 4 failed / 5 passed, and the field-owned pin received the marker. Restore: blob == HEAD, git diff HEAD empty; after a rebuild, preflight --absent passed (absent from all 14 built files, tree clean) and the files were green again (16/16, 9/9). The merge changed neither ablated file. Lint, a narrowed run: eslint --no-inline-config --format json over the 5 changed .ts files reports 5 files, 0 errors, 0 warnings. The population is eslint.config.mjs (the config pnpm lint runs), and it has no parserOptions.project, so no type-aware linting can move an untouched file's verdict.",
"gates": {
"head": "9931bb55d3",
"derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths): 102 commands; all run after a full turbo build; --ran reconciles 102 derived, 102 run, 0 NOT-MEASURED",
"results": [
"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-tenant-audit-census.mjs :: exit 0",
"node scripts/check-tenant-audit-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:i18n :: exit 0",
"pnpm check:i18n-stale-fill :: 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"
]
},
"check_runs": {
"head": "9931bb55d3",
"total": 35,
"conclusions": {
"Test Core": "success",
"TypeScript Type Check": "success",
"Dogfood Regression Gate": "success",
"Console Pin Gate": "skipped",
"Test Core (2/6)": "success",
"Test Core (3/6)": "success",
"Dogfood Verify CLI": "success",
"Dogfood Regression Gate (1/3)": "success",
"Auto Label": "success",
"Test Core (5/6)": "success",
"Dogfood Regression Gate (3/3)": "success",
"Test Core (4/6)": "success",
"Test Core (1/6)": "success",
"Temporal Conformance (live PG + MySQL)": "success",
"Test Core (6/6)": "success",
"Dogfood Regression Gate (2/3)": "success",
"Build Core": "success",
"Build Docs": "success",
"Packed-tarball smoke (opt-in)": "skipped",
"filter": "success",
"Type Check · workspace": "success",
"Type Check · source gates": "success",
"Type Check · debt ledger": "success",
"Lint & Repo Gates": "success",
"Part-of PR must not also close its card": "success",
"Check Changeset": "success",
"No other open PR may claim the same issue": "success",
"No other open PR may claim the same single-writer path": "success",
"Flag docs affected by code changes": "success",
"Type Check · consumer gates": "success",
"Check PR Size": "success",
"The card this PR closes must claim this branch": "success",
"Governed Surface Queue Guard": "success",
"Spec property liveness": "success",
"Check Documentation Links": "success"
}
},
"files_changed": [
".changeset/22637-file-field-metadata-parent-derived.md",
"content/docs/permissions/attachments-access.mdx",
"content/docs/protocol/objectql/types.mdx",
"packages/objectql/src/engine-file-hydrate-parent-derived.test.ts",
"packages/objectql/src/engine.ts",
"packages/objectql/src/index.ts",
"packages/services/service-storage/src/field-owned-file-hydration-door-agreement.test.ts",
"packages/services/service-storage/src/storage-service-plugin.ts"
],
"deviations": [
"Cross-lane edits beyond packages/objectql. service-storage (2 paths) was declared on #6021 (comment 6097907456) and the two docs passages on #6023 (comment 6098008938), both by the PM at my mid-run request and before any edit. Both declarations were read back before editing.",
"Two first-pass gate reds were caused by my own new test code and are fixed: check:query-options-erasure went from 236 to 240 test sites and is back to 236; check:slot-lookup flagged getService typed any. A scratch measurement file briefly reddened check:cross-package-test-inputs, which is green after its deletion. The full gate list was re-run on the final head.",
"origin/main was merged into the branch (9931bb5) before pr_create, per AGENTS 10. Three later main commits touch none of these packages and were not merged.",
"The order's wait-for-CI clause was honoured with foreground polling of the check-runs REST read, no background watcher."
],
"mcp_calls": "0",
"api_writes": "3 REST writes, each through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft, #22697); label-write --assign os-tesla, POST /repos//issues/22697/assignees; post-stamped, POST /repos//issues/22637/comments (this report). git push is not counted.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed: packages/spec FileRefusedValueSchema TSDoc ('when the reader is refused the sys_file read that resolves it') and fileAccessDelegate TSDoc / .describe() ('authorizes downloads') are now incomplete rather than false. The marker now also means the parent-derived verdict did not allow, and the delegate is also consulted for a refused reader's metadata. They are left unchanged to avoid gen:schema / gen:docs and are named in the PR's Acceptance notes. Dedupe words: FileRefusedValueSchema TSDoc parent-derived · fileAccessDelegate describe metadata hydration.",
"carrier: none · noted, not filed: the full composition (real SecurityPlugin + StorageServicePlugin + REST) has no committed pin; it was measured once in a scratch run. A dogfood pin would sit in packages/qa (domain:cli). This is in the PR's Acceptance notes.",
"carrier: none · noted, not filed: by the ruling, hydration is narrower than the door. A file's uploader without sys_file read, reading a delegate-governed record whose delegate refuses them, sees the marker while the door serves them the bytes (the door's owner arm). This is in the PR's Acceptance notes."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
ACCEPT (seat review): PR #22697 at head
9931bb55d3. A reader refusedsys_fileread is served the metadata of a file the record being read owns, by the download door's own field-owned verdict (ruling B on #22624)domain:engine#2·session_01Bw3y2DWhT9RPnrmDsNqEVG(os-tesla), claim 6097826006 · 2026-10-10T15:32Z. Read against GitHub and the branch, not the report (os-dev-report 6098964328).Contract review at
CONTRACT_REVIEW_TIER: 6099114132, PASS on this head. It is owed because of theClause-②: yesline.Shape.
main. Line 1 isFixes #22637and line 2 isClause-②: yes, matching the claim.origin/main(git merge-tree).service-storageon [PM seat] domain:services — ⏳ vacant #6021 (6097907456) and two docs passages on [PM seat] domain:devx @ objectstack — 🟢 zhuangjianguo · session_012zwsLMxQBwHUYJgPyrpEvx #6023 (6098008938). The PR body lists both.The change, as read
objectql/src/engine.ts). On the refused arm that PR fix(objectql): a refused sys_file read marks the file field refused instead of reading as no file #22620 added,resolveFileReferencesmakes one system-contextsys_fileread, limited to the held ids whoseref_object/ref_idname a record in the result.{ id, metadataRefused: true }.service-storageextracts the field-owned arm ofbuildFileReadAuthorizerinto the module-privatereadableFieldOwners, with the delegate arm and the fail-closed handling.start()registers it through the new seamregisterFieldOwnedFileReadAuthorizer(exported typeFieldOwnedFileReadAuthorizer), besideregisterHeldFileResolver.sys_file's own read rule, directsys_filequeries (pinned 403) andpublic_read(pinned).engine-file-hydrate-parent-derived.test.ts(16) covers field-owned, copied id, attachment-only, delegate deny and allow,multiple, and mixed arrays.field-owned-file-hydration-door-agreement.test.ts(9) asks the door and the record read the same question on one realObjectQL+SqlDriverfixture.objectqlandservice-storage, both minor.CI on
9931bb55d3. 35 runs: 33 success, 2 skipped (Console Pin Gate and Packed-tarball smoke, roster skips), 0 failure. All seven required contexts are success. The Vercel status is success.Boundary.
sys_fileread, on a record whose delegate refuses them, sees the marker while the door serves them the bytes through its owner arm. That is within the ruling, which cites the field-owned arm only.fileAccessDelegate's.describe()andFileRefusedValueSchema's TSDoc now understate what they cover. Filed as spec:fileAccessDelegate's.describe()andFileRefusedValueSchema's TSDoc still describe the download door only — after #22637 the delegate also decides a refused reader's file metadata #22698 fordomain:spec, since the fix regenerates the schema and the docs.packages/qa,domain:cli). It is optional and named in the PR's Acceptance notes.Next: ready, then auto-merge, in this act. On merge,
Fixes #22637closes this card.objectstack-fleet commentedon Oct 10, 2026
Landed: PR #22697 →
1bb1da1bcbthrough the merge queue, at 2026-10-10T15:58Z.domain:engine#2·session_01Bw3y2DWhT9RPnrmDsNqEVG· 2026-10-10T15:59Z.origin/main: all 8 files are blob-identical to the reviewed head9931bb55d3.service-storage'sreadableFieldOwnerswith its registration, both new pin files, the two docs passages and the changeset.1bb1da1bcbis an ancestor oforigin/main.Fixes #22637closed it as completed.pm:dispatchedis removed in this act.fileAccessDelegate's.describe()andFileRefusedValueSchema's TSDoc still describe the download door only — after #22637 the delegate also decides a refused reader's file metadata #22698 (domain:spec), thefileAccessDelegate.describe()and theFileRefusedValueSchemaTSDoc.nextprereleases (18.0.0-next.N) ahead of 18.0 GA — the maintainer's ruling, since 17.x ships no new release objectui#12030 (6095969443) already carries the restricted-state rendering. After this landing, the marker remains only for ids the record being read does not own, or that the verdict refuses.