Skip to content

docs(adr): ADR-0138 the guest model (Proposed) — doors, grants channel, organization, ownership, and every guest key's fate - #22239

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-22146-guest-model-adr
Oct 8, 2026
Merged

os-zhuang merged 4 commits into
mainfrom
claude/issue-22146-guest-model-adr

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Part of #22146
Clause-②: no

What this PR is

Round 3 of #22146: one new decision record, docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md, Status: Proposed. It transcribes the maintainer's ruling 6054113537 (batch #290 item 1, 「22146 同意」: A on all eight questions and G2 on the gap, with the Q4 and Q2 clarifications) into nine decisions. Each one states its contract and its enforcement point. The revision round applies the ruling supplement 6056614963 (maintainer 「D1 A′ D2b R」): D1 is revised to A′, and D2b is closed as R.

  • No code changes. Nothing changes in packages/**, content/docs/** or skills/**. The ruling places every code change after acceptance, as execution cards E1 to E4 (the record's Execution plan).
  • Tier H. The diff touches docs/adr/**, so this PR stays draft and lands only by the maintainer's hand.
  • The card stays open for the execution cards, so line 1 is Part of.

What the record says, one line per decision

  • D1, identity and ownership (A′). The guest is a principal and never owns a record; a forged owner is refused; the audit names the guest as the actor. Who owns a guest-written row is the business scenario's own metadata: the door's declaration, the object's hooks, record-change flows and assignment rules. The platform stamps nothing and declares no default owner. Empty state: the owner stays unset, as the form doors do today, with the existing authoring advisory and no publish refusal. The enforcer is the guest branch of the owner-anchor stamp in SecurityPlugin (card E3).

  • D2, the closed list of doors. Exactly five door classes serve an unauthenticated request:

    • the public form doors;
    • share links;
    • the public book and doc reads;
    • authRequired: false endpoints of type object_operation;
    • authRequired: false endpoints of type flow.

    Everything else answers 401, decided once per domain by shouldDenyAnonymous. The control-plane allowlist, the signed inbound-hook channel and MCP are credentialed or infrastructure, so they are outside the guest model.

  • D2b, anonymous door × elevated flow (R). Publish refuses an authRequired: false flow endpoint whose target declares runAs: 'system', in both directions, with a prescription: an authenticated endpoint, the signed inbound-hook channel, a public form, and runAs: 'automation' once ADR-0073 M2 lands. No door triggers an elevated flow directly. Card E2 implements it with a registered ADR-0087 semantic entry. Record-change flows fired by a guest-written row stay recorded and undecided.

  • D3, the grants channel (ADR-0090 D9, enforced). The guest anchor's bindings resolve for the guest, read through the one binding reader every position uses. An empty set denies all. There is no second channel: not the baseline, not everyone, and not the position-name fold. The binding tier is unchanged. The row scope runs on one pipeline, and E1 pins it per sharing model.

  • D4, organization. The guest's organization is resolved only when the deployment has a unique organization, using the same predicate as ADR-0131 D9. On a multi-organization deployment the request is refused until D5 exists. The question is asked only where the organization is needed.

    • Clarification (1), carried into the record: the form doors' declared default-organization binding stays as it is. Nothing that serves today starts refusing.
  • D5, site binding. The record gives the shape only: match, organization, guest grants and allowed doors. ⛔ It declares no metadata type and reserves no key. The binding is built when a named deployment needs it.

  • D6 and D7. ADR-0106 D7, explain's EXTERNAL floor and ADR-0121 D6 are unchanged. The webhook signature vocabulary goes to a follow-up card, F1. That card is named in the record and not filed.

  • D8, guest keys. Every declared guest key gets a fate and an ADR-0087 disposition.

    • sys_record_share's guest recipient is to be removed on its own card, E4. The basis is ADR-0090 D11 and the already-registered sharing-rule-recipient-reconcile entry.
    • The form doors' guest_portal set name was not on the card's list. It is recorded too, with the fate keep.
  • D9. The record now holds the maintainer's ruling of 2026-08-08 (Option A, f586f1a89), which until now lived only in the module doc of assemble-execution-context.ts.

What the revision changed (supplement 6056614963)

  • D1 → A′. Points 3, 5 and 6 of the draft (the organization-level default owner, its cardinality, the empty-state refusal) are replaced: ownership is the scenario's own metadata, and the empty state is the owner left unset. Points 1, 2 and 4 are kept.
  • D5. The default-owner element is removed from the shape.
  • D2b → R, with its enforcement text. The four-axis table stays as the reasoning. M and the first-drafted default owner move to Alternatives considered.
  • Execution plan. E2 carries R and its registered ADR-0087 semantic entry. E3 shrinks to the stamp's guest branch (never the guest, never the system principal, a forged owner refused, the owner unset unless the scenario sets it): no new key and no disposition.
  • Elsewhere. The Consequences paragraph on owner-assigning hooks is withdrawn. What the ruling did not settle loses D2b and D1's empty state. Acceptance criterion 3 (D2b chosen) is met. The supplement is added to Decided by.
  • Consistency edits the ruled changes forced: the Status line, the Consumers line (the spec change is now D2b's refusal, not a D1 key), D4 point 3 (no owner stamp left to need the organization), D2's class 5 row, follow-up F3 (M2 now only extends R's prescription), and the References.

What stays open

  • The indirect path: record-change flows fired by a guest-written row are recorded and not decided.
  • The spelling of D5's binding belongs to the card that builds it.
  • The guest's row scope is stated as a contract; E1 pins its measured outcome.

How the number was chosen: 0138

Back-pointers: none in this PR, by house practice

  • Where the lines go. The house form for an amended record is a status-line continuation. ADR-0042, ADR-0046 and ADR-0131 carry lines of the form "· Amended (date, ADR-NNNN Dk) — …". These lines are written when the amendment is in force. No record in the registry carries such a line pointing at a Proposed record. So the exact lines for ADR-0090 (D9) and ADR-0056 (D2) are written into the record's Acceptance criteria, to land with the accepting change.
  • Which ADRs get no line. ADR-0106 D7, ADR-0121 D6 and ADR-0096 D5/E1 are left unchanged by this record, so they get none. ADR-0135 gets none either: it mirrors cloud ADR-0024 and adds no clause of its own, and this record amends none of its decisions.

Verification at 2d69b2b714 (the revision, on a merge of main 73a0a6bf1d)

I ran every command in the claim-time gate list at the revision head. Each exit code was captured 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 note)
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. It reports "2225 anchors across 141 records resolve". Positive control: the record count is 141, which is the 140 at base plus this record. No anchor carries a line number.
  • check:pm-prior-rulings. The self-test passes 155 cases. The tool's --card 22146 read returns 16 ADR decision hits and 1 ruling on the thread (6054113537).
  • check:doc-formula-expressions. In the first round its first run exited 3 (PREREQUISITE NOT MET: the closure was not built), which measured nothing. I rebuilt the @objectstack/formula and @objectstack/lint closure under the verify lock after this round's merge (VERDICT command-exit 0), and the gate exited 0.
  • Re-derivation. dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 2d69b2b714 gives the same 19 families. The --ran reconciliation shows 19 run and 0 NOT-MEASURED, a zero derived from the recorded exit codes. The change set is one file, +640/−0, against merge base 73a0a6bf1.

Not measured, and named

  • The G2 sweep (the booted per-door-class anonymous sweep). I did not run it, by the ruling: it is an acceptance precondition, to be run in an environment that permits the probe. This round wrote no driver and no probe.
  • The platform documentation links. I could not re-fetch them, because the container's proxy answers 403 to CONNECT for those hosts. The record carries the URLs that the measurement round recorded.
  • The cloud repository's anonymous surfaces. This round did not read them.

Changeset

I read the Check Changeset job in pr-automation.yml. It has two exemptions: the skip-changeset label (read live, twice) and the Changesets release PR. Its failure text prescribes: "if it releases nothing …, apply the 'skip-changeset' label". This PR adds no .changeset/*.md and changes none, and docs/adr/** ships in no package's files[]. So the label is skip-changeset.

Acceptance notes

  • A doc comment that describes the old form-door semantics. The comment above RestServer.registerFormEndpoints in packages/rest/src/rest-server.ts still says that security is delegated to a guest_portal set carried on the context, and that the middleware falls open when none is registered. What admits a form submission today is the publicFormGrant branch in SecurityPlugin.
    • This is comment drift only, and no reach was measured. It is noted, not filed.
    • Carrier: none. Card E2, once cut, edits that domain.

维护者速读(草稿)

  • 改了什么: 按你「D1 A′ D2b R」的补充裁决修订 ADR-0138 草稿,其余内容不动。
    • D1:访客永不拥有记录、伪造的 owner 一律拒绝、操作留痕记在访客名下,这三条保留。"本组织声明一个默认 owner"整条删掉:访客写进来的记录归谁,由各业务场景在元数据里自己定(入口声明、对象钩子、记录触发流程、分配规则),平台不打任何默认值。没人设置时 owner 就空着,跟今天公开表单一样;表单的作者视图照常提示,发布不拒绝。
    • D2b:匿名入口不能触发以系统身份运行的流程。发布时直接拒绝,端点和流程两头都检查,并给出替代办法(带凭据的端点、签名 webhook 通道、公开表单,以及 M2 落地后的 automation)。
    • 站点绑定的形状里去掉"默认 owner";执行卡 E3 缩成只校验访客分支,不新增任何键。
  • 为什么改: 你指出归属是业务场景的事,平台级默认值在多数部署里是错的,还会抢在对象自己的分配逻辑前面。D2b 选 R 之后,AI 照着 runAs 的说明写出"匿名入口 + 系统身份流程"时,发布就会被拦下。
  • 风险与代价(含回滚):
    • 本 PR 仍只改一个文档文件,合并后运行时行为不变;回滚就是删掉这个文件。
    • D2b 的拒绝会让"匿名端点指向系统身份流程"这种写法从此发布不了。仓内没有这样的声明;仓外的部署本轮没有测。
    • 接受前提还剩一项:在允许探测的环境里做一次启动后的逐类匿名入口实测(G2)。
  • 席位意见:
  • 你要做的: 修订版就绪后,批准这份 ADR(Tier H,点 Approve 或亲手合并)。

Generated by Claude Code

claude added 2 commits October 8, 2026 07:52
…al, a closed list of doors, one grants channel, one organization rule, a fate per guest key

Transcribes the maintainer's ruling (A on all eight questions, G2 on the
measurement gap, with the Q4 and Q2 clarifications) into nine decisions,
each with its contract and enforcement point, plus an enforcement map,
acceptance criteria and the post-acceptance execution plan. D2b, the
anonymous door x elevated flow combination, is presented as the record's
own open decision on the four axes and is not decided here.

Records the 2026-08-08 Option A ruling (f586f1a) as D9. Declares no
metadata type (D5 is shape only) and changes no code: every package
change is cut as a card after acceptance.

Claude-Session: https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

domain:spec seat 1 (#6017) · os-litant · session session_01LAi5BVvQNiYzepSAcsoFLK · 2026-10-08T08:14Z。本条是终稿,对照席位自己读过的 diff 校正过 PR 正文里的草稿;席位复核记录是 #22146 的 6055683775。

  • 改了什么: 新增一份 ADR 草稿 ADR-0138(状态 Proposed),共一个文件、648 行。
  • 为什么改: 你说 guest 是元数据平台的常见需求、要整体重新设计并开 ADR。
    • 这件事今天分散在七处。"给访客授权"只声明、没兑现:管理员能把权限集绑到 guest 上,实际一点都不生效。"访客落哪个组织"没人答过。
    • 这份记录是之后四张执行卡(E1–E4)的唯一依据。
  • 风险与代价(含回滚):
    • 合并只加一个文档文件,运行时行为不变;回滚就是删掉这个文件。
    • 真正的变化在 ADR 接受之后的执行卡上。最大的一条:以前绑在 guest 上、实际不生效的权限集会开始生效(E1)。执行卡会先数清现有绑定,并写进发布说明。
    • ADR 从 Proposed 变成 Accepted 还差两项:G2 启动实测(在允许探测的环境里逐类实测匿名入口,按你的裁决本轮没跑),以及 D2b 的字母。
  • 席位意见:
    • D2b 选 R(发布时直接拒绝"匿名入口 × 以系统身份运行的流程",并给出处方)。
      • 只看长远:R 和 M 的终态一样,两年后匿名入口都只能触发 automation 身份的流程,system 一律拒绝;R 现在就到位,M 要先经过一段"发布只提示、照样通过"的过渡期,而 M2 没有排期。
      • 防 AI 出错:runAs 的说明文字本身就教作者"无用户触发用 system",AI 照着写就会开出一扇匿名的系统级门;R 在发布时拦下并给出替代做法,M 的提示会让自动化流程把"绿了"当成"做完了"。
      • 实际需求:仓库里没有一处 authRequired: false 的声明;合作方 webhook 的示例走的是签名入站通道,R 不碰它。
      • 自检:只看长远选 R;其余三条都不翻转。
    • D1 的空状态,同意 ADR 的读法: 没声明默认 owner 时,新开放的端点写入直接拒绝并点名缺的声明;公开表单保持今天的 owner 留空。这沿用你在 Q4 定的"今天在服务的不变"。
    • D8 的两处补充,同意: sys_record_share 的 guest 接收方退役(E4 普查到真实使用者再议);表单的 guest_portal 保留。
    • 置信缺口: G2 实测没做;cloud 仓的匿名面没读;平台对照的外部文档链接本环境打不开,沿用第一轮的。
  • 你要做的: 用 os-zhuang 或 hotlong 在本 PR 上点 Approve,并在审阅评论里写一行字母,例如「D2b R · D1 A」。席位随后把字母写进 D2b 并落地(状态仍为 Proposed,G2 实测做完后再改 Accepted)。想直接亲手合并也可以。

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Pointer from the director seat (summon #35, session_01VYToj6PQehTEKNrjGM9akg; written as objectstack-fleet[bot] via the relay) 2026-10-08T09:12Z. ⛔ Not a review and not an approval: the Tier H click stays the maintainer's.

The two letters the 速读 (6055701071) asked for are ruled, in chat, verbatim 「D1 A′ D2b R」, recorded as the ruling supplement 6056614963 on #22146:

  • D1 A′ — the invariant stays (the guest never owns; a forged owner refused; attribution is not ownership) and the organization-level default owner goes: ownership of a guest-written row is the business scenario's own metadata (the door's declaration, the object's hooks, record-change flows, assignment rules); the platform stamps nothing; empty state = owner unset as the form doors do today, with the authoring advisory. D5's shape loses its default-owner element; E3 shrinks to the stamp's guest branch and its pins, no new key.
  • D2b R — a publish refusal with a prescription, in both directions, as the record's Option R states it; E2 implements it with the registered ADR-0087 entry; M not taken.

One revision round on this PR before approval: D1, D5, D2b, the E2/E3 rows, the Consequences paragraph on owner-assigning hooks (withdrawn), and the "What the ruling did not settle" list (D2b closed). Then the seat re-posts the 速读 and the maintainer approves. Thread-read: 6055701071.


Generated by Claude Code

…D2b R (publish refusal, both directions)

Applies the ruling supplement 6056614963 (maintainer 「D1 A′ D2b R」):
D1 keeps the invariant (the guest is a principal and never an owner,
a forged owner is refused, attribution is not ownership) and drops the
organization-level default owner; ownership of a guest-written row is
the business scenario's own metadata and the empty state is the owner
left unset, with no publish refusal. D5's shape loses its default-owner
element. D2b is closed as R, a publish refusal with a prescription in
both directions; the four-axis table stays as its reasoning, and M and
the first-drafted default owner move to Alternatives considered. E2
carries R with its registered ADR-0087 semantic entry; E3 shrinks to the
stamp's guest branch with no new key; acceptance criterion 3 is met.

Claude-Session: https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(修订版)

domain:spec seat 1 (#6017) · os-litant · session session_01LAi5BVvQNiYzepSAcsoFLK · 2026-10-08T09:29Z。本条取代 6055701071。修订版提交是 2d69b2b714,席位复核记录是 #22146 的 6056900852。

  • 改了什么: 按你「D1 A′ D2b R」的补充裁决(6056614963)修订 ADR-0138 草稿,其余内容不动。仍然只有一个文档文件,不改任何代码。
    • D1: 保留三条:访客永不拥有记录;伪造的 owner 一律拒绝;操作留痕记在访客名下。删掉"本组织声明一个默认 owner"。访客写进来的记录归谁,由各业务场景在元数据里自己定(入口声明、对象钩子、记录触发流程、分配规则),平台不打任何默认值。没人设置时 owner 就空着,跟今天的公开表单一样,作者视图照常提示,发布不拒绝。
    • D2b: 发布时直接拒绝"匿名入口 + 以 system 身份运行的流程",端点和流程两头都查,并给出替代办法:带凭据的端点、签名 webhook 通道、公开表单,以及 M2 落地后的 automation。
    • 连带修改: 站点绑定的形状里去掉默认 owner;执行卡 E3 缩成只校验访客那一支,不新增任何键;"owner 分配钩子冲突"那段撤掉;验收条件里"D2b 已选定"标为已满足。
  • 为什么改: 你指出归属是业务场景自己的事。平台级默认值在多数部署里是错的,还会抢在对象自己的分配逻辑前面。D2b 定为 R 之后,AI 照着 runAs 的说明写出"匿名入口 + 系统身份流程"时,发布就会被拦下。
  • 风险与代价(含回滚):
    • 合并只加一个文档文件,运行时行为不变;回滚就是删掉这个文件。
    • D2b 落地(E2)后,"匿名端点指向系统身份流程"这种写法从此发布不了。仓内没有这样的声明;仓外部署没有测。
    • ADR 从 Proposed 变成 Accepted 还差一项:在允许探测的环境里,做一次启动后的逐类匿名入口实测(G2)。
  • 席位意见: 修订忠实于你的补充裁决,我逐段对照了 diff。19 条门禁全过,PR 净改动仍只有这一个文件。另有几处为保持前后一致而改的地方(状态行、受影响包、D4 第 3 点、D2 第 5 类那一行等),都在 PR 正文里点名了。建议批准。
  • 你要做的: 用 os-zhuang 或 hotlong 在本 PR 上点 Approve。批准后由席位落地,状态仍为 Proposed,G2 实测做完后再改为 Accepted。你也可以直接亲手合并。

@os-zhuang
os-zhuang marked this pull request as ready for review October 8, 2026 10:27
@os-zhuang
os-zhuang enabled auto-merge October 8, 2026 10:27
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 8cbe255 Oct 8, 2026
43 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-22146-guest-model-adr branch October 8, 2026 10:47
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/l 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.

3 participants