From aab896300bcf9cc182bce6459ab394a9fd0666b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:01:58 +0000 Subject: [PATCH] docs(adr): ADR-0073 amendment and ADR-0096 row for the system flow's refused identity-assignment writes ADR-0073 gains a dated Amendment: a runAs system run's governed writes to sys_user_position, sys_user_permission_set, sys_invitation and sys_member are refused with PERMISSION_DENIED, so M2's "stays god-mode throughout" holds with that one exception. The #3760 amendment's sys_user_position example gains a pointer to it. ADR-0096's surface table row for system flows names the carve-out. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_013yig39ZouqWBxABRfM5FN4 --- docs/adr/0073-automation-execution-identity.md | 16 +++++++++++++++- .../0096-execution-surface-identity-admission.md | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/adr/0073-automation-execution-identity.md b/docs/adr/0073-automation-execution-identity.md index b109bd74bed..9fc3cbe8945 100644 --- a/docs/adr/0073-automation-execution-identity.md +++ b/docs/adr/0073-automation-execution-identity.md @@ -11,6 +11,20 @@ --- +## Amendment: a `runAs:'system'` run writes no identity assignment (2026-10-24, #25251) + +M2's last bullet below says "`runAs:'system'` stays god-mode throughout". That no longer holds for one family of writes. Ruling 6354966423 (letter B) on #25251 carved identity assignment out of a flow's system elevation, the same way the 2026-10-03 ruling carved the stored-metadata tables out of app-authored work: a flow is app-authored automation, and `runAs` declares its authorization posture, not an identity (D2). + +**What is refused.** An insert, update, delete, transfer, restore or purge of a row of `sys_user_position`, `sys_user_permission_set`, `sys_invitation` or `sys_member`, from a context that carries `isSystem` **and** a `flowRunId`. That is the context a `runAs:'system'` run's data nodes write under (`resolveRunDataContext`). The security middleware refuses it ahead of its `isSystem` short-circuit with `PERMISSION_DENIED` (403), and nothing is written. The refusal holds at any depth of the flow's own write: a cascade leg or a hook's write under the same context is refused the same way. Landed in #25483 (plugin-security, the run-time arm) and #25564 (the `FlowSchema.runAs` description and the ADR-0087 D3 entry `flow-run-as-system-identity-assignment-write-refused`). + +**What is unchanged.** A system run's reads of the four objects, and its writes to every other object. A `runAs:'user'` run, which carries a `flowRunId` without `isSystem` and reaches the ADR-0090 D12 delegated-administration gate as the user it acts for. Every platform writer whose system context carries no flow run: the seeders, the publish materializer, better-auth and its reconciliation, invitation placement, and the organization-administrator grant. + +**The remedy the refusal names.** For position holdings and permission-set grants, `runAs:'user'`: the D12 gate then judges the acting user and records that user as `granted_by`. For invitations and memberships, the organization endpoints (`invite-member`, `cancel-invitation`, `update-member-role`, `remove-member`), which apply the invitation's placement and role-cap checks. + +So `system` stays full elevation everywhere else, and the M2 bullet is read with this one exception. + +--- + ## Amendment: user-less data ops are now REFUSED (2026-07-28, #3760) Two load-bearing claims below turned out to be wrong. Both were about *severity*, not about the model — D1–D5 stand unchanged, and D5 shipped in full. @@ -21,7 +35,7 @@ Two load-bearing claims below turned out to be wrong. Both were about *severity* **Consequently `runAs:'user'` + no trigger user now REFUSES the data operation** (`UnscopedRunDataAccessError`, thrown from `resolveRunDataContext` — the single place every data node resolves its context). This implements D5's ruling ("user-less `runAs:'user'` is a configuration error") at the only layer that can see the record-change case, since whether a triggering write carries a user is not knowable at authoring time. -Note what this deliberately is **not**: it does not re-badge these runs as `isSystem`. The security middleware's `isSystem` short-circuit precedes its package-managed-row / system-row / audience-anchor / delegated-admin gates, all of which a principal-less context still has to clear — so "unscoped" was never equivalent to "system", and elevating would have *widened* these runs (e.g. letting them write `sys_user_position`) rather than preserving the status quo. +Note what this deliberately is **not**: it does not re-badge these runs as `isSystem`. The security middleware's `isSystem` short-circuit precedes its package-managed-row / system-row / audience-anchor / delegated-admin gates, all of which a principal-less context still has to clear — so "unscoped" was never equivalent to "system", and elevating would have *widened* these runs (e.g. letting them write `sys_user_position`) rather than preserving the status quo — a write that, since the 2026-10-24 Amendment above, a `runAs:'system'` run is itself refused. This is an **interim** posture, not a replacement for D2. Refusal is strictly safer than today and forward-compatible: when the `automation` principal lands, the refusal point becomes the place that resolves it, and these runs go from *refused* to *RLS-enforced* with no third state. Ordering is unchanged — D2 still waits for a real consumer. diff --git a/docs/adr/0096-execution-surface-identity-admission.md b/docs/adr/0096-execution-surface-identity-admission.md index a76934dffd1..8093ef61e29 100644 --- a/docs/adr/0096-execution-surface-identity-admission.md +++ b/docs/adr/0096-execution-surface-identity-admission.md @@ -53,7 +53,7 @@ The comment states the original intent: *authentication* is the auth layer's job | REST / MCP object CRUD | caller's `ExecutionContext`, threaded | by design | | Business-action `script`/`body` | **none** — facade drops context → fall-open | #2849 (adversarial review) | | Flow (`runAs:'user'`, user-less trigger) | none → fall-open, warned | #2308 (adversarial review) | -| Flow (`runAs:'system'`) | explicit elevation | ADR-0049/#1888, by design | +| Flow (`runAs:'system'`) | explicit elevation, except identity assignment: governed writes to `sys_user_position`, `sys_user_permission_set`, `sys_invitation` and `sys_member` are refused `PERMISSION_DENIED` (ADR-0073 Amendment 2026-10-24) | ADR-0049/#1888, by design; the carve-out #25251 / #25483 | | Record-change / schedule triggers | threaded (after #1888 fixes) | bug class, then fixed | | Boot / seeding / internal jobs | `{ isSystem: true }` literals, scattered | by design, unaudited |