Repository navigation
Commit 05c7c3f
docs(adr): ADR-0138 D2 gains class 6, the acl public_read file download; the G2 result recorded (still Proposed) (#22433)
Part of #22146
Clause-②: no
## What this PR is
Round 4 of #22146: a record revision of
`docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md`
under the G2 ruling
[`6074960686`](#22146 (comment))
(batch #300 item 1, letter A, maintainer 「同意」). **Status stays
Proposed.**
- **One file, no code.** Nothing changes in `packages/**`,
`content/docs/**`, `skills/**` or any other record. No back-pointer line
is written into ADR-0090 or ADR-0056.
- **Tier H.** The diff touches `docs/adr/**`, so this PR stays draft and
lands only on the maintainer's approval.
- **The card stays open**, so line 1 is `Part of`. What remains on
#22146: the three defect cards, the re-measurement of the
everything-else class, and the acceptance PR.
## What the revision records
**D2 gains class 6, the `acl: 'public_read'` file download (ADR-0104 D3
wave 2).** The row uses the table's existing columns:
- **What admits the request:** the file's own `acl: 'public_read'`
(`packages/services/service-storage/src/objects/system-file.object.ts#acl`).
It is an explicit opt-in, because the upload routes create every file as
`private`.
- **What it runs as:** a read of that one file's bytes, nothing else. No
principal is evaluated.
- **Enforced by:**
`packages/services/service-storage/src/storage-routes.ts#registerStorageRoutes`,
at the download gate `authorizeDownload`, which both download routes
call.
- I read this on `origin/main` `e02833c240`: a file marked `public_read`
skips the parent-record authorizer and gets the stable URL.
- `authorizeDownload` is a closure inside `registerStorageRoutes`. So
the row anchors the function and names the gate as a same-line
continuation anchor, and both resolve as declarations.
**The adoption rule** now reads: classes 4 and 5 take the guest
envelope; classes 1 to 3 and 6 take their own declaration; nothing else
serves a guest.
**The G2 result and the ruling, at class level only.** The full record
is under acceptance criterion 2, with short pointers in the Status line,
D2 and the enforcement map.
- **Classes 1 to 5 match** D2's table.
- **The `public_read` download was served anonymously**, as ADR-0104
rules, while D2 did not yet name it. The ruling adds it as class 6.
- **The everything-else class did not match.** Three families, named at
function level as the three defect cards name them:
- the `/i18n` dispatcher domain
(`packages/runtime/src/domains/i18n.ts#handleI18nRequest` makes no
`shouldDenyAnonymous` call);
- the API-description endpoints
(`packages/rest/src/rest-server.ts#RestServer.registerOpenApiEndpoints`);
- storage download of a file with no scope and no field owner.
- **Acceptance waits** on #22432, #22430 and #22431 and on a
re-measurement. The record quotes the ruling's sentence on that
verbatim.
- No route, size, payload or request shape appears. The readings stay in
the seat's container.
**Consistency edits that the ruling forced:**
- **Status line.** It now says the G2 sweep was measured and not met. "⛔
Nothing in `packages/**` … changes before acceptance" is now "no change
this record decides …", followed by one sentence explaining why the
three defect cards land first: they enforce ADR-0056 D2 and ADR-0104,
which are both Accepted.
- **Front matter:**
- *Decided by* gains the G2 ruling.
- *Builds on* gains ADR-0104 D3 wave 2.
- *Leaves unchanged* gains ADR-0104.
- *Consumers* gains `@objectstack/service-storage`.
- *Card* now mentions the G2 sweep and this revision.
- **TL;DR:** a note under the verbatim ruling summary.
- **Context:** the G2 bullet points to criterion 2.
- **D2:** a short "Measured, and not yet held" paragraph.
- **Enforcement map:** the anonymous default-deny row's status names the
three G2 exceptions.
- **Consequences:**
- a new bullet: class 6 changes nothing that serves today;
- the "Not measured" bullet now counts G2 as measured.
- **Alternatives considered:** gains G2-result B and C, in the ruling's
words.
- **Criterion 4:** "No line is added to …" gains ADR-0104.
- **Execution plan:** a sentence saying the three defect cards are not
execution cards.
- **References:** gain the G2 report, its review and the ruling, and the
three cards.
## Every count and class-number site
Line numbers are base `e02833c240` (identical to `27a8b33dec` for this
file) → head `024872cb90`.
| Site | Before | After |
|:--|:--|:--|
| TL;DR decision table, D2 row (`:63` → `:77`) | five door classes | six
door classes; the Ruled cell adds "class 6 added by the G2 ruling" |
| D2 intro (`:215` → `:238`) | exactly these five door classes | six;
the Ruled line names the G2 ruling |
| D2 table | five rows | row 6 added (`:248`) |
| D2 adoption rule (`:244`–`:245` → `:268`–`:270`) | classes 1 to 3 take
their own declared grant | classes 1 to 3 and 6 take their own
declaration |
| D2 *Enforced by* (`:249` → `:274`), **not in the dispatch's lead
list** | Card E2 records the five classes | six |
| Enforcement map (`:487` → `:519`) | the five door classes | the six
door classes |
| Criterion 2 (`:578` → `:624`) | each of D2's five classes | six, plus
the measured result and what remains |
| Criterion 4, quoted ADR-0056 back-pointer text (`:592` → `:665`) | a
closed list of five door classes | six. This changes only the count
inside the quoted text; the back-pointer itself is not landed |
| E2 (`:607` → `:682`) | the five classes recorded in the conformance
matrix | six |
Sites deliberately left as they are:
- **The first ruling's summary** (`:74` → `:88`). It says "the closed
list of five door classes", and it is a verbatim quote of `6054113537`,
so editing it would rewrite a ruling. A new paragraph under it
(`:101`–`:106`) records the G2 ruling and says why the quote still says
five.
- **Sites whose numbers did not move.** Class 6 is appended, so classes
1 to 5 keep their numbers:
- D1 point 1 "(D2, classes 4 and 5)" (`:207`);
- D3 point 1, the same phrase (`:330`);
- D8 "D2 class 1" (`:449`);
- D2b "D2's class 5 row" (`:504`);
- E1 "denied on class 4" (`:681`).
- **"except at the two endpoint classes above"** (`:254`). It is still
two: class 6 takes no guest envelope.
- **The Context table "re-measured at `7b926f7600`"** (`:122`) is a
dated reading, so it stays as measured.
- **Alternatives, G1** ("every door class reads as a denial or a narrow
door") is the reasoning recorded for a letter that was not taken.
- **Criterion 1** is left as the record has it. The landing comment
`6058198880` read it as met by the approval on PR #22239. This revision
is itself a Tier H approval, so whether to mark it met is the seat's
call.
## Verification at `024872cb90`
I derived the gate list with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` from the worktree. The
change set is 1 path, +121 / −37 against merge base `e02833c24`, and the
tool gave 19 commands. I ran every one at `024872cb90` and captured each
exit code before any pipe:
| Command | Exit |
|:--|:--|
| `node scripts/check-adr-links.mjs` (and `--self-test`) | 0, 0 |
| `node scripts/check-adr-symbol-anchors.mjs` (and `--self-test`) | 0, 0
|
| `node scripts/check-ci-filter-parity.mjs` | 0 |
| `node scripts/check-closing-keyword-parity.mjs` (and `--self-test`) |
0, 0 |
| `node scripts/check-comment-mask-corpus.mjs` | 0 |
| `pnpm --filter @objectstack/lint run check:doc-formula-expressions` |
0 (see below) |
| `pnpm check:adr-anchors` | 0 |
| `pnpm check:cross-package-test-inputs` | 0 |
| `pnpm check:doc-authoring` | 0 |
| `pnpm check:driver-memory-census` | 0 |
| `pnpm check:gitlink-declared` | 0 |
| `pnpm check:nul-bytes` | 0 |
| `pnpm check:pm-governed-merges` | 0 |
| `pnpm check:pm-prior-rulings` | 0 |
| `pnpm check:refd-timer-probe` | 0 |
| `pnpm check:watch-hint-literal` | 0 |
- **`check-adr-symbol-anchors`** reports "2232 anchors across 141
records resolve".
- Positive control: the shared extractor reads 57 anchors in this record
at base and 63 at head. The +6 are `system-file.object.ts#acl`,
`storage-routes.ts#registerStorageRoutes` twice, the
`#authorizeDownload` continuation, `i18n.ts#handleI18nRequest` and
`rest-server.ts#RestServer.registerOpenApiEndpoints`. All six resolve as
declarations.
- Negative control: a made-up `authorizeDownloadNope` reads as absent.
- **`check-adr-links`** reports "715 relative link destination(s) under
docs/adr/ resolve".
- **`check:doc-formula-expressions`.** The first run exited 3
(PREREQUISITE NOT MET: `@objectstack/formula` not built in the new
worktree), which measured nothing. I built the formula and lint closure
under `os-verify-lock.sh` (`VERDICT command-exit 0`), and the rerun
exited 0.
- **`check:nul-bytes`** is OK. A separate control-byte grep on the
record exited 1, meaning no hits.
- **Reconciliation.** `dispatch-gates.mjs --ran` with the recorded exit
codes reports "19 derived, 19 run, 0 NOT-MEASURED, 0 UNRUN".
- **`check-issue-citations`** was named by the dispatch but is not in
the derived set. I ran it anyway and it exited 0, but it read 0 files,
because `docs/adr/**` is one of its deferred surfaces. So it measured
nothing for this diff. Instead I checked by REST that #22430, #22431 and
#22432 are open issues and not pull requests.
- **CI:** in progress, not awaited.
## Changeset
None. `docs/adr/**` ships in no package's `files[]`, and this PR adds or
changes no `.changeset/*.md`. The label is `skip-changeset`, the opt-out
that the Check Changeset job in `pr-automation.yml` reads.
## Acceptance notes
- **The `public_read` opt-in has no in-repo producer.**
- The upload routes in `registerStorageRoutes` create every `sys_file`
row as `private`, and copy-on-claim copies the source row's `acl`.
`FieldSchema` declares no file `acl`, so ADR-0104's "the field/file
explicitly declares" exists today only on the file-row side, as the
`sys_file.acl` select.
- This is not a defect under ADR-0104, which rules an opt-in. But it
bears on #22431: once an unclaimed upload, such as an organization logo,
requires a signed-in caller, nothing in the repository marks that file
`public_read`.
- Carrier: #22431, whose acceptance already asks the dev to measure the
in-repo producers of unclaimed uploads. Noted, not filed.
## 维护者速读(草稿)
- **改了什么:** 按你对 G2 实测结果的裁决(「同意」,A),修订 ADR-0138 这份文档。ADR 仍然是
Proposed(提议中),本 PR 不改任何代码,也不改其他 ADR。
- D2 的"匿名可访问入口清单"从五类加到六类。第六类是文件被显式标成 `public_read` 后的匿名下载(头像、Logo 这类),这是
ADR-0104 早已裁定的做法,这里只是把它写进清单。
- 把 G2
实测结论按"类"写进文档:前五类都和文档一致;"其余一律拒绝匿名"这条不成立,有三处还对未登录的人开着。这三处只写到函数级:界面文案接口、API
文档接口、没有归属的上传文件下载。
- 写明 ADR 转为 Accepted 的前提:三张缺陷卡(#22432、#22430、#22431)落地后,重测"其余"一类,全部返回
401,再开验收 PR。
- **为什么改:** 实测证明"其余一律 401"今天还不是事实。你选择先关门、再验收,所以文档必须把 `public_read`
这扇合法的门写进清单,把还开着的三扇门和验收条件写清楚,不然下一次重测会把合法的门误判为漏洞,或者把漏洞当成已经关好。
- **风险与代价(含回滚):**
- 合并后运行时行为不变,因为只改了一个文档文件。回滚就是还原这次修订。
- 第六类不改变任何现有行为:`public_read` 文件今天本来就能匿名下载,#22431 修复后仍然可以。
- 仓内目前没有任何代码会把文件标成 `public_read`(上传一律是 `private`)。#22431 收紧"无归属上传"之后,像组织
Logo 这类文件,如果没人手动标成 `public_read`,未登录时就看不到了。这一点交给 #22431 实测。
- **席位意见:**
- **你要做的:** 席位复核之后,批准这份修订(Tier H,点 Approve 或亲手合并)。注意:批准的是这次修订,不是接受
ADR,ADR 仍保持 Proposed。
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 46692c1 commit 05c7c3f
1 file changed
Lines changed: 121 additions & 37 deletions
0 commit comments