Skip to content

Commit 34d144c

Browse files
docs(adr-0138): the guest reaches rows through share links only, never the object's sharing model (D3 item 6, ruling C) (#23326)
Fixes #23266 Clause-②: no This executes item 3 of the maintainer's ruling C on #23266 (comment `6148183570`, 「同意」, on the director seat's reading `6148041259`). ADR-0138 D3 item 6 drops "through the object's declared sharing model and". It now says the guest reaches rows through share links (and, when it exists, an explicit guest sharing rule), never through the object's sharing model. ADR-0090 D9 and D11 are unchanged. The PR has no code path: E1's code is PR #23321. **Widened before approval.** On the seat's answer `6153761503` (option A), two more passages of the same record now match ruling C as well: the Consequences bullet and item 3 of "What the ruling did not settle" (sections 4 and 5 below). This lets the one Tier H approval cover a record that does not contradict itself. **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:engine` seat 1 dispatched (`session_01P88ehrZ6hM19hBzdpsQTTo`), on the claim `6153293163`. ## The change: one file, +28 / −11 `docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md` changes in four places: - D3 item 6 (sections 1 and 2); - one new line continuing the status line (section 3); - the Consequences bullet "The guest is one principal on one pipeline." (section 4); - item 3 of "What the ruling did not settle" (section 5). Nothing else in the file changes. The "Decided by" block is left as it is, and ADR-0090 is untouched. ### D3 item 6, exact before and after Before: ```text 6. **Row scope: one pipeline.** The guest's grant is evaluated by the same row-level pipeline as every caller, with no guest special case. The guest owns nothing (D1), so it reaches rows only through the object's declared sharing model and through share links — ADR-0090 D9's division of labour (the guest position answers which object classes are reachable at all, share links answer which records), and D11's statement that the guest is not an OWD audience. Card E1 pins the guest's row scope for each sharing model before it lands; a result that departs from that division of labour is that card's finding to raise, not a choice to make silently. ``` After: ```text 6. **Row scope: one pipeline.** The guest's grant is evaluated by the same row-level pipeline as every caller, except that the guest never takes the pipeline's sharing-model (OWD) branch: neither `sharingModel` nor `externalSharingModel`. The guest owns nothing (D1), so it reaches rows only through share links (and, when it exists, an explicit guest sharing rule), never through the object's sharing model — ADR-0090 D9's division of labour (the guest position answers which object classes are reachable at all, share links answer which records), and D11's statement that the guest is not an OWD audience. Card E1 pins the guest's row scope for each sharing model before it lands: with a bound read set and no share link, `private`, `public_read`, `public_read_write` and `controlled_by_parent` each read zero rows. This scope is the maintainer's ruling C on [#23266](#23266) ([`6148183570`](#23266 (comment))), which answered the finding E1 raised under this item; the explicit guest sharing rule is named by that ruling and not built. ``` ### 1. The ruled sentence (ruling item 3) Before: > The guest owns nothing (D1), so it reaches rows only through the object's declared sharing model and through share links — ADR-0090 D9's division of labour (…), and D11's statement that the guest is not an OWD audience. After: > The guest owns nothing (D1), so it reaches rows only through share links (and, when it exists, an explicit guest sharing rule), never through the object's sharing model — ADR-0090 D9's division of labour (…), and D11's statement that the guest is not an OWD audience. ### 2. Consistency with ruling C, beyond the ruled sentence (both inside D3 item 6) **2a. The first sentence of item 6.** Under C, "with no guest special case" is no longer true. E1's implementation (PR #23321, `SharingService.buildReadFilter` in `plugin-sharing`) returns deny-all for the guest before the sharing-model branch, so the row-level evaluator now has a guest branch. The new wording takes ruling item 1's words ("the guest does not take the OWD branch (neither `sharingModel` nor `externalSharingModel`)"). Before: > The guest's grant is evaluated by the same row-level pipeline as every caller, with no guest special case. After: > The guest's grant is evaluated by the same row-level pipeline as every caller, except that the guest never takes the pipeline's sharing-model (OWD) branch: neither `sharingModel` nor `externalSharingModel`. **2b. The last sentence of item 6.** It told E1 to raise any departure from the division of labour. E1 raised one, and ruling C answered it. The pin now states what ruling item 1 states, and the sentence records which ruling set the scope. Before: > Card E1 pins the guest's row scope for each sharing model before it lands; a result that departs from that division of labour is that card's finding to raise, not a choice to make silently. After: > Card E1 pins the guest's row scope for each sharing model before it lands: with a bound read set and no share link, `private`, `public_read`, `public_read_write` and `controlled_by_parent` each read zero rows. This scope is the maintainer's ruling C on #23266 (`6148183570`), which answered the finding E1 raised under this item; the explicit guest sharing rule is named by that ruling and not built. ### 3. The amendment record (ADR convention, outside item 6) When a later ruling amends an accepted record, the repository records it as a dated `· **Amended** (DATE, ruling …) — …` line continuing the record's status line. ADR-0087 and ADR-0043 show the form for a ruling on the record itself, and ADR-0090's line 4 shows it for an amendment by another record. Prime Directive #13 names "an amended status line on the old one" as the way a recorded decision changes. One line follows ADR-0138's Status paragraph: > · **Amended** (2026-10-13, ruling C on #23266, `6148183570`, maintainer 「同意」) — D3 item 6: the guest reaches rows only through share links (and, when it exists, an explicit guest sharing rule), never through the object's sharing model; ADR-0090 D9 and D11 are unchanged. ### 4. Consistency with ruling C: the Consequences bullet (seat answer `6153761503`) The bullet said that no guest special case enters the row-level evaluator. Under C, E1's row-level evaluator has exactly one: the deny-all ahead of the sharing-model branch in `SharingService.buildReadFilter`. The edit names that one case and keeps the bullet's CRUD-gate and masker claims word for word. Before: ```text - **The guest is one principal on one pipeline.** No guest special case enters the row-level evaluator, the CRUD gate or the masker; what differs for the guest is what it holds (D3) and where it acts (D4), both resolved before any gate runs. ``` After: ```text - **The guest is one principal on one pipeline.** The row-level evaluator carries one guest special case: the guest never takes its sharing-model (OWD) branch, so it reaches rows only through share links (and, when it exists, an explicit guest sharing rule; D3 item 6). No guest special case enters the CRUD gate or the masker; what differs for the guest there is what it holds (D3) and where it acts (D4), both resolved before any gate runs. ``` **What was checked, against E1 at `1d394b81c3`** (PR #23321's head at this read; diff taken from its merge base `01d503432b`): - **Row-level evaluator.** The one guest branch is in `packages/plugins/plugin-sharing/src/sharing-service.ts`: `isGuestPrincipal(context)` returns deny-all in `buildReadFilter`, ahead of `effectiveSharingModel`. That is the case the new sentence names. - **Masker: kept, true.** E1 does not touch `packages/plugins/plugin-security/src/field-masker.ts`, and that file has zero mentions of `guest` or `principalKind`. - **CRUD gate: kept, under the bullet's own reading.** The permission evaluation has no guest branch: once the sets are resolved, the guest is judged like any caller. E1 does add two guest-specific steps to the gate's prologue in `security-plugin.ts`, and both are the bullet's "what it holds (D3) and where it acts (D4)": - `resolvePermissionSetsForContextUnmemoized` leaves the guest's position out of the set request. This is D3 item 3: the position-name fold never reaches the guest. - `refuseGuestWithoutOrganization` raises D4's located refusal for a guest that carries no organization. - **One nuance, left as is because it is not ruling C's matter.** E1 derives the organization at the guest entry (`resolveAuthzContext`), before any gate runs. When none was derived, the refusal is raised in the CRUD gate's prologue: the gate asks the same step again (`resolveGuestOrganization`). So "both resolved before any gate runs" holds for the derivation, and the refusal surfaces at the gate. This is named for E1's review and not edited here. ### 5. Consistency with ruling C: "What the ruling did not settle", item 3 (seat answer `6153761503`) The item listed the row scope as open ("one pipeline, no special case"). Ruling C has settled it, so the item now points at ruling C and E1's pin. The item keeps its number, and the section's other two items are unchanged. Before: ```text 3. **The guest's row scope** is stated as a contract (one pipeline, no special case, ADR-0090 D9's division of labour); its measured outcome per sharing model is E1's pin. ``` After: ```text 3. **The guest's row scope** is no longer open: ruling C on [#23266](#23266) ([`6148183570`](#23266 (comment))) settled it as share links only, never the object's sharing model (D3 item 6), and E1 pins it: with a bound read set and no share link, `private`, `public_read`, `public_read_write` and `controlled_by_parent` each read zero rows. ``` ## Verification (head `58464a201a`) - **Docs only.** The diff touches no package, so there is no build, test or typecheck target. - **Base.** `origin/main` moved to `4d4040ccb6` but left this ADR unchanged, so no merge was made. The branch is still based on `87e902dcbb`. - **Gate derivation.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, run on this branch's diff (1 path, +28/−11), derives 19 commands, the same set as at dispatch. - **Gate results.** All 19 exit 0 on `58464a201a`. 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)". - `check:doc-formula-expressions` needs `@objectstack/formula` and `@objectstack/lint` built. They were built first under the verify lock (`turbo run build --filter=@objectstack/formula --filter=@objectstack/lint`, VERDICT command-exit 0). - Verdict lines: - `check-adr-links: 730 relative link destination(s) under docs/adr/ resolve` - `check-adr-symbol-anchors: 2334 anchors across 142 records resolve` - `check-adr-anchors: OK (68 anchored file(s), every governing ADR still referenced; … 45590 citation(s) across 5976 file(s) resolve …)`: one fewer citation than at `86e71fdc39`, because the old item 3's "ADR-0090 D9's" is gone. - `check-closing-keyword-parity: OK (3 parsers agree …)` - `check-nul-bytes: OK (… no raw ASCII control bytes)` - the doc-authoring guard: ✓ - **Anchors.** No anchor under `scripts/adr-anchors/` quotes any edited passage, and this diff adds or removes no `path#symbol` anchor. - **Control bytes.** A self-scan of the edited file finds no control bytes. - **Changeset.** None, because nothing publishes: - The ADR path is in no published package's `files[]`. Of 71 non-private manifests, 0 have an entry under `docs/`. The positive control is `packages/spec`, which lists `dist`. - The root package is private. - No package build copies `docs/adr`. - `skip-changeset` applies. ## Acceptance notes These were checked and do not conflict with ruling C, so they are left as they are: - Consequences says "Not measured, and named: the guest's row scope for each sharing model (E1's pins)". That stays true until E1 lands. - D8 notes that the sharing-rule vocabulary already retired the `guest` recipient (the ADR-0087 ledger entry `sharing-rule-recipient-reconcile`). That describes today. The explicit guest sharing rule in ruling item 2 would be a new rule: criteria-based, per object, under the strictest anchor tier, and decided when it is built. The words "when it exists" keep the two apart. - The `**Decided by**` block lists the rulings on #22146 and #22147 but not ruling C. The status-line continuation records C, which is how the other records do it. The seat's answer leaves the block as it is. - The CRUD-gate nuance in section 4 belongs to E1's review, not to this record edit. ## 维护者速读(草稿) - **改了什么:** 只改 ADR-0138 一个文件,共四处文字修改。 1. **D3 第 6 项**,裁决原句:访客只经分享链接触达行;将来有了显式的访客分享规则,也可经它;永不经对象的共享模型。同一项的首句和末句随之对齐(上文 2a、2b)。 2. **状态行**下加一条带日期的修订记录,写明裁决 C 和评论号(上文第 3 节)。 3. **「Consequences」一节「访客是一条管线上的一个主体」**:原文说行级评估器不进访客特例,现改为写明行级评估器唯一的访客特例,即访客不走共享模型(OWD)分支,只经分享链接触达行;关于 CRUD 闸门和字段掩码的说法原样保留,已对 E1 代码逐一核对(上文第 4 节)。 4. **「What the ruling did not settle」第 3 条**:不再把访客行范围列为未定事项,改为指向裁决 C 和 E1 的钉:绑定了读集合、没有分享链接时,四种共享模型都是 0 行(上文第 5 节)。 以上每处都有逐字前后对照。ADR-0090 的 D9、D11 一字未动,「Decided by」一段未动,本 PR 不含任何代码。 - **为什么改:** 你在 #23266 上回了「同意」(裁决 C):访客的行范围只经分享链接,不经共享模型。记录第 6 项原文写的是「经对象声明的共享模型和分享链接」,与裁决相反。同一记录里另有两处也与裁决相抵,席位答复 A:本 PR 一并修订,让一次批准覆盖一份不自相矛盾的记录。按 AGENTS.md 第 13 条,改变已接受记录的决定,要改原文并留修订行。实现代码在 PR #23321,改了原文,它才有记录可依。 - **风险与代价(含回滚):** - 纯文档:不发布任何包,不改运行时行为。 - 第 1 处里的 2a、2b 以及第 2 至第 4 处,都超出裁决原句,属于与裁决对齐的措辞修改,请过目。 - 有一处细节留给 E1 评审,本 PR 不改:E1 把「组织无法确定」的拒绝放在 CRUD 闸门入口处抛出(上文第 4 节)。 - 回滚:revert 本 PR,原文即恢复。 - **席位意见:** - **你要做的:** 1. 看第 1 处里的 2a、2b 以及第 2 至第 4 处这些超出裁决原句的对齐修改,确认同意。 2. 同意就批准本 PR(Tier H,需你本人批准)。 --- _Generated by [Claude Code](https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4f1e435 commit 34d144c

1 file changed

Lines changed: 28 additions & 11 deletions

File tree

‎docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md‎

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ criterion-2 ruling the re-measurement is a test in the repository, green on `mai
1515
([Execution plan](#execution-plan-after-acceptance)), and every change this record decides lands
1616
through them. The four defect cards landed before it: they enforce ADR-0056 D2 and ADR-0104, which
1717
were already Accepted.
18+
· **Amended** (2026-10-13, ruling C on [#23266](https://github.com/objectstack-ai/objectstack/issues/23266),
19+
[`6148183570`](https://github.com/objectstack-ai/objectstack/issues/23266#issuecomment-6148183570),
20+
maintainer 「同意」) — D3 item 6: the guest reaches rows only through share links (and, when it
21+
exists, an explicit guest sharing rule), never through the object's sharing model; ADR-0090 D9 and
22+
D11 are unchanged.
1823
**Decided by**: the maintainer's ruling on [#22146](https://github.com/objectstack-ai/objectstack/issues/22146)
1924
(comment [`6054113537`](https://github.com/objectstack-ai/objectstack/issues/22146#issuecomment-6054113537),
2025
decision batch #290 item 1, 2026-10-08, 「22146 同意」): **A on all eight questions, and G2 on the
@@ -365,12 +370,18 @@ unchanged. **Card**: E2.
365370
(`packages/plugins/plugin-security/src/suggested-audience-bindings.ts#confirmAudienceBindingSuggestion`).
366371
Packages suggest; they never bind (ADR-0090 D9).
367372
6. **Row scope: one pipeline.** The guest's grant is evaluated by the same row-level pipeline as
368-
every caller, with no guest special case. The guest owns nothing (D1), so it reaches rows only
369-
through the object's declared sharing model and through share links — ADR-0090 D9's division of
370-
labour (the guest position answers which object classes are reachable at all, share links answer
371-
which records), and D11's statement that the guest is not an OWD audience. Card E1 pins the
372-
guest's row scope for each sharing model before it lands; a result that departs from that
373-
division of labour is that card's finding to raise, not a choice to make silently.
373+
every caller, except that the guest never takes the pipeline's sharing-model (OWD) branch:
374+
neither `sharingModel` nor `externalSharingModel`. The guest owns nothing (D1), so it reaches
375+
rows only through share links (and, when it exists, an explicit guest sharing rule), never
376+
through the object's sharing model — ADR-0090 D9's division of labour (the guest position
377+
answers which object classes are reachable at all, share links answer which records), and D11's
378+
statement that the guest is not an OWD audience. Card E1 pins the guest's row scope for each
379+
sharing model before it lands: with a bound read set and no share link, `private`,
380+
`public_read`, `public_read_write` and `controlled_by_parent` each read zero rows. This scope is
381+
the maintainer's ruling C on [#23266](https://github.com/objectstack-ai/objectstack/issues/23266)
382+
([`6148183570`](https://github.com/objectstack-ai/objectstack/issues/23266#issuecomment-6148183570)),
383+
which answered the finding E1 raised under this item; the explicit guest sharing rule is named
384+
by that ruling and not built.
374385
7. **The organization comes first.** Bindings are organization-scoped today. With no organization
375386
resolved, the binding read would gather every organization's bindings, the class the grant rule
376387
in `resolveUserAuthzGrants` exists to stop. So D3 never resolves a binding without D4's
@@ -564,9 +575,11 @@ that card lands; no row is declared without an enforcer.
564575
- **A multi-organization deployment's anonymous endpoints answer a located refusal** where they need
565576
an organization, until D5 is built. Today those requests are refused at the CRUD gate with nothing
566577
granted, so no request that is served today stops being served.
567-
- **The guest is one principal on one pipeline.** No guest special case enters the row-level
568-
evaluator, the CRUD gate or the masker; what differs for the guest is what it holds (D3) and where
569-
it acts (D4), both resolved before any gate runs.
578+
- **The guest is one principal on one pipeline.** The row-level evaluator carries one guest special
579+
case: the guest never takes its sharing-model (OWD) branch, so it reaches rows only through share
580+
links (and, when it exists, an explicit guest sharing rule; D3 item 6). No guest special case
581+
enters the CRUD gate or the masker; what differs for the guest there is what it holds (D3) and
582+
where it acts (D4), both resolved before any gate runs.
570583
- **Class 6 changes nothing that serves today.** `authorizeDownload` already honours ADR-0104's
571584
opt-in. Listing it in D2 makes the re-measurement read a `public_read` download as a door and
572585
not as a gap, and [#22431](https://github.com/objectstack-ai/objectstack/issues/22431), the defect
@@ -584,8 +597,12 @@ Stated so that approving this record approves these readings knowingly.
584597
and reserves no key.
585598
2. **The indirect path** (record-change flows fired by a guest-written row) lies outside D2b's
586599
refusal, as stated there: recorded, not decided.
587-
3. **The guest's row scope** is stated as a contract (one pipeline, no special case, ADR-0090 D9's
588-
division of labour); its measured outcome per sharing model is E1's pin.
600+
3. **The guest's row scope** is no longer open: ruling C on
601+
[#23266](https://github.com/objectstack-ai/objectstack/issues/23266)
602+
([`6148183570`](https://github.com/objectstack-ai/objectstack/issues/23266#issuecomment-6148183570))
603+
settled it as share links only, never the object's sharing model (D3 item 6), and E1 pins it: with
604+
a bound read set and no share link, `private`, `public_read`, `public_read_write` and
605+
`controlled_by_parent` each read zero rows.
589606

590607
## Alternatives considered
591608

0 commit comments

Comments
 (0)