Skip to content

Commit 28fe107

Browse files
committed
docs(permissions): the census row names every door that starts a flow by name; changeset
The elevated self-triggered start's isSystem read moves with the shared predicate, so its row anchors the new module and states the bypass at all three doors. Counts regenerated by gen:system-context-census. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
1 parent 85a0bcd commit 28fe107

2 files changed

Lines changed: 26 additions & 4 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
fix(runtime): the action door and the declared flow endpoint refuse a self-triggered flow declared to run as system to any caller but the system principal
6+
7+
Clause-②: no
8+
9+
The trigger door already refused this start, but two other doors that start a flow by name did not ask: an action of `type: 'flow'` (`POST /api/v1/actions/:object/:action`, and the MCP `run_action` tool, which reaches the same dispatch) and a declared endpoint of `type: 'flow'`. Any signed-in member who was refused at the trigger door could start the same elevated flow through either one, and its elevated write landed.
10+
11+
**What changes.** All three doors now apply one rule from one place. A caller that is not the system principal gets `403 PERMISSION_DENIED` (ADR-0112 envelope) when it starts a flow declared `runAs: 'system'` whose `type` is `autolaunched`, `record_change` or `schedule`. Nothing runs: no run record, no node, no side effect. This includes a platform admin's session and the guest principal an `authRequired: false` endpoint admits. The refusal names no part of the flow, and its message is now the same at every door: it no longer names the trigger door.
12+
13+
**What stays as it was.**
14+
15+
- `screen` and `api` flows, elevated or not: entries the author designed (ADR-0073 D2).
16+
- Every flow that does not declare `runAs: 'system'`.
17+
- A parent flow's `subflow` node calling an elevated flow: the child starts through the engine, never through a door.
18+
- The system principal (an in-process caller, such as a job), which starts every flow.
19+
- The action's own `requiredPermissions` gate (ADR-0066 D4) still answers first. Holding that capability does not open a self-triggered elevated target.
20+
- A declared endpoint's policy chain: an anonymous caller at an `authRequired: true` endpoint still gets `401` first.
21+
22+
**If an action or an endpoint now answers 403.** Its target flow runs on its own trigger. If members are meant to start that elevated work, declare the target `type: 'screen'` (or `type: 'api'` for a signed inbound hook), which states that it is an entry and keeps the elevation reviewable. Or keep it self-triggered and point the action or endpoint at such an entry whose `subflow` node calls it.

‎content/docs/permissions/system-context.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ The largest single consumer — **17 of the 119 sites**.
180180
| 53 | Package REST route capability gate bypassed | rest | Get: a marketplace publish over REST (`POST /packages/publish`, the one route the REST registrar mounts since #14503) without `manage_metadata`; the package read cohort (`studio.access` / `setup.access`) is enforced by the dispatcher `/packages` domain's own read gate, where the reads are served | `packages/rest/src/package-routes.ts#refusePackageRequest` |
181181
| 54 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability | `packages/runtime/src/domains/packages.ts#requireManageMetadata`, `#requireReadCapability` |
182182
| 55 | Activation write / authoring refusals do not fire | runtime | Get: activation artifacts writable and authorable without the activation-authoring capability | `packages/runtime/src/domains/activation-gate.ts#refuseUngrantedActivationWrite`, `#refuseUngrantedActivationAuthoring` |
183-
| 56 | Automation run-state read, flow-authoring write, the paused-run caller gate (screen read and resume), the two operator run-lifecycle writes and the trigger door's elevated self-triggered start all pass | runtime | Get: run state, flow writes, a paused run's screen and its resume with no grant and without being the run's starter — and cancelling or restoring a suspension without the platform-operator rung. The screen read and the resume ask one predicate, so this is one bypass for both. The lifecycle bypass is the in-process owner's door: plugin-approvals' revise-window recall (ADR-0044) cancels on behalf of a decision it already authorized and recorded. And starting, through the trigger door, a flow declared `runAs: 'system'` whose type is self-triggered (`autolaunched`, `record_change`, `schedule`), which every other caller is refused `403 PERMISSION_DENIED` (the maintainer's ruling, letter B). That bypass is the in-process caller's, such as a job: inbound HTTP never carries the flag | `packages/runtime/src/domains/automation.ts#mayReadRunState`, `#refuseUngrantedFlowWrite`, `#isRunStarterOrRunStateReader`, `#refuseUngrantedRunLifecycleWrite`, `#refusesElevatedSelfTriggeredStart` |
183+
| 56 | Automation run-state read, flow-authoring write, the paused-run caller gate (screen read and resume), the two operator run-lifecycle writes and the elevated self-triggered start at every door that starts a flow by name all pass | runtime | Get: run state, flow writes, a paused run's screen and its resume with no grant and without being the run's starter — and cancelling or restoring a suspension without the platform-operator rung. The screen read and the resume ask one predicate, so this is one bypass for both. The lifecycle bypass is the in-process owner's door: plugin-approvals' revise-window recall (ADR-0044) cancels on behalf of a decision it already authorized and recorded. And starting a flow declared `runAs: 'system'` whose type is self-triggered (`autolaunched`, `record_change`, `schedule`) through any door that starts a flow by name — the trigger route, an action of `type: 'flow'` (REST `/actions` and the MCP `run_action` bridge) and a declared endpoint of `type: 'flow'` — which every other caller, the guest principal included, is refused `403 PERMISSION_DENIED` (the maintainer's rulings, letters B and A). The three doors ask one predicate, so this is one bypass for all of them, and it is the in-process caller's, such as a job: inbound HTTP never carries the flag | `packages/runtime/src/domains/automation.ts#mayReadRunState`, `#refuseUngrantedFlowWrite`, `#isRunStarterOrRunStateReader`, `#refuseUngrantedRunLifecycleWrite`, `packages/runtime/src/flow-start-admission.ts#refusesElevatedSelfTriggeredStart` |
184184
| 57 | Audience-binding suggestion recording skipped | plugin-security | Lose: install-time suggestions are not recorded for system callers | `packages/plugins/plugin-security/src/suggested-audience-bindings.ts#assertTenantAdmin` |
185185
| 58 | Email-template organization door passes; webhook provenance stamp skipped | plugin-email, plugin-webhooks | Get (`sys_email_template`): a create or update of a template row. The door refuses every other write that names a caller, `403 PERMISSION_DENIED` (ADR-0131 D6), so the seeds, the boot sweep and the projection of a Studio save are the only writers. Lose (`sys_webhook`): the row is not marked as an admin customization | `packages/plugins/plugin-email/src/email-template-door.ts#isOrganizationWrite`, `packages/plugins/plugin-webhooks/src/webhook-provenance.ts#bindWebhookProvenanceStamp` |
186186
| 59 | **Automation flow data nodes re-add the `owner_id` stamp** (the one place row 2's gap is compensated inline) | service-automation | Get: a flow-authored INSERT under system elevation still lands owned, when the run resolved a user. Fill-only — flow-authored values win | `packages/services/service-automation/src/runtime-identity.ts#stampSystemInsertOwner`, called from `packages/services/service-automation/src/builtin/crud-nodes.ts#registerCrudNodes` |
@@ -356,7 +356,7 @@ still holds equal to the census on every pull request:
356356
| — in tests | 1013 | — |
357357
| — in non-test sources | 798 | — |
358358
| Appearances of the bare identifier `isSystem` in non-test sources | 813 | — |
359-
| — parsed as a declaration | 27 | ✅ |
359+
| — parsed as a declaration | 28 | ✅ |
360360
| — parsed as an object-literal / type key (producers and option objects) | 310 | — |
361361
| — parsed as a property **read** | 125 | ✅ |
362362
| — parsed in some other syntactic position (a local, a cast, a conditional) | 9 | ✅ |
@@ -366,7 +366,7 @@ still holds equal to the census on every pull request:
366366
| — behaviour-bearing (rows 1–61 above) | 116 | ✅ |
367367
| — carry the flag onward only (rows 62–64 above) | 3 | ✅ |
368368
| Packages containing at least one elevation read | **20** | ✅ |
369-
| Files containing at least one elevation read | 55 | ✅ |
369+
| Files containing at least one elevation read | 56 | ✅ |
370370
| — the distinct symbols those reads live in — what this page anchors | 102 | ✅ |
371371
| — of those files, the ones holding more than one read in one symbol | 8 | ✅ |
372372

@@ -431,7 +431,7 @@ same resolver, and the same registration shape, that holds `docs/adr/**`.
431431
Renaming a symbol is now a loud red instead of a silent misdirection.
432432

433433
⚠️ **The precision that costs, priced here rather than buried.** A symbol anchor
434-
cannot say WHICH read inside a function it means, and **8** of the **55**
434+
cannot say WHICH read inside a function it means, and **8** of the **56**
435435
anchored files hold more than one read inside a single symbol. So the population
436436
check runs per file at symbol granularity: every file the census finds a read in
437437
must be anchored, and the set of symbols this page cites into that file must

0 commit comments

Comments
 (0)