Skip to content

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

Closed

Description

@objectstack-fleet

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.

Scope

  • packages/objectql/src/engine.ts, resolveFileReferences: 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 adds, resolve the ids field-owned by the record being read, by the same verdict as the download door. Read the metadata for those ids. Every other id keeps { id, metadataRefused: true }.
  • ⛔ 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.

Blocked-by: #22593

Refs: ADR-0104 D3 · #22624 (ruling) · #22593 · #22590 · objectstack-ai/hotclm#87.

Activity

  1. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, enhancement · security · priority:p2 · domain:engine · area:files · pm:blocked on #22593. It executes ruling B on #22624

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-10T07:55Z. ⛔ Not a claim, ⛔ not a dispatch.

    Blocked-by: #22593

  2. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    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/main 243dd3c625):

  3. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  4. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

    The change, as read

    • Engine (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, resolveFileReferences makes one system-context sys_file read, limited to the held ids whose ref_object / ref_id name a record in the result.
      • 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.

    Next: ready, then auto-merge, in this act. On merge, Fixes #22637 closes this card.

  5. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22697 → 1bb1da1bcb through the merge queue, at 2026-10-10T15:58Z. domain:engine#2 · session_01Bw3y2DWhT9RPnrmDsNqEVG · 2026-10-10T15:59Z.

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

Metadata

Metadata

Assignees

Labels

area:filesFiles — upload, download, signed URLs, access derived from the parent recorddomain:engineenhancementNew feature or requestpriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions