From 3d14f9ae5c7220890b0c42dfa335b469e617b7fa Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 04:10:14 +0000 Subject: [PATCH 1/2] docs(adr-0104): a write references only a file its writer may read Amends ADR-0104 D3 wave 2: claim and copy-on-claim run only for a file the writing caller may read, by the download door's own verdict. Adds the 2026-10-26 addendum, a Status bullet, and a note under the 2026-07-27 worst-case row for exclusive ownership. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016DTRymn12to8YSGMcTzS7h --- ...0104-field-runtime-value-shape-contract.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/docs/adr/0104-field-runtime-value-shape-contract.md b/docs/adr/0104-field-runtime-value-shape-contract.md index e66a3c9e3d9..43a9489d28f 100644 --- a/docs/adr/0104-field-runtime-value-shape-contract.md +++ b/docs/adr/0104-field-runtime-value-shape-contract.md @@ -22,6 +22,12 @@ naming every offending param, with no `fields[]`, at both doors, which corrects item 1's "`400 VALIDATION_FAILED` per-field envelope". See the note under D2 item 1. + · **Amended** (2026-10-26, #25692, PR #25951, merge commit `3761710927`) — + wave 2's claim and copy-on-claim run only for a file the writing caller may + read, by the download door's own verdict. A file the writer may not read is + kept as written and never claimed or copied, the answer #25682 gave across + organizations. This qualifies the 2026-07-27 addendum's worst-case row for + exclusive ownership. See the 2026-10-26 addendum. - **Date**: 2026-07-22 - **Issue**: design follow-up generalizing #3405 / #3406 (inline lookup param silently stripped); relates #3407 (silently dropped writes), #1878 / #1891 @@ -600,6 +606,12 @@ of special case that produces mistakes. So the real question is what happens | Shared + reference-counted | file's readable-by set becomes the **union of every referrer's** — copying a private record's id into a world-readable record silently widens access; and a single miscount authorises an **irreversible byte delete** | no | | Exclusive + copy-on-claim | duplicated bytes | yes — it costs money, not data | +> **Amended 2026-10-26 (#25692, PR #25951, merge commit `3761710927`).** The +> exclusive row's worst case holds only when claim and copy-on-claim run for a +> file the writer may read. The claim path did not ask that question until +> #25692; it now asks the download door's own verdict. See the 2026-10-26 +> addendum. + Read authorisation for attachment files already derives from the parent record. Under sharing, "the parent record" is ambiguous, and the safe reading of an ambiguous ACL is the union — which means generated code can leak data while @@ -1428,3 +1440,51 @@ here so that a grep for `JSON_COLUMN_TYPES`, `VARCHAR(2048)` or "bare id" lands on the decision (Prime Directive #13), and so that the driver card implements a ruling rather than a changeset that quietly does the opposite of D1's clause. + +## Addendum (2026-10-26) — a write references only a file its writer may read + +#25692, implemented by PR #25951 (merge commit `3761710927`). Wave 2's +ownership bookkeeping (step 1 of the 2026-07-27 resequencing) and its governed +download (step 2) each answered a different question about the same file. +The download door asked whether the caller may read it. The claim path, which +claims or copies a file a record write names, asked only whether the file +exists in the acting organization. One file had two access rules. + +### The rule + +> A record write claims or copies only a file its caller may read, by the +> download door's own verdict. Claim and copy-on-claim never run for a file +> the writer may not read. + +- **One judgement, two callers.** The door's verdict is one function, and the + claim path calls it under the write's caller context; neither restates it. + It admits the uploader; a reader of the record that owns the file, through + its object's `fileAccessDelegate` where one is declared; a member of the + organization for a file no record owns; a reader of any parent for an + attachments-scope file; anyone for a `public_read` file. +- **The quiet answer.** A file the writer may not read is answered as one the + organization-scoped lookup cannot see: the id is kept as written, nothing + is claimed or copied, and constraint verdicts do not run ahead of the read + check. #25682 gave this answer across organizations; this addendum states + it within one. +- **A write's own copy** is claimed by the same write without being asked + again. +- **Outage.** An authorization-store outage fails the write with its declared + envelope. Any other fault in the judgement is a refusal. +- **No caller, no question.** System writes and context-less programmatic + writes reference files as before. A deliberate server-side copy between + records runs as a system write. + +### What it qualifies + +The 2026-07-27 worst-case table rates exclusive + copy-on-claim at +"duplicated bytes — recoverable". That rating assumes the copy is taken by a +writer who could read the original. Under this rule it holds, and the table +carries a note saying so. + +### Why this stays inside ADR-0104 + +It revisits neither exclusivity nor copy-on-claim. It states the +precondition both always needed for the exclusive row's worst case to be +true, and records it here so that a grep for copy-on-claim lands on the rule +(Prime Directive #13). From 13d9a1adf7d301e437e4b4a650827270cf37079b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 10:11:06 +0000 Subject: [PATCH 2/2] docs(adr-0104): state what a kept id yields and who counts as a caller The 2026-10-26 addendum's quiet answer is scoped to the write door, says what a kept id gives a reader and how step 5 classifies it, and names #26043. The no-caller exemption names the runAs defaults of flows and hooks and the API-key caller. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016DTRymn12to8YSGMcTzS7h --- ...0104-field-runtime-value-shape-contract.md | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/adr/0104-field-runtime-value-shape-contract.md b/docs/adr/0104-field-runtime-value-shape-contract.md index 43a9489d28f..37a812c5540 100644 --- a/docs/adr/0104-field-runtime-value-shape-contract.md +++ b/docs/adr/0104-field-runtime-value-shape-contract.md @@ -1462,18 +1462,29 @@ exists in the acting organization. One file had two access rules. its object's `fileAccessDelegate` where one is declared; a member of the organization for a file no record owns; a reader of any parent for an attachments-scope file; anyone for a `public_read` file. -- **The quiet answer.** A file the writer may not read is answered as one the - organization-scoped lookup cannot see: the id is kept as written, nothing - is claimed or copied, and constraint verdicts do not run ahead of the read - check. #25682 gave this answer across organizations; this addendum states - it within one. +- **The quiet answer.** At the write door, a file the writer may not read is + answered as one the organization-scoped lookup cannot see: the id is kept + as written, nothing is claimed or copied, and constraint verdicts do not run + ahead of the read check. #25682 gave this answer across organizations; this + addendum states it within one. Unless the record already owned the file, + the kept id gives no reader anything through it: a reader refused + `sys_file` read gets the refused marker (`metadataRefused`), and the + download door judges the file by its own rule, never by this record. A slot + holding a file it does not own is a state that step 5's + `verify-references` classifies as blocking. #26043 tracks replacing this + answer with a field-level refusal. - **A write's own copy** is claimed by the same write without being asked again. - **Outage.** An authorization-store outage fails the write with its declared envelope. Any other fault in the judgement is a refusal. - **No caller, no question.** System writes and context-less programmatic - writes reference files as before. A deliberate server-side copy between - records runs as a system write. + writes reference files as before. Everything else has a caller. A flow runs + as `runAs: 'user'` by default and is judged as the user it acts for. A + hook's data operations take the context of the write that fired them by + default (`runAs: 'inherit'`), or the triggering user's (`runAs: 'user'`). + An API-key caller is judged as the key's user. Only a declared + `runAs: 'system'` run, or a write already made as the system, is exempt. A + deliberate server-side copy between records runs as a system write. ### What it qualifies