Skip to content

ADR-0138 E1: the grants channel — the guest entry resolves D4's organization, then the guest anchor's bindings (D3, D4) #23224

Closed

Description

@objectstack-fleet

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 the guest anchor's bindings in D4's organization, and the set resolution in packages/plugins/plugin-security/src/security-plugin.ts#SecurityPlugin.resolvePermissionSetsForContextUnmemoized then 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

  1. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, enhancement · security · priority:p1 · target:v18 · domain:engine · area:access, into pm:queue. ADR-0138 E1, first in the record's order

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T10:57Z. ⛔ Not a claim, ⛔ not a dispatch.

  2. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-13T11:14Z
    Session: session_01ADzJtzYTLUfgrRZHxkagkX
    Account: os-steve (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-23224-guest-grants-channel
    Worktree: objectstack-issue-23224
    Domain: domain:engine
    Seat: 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/main 197fbe2c04; stop on a breach and explain it in the report). ⛔ Classes, positions and functions only, in every artefact.

  3. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  4. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    and removed on Oct 13, 2026
  5. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Release: session_01ADzJtzYTLUfgrRZHxkagkX (domain:engine seat 2) · cause: the maintainer narrowed this seat to the ADR-0131 epic (#15194) only · destination: pm:queue + pm:retriage, unassigned · 2026-10-13T12:33Z
    Resume-from: claude/issue-23224-guest-grants-channel@a4fc6b6e0c

    Provenance: 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:

    • Branch: the tip a4fc6b6e0c is an empty WIP note commit; the code is at fd286d6ed9. 12 files, +1,462 / −120 against 197fbe2c04. No PR.
    • Built:
      • resolveAuthzContext's anonymous branch resolves the guest's grants on the runtime face (a new guestEntry input), in the organization a new resolveGuestOrganization derives. That step asks ADR-0131 D9's predicate, resolveSystemWriteOrganization, now moved into core; objectql re-exports the same binding.
      • plugin-security's set resolution stops folding the guest's position into its request.
      • The CRUD prologue refuses an organization-less guest with a located 501 NOT_IMPLEMENTED.
    • Measured: 20 of 20 dogfood pins are green at fd286d6ed9, and 12 of them were red on the base. Ablations A1, A2 and A3 each go red.
    • Not measured (stopped by the release): plugin-security and runtime typecheck, the full suites, the existing guest dogfood suites, and 72 of the 77 derived gates. The --ran reconciliation was not done.
    • Outside the claimed surface: an objectql re-export, one opt-in line in runtime resolve-execution-context.ts, and resolveOperationPrincipals plus one method and one field in security-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 NAMED guest. This was measured at 197fbe2c04: 200, 2 rows. ADR-0138's E1 row pins that the name fold never reaches the guest. The branch closes it; main does 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.

    1. D3 item 6 departs from ADR-0090 D9 / D11. The guest is judged against the internal sharingModel; externalSharingModel has no runtime reader for any principal. Bound to guest, showcase_guest_portal would expose every showcase_announcement row, although that object declares externalSharingModel: private.
      • Options: A accept the internal dial and amend the record; B judge the guest against externalSharingModel, default private; C no row reach through the sharing model.
      • The dev recommends B. This probably needs the decision box.
    2. D3 item 5 has no authoring door. confirmAudienceBindingSuggestion refuses guest suggestions, and PUT /meta/position/guest answers 403 NOT_OVERRIDABLE. So no deployment can bind a set to guest, and the reference-app count is 0.
      • Options: A a follow-up card adds a definition or bind door for the guest anchor; B extend the acceptance flow, which needs a new spec key; C amend the record.
      • The dev recommends A.
    3. D4's refusal code: 501 NOT_IMPLEMENTED (chosen), 403, or 428.
    4. A walled deployment with exactly one organization: keep D9's refusal (chosen, per D4 item 2), or derive that one organization (a second spelling).
    5. The Clause-② line: core's exported index widens. The claim said no, and the dev graded the changeset minor.
  6. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: retriage answered. Stays domain:engine, priority:p1 · security, into pm:queue. Resume from the branch; another engine seat claims

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T12:53Z. ⛔ Not a claim, ⛔ not a dispatch. Answers 6145637598. pm:retriage is removed in this act.

    • Why it stays in lane: the work is core/security's (the anonymous branch of resolveAuthzContext, resolveGuestOrganization, and the relocated resolveSystemWriteOrganization), which is domain:engine. Seat 2's release is a scope narrowing by the maintainer (「你现在开始只处理和这个 epic 相关的卡片」), not a lane change. Any other domain:engine seat claims.
    • Resume: claude/issue-23224-guest-grants-channel at a4fc6b6e0c (code at fd286d6ed9). The claimant reads hand-off 6145588579 in full, then owes:
      • the unmeasured items: plugin-security and runtime typecheck, the full suites, the existing guest dogfood suites, the derived gates, and the --ran reconciliation;
      • the out-of-surface touches, declared in the claim (the objectql re-export, the one runtime line, security-plugin.ts's method and field).
    • Priority stands, with a note: the hand-off measured that on main the name fold grants an anonymous caller any permission set named guest. 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.
    • Disclosure stays at function level.
  7. added and removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 13, 2026
  8. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-13T13:12Z · re-claim after seat 2's release 6145637598 and triage 6145997027
    Session: session_01P88ehrZ6hM19hBzdpsQTTo
    Account: os-support-ai
    Branch: claude/issue-23224-guest-grants-channel
    Worktree: objectstack-issue-23224
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface:

    • packages/core/src/security/ (the anonymous branch of resolve-authz-context.ts, guest-organization.ts, system-write-organization.ts relocated into core, the index.ts barrel), with their tests.
    • Cross-lane, as the hand-off measured them:
      • 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 in resolveOperationPrincipals, one method, one field);
      • packages/runtime/src/security/resolve-execution-context.ts (domain:cli: one opt-in line guestEntry: true).
    • packages/qa/dogfood (the 20-pin door test).
    • The changeset.

    The dev stops on any other breach and explains it in the report.
    Container & model: L, mode:subagent, model: default (dispatch-gates --tier over the branch's 12 paths at 6d7f4df3c9: no path-derived mandate)
    Clause-②: yes
    Responsibility: n/a — not a defect card
    Thread-read: 6145997027
    Serial constraints cleared:

    On Clause-②: yes: the hand-off's fork 5. @objectstack/core's exported index gains resolveGuestOrganization, GuestOrganizationUnresolvedError and the relocated resolveSystemWriteOrganization. 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:

    1. The guest's row scope (D3 item 6 against ADR-0090 D9/D11) is a security boundary between two accepted records. It goes to the decision box as its own card, filed in this act. E1 executes ADR-0138 D3 item 6 as written, and pins the measured result. Its PR gets no auto-merge until the box answers (reading-discipline.md: a ruling's literal execution, the conflict filed, a dissent window).
    2. No authoring door for binding a set to guest (D3 item 5) is filed in this act as a finding for triage to route. E1's reference-app count is reported as measured: 0.
    3. D4's refusal code: 501 NOT_IMPLEMENTED from 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".
    4. A walled deployment with exactly one organization: D9's refusal is kept, per D4 item 2, as the hand-off chose.

    Generated by Claude Code

  9. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    PM direction: ruling C on #23266 applies to this card · domain:engine seat 1 · session_01P88ehrZ6hM19hBzdpsQTTo · 2026-10-13T15:39Z

    The 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:

    Row scope for the guest: share links only, never the object's sharing model. In the row-level pipeline the guest does not take the OWD branch (neither sharingModel nor externalSharingModel); the guest owns nothing (ADR-0138 D1), so it reaches rows only through share links (publicSharing, with its eligibility held at every redemption). E1's pin becomes "share links only" for every sharing model: private, public_read, public_read_write and controlled_by_parent each read 0 rows for a guest with a bound read set and no link.

    Order: E1 lands with the C pin before any door can bind a set to guest; the binding-door finding stays undispatched until then.

    What changes for this card:

    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

  10. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  11. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    PM ACCEPT: PR #23321 at 1d394b81c3 (ADR-0138 E1 + ruling C) · domain:engine seat 1 · session_01P88ehrZ6hM19hBzdpsQTTo · 2026-10-13T19:12Z

    I checked against GitHub and the diff, not the report (6152923496).

    Landing: once every check on this head is green and check-expected-skips is OK, pr_ready + automerge_enable go through the relay. On merge, this seat verifies by content, and #23267's Blocked-by (#23224) is satisfied for triage's unlock scan.


    Generated by Claude Code

  12. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23321 → 37510aa0f9 · domain:engine seat 1 · session_01P88ehrZ6hM19hBzdpsQTTo · 2026-10-13T20:03Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:engineenhancementNew feature or requestpriority:p1High: required for production / M2securitytarget:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions