Repository navigation
feat(plugin-sharing,cli)!: declared sharing rules are read from the registry; the ownership ceremony deletes their seeded copies - #24376
Conversation
…egistry; the ownership ceremony deletes the seeded copies ADR-0131 D2 / C3 stage 9 (ruling beta on the C3 epic: stage 9 first, the verified marker after it). plugin-sharing: bootstrapDeclaredSharingRules retires. The sharing-rule service reads a stack's declared sharingRules from the registry (engine.registry, else the metadata service) on every read of the rule set, translated as the seeder translated them, and projects each one per scope: once organization-less under `single`, once per organization under a wall. A declared rule's id is its name, which is what its sys_record_share grants carry as source_id; one organization's grants are told apart from another's by their organization. A stored row of the same name answers for it. A declared rule cannot be deleted through the service (403). sys_sharing_rule holds only organization-authored rules. cli: DECLARED_SHARING_RULE_SEEDER_RUNS flips to false. The ceremony's mirror-deletion step executes the declared-rule-mirrors departure: every grant naming a mirror row by id is renamed to the rule's name, verified, and only then are the mirrors deleted; it re-runs on a checkpointed ceremony while a mirror stands, and blocks on a mirror whose rule no package declares while grants still name it. lint: the sharing-rule enforceability text says "never served" rather than "never seeded". The liveness ledger's producer pointers follow the renamed module. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YHtySYfURHqg298wCCJ8wj
…clared-sharing-rules-registry
📓 Docs Drift CheckThis PR changes 4 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 149 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 6351373e631ef16f8f2e1b7c7670d9b972f89524 && git checkout 6351373e631ef16f8f2e1b7c7670d9b972f89524
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5da2e88818a006145330e3f323759ebef4d6ca87 e0baaa76c69c4d53e43cad8002f7cda487ccd0b3 && git checkout -B drift-repro 5da2e88818a006145330e3f323759ebef4d6ca87 && git merge --no-ff e0baaa76c69c4d53e43cad8002f7cda487ccd0b3
node scripts/docs-audit/affected-docs.mjs --json 5da2e88818a006145330e3f323759ebef4d6ca87
|
Contract reviewServed-tier: Rendered by an isolated at-tier reviewer subagent of the adopting seat ( Check-runs on the head, as read (the 2026-10-18 17:14Z run, last re-read 17:43Z): 40 check-runs — 32 ① Derived judgmentsEvery accept-set and public-surface change the diff implies, named right or wrong. (a) (b) Grants of a declared rule carry (c) The walled posture. No cross-tenant read or write of GRANTS: (d) A stored row of the declared name answers for it. INHERITED, not new: the seeder's (e) Other surfaces the diff moves, all RIGHT: ② Semver levelThe changeset bumps The The ADR-0087 disposition marker is held by a detector miss and must be re-decided with the arm: the body already names the operator's re-run ( ③ Boundary flagsEvery dev flag and every
Blocking items — each with the fix it needs:
Implemented-by: VERDICT: FAIL |
Contract reviewServed-tier: Rendered by an isolated at-tier reviewer subagent of the adopting seat ( Check-runs on the head, as read (2026-10-18, the 18:30Z run and the 19:05Z body-edit re-run): 48 check-runs, 0 ① Derived judgmentsEvery accept-set and public-surface change the diff implies, named right or wrong — the whole PR, not only the round's delta. The three items the FAIL record listed as blocking first, each verified against the code at this head rather than against the addendum. (1) The declared-rule DELETE refusal is pinned on the code and on a sourced status, for both callers. RIGHT, fixed. (2) The walled scope set is complete or UNKNOWN, never partial, and a failed read is never a revocation. RIGHT, fixed. Verified in the code, path by path:
(3) The changeset, the ADR-0087 marker and the semantic entry. RIGHT, fixed; one wording correction owed, not blocking. (a) (b) Grants carry the rule's NAME; the ceremony renames before any mirror deletion (ruling β item 5 (c)). RIGHT. (c) The walled posture. RIGHT — judged in (2); the per-organization projection, the organization-stamped grant scope, the creation-time scope add ( (d) Same-name shadowing. INHERITED and correctly bounded. The (e) Other surfaces the diff moves, all RIGHT: ② Semver levelThe changeset bumps Wrong text, exactly: none on the changeset or the entry that changes a consumer's action. Two corrections for the seat, non-blocking: (i) the entry's D13 paraphrase at ③ Boundary flagsEvery dev flag, deviation and
Implemented-by: VERDICT: PASS |
Fixes #23952
Clause-②: no (narrowing)
C3 stage 9 of ADR-0131 (#15204), in the order ruling β (6216760903) sets. Declared sharing rules are read from the registry, and
bootstrapDeclaredSharingRulesretires. The ownership ceremony renames the grants of the copies that seeder wrote, then deletes the copies.DECLARED_SHARING_RULE_SEEDER_RUNSflips tofalsein this same change, as the #23948 hand-off (6220693269) orders. This is one PR, not 9a then 9b: 2,615 changed lines (+1,881 / -734, 28 files) after patch round 1, under the 3,000 line.What changes
@objectstack/plugin-sharing: enforcement reads declared rules from the registry.declared-sharing-rules.tsreplacesbootstrap-declared-sharing-rules.ts.createDeclaredSharingRuleSourcereadsengine.registry.listItems('sharing_rule'), and the metadata service'slist('sharing_rule')only when the registry holds no rule. That is the same two readers, in the same order, the seeder used. It translates each declaration into the runtime shape, exactly as the seeder did (ADR-0057 D6). Translation is memoised per definition object. A rule that is not served says so once per rule and cause per process.SharingRuleServicetakes the source (declaredRules) and projects each declared rule into the scopes the plugin sets from the posture (setDeclaredRuleScopes/addDeclaredRuleScope). Undersinglethere is one organization-less scope. Under a wall there is one scope per organization, plus an organization created after boot. These are the seeder's passes (The Layer-0 tenant wall's strict equality annihilates the driver's platform bucket: #2734's fix is defeated on every walled read, and the org-less RBAC catalog reads ZERO for every principal #10103), as reads.listRules(which the hooks, the object and boot reconciles, the business-unit recompute and the rebind all go through),getRule,evaluateRuleand the orphaned-grant sweep.source_id= the rule's name (ruling β item 5 (c)). Under a wall each organization's projection reads and writes only its own grants:ruleGrantScopeadds the grant'sorganization_id. An organization-authored rule keeps its row and its id-basedsource_id.singlethat is a row of that name in any organization, and under a wall that organization's row. This is the row the seeder's upsert would have found: an organization's own rule, a customized seeded copy, or a seeded copy the ceremony has not yet deleted. The seeder never overwrote admin-authored or customized rows, and this keeps that.deleteRuleon a declared rule answersPERMISSION_DENIED. It has no row, and is changed in code.sys_sharing_rulerow for a declaration.@objectstack/cli: the ceremony executes thedeclared-rule-mirrorsdeparture.DECLARED_SHARING_RULE_SEEDER_RUNS = false, so the mirrors hold certification again.mirror-deletionstep ends withrunDeclaredRuleMirrorDeletion, which works in the ruled order:managed_bypackage or platform,customizednot true), under every organization.sys_record_sharegrant naming a mirror by row id to the rule's name.dev.db).DeclaredSharingRulesPort. The command answers it from the booted registry, which is what enforcement reads.@objectstack/lint. The sharing-rule enforceability verdict andos explainnow say a declared rule whose condition does not lower is "never served", not "never seeded".Pins flipped (the whole-repo grep for the seeder's behaviour):
organization-ownership.declared-rule-mirrors.integration.test.tscase 1: a served boot writes zero rows and the service serves every declared rule under its name. Case 2: eleven mirrors with grants are renamed and then deleted, and every grant resolves. The result is[complete, blocking, advisory] = [true, 0, 0]and the marker is verified. Then the guarded served boot serves the declared rules.organization-ownership-apply.declared-rule-mirrors.test.ts, which also covers the checkpointed re-run, the undeclared-with-grants block, the grant-less undeclared delete, the no-registry block, the walled copy and the seeding-release advisory leg.organization-ownership-apply.test.ts.sqld).standalone-stack-seeder-declaration-copy.test.tsnow reads the served rules, andsys_sharing_ruleis empty.sharing_ruleliveness producer pointers.Landing points outside the claimed file surface, and why
packages/spec/liveness/sharing_rule.jsonandpackages/spec/scripts/liveness/proof-registry.mts. These are evidence pointers only: no schema, no type and no generated artifact.check:livenessresolves everypath#Symbolproducer anchor, and nine of them namedbootstrap-declared-sharing-rules.ts#bootstrapDeclaredSharingRules. Retiring the seeder without moving them reds that gate. The card's ⛔ onpackages/specconcerns registeringsharing_ruleas a metadata kind, and none was needed (below). This is declared as a deviation in the report.packages/lint. Its author-facing text named the seeder and said "never seeded".packages/runtime(one test) andpackages/qa/dogfood. Pins on the seeder's rows.Measurements (mechanism assumptions A1–A7)
sys_sharing_rule(listRules/getRule/findRuleRowByName/ the sweep), and grants carriedsource_id: rule.id. Every consumer (rule-hooks,bu-tree-recompute, the backfill, the rebind) lists throughlistRules, so the projections reach all of them.packages/specchange is needed. The ObjectQLSchemaRegistryalready holdssharing_ruleitems:METADATA_ARRAY_KEYSlistssharingRules, andAppPlugin.initregisters forward-converted bytes. The metadata service binds the type throughUNREGISTERED_KIND_SCHEMAS. Both are the seeder's own readers.conditionis a bare string where the door has{ dialect, source }, andactiveis absent.compileCelToFiltertakes both condition shapes, and the reader readsactive !== false, which is the schema default. Enforcement reads the registry copy, as the seeder did.view,dashboard,report,translation,email_template), C (flow,action, …) and E (object,app).sharing_ruleis in none of them. ADR-0057 D6 speaks only to seeding. No disable mechanism was built. See the report'sopen_questions.SharingService.grantwithsourceId: rule.id. Its upsert keys on(object, record, recipient_type, recipient, source), not onsource_id, so the ceremony's rename cannot create a duplicate. They are read byreconcile,reconcileForRecord,purgeGrants,revokeRuleGrantsForRetiredRecipientsand the sweep, all throughruleGrantScope. An organization-authored rule keeps its id-basedsource_id, which is pinned.permissions/capabilities/sharingRulesin the ObjectQL SchemaRegistry (AppPlugin.init→manifest.register), and the plugin-security / plugin-sharing seeders read that copy FIRST #14491). Enforcement reads the registry copy, un-parsed, exactly as the seeder did. [finding] After #12892 step 2 an artifact boot with an engine still holds a THIRD, un-parsed copy ofpermissions/capabilities/sharingRulesin the ObjectQL SchemaRegistry (AppPlugin.init→manifest.register), and the plugin-security / plugin-sharing seeders read that copy FIRST #14491's parse axis is unchanged. Its consequence moves from the seeded row to the served rule, and is absorbed forconditionandactive.dispatch-gates; 2,615 (+1,881 / -734, 28 files) after patch round 1.Evidence
@objectstack/plugin-sharing: test 48 files / 1131 passed, and typecheck 0 (including the test typecheck).@objectstack/lint: 144 / 6858 passed, and typecheck 0.@objectstack/cli:--project unit: 310 files. Two needed the CLI's owndistbuilt first and passed afterpnpm --filter @objectstack/cli build.organization-ownership*suite, both tiers: 17 passed, 1 skipped (sqld).@objectstack/runtime: the declaration-copy pin 13/13, and typecheck 0.@objectstack/dogfood: typecheck 0. The rbac-seeding, d3-d4, org-scoped-listing, authz-conformance, criteria-required and org-less-caller suites passed.origin/main(e3658ab).scripts/ablation-replace.mjs, restored to HEAD,git diff HEADempty both times):declaredRulesreturning[]inlistRulesturned 7 of the new registry-read pins red.dispatch-gates --commandsderived 82 families.--ranreports 82 run, 0 NOT-MEASURED, on e3658ab.check:doc-authoringfirst went red on#23952in the departure label. See the first acceptance note.check:slot-lookupfirst went red on three new untyped service lookups. They are typed now.check:dual-build-cjs-loadspassed once the eight packages it reads were built.check:plugin-teardown-shape --self-testpassed after fetching its pinned commit into the shallow clone.Acceptance notes
minorin the launch window.DELETE /api/v1/sharing/rules/:idOrNameon a declared rule: 204 → 403PERMISSION_DENIED(remedy: change or remove the declaration in its package).sys_sharing_rule: onemanaged_by: packagerow per declared rule → none (remedy:GET /api/v1/sharing/ruleslists declared rules; runos migrate organization-ownership --applywith a fresh plan). ADR-0087:registered sharing-rule-declared-mirror-retired. This line andClause-②: no (narrowing)above were written by the epic seat from the dev's patch-round-1 report (6253831516 on C3 stage 9 (#15204): declared sharing rules read from the registry, bootstrapDeclaredSharingRules retires, and the ceremony deletes the declared-rule mirrors; the verified marker follows (director ordering ruling) #23952), which names this edit.check:doc-authoring. Item 6 asks thedeclared-rule-mirrorspost-check label to name this card by number. The gate refuses a tracker id in a runtime string (maintainer ruling 2026-08-12, and its baseline is maintainer-only). The label names the step and the stage instead: "deleted by the mirror-deletion step (ADR-0131 C3 stage 9)". The card number sits in the adjacent comment. After this change the rows no longer wait for a later card: the ceremony itself removes them.sys_sharing_ruleobject view, so it no longer shows declared rules.GET /api/v1/sharing/ruleslists both kinds. The consumer follow-up is Setup → Sharing Rules lists only stored rules once declared rules leave sys_sharing_rule (objectstack C3 stage 9) objectui#12982, dispatched after this PR merges.DefineSharingRuleInput.managedBy. The spec contract keeps thepackage/platforminput. The seeder was its only producer, so it now has none.defineRulekeeps honouring it, so nothing narrows.#14491 is not addressed here: its parse axis is unchanged (A6).Generated by Claude Code