Skip to content

feat(security): the guest's grants channel, with a share-links-only row scope (ADR-0138 E1, ruling C) - #23321

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-23224-guest-grants-channel
Oct 13, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-23224-guest-grants-channel

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23224
Clause-②: yes

ADR-0138 E1, the grants channel (D3, D4): the guest entry finds D4's organization, then the guest anchor's bindings, and raises the located refusal where D4 refuses. It also applies the maintainer's ruling C on #23266 (comment 6148183570): the guest's row scope is share links only.

This resumes the released branch (hand-off 6145588579, claim 6146352069). main was merged twice. The second merge brought #23056 S8 (PR #23222) and ADR-0138 D2 slice 1. Both core/security barrel export sets are kept.

Landing: this is a draft. The seat arms it only after the ruling-C pins are on its head (they are, at 1d394b81c3). The one-sentence ADR-0138 D3 item 6 amendment is a separate Tier H PR from the seat. ⛔ No docs/adr/** path is in this diff.

What changed

  • @objectstack/core
    • resolveAuthzContext's anonymous branch takes a new guestEntry input. On a face that sets it, the branch reads the sets the guest position's definition names, through the same position-bound read every position uses (positionBoundSetNames, factored out of the user path). It grants them only in the organization resolveGuestOrganization derives.
    • If nothing is bound, no read is issued. With no derived organization, nothing is granted.
    • resolveGuestOrganization asks ADR-0131 D9's predicate, resolveSystemWriteOrganization. The predicate moved from objectql into core because core cannot import objectql, and D4 item 2 forbids a copy.
    • New module guest-organization.ts: GuestOrganizationUnresolvedError, which carries NOT_IMPLEMENTED and 501 from the ADR-0112 catalog, plus isGuestOrganizationUnresolvedError.
    • These are new exports. They widen the published index, hence minor and Clause-②: yes.
  • @objectstack/objectql
    • tenancy/system-write-organization.ts re-exports the predicate and its two types from core as the same bindings. Identity is pinned.
  • @objectstack/plugin-security
    • resolvePermissionSetsForContextUnmemoized no longer folds the guest's position into the set request. The guest requests exactly the sets its envelope carries.
    • resolveOperationPrincipals calls a new private refuseGuestWithoutOrganization, guarded by one new field. A guest that reaches a data operation with no organization, on a deployment where the organization step refuses, gets the located refusal. This happens before any set resolves, and ahead of the prologue's catch-all, which would turn it into a 403. The operator's log names the reason once per reason.
  • @objectstack/plugin-sharing (ruling C)
  • @objectstack/runtime
    • resolveExecutionContext passes guestEntry: true. This is the one face that takes the explicit guest entry.

ADR-0087 disposition: none. No authorable key, accept set or stored shape moves.

Bindings that start granting on the reference apps: 0. app-showcase, app-crm and app-multi-package are counted by a dogfood census with a fixture control. app-todo declares no positions or sets.

The seat's answers to the forks, as built:

The record row's pins and their negative controls

Pin Where Negative control / ablation
No binding: denied on class 4 dogfood: 403 PERMISSION_DENIED, guest identity, no set The same door admits once a set is bound (the next pin). A1 turns the bound pins red.
A bound read set: that object only dogfood: 200 on the bound object, envelope carries the set and the derived organization, resolved sets exactly that set; 403 on an unbound object. core unit (15 cases) A1 (anonymous branch ignores guestEntry): 2 core pins red
Baseline, everyone and the name fold never reach the guest dogfood: a set named like the anchor is 403 for the guest (the admin reads it: 200); the guest resolves neither the baseline nor everyone's set while a member resolves both. plugin-security unit (3 cases with human controls) A2 (fold restored): 3 unit pins red. A2d (fold restored, dist rebuilt): 2 door pins red, and the anonymous caller reads the fold object
Multi-organization: the located refusal dogfood: 501, error.code NOT_IMPLEMENTED, message names the site binding, nothing served. plugin-security unit: 501 for find and insert, with single-org 403 and no-organization-object 403 controls A3 (gate call removed): the refusal pin red. A4 (grant without D4's organization): 3 core pins red
The form doors unchanged dogfood: single org 201, row stamped with the organization; two organizations 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED (the form door's own answer, never the guest refusal) The class-4 door on the same deployment answers 501
The guest's row scope for each sharing model (ruling C) dogfood: private, public_read, public_read with externalSharingModel: public_read, public_read_write, a public_read parent and its controlled_by_parent child. Each is admitted (200) and reads 0 rows; the admin reads more than 0 of each. plugin-sharing unit: deny-all for 5 shapes, with a member control and a system control A5 (the guard kept referenced but never firing): 4 unit pins red, and 5 door pins red (dist rebuilt). private stays 0 under A5 by construction, because the guest was never owner-matched there
Share links still serve the guest (ruling C's control) dogfood: the guest redeems a link and reads the linked record on an object whose rows it otherwise reads none of. Eligibility is held at redemption (the record stops qualifying: 404 INVALID_OR_EXPIRED) and at the mint (422 RECORD_NOT_ELIGIBLE) n/a
The bindings that start granting, counted dogfood census: 3 reference apps, 0 anchor sets and 0 fold sets The fixture control counts its own set named like the anchor

Ablations are one-shot proofs, run through scripts/ablation-replace.mjs. For every leg the anchor hit was 1 and the blob changed. Each restore was proven: the blob equals HEAD and git diff HEAD is empty. The door-level legs also ran scripts/ablation-dist-preflight.mjs (--absent on the mutation, present on the restore, rebuilt each way). No ablation file is kept.

  • A1 to A4 were re-run on the final head 1d394b81c3.
  • A2d ran at df4be9b562.
  • A5 ran at 02caf8afc6. The read filter is unchanged since then.

Tests (final head 1d394b81c3 unless noted)

Package Suite Typecheck
core full suite: 96 files, 2416 passed, at a001a387ce; after the matcher change, the guest-grants file re-run: 15/15 0
objectql full local suite in thirds: 411 files, 8037 passed 0 (at a001a387ce; objectql unchanged since)
plugin-security full suite in halves: 189 files, 3865 passed, 45 skipped 0
plugin-sharing full suite: 42 files, 1032 passed, at a001a387ce (package unchanged since) 0
runtime full local suite in fifths: 369 files, 5302 passed, 19 skipped. This includes resolve-execution-context and the endpoint integration tests 0 (at a001a387ce; runtime unchanged since)
dogfood E1 22/22. Anonymous and guest suites on the final dist: 24 files, 394 passed. This covers the guest endpoint, policy, anonymous door sweep, public form, showcase anonymous-deny, share-link, authz conformance, probe blind spot and RLS runner suites 0

Gates

node scripts/pm/dispatch-gates.mjs --commands was run on 1d394b81c3 and derived 85 commands. All 85 ran:

  • 84 exit 0 in the battery;
  • check:query-options-erasure exceeded the battery's 400 s timeout and was re-run alone, exit 0.

--ran reconciliation: 85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN.

Four gates went red on the hand-off's files during this round and were fixed here:

  • check-system-context-census: the guest refusal's redundant system read was removed. Both callers return for a system context first.
  • check:engine-double-contract: the ledger records the plugin-security unit file's pinned findOne, through --write, one row.
  • check:slot-lookup: the dogfood file's engine and security slots are typed.
  • check:where-matcher: the core unit double refuses a combinator.

File surface against the claim

  • The claim's files: as the hand-off measured them.
    • security-plugin.ts grew by 4 comment lines, and the guest rule lost its redundant system read.
  • Grown, named:
    • packages/plugins/plugin-sharing/src/sharing-service.ts and its test. This is ruling C's site (domain:services), directed by PM comment 6149308636.
    • scripts/engine-double-contract.pinned.json, one ledger row that the gate requires for the hand-off's unit file.

Acceptance notes

  • Bypass-listed objects: the sharing service's bypass list (sys_user, sys_organization, sys_member, sys_record_share, sys_user_permission_set) still skips record scoping for every principal, the guest included. This is the platform bypass, not the OWD branch, so ruling C does not reach it.
  • Write side: the write gates (checkEdit, checkDelete, buildWriteFilter) still take the OWD branch for a guest. A guest never reaches them: the guest tier refuses every edit and delete bit (describeAnchorForbiddenBits), so a guest update or delete is refused at the object level first. Noted.
  • The form door on a multi-organization deployment answers its own 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED, as before (D4 item 4 keeps it). It is pinned unchanged here, not altered.

Generated by Claude Code

claude added 11 commits October 13, 2026 12:11
…D4's organization, then the guest anchor's bindings (ADR-0138 E1)

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
…'s binding, the form door keeps its own answer, the anchor's definition is sealed at the metadata door

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
…, unit pins and dogfood pins in place, PR not opened

Done: the guest entry resolves ADR-0138 D4's organization (resolveGuestOrganization over
resolveSystemWriteOrganization, moved to core and re-exported unchanged by objectql), then
the guest anchor's bindings through the shared position-bound read; the name fold is closed
for the guest; the CRUD gate raises the located refusal (NOT_IMPLEMENTED, 501) for a guest
with no organization where D4 refuses. Remains: the plugin-security and runtime typecheck,
the package test suites, the full gate list, the PR, and a ruling on the guest's row scope
(D3 item 6) and on the missing authoring door for a guest binding (D3 item 5).

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
Resume of the E1 branch on main 6d7f4df; no path overlaps this branch.

Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo
Co-authored-by: Claude <noreply@anthropic.com>
ADR-0138 D3 item 6 is executed as written and pinned as measured; the
comment beside the pin names the open decision that may change it, so a
later ruling flips the numbers in one place.

Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo
Co-authored-by: Claude <noreply@anthropic.com>
Brings #23056 S8 (the catalog row objects retired) and ADR-0138 D2
slice 1; both sets of core/security barrel exports are kept.

Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo
Co-authored-by: Claude <noreply@anthropic.com>
… filter — share links only (ADR-0138 D3 item 6, ruling C)

The maintainer's ruling C (comment 6148183570 on #23266) replaces the
as-written row scope: the guest reaches rows only through share links,
never through the object's sharing model. SharingService.buildReadFilter
answers deny-all for the guest principal before the OWD branch (and its
owner-less and phantom-anchor fall-throughs). Share links are redeemed
at their own door and never pass through this filter.

The E1 row-scope pin flips to 0 rows for every sharing model, with an
admin control per object; a share-link control redeems a link (and
holds eligibility at the mint and at redemption).

Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo
Co-authored-by: Claude <noreply@anthropic.com>
…n control reads the data door's own shape

Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo
Co-authored-by: Claude <noreply@anthropic.com>
…s plugin-sharing

Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo
Co-authored-by: Claude <noreply@anthropic.com>
…elevation of its own; the engine-double ledger records its unit file's pinned findOne

Both callers of resolveOperationPrincipals return for a system context
before it, so the guest rule's own isSystem read was redundant, and the
system-context census counted it as an unanchored elevation read.

Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo
Co-authored-by: Claude <noreply@anthropic.com>
…nd its slots are typed

The core unit file's fake WHERE matcher refuses a combinator instead of
reading it as a field name; the dogfood door test reads the engine and
security slots through their contract types.

Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/core, @objectstack/objectql, @objectstack/plugin-security, @objectstack/plugin-sharing, @objectstack/runtime, touching 41 documentable anchor(s).

38 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 404fc477e52f30aa9b77cd2ad52f7adb0e3009fc.

⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 57 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 404fc477e52f30aa9b77cd2ad52f7adb0e3009fc → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5e52e5b2188f1a5703de3abbc2ad08e14bcd65b4 — the merge of head 1d394b81c310005c5f54f36fe9113623fb8fcdf0 into base 404fc477e52f30aa9b77cd2ad52f7adb0e3009fc, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5e52e5b2188f1a5703de3abbc2ad08e14bcd65b4 && git checkout 5e52e5b2188f1a5703de3abbc2ad08e14bcd65b4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 404fc477e52f30aa9b77cd2ad52f7adb0e3009fc 1d394b81c310005c5f54f36fe9113623fb8fcdf0 && git checkout -B drift-repro 404fc477e52f30aa9b77cd2ad52f7adb0e3009fc && git merge --no-ff 1d394b81c310005c5f54f36fe9113623fb8fcdf0

node scripts/docs-audit/affected-docs.mjs --json 404fc477e52f30aa9b77cd2ad52f7adb0e3009fc

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 404fc477e52f30aa9b77cd2ad52f7adb0e3009fc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1d394b81c310005c5f54f36fe9113623fb8fcdf0
Local-runs: none

Inputs: card #23224 (body and all 8 comments, 6144021196 through 6152923496), #23266's body and its 3 comments (ruling 6148183570 read in full), PR #23321 (body, the 15-file list, the three-dot diff against main, with file content read at the head through git show), and the head's check-runs read at 2026-10-13T19:11Z: 32 completed (26 success; 6 skipped: the docs build, the console pin gate, the dogfood shards and CLI, temporal conformance, the opt-in tarball smoke) and 7 in progress (Test Core 1/6 to 6/6, Type Check · workspace). The in-progress runs are not judged here and this record does not vouch for them; landing precondition ② (every check green) is the seat's own re-read before arming. Judged against contract-review.md, the Clause-② lines of execution-duties.md, ADR-0138 D1/D3/D4 and its E1 row, ADR-0090 D9/D11, and ruling C.

① Derived judgments

Published surface. @objectstack/core's root entry (. maps to dist/index.d.ts, whose source does export * from './security/index.js') gains ten additive bindings, each named by the changeset:

  • resolveGuestOrganization — D4's organization step, one step with two askers (the guest entry and the CRUD gate). Right.
  • resolveSystemWriteOrganization, SystemWriteOrganizationDecision, SystemWriteRefusalReason — the ADR-0131 D9 predicate relocated one layer down with its decision table unchanged (a wall refuses; zero refuses no-organization or, with no organization object registered, no-organization-object; one derives; several refuse ambiguous-organization). Right: D4 item 2 forbids a second spelling and core cannot import objectql.
  • GuestOrganizationUnresolvedError, isGuestOrganizationUnresolvedError, GUEST_ORGANIZATION_UNRESOLVED_CODE, GUEST_ORGANIZATION_UNRESOLVED_STATUS, GUEST_ORGANIZATION_UNRESOLVED_MESSAGE, GuestOrganizationRefusalReason — right. The code NOT_IMPLEMENTED is a member of the ADR-0112 catalog (packages/spec/src/api/errors.zod.ts in StandardErrorCode; error-code-ledger.zod.ts at 501), so no new code enters the ledger and the "new code is always yes" rule is not what makes this yes. The recogniser compares code and name, never instanceof, which holds across a package boundary. Right.
  • ResolveAuthzInput.guestEntry?: boolean — an optional field on an exported input type, additive and named ("the new guestEntry input"). Right.
  • No name collision: Type Check · source gates is green on the head, where an export * ambiguity would have failed.

@objectstack/objectql: tenancy/system-write-organization.ts re-exports the three bindings from core; the barrel's resolveSystemWriteOrganization and its two types keep their names and resolve to core's one function (identity pinned in system-write-organization.test.ts). Nothing a consumer imports moves. Right. plugin-security, plugin-sharing, runtime: no export moves (isGuestPrincipal is module-private; refuseGuestWithoutOrganization and guestOrganizationRefusalWarned are private members). Right.

Accept set. No packages/spec path; no authorable key, stored shape or response field moves; the ADR-0087 disposition none matches the E1 row verbatim. Right. The behaviour changes are runtime security-boundary changes (Clause ②'s negative boundary), each judged against the record:

  • D3 item 1 — the guest's sets are read by positionBoundSetNames, factored out of resolveUserAuthzGrants and now the one reader both paths call. Right.
  • D3 item 2 — nothing bound: no set, no organization, no read issued (core pin asserts the engine saw zero reads). Right.
  • D3 item 3 — resolvePermissionSetsForContextUnmemoized drops the guest's positions from the request (isGuest = principalKind: 'guest' with no userId), closing the name fold for the guest; the baseline attaches only to a caller with a userId; everyone lives in the user path a guest never reaches. permissionSetMemoKey carries principalKind and the presence of userId, so a guest and a human holding positions: ['guest'] never share a memo entry. Right, with human controls in the unit file.
  • D3 item 7 and D4 — the order in resolveGuestAnchorGrants is definition (in-process), then organization, then ledger, then bodies; nothing is granted without a derived decision, and the only row read before the decision is the organization probe itself. Right.
  • D4 item 1 — ctx.tenantId for a guest is written only from guest.organizationId; the tenantId the session and API-key paths derive is assigned after the anonymous branch has already returned, so nothing the client sends names the guest's organization, and tenancyPosture is deployment configuration. Right.
  • D4 item 3 — the refusal is raised at the CRUD gate (resolveOperationPrincipals, after the principal-less check and ahead of the fail-closed catch that would mask it as 403), not at the entry, so every 401 domain and the flow door (class 5) stay untouched. Right. Dropping the rule's own isSystem read in commit 5283490 is sound: both callers return for a system context first.
  • D4 "refused, loudly, naming the missing site binding" — the message names the site binding and both remedies and discloses no organization count or posture (the unit pin asserts the message carries no id, reason or count). 501 over 403 keeps an ungranted guest and an unresolvable deployment distinguishable, which is what a located refusal is for. Right.
  • Ruling C (6148183570) — SharingService.buildReadFilter answers { id: '__deny_all__' } for the guest principal after shouldBypass (the system bypass and the platform bypass list) and before the sharing-model branch and its owner-less and phantom-anchor fall-throughs, so private, public_read, public_read_write and controlled_by_parent each read 0 rows (5 unit shapes with member and system controls; 6 door pins with admin controls). resolveSharingReadFilter spreads the context into the call, so the analytics path meets the same guard. Share links are redeemed at their own door and never pass this filter (door control: the linked record is read; eligibility held at the mint, 422, and at redemption, 404). No externalSharingModel reader was added (security: externalSharingModel is declared and authored but has no runtime reader — an object's externalSharingModel: private changes no answer for any principal (ADR-0090 D11 declared-not-enforced) #23268 stays its own card). Right.
  • The write gates (buildWriteFilter, checkEdit, checkDelete) keep the OWD branch for a guest. Judged acceptable, not a gap: the anchor tier (describeAnchorForbiddenBits over describeHighPrivilegeBits, read at the head) refuses allowEdit, allowDelete, allowTransfer, allowExport, view-all and modify-all, and any systemPermissions for guest, so a tier-conformant binding never reaches those gates; create is the one case-by-case write and it takes no row filter. The residual is in ③.
  • runtime: resolveExecutionContext is the one face passing guestEntry: true; the other faces that call resolveAuthzContext (rest-server, mcp, sharing-plugin, settings, storage, datasource admin routes, marketplace install) omit it and stay fail-closed, matching D2's "everything else answers 401". Right.
  • The reference-app count: 0 bindings start granting and 0 fold grants stop, pinned by the dogfood census over app-showcase, app-crm and app-multi-package with a fixture control; app-todo declares neither positions nor sets. The changeset names them, per the row. Right.
  • scripts/engine-double-contract.pinned.json gains one row for the new unit file's pinned findOne, written by the gate's own --write: a record the gate requires, not a widened exemption. Right.

② Semver level

.changeset/23224-guest-grants-channel.md: @objectstack/core: minor; @objectstack/objectql, @objectstack/plugin-security, @objectstack/plugin-sharing, @objectstack/runtime: patch. This matches what the diff publishes: only core's exported index widens (ten additive bindings; nothing removed or narrowed), and the other four move no export and change behaviour only under the accepted record and ruling C. skip-changeset does not apply. The changeset's row-scope sentences state ruling C ("returns no row, whatever the object's sharing model"; "the guest reaches a record only through a share link"), its ADR-0087 line reads none, and it names the bindings that start granting, as the E1 row requires. Check Changeset is green on the head.

Clause-②: — the PR body's second line reads Clause-②: yes, and the governing claim 6146352069 reads Clause-②: yes (the first claim's no was corrected by the hand-off's measured fork 5 and the re-claim). Right: the published surface widens, so a contract-tier review is owed before the queue, and this record is that review. No governed surface is in the file list (Governed Surface Queue Guard green), so this record is the Clause-② control and not a Tier S one.

③ Boundary flags

Hand-off 6145588579, open_questions:

  1. Row scope (D3 item 6 against ADR-0090 D9/D11) — ruled C on [Decision] 访客(guest)的行可见范围:沿用对象的内部共享模型(A,现状,公开对象全部行可见)、按 externalSharingModel 判(B)、还是只经分享链接(C) #23266 (6148183570, 「同意」); applied here at buildReadFilter, pinned at unit and door with the link control. Answered.
  2. No authoring door for a guest binding — filed as security: no door can bind a permission set to the guest anchor — ADR-0138 D3 item 5 has no enforcer (PUT /meta/position/guest answers 403 NOT_OVERRIDABLE; guest suggestions are refused) #23267, re-pointed to this card by triage 6148511128, held undispatched until E1 lands (ruling item 4). Escalated, outside this PR.
  3. The refusal code — 501 NOT_IMPLEMENTED, a catalog member as D4 requires; judged right in ①. Answered.
  4. A walled deployment with exactly one organization — D9's refusal kept, per D4 item 2 (one predicate, no second spelling). It fails closed, and a deployment that needs the derivation is F2/D5's demand. Answered, no escalation.
  5. The Clause-② line — flipped to yes at the re-claim. Answered.

Final report 6152923496, open_questions: []; its deviations, each judged:

  • sharing-service.ts and its test outside the claim's files — ruling C's one site, pre-declared by PM direction 6149308636 ("the file surface grows by that one site"), and the dev named it. Answered.
  • The ledger row, the 4 comment lines and the dropped isSystem read in security-plugin.ts, the unit double's refusal of an unimplemented combinator, and the dogfood slot typing — gate-driven, each judged right in ①.
  • A2d (measured at df4be9b) and A5 (at 02caf8a) not re-run on the final head — verified by reading: packages/plugins/plugin-sharing/ has no commit after 02caf8a, and the fold hunk of security-plugin.ts is identical between df4be9b and the head (only the refusal's isSystem line and the S8 merge moved there, and the unit legs A1 to A4 were re-run at the head). The measurements carry.
  • Two merges of main, five commits behind at the final derivation — the queue rebuilds on the current main; no overlap claimed, none seen in the file list.
  • check:query-options-erasure re-run alone after a battery timeout — the head's own scoped run is green.
  • Commit trailers and background commands — outside the contract.

Out-of-scope findings and acceptance notes:

Implemented-by: claude/issue-23224-guest-grants-channel
Reviewed-by: session_01P88ehrZ6hM19hBzdpsQTTo

VERDICT: PASS


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants