Repository navigation
fix(storage)!: a record write claims or copies only a file its caller may read - #25951
Conversation
… may read The file-reference claim path asks the download door's own read judgement, under the write's caller context, before it copies or claims a referenced file. A file the caller may not read is answered as one the organization-scoped lookup cannot see: the value is kept as written, nothing is claimed or copied, and the field's declared constraints are not judged against it. System and context-less writes are unchanged. The door's judgement moves into one function both the door and the lifecycle call, so the two cannot diverge. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DTRymn12to8YSGMcTzS7h
A write's own copy is claimed without a second question; the verdict is asked of the source file only. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DTRymn12to8YSGMcTzS7h
…per file Through a real engine with the storage plugin booted as it ships, each file is asked of the download door and of a record write by the same caller; the write takes the file exactly when the door serves it. An authorization-store outage fails the write with its declared envelope; any other fault fails closed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DTRymn12to8YSGMcTzS7h
…dict Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DTRymn12to8YSGMcTzS7h
…ence Code comments, one test name and the changeset now say only that constraint verdicts no longer run ahead of the read check. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DTRymn12to8YSGMcTzS7h
…tion The file-reference claim path reads the hook session's system flag to skip the read verdict for a system write; the system-context page now anchors it. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DTRymn12to8YSGMcTzS7h
📓 Docs Drift CheckThis PR changes 1 package(s): 16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 10 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 cb5acfa9855e8d70c9a862baf6177f4476ea4f66 && git checkout cb5acfa9855e8d70c9a862baf6177f4476ea4f66
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2a728ba2acf52c13d1918c2afca7b79bf391e1f5 4f3c21e550894267d48aff9ac2985567ea2b3c18 && git checkout -B drift-repro 2a728ba2acf52c13d1918c2afca7b79bf391e1f5 && git merge --no-ff 4f3c21e550894267d48aff9ac2985567ea2b3c18
node scripts/docs-audit/affected-docs.mjs --json 2a728ba2acf52c13d1918c2afca7b79bf391e1f5
|
Contract reviewServed-tier: Face: narrowing ( ① Derived judgments
② Semver level
③ Boundary flags
Ablation legs A to F in the PR body name the pins each leg reddens and the controls that stay green; the counts are consistent with the 15 unit and 11 integration cases read above. Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
Requested by Elon · project thread
Fixes #25692
Clause-②: no (narrowing)
Metadata-protocol: none
What changes
ADR-0104 D3 (wave 2) gives each
sys_fileat most one owning field slot. A record write that references a file id either claims an unclaimed file for the writing slot, or, when another slot owns the file, copies its bytes into a freshsys_filethe writing slot owns (copy-on-claim). Neither step asked whether the write's caller may read the file. The claim path now asks the download door's own verdict, under the write's caller context, and claims or copies only on an allow.buildFileReadAuthorizerintojudgeFileRead: the uploader, a reader of the record whose field owns the file (readableFieldOwners, honouringfileAccessDelegate), a member of the organization for a file no record owns (files: an authorization-adjacent gap on a file-serving door (detail withheld pending measurement) #25693), a reader of anysys_attachmentparent for an attachments-scope file. The download door calls it with no change in behaviour. The claim path calls it throughbuildFileReferenceReadAuthorizer, which also admits apublic_readfile and asks nothing on a kernel with noauthservice, where the door binds no gate either. One file has one access rule, decided in one place.AuthzStoreUnavailableError) fails the write with its declared envelope rather than reading as either answer. Any other throw from the judgement is a deny.callerContext(attachment-access hooks) is exported so the lifecycle reads a write's caller the same way, with operation-private keys dropped. The system-context page (content/docs/permissions/system-context.mdx) gains row 47f for the claim path's system-write exemption, whichcheck-system-context-censusrequires.BREAKING, shipped as
patchThe changeset carries Before / Now / Fix and the ADR-0087 marker
not-required (no-migration-prescription): no spec key, stored row or exported declaration moves. A write that referenced a file its caller may not read now keeps the id unowned. To fix, reference the file as a caller who may read it, or upload it again. A server-side process that copies files between records on purpose runs as a system write.Tests
file-reference-lifecycle.test.ts: 15 new cases against a stand-in verdict. They cover refusal on insert and update for owned files (no copy) and unclaimed files (no claim, a tombstone not revived), per-id judging of a multiple-value field, the constraint ordering with its control, the verdict being asked with the caller context (operation-private keys dropped), outage propagation (the record is not written), and system, context-less and unbound writes unchanged. Controls show an admitted caller still copies and claims.file-reference-read-verdict.integration.test.ts(new): 11 cases on a real ObjectQL and SqlDriver withStorageServicePluginbooted. For each file, the download door's answer to the writer and the claim path's outcome for the same writer agree. A refused file gets 403 and is kept; a served file gets 200 and is copied or claimed. The cases span the field-owned, delegate, attachments, uploader,public_readand unclaimed arms, plus outage and other-fault pins and a system write.tsc -p tsconfig.test.jsoncompiles both files.Ablations
Every negative pin was put back to the refused behaviour and shown red, then restored. Each leg ran through
scripts/ablation-replace.mjs: the anchor hit 1 → 0, the blob changed, the restored blob equals HEAD, andgit diff HEADwas empty.Gates
All on head
4f3c21e5. The service-storage closure was built first, then the whole repo (pnpm build, 119/119 tasks).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 102 gates. All 102 exited 0, and--ranreconciles them: 102 derived, 102 run, 0 NOT-MEASURED, 0 UNRUN.pnpm lint(repo-wideeslint . --no-inline-config): exit 0.check:commit-card-trailers --range origin/main..HEAD: all 6 commits carry the model-free trailer pair.pnpm --filter @objectstack/dogfood test, no shard, no path, under the verify lock): 295 of 299 files passed and 3 were skipped; 2522 of 2541 tests passed and 18 were skipped. The one failure waspackaged-flow-write-door-parity.dogfood.test.ts, "a flow no package ships is still created, updated and removed through the same door", which hit its 5 s timeout while the 102 gates ran on the same 4 cores. Re-run alone under the lock: 5/5 passed. The test touches no file field.Acceptance notes
Generated by Claude Code