Skip to content

docs(adr): ADR-0138 amendment — D2 class 7, the guest upload door bound to a public form - #25619

Closed
objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/issue-25572-adr-0138-guest-upload
Closed

objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/issue-25572-adr-0138-guest-upload

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #25572

维护者速读(草稿)

改了什么

在 ADR-0138 的「访客可以进的门」清单(D2)里加第 7 类门:绑定在公开表单上的访客上传门。本 PR 只改文档:ADR-0138 正文,以及 ADR-0056 状态行加一行回指。不含任何代码,上线后系统行为不变。门本身由 #25572 的第二阶段实现,等本文合并后再派。

为什么改

hotsrm 的供应商注册公开表单要求访客在第一次提交时上传营业执照扫描件。现在访客上传不了任何文件(所有上传接口对未登录访客一律拒绝),表单里一旦有必填的附件字段,访客就无法提交。按裁决 B,匿名写入文件是安全边界,所以先把规则写进 ADR,再写代码。

ADR 写下的规则,逐条对应裁决原文:只收表单里声明了的文件字段;大小与类型按该字段的声明限制;文件先以「未挂接」状态存进表单所属的组织;随后的提交引用它就挂到那条记录上,否则过一段短时间自动清理(时长由实现声明);与表单相同的反滥用姿态:按地址限速、不可列举、访客读不回任何人的文件。

风险与代价(含回滚)

  • 本 PR 是纯文档,合并本身没有运行时风险;访客上传在实现落地前仍然全部被拒。
  • 真正的风险在实现阶段,ADR 已把每条约束写成实现必须满足的规则,并列出实现必须带的验收用例。
  • 有两个「空态」需要您知情(ADR 新增的「裁决未定事项」第 4 条):
    1. 平台的按地址限速要部署自己开启(默认关闭)。实测仓库内的示例应用都没有开启。按原文,没开限速的部署上,表单和这个上传门都不限速,与表单现状一致。
    2. 文件字段如果没有声明大小上限和允许类型,就只受部署整体的上传上限约束(默认 100 MB)。
      按原文,两种空态下这个门照样开放。
  • 回滚:直接 revert 本 PR,没有数据或代码影响。

席位意见

你要做的(一个动作)

在本 PR 上回复一个字母:A = 按原文批准(两种空态下门照开,与表单现状一致);B = 两种空态下门不开(部署没开限速、或字段没声明大小与类型时,不提供访客上传),由席位改一段文字后再请您批准。


Clause-②: no
Metadata-protocol: none

Summary

Records letter B of ruling record 6377731715 on #25496 (decision batch #388 item 4, maintainer 「同意」), carried on #25445 by the director's 6377853567 and the maintainer account's 6377887908: an ADR-0138 amendment that declares a guest upload door bound to a public form, as D2's class 7. ⛔ No implementation code: the ruling orders the ADR first and the door after it lands, so this PR is phase 1 of #25572 and the card stays open for phase 2 (E5).

Files: docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md (the amendment) and docs/adr/0056-permission-model-landing-verification.md (one dated back-pointer line).

What the amendment states

Every constraint of the two ruling records is one rule of the new #### Class 7 subsection under D2, each with the code it binds to, cited by symbol on origin/main:

  1. Bound to a public form: the door serves a form exactly when class 1's doors do, through the one form resolution (packages/rest/src/form-routes.ts#resolveFormBySlug); the storage upload doors keep refusing a caller with no session.
  2. Only the form's declared file fields: an entry of the form's sections whose type is a file type (FILE_REFERENCE_TYPES).
  3. Size and type limited by the field: the field's maxSize and accept (packages/spec/src/data/field.zod.ts), judged before a byte is stored, beside the deployment upload limit, and judged again by the attaching record write (assertFileConstraints).
  4. Stored unattached in the form's organization (D4): field-reference columns empty, acl: 'private', owner_id unset (D1), organization from D4's clarification (1), never from the client.
  5. Attached by the submission that follows: the class-1 submit claims it through the field-reference lifecycle (claimFile, ADR-0104 D3 wave 2).
  6. Otherwise collected after a short TTL: by the one platform sweep (LifecycleService, ADR-0057 §3.3), bytes reclaimed by the sys_file reap guard. The TTL is a bound the implementation declares; no existing constant covers it (measured below).
  7. The form doors' anti-abuse posture: the one inbound limiter, keyed by caller address for a sessionless caller; the door answers the file id only and nothing lists guest uploads; no anonymous read-back of any guest file, the guest's own included (the guest is one principal).
  8. The signed byte URL stays outside: criterion 2's outside family is unchanged.

Kept in step: the status line, TL;DR, D2's table, its adoption sentence and enforcer line, the Enforcement map, Consequences, What the ruling did not settle (items 4 and 5), the Execution plan (new row E5, #25572, with its pins) and References.

Convention followed

ADR-0138's own amendment form, as its 2026-10-13 and 2026-10-14 amendments use it: a dated status-line continuation (· **Amended** (DATE, ruling record, maintainer words) — what changed), and the change written in place where the record's structure holds it. Here that is D2, whose text says "naming a new door class is an amendment of this record". The record whose text the change alters gets a dated back-pointer continuation, as ADR-0138's criterion 4 and its D3 item 5 amendment did for ADR-0090 and ADR-0086: here ADR-0056's D2 line, which counts six classes. docs/adr/ has no index file, and scripts/adr-anchors/ holds one file per anchored code path, which lands with code, so this PR adds none.

File surface: the claim names ADR-0138 plus any index or anchor file the ADR process requires. The ADR-0056 back-pointer line is beyond that literal list. It is added because of the precedent above, and it is declared here.

Measurements (origin/main e649c2d91e; re-checked unchanged at 10b8250ad5 for every path cited)

  • The card's POST /storage/upload is not a route. It was retired from the dispatcher (packages/runtime/src/route-ledger.ts, the storage block), so it answers 404 to every caller. The real upload doors are /storage/upload/presigned, /complete and /chunked (registerStorageRoutes), and each refuses a sessionless caller first with 401 AUTH_REQUIRED (requireUploadSession). The premise holds: a guest cannot upload, so a required file field makes a public form unsubmittable.
  • TTL: sys_file declares retention of 7d only when status is pending, and a ttl on deleted_at (30d). Neither reaches a completed file that no record claims (stranded-orphan-inventory.ts: such rows are never a sweep candidate). So no platform constant covers the guest TTL. The ADR leaves the value to the implementation, as a declared bound.
  • Rate limit: the form doors have no limiter of their own. The one per-address limiter is the server-wide createInboundRateLimitMiddleware, armed by server.security.rateLimit (opt-in, enabled defaults to false). No app under examples/ arms it, and hotcrm's docs (objectstack-ai/hotcrm at 8a14b56, content/docs/reference/performance-and-limits.mdx) say it declares none. The ADR binds the door to that limiter and states the unarmed empty state.

Open question for the maintainer (What the ruling did not settle, item 4)

The ruling names the form doors' posture and the field's limits, and both can be empty: an unarmed limiter, or a file field with neither maxSize nor accept. A (as written): the door opens in both, exactly as the form doors do today. B: the door stays shut until the limiter is armed and the field declares its limits, the shape ADR-0121 D6 gives an anonymous endpoint. B adds no key and no gate script. My recommendation is B: "rate limit per address" is a named constraint, and under A it holds on no measured deployment. The text carries A because A is the ruled words read literally ("与表单同样"). Choosing B changes item 4 and two empty-state sentences.

Acceptance notes

Observations, not filed:

Verification

Docs-only diff, so there are no tests, no build of a touched package, no pnpm lint (no package touched) and no ablation. The gates were derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on head 4285f5e455: 20 commands, all run, each exit 0. Among them: check-adr-symbol-anchors (2408 anchors found, the 21 this diff adds each found as a declaration), check-adr-links (773 link destinations found), check:adr-anchors, check:doc-authoring, check:nul-bytes, check:pm-governed-merges and check:pm-prior-rulings. --ran reconciliation: 20 derived, 20 run, 0 NOT-MEASURED, 0 UNRUN. check:doc-formula-expressions first answered exit 3 (prerequisite not built). After building @objectstack/formula and @objectstack/lint under the verify lock, it answered 0. Tier H: this PR stays draft for the maintainer's approval.


Generated by Claude Code

…nd to a public form

Records letter B of ruling 6377731715 (#25496): a guest file is taken only
for a file field the public form declares, limited in size and type by that
field, stored unattached and private in the form's organization (D4),
attached by the submission that follows or collected after a declared TTL,
under the form doors' own anti-abuse posture. ADR-0056 gains the dated
back-pointer line. No code: the door is built by #25572 after this lands.

Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB
Co-authored-by: Claude <noreply@anthropic.com>
…wording and the record line

Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 24, 2026
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Oct 24, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

改了什么: 本 PR 只改文档。ADR-0138 的「访客可以进的门」(D2)加第 7 类:绑定在公开表单上的访客上传门,ADR-0056 加一行回指。不含代码,合并后系统行为不变。门本身是 #25572 第二阶段的活。

为什么改: hotsrm 的供应商注册公开表单要上传营业执照,而现在访客上传不了任何文件,表单一旦有必填附件就提交不了。裁决 B 的意思是「先写 ADR,再写代码」。

风险与代价(含回滚):

  • 文档本身合并没有运行时风险,revert 即可回滚。
  • 契约复审档复核结论 PASS:裁决的六条约束都写进去了,14 处引用在 main 上逐条成立,没有多加能力。
  • 复核顺带确认了一个已存在的安全缺口:今天的公开表单提交路径就有,和本 PR 无关。细节按安全披露纪律不落公开面,另立 public forms: an authorization-adjacent gap in how a submission's file field is handled (detail withheld pending maintainer) #25622 作归属记录,细节已在席位会话里交给您。三个选项都得把它关掉。
  • 待定的「空态」:平台的按地址限速默认不开,没有任何示例应用开了它;文件字段也可以不声明大小和类型(只受部署上传上限约束,默认 100 MB)。

席位意见: 推荐 C,带前提。

你要做的(一个动作): 在本 PR 回复一个字母 C / B / A。选 C 请顺带告诉我 hotsrm 跑在自托管还是云托管。


席位核对

domain:server seat 2 · sunhaolin · session_01JVTd3PprStGa26aez5omLB · 2026-10-24T10:28Z · head 4285f5e455 · claim 6382646973

  1. Shape: draft, base main, author objectstack-fleet[bot], assignee sunhaolin. The first line is Part of #25572, and no closing keyword appears in the body or the three commits. Labels documentation, skip-changeset. Reviewers hotlong and os-zhuang were requested by the relay's governed follow-up.
  2. Premise, measured: POST /storage/upload is retired (route ledger); the live upload doors refuse a guest at requireUploadSession (401 AUTH_REQUIRED). ADR-0138 on main names no guest upload door.
  3. Gates: CI on head 4285f5e455: 32 success, 13 skipped, 0 red. Tier H by check-governed-merges --pr 25619 (2 of 2 paths governed; 187 changed lines).
  4. Governed-text review: an isolated contract-tier review (CONTRACT_REVIEW_TIER), read-only, returned PASS. All six ruling constraints map to rules 1–7 of Class 7; rule 8 restates an existing criterion; the two tightenings (no read-back of the guest's own file; owner_id unset) narrow from D9 and D1; 14 citations resolve on origin/main. Non-blocking notes for phase 2: the session-resolver fall-open in bare-kernel mode (the existing model), and sys_file's lifecycle slots are both in use, so the TTL rule needs a status value under the one retention slot or a packages/spec change.
  5. Deviations accepted: the ADR-0056 back-pointer line (precedent ADR-0090 and ADR-0086); Part of as line 1, ahead of the 速读 (os-dev.md); the gate rerun after building the formula/lint closure; the merge of origin/main before opening.
  6. Out of scope, where it went: the pre-existing gap on the class-1 submit path → public forms: an authorization-adjacent gap in how a submission's file field is handled (detail withheld pending maintainer) #25622 (detail withheld). The stale "six door classes" texts in code → E5 of storage/public forms: an anonymous public-form submission cannot carry a file — /storage/upload answers 404 to the guest principal, so a required attachment field is impossible on a public form #25572 (rides the implementation). The open A/B/C choice → the maintainer, here.
  7. Landing clause: Tier H. The PR stays draft and waits for the maintainer's letter and approval; ⛔ the seat neither flips nor enqueues it before an approval from a GOVERNED_APPROVERS account. Under C it closes instead, and storage/public forms: an anonymous public-form submission cannot carry a file — /storage/upload answers 404 to the guest principal, so a required attachment field is impossible on a public form #25572 is re-scoped.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Ruled: C · the maintainer, verbatim, in domain:server seat 2's session (session_01JVTd3PprStGa26aez5omLB), 2026-10-24T13:00Z: 「25619 C 25495 B」 · recorded by domain:server seat 2 · sunhaolin

C = the seat record's option (6383110676), in its words: 访客表单不收附件。供应商先注册账号、登录后再填表上传:不开新的匿名写入门,复用已登录的上传路径;发布时响亮拒绝「公开表单声明了文件字段」.

Consequence for this PR: it is closed unmerged in this act. ADR-0138's door list keeps no guest upload door, so nothing in it changes. needs-user-decision leaves the PR.

What C sets in motion (each on its card):


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants