You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(spec): ShareRecipientType gains field — the record-relative sharing recipient (#14103) (#15084)
* feat(spec): ShareRecipientType gains field — the record-relative sharing recipient
Maintainer ruling 2026-09-02 (B): a criteria sharing rule may name a user-typed
field on the matched record as its recipient; no manager member. Spec half only:
enum member + describes, field-scoped value refinement, stored-row union mirror,
pins, lint partition row, docs, changeset. Executor is the services half.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
* chore(spec): regenerate sharing reference docs; keep the manager refusal pin independent of the accept set
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
* docs(permissions): enforcement-status sentence tells the truth between the field recipient and its executor
The permissions matrix said every authorable recipient type is enforced; between
the field recipient landing and its per-record executor that is false. Say what
is true: it parses, and the declared-rule bootstrap skips it with a logged
warning until the executor lands — never silently, never wider.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(spec): `ShareRecipientType` gains `field` — a sharing rule can share each matched record with the user or users a field on that record names (#14103)
6
+
7
+
Maintainer ruling 2026-09-02 (B): a criteria sharing rule may now be authored as
8
+
`sharedWith: { type: 'field', value: 'assignees' }`, where `value` is the snake_case
9
+
name of a user-typed field on the shared object. Each record the rule's `condition`
10
+
matches is shared with the user or users that column holds on that record; a field
11
+
with `multiple: true` shares with every user it names; an empty column shares with
12
+
nobody (fail-closed). Unlike every other recipient, which resolves once per rule,
13
+
a `field` recipient expands once per matched record, and its grants re-materialise
14
+
when the record's own write changes that column.
15
+
16
+
There is deliberately **no `manager` member**. "Share with the owner's manager" is
17
+
authored as a user field the application stores on the record (a snapshot or kept
18
+
in sync — the application's explicit choice) plus a `field` recipient naming it. A
19
+
`manager` member would walk `sys_user.manager_id` from the record and re-introduce
20
+
the graph-change re-materialisation obligation that once removed the `owner`
21
+
recipient type; with `field` the recipient stays visible on the record.
22
+
23
+
What this release ships is the **contract**: the enum member, the `sharedWith`
24
+
describe text, a `field`-scoped refinement on `value` (an empty name or a dotted
25
+
path such as `owner.manager_id` is refused at parse — a field name, never a graph
26
+
walk), the stored-row mirror `SharingRuleRecipientType` in `contracts/sharing-service.ts`
27
+
widened in step, and the generated JSON schema / authorable surface / reference
28
+
docs. The per-record executor (`plugin-sharing``expandRecipient`, the
29
+
`sys_sharing_rule.recipient_type` select, re-materialisation on the record's own
30
+
update) is the services half, #15072; until it lands the declared-rule bootstrap
31
+
skips a `field` rule with a logged warning rather than seeding it.
32
+
33
+
Accept-set widening only: every sharing rule that parsed before parses
34
+
identically — the refinement is scoped to `type: 'field'`, and the other members'
Copy file name to clipboardExpand all lines: content/docs/permissions/permissions-matrix.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -152,7 +152,7 @@ Sharing rules extend access beyond ownership and the depth axis. The declarative
152
152
|**Criteria-Based**|`criteria`| Share records matching a CEL predicate over field values | All opportunities where `record.amount > 100000` are shared with "VP Sales" |
153
153
154
154
<Callouttype="warn">
155
-
**Enforcement status:** every authorable rule and recipient type is enforced. v17 reconciled the surface with the runtime (#1878): `owner`-type rules (`type: 'owner'`, `ownedBy`) and `group` / `guest` recipients — previously declared but skipped at seed time — **no longer parse**; `group` became the enforced `team` and `business_unit` joined the enum. That is a stronger statement than "declared but not enforced": a skipped rule is still authorable and is ignored, whereas a removed one is rejected by `SharingRuleSchema`, so a stale definition fails loudly at authoring time instead of silently doing nothing (ADR-0078). See [Sharing Rules](/docs/permissions/sharing-rules#recipient-types).
155
+
**Enforcement status:** every authorable rule type is enforced, and so is every recipient type but the newest: the `field` recipient (#14103) parses, and until its per-record executor (#15072) lands the declared-rule bootstrap skips such a rule with a logged warning — never silently, never as a wider grant. v17 reconciled the surface with the runtime (#1878): `owner`-type rules (`type: 'owner'`, `ownedBy`) and `group` / `guest` recipients — previously declared but skipped at seed time — **no longer parse**; `group` became the enforced `team` and `business_unit` joined the enum. That is a stronger statement than "declared but not enforced": a skipped rule is still authorable and is ignored, whereas a removed one is rejected by `SharingRuleSchema`, so a stale definition fails loudly at authoring time instead of silently doing nothing (ADR-0078). See [Sharing Rules](/docs/permissions/sharing-rules#recipient-types).
|`field`|Theuserorusersnamedbya**user-typedfieldoneachmatchedrecord** — `value`isthatfield's name (`assignees`). A `multiple: true` field shares with every user it holds; an empty column shares with nobody. There is deliberately no `manager` recipient: "the owner'smanager" is a user field the application stores on the record, named here (maintainer ruling 2026-09-02, objectstack#14103). The per-record executor is objectstack#15072 — until it lands, a `field` rule is skipped with a logged warning at seed, never seeded wider |
Copy file name to clipboardExpand all lines: content/docs/protocol/objectql/security.mdx
+4-2Lines changed: 4 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -377,12 +377,14 @@ object: account
377
377
accessLevel: read # read | edit
378
378
condition: 'record.account_type == "Enterprise"'
379
379
sharedWith:
380
-
type: position # user | team | position | unit_and_subordinates | business_unit
380
+
type: position # user | team | position | unit_and_subordinates | business_unit | field
381
381
value: sales_rep
382
382
```
383
383
384
384
`unit_and_subordinates` expands a **business-unit subtree**: the unit named by `value` plus every descendant unit's members (ADR-0057 D5 / ADR-0090 D3 — the former position-tree walk was re-homed onto the `sys_business_unit` tree).
385
385
386
+
`field`is the **record-relative** recipient (#14103, maintainer ruling 2026-09-02): `value` names a user-typed field on the object, and each matched record is shared with the user or users that column holds on it (`multiple: true` shares with every user it names; an empty column shares with nobody). It expands once per matched record, not once per rule. There is no `manager` recipient — "the owner's manager" is a user field the application stores on the record, named by a `field` recipient. The per-record executor is #15072.
387
+
386
388
### Owner-Based Sharing — removed in v17
387
389
388
390
Owner-based rules (`type: 'owner'`, `ownedBy`) were removed from the authoring
@@ -417,7 +419,7 @@ sharedWith:
417
419
value: west_region_managers
418
420
```
419
421
420
-
> **Enforcement status.** Every authorable rule and recipient type is enforced. Criteria rules with `user` / `team` / `position` / `unit_and_subordinates` / `business_unit` recipients compile and enforce (the CEL condition lowers to a runtime filter that materializes `sys_record_share` grants, ADR-0058 D3). Owner-type rules and the `group` / `guest` recipients are **not** `[experimental — not enforced]` and are no longer skipped at seed time — v17 removed them from the schema, so they do not parse at all (see above). What is still skipped-and-logged is a `condition` the compiler cannot lower (functions, cross-object traversal): it is never seeded as a permissive match-all (ADR-0049).
422
+
> **Enforcement status.** Criteria rules with `user` / `team` / `position` / `unit_and_subordinates` / `business_unit` recipients compile and enforce (the CEL condition lowers to a runtime filter that materializes `sys_record_share` grants, ADR-0058 D3). The `field` recipient is the contract half of a two-part landing (#14103): it parses, and until its per-record executor (#15072) lands the declared-rule bootstrap skips such a rule with a logged warning — never silently, never as a wider grant. Owner-type rules and the `group` / `guest` recipients are **not** `[experimental — not enforced]` and are no longer skipped at seed time — v17 removed them from the schema, so they do not parse at all (see above). What is still skipped-and-logged is a `condition` the compiler cannot lower (functions, cross-object traversal): it is never seeded as a permissive match-all (ADR-0049).
421
423
422
424
> `accessLevel` is one of `read` or `edit`. Sharing widens **which rows** a principal reaches, never **which verbs** they may use — an `edit` share opens *update*, not *delete*: delete comes from ownership, the ADR-0057 DEPTH scopes, or the `modifyAllRecords` bypass, enforced by the sharing layer's own `canDelete` gate (distinct from the `canEdit` update gate) on top of the object-level CRUD gate (ADR-0111 D3). A third level `full` ("Full Access — transfer/share/delete") was authorable through protocol 16 but never granted any of those verbs: both enforcement sites matched `edit`/`full` alike, so it was equivalent to `edit` while telling admins otherwise, and it was removed (#3865, ADR-0078). Stacks still authoring it are rewritten to `edit` at load by the `sharing-rule-access-level-full-to-edit` conversion.
|**sharedWith**|`{ type: Enum<'user' \| 'team' \| 'position' \| 'unit_and_subordinates' \| 'business_unit'>; value: string }`| ✅ | The recipient of the shared access |
36
+
|**sharedWith**|`{ type: Enum<'user' \| 'team' \| 'position' \| 'unit_and_subordinates' \| 'business_unit' \| 'field'>; value: string }`| ✅ | The recipient of the shared access: a principal resolved once per rule, or — `type: field` — the user or users named by a field on each matched record|
|**value**|`string`| ✅ |The recipient principal: the id or code of the user / team / position / business unit — or, for `type: 'field'`, the snake_case name of a user-typed field on the record whose value names the user or users to share each matched record with|
53
53
54
54
55
55
---
@@ -75,6 +75,7 @@ const result = CriteriaSharingRuleSchema.parse(data);
75
75
*`position`
76
76
*`unit_and_subordinates`
77
77
*`business_unit`
78
+
*`field`
78
79
79
80
80
81
---
@@ -101,7 +102,7 @@ const result = CriteriaSharingRuleSchema.parse(data);
|**sharedWith**|`{ type: Enum<'user' \| 'team' \| 'position' \| 'unit_and_subordinates' \| 'business_unit'>; value: string }`| ✅ | The recipient of the shared access |
105
+
|**sharedWith**|`{ type: Enum<'user' \| 'team' \| 'position' \| 'unit_and_subordinates' \| 'business_unit' \| 'field'>; value: string }`| ✅ | The recipient of the shared access: a principal resolved once per rule, or — `type: field` — the user or users named by a field on each matched record|
|**value**|`string`| ✅ |The recipient principal: the id or code of the user / team / position / business unit — or, for `type: 'field'`, the snake_case name of a user-typed field on the record whose value names the user or users to share each matched record with|
* | `user` | — | A literal user id, no expansion at all. No tree to resolve, so no org to resolve it in. |
131
131
* | `team` | — | `sys_team` is a FLAT collaboration grouping (ADR-0090 D3 renamed `group` → `team`); `TeamGraphService`, not the BU graph. |
132
132
* | `position` | — | Flat holder expansion (ADR-0090 D3 finalized the retirement of the position hierarchy); `PositionGraphService`, not the BU graph. The BU *depth scopes* D6 ② also names are a SCOPE mechanism, not a sharing-rule recipient. |
133
+
* | `field` | — | RECORD-RELATIVE (#14103, maintainer ruling 2026-09-02): the user or users a user-typed column on the matched record names — read off the row itself, no tree walked, so no organization needed to resolve one in. Per-record expansion is `plugin-sharing`'s (#15072). |
133
134
*
134
-
* The three allowed recipients are the sanctioned way to share a
135
-
* platform-global object (ADR-0066): naming a user, a flat team, or a flat
136
-
* position audience grants those people the catalog, which is the entire point
137
-
* of `tenancy.enabled: false`. What ② forbids is not "sharing a global object"
135
+
* The four allowed recipients are the sanctioned way to share a
136
+
* platform-global object (ADR-0066): naming a user, a flat team, a flat
137
+
* position audience, or the users a column on the record itself names grants
138
+
* those people the catalog, which is the entire point of
139
+
* `tenancy.enabled: false`. What ② forbids is not "sharing a global object"
138
140
* but "resolving a BU SUBTREE with no organization to resolve it within".
0 commit comments