Skip to content

feat(sharing): a share or sharing rule on a public object reaches an external member of the record's organization - #25591

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-eflr1z
Oct 24, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-eflr1z

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Closes #25549

Clause-②: yes (widening: the grant door and the materialiser admit one recipient population on effective-public objects)
Metadata-protocol: semantic

Letter A of the ruling on #25549 (record 6377844010, re-recorded 6378238137 after Tesla's item-by-item approval of 2026-10-24T01:28Z). An external member (sys_member.role exactly external) meets an owner-only floor on every object since #25541, so on a public object a share is how it reaches one record. The D7 pre-flight still refused that share as one "no gate would consult". This PR makes that verdict read the recipient's population.

What changes

Item Where What
The verdict plugin-sharing sharing-service.ts inertGrantReason(object, recipient?) On an effective-public object with a real owner_id (not a federated phantom anchor), a user recipient whose sys_member.role in the record's organization is exactly external is admitted. Every other recipient keeps the 422 SHARING_NOT_ENABLED, now worded "consulted only for an external member of the record's organization, and the recipient is not one". controlled_by_parent (ADR-0111 D7.2), owner-less, phantom-anchor and bypass objects are refused for every recipient, as before.
Where it is asked grant() The object half stays first and caller-free. The recipient half runs on both paths after the organization is resolved (the rule's threaded organization, else the record's; the record's, else the session's for a direct grant). On the direct path it runs after the D1 management check, so a caller who cannot manage the record learns nothing about the recipient. No organization resolved is refused with its own named reason.
The population test isExternalMemberIn The stage-1 doors' test (isExternalMembershipRole in plugin-auth): a sys_member row in that organization whose role list is exactly external. Mirrored locally, since plugin-sharing does not depend on plugin-auth.
Rule materialisation sharing-rule-service.ts The per-recipient refusal is marked (isInternalRecipientRefusal), and all three passes (reconcile, reconcileForRecord, reconcileRuleForUser) skip that recipient instead of aborting. A pass that skipped any carries one note line on its result and logs the count once ([sharing-rule] internal recipients skipped on a public object). Every other D7 refusal still aborts the rule's pass, as before.
Lint validate-sharing-rule-enforceability.ts, rule-explanations.ts sharing-rule-object-not-shareable on a public anchor drops from error to warning: "so the rule is effective for external recipients only". The controlled_by_parent arm stays an error. The os explain entry says when to keep the rule and when to change the object's sharingModel.
Docs content/docs/permissions/sharing-rules.mdx One paragraph under "External principals". permissions/index.mdx and the app-crm comment said the rule "fails the build"; both now say what it does.
Changeset .changeset/25549-external-share-public-object.md @objectstack/plugin-sharing and @objectstack/lint minor, with both declaration lines.

How the rule's report surfaces (measured)

POST /api/v1/sharing/rules/:idOrName/evaluate answers evaluateRule's return value unfiltered (sharing-routes.ts, res.json(result)), so the note reaches the admin who evaluates a rule. The boot backfill, the write hooks and the re-grant queues read only the counts, so for them the log line is the report. note is declared on plugin-sharing's own SharingRuleReconcilePassResult, not on the spec's SharingRuleEvaluationResult.

Verification (head a1260e57)

  • plugin-sharing, system-caller-inert-grant.test.ts, new block (9 cases): an external recipient is admitted, its row carries the record's organization and buildReadFilter for that external returns exactly that record; an internal recipient gets SHARING_NOT_ENABLED: and no row; a user external in org B and a member of org A is refused on an org A record, and the reverse user is admitted; a role list external,member is refused; a system caller gets the same two answers; a caller without management rights gets PERMISSION_DENIED first; a team rule expanding to one external and one staff member on two gold records writes two rows for the external, grantsRefused: 0, the note, and one log line with skipped: 2; a rule to externals only has no note; an owner-less public object still refuses an external.
  • The plugin-sharing: grant() skips the ADR-0111 D7 inert-grant guard entirely for SYSTEM callers, so the sharing-rule evaluator can still materialise rows no gate consults #8207 pins in the same file: the rule-pass refusal block now covers the four object classes that still abort, and a new case pins that a rule on the public object with an internal recipient mints nothing and carries the note.
  • pnpm --filter @objectstack/plugin-sharing typecheck exit 0; the suite: 55/55 files, 1233 tests passed.
  • pnpm --filter @objectstack/lint typecheck exit 0; the suite: 158/158 files, 7237 tests passed.
  • Dogfood, external-member-directory.dogfood.test.ts on a showcase boot, two new cases: the owner POSTs a share of one showcase_invoice (public_read_write) to an external over /data/showcase_invoice/ID/shares and gets 201 with the organization stamped; the external read zero invoices before and exactly that one after. The same share to an ordinary member answers 422 with error.code SHARING_NOT_ENABLED, and no row is written. The file: 13/13. tsc --noEmit on the dogfood package exit 0.
  • Ablation (committed fix first, scripts/ablation-replace.mjs): if (recipient === 'external') return null; replaced so the external arm returns a reason. Anchor 1 to 0, blob c1f616f5 to 5d5c8c31; the file went red, 8 failed of 33; restored, blob equals HEAD, git diff HEAD empty.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at a1260e57 derived 104 families; each ran with its exit code captured before any pipe. First sweep: 101 exit 0, 3 exit 3 (PREREQUISITE NOT MET: the pinned control commit absent from the shallow clone for check-plugin-teardown-shape --self-test; no client-react dist for check:skill-examples; workspace dists for check:dual-build-cjs-loads). After fetching that commit and a locked workspace build, all three re-ran exit 0. --ran reconciliation: 104 derived, 104 run, 0 NOT-MEASURED, 0 UNRUN.

Acceptance notes

  • The two confidence gaps. The REST door and the rule evaluator both have the organization at hand: grant() resolves it on both paths before writing (commit 3f64fe6), and the recipient half reads it from there. When neither path resolves one, the grant is refused with a named reason, not judged against the session's organization.
  • Error order on a public object. For a direct grant on a public object the 422 now comes after the D1 management pre-flight, so a caller who may not manage the record gets 403/404 where it used to get 422. That is deliberate (the population is only read for an authorized caller). On every other object the order is unchanged.
  • Existing inert rows. A source: 'rule' row for an internal recipient on a public object, minted by an older build, is still left standing by a pass when its level is unchanged; it is never consulted for that recipient.
  • Spec contract docblock. ISharingService.grant's docblock in packages/spec/src/contracts/sharing-service.ts still says a share on a public object "enforces nothing". It is outside this claim's file surface and is left for the seat to route with the ADR-0111 line drafted in the os-dev-report.
  • ADR-0111 D7. The dated line is Tier H and is not in this PR; its draft is in the os-dev-report on the card.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AjwMndyviznGagEUcX7QVx

On an effective-public object the D7 inert-grant verdict now reads the
recipient's population: a user recipient whose sys_member.role in the
record's organization is exactly `external` is admitted, because the
external OWD floor consults its share rows there. Every other recipient
keeps the 422 SHARING_NOT_ENABLED. The rule materialiser skips the
internal recipients a rule expands to, keeps its pass going, and reports
the skipped half in one note line and one log line per pass.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjwMndyviznGagEUcX7QVx
…or external recipients only

sharing-rule-object-not-shareable drops from error to warning on its
public arm: the runtime now admits a grant there for an external member
of the record's organization, so the rule is live for its external half.
The os explain entry, the docs sentence in sharing-rules.mdx, the
permissions index paragraph and the app-crm comment say the same.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjwMndyviznGagEUcX7QVx
…external

On a showcase boot the record's owner shares one invoice with an external
member over the REST share door (201) and the external then reads that
invoice and no other; the same share to an ordinary member answers 422
SHARING_NOT_ENABLED. Adds the changeset.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjwMndyviznGagEUcX7QVx
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/plugin-sharing, touching 25 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/environment-variables.mdx (via sys_member (literal, a string literal in isExternalMemberIn))
  • content/docs/deployment/tenancy-modes.mdx (via sys_member (literal, a string literal in isExternalMemberIn))
  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object))
  • content/docs/kernel/runtime-services/sharing-service.mdx (via evaluateRule (symbol, a method of class SharingRuleService))
  • content/docs/permissions/authentication.mdx (via sys_member (literal, a string literal in isExternalMemberIn))
  • content/docs/permissions/delegated-administration.mdx (via sys_member (literal, a string literal in isExternalMemberIn))
  • content/docs/permissions/permission-sets.mdx (via sys_member (literal, a string literal in isExternalMemberIn))
  • content/docs/permissions/positions.mdx (via sys_member (literal, a string literal in isExternalMemberIn))
  • content/docs/permissions/sharing-rules.mdx (via SharingRuleService (symbol, a top-level class), /sharing/rules/:idOrName/evaluate (route, bridged from symbol evaluateRule — its route source's handler names it))
  • content/docs/permissions/system-context.mdx (via sys_member (literal, a string literal in isExternalMemberIn))

⛔ 8 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via sys_member (literal, a string literal in isExternalMemberIn))
  • content/docs/releases/v16.mdx (via sys_member (literal, a string literal in isExternalMemberIn))
  • content/docs/releases/v17/17-0.mdx (via sys_member (literal, a string literal in isExternalMemberIn))
  • content/docs/releases/v17/17-1.mdx (via sys_member (literal, a string literal in isExternalMemberIn))
  • content/docs/releases/v17/17-3.mdx (via SharingService (symbol, a top-level class), sys_member (literal, a string literal in isExternalMemberIn))
  • content/docs/releases/v17/17-6.mdx (via sys_member (literal, a string literal in isExternalMemberIn))
  • content/docs/releases/v18/18-0.mdx (via sys_member (literal, a string literal in isExternalMemberIn))
  • content/docs/releases/v18/18-1.mdx (via sys_member (literal, a string literal in isExternalMemberIn))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 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 — 13 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 0951a00315a256cdb5ae09367f56a78b01636d6b → packageMentionDocs.

Which tree this was computed on

This run read content/docs from da8c1c90b353ce87a487ffe195feeb2d33af19ec — the merge of head a1260e572f68a044e90b689eb0f360fd4f2813a3 into base 0951a00315a256cdb5ae09367f56a78b01636d6b, 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 da8c1c90b353ce87a487ffe195feeb2d33af19ec && git checkout da8c1c90b353ce87a487ffe195feeb2d33af19ec
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0951a00315a256cdb5ae09367f56a78b01636d6b a1260e572f68a044e90b689eb0f360fd4f2813a3 && git checkout -B drift-repro 0951a00315a256cdb5ae09367f56a78b01636d6b && git merge --no-ff a1260e572f68a044e90b689eb0f360fd4f2813a3

node scripts/docs-audit/affected-docs.mjs --json 0951a00315a256cdb5ae09367f56a78b01636d6b

⚠️ 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 0951a00315a256cdb5ae09367f56a78b01636d6b → 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: TIER_DEFAULT
Head-sha: a1260e572f68a044e90b689eb0f360fd4f2813a3
Local-runs: none

Inputs: card #25549 (body, the director increment 6377536821, rulings 6377738234 / 6377844010 / 6378238137 and the retraction pair, the Claim 6378250869, the os-dev-report 6378915159), PR #25591 (body, 12-file list, net diff against main), and the check-runs on the head. Review face: declaration (Clause-②: yes on the PR body and the changeset; no published schema or governed path in the committed tree — packages/spec is untouched).

① Derived judgments

The ruling (letter A) admits exactly one new accept-set: on an effective-public object, a share or sharing rule whose recipient is a user who is an external member of the record's organization. Each change the diff implies, read against that:

  1. inertGrantReason(object, recipient?) — the object half is unchanged, the recipient half is bounded. Right. The new arm fires only when effectiveSharingModel(schema) === 'public' AND hasOwnerField(schema) AND !hasPhantomOwnerAnchor(schema) AND a recipient standing was supplied. public_read (effective read) is not touched; owner-less objects, phantom-anchor federated objects, controlled_by_parent details and the bypass set keep their caller-free refusal for every recipient (the arms are tested in order; controlled_by_parent and bypass return before the public arm). With recipient omitted the arm refuses as before, so assertNotInertGrant(object) on its own is not widened.
  2. Recipient population test (isExternalMemberIn). Right. One sys_member row with user_id = recipient and organization_id = the resolved organization, whose role list is exactly external (distinct, trimmed, lower-cased tokens, length 1). I compared it to plugin-auth's isExternalMembershipRole / roleTokens on main (packages/plugins/plugin-auth/src/external-membership.ts:48-62): same tokenisation, same MEMBERSHIP_ROLE_EXTERNAL constant from @objectstack/spec. external,member is refused (pinned). A user external in org B and member of org A is refused on an org A record; the reverse user is admitted (pinned). Non-user recipient types never reach the new arm: grant() still refuses them VALIDATION_FAILED before any pre-flight.
  3. Where the recipient half runs. Right, and narrower than it had to be. assertRecipientNotInert runs on both paths after the organization is resolved; on the direct path after assertSharingEnforced (existence) and assertCanManageShares (D1), so an unauthorised caller gets 403/404 and never learns a recipient's population. The granter is never an input (system caller and owner get the same two answers, pinned). organizationId === null yields unknown-organization and a named SHARING_NOT_ENABLED refusal — the acceptance item "both paths resolve the recipient's role in the object's organization, or refuse with a named reason" is met. The direct path's fallback to the session's organization applies only to a record that carries no organization at all (reading.kind === 'none'), not to a failed read; that is the pre-existing commit-3f64fe6c6 shape and is the only organization such a record has.
  4. Rule materialisation. Right. grantOrAbsorbOrganizationRefusal returns a three-way outcome; only a refusal carrying the INTERNAL_RECIPIENT_REFUSAL symbol becomes 'internal' (skipped, not counted as refused); every other SHARING_NOT_ENABLED still propagates and aborts the rule's pass (the [#8207] block now pins the four aborting classes and a new case pins that the public class mints nothing for an internal recipient). All three passes (reconcile, reconcileForRecord, reconcileRuleForUser) thread skipped into withInternalHalfNote, which writes one note on the pass result and one log line per pass. The acceptance item "one report line when the staff half is skipped" is met, and the PR measured the route that surfaces it (POST /sharing/rules/:idOrName/evaluate answers the result unfiltered).
  5. Internal-only invariants still hold. Checked against the read side, which this diff does not touch: buildReadFilter returns null for an internal reader on a public object (owdForPrincipal(...) !== 'private' at sharing-service.ts:612 on the head), so a share row minted for an external is never consulted for staff, and no row is minted for an internal recipient on either path (dogfood: 422, zero sys_record_share rows; unit: zero rows). For the external, the floored filter is {$or: [{owner_id: me}, {id: {$in: [shared]}}]} — exactly the record handed to it (pinned at the unit level and over REST on a showcase boot).
  6. Lint sharing-rule-object-not-shareable: the public arm drops to warning with the ruled wording "effective for external recipients only"; the controlled_by_parent arm stays error. Right — item 3 of the ruling, and the lint's verdict function still mirrors the runtime's object half.
  7. Docs / examples / registry comment. sharing-rules.mdx gains the ruled paragraph; permissions/index.mdx, examples/app-crm/objectstack.config.ts and the AUTHORING_RULES comment said the rule "fails the build" — a sentence this round makes false, corrected. Right (see ③ for the file-surface flag).
  8. Public surface. No spec key, value or type changes (Metadata-protocol: none on the card); SharingRuleReconcilePassResult.note? is an optional field on plugin-sharing's own interface, not the spec's SharingRuleEvaluationResult; isInternalRecipientRefusal is a new named export of plugin-sharing. Additive, minor.

Adversarial probes that found nothing wider than the ruling: a user whose membership row is in another organization (refused); a role list with a second token (refused); a public object with no owner_id (refused for an external — pinned); a phantom-anchor federated public object (refused — guarded in the arm); a system caller (same answer as a user caller); an unauthorised caller (403 first); a public_read object (effective read, arm never reached); a controlled_by_parent detail (refused before the arm).

Two small wording-level observations, neither blocking:

  • system-caller-inert-grant.test.ts:196 says the public class is "pinned ... in external-recipient-public-object.test.ts"; no file of that name exists on the head (the pins live in the same file's new block 3). Stale comment, fix at leisure.
  • When a rule-path grant resolves no organization (a deployment-wide rule over a record with no organization), the refusal is unknown-organization, still carries the INTERNAL_RECIPIENT_REFUSAL mark, and the pass counts it under the "internal recipients skipped" note. Fail-closed and no row is written, so the ruling is kept; the note's wording slightly misnames that case.

② Semver level

.changeset/25549-external-share-public-object.md: @objectstack/plugin-sharing minor, @objectstack/lint minor. Matches what the diff publishes: a widened accept-set on SharingService.grant, a new optional result field and a new named export in plugin-sharing; a severity drop and new wording in lint. No major (nothing removed), not patch (behaviour admitted that was refused). The Clause-②: line reads yes (widening: the grant door and the materialiser admit one recipient population on effective-public objects) on both the PR body and the changeset, identically, and names the widening this diff makes and no other. Metadata-protocol: semantic on the changeset (the card body said none; the Claim said semantic, the dev reports check-changeset-no-major exit 0 and the Check Changeset check-run is green twice on the head) — consistent with a lint refusal becoming advice, and the two lines agree between PR body and changeset.

Check-runs on the head: every completed run is success or skipped (opt-in / conditional lanes). Completed green: Lint & Repo Gates, Check Changeset, Governed Surface Queue Guard, Check PR Size, Spec property liveness, Dogfood Regression Gate, Build Docs, all Lint lanes, Type Check · source gates, Type Check · debt ledger, Check Documentation Links, the four PR-claim guards, Flag docs affected by code changes. Still in_progress when this record was rendered: Test Core 1/6–6/6, Build Core, Type Check · consumer gates, Type Check · workspace. The verdict below is judged on the completed runs and the diff; the dev's local runs (plugin-sharing 1233/1233, lint 7237/7237, dogfood file 13/13, 104/104 dispatch-gate families) are the dev's claim, not a gate verdict, and are not what carries it.

③ Boundary flags

open_questions: [], out_of_scope_findings: []. The six deviations in the os-dev-report, each answered:

  1. File surface — three files outside the Claim's list (permissions/index.mdx, examples/app-crm/objectstack.config.ts, lint/src/authoring-rules.ts). Accepted: each is a shipped sentence this change falsifies ("fails the build" / "build error"), the edits are comment- or prose-only, and none adds behaviour. Seat: add the three paths to the Claim's file surface as the dev asks.
  2. Changeset minor, Metadata-protocol: semantic where the card said none. Accepted — see ②; the semantic line is what the gate reads and it is green.
  3. Unit test placed in system-caller-inert-grant.test.ts rather than a new file. Accepted (reuses the pinned engine double; no ledger change). The stale cross-reference at line 196 noted in ① follows from this.
  4. note declared on plugin-sharing's SharingRuleReconcilePassResult, not on the spec's type. Accepted; keeps packages/spec out of the diff and the tier at TIER_DEFAULT. If a client is ever to read the note from the typed evaluate result, that is a spec change for a later card.
  5. PR body erratum: Acceptance note "Spec contract docblock" ends "with the ADR line below", and no ADR line is in the PR. Escalated to the seat with the dev's PATCH text: replace "with the ADR line below" by "with the ADR-0111 line drafted in the os-dev-report". Prose only; does not affect the verdict.
  6. ADR-0111 D7 dated line left out (Tier H) — correct per the Claim (⛔ no ADR edit); the draft rides in adr_0111_draft. Escalated to the seat to route with the Tier H docs PR for Security: an external organisation role whose members do not see the member directory and reach data only through explicit grants (portal users on hosted environments) #25233.

Further flag raised by the dev and not a deviation: spec_docblock_followup — ISharingService.grant's docblock in packages/spec/src/contracts/sharing-service.ts still says a share on a public object "enforces nothing", now half true. Comment-only on a published d.ts, outside this claim's surface. Escalated to the seat to route alongside item 6.

PR-body acceptance notes read and agreed: the error-order change on a public object (D1 before the recipient half — deliberate, tested, narrower not wider); existing source: 'rule' rows for internal recipients on public objects minted by older builds are left standing and are never consulted for that population (no leak; a follow-up purge is optional hygiene).

Implemented-by: claude/project-thread-eflr1z
Reviewed-by: session_013odhA26dvzy7pQmevdTTvy/contract-review-agent

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (seat post #6017) · os-tesla · session session_013odhA26dvzy7pQmevdTTvy · 2026-10-24T03:19Z

Head a1260e572f · card #25549 · ruling 6378238137 (letter A, Tesla's approval relayed by the director thread) · claim 6378250869 plus its file-surface addendum · report 6378915159 · contract review 6378972131 (PASS).

  • Shape: draft, base main, Closes #25549. 12 files, +531 / −82. check-governed-merges: not governed, 613 lines, which is under the human-merge threshold.
  • Contract: the one ruled widening. A user recipient who is exactly an external member of the record's organization may be shared a record of an effective-public object that has a real owner. Both paths do this: the grant door and the rule materialiser. public_read, owner-less, phantom-anchor, controlled_by_parent and bypass objects keep the caller-free refusal. Internal recipients get no rows on either path. An unresolved organization is refused with a named reason. The granter is never an input.
  • Changeset: @objectstack/plugin-sharing and @objectstack/lint minor. Clause-②: yes (widening …) and Metadata-protocol: semantic agree between the PR body and the changeset. PR-body erratum patched by the seat.
  • Gates pinned to the head: 49 check-runs on a1260e572f, 41 success + 8 skipped, 0 red. Mergeable state clean.
  • Follow-ups, not in this PR: the ADR-0111 D7 dated line and the ISharingService.grant docblock in packages/spec go as one Tier H PR (drafts in the report). It also carries the wording-only nits the review named: the stale test-file name in the comment at system-caller-inert-grant.test.ts:196, and the note text that also covers the unknown-organization case.
  • Landing: pr_ready + automerge_enable now.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 24, 2026 03:20
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit 4a25192 Oct 24, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-eflr1z branch October 24, 2026 03:47
marchtian pushed a commit that referenced this pull request Oct 24, 2026
…25549 words

#25591 (#25549) made a public object's share row reachable by an external
member of the record's organization, so SharingService.grant's refusal on a
public object now names the recipient's standing. The showcase D3/D4 proof
still pinned the pre-#25549 sentence and failed the Dogfood Regression Gate on
main from a57643d, which refused the 18.1.0 release at its version commit.

Claude-Session: https://claude.ai/code/session_014C7q5Hm34g4EPdirar9SzN
Co-authored-by: Claude <noreply@anthropic.com>
marchtian pushed a commit that referenced this pull request Oct 24, 2026
This reverts commit 74fa3cb.

The 18.1.0 publish was refused at that version commit: its full battery is red
(Dogfood Regression Gate, the showcase D3/D4 proof pinning the pre-#25549
refusal sentence), and the release gate reads only runs at or before the
version commit, so no re-run on it can go green. Reverting restores the 265
changesets it consumed, including the five that landed after the Version
Packages PR was generated (#25591, #25592, #25596 and two patches), so the
regenerated Version Packages PR versions 18.1.0 again from a tree whose
battery can be read green, with a changelog that names everything it ships.
Nothing was published: npm still has 18.0.0 and no 18.1.0 tag exists.

Claude-Session: https://claude.ai/code/session_014C7q5Hm34g4EPdirar9SzN
Co-authored-by: Claude <noreply@anthropic.com>
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/l tests tooling

Projects

None yet

2 participants