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
{{ message }}
Repository navigation
Commit a99d03a
Browse filesBrowse the repository at this point in the historyBrowse files
fix(runtime): gate POST /automation/:name/runs/:runId/resume on WHO is resuming
The resume arm validated the body and called resume() without reading any
identity, so an authenticated caller holding another user's run id continued
that user's paused run, which then ran under the starter's stored context.
The arm now asks the screen read's own question through one shared predicate,
isRunStarterOrRunStateReader: the run's trigger identity, OR the
sys_automation_run read grant as the operator override, OR a system context.
Refused callers get the read twin's 403 PERMISSION_DENIED with the same
requirement sentence under the door's own verb, and resume() is never called.
The gate sits after the body checks and immediately before the engine, and
fails closed on a run getRun cannot resolve.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TnPAC1UsTGfHPXVUCL6iLn
fix(runtime): `POST /automation/:name/runs/:runId/resume` checks WHO is resuming — the run's own starter, or the `sys_automation_run` read grant (#19987)
6
+
7
+
Clause-②: no
8
+
9
+
**The defect.** The resume route checked the body and then resumed. It never asked who the caller was. An authenticated user holding another user's run id could continue that user's paused run, and the run then went on under the identity STORED on the run: its data nodes ran as the user who started it, with the values the other user submitted. The screen read on the same pause, `GET /automation/:name/runs/:runId/screen`, already refused that caller.
10
+
11
+
**The fix.** The route now asks the screen read's own question, through the same function: the caller must be the user who started the run (`getRun(runId).trigger.userId`), OR hold read access to `sys_automation_run` (the operator override), OR be a system context. Anyone else gets `403 PERMISSION_DENIED`, the screen read's code and status, and the run is not touched: nothing reaches the engine, so the pause stays parked for the person it is waiting on. The message states the screen read's requirement word for word under this route's own verb: "Resuming a paused run requires being the identity that triggered the run, or read access to 'sys_automation_run'."
12
+
13
+
**What stays the same.**
14
+
15
+
- The user who started a run resumes it exactly as before, with no grant needed, and still does while the permission subsystem is down.
16
+
- The suspended node's own gate (`resumeAuthority`) is unchanged and still answers behind this one. An `approval` pause is still refused on this route for every caller; approvers decide through the approvals API, which never uses this route.
17
+
- Body checks come first, as before, so every `400` for a malformed body is unchanged for every caller.
18
+
- For the starter, a `sys_automation_run` reader and a system context, every engine answer is unchanged, including the `404` for an unknown or finished run.
19
+
20
+
**Who needs to act.** A caller that resumes runs it did NOT start (for example an integration that feeds a `wait` node's signal, or a support tool) now needs read access to `sys_automation_run`. Without it, it gets the `403` above. Such a caller also gets that `403`, not the old `404`, for a run id that does not resolve. The check fails closed there because a run that is still paused can read as "not found" while the run store is degraded.
Copy file name to clipboardExpand all lines: content/docs/automation/flows.mdx
+32-6Lines changed: 32 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -764,7 +764,7 @@ POST /api/v1/automation/{flow}/runs/{runId}/resume
764
764
|`approval`| a human decision | the approvals service (`POST /api/v1/approvals/requests/:id/approve\|reject`) — resumes down the matching `approve` / `reject` edge. **Decide through the approvals API**; the resume route above **refuses** an approval pause outright (see below). |
765
765
|`screen`| a user submits the form | the UI runner posting the collected `inputs` — **validated server-side against the screen's declared `fields`** (see below); a `paused` response carrying the next `screen` chains multi-step wizards under one stable `runId`|
766
766
|`wait` (timer) | an ISO-8601 duration elapses |**automatically** — a one-shot job resumes the run; after a cold boot the engine re-arms pending timers from the durable store (overdue timers resume immediately) |
767
-
|`wait` (signal) | a named external event |any caller invoking `resume(runId)`|
767
+
|`wait` (signal) | a named external event |the external producer calling the resume route — as the user who started the run, or with read access to `sys_automation_run` (see below); in process, `resume(runId)` on the automation service|
768
768
769
769
<Callouttype="warn">
770
770
**A pause ends the flow's retry-governed segment.** `errorHandling: {strategy:
@@ -807,11 +807,37 @@ change (there is no `FLOW_STRANDED`), and the trigger route's `400` carries
807
807
referent there. The shape is `ResumeFailureDetailsSchema` in
808
808
`@objectstack/spec/api`.
809
809
810
-
### Who may resume — the gate is the suspended node
811
-
812
-
The resume route is generic, so **the node the run is parked on** decides
813
-
whether a raw resume is a legitimate continuation. Every action descriptor
814
-
carries a `resumeAuthority`:
810
+
### Who may resume — the caller, then the suspended node
811
+
812
+
The route asks two questions, in this order. Both must pass.
813
+
814
+
**1. Who is resuming.** A resumed run continues under the identity stored on
815
+
the run, so its data nodes run as the user who **started** it, whoever sent the
816
+
values. The route therefore admits only:
817
+
818
+
- the user who started the run (the run's `trigger.userId`) — no extra
819
+
permission needed;
820
+
- a caller with **read access to `sys_automation_run`** — the operator
821
+
override, for support tooling or an integration that feeds a signal `wait` on
822
+
runs it did not start;
823
+
- a system context.
824
+
825
+
Anyone else gets **403**`PERMISSION_DENIED` — "Resuming a paused run requires
826
+
being the identity that triggered the run, or read access to
827
+
'sys_automation_run'." — and nothing reaches the engine, so the run stays
828
+
parked for the person it is waiting on. This is the same rule, code and status
829
+
as reading the run's screen (`GET /api/v1/automation/{flow}/runs/{runId}/screen`),
830
+
checked by the same function. The check runs after the body checks, so a
831
+
malformed body is still a `400` for every caller. If the run id does not
832
+
resolve, only an admitted caller (a system context or a `sys_automation_run`
833
+
reader) gets the engine's `404`. Everyone else gets the `403`, because a run
834
+
that is still paused can read as "not found" while the run store is degraded.
835
+
The MCP `resume_run` tool is stricter: only the user who started the run, with
836
+
no operator override (see [actions as tools](/docs/ai/actions-as-tools)).
837
+
838
+
**2. What the run is parked on.** The resume route is generic, so **the node
839
+
the run is parked on** decides whether a raw resume is a legitimate
840
+
continuation. Every action descriptor carries a `resumeAuthority`:
815
841
816
842
-**`'any'`** (`screen`, `wait`): the caller supplies the continuation and the
0 commit comments