Skip to content

Commit b2971aa

Browse files
docs(adr-0104): a write references only a file its writer may read (#25971)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DM7SePpQnQZ7NUSKUCvUrek)_ Follow-up to #25692, which #25951 fixed (merge commit `3761710927`). This PR changes no code. ## 维护者速读 **改了什么**:给 ADR-0104 D3 补一条修订,记明记录写入时的「认领」和「复制认领」只对写入者能读的文件发生,判断用的就是下载接口那一套规则。具体有三处:Status 加一条修订记录;在 2026-07-27 增补的最坏情况表下加注;末尾新增 2026-10-26 增补。按你在设计审阅后的选择(先改措辞再批),增补又补了三处准确性说明:保留的 ID 通过这条记录对任何读者都不可用,后续由 #26043 改成明确报错;流程和钩子按各自的 runAs 身份判断,只有系统身份免检;「分不清无权和不存在」只在保存这一步成立。 **为什么改**:#25951 已经把这条规则写进代码。但 ADR 里「独占加复制认领,最坏只是多存一份字节」这一行,隐含了一个前提:复制的人本来就能读原文件。代码之前没有检查这个前提,所以决策记录不完整。把前提写进 ADR,以后改这块的人搜 copy-on-claim 就能读到这条规则。 **风险与代价(含回滚)**:只改文档,不动代码和数据。回滚就是 revert 这个 PR,对运行时没有影响。 **席位意见**:建议批准。增补只把 #25951 已经落地的规则写进决策记录,没有新增或放宽任何行为,措辞只写机制,符合本卡的披露要求。逐句核对增补与合入代码是否一致,交给契约复审(`CONTRACT_REVIEW_TIER`)记录,记录会贴在本 PR 上,批准前请先看它的结论(措辞修改后的 head 会重新复审)。有一个相邻的问题由席位在线下另行转告维护者,不写在这里。 **你要做的**:审阅并批准这个 PR。它改的是受管面 `docs/adr/**`,需要你本人批准才能落地。 ## What changes - **Status**: an **Amended** bullet (2026-10-26, #25692, PR #25951, merge commit `3761710927`). - **2026-07-27 ownership addendum**: a note under the worst-case table. The exclusive row's worst case holds only when claim and copy-on-claim run for a file the writer may read. - **New addendum (2026-10-26), "a write references only a file its writer may read"**: - the rule, and the one judgement its two callers share; - the quiet answer, after the #25682 precedent; - a write's own copy; - the outage envelope; - the exemption for writes with no caller; - what it qualifies, and why it stays inside ADR-0104. Per the card's disclosure rule, the text states the mechanism only. ## Gates Run on head `13d9a1ad` (`3d14f9ae` plus one wording commit), whose base is main `3761710927`. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 20 gates. All 20 exit 0, and `--ran` reconciles them: 20 derived, 20 run, 0 NOT-MEASURED. - `pnpm check:pm-governed-prose`: exit 0. - `check:commit-card-trailers --range origin/main..HEAD`: the 1 commit passes. - No changeset: the diff publishes nothing from any released package (`skip-changeset`). ## Acceptance notes - Tier H (`docs/adr/**`): this lands only on an approval by a governed approver. --- _Generated by [Claude Code](https://claude.ai/code/session_016DTRymn12to8YSGMcTzS7h)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d12b0c1 commit b2971aa

1 file changed

Lines changed: 71 additions & 0 deletions

File tree

‎docs/adr/0104-field-runtime-value-shape-contract.md‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
naming every offending param, with no `fields[]`, at both doors, which
2323
corrects item 1's "`400 VALIDATION_FAILED` per-field envelope". See the
2424
note under D2 item 1.
25+
· **Amended** (2026-10-26, #25692, PR #25951, merge commit `3761710927`) —
26+
wave 2's claim and copy-on-claim run only for a file the writing caller may
27+
read, by the download door's own verdict. A file the writer may not read is
28+
kept as written and never claimed or copied, the answer #25682 gave across
29+
organizations. This qualifies the 2026-07-27 addendum's worst-case row for
30+
exclusive ownership. See the 2026-10-26 addendum.
2531
- **Date**: 2026-07-22
2632
- **Issue**: design follow-up generalizing #3405 / #3406 (inline lookup param
2733
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
600606
| 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 |
601607
| Exclusive + copy-on-claim | duplicated bytes | yes — it costs money, not data |
602608

609+
> **Amended 2026-10-26 (#25692, PR #25951, merge commit `3761710927`).** The
610+
> exclusive row's worst case holds only when claim and copy-on-claim run for a
611+
> file the writer may read. The claim path did not ask that question until
612+
> #25692; it now asks the download door's own verdict. See the 2026-10-26
613+
> addendum.
614+
603615
Read authorisation for attachment files already derives from the parent record.
604616
Under sharing, "the parent record" is ambiguous, and the safe reading of an
605617
ambiguous ACL is the union — which means generated code can leak data while
@@ -1428,3 +1440,62 @@ here so that a grep for `JSON_COLUMN_TYPES`, `VARCHAR(2048)` or "bare id"
14281440
lands on the decision (Prime Directive #13), and so that the driver card
14291441
implements a ruling rather than a changeset that quietly does the opposite of
14301442
D1's clause.
1443+
1444+
## Addendum (2026-10-26) — a write references only a file its writer may read
1445+
1446+
#25692, implemented by PR #25951 (merge commit `3761710927`). Wave 2's
1447+
ownership bookkeeping (step 1 of the 2026-07-27 resequencing) and its governed
1448+
download (step 2) each answered a different question about the same file.
1449+
The download door asked whether the caller may read it. The claim path, which
1450+
claims or copies a file a record write names, asked only whether the file
1451+
exists in the acting organization. One file had two access rules.
1452+
1453+
### The rule
1454+
1455+
> A record write claims or copies only a file its caller may read, by the
1456+
> download door's own verdict. Claim and copy-on-claim never run for a file
1457+
> the writer may not read.
1458+
1459+
- **One judgement, two callers.** The door's verdict is one function, and the
1460+
claim path calls it under the write's caller context; neither restates it.
1461+
It admits the uploader; a reader of the record that owns the file, through
1462+
its object's `fileAccessDelegate` where one is declared; a member of the
1463+
organization for a file no record owns; a reader of any parent for an
1464+
attachments-scope file; anyone for a `public_read` file.
1465+
- **The quiet answer.** At the write door, a file the writer may not read is
1466+
answered as one the organization-scoped lookup cannot see: the id is kept
1467+
as written, nothing is claimed or copied, and constraint verdicts do not run
1468+
ahead of the read check. #25682 gave this answer across organizations; this
1469+
addendum states it within one. Unless the record already owned the file,
1470+
the kept id gives no reader anything through it: a reader refused
1471+
`sys_file` read gets the refused marker (`metadataRefused`), and the
1472+
download door judges the file by its own rule, never by this record. A slot
1473+
holding a file it does not own is a state that step 5's
1474+
`verify-references` classifies as blocking. #26043 tracks replacing this
1475+
answer with a field-level refusal.
1476+
- **A write's own copy** is claimed by the same write without being asked
1477+
again.
1478+
- **Outage.** An authorization-store outage fails the write with its declared
1479+
envelope. Any other fault in the judgement is a refusal.
1480+
- **No caller, no question.** System writes and context-less programmatic
1481+
writes reference files as before. Everything else has a caller. A flow runs
1482+
as `runAs: 'user'` by default and is judged as the user it acts for. A
1483+
hook's data operations take the context of the write that fired them by
1484+
default (`runAs: 'inherit'`), or the triggering user's (`runAs: 'user'`).
1485+
An API-key caller is judged as the key's user. Only a declared
1486+
`runAs: 'system'` run, or a write already made as the system, is exempt. A
1487+
deliberate server-side copy between records runs as a system write.
1488+
1489+
### What it qualifies
1490+
1491+
The 2026-07-27 worst-case table rates exclusive + copy-on-claim at
1492+
"duplicated bytes — recoverable". That rating assumes the copy is taken by a
1493+
writer who could read the original. Under this rule it holds, and the table
1494+
carries a note saying so.
1495+
1496+
### Why this stays inside ADR-0104
1497+
1498+
It revisits neither exclusivity nor copy-on-claim. It states the
1499+
precondition both always needed for the exclusive row's worst case to be
1500+
true, and records it here so that a grep for copy-on-claim lands on the rule
1501+
(Prime Directive #13).

0 commit comments

Comments
 (0)