Repository navigation
ADR-0138 E1: the grants channel — the guest entry resolves D4's organization, then the guest anchor's bindings (D3, D4) #23224
Description
This card carries execution card E1 of ADR-0138, which is now Accepted (docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md). PR #23219 accepted it on the maintainer's approval (os-zhuang, review 5501651056), merged as 38a2d3c76, and closed #22146. The record's Execution plan orders: "The seat cuts these cards from the accepted record."
Filed by domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H, at #22146's landing. ⛔ Not a claim. Reader who acts: triage, to grade and route. The landing package decides the domain; the record's Consumers line names the packages. Then the owning seat.
The record's row, verbatim
| Card | Decisions | What lands | Pins | ADR-0087 disposition | Order |
|---|---|---|---|---|---|
| E1 — the grants channel | D3, D4 | the guest entry resolves D4's organization, then the guest anchor's bindings; the located refusal where D4 refuses |
no binding: denied on class 4; a bound read set: that object only; the baseline, everyone and the name fold never reach the guest; a multi-organization deployment: the located refusal; the form doors unchanged; the guest's row scope for each sharing model; the bindings that start granting, counted on the reference apps |
none: no authorable key, accept set or stored shape moves; the changeset names the bindings that start granting | first |
Where the record decides it
- D3 — The grants channel: ADR-0090 D9, enforced — Enforced by, quoted: "the anonymous branch of
resolveAuthzContext, which today returns before any position or binding is resolved. That branch is the declared-not-enforced point; it resolves theguestanchor's bindings in D4's organization, and the set resolution inpackages/plugins/plugin-security/src/security-plugin.ts#SecurityPlugin.resolvePermissionSetsForContextUnmemoizedthen treats them as any requested set." - D4 — The organization a guest acts in — Enforced by, quoted: "the guest entry's organization step, sharing
resolveSystemWriteOrganization's predicate, delivered with D3 because D3 cannot resolve without it. The refusal's code comes from the ADR-0112 catalog."
⛔ Nothing here re-decides the record. A measurement that departs from it is the claimant's finding to raise, never a choice to make silently. D3 item 6 says so for E1's row-scope pins.
Acceptance
Each pin in the row above lands with a negative control that fails when the behavior is removed. The changeset carries the row's ADR-0087 disposition.
Dedupe
MCP search_issues on this repo, closed included, run in the filing act: ADR-0138 execution card guest anchor bindings grants channel E1 E2 E3 E4 guest recipient sys_record_share → 6 hits. Of these, #22146 is the card these are cut from, and #21158 (closed) is D3's original finding. None is an execution card. retire sys_record_share guest recipient select option → 0 hits.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 13, 2026
Triage: first grade,
enhancement·security·priority:p1·target:v18·domain:engine·area:access, intopm:queue. ADR-0138 E1, first in the record's orderTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-13T10:57Z. ⛔ Not a claim, ⛔ not a dispatch.securitytarget:v18: it executes the accepted guest model (ADR-0138, design(v18): the complete guest model in one ADR — identity, doors, grants channel, organization, public-site binding, disclosure, rate limits, and each declared guest key's fate (ADR-0090 D9 enforce-or-remove) #22146 p1securitytarget:v18). E1 is the grants channel, the declared-not-enforced point D3 names. E3 (ADR-0138 E3: the owner-anchor stamp's guest branch — never the guest, never the system principal, a forged owner refused (D1) #23226) waits on it.@objectstack/core(security/: the anonymous branch ofresolveAuthzContext, and the guest entry's organization step sharingresolveSystemWriteOrganization's predicate).packages/coreisdomain:engineby the domain table.objectstack-fleet commentedon Oct 13, 2026
Claim: PM loop round 1 · 2026-10-13T11:14Z
Session:
session_01ADzJtzYTLUfgrRZHxkagkXAccount:
os-steve(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23224-guest-grants-channelWorktree:
objectstack-issue-23224Domain:
domain:engineSeat:
domain:engine#2(seat post #20966)Triage-ref: 6144021196 (direction: ADR-0138's E1 row, verbatim; ⛔ nothing re-decided; a measurement that departs from the record is a finding to raise; each pin lands with a negative control; the changeset carries the row's ADR-0087 disposition)
File surface (read on
origin/main197fbe2c04; stop on a breach and explain it in the report). ⛔ Classes, positions and functions only, in every artefact.packages/core/src/security/resolve-authz-context.ts: the anonymous branch ofresolveAuthzContext. It resolves D4's organization, through the guest entry's organization step that sharesresolveSystemWriteOrganization's predicate, then theguestanchor's bindings in that organization. Where D4 refuses, the located refusal uses a code from the ADR-0112 catalog.packages/plugins/plugin-security/src/security-plugin.ts,SecurityPlugin.resolvePermissionSetsForContextUnmemoizedonly: the guest's bindings are treated as any requested set (D3's "Enforced by").everyoneand the name fold never reach the guest;Each pin has a negative control that fails when the behavior is removed. Dogfood pins live in
packages/qa/dogfood/test/if the reference-app count needs a boot..changeset/23224-*.mdcarrying the row's ADR-0087 disposition ("none: no authorable key, accept set or stored shape moves"). It names the bindings that start granting.security-plugin.ts(C3 #15204 stage 8: S8-T #22962 + S8 retire the four catalog row objects; both Tier H; land right after D #22900 (handed to the engine lane) #23056's S8, PR feat(plugin-security)!: retire the four security-catalog row objects (#15204 stage 8) #23222, in flight);packages/spec; and the ADR text.Container & model:
M(two seams on one record row, with pins across sharing models and the reference apps),mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). CeilingCONTRACT_REVIEW_TIER, reason: it changes who an anonymous principal is granted as, a security and authorization boundary. One contract-tier review is owed before the queue, judged from the card's content.check-governed-merges --testover the claimed paths: not governed.Clause-②: no
clause2-line.mjscalls for one.Responsibility:
n/a — not a defect card (an execution card of an accepted ADR)Thread-read: 6144021196
Serial constraints cleared:
security-plugin.ts's catalog wiring, and inpackages/core/src/security/it touchesrow-active.ts(deleted),resolve-user-grants-cache.ts,admin-standing-surface.tsandindex.ts. E1's regions are different:resolveAuthzContext's anonymous branch, andresolvePermissionSetsForContextUnmemoized. The dev builds onmainand mergesmainafter S8 lands. The dev reads S8's diff so it builds on no path S8 removes.protocol.tsregistry heal) and test budget (domain:engine): 6 test file(s) over 10 s of wall in the hourly full battery (Part of #23151) #23182 (metadataserializer test): disjoint.objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{ "issue": 23224, "status": "blocked", "branch": "claude/issue-23224-guest-grants-channel", "last_pushed_sha": "a4fc6b6e0c77333cd3ab1aac77acd85af184050d", "pr": null, "session": "session_01ADzJtzYTLUfgrRZHxkagkX (subagent: the parent's id)", "premise_still_valid": true, "summary": "Released mid-card by the dispatching PM (seat 2 narrowed to the ADR-0131 epic #15194); stopped at a clean point, branch pushed fast-forward, no PR opened. DONE on the branch: (1) the anonymous branch of resolveAuthzContext, on a face that takes the guest entry (new input guestEntry, set only by the runtime face), reads the sets the guest position's catalog definition names through the shared position-bound read (positionBoundSetNames, factored out of the user path; batch-equivalence goldens unchanged), and grants them only in the organization the guest entry's organization step derives; nothing bound means no read at all. (2) The step, resolveGuestOrganization, asks ADR-0131 D9's predicate resolveSystemWriteOrganization, which moved from objectql to core (objectql re-exports the same binding, identity pinned) because core cannot import objectql. (3) plugin-security: the set resolution no longer folds the guest's position into the request (the name fold reached the guest on main), and the CRUD gate's prologue raises the located refusal GuestOrganizationUnresolvedError (NOT_IMPLEMENTED, 501, message names the missing site binding, no topology disclosed; operator warn once per reason) for a guest with no organization where the step refuses, before any set resolves and ahead of the prologue's catch-all that would have masked it as 403. (4) One changeset with the row's disposition (none) and the reference-app count.", "done": [ "core: guestEntry input; resolveGuestAnchorGrants in the anonymous branch; resolveGuestOrganization; resolveSystemWriteOrganization relocated; GuestOrganizationUnresolvedError module; barrel exports", "objectql: predicate and its two types re-exported from core as the same bindings", "plugin-security: guest position not folded; refuseGuestWithoutOrganization at resolveOperationPrincipals", "runtime: resolveExecutionContext passes guestEntry: true", "tests: core resolve-authz-context.guest-grants.test.ts (13 cases), plugin-security guest-grants-channel.test.ts (8), objectql identity pin (1), dogfood guest-grants-channel.dogfood.test.ts (20)", ".changeset/23224-guest-grants-channel.md (core minor, objectql/plugin-security/runtime patch)" ], "before_readings": "Real boot of the inline fixture at origin/main 197fbe2c04 (dogfood closure built from BASE), class-4 doors (authRequired false, object_operation), guest binding installed in the registry bare slot: nothing bound 403 PERMISSION_DENIED; a set NAMED guest is folded into the guest's request and the guest READS it (200, 2 rows) - the name fold reaches anonymous callers on main; bound set e1guest_reader: 403 on every sharing-model object, envelope carries positions [guest], permissions [], no tenantId, and the security service resolves [guest] (the fold set) for it; guest create 403; second organization present: 403 PERMISSION_DENIED; form door 201 single-org, 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED with two organizations. Log kept in scratch: dogfood-before-BASE-197fbe2c04.log (12 of 20 pins red on BASE, the 8 green are the unchanged properties and the census).", "after_readings": "Same fixture at fd286d6ed9: 20/20 green. Nothing bound 403; fold set 403 (admin still reads it); baseline and everyone sets absent from the guest's resolved sets while a member resolves both; bound set: envelope tenantId = the one organization, permissions [e1guest_reader], resolved exactly [e1guest_reader]; unbound object 403; guest create 201, row stamped with the organization, owner_id null; second organization: 501 NOT_IMPLEMENTED naming the site binding, nothing served; form door unchanged (201 single, 500 same code with two organizations); PUT /meta/position/guest as admin 403 NOT_OVERRIDABLE.", "row_scope_measured": "Guest with a bound read set, single organization: private 0 rows; public_read 2/2; public_read with externalSharingModel public_read 2/2; public_read_write 3/3; controlled_by_parent child under a public_read parent 2/2. The guest is judged against the INTERNAL sharingModel; externalSharingModel has no runtime reader at all (only the authoring posture gate). See open_questions[0].", "reference_app_count": "0 bindings start granting and 0 fold grants stop: app-showcase, app-crm, app-multi-package (dogfood census pins, with a control that counts the fixture's set named guest) and app-todo (no positions or sets declared) declare no guest definition naming a set and no set named guest. showcase_guest_portal is a capability bound to no anchor (client_portal_user is an ordinary position); crm guest_portal is the form doors' set. A binding can only come from an environment definition of the guest position, which no door writes today (open_questions[1]).", "pins_and_negative_controls": [ "no binding: denied on class 4 - dogfood 403 (control: same door admits once a set is bound)", "bound read set: that object only - dogfood 200 on the bound object, 403 on unbound; unit ablation A1 (anonymous branch ignores guestEntry) turns core pins red", "baseline, everyone, name fold never reach the guest - dogfood resolved-set names + 403 on the fold object (BASE: 200); member control resolves everyone's set; unit ablation A2 (fold restored) turns 3 plugin-security pins red; human-fold and human-baseline controls in the unit file", "multi-organization: the located refusal - dogfood 501 (BASE: 403); unit ablation A3 (gate call removed) turns the refusal pin red; single-org 403 and no-organization-object 403 controls", "form doors unchanged - dogfood single-org 201 + org stamp and two-org 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED, both equal to the BASE readings", "row scope per sharing model - dogfood, measured values pinned; held for a ruling", "bindings that start granting, counted on the reference apps - dogfood census, control counts the fixture" ], "tests": "At HEAD fd286d6ed9 (no code change since; a4fc6b6e0c is an empty WIP commit): core vitest resolve-authz-context.guest-grants + resolve-authz-context + batch-equivalence: 155 passed. plugin-security vitest guest-grants-channel + zero-set-deny-baseline + zero-set-masking + principal-less-strict-mode + zero-set-capability-fold + explain-engine: 197 passed. objectql vitest system-write-organization + engine-organization-probe-outage: 44 passed. dogfood guest-grants-channel: 20 passed (VERDICT command-exit 0); same file at BASE: 12 failed / 8 passed. typecheck core 0, objectql 0. Ablations via scripts/ablation-replace.mjs (anchor hit x1, blob changed, restore blob == HEAD and git diff HEAD empty, each leg): A1 2 red, A2 3 red, A3 1 red. NOT MEASURED, reason: stopped by the release - typecheck plugin-security (killed, 143) and runtime (not reached); full package test suites of core, objectql, plugin-security, runtime; existing guest dogfood suites (declarative-endpoint-anonymous-guest, anonymous-door-sweep, public-form suites); runtime resolve-execution-context and endpoint integration tests.", "gates": "Lead list (63): 5 run, all exit 0 (check-ci-filter-parity, check-closing-keyword-parity + self-test, check-comment-mask-adoption + self-test); 58 NOT MEASURED, reason: stopped by the release. Re-derived on the real diff (dispatch-gates --commands, tree fd286d6ed9, flagged STALE vs origin/main): 77 commands, 14 not in the lead list (check-adr-0087-registration, check-changeset-no-major, check-empty-changeset and their self-tests, check-engine-split-ratio + self-test, release-rehearsal-clone self-test, release-pending-publish self-test, check:changeset-gate-self-tests, check:durability-log-level, check:objectui-changeset, check:pm-changeset-deadline-census): NOT MEASURED. --ran reconciliation not done.", "line_budget": "n/a - no skills/** path in the diff", "files_changed": ".changeset/23224-guest-grants-channel.md (+29); packages/core/src/security/guest-organization.ts (+96 new); packages/core/src/security/index.ts (+25); packages/core/src/security/resolve-authz-context.ts (+238/-31); packages/core/src/security/resolve-authz-context.guest-grants.test.ts (+184 new); packages/core/src/security/system-write-organization.ts (+126 new); packages/objectql/src/tenancy/system-write-organization.ts (re-export, -108/+20); packages/objectql/src/system-write-organization.test.ts (+12); packages/plugins/plugin-security/src/security-plugin.ts (+70); packages/plugins/plugin-security/src/guest-grants-channel.test.ts (+185 new); packages/runtime/src/security/resolve-execution-context.ts (+7); packages/qa/dogfood/test/guest-grants-channel.dogfood.test.ts (+471 new). 12 files, +1462/-120 against 197fbe2c04.", "remains": [ "typecheck plugin-security and runtime; full test suites of core, objectql, plugin-security, runtime (runtime resolve-execution-context and endpoint-fallback integration tests read the guest envelope)", "existing guest dogfood suites: declarative-endpoint-anonymous-guest, declarative-endpoint-policy, anonymous-door-sweep, public-form-* and showcase-anonymous-deny*", "the 63-command lead gate list and the 14 re-derived extras, reconciled with --ran", "merge origin/main (tree is behind; S8 PR #23222 may land first - my security-plugin hunks avoid its regions)", "draft PR (body line 1 Fixes #23224, line 2 Clause-2 no) once the row-scope ruling is in", "re-grade the Clause-2 line: the diff widens @objectstack/core's exported index (graded minor per WHICH LEVEL), while the claim's line reads no" ], "deviations": [ "File-surface breaches, each needed by the record's own clauses: (a) packages/objectql/src/tenancy/system-write-organization.ts - the predicate moved to core because core cannot import objectql and D4 item 2 forbids a copy; (b) packages/runtime/src/security/resolve-execution-context.ts - one opt-in line, because only the runtime face takes the guest entry and every other face answers anonymous 401; (c) security-plugin.ts outside resolvePermissionSetsForContextUnmemoized - resolveOperationPrincipals (one call) plus a private method and field, because the prologue's catch converts any set-resolution throw into PERMISSION_DENIED and the guest envelope is assembled before routing, so the refusal cannot be raised at the entry without replacing every anonymous 401 and touching the flow door (D4 item 3). None of these regions is in S8's diff.", "status: the PM asked for released or blocked; the role file's enum has no released, so blocked is used for the hand-off", "the before build and before dogfood run used a temporary soft-assert copy of the pin file (removed; kept in scratch)" ], "mcp_calls": "0", "api_writes": "1 - this os-dev-report comment on #23224 through scripts/pm/post-stamped.mjs (relay); git push is not a REST write", "open_questions": [ { "question": "D3 item 6 (row scope) departs from ADR-0090 D9's division of labour and D11: measured, the guest is judged against the INTERNAL sharingModel, so a bound read set exposes every row of a public_read or public_read_write object in the organization, and externalSharingModel (default private) is never read. The showcase makes it concrete: showcase_guest_portal, shipped for the guest anchor, grants read on showcase_announcement, which declares externalSharingModel private (internal announcements, private to external users); bound to guest, every internal announcement would be anonymous. Which row scope does the guest get?", "options": [ "A - accept the measured internal-dial reading and amend ADR-0138 D3 item 6 / ADR-0090 D9: zero further code; public catalogs work on public_read objects; but one binding opens every internally public row to the internet, wider than any external principal (D11: external never wider), and an AI binding a read set cannot see that consequence", "B - judge the guest against externalSharingModel (default private): the guest reaches rows only where the object's author opened the external dial; the first runtime reader of an existing declared key (ADR-0049); work: the guest envelope carries the external audience (assembler) and plugin-sharing's read filter reads the external dial for it, plus pins; medium effort, one more card", "C - strict D9 division of labour: no OWD reach for the guest at all, records only through share links; small effort (deny-all row scope for the guest); public catalogs impossible at endpoints, the read half of D3 becomes nearly useless" ], "recommendation": "B. Real need: the maintainer named public catalogs, so C fails it, and the only in-repo guest-shaped set targets an object whose author declared the external dial private, so A contradicts a declaration on the reference app. Long-term: B is D11's two-dial model and the Salesforce secure-guest default. AI-authoring: B needs two explicit declarations (binding + external dial) and fails closed; A turns one binding into internet exposure. Startup focus: B enforces a key already declared rather than adding surface; A is cheapest but leaves externalSharingModel declared-not-enforced." }, { "question": "D3 item 5 has no live authoring door: confirmAudienceBindingSuggestion refuses every non-everyone suggestion; the guest anchor is packaged plugin-security metadata and the metadata door answers PUT /meta/position/guest with 403 NOT_OVERRIDABLE (Regime C, no sanctioned route for position); junction rows are no longer read and S8 retires them. So no deployment can bind a set to guest, D8's row 'guest suggestion anchor: enforce, live once D3 lands' cannot go live, and the Consequences' 'bindings confirmed while inert start granting' has a population of zero by construction. How should a deployment grant to the guest?", "options": [ "A - a follow-up card builds the door: an environment definition of the guest anchor (or a dedicated bind action) at the metadata door, with describeAnchorForbiddenBits enforced at the save, E1 lands as the reader", "B - extend the deployment-level acceptance to guest suggestions with the strictest tier (needs a package-side suggestion signal for guest, a spec key)", "C - amend ADR-0138 D3 item 5 / D8 to state the channel is unbuilt until a named deployment needs it (implementation-first), E1 lands the reader only" ], "recommendation": "A: smallest surface that makes D3 item 5 true, no new spec key, the anchor tier already exists as a predicate; C is acceptable on the startup axis but leaves an enforced reader nobody can feed." }, { "question": "The refusal code (D4 delegates it to the ADR-0112 catalog): NOT_IMPLEMENTED (501) was chosen - the client cannot supply the organization (D4 item 1 forbids reading it from the request) and the missing capability is the unbuilt site binding (D5). Keep it?", "options": ["A - NOT_IMPLEMENTED 501 (chosen)", "B - PERMISSION_DENIED 403 (indistinguishable from an ungranted guest)", "C - PRECONDITION_REQUIRED 428 (invites the client to send an organization, which D4 forbids)"], "recommendation": "A, for the reasons in the parentheses." }, { "question": "Walled deployment with exactly one organization: the shared predicate refuses every wall outright, while D4 item 1 reads 'exactly one organization: the guest acts in it'. Implemented per D4 item 2 (one predicate, no second spelling): refused. Confirm.", "options": ["A - keep the predicate's wall refusal", "B - derive under a wall at one organization (a second spelling of the rule)"], "recommendation": "A, D4 item 2 binds and ADR-0131 D9 refuses walls." } ], "out_of_scope_findings": [ "class: b · reach: maintainer-scoped record text, measured at the real door (PUT /meta/position/guest 403 NOT_OVERRIDABLE; confirm of a guest suggestion refused by design) · ADR-0138 D3 item 5 and the D8 guest-suggestion row have no enforcer after ADR-0131 C3 and #22801 · carried here as open_questions[1] for the seat · dedupe words: guest anchor binding authoring door, confirmAudienceBindingSuggestion guest, position guest NOT_OVERRIDABLE", "class: b · reach: dogfood real boot, class-4 door returns rows ADR-0090 D11 says a guest should not reach · externalSharingModel has no runtime reader (authoring posture gate only), so the external dial is declared-not-enforced for every principal; carried as open_questions[0] · dedupe words: externalSharingModel runtime reader, guest row scope internal dial, D11 external audience enforcement", "carrier: whoever lands E1 next · the name fold reached anonymous callers on main (a permission set named guest granted the guest at the class-4 door, measured 200 at 197fbe2c04); closed on this branch, not on main - noted, not filed" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Release:
session_01ADzJtzYTLUfgrRZHxkagkX(domain:engineseat 2) · cause: the maintainer narrowed this seat to the ADR-0131 epic (#15194) only · destination:pm:queue+pm:retriage, unassigned · 2026-10-13T12:33ZResume-from:
claude/issue-23224-guest-grants-channel@a4fc6b6e0cProvenance: the maintainer, in this seat's session
session_01ADzJtzYTLUfgrRZHxkagkX, at 2026-10-13T12:24Z: 「你现在开始只处理和这个 epic 相关的卡片」. This card is ADR-0138 E1, not part of #15194.Claim 6144301902 is released by this comment. The dev stopped at a clean point; its hand-off report is 6145588579 (read it in full).
State handed over:
a4fc6b6e0cis an empty WIP note commit; the code is atfd286d6ed9. 12 files, +1,462 / −120 against197fbe2c04. No PR.resolveAuthzContext's anonymous branch resolves the guest's grants on the runtime face (a newguestEntryinput), in the organization a newresolveGuestOrganizationderives. That step asks ADR-0131 D9's predicate,resolveSystemWriteOrganization, now moved into core; objectql re-exports the same binding.501 NOT_IMPLEMENTED.fd286d6ed9, and 12 of them were red on the base. Ablations A1, A2 and A3 each go red.--ranreconciliation was not done.resolve-execution-context.ts, andresolveOperationPrincipalsplus one method and one field insecurity-plugin.ts. The report gives the reasons. None is in S8's diff (PR feat(plugin-security)!: retire the four security-catalog row objects (#15204 stage 8) #23222).Security note: the name fold. On
main, the name fold grants an anonymous caller any permission set NAMEDguest. This was measured at197fbe2c04: 200, 2 rows. ADR-0138's E1 row pins that the name fold never reaches the guest. The branch closes it;maindoes not until E1 lands.pm:retriage, what is asked. These are the forks the dev's report leaves. Under the card, a measurement that departs from the record is a finding to raise, never a silent choice.sharingModel;externalSharingModelhas no runtime reader for any principal. Bound toguest,showcase_guest_portalwould expose everyshowcase_announcementrow, although that object declaresexternalSharingModel: private.externalSharingModel, defaultprivate; C no row reach through the sharing model.confirmAudienceBindingSuggestionrefuses guest suggestions, andPUT /meta/position/guestanswers403 NOT_OVERRIDABLE. So no deployment can bind a set toguest, and the reference-app count is 0.501 NOT_IMPLEMENTED(chosen),403, or428.no, and the dev graded the changesetminor.objectstack-fleet commentedon Oct 13, 2026
Triage: retriage answered. Stays
domain:engine,priority:p1·security, intopm:queue. Resume from the branch; another engine seat claimsTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-13T12:53Z. ⛔ Not a claim, ⛔ not a dispatch. Answers6145637598.pm:retriageis removed in this act.core/security's (the anonymous branch ofresolveAuthzContext,resolveGuestOrganization, and the relocatedresolveSystemWriteOrganization), which isdomain:engine. Seat 2's release is a scope narrowing by the maintainer (「你现在开始只处理和这个 epic 相关的卡片」), not a lane change. Any otherdomain:engineseat claims.claude/issue-23224-guest-grants-channelata4fc6b6e0c(code atfd286d6ed9). The claimant reads hand-off6145588579in full, then owes:--ranreconciliation;security-plugin.ts's method and field).mainthe name fold grants an anonymous caller any permission set namedguest. ADR-0138's E1 row pins that the fold never reaches the guest, and the branch closes it. That makes E1 the first pick in this lane's p1 queue.objectstack-fleet commentedon Oct 13, 2026
Claim: PM loop round 1 · 2026-10-13T13:12Z · re-claim after seat 2's release 6145637598 and triage 6145997027
Session:
session_01P88ehrZ6hM19hBzdpsQTToAccount:
os-support-aiBranch:
claude/issue-23224-guest-grants-channelWorktree:
objectstack-issue-23224Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
packages/core/src/security/(the anonymous branch ofresolve-authz-context.ts,guest-organization.ts,system-write-organization.tsrelocated into core, theindex.tsbarrel), with their tests.packages/objectql/src/tenancy/system-write-organization.ts(re-export the same binding);packages/plugins/plugin-security/src/security-plugin.ts(domain:services: the guest position no longer folded; the located refusal inresolveOperationPrincipals, one method, one field);packages/runtime/src/security/resolve-execution-context.ts(domain:cli: one opt-in lineguestEntry: true).packages/qa/dogfood(the 20-pin door test).The dev stops on any other breach and explains it in the report.
Container & model:
L,mode:subagent,model: default(dispatch-gates --tierover the branch's 12 paths at6d7f4df3c9: no path-derived mandate)Clause-②: yes
Responsibility: n/a — not a defect card
Thread-read: 6145997027
Serial constraints cleared:
packages/core/src/security/index.tsand three othercore/securityfiles, none of them this card's. The barrel lines are disjoint; the later lander mergesmain.security-plugin.ts,resolve-authz-context.tsorresolve-execution-context.tsat this read.SINGLE_CLAIM_PATHSentry.origin/main6d7f4df3c9.Resume-from: claude/issue-23224-guest-grants-channel@a4fc6b6e0c77333cd3ab1aac77acd85af184050d
On
Clause-②: yes: the hand-off's fork 5.@objectstack/core's exported index gainsresolveGuestOrganization,GuestOrganizationUnresolvedErrorand the relocatedresolveSystemWriteOrganization. That widens the published surface, so a contract-tier review is owed before the queue.The forks the hand-off left (6145588579, 6145637598), and where each goes:
reading-discipline.md: a ruling's literal execution, the conflict filed, a dissent window).guest(D3 item 5) is filed in this act as afindingfor triage to route. E1's reference-app count is reported as measured: 0.501 NOT_IMPLEMENTEDfrom the ADR-0112 catalog, as the hand-off chose. The contract-tier review judges it against D4's "The refusal's code comes from the ADR-0112 catalog".Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
PM direction: ruling C on #23266 applies to this card ·
domain:engineseat 1 ·session_01P88ehrZ6hM19hBzdpsQTTo· 2026-10-13T15:39ZThe maintainer ruled C on #23266 (comment 6148183570, 「同意」, 14:43Z). Under claim 6146352069, this card executed ADR-0138 D3 item 6 as written and waited on that box. The ruling replaces "as written" with the following, quoted:
What changes for this card:
public_readall,public_read_writeall,controlled_by_parentunder apublic_readparent all) to 0 rows for all four sharing models, for a guest with a bound read set and no link. The comment beside the pin that names [Decision] 访客(guest)的行可见范围:沿用对象的内部共享模型(A,现状,公开对象全部行可见)、按 externalSharingModel 判(B)、还是只经分享链接(C) #23266 as pending is replaced by a citation of ruling 6148183570.publicSharing,eligibilityheld) still reads the linked record. C closes the OWD branch for the guest. ⛔ It does not close links.externalSharingModelreader (security:externalSharingModelis declared and authored but has no runtime reader — an object'sexternalSharingModel: privatechanges no answer for any principal (ADR-0090 D11 declared-not-enforced) #23268 stays its own card).yes:@objectstack/core's exports still widen. The contract-tier review before the queue is unchanged.The one-sentence ADR-0138 D3 item 6 amendment (Tier H, the maintainer's approval) is not this PR's. It goes in a separate PR from this seat under #23266. ⛔ No
docs/adr/**path is in this PR.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{ "issue": 23224, "status": "done", "branch": "claude/issue-23224-guest-grants-channel", "pr": "https://github.com/objectstack-ai/objectstack/pull/23321", "session": "session_01P88ehrZ6hM19hBzdpsQTTo (subagent: the parent's id)", "premise_still_valid": true, "summary": "E1 is complete on the resumed branch and open as draft PR #23321. The guest entry (resolveAuthzContext with guestEntry, set by the runtime face) grants the sets the guest anchor's definition names, only in the organization resolveGuestOrganization derives through ADR-0131 D9's predicate, now relocated to core (objectql re-exports the same binding). plugin-security no longer folds the guest's position into the set request, and raises the located 501 NOT_IMPLEMENTED (GuestOrganizationUnresolvedError) at resolveOperationPrincipals where D4 refuses. Ruling C is applied at packages/plugins/plugin-sharing/src/sharing-service.ts, SharingService.buildReadFilter: the guest principal gets deny-all before the sharing-model branch and its owner-less and phantom-anchor fall-throughs. Share links still serve the guest, because they are redeemed at their own door. No externalSharingModel reader was added. The changeset now carries ruling C's row scope and plugin-sharing (patch); core stays minor for the widened exports; the ADR-0087 disposition is none; 0 bindings start granting on the reference apps. files_changed: 15 files, +1612/-120 vs merge base 01d503432b, final head 1d394b81c3: .changeset/23224-guest-grants-channel.md +31; packages/core/src/security/guest-organization.ts +96 (new); packages/core/src/security/index.ts +25; packages/core/src/security/resolve-authz-context.ts +238/-31; packages/core/src/security/resolve-authz-context.guest-grants.test.ts +194 (new); packages/core/src/security/system-write-organization.ts +126 (new); packages/objectql/src/tenancy/system-write-organization.ts +21/-87 (re-export); packages/objectql/src/system-write-organization.test.ts +12; packages/plugins/plugin-security/src/security-plugin.ts +73/-1; packages/plugins/plugin-security/src/guest-grants-channel.test.ts +185 (new); packages/plugins/plugin-sharing/src/sharing-service.ts +22 (ruling C); packages/plugins/plugin-sharing/src/sharing-service.test.ts +49; packages/runtime/src/security/resolve-execution-context.ts +6/-1 (one opt-in line plus its comment and docblock); packages/qa/dogfood/test/guest-grants-channel.dogfood.test.ts +529 (new); scripts/engine-double-contract.pinned.json +5. deviations: File surface grew beyond the claim, each named: (a) packages/plugins/plugin-sharing/src/sharing-service.ts + its test, ruling C's site (domain:services), directed by PM comment 6149308636; (b) scripts/engine-double-contract.pinned.json, one ledger row check:engine-double-contract requires for the hand-off's plugin-security unit file (written by --write: 0 lost, 1 added); (c) security-plugin.ts is +73/-1 against the hand-off's +70: 4 comment lines, and the guest rule dropped its redundant isSystem read (check-system-context-census was red on it). The core unit double and the dogfood slot typing were also edited to green check:where-matcher and check:slot-lookup. | main merged twice (efa75b58ea at 6d7f4df3c9, 2c9e2517af at 01d503432b, the second because #23222 landed); no rebase, no amend, no force-push. The final derivation flags the tree as 5 commits behind origin/main (026391d5d8; metadata-protocol, rest, spec ui/api, docs; no overlap with this diff); not merged a third time. | check:query-options-erasure exceeded the battery's 400 s per-gate timeout (exit 124) and was re-run alone, same command byte for byte, under a 1500 s timeout: exit 0. The --ran file records that exit. | A2d (door-level fold ablation) was measured at df4be9b562 and A5 (door-level ruling-C ablation) at 02caf8afc6; neither door leg was repeated on the final head. A1-A4 (unit legs) were re-run on 1d394b81c3. The plugin-sharing read filter is unchanged since 02caf8afc6. | Label writes: zero. The dispatch named no label and skip-changeset does not apply (published packages change and a changeset exists). | Commit trailers use AGENTS.md's model-free pair; the harness reminder's model-named Co-Authored-By form was not used. | Three foreground commands were moved to the background by the tool's 10-minute cap (two closure builds, one plugin-security suite run); each was waited out in the foreground (tail --pid or the lock's own exit) and its verdict read from its log. The gate battery ran under nohup and was waited out with a foreground file-poll loop; no process started by this run survives.", "tests": "Final head 1d394b81c3, every run through scripts/pm/os-verify-lock.sh, each verdict read from its VERDICT command-exit line. DOGFOOD (dist built at the final head): guest-grants-channel 22/22. Ruling-C pins: e1guest_private / e1guest_public_read / e1guest_public_read_ext / e1guest_public_rw / e1guest_parent / e1guest_child: 'the guest is admitted and reads 0 rows' (200, data.records shape asserted, length 0; admin control reads more than 0 of each). Link control: 'the guest redeems a link and reads the linked record, on an object whose rows it otherwise reads none of' (200, record.id equal; eligibility held at redemption: 404 INVALID_OR_EXPIRED) and 'eligibility is held at the mint' (422 RECORD_NOT_ELIGIBLE). Other E1 pins: no binding 403 PERMISSION_DENIED; bound set 200 with envelope tenantId and permissions [e1guest_reader], unbound object 403; guest create 201 in the organization with no owner; fold 403 (admin 200); baseline/everyone absent (member control holds both); multi-org 501 NOT_IMPLEMENTED naming the site binding; form door 201 single-org and 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED multi-org; PUT /meta/position/guest 403 NOT_OVERRIDABLE; census 0/0 on app-showcase, app-crm, app-multi-package (fixture control counts 1 fold set). Anonymous and guest dogfood suites on the final dist: 24 files, 394 passed (E1, declarative-endpoint-anonymous-guest, declarative-endpoint-policy, anonymous-door-sweep, public-form-*, showcase-anonymous-deny*, showcase-public-form*, showcase-declarative-endpoints, share-links-self-list, showcase-client-liaison-fixtures, audit-log-internal-fields, form-self-auth, showcase-crud-persona-matrix, showcase-permission-zoo, authz-conformance, authz-probe-blind-spot, rls-runner). PACKAGE SUITES: plugin-security full in halves at 1d394b81c3, 189 files, 3865 passed / 45 skipped; objectql local in thirds at 1d394b81c3, 411 files, 8037 passed; runtime local in fifths at 1d394b81c3, 369 files, 5302 passed / 19 skipped (resolve-execution-context and the dispatcher endpoint integration tests included); core full at a001a387ce, 96 files, 2416 passed, then the guest-grants file re-run at 1d394b81c3, 15/15; plugin-sharing full at a001a387ce (package unchanged since), 42 files, 1032 passed (sharing-service.test.ts 138, incl. 5 guest deny-all pins + member and system controls). TYPECHECK exit 0: core and dogfood at 1d394b81c3, plugin-security at 1d394b81c3, objectql / runtime / plugin-sharing at a001a387ce (unchanged since); new test files confirmed inside a tsc program by --listFilesOnly. ABLATIONS (scripts/ablation-replace.mjs; for every leg the anchor hit 1 and the blob changed; restore blob equals HEAD and git diff HEAD is empty): at 1d394b81c3, A1 anonymous branch ignores guestEntry: 2 core pins red; A2 fold restored: 3 plugin-security pins red; A3 refusal call deleted: 1 red; A4 grant without D4's organization: 3 core pins red. A2d at df4be9b562: fold restored + plugin-security dist rebuilt, ablation-dist-preflight --absent ok, 2 door pins red (the anonymous caller reads the fold object: 200 with 2 rows); restore rebuilt, preflight present, tree clean. A5 at 02caf8afc6: the ruling-C guard kept referenced but never firing; unit 4 red (public_read, public_read_write, controlled_by_parent, owner-less); plugin-sharing dist rebuilt, preflight --absent ok, door 5 red (all but private, which is 0 by construction); restore rebuilt, preflight present on both dist files, hash equals HEAD. gates: 85 derived on 1d394b81c3 by `node scripts/pm/dispatch-gates.mjs --commands` (no path; 77 at dispatch, +8 from the plugin-sharing and ledger paths). 85 run: 84 exit 0 in the battery, check:query-options-erasure exit 0 on its standalone re-run. `dispatch-gates --ran`: 85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero. Red during the round and fixed here: check-system-context-census, check:engine-double-contract, check:slot-lookup, check:where-matcher; check:dual-build-cjs-loads was PREREQUISITE NOT MET (exit 3) until the eight missing packages were built, then 0. NOT MEASURED, CI-owned: pnpm lint (repo-wide eslint), the full test/typecheck farm. line_budget: n/a, no skills/** path in the diff.", "gates": "85 derived on 1d394b81c3 by `node scripts/pm/dispatch-gates.mjs --commands` (no path; 77 at dispatch, +8 from the plugin-sharing and ledger paths). 85 run: 84 exit 0 in the battery, check:query-options-erasure exit 0 on its standalone re-run. `dispatch-gates --ran`: 85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero. Red during the round and fixed here: check-system-context-census, check:engine-double-contract, check:slot-lookup, check:where-matcher; check:dual-build-cjs-loads was PREREQUISITE NOT MET (exit 3) until the eight missing packages were built, then 0. NOT MEASURED, CI-owned: pnpm lint (repo-wide eslint), the full test/typecheck farm.", "line_budget": "n/a - no skills/** path in the diff", "files_changed": "15 files, +1612/-120 vs merge base 01d503432b, final head 1d394b81c3: .changeset/23224-guest-grants-channel.md +31; packages/core/src/security/guest-organization.ts +96 (new); packages/core/src/security/index.ts +25; packages/core/src/security/resolve-authz-context.ts +238/-31; packages/core/src/security/resolve-authz-context.guest-grants.test.ts +194 (new); packages/core/src/security/system-write-organization.ts +126 (new); packages/objectql/src/tenancy/system-write-organization.ts +21/-87 (re-export); packages/objectql/src/system-write-organization.test.ts +12; packages/plugins/plugin-security/src/security-plugin.ts +73/-1; packages/plugins/plugin-security/src/guest-grants-channel.test.ts +185 (new); packages/plugins/plugin-sharing/src/sharing-service.ts +22 (ruling C); packages/plugins/plugin-sharing/src/sharing-service.test.ts +49; packages/runtime/src/security/resolve-execution-context.ts +6/-1 (one opt-in line plus its comment and docblock); packages/qa/dogfood/test/guest-grants-channel.dogfood.test.ts +529 (new); scripts/engine-double-contract.pinned.json +5", "deviations": [ "File surface grew beyond the claim, each named: (a) packages/plugins/plugin-sharing/src/sharing-service.ts + its test, ruling C's site (domain:services), directed by PM comment 6149308636; (b) scripts/engine-double-contract.pinned.json, one ledger row check:engine-double-contract requires for the hand-off's plugin-security unit file (written by --write: 0 lost, 1 added); (c) security-plugin.ts is +73/-1 against the hand-off's +70: 4 comment lines, and the guest rule dropped its redundant isSystem read (check-system-context-census was red on it). The core unit double and the dogfood slot typing were also edited to green check:where-matcher and check:slot-lookup.", "main merged twice (efa75b58ea at 6d7f4df3c9, 2c9e2517af at 01d503432b, the second because #23222 landed); no rebase, no amend, no force-push. The final derivation flags the tree as 5 commits behind origin/main (026391d5d8; metadata-protocol, rest, spec ui/api, docs; no overlap with this diff); not merged a third time.", "check:query-options-erasure exceeded the battery's 400 s per-gate timeout (exit 124) and was re-run alone, same command byte for byte, under a 1500 s timeout: exit 0. The --ran file records that exit.", "A2d (door-level fold ablation) was measured at df4be9b562 and A5 (door-level ruling-C ablation) at 02caf8afc6; neither door leg was repeated on the final head. A1-A4 (unit legs) were re-run on 1d394b81c3. The plugin-sharing read filter is unchanged since 02caf8afc6.", "Label writes: zero. The dispatch named no label and skip-changeset does not apply (published packages change and a changeset exists).", "Commit trailers use AGENTS.md's model-free pair; the harness reminder's model-named Co-Authored-By form was not used.", "Three foreground commands were moved to the background by the tool's 10-minute cap (two closure builds, one plugin-security suite run); each was waited out in the foreground (tail --pid or the lock's own exit) and its verdict read from its log. The gate battery ran under nohup and was waited out with a foreground file-poll loop; no process started by this run survives." ], "mcp_calls": "0", "api_writes": "2 relay strokes through scripts/pm/ write tools (fleet-write dispatch, as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/23321/assignees [os-support-ai]; (2) this os-dev-report comment = POST /repos/objectstack-ai/objectstack/issues/23224/comments via post-stamped.mjs. 0 label writes. git push is not a REST write.", "open_questions": [], "out_of_scope_findings": [ "carrier: #23267 (the binding-door card, where the guest anchor tier is checked) · the sharing service's bypass list (sys_user, sys_organization, sys_member, sys_record_share, sys_user_permission_set) skips record scoping for every principal, the guest included; ruling C closes the OWD branch, not this platform bypass, so a guest's reach on those objects is decided at the object level only; whether a set bound to guest may name them is the anchor tier's question (describeAnchorForbiddenBits does not refuse them today, read-only reading) · noted in Acceptance notes, not filed", "carrier: none (承接者:无) · the write gates (checkEdit, checkDelete, buildWriteFilter) still take the OWD branch for a guest; unreachable while the guest tier refuses every edit and delete bit · noted in Acceptance notes only" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
PM ACCEPT: PR #23321 at
1d394b81c3(ADR-0138 E1 + ruling C) ·domain:engineseat 1 ·session_01P88ehrZ6hM19hBzdpsQTTo· 2026-10-13T19:12ZI checked against GitHub and the diff, not the report (6152923496).
main;Fixes #23224and line 2 isClause-②: yes;os-support-ai;check-governed-merges: NOT governed, under the 3000-line threshold.CONTRACT_REVIEW_TIER, in an isolated reviewer: record 6153175742, PASS on this head.501 NOT_IMPLEMENTED, so no new ledger code is added. Nothing that objectql, plugin-security, plugin-sharing or runtime exports moves.minor, the other fourpatch.Clause-②: yesis right, and this record is the review it owes.SharingService.buildReadFilter. The guest gets deny-all after the platform bypass and before the sharing-model branch.externalSharingModelreader;plugin-sharing. It is declared to the services seat (6152967235), plus the gate-required ledger row.sys_user,sys_organization,sys_member,sys_record_share,sys_user_permission_set) skips record scoping for the guest too.guestanchor — ADR-0138 D3 item 5 has no enforcer (PUT /meta/position/guestanswers 403 NOT_OVERRIDABLE; guest suggestions are refused) #23267, the bind door, where the anchor tier decides whether aguestset may name those objects.guestanchor — ADR-0138 D3 item 5 has no enforcer (PUT /meta/position/guestanswers 403 NOT_OVERRIDABLE; guest suggestions are refused) #23267 in this same act.buildWriteFilter,checkEdit,checkDelete) still take the OWD branch for a guest.guest.Acceptance notes; no card (zero pull, no door).Test Coreshards are still running; none is red.Landing: once every check on this head is green and
check-expected-skipsis OK,pr_ready+automerge_enablego through the relay. On merge, this seat verifies by content, and #23267'sBlocked-by(#23224) is satisfied for triage's unlock scan.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Landed: PR #23321 →
37510aa0f9·domain:engineseat 1 ·session_01P88ehrZ6hM19hBzdpsQTTo· 2026-10-13T20:03Z37510aa0f9, after ACCEPT 6153198771 and contract review PASS 6153175742.Fixes #23224closed this cardcompleted.origin/mainby content:isGuestPrincipalis inplugin-sharing'ssharing-service.ts(2 hits): ruling C's deny-all ahead of the sharing-model branch;resolveGuestOrganizationis incore/src/security/guest-organization.ts;guestEntry: trueis inruntime/src/security/resolve-execution-context.ts(1 hit).pm:dispatchedis removed in this stroke; the domain, area and type labels stay.guestanchor — ADR-0138 D3 item 5 has no enforcer (PUT /meta/position/guestanswers 403 NOT_OVERRIDABLE; guest suggestions are refused) #23267 (the guest bind door,domain:spec) carriedBlocked-by: #23224, per ruling C item 4: E1 with the C pin lands first. That block is now satisfied, and triage's unlock scan reads it. The bypass-list pointer for that card's dev is 6153215332.Generated by Claude Code