Repository navigation
feat(sharing): a share or sharing rule on a public object reaches an external member of the record's organization - #25591
Conversation
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
📓 Docs Drift CheckThis PR changes 2 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 8 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Contract reviewServed-tier: 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 ① Derived judgmentsThe ruling (letter A) admits exactly one new accept-set: on an effective-
Adversarial probes that found nothing wider than the ruling: a Two small wording-level observations, neither blocking:
② Semver level
Check-runs on the head: every completed run is ③ Boundary flags
Further flag raised by the dev and not a deviation: 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 Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT ·
|
…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>
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>
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.roleexactlyexternal) 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
plugin-sharingsharing-service.tsinertGrantReason(object, recipient?)publicobject with a realowner_id(not a federated phantom anchor), auserrecipient whosesys_member.rolein the record's organization is exactlyexternalis admitted. Every other recipient keeps the 422SHARING_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.grant()isExternalMemberInisExternalMembershipRolein plugin-auth): asys_memberrow in that organization whose role list is exactlyexternal. Mirrored locally, since plugin-sharing does not depend on plugin-auth.sharing-rule-service.tsisInternalRecipientRefusal), and all three passes (reconcile,reconcileForRecord,reconcileRuleForUser) skip that recipient instead of aborting. A pass that skipped any carries onenoteline 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.validate-sharing-rule-enforceability.ts,rule-explanations.tssharing-rule-object-not-shareableon a public anchor drops fromerrortowarning: "so the rule is effective for external recipients only". Thecontrolled_by_parentarm stays anerror. Theos explainentry says when to keep the rule and when to change the object'ssharingModel.content/docs/permissions/sharing-rules.mdxpermissions/index.mdxand the app-crm comment said the rule "fails the build"; both now say what it does..changeset/25549-external-share-public-object.md@objectstack/plugin-sharingand@objectstack/lintminor, with both declaration lines.How the rule's report surfaces (measured)
POST /api/v1/sharing/rules/:idOrName/evaluateanswersevaluateRule's return value unfiltered (sharing-routes.ts,res.json(result)), so thenotereaches 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.noteis declared on plugin-sharing's ownSharingRuleReconcilePassResult, not on the spec'sSharingRuleEvaluationResult.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 andbuildReadFilterfor that external returns exactly that record; an internal recipient getsSHARING_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 listexternal,memberis refused; a system caller gets the same two answers; a caller without management rights getsPERMISSION_DENIEDfirst; 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 withskipped: 2; a rule to externals only has no note; an owner-less public object still refuses an external.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 typecheckexit 0; the suite: 55/55 files, 1233 tests passed.pnpm --filter @objectstack/lint typecheckexit 0; the suite: 158/158 files, 7237 tests passed.external-member-directory.dogfood.test.tson a showcase boot, two new cases: the owner POSTs a share of oneshowcase_invoice(public_read_write) to an external over/data/showcase_invoice/ID/sharesand 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 witherror.codeSHARING_NOT_ENABLED, and no row is written. The file: 13/13.tsc --noEmiton the dogfood package exit 0.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 HEADempty.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackata1260e57derived 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 forcheck-plugin-teardown-shape --self-test; noclient-reactdist forcheck:skill-examples; workspace dists forcheck:dual-build-cjs-loads). After fetching that commit and a locked workspace build, all three re-ran exit 0.--ranreconciliation: 104 derived, 104 run, 0 NOT-MEASURED, 0 UNRUN.Acceptance notes
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.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.ISharingService.grant's docblock inpackages/spec/src/contracts/sharing-service.tsstill 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.🤖 Generated with Claude Code
https://claude.ai/code/session_01AjwMndyviznGagEUcX7QVx