Skip to content

Commit 1e8b628

Browse files
fix(spec,metadata-protocol)!: the /meta door no longer authors sharing rules; Setup lists declared sharing rules (#24599)
Closes #24550 Clause-②: yes (narrowing) The console half is objectstack-ai/objectui#13050 (Setup's declared-rules page). This PR is the server half: it closes the third authoring door and adds the Setup nav entry that page needs. ## What changed **1. The `/meta` door no longer authors sharing rules (BREAKING narrowing, launch-window `minor`).** `sharing_rule` had no metadata-type registry entry. Both `/meta` doors read that absence as "runtime-creatable": `GET /meta/types` advertised `allowRuntimeCreate: true`, and `PUT /meta/sharing_rule/:name` stored an environment-wide item that `GET /sharing/rules` then served and enforced. That item bypassed the same-name guard and the organization wall. - **Mechanism, measured.** The position and permission-set seal (`refusePackagedBaseOverride`, `allowEnvironmentOverlay: false`) refuses writes over a *packaged* name only, and both types keep `allowRuntimeCreate: true`. So it cannot refuse a new name. The security-catalog namespace rule judges package registration, not a `/meta` save. The mechanism that fits is the code-only pair `capability` uses (#5961): `sharing_rule` joins `MetadataTypeSchema` and `DEFAULT_METADATA_TYPE_REGISTRY` with `allowRuntimeCreate: false` and `allowEnvironmentOverlay: false`. Its `SharingRuleSchema` binding moves into the kind map. The listing and the save door now read one declaration. - **The refusals,** on every kernel, in publish and `?mode=draft`: - a new name gets `403 NOT_CREATABLE`; - a declared rule's name gets `403 NOT_OVERRIDABLE`. - **What they say.** `sharing_rule` gains an ADR-0126 Regime C row in `PACKAGED_BASE_REGIME`: the clone is `POST /api/v1/sharing/rules`, and the switch is `POST /api/v1/security/_activation/sharing_rule/:name`. Both refusals name the two sanctioned paths. Neither prescribes the `OS_METADATA_WRITABLE` hatch, because for a Regime C type the hatch would re-open the source the regime closes. Both stay under the REST door's 500-character bound (`truncateClientMessage`). The first cut did not: the showcase boot showed the switch route cut off, and it was fixed. - **ADR-0087:** semantic entry `sharing-rule-runtime-create-withdrawn`. The changeset carries FROM → TO and the one-line fix: "author an organization rule, or clone the declared one". **2. The Setup nav entry.** plugin-sharing adds `nav_declared_sharing_rules`, a `type: 'url'` entry like `nav_permission_sets`. It points at `/apps/setup/metadata/sharing_rule?scope=environment` and sits beside `nav_sharing_rules`, with the same `manage_platform_settings` requirement. Its labels are in `platform-objects`' Setup translations (`en`, `zh-CN`, `ja-JP`, `es-ES`), with source hashes recorded, the same way the plugin's other entries are labelled. **Derived artifacts:** `meta-url-data.generated.ts`, the reference docs, the metadata-form i18n bundles (translated by hand in the three locales), the engine-double ledger, and the overlay-table count in `metadata-lifecycle.mdx` (with a `sharing_rule` row). ## Item 3: measured, not built - **The ledger's reading on `/meta/sharing_rule` items.** The metadata protocol reads `sys_metadata_activation` for no type. `/meta/position`, `/meta/permission`, `/meta/flow` and `/meta/action` carry no ledger state either. Adding it for one type is a new cross-layer seam, so it does not meet "removes a client read without a new surface". Not built. Measured on the showcase: after the switch-off, the `/meta` item still reads `active: true`. - **`GET /sharing/rules/:name` for a switched-off declared rule.** It answers `404 RULE_NOT_FOUND` (measured on the showcase). `getRule` falls through to `declaredRules()`, which drops ledger-off rules. That filter is right for enforcement, because `listRules` feeds every enforcement path. Reported only, with a recommendation in the report. ## Verification (head `47f50805`; the gate battery ran at `03ec6356`, and only the changeset text and one CLI test changed after it — their families were re-run at `47f50805`) - **HTTP-door pin.** `packages/runtime/src/meta-sharing-rule-door.test.ts` drives the real `HttpDispatcher`, protocol and repository on both kernel shapes. It covers a new name (publish and draft): 403 `NOT_CREATABLE`, no row, nothing registered, both routes named, under 500 characters. It covers a declared name: 403 `NOT_OVERRIDABLE`, no row. It covers `/meta/types`: `allowRuntimeCreate: false`. The control is a new position, which still saves (200). Result: 10/10. - **Ablation.** The registry entry was deleted through `ablation-replace.mjs`, spec was rebuilt, and `ablation-dist-preflight --absent` confirmed the marker was gone from 232 built files. Then **6 red / 4 green**: the create cases answered `200 "Saved sharing_rule 'probe_meta_rule' (env-wide, state=active)"`, and the listing returned `allowRuntimeCreate: true`. That is the reported defect, reproduced. The declared-name case stayed green under ablation, because the package door already sealed declared names before this change. The restore was confirmed by a blob hash match and a clean tree, then a rebuild with the marker present in 16 files: 10/10 green. - **Showcase boot** (`pnpm dev -- --fresh`): - The door refuses with the two messages above. - `/meta/types` lists `sharing_rule` with `allowRuntimeCreate: false`. - A new position saves (200). - `GET /meta/app/setup` serves `nav_declared_sharing_rules` in `group_access_control`, between `nav_sharing_rules` and `nav_record_shares`, and its `zh-CN` label is served. - Both sanctioned paths still work: `POST /sharing/rules` answers 201, and its same-name guard answers 409. - The rendered console was NOT MEASURED: this container has no console build. objectstack-ai/objectui#13050 renders the page. - **Tests:** - spec: full suite, then the re-judged files, 216/216. - metadata-protocol: full suite, 20725 passed. - platform-objects: full suite, then the two re-judged files, 73/73. - plugin-sharing: full suite. - objectql: `protocol-meta*`, 100/100. - rest: `meta-unknown-type-read-refusal`, 21/21. - runtime: the new pin plus `meta-managed-content-seal`. - CLI: the unit-tier registry-parity tests, 40/40. The integration-tier `metadata-type-schema-gate` (8/8, after classifying `sharing_rule` as gated at `sharingRules:`) and `organization-ownership-apply` were run locally because they enumerate the registry. - metadata, rest, runtime, lint and client: the 16 files that read the registry, 364/364. - **Typecheck:** spec, metadata-protocol, plugin-sharing, platform-objects, runtime and objectql, all clean. - **Gates:** every family in `dispatch-gates.mjs --commands`, plus the 34 artifact-roster rows, with exit codes captured before any pipe: 158 of 164 exit 0. The other six are NOT MEASURED, not findings: four PR-context guards that read the workflow event (`check-closing-target-claim`, `check-partof-closing-keyword`, `check-single-claim-paths`, `check-governed-queue-guard`), and two refusals on this shallow clone (`check-engine-split-ratio`, `check-plugin-teardown-shape --self-test`). On the first battery, three real reds were fixed: `check:engine-double-contract` (ledger `--write`), `check:i18n` (bundles regenerated, then translated), and `check:overlay-whitelist-table` (registry count 28 → 29, plus a row). ## Acceptance notes - **The hatch.** `OS_METADATA_WRITABLE=sharing_rule` still opens a create, as it does for every code-only type. It is not prescribed in any refusal. It is also the repair path for an item stored through the old door on an environment kernel, where `DELETE /meta/sharing_rule/:name` otherwise answers `403 NOT_CREATABLE`. A host-config kernel deletes such an item without it (measured). - **Items already stored through the old door** stay served until deleted, so a database created by 18.0 keeps its data (R2). Nothing rewrites them. - **File surface.** The claim named `plugin-sharing/**` and "the metadata door's seal". The seal lives in `packages/spec` (the registry) and `packages/metadata-protocol` (the refusal text and the regime row). The nav labels live in `packages/platform-objects`. The derived ledgers and docs follow from those. Tests re-judged: spec, metadata-protocol, objectql and platform-objects pins that asserted the open door or a registry count. Nothing in `plugin-webhooks` changed, and #24529 touches none of these files. --- _Generated by [Claude Code](https://claude.ai/code/session_013BWJEDZYcJKmZYj6ApCCa7)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9a7838e commit 1e8b628

38 files changed

Lines changed: 677 additions & 99 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/metadata-protocol': minor
4+
'@objectstack/plugin-sharing': minor
5+
'@objectstack/platform-objects': patch
6+
---
7+
8+
feat(spec,metadata-protocol)!: the `/meta` door no longer authors sharing rules — `sharing_rule` is a code-only metadata type; Setup lists declared sharing rules
9+
10+
Clause-②: yes (narrowing)
11+
12+
<!-- adr-0087: registered sharing-rule-runtime-create-withdrawn -->
13+
14+
**BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes (ADR-0131 D6, regime C).
15+
16+
A sharing rule has two sources: a package declares it (the stack's `sharingRules`, switched off through the activation ledger), or an organization authors its own (`sys_sharing_rule`). A runtime metadata item was a third, and it bypassed the same-name guard and the organization wall. The narrowings, FROM → TO:
17+
18+
- `PUT /api/v1/meta/sharing_rule/:name`, with a name nothing declares, in publish or `?mode=draft` — FROM `200`, with an environment-wide item stored and served by `GET /api/v1/sharing/rules`, TO `403 NOT_CREATABLE`, nothing stored. With a declared rule's name — TO `403 NOT_OVERRIDABLE`.
19+
- `GET /api/v1/meta/types` — FROM `sharing_rule` advertised `allowRuntimeCreate: true`, TO `allowRuntimeCreate: false` and `allowEnvironmentOverlay: false`.
20+
21+
**The fix:** author an organization rule, or clone the declared one. `POST /api/v1/sharing/rules` (Setup > Sharing Rules) creates an organization rule. To customize a declared rule, copy it to an organization rule under a new name, then switch the declared one off with `POST /api/v1/security/_activation/sharing_rule/:name`, body `{ "enabled": false }`. A package declares its rules in the stack's `sharingRules`, as before. An item stored through the old door is still served until you remove it with `DELETE /api/v1/meta/sharing_rule/:name`. On an environment kernel, removing it needs `OS_METADATA_WRITABLE=sharing_rule`. That hatch is an explicit operator opt-in, not an authoring route: as for any code-only type, it reopens every write on the type, including creates of new names, and a sharing rule grants access. Unset it after the removal.
22+
23+
Also in this change:
24+
25+
- **`@objectstack/spec`.** `MetadataTypeSchema` (so the `MetadataType` union) and `DEFAULT_METADATA_TYPE_REGISTRY` gain `sharing_rule`. Its schema binding (`SharingRuleSchema`) moves from the non-kind collections into the kind map. `META_URL_TO_SINGULAR` gains the `sharing_rules` spelling, so `/api/v1/meta/sharing_rules` now folds to `sharing_rule`; before, that plural was not a recognised spelling.
26+
- **`@objectstack/metadata-protocol`.** The refusal names the two sanctioned paths, not a source glob. `sharing_rule` gains its ADR-0126 Regime C row (clone, and the ledger switch).
27+
- **`@objectstack/plugin-sharing`.** Setup's Access Control group gains **Declared Sharing Rules** (`nav_declared_sharing_rules`). It opens `/apps/setup/metadata/sharing_rule?scope=environment` beside **Sharing Rules**, which lists the organization's own rules.
28+
- **`@objectstack/platform-objects`.** That entry's label, in `en`, `zh-CN`, `ja-JP` and `es-ES`.

‎content/docs/concepts/metadata-lifecycle.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,9 @@ later ships. See [ADR-0070](https://github.com/objectstack-ai/objectstack/blob/m
118118
| `field` | ❌ | **Also `allowRuntimeCreate: false` since protocol 17** (#7893). A field is not a standalone artifact — fields are authored inside their object (`ObjectSchema.fields`), so a `field` write minted a *separate* `sys_metadata` row keyed `('field','<object>.<name>')` that nothing ever composed into the parent: `PUT /meta/field/showcase_task.zz_probe` answered 200 `state=active`, the row read back `_diagnostics.valid: true`, and `GET /meta/object/showcase_task` never listed the field. The door is closed rather than bridged: add a field by writing its **object** (`PUT /meta/object/:name`, or `**/*.object.ts` and redeploy), which both persists and composes. Existing rows are untouched — they were inert before the change too — and remain deletable. |
119119
| `datasource` | ❌ | Connection strings; multi-tenant isolation is enforced at a higher layer. (`allowRuntimeCreate: true` — the datasource wizard persists `origin: 'runtime'` rows.) |
120120
| `job` | ❌ | **Also `allowRuntimeCreate: false` since protocol 17** (#4509). `JobSchema.handler` names a function in the compiled bundle's function table, which a runtime writer has no way to reach — so a job created in Studio or through `PUT /meta` parsed, saved, reported success and was never scheduled. The door is closed rather than bridged: `job` stays first-class through `*.job.ts` / `defineStack({ jobs, functions })`, where every schedule shape, `retryPolicy` and `timeout` does reach the scheduler. Existing rows are untouched — they were never scheduled — and `migrateStoredMetadata` reports them `skipped`. |
121+
| `sharing_rule` | ❌ | **Also `allowRuntimeCreate: false` since protocol 18** (ADR-0131 D6, regime C). A sharing rule has two sources: a package declares it (the stack's `sharingRules`, switched off through the activation ledger), or an organization authors its own through `POST /api/v1/sharing/rules`, behind the same-name guard and the organization wall. A runtime `sys_metadata` item was a third source that bypassed both, so `PUT /meta/sharing_rule/:name` now answers `403 NOT_CREATABLE` (`NOT_OVERRIDABLE` over a declared rule) and names the two sanctioned paths: author an organization rule, or clone the declared one and switch it off. Items stored before the change are still served until deleted. |
121122

122-
Those five are the **complete** `allowEnvironmentOverlay: true` set: of the 28 types in `DEFAULT_METADATA_TYPE_REGISTRY`, every other one is `false`. The ❌ rows above are the `false` types whose *second* tier (`allowRuntimeCreate`) is worth calling out; any type not listed is `allowEnvironmentOverlay: false`.
123+
Those five are the **complete** `allowEnvironmentOverlay: true` set: of the 29 types in `DEFAULT_METADATA_TYPE_REGISTRY`, every other one is `false`. The ❌ rows above are the `false` types whose *second* tier (`allowRuntimeCreate`) is worth calling out; any type not listed is `allowEnvironmentOverlay: false`.
123124

124125
There is no `workflow` metadata type (per [ADR-0020](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0020-state-machine-converge-and-enforce.md), record state machines are a `state_machine` validation). Nor is there a standalone `validation` type any more — it was retired in protocol 17 under [ADR-0088](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0088-metadata-kind-admission-and-retirement.md) because `ValidationRuleSchema` carries no object-binding key, so a rule authored through that door could never say what it protected; author rules in the object's own `validations[]` instead. The runtime gate is implemented in `OVERLAY_ALLOWED_TYPES` (derived from the registry) and enforced by `SysMetadataRepository.put()`.
125126

‎content/docs/references/api/metadata.mdx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,7 @@ Metadata query with filtering, sorting, and pagination
648648

649649
| Property | Type | Required | Description |
650650
| :--- | :--- | :--- | :--- |
651-
| **types** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'picklist' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'api' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'capability' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
651+
| **types** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'picklist' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'api' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'capability' \| 'sharing_rule' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
652652
| **namespaces** | `string[]` | optional | Filter by namespaces |
653653
| **packageId** | `string` | optional | Filter by owning package |
654654
| **search** | `string` | optional | Full-text search query |
@@ -715,7 +715,7 @@ Metadata query with filtering, sorting, and pagination
715715

716716
| Property | Type | Required | Description |
717717
| :--- | :--- | :--- | :--- |
718-
| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'picklist' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| … +13 more>` | ✅ | Metadata type |
718+
| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'picklist' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| … +14 more>` | ✅ | Metadata type |
719719
| **name** | `string` | ✅ | Item name (snake_case) |
720720
| **data** | `Record<string, any>` | ✅ | Metadata payload |
721721
| **namespace** | `string` | optional | Optional namespace |
@@ -747,6 +747,7 @@ Metadata query with filtering, sorting, and pagination
747747
* `permission`
748748
* `position`
749749
* `capability`
750+
* `sharing_rule`
750751
* `agent`
751752
* `tool`
752753
* `skill`

‎content/docs/references/kernel/metadata-plugin.mdx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ const result = MetadataBulkResultSchema.parse(data);
179179

180180
| Property | Type | Required | Description |
181181
| :--- | :--- | :--- | :--- |
182-
| **types** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'picklist' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'api' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'capability' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
182+
| **types** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'picklist' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'api' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'capability' \| 'sharing_rule' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
183183
| **namespaces** | `string[]` | optional | Filter by namespaces |
184184
| **packageId** | `string` | optional | Filter by owning package |
185185
| **search** | `string` | optional | Full-text search query |
@@ -250,6 +250,7 @@ const result = MetadataBulkResultSchema.parse(data);
250250
* `permission`
251251
* `position`
252252
* `capability`
253+
* `sharing_rule`
253254
* `agent`
254255
* `tool`
255256
* `skill`
@@ -263,7 +264,7 @@ const result = MetadataBulkResultSchema.parse(data);
263264

264265
| Property | Type | Required | Description |
265266
| :--- | :--- | :--- | :--- |
266-
| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'picklist' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| … +13 more>` | ✅ | Metadata type identifier |
267+
| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'picklist' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| … +14 more>` | ✅ | Metadata type identifier |
267268
| **label** | `string` | ✅ | Display label for the metadata type |
268269
| **description** | `string` | optional | Description of the metadata type |
269270
| **filePatterns** | `string[]` | ✅ | Glob patterns to discover files of this type |
@@ -304,6 +305,7 @@ const result = MetadataBulkResultSchema.parse(data);
304305
* `permission`
305306
* `position`
306307
* `capability`
308+
* `sharing_rule`
307309
* `agent`
308310
* `tool`
309311
* `skill`

‎examples/app-showcase/src/coverage.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,13 @@ export const KIND_COVERAGE: Record<MetadataType, KindCoverage> = {
167167
notes:
168168
'Package-declared authorization capabilities (`capabilities: allCapabilities` in objectstack.config.ts), served from the security catalog under the package (ADR-0131 D3). Code-only kind: runtime creation answers 403 not_creatable, which protocol.capability-write-door.test.ts pins.',
169169
},
170+
// [ADR-0131 D6] `sharing_rule` joined the registry as a code-only kind (regime C).
171+
sharing_rule: {
172+
status: 'demonstrated',
173+
files: ['src/security/sharing-rules.ts'],
174+
notes:
175+
'Regime C (ADR-0131 D6): package-declared rules (`sharingRules` in objectstack.config.ts), served from the registry and switched off through the activation ledger (POST /api/v1/security/_activation/sharing_rule/:name). Organizations author their own through POST /api/v1/sharing/rules. Code-only kind: PUT /meta/sharing_rule/:name answers 403 NOT_CREATABLE (NOT_OVERRIDABLE over a declared rule), which runtime meta-sharing-rule-door.test.ts pins.',
176+
},
170177

171178
// ── ai ──
172179
agent: {

‎packages/cli/test/metadata-type-schema-gate.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ const GATED_AT: Readonly<Record<string, string>> = {
9595
// shape (`stack.zod.ts`: `z.array(CapabilityDeclarationSchema)`), and that
9696
// shape is `.strict()`, so both gates reject an undeclared key identically.
9797
capability: 'capabilities',
98+
// [ADR-0131 D6] `sharing_rule` joined the registry as a code-only, CLOSED
99+
// shape: the stack authors it at `sharingRules:` as a flat array of the
100+
// registry's own `.strict()` `SharingRuleSchema`, so it is gated from the start.
101+
sharing_rule: 'sharingRules',
98102
agent: 'agents',
99103
tool: 'tools',
100104
skill: 'skills',

‎packages/metadata-protocol/src/packaged-base-regime.ts‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,13 @@
5555
* to `POST /data/sys_permission_set` under a new name. Spelled as
5656
* `plugin-security`'s own lock refusal spells that same path
5757
* (`packaged-permission-set-lock.ts`), so one condition reads one way at
58-
* every door.
58+
* every door;
59+
* - `sharing_rule` — both (ADR-0131 D6 charts it under regime C): the clone is
60+
* an organization rule under a new name, `POST /sharing/rules`, and the
61+
* switch is the activation ledger's row, `POST /security/_activation/sharing_rule/:name`,
62+
* body `{ enabled }`.
5963
*
60-
* Both switch routes are gated by the ONE §5 write-authority gate
64+
* The switch routes are gated by the ONE §5 write-authority gate
6165
* (`packages/runtime/src/domains/activation-gate.ts`), so both carry the same
6266
* operator-only clause. Still absent: ADR-0126 §3's pre-charted `tool` /
6367
* `skill` / `position` — no sanctioned path is built for them, and a row with no
@@ -198,6 +202,14 @@ export const PACKAGED_BASE_REGIME: Readonly<Record<string, PackagedBaseRegimeRow
198202
+ 'or POST /api/v1/security/permission-sets/:name/clone with a new name',
199203
},
200204
},
205+
sharing_rule: {
206+
regime: 'C',
207+
routes: {
208+
clone: 'POST /api/v1/sharing/rules',
209+
switchOff: 'POST /api/v1/security/_activation/sharing_rule/:name, body {enabled: false}; '
210+
+ 'operator-only where one install serves several organizations',
211+
},
212+
},
201213
datasource: {
202214
regime: 'origin-gated',
203215
noun: 'Datasource',
@@ -308,8 +320,8 @@ export function isOriginGatedType(type: string): boolean {
308320
* (`truncateClientMessage`, `packages/rest/src/error-response.ts`), past which
309321
* the tail is truncated. Characters outside the item's name, save / removal:
310322
* `flow` 395 / 388, `action` 349 / 342, `permission` 301 / 294,
311-
* `datasource` 192 / 193 — so a name of up to 88 characters arrives whole for
312-
* every row (pinned). [#21944] A row's `hostOwned` name is a fixed, short name
323+
* `datasource` 192 / 193, `sharing_rule` 390 / 383 — so a name of up to 88
324+
* characters arrives whole for every row (pinned). [#21944] A row's `hostOwned` name is a fixed, short name
313325
* with its own remedy (`default`: under 300 characters whole).
314326
*/
315327
export function packagedBaseRegimeSentence(

‎packages/metadata-protocol/src/protocol.code-only-types.test.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,23 @@ const PROBES: Record<string, { name: string; item: Record<string, unknown> }> =
169169
options: [{ label: 'One', value: 'one' }],
170170
},
171171
},
172+
// [ADR-0131 D6] `sharing_rule`, regime C: a sharing rule is a package's
173+
// declaration or an organization's `sys_sharing_rule` row, and a runtime
174+
// `sys_metadata` item was a third source past the same-name guard and the
175+
// organization wall. Schema-valid for the same reason as the rest.
176+
sharing_rule: {
177+
name: 'rc3_sharing_rule_probe',
178+
item: {
179+
type: 'criteria',
180+
name: 'rc3_sharing_rule_probe',
181+
label: 'Probe',
182+
object: 'task',
183+
condition: "record.status == 'open'",
184+
accessLevel: 'read',
185+
sharedWith: { type: 'position', value: 'probe_position' },
186+
active: true,
187+
},
188+
},
172189
};
173190

174191
function makeStubEngine(artifacts: Array<{ type: string; name: string }> = []) {
@@ -285,7 +302,7 @@ describe('code-only metadata types are refused on every kernel (#5086)', () => {
285302
// auto-enrolment is the point of deriving the set instead of listing it
286303
// (Prime Directive #8).
287304
expect(CODE_ONLY_TYPES.length).toBeGreaterThan(0);
288-
expect([...CODE_ONLY_TYPES].sort()).toEqual(['agent', 'api', 'capability', 'field', 'job', 'picklist']);
305+
expect([...CODE_ONLY_TYPES].sort()).toEqual(['agent', 'api', 'capability', 'field', 'job', 'picklist', 'sharing_rule']);
289306
for (const type of CODE_ONLY_TYPES) {
290307
expect(PROBES[type], `no probe payload for code-only type '${type}'`).toBeDefined();
291308
}

‎packages/metadata-protocol/src/protocol.meta-types-mint-door-agreement.test.ts‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
* 1. **statically declared** (`view`, `hook`, `agent`) — the flag comes off
3939
* the registry entry, as it always did, in both the `true` and the `false`
4040
* direction;
41-
* 2. **URL-map-only plugin kinds** (`webhook`, and its four siblings) — no
41+
* 2. **URL-map-only plugin kinds** (`webhook`, and its three siblings) — no
4242
* registry entry, IN the static spelling contract, still advertised and
4343
* still mintable. This is the discriminating control: without it the change
4444
* cannot show its narrowing is narrow;
@@ -156,6 +156,20 @@ const SAMPLE: Array<{
156156
creatable: false,
157157
item: { name: 'probe_agent', label: 'Probe' },
158158
},
159+
{
160+
// [ADR-0131 D6] `sharing_rule` left class 2 for class 1 when regime C gave
161+
// it a code-only registry entry: an organization authors its rules through
162+
// the sharing door, so the `/meta` door neither advertises nor honours a
163+
// create. Spec-valid body, so the refusal read is the declaration's.
164+
type: 'sharing_rule',
165+
klass: 'declared',
166+
creatable: false,
167+
item: {
168+
type: 'criteria', name: 'probe_meta_rule', label: 'Probe', object: 'task',
169+
condition: "record.status == 'new'", accessLevel: 'read',
170+
sharedWith: { type: 'position', value: 'probe_position' }, active: true,
171+
},
172+
},
159173
{
160174
type: 'webhook',
161175
klass: 'url-map-only',
@@ -247,7 +261,7 @@ describe('#8421 — the read door and the mint door agree, across all three clas
247261
}
248262
});
249263

250-
it('the five URL-map-only plugin kinds are ALL still advertised as creatable', async () => {
264+
it('the four URL-map-only plugin kinds are ALL still advertised as creatable', async () => {
251265
// The blanket-flip guard, quantified rather than sampled. `webhook`
252266
// above is the one driven end-to-end through a write; the others have
253267
// no hand-written spec-valid body here, so they are pinned on the door
@@ -257,7 +271,7 @@ describe('#8421 — the read door and the mint door agree, across all three clas
257271
const { protocol } = makeProtocol();
258272
const listing = await protocol.getMetaTypes();
259273
for (const kind of [
260-
'analytics_cube', 'connector', 'rag_pipeline', 'sharing_rule', 'webhook',
274+
'analytics_cube', 'connector', 'rag_pipeline', 'webhook',
261275
]) {
262276
const entry = listing.entries.find((e: any) => e.type === kind);
263277
expect(entry, `${kind} must be listed`).toBeDefined();

‎packages/metadata-protocol/src/protocol.recovery-doors-emit-mutation.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ describe('[#14179] reachability: the legacy exit is confined to bootstrap mode',
542542
// Non-empty: the branch is not dead code.
543543
expect(codeOnly.length).toBeGreaterThan(0);
544544
expect(codeOnly.map((e) => e.type).sort())
545-
.toEqual(['agent', 'api', 'capability', 'field', 'job', 'picklist']);
545+
.toEqual(['agent', 'api', 'capability', 'field', 'job', 'picklist', 'sharing_rule']);
546546
// …and the #6960 carve-out reaches none of them, which is what makes
547547
// the 403 below total rather than incidental.
548548
expect(codeOnly.filter((e) => e.supportsOverlay)).toEqual([]);

0 commit comments

Comments
 (0)