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
{{ message }}
Repository navigation
Commit 9a26455
Browse filesBrowse the repository at this point in the historyBrowse files
docs(adr): record the stage-8 retirement of the four security-catalog row objects in the ADRs that name them
ADR-0131 D2/D3/D13, executed by #15204 stage 8 (66949b7, 2026-10-13),
retired sys_position, sys_permission_set, sys_position_permission_set and
sys_capability as row objects and unwired the ADR-0094 projector, its
boot reconcile and the data-door write-throughs. Nine ADRs that name
them gain an amended status-line segment (AGENTS.md rule 13) and a
dated note at each affected section; no decision text is rewritten and
no heading changes.
ADR-0131 (the deciding record) and ADR-0057's data-lifecycle record are
left unchanged: the first already reads as retiring them, the second
names none of them.
Claude-Session: https://claude.ai/code/session_013zGKMG1cRedVzWtqGGGZW9
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/adr/0029-kernel-object-ownership-and-platform-objects-decomposition.md
+7-1Lines changed: 7 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
# ADR-0029: Kernel Object Ownership — First-Party Capabilities as Plugins That Own Their Data, and Decomposing the `platform-objects` Monolith
2
2
3
-
**Status**: Accepted — K0/K1/K2/D7 implemented; K3 (pending ADR-0030/storage) + K4 cleanup remaining (proposed 2026-06-01 · calibrated 2026-06-12) · **Amended** (2026-08-09, [#6853](https://github.com/objectstack-ai/objectstack/issues/6853) — **D9**: a tenant `sys_metadata` overlay of an `object` registers as its own `overlay` contributor layer instead of taking the packaged `own` slot and splicing the packaged definition out. D3's single-owner invariant is unchanged. **Design only — nothing is implemented yet**; see **"Amendment (2026-08-09, #6853)"** at the end for the measurement, the semantics, the blast radius, and what is deliberately left open.)
3
+
**Status**: Accepted — K0/K1/K2/D7 implemented; K3 (pending ADR-0030/storage) + K4 cleanup remaining (proposed 2026-06-01 · calibrated 2026-06-12) · **Amended** (2026-08-09, [#6853](https://github.com/objectstack-ai/objectstack/issues/6853) — **D9**: a tenant `sys_metadata` overlay of an `object` registers as its own `overlay` contributor layer instead of taking the packaged `own` slot and splicing the packaged definition out. D3's single-owner invariant is unchanged. **Design only — nothing is implemented yet**; see **"Amendment (2026-08-09, #6853)"** at the end for the measurement, the semantics, the blast radius, and what is deliberately left open.) · **Amended** (2026-10-14, [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D3/D13 — two security objects in D1's `plugin-sharing` row, `sys_permission_set` and `sys_position_permission_set` (one of the `sys_*_permission_set` tables), are retired as row objects by #15204 stage 8 (`66949b7729`, 2026-10-13); see the note under D1.)
4
4
**Deciders**: ObjectStack Protocol Architects
5
5
**Builds on**: [ADR-0003](./0003-package-as-first-class-citizen.md) (package as first-class citizen), [ADR-0019](./0019-app-as-consumer-unit.md) (app as the consumer-facing unit), [ADR-0025](./0025-plugin-package-distribution.md) (plugin package distribution + dependencies)
6
6
**Related**: [ADR-0028](./0028-metadata-naming-and-namespace-isolation.md) (metadata naming & namespace isolation) **depends on** this ADR — its D5/D6 (reserved `sys` namespace, single-owner-per-object, apps-cannot-define-kernel) assume the kernel is properly owned. This ADR is sequenced **first** and is independently valuable; ADR-0028 owns the naming model, this ADR owns kernel object *ownership*.
@@ -100,6 +100,12 @@ data monolith. Ownership *declaration* (`manifest`) and object *definition*
100
100
(Exact assignment of security objects — under `plugin-sharing` vs a dedicated
101
101
`plugin-rbac` — to settle in implementation.)
102
102
103
+
> **Note (2026-10-14) — two security objects in the `plugin-sharing` row are retired.**
104
+
> `sys_permission_set` and `sys_position_permission_set` are no longer registered since #15204
105
+
> stage 8 (`66949b7729`, 2026-10-13), by execution of [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md)
106
+
> D3/D13: a permission set is a definition in the metadata registry, and a position names its sets
107
+
> in its own definition. `sys_user_permission_set` stays; D1's ownership rule is unchanged.
108
+
103
109
### D2 — Small core; everything else is a capability plugin
104
110
105
111
Split kernel objects into two tiers by a clear criterion:
**Status**: Accepted in part (2026-06-21) — D1–D3/D8 implemented (scope-depth enum + evaluator, `sys_business_unit` tree, hierarchy-resolver seam, conformance rows); **D4–D7 superseded by ADR-0090 D3 / ADR-0095**: the `sys_role`/`sys_user_role`/`role_and_subordinates` vocabulary never shipped — the code realizes them as flat `sys_position`/`sys_user_position` with rollup on the BU tree. This ADR's own implementation-status table predates that supersession; read it with the rename applied.
3
+
**Status**: Accepted in part (2026-06-21) — D1–D3/D8 implemented (scope-depth enum + evaluator, `sys_business_unit` tree, hierarchy-resolver seam, conformance rows); **D4–D7 superseded by ADR-0090 D3 / ADR-0095**: the `sys_role`/`sys_user_role`/`role_and_subordinates` vocabulary never shipped — the code realizes them as flat `sys_position`/`sys_user_position` with rollup on the BU tree. This ADR's own implementation-status table predates that supersession; read it with the rename applied. · **Amended** (2026-10-14, [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D3/D13): the flat `sys_position` row object named above is itself retired by #15204 stage 8 (`66949b7729`, 2026-10-13), and its `sys-position.object.ts`, which the path note below names, is deleted; a position is a metadata-registry definition, and the `sys_user_position` assignment stays.
4
4
**Deciders**: ObjectStack Protocol Architects
5
5
**Path note (2026-09), paths only — no decision changes**: this record cites the **pre-rename**
6
6
vocabulary, and those four paths no longer resolve. `sys-department.object.ts` and `department-graph.ts`
**Status**: Accepted (2026-06-23) — D1–D5 + refinements ⑤/⑨ implemented (capability registry + `defineCapability` seeding, `access.default` posture with posture-gated superuser bypass, `requiredPermissions` AND-gates on object/field/action, `crudBucketForOperation`, `validateCapabilityReferences` lint); D4's UI half enforced in objectui ActionRunner. · **Amended** (2026-10-14, [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D2/D3/D13): the stored records this record names — `sys_permission_set` and `sys_capability` — are retired as row objects by #15204 stage 8 (`66949b7729`, 2026-10-13), and the seeders that filled them are retired under ADR-0131 D2; permission sets and capabilities are metadata-registry definitions, and the `sys_user_permission_set` assignment stays. The three-way separation stands; see the notes under the principle, D1, D5 and ⑨.
4
4
**Deciders**: ObjectStack Protocol Architects
5
5
**Builds on**: [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (`readScope`/`writeScope` depth: own/unit/unit_and_below/org), [ADR-0058](./0058-expression-and-predicate-surface.md) (CEL predicate surface used by RLS); relates to cloud ADR-0016 (authz open/paid boundary)
@@ -19,6 +19,13 @@ Authorization splits into three concerns that must stay decoupled:
19
19
20
20
The design rule that resolves the recurring confusion: **a resource declares the capability it requires (a stable contract); the capability and its assignment are dynamic, admin-maintained records.** A resource never bakes "who" — only "what is required".
21
21
22
+
> **Note (2026-10-14) — of item 2's records, only the assignments remain.**`sys_permission_set`,
23
+
> and `sys_role` / `sys_role_permission_set` under their ADR-0090 D3 names `sys_position` /
24
+
> `sys_position_permission_set`, are retired by #15204 stage 8 (`66949b7729`, 2026-10-13), by
25
+
> execution of [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D3/D13: a permission set, and
26
+
> the sets a position carries, are definitions in the metadata registry; the user assignments
27
+
> `sys_user_permission_set` and `sys_user_position` stay runtime records.
28
+
22
29
## Authorization needs (taxonomy: need × status)
23
30
24
31
| # | Need | Status |
@@ -44,6 +51,13 @@ Promote capabilities from bare strings to **first-class records** (`sys_permissi
44
51
45
52
**Landed (2026-07):** the curated platform set is `@objectstack/spec` `PLATFORM_CAPABILITIES` (`security/capabilities.ts`), seeded `managed_by:'platform'` by `bootstrap-system-capabilities.ts`. "**Packages declare their capabilities**" is now an EXPLICIT API — `defineCapability` + a stack's `capabilities` array (`{ name, label?, description?, scope, packageId? }`) — materialized into `sys_capability` with `managed_by:'package'` + `package_id` provenance by `bootstrap-declared-capabilities.ts` (idempotent, upgrade-aware; refuses to hijack curated/foreign capabilities, never clobbers admin rows, and *claims* a pre-existing derived placeholder). This retires the implicit back-door where a capability existed only as an untitled placeholder derived from a permission set's `systemPermissions[]`; that derivation still runs for back-compat but skips any explicitly-declared name. A capability is **not** a contract and carries no `inputs`: DEFINE (`defineCapability`) / GRANT (`systemPermissions`) / REQUIRE (`requiredPermissions`) stay decoupled. Aligns with ADR-0066 D1 (retire implicit `managed_by`-guessing).
46
53
54
+
> **Note (2026-10-14) — `sys_capability` is retired.** The row object this paragraph materializes
55
+
> declared capabilities into is no longer registered since #15204 stage 8 (`66949b7729`, 2026-10-13), by execution of
56
+
> [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D3/D13, and no seeder writes a
57
+
> declared capability into any table (ADR-0131 D2): `PLATFORM_CAPABILITIES` and a stack's
58
+
> `defineCapability` declarations are served by the metadata registry. DEFINE / GRANT / REQUIRE stay
59
+
> decoupled.
60
+
47
61
### D2 — Secure-by-default object/field posture [new] (data-model posture, NOT a permission)
48
62
Add an object (and field) flag that opts it **out of blanket wildcard grants** — e.g. `access: { default: 'private' }` (vs the implicit `'public'`). A `private` object is **not** covered by `'*': {allowRead:true}`; access requires an **explicit** permission-set grant. Mirrors Salesforce "new object = no access until granted." This is a posture like `tenancy`, declared on the object — it is **not** an assignment and names no principal. `admin_full_access` (the superuser `'*'` grant) still covers private objects unless it too is excluded (rare).
49
63
@@ -62,6 +76,11 @@ An action declaring `requiredPermissions` is enforced in **one place, two surfac
62
76
### D5 — Package-seeded, admin-maintainable policies [existing mechanism, fill the gap]
63
77
A package may seed permission-set policies (incl. per-object grants for **its own** objects) via `stack.permissions`**[existing]**; these land as `sys_permission_set` records **admins can edit in Setup****[existing]**. The gap to close: make a package's per-object secure default (D2 + an admin-only grant) expressible + seedable so a sensitive package object is locked on install and tunable thereafter.
64
78
79
+
> **Note (2026-10-14) — a package's sets no longer land as `sys_permission_set` records.** That row
80
+
> object is retired by #15204 stage 8 (`66949b7729`, 2026-10-13), by execution of
81
+
> [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D3/D13: a package's sets are served from
82
+
> the metadata registry. Of the runtime records the next section keeps, only the assignments remain.
83
+
65
84
## What stays unchanged (existing strengths)
66
85
Permission sets / roles / bindings as runtime records (assignment layer); the full RLS spectrum (ownership, hierarchy depth ADR-0057, tenant isolation, sharing, CEL); `tenancy`; `App.requiredPermissions`; anti-escalation; union grant semantics.
67
86
@@ -112,3 +131,8 @@ Captured for the record; **out of scope for Phases 1–4 above**. Each is anchor
112
131
-**⑦ Permission-set groups + subtractive *muting*.** Pure union does not scale governance ("permission-set explosion"); Salesforce added permission-set-group *muting* precisely to allow taking access away. Roles→permission-sets already bundle; a subtractive/deny layer (precedence step 4) is the missing piece for large-org administration. Pairs with delegated admin (#9).
113
132
-**⑧ FLS posture: unlisted fields are visible, and field grants union without deny***(recorded 2026-07 pre-launch architecture assessment)*. Runtime FLS is block-list-shaped: `field-masker.ts` hides a field only when an explicit rule marks it `readable:false`, so an **undeclared** sensitive field is exposed by default, and `getFieldPermissions` merges most-permissively, so one set's `readable:true` permanently out-votes another's `false`. Consequences: protecting a salary/ID-number field relies on discipline (grant it only where needed), not mechanism. The object-level `private` posture (D2/④) mitigates the unlisted-default-visible half for objects that adopt it; the union half is the field-level face of ⑦ — a muting/deny layer must cover **field** grants, not just object grants, when it lands. Until then, treat "sensitive field on a `public` object" as a smell in review.
114
133
- **⑨ Authoring-time validation for capability references** *(recorded 2026-07 pre-launch architecture assessment)*. `systemPermissions` / `requiredPermissions` are free strings today; a typo (`mange_users`) fails closed at runtime — the safe direction — but is undiscoverable: nothing reports that the referenced capability exists nowhere. When D1 lands the `sys_permission` registry, add the authoring/publish-gate lint alongside it: referencing an unregistered capability warns at author time (consistent with ADR-0049 honesty and the contract-first Prime Directive — reject at the producer, don't tolerate at the consumer). **Landed (2026-07):** `validateCapabilityReferences` in `@objectstack/lint` warns (never errors — cross-package capabilities and the fail-closed runtime make a hard gate wrong) when a `requiredPermissions` reference resolves against neither the built-in set (`@objectstack/spec` `PLATFORM_CAPABILITY_NAMES`), nor a capability the stack grants via a permission set's `systemPermissions`, nor a `sys_capability` seed row. Wired into `os validate` and `os lint`. `systemPermissions` (the declaration side) is deliberately not flagged.
134
+
135
+
> **Note (2026-10-14) — ⑨'s seed-row source is gone.** With `sys_capability` retired by #15204
0 commit comments