Repository navigation
docs(spec): fileAccessDelegate and the refused file marker name the record-read verdict beside the download (#22698) - #22713
Conversation
…ecord-read verdict beside the download fileAccessDelegate's describe, its TSDoc and the object form's helpText now say the delegate also decides whether a reader who may not read sys_file sees a field-owned file's name, size and type in a record read (asked once per owning record per read). FileRefusedValueSchema's TSDoc and its metadataRefused describe name the case where the record owns the file and the download verdict allows it. Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
… and metadataRefused describes Output of `pnpm --filter @objectstack/spec gen:docs`; no hand edit. Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
…cord-read verdict in all four locales `en` is the output of `node scripts/check-i18n-bundles.mjs --write --filter=platform-objects`. The zh-CN, ja-JP and es-ES values are hand-written translations that merge mode keeps, so they are updated by hand to say the same thing. Changeset: @objectstack/spec and @objectstack/platform-objects patch, Clause-② no. Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1bfe930fb43d06f8f5523bb869e295d4892e75fd && git checkout 1bfe930fb43d06f8f5523bb869e295d4892e75fd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c63028e5bfb63aba438ed8de9febad86f448de91 ed75b6fea1452d725235ccf2af1f9b3dc6dc6ff1 && git checkout -B drift-repro c63028e5bfb63aba438ed8de9febad86f448de91 && git merge --no-ff ed75b6fea1452d725235ccf2af1f9b3dc6dc6ff1
node scripts/docs-audit/affected-docs.mjs --json c63028e5bfb63aba438ed8de9febad86f448de91
|
…on beside the download The interface docblock and authorizeFileRead's doc now say one verdict answers both questions about a field-owned file: the download, and whether a reader refused sys_file read 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. The changeset gains one bullet for it. Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #22698 (body and every comment), PR #22713 (body, file list, net diff against ① Derived judgmentsAccept sets: none move. Each surface the diff touches, judged:
Truth of the new sentences, against the code:
One imprecision, noted and not a FAIL item: ② Semver level
Check-runs on this head: every run is success or skipped (the skips are the second-run duplicates of ③ Boundary flags
Round-1 deviations:
Out-of-scope findings:
Card acceptance, read against the diff: the three named texts and the form Implemented-by: VERDICT: PASS |
Fixes #22698
Clause-②: no
Wording only. No key, type, schema shape or runtime code changes. Dispatched by the PM loop (round 1,
domain:specseat 1), sessionsession_01S3aAf11JjbW1mSGL1EhfFj, branchclaude/issue-22698-file-delegate-metadata-text.What changes
PR #22697 (#22637) made a record read follow the download door's field-owned verdict when the reader's own
sys_fileread is refused. The texts an author reads aboutfileAccessDelegateand the refused marker still described the download door only. This PR names the record-read verdict beside the download verdict:fileAccessDelegate.describe()(object.zod.ts), which feeds the JSON Schema and three reference pagesfileAccessDelegateformhelpText(object.form.ts), plus itsen/zh-CN/ja-JP/es-ESbundle valuesfileAccessDelegateTSDoc (object.zod.ts)FileRefusedValueSchemaTSDoc (field-value.zod.ts)metadataRefused.describe()(field-value.zod.ts), which feedsreferences/data/field-value.mdxIFileAccessDelegatedocblock andauthorizeFileReaddoc (packages/spec/src/contracts/storage-service.ts), the implementer's text (patch round 1)recordId", "widens who can reach the bytes"sys_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 keptEach sentence, read off the code at
d8830c2805ObjectQL.resolveFileReferences(packages/objectql/src/engine.ts) callsreadParentDerivedFilesonly in theisReadRefusal(error)arm, which is the caller's ownsys_fileread answeringPERMISSION_DENIED. A caller whosesys_fileread succeeds hydrates from that read and never reaches the verdict. That is the source of "a reader who may readsys_filenever reaches the delegate on this path".readParentDerivedFilesreadssys_fileunder{ ...caller, isSystem: true }, whereid $inis the ids the result holds,ref_objectis this object andref_id $inis the result's record ids. It then calls the registered authorizer once with the distinct owner ids. The authorizer isreadableFieldOwners(packages/services/service-storage/src/storage-service-plugin.ts). When the owner object declaresfileAccessDelegate, it callsdelegate.authorizeFileRead(ownerId, authz)once for each owner id. That gives "once per owning record on each such read".servableFileRowsandtoFileValue, the same renderer the caller's own read uses, so the reader gets{ id, name, size, mimeType, url }, or the bare id for an unservable row, exactly as asys_filereader would. Every other id gets{ id, metadataRefused: true }: one not owned by the record it sits on, one whose owner the verdict refused, and every id when the verdict is unwired or fails.buildFileReadAuthorizer, the uploader is allowed before the field-owned arm. The field-owned arm then calls the samereadableFieldOwnerswith the oneref_id. That gives "unless the caller uploaded the file".readableFieldOwners, agetServicethrow, a missing delegate, a delegate with noauthorizeFileRead, a falsy answer and a throw all leave that owner out of the readable set. The door then answersdeny, and the record read keeps the marker.Regenerated artifacts (generator output only, no hand edits)
pnpm --filter @objectstack/spec build(runsgen:schema), thenpnpm --filter @objectstack/spec check:generatednamed exactly one stale artifact,content/docs/references/**.pnpm --filter @objectstack/spec gen:docsmoved four pages:api/metadata.mdx,data/object.mdx,system/migration.mdx(two rows) anddata/field-value.mdx.node scripts/check-i18n-bundles.mjs --write --filter=platform-objectsrewroteen.metadata-forms.generated.ts, one line.A producer outside
packages/spec: the three translated help textsStudio renders the
fileAccessDelegatehelp text in the author's locale. Thezh-CN,ja-JPandes-ESvalues are hand-written translations, and--writekeeps them in merge mode. Without an edit, those three locales would still say "downloads" only. No gate catches this: none of the three leaves has a source-hash record, so it counts as legacy-trusted. So the three values are updated by hand to say the same asen, and the changeset also carries@objectstack/platform-objects: patch, because the bundles ship in itsdist.check:i18nandcheck:i18n-stale-fillare green after the edit.Verification, at HEAD
c5513532f2(round 1) anded75b6fea1(patch round 1)Patch round 1, at
ed75b6fea1(addsstorage-service.tsand one changeset bullet; TSDoc only):pnpm --filter @objectstack/spec run typecheckexit 0; the speclocaltest projectTest Files 642 passed (642),Tests 19164 passed | 1 todo (19165);dispatch-gates --ran"106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN";check:generated"All 15 generated artifacts are up to date". The bullets below are round 1's, atc5513532f2.pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2(underos-verify-lock):Test Files 642 passed (642),Tests 19164 passed | 1 todo (19165).pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2:Test Files 69 passed (69),Tests 1082 passed (1082).pnpm --filter @objectstack/spec run typecheckexit 0.pnpm --filter @objectstack/platform-objects run typecheckexit 0.pnpm --filter @objectstack/spec check:generated: "All 15 generated artifacts are up to date".pnpm check:i18n: "OK (9 package(s) — all bundles in sync, no undeclared authoring keys)".pnpm check:nul-bytes: OK.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, derived from this diff, gives 105 commands. All 105 ran, and the exit codes were recorded before reading.--ranreconciliation: "105 derived, 105 run, 0 NOT-MEASURED, 0 UNRUN". Two gates first answered exit 3, PREREQUISITE NOT MET, because packages were unbuilt in this worktree:check:skill-examplesneededclient-react's dist andcheck:dual-build-cjs-loadsneeded nine dists. After building those packages, both re-ran with exit 0.Lint, narrowed and proven: ESLint ran on the 7 changed TypeScript files with
--no-inline-config --format json: 7 files, 0 errors, 0 warnings. ① Population: ESLint's ownisPathIgnoredreadsfalsefor all 7. ② Count: from the JSON output. ③ Invariance:eslint.config.mjsenables no type-aware linting (noparserOptions.project), so this diff cannot change a verdict on an untouched file. The repo-widepnpm lintis CI's.Not applicable: reverse verification and ablation. No type, schema or behaviour changed, so there is no assertion to flip.
Acceptance notes
IFileAccessDelegateTSDoc (packages/spec/src/contracts/storage-service.ts): reported to the seat in round 1, and resolved in this PR by patch round 1 (seat order on 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), since this card is the family's close-out.packages/spec/liveness/object.jsonfileAccessDelegate: thenotenames the download question only, andevidencecites onlybuildFileReadAuthorizer. Statusliveis still right. This is an internal ledger, not author-facing.origin/mainmoved to9f159397fbwhile this ran.git diff --name-only d8830c2805 origin/maintouches none of these 12 paths, somainwas not merged in. The queue rebuilds on the merge ref.Generated by Claude Code