Skip to content

Commit 0951a00

Browse files
docs(adr): ADR-0073 amendment and ADR-0096 row for the system flow's refused identity-assignment writes (#25587)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoVgqGdGWoJtcHYXUFi5Es7m)_ Part of #25554 The ADR half of ruling 6354966423 (letter B) on #25251, the follow-up of card #25554. #25483 made a `runAs: 'system'` flow's governed writes to the four identity-assignment objects refused, and #25564 put that into the `FlowSchema.runAs` description and the ADR-0087 D3 ledger. Two ADRs still describe system flows as unconditionally elevated. `docs/adr/**` is a governed path, so these edits come as their own PR for a maintainer merge. ## 维护者速读(草稿) - **改了什么:** 只改两份 ADR 的文字。ADR-0073 新增一节带日期的 Amendment(2026-10-24),写明 `runAs:'system'` 流程对岗位持有、权限集授予、邀请、成员这四张表的写入会被拒绝(`PERMISSION_DENIED`),M2 里「system 始终是 god-mode」一句按这个例外来读;#3760 那节里的 `sys_user_position` 例子补一句指向新 Amendment。ADR-0096 的执行面表格里 system 流程那一行补上这个例外。 - **为什么改:** 运行时(#25483)和 spec 描述(#25564)已经落地这条规则,ADR 还写着 system 流程不受限,三处说法不一致,AI 作者读 ADR 会被误导。 - **风险与代价(含回滚):** 纯文档,不改代码、不发包,无运行时影响。回滚就是 revert 本 PR。 - **席位意见:** - **你要做的:** 看一下 Amendment 的措辞是否准确,没问题就合并。 ## What changed - **ADR-0073:** a dated `## Amendment:` section (2026-10-24, #25251), placed above the #3760 amendment, following that section's form. It names what is refused (the six governed verbs on `sys_user_position`, `sys_user_permission_set`, `sys_invitation`, `sys_member`, from a context with `isSystem` and a `flowRunId`), what is unchanged (reads, other objects, `runAs:'user'`, flow-less platform writers), and the remedies. The #3760 amendment's note, whose example is "letting them write `sys_user_position`", gains one trailing clause pointing at it. - **ADR-0096:** the surface table row `Flow (runAs:'system')` now reads "explicit elevation, except identity assignment", naming the four objects and the ADR-0073 amendment. ## Verification `node scripts/pm/dispatch-gates.mjs --commands` derived 20 families for this diff. All 20 exit 0, among them `check-adr-links`, `check-adr-symbol-anchors`, `check:adr-anchors`, `check:doc-authoring` and `check:pm-governed-merges`. `dispatch-gates --ran`: 20 run, 0 NOT-MEASURED. No changeset: `docs/adr/**` ships in no package. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_013yig39ZouqWBxABRfM5FN4 --- _Generated by [Claude Code](https://claude.ai/code/session_013yig39ZouqWBxABRfM5FN4)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent cb38a8c commit 0951a00

2 files changed

Lines changed: 16 additions & 2 deletions

File tree

‎docs/adr/0073-automation-execution-identity.md‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@
1111
1212
---
1313

14+
## Amendment: a `runAs:'system'` run writes no identity assignment (2026-10-24, #25251)
15+
16+
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).
17+
18+
**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`).
19+
20+
**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.
21+
22+
**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.
23+
24+
So `system` stays full elevation everywhere else, and the M2 bullet is read with this one exception.
25+
26+
---
27+
1428
## Amendment: user-less data ops are now REFUSED (2026-07-28, #3760)
1529

1630
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*
2135

2236
**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.
2337

24-
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.
38+
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.
2539

2640
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.
2741

‎docs/adr/0096-execution-surface-identity-admission.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ The comment states the original intent: *authentication* is the auth layer's job
5353
| REST / MCP object CRUD | caller's `ExecutionContext`, threaded | by design |
5454
| Business-action `script`/`body` | **none** — facade drops context → fall-open | #2849 (adversarial review) |
5555
| Flow (`runAs:'user'`, user-less trigger) | none → fall-open, warned | #2308 (adversarial review) |
56-
| Flow (`runAs:'system'`) | explicit elevation | ADR-0049/#1888, by design |
56+
| 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 |
5757
| Record-change / schedule triggers | threaded (after #1888 fixes) | bug class, then fixed |
5858
| Boot / seeding / internal jobs | `{ isSystem: true }` literals, scattered | by design, unaudited |
5959

0 commit comments

Comments
 (0)