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
docs(adr): ADR-0073 — correct tenancy model to environment-per-database (ADR-0002); automation principal is per-env Data-Plane, cross-env is Control-Plane
Copy file name to clipboardExpand all lines: docs/adr/0073-automation-execution-identity.md
+18-14Lines changed: 18 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
3
3
**Status**: Proposed (2026-06-25)
4
4
**Deciders**: ObjectStack Protocol Architects
5
-
**Builds on**: [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce-or-mark gate; stage by whether the feature exists), [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (`sys_role` is platform-native; `ExecutionContext.roles`; scheduled/lifecycle jobs), [ADR-0066](./0066-unified-authorization-model.md) (capability / assignment / requirement separation — resources declare a capability, never "who"), [ADR-0068](./0068-unified-user-context-and-built-in-identity-roles.md) (`EvalUser` / `current_user`; **identities are roles, not booleans**)
5
+
**Builds on**: [ADR-0002](./0002-environment-database-isolation.md) (**environment-per-database** isolation; Control-Plane / Data-Plane split), [ADR-0004](./0004-cloud-multi-kernel.md) (`ObjectKernel` per environment), [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce-or-mark gate; stage by whether the feature exists), [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (`sys_role` is platform-native; `ExecutionContext.roles`; scheduled/lifecycle jobs), [ADR-0066](./0066-unified-authorization-model.md) (capability / assignment / requirement separation — resources declare a capability, never "who"), [ADR-0068](./0068-unified-user-context-and-built-in-identity-roles.md) (`EvalUser` / `current_user`; **identities are roles, not booleans**)
6
6
**Consumers**: `@objectstack/spec` (the identity contract + `runAs` semantics), `@objectstack/cli` (author-time validation), `@objectstack/plugin-security` (`resolve-execution-context`, RLS, seeded roles — M2), `@objectstack/service-automation` (the engine's `runAs` resolution — M2), `@objectstack/trigger-schedule` + `@objectstack/trigger-api` (user-less trigger surfaces), `@objectstack/plugin-reports` (the report scheduler — today hand-rolls `SYSTEM_CTX`), `@objectstack/runtime` (audit actor), and the ADR-0057 lifecycle/retention jobs.
7
7
8
8
**Premise**: pre-launch — specify the target end-state, then land only the non-speculative slice now (ADR-0049). This ADR **completes** the identity model: ADR-0068 unified the *human* identity surface (`current_user`, identities-as-roles); this ADR adds the **non-human / automation** identity that user-less runs need, in the same idiom. **It is deliberately mostly a decision record**: the acute risk is already mitigated (see Severity), so v1 builds almost nothing — it pins semantics before the AI authors a large body of metadata against the wrong model, and ships one author-time guardrail.
@@ -13,12 +13,12 @@
13
13
14
14
## TL;DR
15
15
16
-
1.**[model] Automation is a first-class non-human identity, expressed as built-in roles** (the ADR-0068 idiom): `automation`(org-scoped — app-authored scheduled/user-less flows) and `platform_automation` (unscoped — platform-internal jobs only). A user-less run resolves to an `EvalUser` whose `id` is a stable automation principal and whose `roles` carry the automation role. There is **no anonymous run**.
16
+
1.**[model] Automation is a first-class non-human identity, expressed as a built-in role** (the ADR-0068 idiom): the **environment's `automation`principal** — a Data-Plane identity living in that environment's own kernel/DB. A user-less run resolves to an `EvalUser` whose `id` is the env's stable automation principal and whose `roles` carry the `automation` role. There is **no anonymous run**. (Cross-environment, platform-wide automation is a **Control-Plane** concern — ADR-0002/0004 — out of scope; see D4.)
17
17
2.**[model]`runAs` declares *authorization posture*, not identity** — decoupling the two axes the platform conflates today:
18
18
-`user` — run as the triggering human (only valid when one exists);
19
19
-**`automation`** (the target default for user-less triggers) — run as the automation principal **with RLS enforced** against its grants (Salesforce "with sharing"; the safe middle);
3.**[model] Attribution is always concrete** — every run carries an identity, so the **audit actor** is the human, the automation principal, or `platform_automation`. **No more `created_by = NULL` automation writes.** Attribution is recorded at the audit layer and is **decoupled from record ownership** (automation must not silently *own* the rows it writes, or owner-RLS would hide them from humans).
21
+
3.**[model] Attribution is always concrete** — every run carries an identity, so the **audit actor** is the human or the environment's automation principal. **No more `created_by = NULL` automation writes.** Attribution is recorded at the audit layer and is **decoupled from record ownership** (automation must not silently *own* the rows it writes, or owner-RLS would hide them from humans).
22
22
4.**[ruled] User-less + `runAs:'user'` is a configuration error** (no user to scope to). Mainstream platforms do not offer "as the triggering user" for scheduled work; neither do we.
23
23
5.**[staging — the key call] Build almost nothing now.** v1 = **this decision record + the author-time guardrail** (extend the #2308 lint to every user-less trigger; turn user-less `runAs:'user'` into a validation error). **Everything runtime — seeding the roles, the principal, attribution wiring, the `automation` default — is M2, *gated on the first real consumer*, not a date.** Rationale below.
24
24
@@ -54,7 +54,7 @@ This is a **footgun / hardening** issue, not an actively exploited hole, and the
54
54
55
55
- Scheduled flows are **admin/AI-authored metadata**; an unprivileged user **cannot trigger a schedule**, so there is no untrusted-input path to the fail-open.
56
56
-**#2308 already shipped** the cheap mitigations: a build-time lint, a runtime warning, and fixing the example flows to explicit `runAs:'system'`. The bleeding is stopped.
57
-
-The platform is **pre-launch, single-operator** (ADR-0068: "sole operator = the founder"), so multi-tenant automation scoping is still theoretical.
57
+
-Tenant isolation is **physical — environment-per-database** (ADR-0002): each tenant environment is its own kernel + DB. So the hard problem (cross-tenant RLS for an automation principal) **does not exist in this architecture** — the automation principal is a purely *intra-environment* Data-Plane identity with no cross-tenant data reach to scope. (The platform is also pre-launch / single-operator.)
58
58
- The live automation surface is **tiny**, and — decisively — the existing scheduled flows (`stale_opportunity_sweep`, the app-todo sweeps) all want **full `system` elevation**, not the RLS-respecting middle. **The `automation` mode this ADR introduces has zero consumers in the current app set.**
59
59
60
60
Building the runtime machinery now would therefore be the speculative enforcement ADR-0049 explicitly warns against. The real, present value is (a) pinning `runAs` semantics + the automation-identity model **before the AI authors metadata at scale**, and (b) the author-time guardrail.
@@ -83,14 +83,15 @@ This primitive is **not flow-specific**: `plugin-reports` already hand-rolls `SY
83
83
84
84
### D1 — A non-human automation identity, expressed as built-in roles
85
85
86
-
Extend ADR-0068's "identities are roles" to the non-human case. Reserved, managed `sys_role`rows (siblings of `platform_admin` / `org_*`), carrying `label` + `description`:
86
+
Extend ADR-0068's "identities are roles" to the non-human case. A single reserved, managed `sys_role`row per environment (sibling of `platform_admin` / `org_*`), carrying `label` + `description`:
87
87
88
88
| name | scope | meaning |
89
89
|---|---|---|
90
-
|`automation`| org-scoped (`organization_id` = the run's tenant) | the identity for **app-authored** scheduled / user-less flows within a tenant. |
91
-
|`platform_automation`| unscoped (`org_id = null`) | the identity for **platform-internal** jobs (retention, telemetry, migrations). **Not author-selectable** — reserved for the platform, sibling to `platform_admin`. |
90
+
|`automation`| the environment (its own kernel/DB) | the identity for **app-authored** scheduled / user-less / system-initiated runs **within this environment**. |
92
91
93
-
A user-less run resolves to an `EvalUser` (ADR-0068) whose `id` is a stable automation principal id and whose `roles` include the appropriate automation role. It appears as `current_user` like any other identity — so RLS, formulas, and audit treat it uniformly, with **zero bespoke booleans** (ADR-0068 D2). The principal is **non-loginable** and excluded from human/admin enumerations (the existing `usr_system` exclusion guards already model this). This does **not** resurrect `usr_system` as a seed-ownership crutch (seeds keep NULL-then-claim); it adds a non-human **execution + attribution** identity in the modern idiom.
92
+
Because isolation is **environment-per-database** (ADR-0002, see D4), there is **no in-kernel cross-tenant automation role** — the DB boundary is the tenant boundary. Cross-environment, platform-wide automation (retention across customers, fleet telemetry) is a **Control-Plane** actor (ADR-0002/0004) that iterates over environments; it is *not* an in-kernel `sys_role` and is out of scope here.
93
+
94
+
A user-less run resolves to an `EvalUser` (ADR-0068) whose `id` is the environment's stable automation principal id and whose `roles` include the `automation` role. It appears as `current_user` like any other identity — so RLS, formulas, and audit treat it uniformly, with **zero bespoke booleans** (ADR-0068 D2). The principal is **non-loginable** and excluded from human/admin enumerations (the existing `usr_system` exclusion guards already model this). This does **not** resurrect `usr_system` as a seed-ownership crutch (seeds keep NULL-then-claim); it adds a non-human **execution + attribution** identity in the modern idiom.
94
95
95
96
### D2 — `runAs` declares authorization posture, not identity
96
97
@@ -106,13 +107,16 @@ A user-less run resolves to an `EvalUser` (ADR-0068) whose `id` is a stable auto
106
107
107
108
### D3 — Attribution is always concrete, and decoupled from ownership
108
109
109
-
- Every run carries an identity, so the **audit actor** is always concrete: the human, `automation`, or `platform_automation`. The anonymous/`NULL` automation write is eliminated.
110
+
- Every run carries an identity, so the **audit actor** is always concrete: the humanor the environment's `automation` principal. The anonymous/`NULL` automation write is eliminated.
110
111
-**Attribution ≠ ownership.** Automation must **not** be force-stamped as `created_by` / `owner_id` of the rows it writes — owner-RLS keys on `created_by == current_user.id` (ADR-0057/0068), so automation-owned rows would become **invisible to the humans they are for**. Salesforce models this exactly: `CreatedBy = Automated Process` (audit) while `OwnerId` is set by flow logic. Therefore: record the automation actor at the **audit layer**; let flow logic set ownership explicitly (or leave the normal default), not the execution identity.
111
112
112
-
### D4 — Scope follows the isolation boundary (ADR-0068 D3)
113
+
### D4 — Isolation is physical (environment-per-database); the automation principal is per-environment
114
+
115
+
ObjectStack is **environment-per-database** (ADR-0002): each tenant environment is its own `ObjectKernel` + DB (ADR-0004), with a hard **Control-Plane / Data-Plane** split. This dissolves the multi-tenant scoping question rather than answering it:
113
116
114
-
- The `automation` identity is **tenant-scoped**: a scheduled flow belongs to an app installed in a tenant, so its automation principal carries that tenant's `organizationId`; RLS evaluates within the tenant; its grants bound what it can touch.
115
-
-`platform_automation` is the **only** cross-tenant automation identity, reserved for platform-internal jobs and never author-selectable. Mirrors `platform_admin` (operator) vs `org_admin` (tenant), and answers "which tenant does a scheduled sweep run in?" — its own.
117
+
- The `automation` principal is a **Data-Plane** identity that lives in the environment's **own** kernel/DB. There is **no cross-tenant data access from inside a tenant kernel** — the database boundary *is* the tenant boundary — so **no "unscoped cross-tenant" automation role is needed** (or wanted).
118
+
- Its RLS is therefore purely **intra-environment**: it respects *this environment's* users' ownership/sharing (`runAs:'automation'`) or bypasses them (`runAs:'system'`). The `automation` vs `system` distinction is about the env's own users, never about tenants.
119
+
-**Cross-environment / platform-wide automation** (retention across customers, fleet telemetry, migrations) is a **Control-Plane** concern that iterates over environments — each a separate DB — and acts within each via that env's own principal. It is **not** a kernel-resident role with cross-tenant reach, and is **out of scope** for this (Data-Plane) ADR.
116
120
117
121
### D5 — User-less `runAs:'user'` is a configuration error [enforced in v1]
118
122
@@ -131,7 +135,7 @@ Runtime behavior is otherwise **unchanged** from #2308 (the audible warning stay
131
135
**M2 — gated on the FIRST REAL CONSUMER, not a date.** Build the principal *with* its first user, in this order:
132
136
1.**Attribution wiring (non-breaking):** user-less runs carry the automation principal as the **audit actor** (D3). Likely first domino: **migrating `plugin-reports` / audit / ADR-0057 lifecycle jobs off ad-hoc `SYSTEM_CTX` onto the principal** — that migration is what first makes the principal earn its keep.
133
137
2.**Authorization (the behavior change):** seed the roles (D1); `runAs:'automation'` becomes the user-less default and runs **RLS-enforced** (D2); assign capabilities/permission-sets to the automation role (ADR-0066). Ship **default-broad → tighten** (the GitHub-Actions playbook) with each flow's effective grants **visible and restrictable**.
134
-
- Other triggers to start M2: **multi-tenant lands** (D4 becomes real), or a **real volume of scheduled CRUD flows** appears (orphan attribution starts to bite).
138
+
- Other trigger to start M2: a **real volume of scheduled CRUD flows** appears (orphan attribution starts to bite). (Multi-tenant is *not* a trigger — it is already handled physically by env-per-DB, ADR-0002.)
135
139
-`runAs:'system'` stays god-mode throughout.
136
140
137
141
**Non-goals / deferred:**
@@ -169,7 +173,7 @@ Runtime behavior is otherwise **unchanged** from #2308 (the audible warning stay
169
173
2.**`@objectstack/cli`** — extend `flow-schedule-runas-unscoped` to all user-less trigger types; make user-less `runAs:'user'` a hard validation error (D5).
170
174
171
175
**M2 (gated on first consumer):**
172
-
3.**`plugin-security`** — seed `automation` / `platform_automation``sys_role`rows (sibling to `bootstrap-declared-roles`); extend the non-human exclusion guards.
176
+
3.**`plugin-security`** — seed the per-environment `automation``sys_role`row (sibling to `bootstrap-declared-roles`); extend the non-human exclusion guards.
173
177
4.**`service-automation`** — `resolveRunContext` resolves user-less runs to the automation `EvalUser`; `runAs:'automation'` threads an RLS-enforcing context (not `isSystem`); stamp the audit actor (D3).
174
178
5.**`runtime` / audit** — record the automation actor; do **not** stamp `created_by`/`owner_id` to it (D3).
175
179
6.**`plugin-reports`, ADR-0057 jobs** — adopt the principal in place of ad-hoc `SYSTEM_CTX` (the likely first consumer that triggers M2).
0 commit comments