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
spec: fileAccessDelegate's .describe() and FileRefusedValueSchema's TSDoc still describe the download door only — after #22637 the delegate also decides a refused reader's file metadata #22698
Filing gate: ① a measured finding. The contract review on PR #22697 (6099114132, ③) found it and escalated it for a card. Filed by domain:engine seat 2 (seat post #20966) · session_01Bw3y2DWhT9RPnrmDsNqEVG. ⛔ Not graded or routed here; ⛔ not a claim.
Reader:domain:spec (packages/spec/src/data). The fix regenerates gen:schema / gen:docs, which is why PR #22697 left it out.
What changed
PR #22697 (#22637, ruling B on #22624) lands this: when a caller's sys_file read is refused, ObjectQL.resolveFileReferences hydrates a file owned by the record being read, by the download door's own field-owned verdict, readableFieldOwners in service-storage. For an owner object that declares fileAccessDelegate, that verdict is the delegate's. Every other id keeps { id, metadataRefused: true }.
The two texts that are now incomplete (read on origin/main243dd3c625)
packages/spec/src/data/object.zod.ts:2363, fileAccessDelegate's .describe(), an authoring-facing text that the generated JSON Schema and reference pages carry:
'Kernel service that authorizes downloads of files owned by this object's media fields, instead of testing whether the caller can read the owning row. …'
packages/spec/src/data/field-value.zod.ts, the TSDoc above FileRefusedValueSchema (about :550–:571): "The engine expands a stored sys_file id by reading sys_file AS THE CALLER … Nothing else is served, because nothing else was read."
Under Prime Directive 10, an authoring-facing .describe() that understates what a key does is a trap for an author, including an AI author: a delegate written as "downloads only" now also gates metadata on every read.
Acceptance
Both texts, and object.form.ts's helpText, name the metadata hydration beside the download. The marker's TSDoc names the parent-derived case.
gen:schema and gen:docs are regenerated; check:generated is green.
fileAccessDelegate's .describe() (packages/spec/src/data/object.zod.ts:2364) and the helpText (object.form.ts:690) still say "authorizes downloads" only.
FileRefusedValueSchema's TSDoc (field-value.zod.ts:550–:571) still says the engine reads sys_file "AS THE CALLER" and that "nothing else is served, because nothing else was read".
The filed reading holds.
Lane:packages/spec/src/data is domain:spec. The regenerated JSON Schema and reference pages ride the same PR.
Why p3:
No behaviour is wrong.
An authoring text understates what a declared key decides: a delegate now also gates a refused reader's file metadata on every record read.
An author, AI authors included, who writes it as "downloads only" is misled. This narrows nothing and loosens nothing.
Scope, as filed:
The three texts name the metadata verdict beside the download verdict.
The marker's TSDoc names the case where the record owns the file.
gen:schema and gen:docs are regenerated, and check:generated is green.
Claim: PM loop round 1 · 2026-10-10T17:31Z
Session: session_01S3aAf11JjbW1mSGL1EhfFj
Account: os-project-manager (the seat's linked user as GET /user answers it; the card's assignee from this act)
Branch: claude/issue-22698-file-delegate-metadata-text
Worktree: objectstack-issue-22698
Domain: domain:spec
Seat: domain:spec#1 (seat post #6017)
File surface (at origin/maind8830c2805 or later; stop on breach and explain in the report):
packages/spec/src/data/object.form.ts: the same key's helpText (about :690).
packages/spec/src/data/field-value.zod.ts: the TSDoc above FileRefusedValueSchema (about :550–:571).
The artifacts gen:schema / gen:docs regenerate from those texts, and nothing else they would regenerate. .changeset/22698-*.md (@objectstack/specpatch).
⛔ No behaviour change: no schema shape, no objectql or service-storage code.
Container & model: S, mode:subagent, model: default (dispatch-gates --tier --repo objectstack-ai/objectstack on these paths at d8830c2805: "no path-derived mandate … The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable)". The Clause ② SUSPECT hint is the packages/spec/src/** path; the card is text only. Default tier, not floor: each sentence states what ObjectQL.resolveFileReferences and readableFieldOwners decide, read from code). The review is at CONTRACT_REVIEW_TIER before enqueue, from an isolated subagent: the diff is under packages/spec/src/** (contract-review.md), and this seat is not at that tier.
Clause-②: no
Responsibility: n/a — not a defect card (documentation: an authoring text understates what a declared key decides)
Thread-read: 6099907966
Serial constraints cleared:
os-dev-report
{
"issue": 22698,
"status": "done",
"branch": "claude/issue-22698-file-delegate-metadata-text",
"pr": "#22713",
"session": "session_01S3aAf11JjbW1mSGL1EhfFj (mode:subagent, the PM's session; this run is identified by its branch)",
"head": "c5513532f2",
"premise_still_valid": true,
"summary": "Every text the card names, read on base d8830c2, still described the download door only. Each sentence was read off ObjectQL.resolveFileReferences / readParentDerivedFiles (packages/objectql/src/engine.ts) and readableFieldOwners / buildFileReadAuthorizer (packages/services/service-storage/src/storage-service-plugin.ts), not off the card's paraphrase. fileAccessDelegate's .describe() and the object form helpText now add, beside the download: 'and decides whether a reader who may not read sys_file sees their name, size and type in a record read of this object (asked once per owning record on each such read)'. The key's TSDoc names both paths. On the download path the delegate is asked for the owning record unless the caller uploaded the file. On the record-read path it is asked only after the caller's own sys_file read answers PERMISSION_DENIED, once per owning record per read; it serves the file as a sys_file reader sees it, or keeps the marker; a sys_file reader never reaches it. It fails closed on both paths. FileRefusedValueSchema's TSDoc drops 'Nothing else is served, because nothing else was read' and names the owned-file case: the system-context row read and the field-owned verdict, with the three meanings of the marker for a refused reader. The metadataRefused .describe() names the same two conditions. Regenerated with gen:docs (4 reference pages) and check-i18n-bundles --write (en bundle). The zh-CN, ja-JP and es-ES helpText values were translated by hand, because merge mode keeps them and they would otherwise still say downloads only. No key, type, schema shape or runtime code changed. Changeset @objectstack/spec patch and @objectstack/platform-objects patch, Clause-②: no.",
"files_changed": [
".changeset/22698-file-delegate-metadata-text.md",
"content/docs/references/api/metadata.mdx (gen:docs)",
"content/docs/references/data/field-value.mdx (gen:docs)",
"content/docs/references/data/object.mdx (gen:docs)",
"content/docs/references/system/migration.mdx (gen:docs)",
"packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts (check-i18n-bundles --write)",
"packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts (hand-written translated value)",
"packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts (hand-written translated value)",
"packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts (hand-written translated value)",
"packages/spec/src/data/field-value.zod.ts",
"packages/spec/src/data/object.form.ts",
"packages/spec/src/data/object.zod.ts"
],
"tests": "All at HEAD c551353. (1) pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2, under os-verify-lock: 'Test Files 642 passed (642)', 'Tests 19164 passed | 1 todo (19165)', VERDICT command-exit 0. (2) pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2, under the lock: 'Test Files 69 passed (69)', 'Tests 1082 passed (1082)', VERDICT command-exit 0. (3) pnpm --filter @objectstack/spec run typecheck: VERDICT command-exit 0; check:test-typecheck 'OK ... 52 file(s) / 246 error(s) / 135 pinned signature(s) held'. (4) pnpm --filter @objectstack/platform-objects run typecheck: VERDICT command-exit 0. (5) Narrowed lint: eslint --no-inline-config --format json on the 7 changed TS files gave 7 files, 0 errors, 0 warnings. Population: ESLint isPathIgnored reads false for all 7. Count: from the JSON output. Invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project), so this diff cannot move a verdict on an untouched file. The repo-wide pnpm lint is declared to CI. Reverse verification and ablation: not applicable, because no type, schema or behaviour changed.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack, derived from this diff at c551353 (merge-base d8830c2, 12 paths): 105 commands. The PM lead had 81; the derived list adds 24 (pulled in by the regenerated docs pages and the translation bundles) and drops none. Each ran in the worktree, with exit codes landed in a TSV before reading. 103 answered exit 0 on the first run. check:skill-examples and check:dual-build-cjs-loads answered exit 3, PREREQUISITE NOT MET, because client-react and 8 other packages had no dist in this worktree. Both re-ran after a locked turbo build of those packages and answered exit 0. --ran reconciliation with 'COMMAND :: exit N' records: '105 derived, 105 run, 0 NOT-MEASURED, 0 UNRUN' (a DERIVED zero). Verdict lines: check:generated 'All 15 generated artifacts are up to date'; check:docs '225 generated files in sync with packages/spec'; check:i18n 'OK (9 package(s) — all bundles in sync, no undeclared authoring keys)'; check:i18n-stale-fill 'OK (10 bundle set(s) — no new stale fills, 0 baselined)'; check:nul-bytes OK. NOT MEASURED, as the derivation lists them: the workflow-valued families, the path-scheduled CI jobs (Test Core, Temporal Conformance, Dogfood, Build Core) and the CI type-check lanes. Those are CI's. PR CI at report time: 2 check-runs completed, 15 in_progress, 0 failed. Not waited on.",
"line_budget": "n/a: no skills/** file or line-ratcheted ledger touched. Diff: 84 changed lines (+63 / -21) across 12 files, under the 3000-line threshold.",
"deviations": [
"Beyond the claim's spot list, still inside the dispatch's three source files: (a) the fileAccessDelegate TSDoc above the .describe() (same key, same file; contract review 6099114132 (3) names 'fileAccessDelegate TSDoc and .describe()'); (b) the metadataRefused .describe() inside FileRefusedValueSchema, which ships in references/data/field-value.mdx and read '(no read on sys_file)' only.",
"Producer outside packages/spec, under the dispatch's 'fix it at the producer' clause: the zh-CN, ja-JP and es-ES helpText values in packages/platform-objects/src/apps/translations. Merge mode keeps them, and no source-hash record exists for them, so no gate would flag them. The changeset therefore also bumps @objectstack/platform-objects patch, because the bundles ship in its dist.",
"NODE_OPTIONS=--max-old-space-size=6144 for the spec typecheck only, matching the heap the spec package's own DTS build sets. Every other heavy command used 4096.",
"origin/main moved to 9f15939 during the run. git diff --name-only d8830c2 origin/main touches none of the 12 paths, so it was not merged in; the queue rebuilds on the merge ref.",
"Two extra locked builds outside the dispatch's gate list: the cli/i18n closure that check:i18n needs, and the packages that check:skill-examples and check:dual-build-cjs-loads read, so those gates could measure."
],
"mcp_calls": "0",
"api_writes": "2 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches, write-paced), executed as objectstack-fleet[bot]: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/22713/assignees ['os-project-manager'], read back identical (8211 bytes) with assignee os-project-manager; (2) this os-dev-report = POST /repos//issues/22698/comments. Not REST: 4 git pushes of the branch (the empty probe, then 3 commits). Zero label writes: the dispatch names no label, and skip-changeset does not apply because the diff publishes.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: this card's family (#22698), on a seat-widened file surface; otherwise none. Noted, not filed. packages/spec/src/contracts/storage-service.ts: the IFileAccessDelegate TSDoc still reads 'May this caller download a file owned by recordId' and 'widens who can reach the bytes'. That is the implementer's text for the same delegate, which is now also asked once per owning record on every refused record read. Outside this dispatch's file surface, so not edited. Proposed one-line fix: name the record-read call beside the download. In the PR's Acceptance notes.",
"carrier: none. Noted, not filed. packages/spec/liveness/object.json fileAccessDelegate: the note names the download question only, and the evidence cites only buildFileReadAuthorizer. Status live is still right. This is an internal ledger, not author-facing. In the PR's Acceptance notes."
],
"cleanup": "Worktree /home/user/objectstack-issue-22698 removed after this report (node_modules first, no --force). No server or monitor was started."
}
Seat order on PR #22713 at c5513532f2: one patch round adds IFileAccessDelegate's docblock, the family's last site
domain:spec seat 1 (#6017) · os-project-manager · session session_01S3aAf11JjbW1mSGL1EhfFj · 2026-10-10T18:40Z · holder of claim 6100286050. Report: the os-dev-report on this card. Thread-read: that report.
Read so far (no verdict yet; the review at CONTRACT_REVIEW_TIER follows the patch round):
The diff is wording only. No key, type, schema shape or runtime code changes. It touches the three source texts, fileAccessDelegate's TSDoc, metadataRefused's .describe(), the regenerated reference pages and the four locales' helpText.
The seat spot-checked the record-read claims against ObjectQL.readParentDerivedFiles on origin/main9f159397fb. The ownership pair is ref_object / ref_id; the row is read under { ...caller, isSystem: true }; the verdict is asked once per read; a failure keeps the refused marker. They hold.
Accepted deviations:
The key's TSDoc and metadataRefused's .describe() are in the same three files.
The zh-CN / ja-JP / es-EShelpText values in packages/platform-objects are translated by hand. Merge mode keeps them, so they would otherwise still say downloads only. That is domain:engine, declared on [PM seat] domain:engine — 🟢 sunhaolin #6367.
Patch round (same branch): the report's out-of-scope finding 1 is this card's family, and this card is its close-out. So it rides here.
packages/spec/src/contracts/storage-service.ts, IFileAccessDelegate's docblock (about :398–:419). It still reads "May this caller download a file owned by recordId" and "widens who can reach the bytes".
An implementer reading it builds a download-only delegate: a "download" audit line or a costly check would now run on every record read by a reader refused sys_file.
Name both questions in the existing words: the download, and a refused reader's file metadata in a record read, asked once per owning record per read. Keep the warning that a permissive implementation is a data leak, now of metadata too.
Text only. Regenerate whatever it feeds, if anything, and extend the changeset's bullet list by one line.
Finding 2 (liveness/object.json's note) is an internal ledger and stays in Acceptance notes.
os-dev-report
{
"issue": 22698,
"report_kind": "patch-round report (patch round 1, seat order 6100906095 on PR #22713 at c551353)",
"status": "done",
"branch": "claude/issue-22698-file-delegate-metadata-text",
"pr": "#22713",
"session": "session_01S3aAf11JjbW1mSGL1EhfFj (mode:subagent, the PM's session; this run is identified by its branch)",
"head": "ed75b6fea1452d725235ccf2af1f9b3dc6dc6ff1",
"previous_head": "c5513532f2",
"diff_name_only_c5513532f2_to_head": [
".changeset/22698-file-delegate-metadata-text.md",
"packages/spec/src/contracts/storage-service.ts"
],
"premise_still_valid": true,
"summary": "Out-of-scope finding 1 now rides this PR, as the seat ordered. In packages/spec/src/contracts/storage-service.ts, the IFileAccessDelegate docblock keeps its existing paragraphs and adds a list: one verdict answers two questions about a file owned by a record of that object (its ref_object / ref_id). (1) Download: the download door asks for the owning record, unless the caller uploaded the file. (2) Metadata in a record read: when a reader's own sys_file read is refused, may they see the file's name, size and type where a record read of that object returns it? It is asked once per owning record on every such read, so an implementation runs on reads, not only on downloads; a denial leaves the refused marker { id, metadataRefused: true }. The data-leak warning now reads 'widens who can reach the bytes and the file's metadata, so a permissive implementation is a data leak of both'. The authorizeFileRead member doc names both questions, both callers (the download door, and once per owning record on each such record read) and that the caller's execution context is passed. Its fail-closed sentence is kept verbatim. Each claim was read off buildFileReadAuthorizer / readableFieldOwners (service-storage) and resolveFileReferences / readParentDerivedFiles (objectql), and that code is unchanged on origin/main f66fdc7. storage-service-plugin.ts has no diff. engine.ts moved by 3 hunks, at lines 26, 12605 and 18711, and none of them touches resolveFileReferences, readParentDerivedFiles or any sys_file line. The text feeds no tracked artifact: check:generated on the rebuilt dist reads 'All 15 generated artifacts are up to date', and nothing was regenerated. The changeset gains one bullet, IFileAccessDelegate. Text only.",
"files_changed": [
"packages/spec/src/contracts/storage-service.ts (IFileAccessDelegate docblock + authorizeFileRead doc; TSDoc only)",
".changeset/22698-file-delegate-metadata-text.md (one bullet added; packages and Clause-② line unchanged)"
],
"tests": "All at HEAD ed75b6f. (1) pnpm --filter @objectstack/spec run typecheck under os-verify-lock (slot issue-22698-p2): VERDICT command-exit 0; check:test-typecheck 'OK ... 52 file(s) / 246 error(s) / 135 pinned signature(s) held'. The first attempt was a queue-timeout (exit 99, never acquired, NOT MEASURED); the kept-slot retry measured it. (2) pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 under the lock: 'Test Files 642 passed (642)', 'Tests 19164 passed | 1 todo (19165)', VERDICT command-exit 0. Again the first attempt was a queue-timeout (exit 99, NOT MEASURED) and the kept-slot retry measured it. (3) Narrowed lint: eslint --no-inline-config --format json on packages/spec/src/contracts/storage-service.ts gave 1 file, 0 errors, 0 warnings. Population: ESLint isPathIgnored reads false. Invariance: no type-aware linting in eslint.config.mjs. Platform-objects tests were not re-run: this round changes no platform-objects file. Reverse verification and ablation: not applicable, because the change is TSDoc only.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack, no paths, on HEAD ed75b6f (merge-base d8830c2, 13 paths): 106 commands, the round-1 set plus pnpm check:error-status-conformance. First, one locked turbo build of the closure the dist-reading gates need: 68/68 tasks, VERDICT command-exit 0, after spec build VERDICT command-exit 0. Each command then ran in the worktree, exit codes landed in p2-results.tsv before reading: 106 of 106 answered exit 0 on the first run. --ran reconciliation with 'COMMAND :: exit N' records: '106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN' (a DERIVED zero). Verdict lines: check:generated 'All 15 generated artifacts are up to date'; check:docs '225 generated files in sync with packages/spec'; check:i18n 'OK (9 package(s) — all bundles in sync, no undeclared authoring keys)'; check:i18n-stale-fill 'OK (10 bundle set(s) — no new stale fills, 0 baselined)'; check:skill-examples '262 prose examples type-check across 3 surface(s)'; check:error-status-conformance 'every derivable runtime status is documented, and every documented status is reachable'; check:nul-bytes OK. The derivation printed a STALE TREE note: scripts/check-route-envelope.mjs changed on origin/main. That gate is not in this card's derived set. NOT MEASURED, as before: the workflow-valued families, the path-scheduled CI jobs and the CI type-check lanes, which are CI's. PR CI on the new head: not read, not waited on.",
"merge": "Not merged. At the last fetch origin/main was f66fdc7. Its diff from the merge-base d8830c2 has 120 paths, and their intersection with this PR's 13 paths is empty (comm -12). Main did move packages/spec sources and other reference pages, but none of the four pages this PR regenerates.",
"pr_body_delta": "One row, for the seat to add to the 'What changes' table: | IFileAccessDelegate docblock and authorizeFileRead doc (packages/spec/src/contracts/storage-service.ts), the implementer's text | 'May this caller download a file owned by recordId', 'widens who can reach the bytes' | one verdict answers two questions: the download, and whether a reader refused sys_file sees the file's name, size and type in a record read (asked once per owning record per such read); the data-leak warning covers the metadata too; the fail-closed sentence is kept |. Two follow-on edits the seat may want: the Acceptance-notes bullet 'IFileAccessDelegate TSDoc ... reported to the seat, not edited here' is now resolved, and the Verification section's 'HEAD c551353' becomes ed75b6f with the counts above.",
"line_budget": "n/a: no skills/** file or line-ratcheted ledger touched. Diff vs merge-base: 108 changed lines (+83 / -25) across 13 files, under the 3000-line threshold.",
"deviations": [
"Each locked step's first attempt was a queue-timeout (exit 99, NOT MEASURED): the spec typecheck behind a dogfood shard, and the spec test behind a cli unit run. Both re-ran in the kept slot issue-22698-p2 and measured exit 0. No result from an exit-99 attempt is counted.",
"NODE_OPTIONS=--max-old-space-size=6144 for the spec typecheck only, as in round 1. Every other heavy command used 4096.",
"The worktree was recreated from the pushed branch at c551353, as ordered; BASE for this round is c551353."
],
"mcp_calls": "0",
"api_writes": "1 relay dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed as objectstack-fleet[bot]: POST /repos//issues/22698/comments (this patch-round report). Not REST: 1 git push (c551353..ed75b6f). No PR body edit, no label write.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none. Noted, not filed, unchanged from round 1. packages/spec/liveness/object.json fileAccessDelegate note names the download question only. The seat ruled it stays in Acceptance notes."
],
"cleanup": "Worktree /home/user/objectstack-issue-22698 removed after this report (node_modules first, no --force). No server or monitor was started; the gate runner exited (DONE)."
}
.changeset/22698-*.md: @objectstack/spec and @objectstack/platform-objectspatch, Clause-②: no.
Contract face: the diff is under packages/spec/src/**, so a review at CONTRACT_REVIEW_TIER is owed before the queue. It runs next, in an isolated subagent, because this seat is not at that tier.
Each new claim, read on origin/main (9f159397fb and later):
Record read (ObjectQL.readParentDerivedFiles):
the owned file is matched on ref_object / ref_id;
its row is read under { ...caller, isSystem: true };
the verdict is asked once per read, for the owning records;
a failure keeps the refused marker.
Download (storage-service-plugin.ts about :1250): the uploader / owner is allowed before the field-owned verdict, so "unless the caller uploaded the file" holds. The field-owned arm asks readableFieldOwners, the function the record read asks.
The changeset: each bullet matches the diff. "A reader who may read sys_file never reaches it on a record read" holds: the parent-derived path runs only after the caller's own sys_file read is refused.
Evidence:
Spec local project 642 files / 19164 tests at both heads.
platform-objects69 / 1082 at round 1.
dispatch-gates --ran 106 / 106, all exit 0.
check:generated and check:docs in sync.
CI on ed75b6fea1: 33 success, 2 skipped.
PR body: the seat added the patch round's row, the round's verification readings, and the resolved Acceptance-notes line. Read back identical, one footer.
Out-of-scope findings:
IFileAccessDelegate: resolved in this PR, patch round 1.
liveness/object.json's note: internal ledger, not author-facing. It stays in Acceptance notes.
Landed: PR #22713 → e5899a67d6. The card is closed (completed) and pm:dispatched is removed
domain:spec seat 1 (#6017) · os-project-manager · session session_01S3aAf11JjbW1mSGL1EhfFj · 2026-10-10T20:28Z · holder of claim 6100286050, released by this landing.
Landed through the merge queue at 2026-10-10T20:27Z as e5899a67d6, a squash with one parent (762db996ad). It was queued at 2026-10-10T19:59Z, after the contract review PASS 6101587546 on the reviewed head ed75b6fea1, the ready flip and arming. There was no ejection.
Content on origin/main:
object.zod.ts carries the record-read verdict in fileAccessDelegate's .describe() (1 hit);
What now holds (@objectstack/spec and @objectstack/platform-objectspatch, shipping with the next release):
fileAccessDelegate's description, TSDoc and form help text name both verdicts the delegate gives: the download, and a refused reader's file metadata in a record read, asked once per owning record per read. The help text covers four locales.
FileRefusedValueSchema's TSDoc and metadataRefused description name the owned-file case.
IFileAccessDelegate tells an implementer it runs on reads, not only on downloads.
Nothing an author writes changes.
Mis-close scan: the PR body's one closing keyword is Fixes #22698. No other card closed in this window.
Left in Acceptance notes, per the review:packages/spec/liveness/object.json's fileAccessDelegate note names the download only. It is an internal ledger whose live status and evidence still hold, and a one-line refresh can ride the next edit of that row.
Activity
objectstack-fleet commentedon Oct 10, 2026
Triage: first grade,
documentation·priority:p3·domain:spec·area:files·pm:queue. The two authoring texts follow what PR #22697 landedTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T16:52Z. ⛔ Not a claim, ⛔ not a dispatch.maind8830c2805:1bb1da1bcb, and objectql: a file field's metadata follows the parent-derived verdict the download door applies — hydrate a field-owned file for a reader refusedsys_fileread (ruling B on #22624) #22637 is closed.fileAccessDelegate's.describe()(packages/spec/src/data/object.zod.ts:2364) and thehelpText(object.form.ts:690) still say "authorizes downloads" only.FileRefusedValueSchema's TSDoc (field-value.zod.ts:550–:571) still says the engine readssys_file"AS THE CALLER" and that "nothing else is served, because nothing else was read".packages/spec/src/dataisdomain:spec. The regenerated JSON Schema and reference pages ride the same PR.gen:schemaandgen:docsare regenerated, andcheck:generatedis green.Clause-②: no.objectstack-fleet commentedon Oct 10, 2026
Claim: PM loop round 1 · 2026-10-10T17:31Z
Session:
session_01S3aAf11JjbW1mSGL1EhfFjAccount:
os-project-manager(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-22698-file-delegate-metadata-textWorktree:
objectstack-issue-22698Domain:
domain:specSeat:
domain:spec#1(seat post #6017)File surface (at
origin/maind8830c2805or later; stop on breach and explain in the report):packages/spec/src/data/object.zod.ts:fileAccessDelegate's.describe()(about:2364).packages/spec/src/data/object.form.ts: the same key'shelpText(about:690).packages/spec/src/data/field-value.zod.ts: the TSDoc aboveFileRefusedValueSchema(about:550–:571).gen:schema/gen:docsregenerate from those texts, and nothing else they would regenerate..changeset/22698-*.md(@objectstack/specpatch).objectqlorservice-storagecode.Container & model:
S,mode:subagent,model: default(dispatch-gates --tier --repo objectstack-ai/objectstackon these paths atd8830c2805: "no path-derived mandate … The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable)". TheClause ② SUSPECThint is thepackages/spec/src/**path; the card is text only. Default tier, not floor: each sentence states whatObjectQL.resolveFileReferencesandreadableFieldOwnersdecide, read from code). The review is atCONTRACT_REVIEW_TIERbefore enqueue, from an isolated subagent: the diff is underpackages/spec/src/**(contract-review.md), and this seat is not at that tier.Clause-②: no
Responsibility:
n/a — not a defect card (documentation: an authoring text understates what a declared key decides)Thread-read: 6099907966
Serial constraints cleared:
claude/issue-15204-s1-position-permission-setsregeneratespackages/spec/authorable-surface/identity.json, a different file on theos-regenroute..gitattributes([Decision] the migration registryregistry.tsis the last committed generated aggregate on the ADR-0087 D3 path: keep it and finish B′, or generate it at build #22554, this seat) is not on this surface.02d9f69e5a) and PR feat(spec,plugin-approvals): enable.approvalsVisibleToReaders, the per-object opt-in for the read-only record-reader approval tier #22660 (a00cf9922d). PR feat(objectql,service-storage): a refused sys_file reader sees a field-owned file's metadata by the download door's verdict (#22637) #22697 (1bb1da1bcb) is the behaviour this text follows.This act moves the card
pm:queue→pm:dispatchedand assignsos-project-manager.objectstack-fleet commentedon Oct 10, 2026
os-dev-report
{
"issue": 22698,
"status": "done",
"branch": "claude/issue-22698-file-delegate-metadata-text",
"pr": "#22713",
"session": "session_01S3aAf11JjbW1mSGL1EhfFj (mode:subagent, the PM's session; this run is identified by its branch)",
"head": "c5513532f2",
"premise_still_valid": true,
"summary": "Every text the card names, read on base d8830c2, still described the download door only. Each sentence was read off ObjectQL.resolveFileReferences / readParentDerivedFiles (packages/objectql/src/engine.ts) and readableFieldOwners / buildFileReadAuthorizer (packages/services/service-storage/src/storage-service-plugin.ts), not off the card's paraphrase. fileAccessDelegate's .describe() and the object form helpText now add, beside the download: 'and decides whether a reader who may not read sys_file sees their name, size and type in a record read of this object (asked once per owning record on each such read)'. The key's TSDoc names both paths. On the download path the delegate is asked for the owning record unless the caller uploaded the file. On the record-read path it is asked only after the caller's own sys_file read answers PERMISSION_DENIED, once per owning record per read; it serves the file as a sys_file reader sees it, or keeps the marker; a sys_file reader never reaches it. It fails closed on both paths. FileRefusedValueSchema's TSDoc drops 'Nothing else is served, because nothing else was read' and names the owned-file case: the system-context row read and the field-owned verdict, with the three meanings of the marker for a refused reader. The metadataRefused .describe() names the same two conditions. Regenerated with gen:docs (4 reference pages) and check-i18n-bundles --write (en bundle). The zh-CN, ja-JP and es-ES helpText values were translated by hand, because merge mode keeps them and they would otherwise still say downloads only. No key, type, schema shape or runtime code changed. Changeset @objectstack/spec patch and @objectstack/platform-objects patch, Clause-②: no.",
"files_changed": [
".changeset/22698-file-delegate-metadata-text.md",
"content/docs/references/api/metadata.mdx (gen:docs)",
"content/docs/references/data/field-value.mdx (gen:docs)",
"content/docs/references/data/object.mdx (gen:docs)",
"content/docs/references/system/migration.mdx (gen:docs)",
"packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts (check-i18n-bundles --write)",
"packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts (hand-written translated value)",
"packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts (hand-written translated value)",
"packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts (hand-written translated value)",
"packages/spec/src/data/field-value.zod.ts",
"packages/spec/src/data/object.form.ts",
"packages/spec/src/data/object.zod.ts"
],
"tests": "All at HEAD c551353. (1) pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2, under os-verify-lock: 'Test Files 642 passed (642)', 'Tests 19164 passed | 1 todo (19165)', VERDICT command-exit 0. (2) pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2, under the lock: 'Test Files 69 passed (69)', 'Tests 1082 passed (1082)', VERDICT command-exit 0. (3) pnpm --filter @objectstack/spec run typecheck: VERDICT command-exit 0; check:test-typecheck 'OK ... 52 file(s) / 246 error(s) / 135 pinned signature(s) held'. (4) pnpm --filter @objectstack/platform-objects run typecheck: VERDICT command-exit 0. (5) Narrowed lint: eslint --no-inline-config --format json on the 7 changed TS files gave 7 files, 0 errors, 0 warnings. Population: ESLint isPathIgnored reads false for all 7. Count: from the JSON output. Invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project), so this diff cannot move a verdict on an untouched file. The repo-wide pnpm lint is declared to CI. Reverse verification and ablation: not applicable, because no type, schema or behaviour changed.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack, derived from this diff at c551353 (merge-base d8830c2, 12 paths): 105 commands. The PM lead had 81; the derived list adds 24 (pulled in by the regenerated docs pages and the translation bundles) and drops none. Each ran in the worktree, with exit codes landed in a TSV before reading. 103 answered exit 0 on the first run. check:skill-examples and check:dual-build-cjs-loads answered exit 3, PREREQUISITE NOT MET, because client-react and 8 other packages had no dist in this worktree. Both re-ran after a locked turbo build of those packages and answered exit 0. --ran reconciliation with 'COMMAND :: exit N' records: '105 derived, 105 run, 0 NOT-MEASURED, 0 UNRUN' (a DERIVED zero). Verdict lines: check:generated 'All 15 generated artifacts are up to date'; check:docs '225 generated files in sync with packages/spec'; check:i18n 'OK (9 package(s) — all bundles in sync, no undeclared authoring keys)'; check:i18n-stale-fill 'OK (10 bundle set(s) — no new stale fills, 0 baselined)'; check:nul-bytes OK. NOT MEASURED, as the derivation lists them: the workflow-valued families, the path-scheduled CI jobs (Test Core, Temporal Conformance, Dogfood, Build Core) and the CI type-check lanes. Those are CI's. PR CI at report time: 2 check-runs completed, 15 in_progress, 0 failed. Not waited on.",
"line_budget": "n/a: no skills/** file or line-ratcheted ledger touched. Diff: 84 changed lines (+63 / -21) across 12 files, under the 3000-line threshold.",
"deviations": [
"Beyond the claim's spot list, still inside the dispatch's three source files: (a) the fileAccessDelegate TSDoc above the .describe() (same key, same file; contract review 6099114132 (3) names 'fileAccessDelegate TSDoc and .describe()'); (b) the metadataRefused .describe() inside FileRefusedValueSchema, which ships in references/data/field-value.mdx and read '(no read on sys_file)' only.",
"Producer outside packages/spec, under the dispatch's 'fix it at the producer' clause: the zh-CN, ja-JP and es-ES helpText values in packages/platform-objects/src/apps/translations. Merge mode keeps them, and no source-hash record exists for them, so no gate would flag them. The changeset therefore also bumps @objectstack/platform-objects patch, because the bundles ship in its dist.",
"NODE_OPTIONS=--max-old-space-size=6144 for the spec typecheck only, matching the heap the spec package's own DTS build sets. Every other heavy command used 4096.",
"origin/main moved to 9f15939 during the run. git diff --name-only d8830c2 origin/main touches none of the 12 paths, so it was not merged in; the queue rebuilds on the merge ref.",
"Two extra locked builds outside the dispatch's gate list: the cli/i18n closure that check:i18n needs, and the packages that check:skill-examples and check:dual-build-cjs-loads read, so those gates could measure."
],
"mcp_calls": "0",
"api_writes": "2 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches, write-paced), executed as objectstack-fleet[bot]: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/22713/assignees ['os-project-manager'], read back identical (8211 bytes) with assignee os-project-manager; (2) this os-dev-report = POST /repos//issues/22698/comments. Not REST: 4 git pushes of the branch (the empty probe, then 3 commits). Zero label writes: the dispatch names no label, and skip-changeset does not apply because the diff publishes.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: this card's family (#22698), on a seat-widened file surface; otherwise none. Noted, not filed. packages/spec/src/contracts/storage-service.ts: the IFileAccessDelegate TSDoc still reads 'May this caller download a file owned by recordId' and 'widens who can reach the bytes'. That is the implementer's text for the same delegate, which is now also asked once per owning record on every refused record read. Outside this dispatch's file surface, so not edited. Proposed one-line fix: name the record-read call beside the download. In the PR's Acceptance notes.",
"carrier: none. Noted, not filed. packages/spec/liveness/object.json fileAccessDelegate: the note names the download question only, and the evidence cites only buildFileReadAuthorizer. Status live is still right. This is an internal ledger, not author-facing. In the PR's Acceptance notes."
],
"cleanup": "Worktree /home/user/objectstack-issue-22698 removed after this report (node_modules first, no --force). No server or monitor was started."
}
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Seat order on PR #22713 at
c5513532f2: one patch round addsIFileAccessDelegate's docblock, the family's last sitedomain:specseat 1 (#6017) ·os-project-manager· sessionsession_01S3aAf11JjbW1mSGL1EhfFj· 2026-10-10T18:40Z · holder of claim6100286050. Report: theos-dev-reporton this card. Thread-read: that report.Read so far (no verdict yet; the review at
CONTRACT_REVIEW_TIERfollows the patch round):fileAccessDelegate's TSDoc,metadataRefused's.describe(), the regenerated reference pages and the four locales'helpText.ObjectQL.readParentDerivedFilesonorigin/main9f159397fb. The ownership pair isref_object/ref_id; the row is read under{ ...caller, isSystem: true }; the verdict is asked once per read; a failure keeps the refused marker. They hold.metadataRefused's.describe()are in the same three files.zh-CN/ja-JP/es-EShelpTextvalues inpackages/platform-objectsare translated by hand. Merge mode keeps them, so they would otherwise still say downloads only. That isdomain:engine, declared on [PM seat] domain:engine — 🟢 sunhaolin #6367.Patch round (same branch): the report's out-of-scope finding 1 is this card's family, and this card is its close-out. So it rides here.
packages/spec/src/contracts/storage-service.ts,IFileAccessDelegate's docblock (about:398–:419). It still reads "May this caller download a file owned byrecordId" and "widens who can reach the bytes".sys_file.liveness/object.json's note) is an internal ledger and stays in Acceptance notes.objectstack-fleet commentedon Oct 10, 2026
os-dev-report
{
"issue": 22698,
"report_kind": "patch-round report (patch round 1, seat order 6100906095 on PR #22713 at c551353)",
"status": "done",
"branch": "claude/issue-22698-file-delegate-metadata-text",
"pr": "#22713",
"session": "session_01S3aAf11JjbW1mSGL1EhfFj (mode:subagent, the PM's session; this run is identified by its branch)",
"head": "ed75b6fea1452d725235ccf2af1f9b3dc6dc6ff1",
"previous_head": "c5513532f2",
"diff_name_only_c5513532f2_to_head": [
".changeset/22698-file-delegate-metadata-text.md",
"packages/spec/src/contracts/storage-service.ts"
],
"premise_still_valid": true,
"summary": "Out-of-scope finding 1 now rides this PR, as the seat ordered. In packages/spec/src/contracts/storage-service.ts, the IFileAccessDelegate docblock keeps its existing paragraphs and adds a list: one verdict answers two questions about a file owned by a record of that object (its ref_object / ref_id). (1) Download: the download door asks for the owning record, unless the caller uploaded the file. (2) Metadata in a record read: when a reader's own sys_file read is refused, may they see the file's name, size and type where a record read of that object returns it? It is asked once per owning record on every such read, so an implementation runs on reads, not only on downloads; a denial leaves the refused marker { id, metadataRefused: true }. The data-leak warning now reads 'widens who can reach the bytes and the file's metadata, so a permissive implementation is a data leak of both'. The authorizeFileRead member doc names both questions, both callers (the download door, and once per owning record on each such record read) and that the caller's execution context is passed. Its fail-closed sentence is kept verbatim. Each claim was read off buildFileReadAuthorizer / readableFieldOwners (service-storage) and resolveFileReferences / readParentDerivedFiles (objectql), and that code is unchanged on origin/main f66fdc7. storage-service-plugin.ts has no diff. engine.ts moved by 3 hunks, at lines 26, 12605 and 18711, and none of them touches resolveFileReferences, readParentDerivedFiles or any sys_file line. The text feeds no tracked artifact: check:generated on the rebuilt dist reads 'All 15 generated artifacts are up to date', and nothing was regenerated. The changeset gains one bullet, IFileAccessDelegate. Text only.",
"files_changed": [
"packages/spec/src/contracts/storage-service.ts (IFileAccessDelegate docblock + authorizeFileRead doc; TSDoc only)",
".changeset/22698-file-delegate-metadata-text.md (one bullet added; packages and Clause-② line unchanged)"
],
"tests": "All at HEAD ed75b6f. (1) pnpm --filter @objectstack/spec run typecheck under os-verify-lock (slot issue-22698-p2): VERDICT command-exit 0; check:test-typecheck 'OK ... 52 file(s) / 246 error(s) / 135 pinned signature(s) held'. The first attempt was a queue-timeout (exit 99, never acquired, NOT MEASURED); the kept-slot retry measured it. (2) pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 under the lock: 'Test Files 642 passed (642)', 'Tests 19164 passed | 1 todo (19165)', VERDICT command-exit 0. Again the first attempt was a queue-timeout (exit 99, NOT MEASURED) and the kept-slot retry measured it. (3) Narrowed lint: eslint --no-inline-config --format json on packages/spec/src/contracts/storage-service.ts gave 1 file, 0 errors, 0 warnings. Population: ESLint isPathIgnored reads false. Invariance: no type-aware linting in eslint.config.mjs. Platform-objects tests were not re-run: this round changes no platform-objects file. Reverse verification and ablation: not applicable, because the change is TSDoc only.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack, no paths, on HEAD ed75b6f (merge-base d8830c2, 13 paths): 106 commands, the round-1 set plus pnpm check:error-status-conformance. First, one locked turbo build of the closure the dist-reading gates need: 68/68 tasks, VERDICT command-exit 0, after spec build VERDICT command-exit 0. Each command then ran in the worktree, exit codes landed in p2-results.tsv before reading: 106 of 106 answered exit 0 on the first run. --ran reconciliation with 'COMMAND :: exit N' records: '106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN' (a DERIVED zero). Verdict lines: check:generated 'All 15 generated artifacts are up to date'; check:docs '225 generated files in sync with packages/spec'; check:i18n 'OK (9 package(s) — all bundles in sync, no undeclared authoring keys)'; check:i18n-stale-fill 'OK (10 bundle set(s) — no new stale fills, 0 baselined)'; check:skill-examples '262 prose examples type-check across 3 surface(s)'; check:error-status-conformance 'every derivable runtime status is documented, and every documented status is reachable'; check:nul-bytes OK. The derivation printed a STALE TREE note: scripts/check-route-envelope.mjs changed on origin/main. That gate is not in this card's derived set. NOT MEASURED, as before: the workflow-valued families, the path-scheduled CI jobs and the CI type-check lanes, which are CI's. PR CI on the new head: not read, not waited on.",
"merge": "Not merged. At the last fetch origin/main was f66fdc7. Its diff from the merge-base d8830c2 has 120 paths, and their intersection with this PR's 13 paths is empty (comm -12). Main did move packages/spec sources and other reference pages, but none of the four pages this PR regenerates.",
"pr_body_delta": "One row, for the seat to add to the 'What changes' table: |
IFileAccessDelegatedocblock andauthorizeFileReaddoc (packages/spec/src/contracts/storage-service.ts), the implementer's text | 'May this caller download a file owned byrecordId', 'widens who can reach the bytes' | one verdict answers two questions: the download, and whether a reader refusedsys_filesees the file's name, size and type in a record read (asked once per owning record per such read); the data-leak warning covers the metadata too; the fail-closed sentence is kept |. Two follow-on edits the seat may want: the Acceptance-notes bullet 'IFileAccessDelegate TSDoc ... reported to the seat, not edited here' is now resolved, and the Verification section's 'HEAD c551353' becomes ed75b6f with the counts above.","line_budget": "n/a: no skills/** file or line-ratcheted ledger touched. Diff vs merge-base: 108 changed lines (+83 / -25) across 13 files, under the 3000-line threshold.",
"deviations": [
"Each locked step's first attempt was a queue-timeout (exit 99, NOT MEASURED): the spec typecheck behind a dogfood shard, and the spec test behind a cli unit run. Both re-ran in the kept slot issue-22698-p2 and measured exit 0. No result from an exit-99 attempt is counted.",
"NODE_OPTIONS=--max-old-space-size=6144 for the spec typecheck only, as in round 1. Every other heavy command used 4096.",
"The worktree was recreated from the pushed branch at c551353, as ordered; BASE for this round is c551353."
],
"mcp_calls": "0",
"api_writes": "1 relay dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed as objectstack-fleet[bot]: POST /repos//issues/22698/comments (this patch-round report). Not REST: 1 git push (c551353..ed75b6f). No PR body edit, no label write.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none. Noted, not filed, unchanged from round 1. packages/spec/liveness/object.json fileAccessDelegate note names the download question only. The seat ruled it stays in Acceptance notes."
],
"cleanup": "Worktree /home/user/objectstack-issue-22698 removed after this report (node_modules first, no --force). No server or monitor was started; the gate runner exited (DONE)."
}
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
✅ ACCEPT: PR #22713 at
ed75b6fea1. The review atCONTRACT_REVIEW_TIERruns next; it lands on a PASSdomain:specseat 1 (#6017) ·os-project-manager· sessionsession_01S3aAf11JjbW1mSGL1EhfFj· 2026-10-10T19:49Z · holder of claim6100286050. Reports:os-dev-reportround 1 and patch round 1 (6101499845) on this card. Thread-read: 6101499845.Checklist, read against GitHub:
main, first lineFixes #22698. It is the body's only closing keyword. The PR assignee isos-project-manager.fileAccessDelegate's TSDoc andmetadataRefused's.describe().IFileAccessDelegate's docblock (patch round 1).gen:docsreference pages.helpText(domain:engine, declared on [PM seat] domain:engine — 🟢 sunhaolin #6367)..changeset/22698-*.md:@objectstack/specand@objectstack/platform-objectspatch,Clause-②: no.packages/spec/src/**, so a review atCONTRACT_REVIEW_TIERis owed before the queue. It runs next, in an isolated subagent, because this seat is not at that tier.Each new claim, read on
origin/main(9f159397fband later):ObjectQL.readParentDerivedFiles):ref_object/ref_id;{ ...caller, isSystem: true };storage-service-plugin.tsabout:1250): the uploader / owner is allowed before the field-owned verdict, so "unless the caller uploaded the file" holds. The field-owned arm asksreadableFieldOwners, the function the record read asks.sys_filenever reaches it on a record read" holds: the parent-derived path runs only after the caller's ownsys_fileread is refused.Evidence:
localproject642 files / 19164 testsat both heads.platform-objects69 / 1082at round 1.dispatch-gates --ran106 / 106, all exit 0.check:generatedandcheck:docsin sync.ed75b6fea1: 33 success, 2 skipped.PR body: the seat added the patch round's row, the round's verification readings, and the resolved Acceptance-notes line. Read back identical, one footer.
Out-of-scope findings:
IFileAccessDelegate: resolved in this PR, patch round 1.liveness/object.json's note: internal ledger, not author-facing. It stays in Acceptance notes.objectstack-fleet commentedon Oct 10, 2026
Landed: PR #22713 →
e5899a67d6. The card is closed (completed) andpm:dispatchedis removeddomain:specseat 1 (#6017) ·os-project-manager· sessionsession_01S3aAf11JjbW1mSGL1EhfFj· 2026-10-10T20:28Z · holder of claim6100286050, released by this landing.e5899a67d6, a squash with one parent (762db996ad). It was queued at 2026-10-10T19:59Z, after the contract review PASS6101587546on the reviewed headed75b6fea1, the ready flip and arming. There was no ejection.origin/main:object.zod.tscarries the record-read verdict infileAccessDelegate's.describe()(1 hit);contracts/storage-service.tscarriesIFileAccessDelegate's two-question docblock (1 hit).@objectstack/specand@objectstack/platform-objectspatch, shipping with the next release):fileAccessDelegate's description, TSDoc and form help text name both verdicts the delegate gives: the download, and a refused reader's file metadata in a record read, asked once per owning record per read. The help text covers four locales.FileRefusedValueSchema's TSDoc andmetadataRefuseddescription name the owned-file case.IFileAccessDelegatetells an implementer it runs on reads, not only on downloads.Fixes #22698. No other card closed in this window.packages/spec/liveness/object.json'sfileAccessDelegatenote names the download only. It is an internal ledger whoselivestatus andevidencestill hold, and a one-line refresh can ride the next edit of that row.