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
@objectstack-fleet objectstack-fleet Bot removed the needs-user-decision Blocked on a maintainer decision — do not dispatch; decision cards only, never on a PR label 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