Skip to content

Commit c36403b

Browse files
os-zhuangclaude
andcommitted
feat(spec): add explicit engine-owned managedBy bucket (ADR-0103 addendum, #3343)
ADR-0103 deferred the enum split ("revisitable later as a rename") because a new `managedBy` value would fall through to the fully-editable `platform` default on deployed Console clients. Both objections are now retired — the server-side write guard / apiMethods reconciliation / /me/permissions clamp make that fallthrough cosmetic (the write is rejected regardless of what the client renders), and objectui#2712 closed the UI union — so v16 lands it additively. - spec: new enum value `engine-owned` with the same all-locked default affordance row as `system`; joins ENGINE_OWNED_BUCKETS (guard) + GUARDED_WRITE_BUCKETS (clamp). Guard / reconcile / clamp MECHANISMS unchanged — engine-owned is an explicit member of the set they already covered by resolved affordance. - relabel 20 objects `system → engine-owned` (metadata store, jobs, approvals runtime rows, sharing rows, automation runs, messaging delivery/receipt, secrets, settings) — one-line, behaviour-identical each. - 8 admin/user-writable objects keep `managedBy: 'system'` (RBAC link tables, prefs, approval delegation, messaging config) — `system` now means "engine-managed schema, writable via userActions". - tests: engine-owned coverage in system-write-guard / fold-wildcard-superuser / object affordance-matrix; ADR-0103 addendum; objects.mdx + regenerated ref. Behaviour-, enforcement- and wire-identical: a self-documenting relabel, no data migration, no code branches on the `'system'` literal. Retiring the overloaded `system` (moving the 8 writable objects out) is a breaking rename deferred to v17. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 674457a commit c36403b

31 files changed

Lines changed: 194 additions & 89 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/plugin-security": minor
4+
"@objectstack/plugin-hono-server": minor
5+
"@objectstack/metadata-core": patch
6+
"@objectstack/platform-objects": patch
7+
"@objectstack/plugin-approvals": patch
8+
"@objectstack/plugin-sharing": patch
9+
"@objectstack/service-automation": patch
10+
"@objectstack/service-messaging": patch
11+
---
12+
13+
**Split the overloaded `managedBy: 'system'` bucket with an explicit `engine-owned` value (ADR-0103 addendum, #3343).** ADR-0103 deferred the enum split ("revisitable later as a rename") because a new `managedBy` value would fall through to the fully-editable `platform` default on deployed Console clients. Both reasons against it are now retired — the server-side write guard / `apiMethods` reconciliation / `/me/permissions` clamp make that fallthrough cosmetic (the write is rejected regardless of what the client renders), and objectui#2712 closed the UI union — so v16 lands it, **additively**.
14+
15+
- **New enum value `engine-owned`** with the same all-locked default affordance row as `system` (`create/import/edit/delete: false`, `exportCsv: true`). It joins `ENGINE_OWNED_BUCKETS` (the engine write guard) and `GUARDED_WRITE_BUCKETS` (the `/me/permissions` clamp); the guard, `reconcileManagedApiMethods`, and the clamp mechanisms are unchanged — `engine-owned` is an explicit member of the set they already covered by resolved affordance.
16+
- **20 objects relabelled `system → engine-owned`** — the ones the engine owns end to end and that declared no write-opening `userActions` (the metadata store, jobs, approval runtime rows, sharing rows, `sys_automation_run`, the messaging delivery/receipt pipeline, `sys_secret`, settings). One-line, behaviour-identical per object.
17+
- **8 admin/user-writable objects keep `managedBy: 'system'`** (the RBAC link tables, `sys_user_preference`, `sys_approval_delegation`, the messaging config grids) — `system` now reads as "engine-managed schema, writable via `userActions`".
18+
19+
Behaviour-, enforcement- and wire-identical: resolved affordances, the guard verdict, the 405 `apiMethods` reconciliation, and the permissions clamp are the same before and after — this is a self-documenting relabel, not a policy change. No data migration (`managedBy` is schema metadata) and no code branches on the `'system'` literal. Retiring the overloaded `system` entirely (moving the 8 writable objects to a dedicated bucket) is a breaking rename deferred to v17.

content/docs/data-modeling/objects.mdx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ indexes: [
217217
| Property | Type | Description |
218218
| :--- | :--- | :--- |
219219
| `isSystem` | `boolean` | System object, protected from deletion (default: `false`) |
220-
| `managedBy` | `enum` | Lifecycle bucket that sets the default CRUD affordances and write policy — `'platform'` (default), `'config'`, `'system'`, `'append-only'`, `'better-auth'`. See [Lifecycle bucket](#lifecycle-bucket-managedby) below. |
220+
| `managedBy` | `enum` | Lifecycle bucket that sets the default CRUD affordances and write policy — `'platform'` (default), `'config'`, `'system'`, `'engine-owned'`, `'append-only'`, `'better-auth'`. See [Lifecycle bucket](#lifecycle-bucket-managedby) below. |
221221
| `userActions` | `object` | Per-object override of the CRUD affordances the `managedBy` default implies — `{ create?, edit?, delete?, import?, exportCsv? }`. This is what makes a `system`/`append-only` object admin/user-writable. See [Lifecycle bucket](#lifecycle-bucket-managedby). |
222222
| `sharingModel` | `enum` | Org-Wide Default record visibility (ADR-0055/0056/0090). Canonical four only: `'private'`, `'public_read'`, `'public_read_write'`, `'controlled_by_parent'` (detail visibility derived from its master). The legacy aliases (`'read'`, `'read_write'`, `'full'`) were removed from the enum (ADR-0090 D4) — authoring rejects them. Unset on a custom object resolves to `'private'` (ADR-0090 D1) |
223223
| `ownership` | `enum` | Record-ownership model: `'user'` (default — injects the reassignable `owner_id` lookup, engaging owner-scoped RLS, "My" views and owner reports), `'org'`, or `'none'` (no per-record owner — Dataverse-style catalog / junction tables, skips `owner_id`). Distinct from the package `own`/`extend` contribution kind. |
@@ -235,20 +235,23 @@ bare bucket string.
235235
| :--- | :--- |
236236
| `platform` | **Default.** User-owned business data — full New / Import / Edit / Delete. |
237237
| `config` | Admin-authored configuration — New / Edit / Delete, no CSV import. |
238-
| `system` | Platform-defined schema. **Engine-owned by default**: a platform service owns the row lifecycle, generic CRUD is hidden, and the object is exposed `['get', 'list']` only. |
238+
| `system` | Platform-defined schema holding **admin/user-writable data** (RBAC link tables, preferences, messaging config). Locked by default; each object opens its writes via `userActions`. |
239+
| `engine-owned` | Runtime rows a platform service owns end to end — generic CRUD hidden, exposed `['get', 'list']` only, **no user writes ever**. |
239240
| `append-only` | Immutable audit trail — View + Export only. |
240241
| `better-auth` | Identity tables owned by the better-auth driver — generic user-context CRUD is suppressed; mutations flow through the auth API (sign-in, invite, reset). |
241242

242-
**Engine-owned vs. admin-writable `system` objects (ADR-0103).** The `system`
243-
bucket covers two kinds of object. Most are *engine-owned* — jobs,
244-
notifications, approval rows, `sys_record_share`, `sys_automation_run`, audit
245-
trails, `sys_secret` — written only by their owning service under a system
246-
context, never through the generic `/data` API; a fail-closed guard
247-
(`assertEngineOwnedWriteAllowed`) rejects user-context generic writes to them.
248-
A minority are *platform-defined schema with admin/user-writable data* — the
249-
RBAC link tables, `sys_user_preference`, `sys_approval_delegation`, the
250-
messaging config grids. These keep `managedBy: 'system'` but declare
251-
`userActions` to open the writes they legitimately take:
243+
**`engine-owned` vs. writable `system` objects (ADR-0103).** Two buckets share
244+
the same locked default matrix but say different things:
245+
246+
- **`engine-owned`** — jobs, notifications, approval runtime rows,
247+
`sys_record_share`, `sys_automation_run`, the metadata store, `sys_secret`,
248+
audit trails — written only by their owning service under a system context,
249+
never through the generic `/data` API. A fail-closed guard
250+
(`assertEngineOwnedWriteAllowed`) rejects user-context generic writes to them.
251+
- **`system`** — platform-defined schema holding admin/user-writable *data*: the
252+
RBAC link tables, `sys_user_preference`, `sys_approval_delegation`, the
253+
messaging config grids. These declare `userActions` to open the writes they
254+
legitimately take:
252255

253256
```typescript
254257
export const SysUserPreference = ObjectSchema.create({

content/docs/references/data/object.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ const result = ApiMethod.parse(data);
101101
| **description** | `string` | optional | Developer documentation / description |
102102
| **icon** | `string` | optional | Icon name (Lucide/Material) for UI representation |
103103
| **isSystem** | `boolean` | optional | Is system object (protected from deletion; defaults its org-wide sharing to public when no sharingModel is set — plugin-sharing) |
104-
| **managedBy** | `Enum<'platform' \| 'config' \| 'system' \| 'append-only' \| 'better-auth'>` | optional | Lifecycle bucket — platform (user CRUD) \| config (admin authored) \| system (engine-managed) \| append-only (audit) \| better-auth (identity). UI clients honour the resolved affordance matrix. |
104+
| **managedBy** | `Enum<'platform' \| 'config' \| 'system' \| 'engine-owned' \| 'append-only' \| 'better-auth'>` | optional | Lifecycle bucket — platform (user CRUD) \| config (admin authored) \| system (engine-managed schema, writable via userActions) \| engine-owned (engine owns the lifecycle, no user writes) \| append-only (audit) \| better-auth (identity). UI clients honour the resolved affordance matrix. |
105105
| **ownership** | `Enum<'user' \| 'org' \| 'none'>` | optional | Record-ownership model: user (default — injects reassignable owner_id) \| org \| none (no per-record owner, skips owner_id). Distinct from the package own/extend contribution kind. |
106106
| **userActions** | `{ create?: boolean; import?: boolean; edit?: boolean \| { enabled?: boolean; visibleWhen?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; disabledWhen?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } }; delete?: boolean \| { enabled?: boolean; visibleWhen?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; disabledWhen?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } }; … }` | optional | Per-object override of the resolved CRUD affordance matrix. |
107107
| **systemFields** | `'false' \| { tenant?: boolean; audit?: boolean }` | optional | Opt out of, or selectively disable, registry-level system-field auto-injection. |

docs/adr/0103-managedby-write-policy-and-engine-write-guard.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,51 @@ a row users never hand-edit.
144144
the intended ADR-0049 correction, called out in the release note.
145145
- The taxonomy is now honest: a reader can tell engine-owned from writable by the
146146
resolved affordances, and the guard enforces it.
147+
148+
## Addendum (2026-07-20, v16) — the anticipated enum split lands: explicit `engine-owned` bucket
149+
150+
D1 deferred the enum split ("Splitting the enum later remains possible as a pure
151+
rename on top of the now-correct affordances"). Both reasons D1 cited *against* an
152+
enum value are now retired, so v16 adopts it — additively — as the self-documenting
153+
successor to the engine-owned-DEFAULT overload of `system`.
154+
155+
**Why it is now safe** (the D1 objections, resolved):
156+
1. *Silent fully-editable fallthrough on deployed clients* — neutralised by the
157+
server-side enforcement this same ADR added: an unknown bucket resolving to the
158+
`platform` default on an old client is now **cosmetic**, because the engine write
159+
guard (D2), `apiMethods` reconciliation (D3) and the `/me/permissions` clamp (D4)
160+
reject the write regardless of what the client renders. (Re-verified end to end
161+
in the showcase: a generic `/data` create on an engine-owned object returns 405.)
162+
2. *Open-ended UI type across three mirrors* — closed by **objectui#2712**: the
163+
`ManagedByBucket` union is now a single closed type, so a new value is a compile
164+
error to miss, not a silent fallthrough.
165+
166+
**The split (additive — `system` is retained, nothing is removed):**
167+
- New enum value **`engine-owned`** with the same all-locked default affordance row
168+
as `system` (`create/import/edit/delete: false`, `exportCsv: true`). It joins
169+
`ENGINE_OWNED_BUCKETS` (guard) and `GUARDED_WRITE_BUCKETS` (clamp); the guard,
170+
reconciliation and clamp mechanisms are **unchanged** — engine-owned is simply an
171+
explicit member of the set they already covered by resolved affordance.
172+
- The **20** objects that were `system` with no write-opening `userActions` (the
173+
metadata store, jobs, approvals runtime rows, sharing rows, automation runs, the
174+
messaging delivery/receipt pipeline, secrets, settings) are relabelled
175+
`system → engine-owned` — a one-line, behaviour-identical change per object.
176+
- The **8** objects that are platform-schema **admin/user-writable DATA** (the RBAC
177+
link tables `sys_user_position` / `sys_user_permission_set` /
178+
`sys_position_permission_set`, `sys_user_preference`, `sys_approval_delegation`,
179+
and the messaging config grids) **keep `managedBy: 'system'`**, which now reads as
180+
"engine-managed schema, writable via `userActions`" — the residual meaning of the
181+
bucket after the engine-owned rows move out.
182+
183+
**Not a behaviour or enforcement change.** Resolved affordances, the guard verdict,
184+
the 405 reconciliation and the permissions clamp are byte-identical before and
185+
after; this is a self-documenting relabel. No data migration (`managedBy` is schema
186+
metadata, not row data), and no code branches on the `'system'` literal (all
187+
enforcement keys off `resolveCrudAffordances` / the bucket-set membership).
188+
189+
**Sequencing (v16 RC).** The framework enum and the objectui union land together and
190+
the vendored console is re-pinned before GA; during any sync window an old console
191+
renders an unknown `engine-owned` object editable but the server still 405s the
192+
write (point 1). Removing the overloaded `system` entirely — moving the 8 writable
193+
objects to a dedicated writable-platform-data bucket (or `config`) and retiring
194+
`system` — is a genuinely breaking rename deferred to **v17**.

packages/metadata-core/src/objects/sys-metadata-commit.object.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const SysMetadataCommitObject = ObjectSchema.create({
2727
pluralLabel: 'Metadata Commits',
2828
icon: 'git-commit',
2929
isSystem: true,
30-
managedBy: 'system',
30+
managedBy: 'engine-owned',
3131
description: 'Package-scoped commit log grouping a turn’s metadata changes (ADR-0067).',
3232

3333
fields: {

packages/metadata-core/src/objects/sys-metadata-history.object.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const SysMetadataHistoryObject = ObjectSchema.create({
4646
pluralLabel: 'Metadata History',
4747
icon: 'history',
4848
isSystem: true,
49-
managedBy: 'system',
49+
managedBy: 'engine-owned',
5050
description: 'Durable event log of metadata overlay changes (per-org, append-only)',
5151

5252
fields: {

packages/metadata-core/src/objects/sys-metadata.object.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ export const SysMetadataObject = ObjectSchema.create({
2020
pluralLabel: 'System Metadata',
2121
icon: 'settings',
2222
isSystem: true,
23-
// managedBy: 'system' — the metadata table backs every other config
23+
// managedBy: 'engine-owned' — the metadata table backs every other config
2424
// object. Writing rows directly here bypasses the typed Zod APIs and
2525
// would let an admin inject malformed payloads. The "All Metadata"
2626
// menu is therefore a read-only debug surface (Export only); typed
2727
// edits flow through the dedicated per-type pages (Approval Process,
2828
// Sharing Rule, etc.).
29-
managedBy: 'system',
29+
managedBy: 'engine-owned',
3030
description: 'Stores platform and user-scope metadata records (objects, views, flows, etc.)',
3131

3232
fields: {
@@ -227,7 +227,7 @@ export const SysMetadataObject = ObjectSchema.create({
227227
trackHistory: true,
228228
searchable: false,
229229
apiEnabled: true,
230-
// #3220 — sys_metadata is `managedBy: 'system'`: customization overlays are
230+
// #3220 — sys_metadata is `managedBy: 'engine-owned'`: customization overlays are
231231
// authored ONLY through the metadata-protocol RPC (its engine writes carry a
232232
// transaction context, not a user session), never the generic /data route.
233233
// Neither the framework nor the Console (objectui) POSTs /data/sys_metadata,

packages/platform-objects/src/audit/sys-import-job.object.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const SysImportJob = ObjectSchema.create({
2525
pluralLabel: 'Import Jobs',
2626
icon: 'upload',
2727
isSystem: true,
28-
managedBy: 'system',
28+
managedBy: 'engine-owned',
2929
description: 'Asynchronous bulk-import job state, progress, and history',
3030
displayNameField: 'object_name',
3131
nameField: 'object_name', // [ADR-0079] canonical primary-title pointer

packages/platform-objects/src/audit/sys-job-queue.object.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const SysJobQueue = ObjectSchema.create({
3030
pluralLabel: 'Job Queue Messages',
3131
icon: 'inbox',
3232
isSystem: true,
33-
managedBy: 'system',
33+
managedBy: 'engine-owned',
3434
description: 'Durable job/message queue including dead letters',
3535
displayNameField: 'queue',
3636
nameField: 'queue', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)

packages/platform-objects/src/audit/sys-job.object.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const SysJob = ObjectSchema.create({
2121
pluralLabel: 'Background Jobs',
2222
icon: 'clock',
2323
isSystem: true,
24-
managedBy: 'system',
24+
managedBy: 'engine-owned',
2525
description: 'Catalogue of registered background jobs',
2626
displayNameField: 'name',
2727
nameField: 'name', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)

0 commit comments

Comments
 (0)