Skip to content

docs(adr-0138): the guest's bind door is built implementation-first, with its shape recorded (D3 item 5, ruling C) - #23370

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-23267-adr0138-guest-door-shape
Oct 14, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-23267-adr0138-guest-door-shape

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23267
Clause-②: no

This executes the maintainer's ruling C on #23267 (comment 6159279205, the director seat's record of the maintainer's words 「23267 我们的系统 和 salesforce 类似,暂时没有 没有公开站点或门户的产品计划」, on the seat's reading 6158189687). ADR-0138 D3 item 5 no longer states the guest's bind door as live. The door is built implementation-first, and the item now records the door's shape (ruling items 1 to 3), the refusal of the definition door (item 4), and the restart condition with the two items the builder re-reads (item 5). The PR has no code path. Nothing in packages/** moves, and E1's reader stays at the declared empty state (D3 item 2, deny-all).

Tier H (docs/adr/**). This PR stays a draft until the maintainer approves it. Before that approval no seat readies it, queues it or arms auto-merge.

Drafted by the os-dev agent that domain:spec seat 2 dispatched (session_01BRQ9Mp1yH7R1az6MaUM18H), on the claim 6159342669. The precedent is PR #23326, the amendment of D3 item 6 under ruling C on #23266.

The change: one file, +41 / −8

docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md changes in five places:

  1. D3 item 5, rewritten (the ruled text; section 1);
  2. one new line continuing the status line, in the form PR docs(adr-0138): the guest reaches rows through share links only, never the object's sharing model (D3 item 6, ruling C) #23326 used (section 2);
  3. the Consequences bullet "Bindings confirmed while inert start granting (D3)", which named the two paths as live (section 3);
  4. one Enforcement map row for what the amended item declares (section 4);
  5. one follow-up, F4, beside F2 (the site binding) (section 5).

Nothing else changes. No other decision is edited: D3 items 2 and 4, D8 and the rest read as before. The "Decided by" block and ADR-0090 are untouched. The status stays Accepted, as in the precedent.

1. D3 item 5, exact before and after

Before:

5. **How a deployment grants.** An administrator binds a low-privilege set to `guest`, directly or
   by confirming a package's suggestion
   (`packages/plugins/plugin-security/src/suggested-audience-bindings.ts#confirmAudienceBindingSuggestion`).
   Packages suggest; they never bind (ADR-0090 D9).

After:

5. **How a deployment grants: implementation-first.** The bind door is built when a named
   deployment needs to grant the guest on a public surface (a site or portal) and the class-1 form
   doors cannot serve it. Until then E1's reader stays at the declared empty state (item 2),
   because no door binds a set to `guest`: a save of the packaged `guest` position answers 403
   `NOT_OVERRIDABLE`, the package door refuses a built-in position name, and
   `packages/plugins/plugin-security/src/suggested-audience-bindings.ts#confirmAudienceBindingSuggestion`
   refuses a `guest` row, which the suggestion sync never produces.
   **The door's shape.** An act of the environment's administrator, never of a package (ADR-0090
   D9), writes one confirmed `sys_audience_binding_suggestion` row for anchor `guest` (D8's `guest`
   value, live with this door), `package_id` optional, scoped to the public surface that publishes
   the door: the binding carries the app or site it serves, and the anchor stays one identity. The
   security plugin re-derives the `guest` definition from the accepted rows, as it does
   `everyone`'s, and on every write and every derivation runs
   `describeAnchorForbiddenBits(set, 'guest')` and refuses the bypass list's objects (those record
   sharing never scopes, declared once in `@objectstack/spec/security`; today plugin-sharing's
   `SharingService` holds the list). No caller writes an anchor's definition
   ([#22801](https://github.com/objectstack-ai/objectstack/issues/22801) ruling A,
   [`6108478638`](https://github.com/objectstack-ai/objectstack/issues/22801#issuecomment-6108478638)),
   so ⛔ opening that door (`PUT /meta/position/guest`) is refused: it reverses the seal and misses
   environment kernels. The row half stays item 6: share links and, when built, an explicit guest
   sharing rule (read-only, criteria-based, the strictest lint tier), the only shape in which D8's
   retired `guest` recipient returns. **Re-read when the door is built:** item 4's guest tier of
   `assertAudienceAnchorBindingGate`, which guards `sys_position_permission_set` rows that no
   binding reader reads, and objectui's pin that a guest row offers no Accept
   (`objectui:packages/app-shell/src/components/SuggestedBindingsPanel.tsx`). Amended by ruling C
   on [#23267](https://github.com/objectstack-ai/objectstack/issues/23267)
   ([`6159279205`](https://github.com/objectstack-ai/objectstack/issues/23267#issuecomment-6159279205)).

Where each ruling item sits in the new text:

What the new text adds beyond the ruling's words, for review:

What was measured before writing (origin/main dc393266f)

  • The metadata door: PUT /meta/position/guest answers 403 NOT_OVERRIDABLE. This is pinned on main by E1's dogfood case "the metadata door refuses a save of the packaged guest position" (packages/qa/dogfood/test/guest-grants-channel.dogfood.test.ts).
  • The package door: BUILT_IN_SECURITY_CATALOG_NAMES.position holds both audience anchors (packages/objectql/src/security-catalog-namespace.ts). The package door's refuseSecurityCatalogNameConflicts refuses a package that declares one. registry-security-catalog-namespace.test.ts pins "a built-in no package can declare".
  • The suggestion path: confirmAudienceBindingSuggestion refuses a row whose anchor is not everyone (409 SUGGESTION_STATE). collectDeclaredSuggestions hard-codes the anchor to EVERYONE_POSITION, so no guest row is produced.
  • The guest definition: registerBuiltinPositions declares guest with no permissionSets. Only everyone has a re-declaration seam (declareEveryoneBaseline).
  • E1's reader: it reads the guest position's catalog definition through positionBoundSetNames, the one position-bound read (packages/core/src/security/resolve-authz-context.ts). The A1 shape re-derives that definition, as the plugin does for everyone.
  • The dormant tier: assertAudienceAnchorBindingGate returns unless the write targets sys_position_permission_set. The [Decision] 装包时包里声明的「默认权限集」怎么生效:管理员整部署接受一次、装包即自动生效,还是取消这条路(安全边界) #22801 ruling's premise reading records that the resolver does not read that table.
  • The bypass list: SharingService's constructor holds sys_record_share, sys_user, sys_organization, sys_member and sys_user_permission_set, plus host extras.
  • The objectui pin: at objectui origin/main 2b6f7609, SuggestedBindingsPanel.test.tsx holds "a guest row offers no Accept, says why, and keeps Dismiss".

2. The amendment record

One line follows the existing · **Amended** line (ruling C on #23266):

· **Amended** (2026-10-14, ruling C on [#23267](https://github.com/objectstack-ai/objectstack/issues/23267),
[`6159279205`](https://github.com/objectstack-ai/objectstack/issues/23267#issuecomment-6159279205),
maintainer 「23267 我们的系统 和 salesforce 类似,暂时没有 没有公开站点或门户的产品计划」) — D3 item 5: the
guest's bind door is built implementation-first, when a named deployment needs to grant the guest on
a public surface; until then no door binds a set to `guest`, and the record carries the door's shape.

The quotation is byte-identical to the ruling comment's first line, including its full-width comma.

3. Consistency: the Consequences bullet

The bullet named "directly or through a confirmed suggestion" as the population E1 would start granting. Neither path writes a binding, and E1's census counted none.

Before:

- **Bindings confirmed while inert start granting (D3).** A set bound to `guest` today, directly or
  through a confirmed suggestion, grants nothing. On the release that lands E1 it grants what it says,
  to anonymous callers at the guest-entry doors. Every such binding passed the strictest anchor tier,
  and the administrator who confirmed it believed it granted, so D3 honours a declaration rather than
  widening one. The change is still visible: E1's changeset names it, and E1 counts the population
  on the reference apps before it lands.

After:

- **Bindings confirmed while inert start granting (D3).** A set bound to `guest` before E1 granted
  nothing. On the release that lands E1 it grants what it says,
  to anonymous callers at the guest-entry doors. Every such binding passed the strictest anchor tier,
  and the administrator who confirmed it believed it granted, so D3 honours a declaration rather than
  widening one. The change is still visible: E1's changeset names it, and its count on the reference
  apps found none. None can arise before D3 item 5's door is built: until then no door binds a set
  to `guest`.

The short second line is deliberate: the two middle lines stay byte-identical, so the diff shows only what changed. The rendered text is the same either way.

4. Consistency: one Enforcement map row

The map lists "every declared thing in this record, and what enforces it". The amended item declares that no door binds a set to guest until the door is built, so it gets a row:

| no door binds a set to `guest` until the bind door is built | D3 item 5 (amended) | the anchors' seal (`NOT_OVERRIDABLE`; the package door's built-in names) and `confirmAudienceBindingSuggestion`'s refusal of a `guest` row | enforced; the door is follow-up F4 |

5. Consistency: follow-up F4

The record names an unbuilt door the way F2 names D5's site binding:

- **F4 — the guest's bind door** (D3 item 5), built when a named deployment needs to grant the
  guest on a public surface and the class-1 form doors cannot serve it; its shape and the two items
  it re-reads are recorded in D3 item 5.

The two re-read items sit in D3 item 5 itself, where the door's builder reads the shape. F4 points there and does not copy them.

Size against the precedent and the estimate

  • The diff is +41 / −8; the precedent is +28 / −11. D3 item 5 alone is +28 / −4, against the ruling's estimate of about 20 lines.
  • Why item 5 runs longer: it carries five ruling items, three measured refusals, and two re-read pointers. Three citations with full links take six of its lines.
  • Why the rest runs longer: the three consistency edits (sections 3 to 5) add a net 5 lines.

Verification (head cc9a40576)

  • Docs only. The diff touches no package, so there is no build, test or typecheck target of its own.
  • Base. origin/main is still dc393266f at the gate run, and the ADR is unchanged there since the branch point.
  • Gate derivation. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, run on the final tree (1 path), derives 19 commands, the same set as the dispatch's lead list.
  • Gate results. All 19 exit 0 on cc9a40576. The --ran reconciliation reads: "19 derived famil(ies) accounted for — 19 run, 0 NOT-MEASURED (a DERIVED zero — all 19 recorded an exit code and none of them is 3)".
    • Prerequisite build: check:doc-formula-expressions needs @objectstack/lint and its closure built. They were built first under the verify lock (turbo run build --filter=@objectstack/lint --concurrency=2: 4 tasks successful, VERDICT command-exit 0).
    • Verdict lines:
      • check-adr-links: 730 relative link destination(s) under docs/adr/ resolve
      • check-adr-symbol-anchors: 2335 anchors across 142 records resolve. The new cross-repo anchor is reported, not judged, without an objectui checkout. Re-run with OBJECTUI_CHECKOUT=/home/user/objectui (read-only), it also exits 0 with the same line.
      • check-adr-anchors: OK (68 anchored file(s), every governing ADR still referenced; … 45873 citation(s) across 6019 file(s) resolve …)
      • check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords …)
      • check-nul-bytes: OK (scanned 11352 text file(s) … no raw ASCII control bytes)
      • the doc-authoring guard: ✓
  • Control bytes. A self-scan of the edited file finds none.
  • Changeset. None, because nothing publishes. docs/adr/** is on the no-publish fast path, and no package's files[] ships it. skip-changeset applies.

Acceptance notes

These were read, do not conflict with the amendment, and are left as they are:

  • D8's row for the suggestion anchor's guest value says "enforce — live once D3 lands". D8 is a separate decision, so it is not edited. Under the amendment D3 lands in full only with item 5's door, and item 5 now says the value goes live with that door.
  • The Context reading at 7b926f7600 says "A package may suggest a guest binding". It is a dated measurement, and dated readings are left as written. At dc393266f the suggestion sync produces everyone rows only.
  • D3's "Enforced by" paragraph and the Consequences "Not measured" line still describe E1 as unbuilt. They concern E1's landing record, not item 5, so they are not touched here.
  • The Enforcement map's "the anchor binding tier … enforced" row is unchanged. Ruling item 5 keeps D3 item 4's dormant guest tier as a re-read item, not a fix now, and item 5 names it.
  • E1's changeset (.changeset/23224-guest-grants-channel.md) says a set starts granting "only where a deployment holds an environment definition of the guest position that names it". That is true today as a necessary condition. Once the door is built, the grant arrives through the plugin's re-derived definition instead, so the builder's own changeset should say that.
  • Three texts outside this file still state the old path as live. They are not edited here (one-file surface) and are listed in the dev report for the seat:
    • ADR-0090 D9's last bullet ("a package may suggest bindings to everyone or guest; the admin confirms each individually");
    • the docblocks of packages/lint/src/validate-security-posture.ts ("a set an operator binds to guest at install time …");
    • the anchor constants' docblock in packages/spec/src/identity/position.zod.ts ("Packages SUGGEST bindings to these anchors at install time").

维护者速读(草稿)

  • 改了什么: 只改 ADR-0138 一个文件,共五处,不含任何代码。
    1. D3 第 5 项(裁决正文):原文说管理员可以「直接绑」或「确认包的建议」,给访客绑权限集。现改为「实现优先」:有具名部署需要在公开站点或门户上给访客授权,且公开表单门满足不了时,才建这扇门;在那之前,没有任何门能给访客绑权限集,访客保持全拒的声明空态。同一项写下将来建门时的形状:由环境管理员执行、写一条已确认的绑定记录、按服务的站点或应用分范围、每次写入和派生都过守卫、绕行名单只声明一份。同一项也写明拒绝改开定义口(A2)及其理由,行范围仍按 [Decision] 访客(guest)的行可见范围:沿用对象的内部共享模型(A,现状,公开对象全部行可见)、按 externalSharingModel 判(B)、还是只经分享链接(C) #23266 的裁决 C,并列出建门时要复读的两条遗留事项。
    2. 状态行下加一条带日期的修订记录,写明裁决 C 和评论号,引用您的原话(逐字节一致)。
    3. 「Consequences」一节中原先写着「直接绑或经确认的建议」的那条,改为如实写明:E1 的普查结果为零,门建成前也不会出现。
    4. **执行表(Enforcement map)**加一行,写明「门建成前无门可绑」由哪三处现有拒绝兜底。
    5. 后续事项加一条 F4,与 F2(站点绑定)同一写法,指回 D3 第 5 项。
  • 为什么改: 您在 security: no door can bind a permission set to the guest anchor — ADR-0138 D3 item 5 has no enforcer (PUT /meta/position/guest answers 403 NOT_OVERRIDABLE; guest suggestions are refused) #23267 上答「我们的系统和 Salesforce 类似,暂时没有公开站点或门户的产品计划」,总监席据此裁 C。原第 5 项写的两条路今天实测都走不通:保存访客岗位定义被 403 拒绝,确认访客建议被 409 拒绝,而且没有任何包能产生访客建议。按 AGENTS.md 第 13 条,改变已接受记录的决定,要改原文并留修订行。
  • 风险与代价(含回滚):
    • 纯文档:不发布任何包,不改运行时行为,没有部署因此多出或少了权限。
    • 超出裁决原话、请您过目的有两类。一是第 3 至第 5 处的一致性修改。二是第 5 项里另写进的四点:三处实测拦截点;「做法同 everyone」;「D8 的访客值随门一起生效」(D8 本身没改);「绕行名单今天在 plugin-sharing 的 SharingService 里」。
    • 篇幅为 +41/−8,比先例的 +28/−11 长;第 5 项本身 +28/−4,比裁决估计的约 20 行长。原因是第 5 项要装下裁决的五条内容和三处带链接的引用。
    • 本 PR 之外还有三处文字仍把旧路写成可用:ADR-0090 D9 的最后一条、lint 包的两段注释、spec 包里锚点常量的注释。按派发要求本 PR 不动它们,已交席位处理。
    • 回滚:revert 本 PR,原文即恢复。
  • 席位意见:
  • 你要做的:
    1. 看第 5 项新增的四点和第 3 至第 5 处一致性修改,确认同意。
    2. 同意就批准本 PR(Tier H,需您本人批准)。

Generated by Claude Code

…with its shape recorded (D3 item 5, ruling C)

D3 item 5 no longer states the bind door as live. Neither path it named
writes a binding: a save of the packaged guest position answers 403
NOT_OVERRIDABLE, and confirmAudienceBindingSuggestion refuses a guest row,
which the suggestion sync never produces. The item now records the door as
implementation-first (built when a named deployment needs to grant the
guest on a public surface and the class-1 form doors cannot serve it), the
door's shape, the refusal of the definition door, the row half, and the two
items the builder re-reads. One status-line continuation records the
amendment (ruling comment 6159279205).

Kept consistent in the same file: the Consequences bullet that named the
two paths, one Enforcement map row for what the amended item declares, and
follow-up F4. No other decision is edited.

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

Copy link
Copy Markdown
Contributor Author

维护者速读

domain:spec 席位 2(#18549)· os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H。终稿:席位已对照 diff(cc9a40576,仅 ADR-0138 一个文件,+41/−8)校正 dev 的草稿,并填写席位意见。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review October 14, 2026 03:08
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 14, 2026
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/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants