Commit b16dcb4
* fix(runtime): gate the paused-run screen read to the run's trigger identity (#7968)
`GET /automation/:name/runs/:runId/screen` served the paused run's ScreenSpec to
any authenticated caller who knew a run id. A screen node's `defaults` and
per-field `defaultValue` are interpolated against the live flow variables at
suspend time, so a flow prefilling from its triggering record persists those
values into the spec this route hands back — measured on a real screen flow over
a `crm_lead` record: company in the title, email in the description, and email,
phone and salary band as field defaults, answered 200 to a stranger explicitly
refused the `sys_automation_run` read grant.
Maintainer ruling 2026-08-12 (Option B): the route now requires the run's own
trigger identity (`ExecutionLogEntry.trigger.userId`) OR read access to
`sys_automation_run` as an operator override.
The object grant ALONE is deliberately not the gate — it would refuse the screen
to the very person the flow paused for, which is why #7900 audited this route out
of its convergence. So the grant is the override half, and the over-block
direction is pinned as hard as the under-block one.
The `sys_automation_run` question is now one predicate (`mayReadRunState`) shared
by both gates rather than a second copy of the resolution / feature-detection /
fail-closed logic.
Unchanged: the 404 for a run with no pending screen (the gate runs after the
lookup, so every not-found path is byte-identical for every caller), the 501, the
401 anonymous floor, `resume`'s own authority checks, and which runs exist.
Option A — the per-run `resumeAuthority` read gate — stays the recorded direction
and is out of scope here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V
* test(runtime): assert the screen route's routing claim on its answer, not on getRun being unused (#7968)
`should get the pending screen via GET /:name/runs/:runId/screen` claimed that
the screen path is not swallowed by the `/:name/runs/:runId` branch below it, and
asserted it as "getRun was never called". The #7968 gate reads the run to resolve
its trigger identity, so that proxy no longer tracks the claim.
Asserted on the answer instead: the caller gets the screen envelope
(`{ runId, screen }`) and not the `ExecutionLogEntry` the run-detail branch serves
verbatim — the mock's entry is `{ id, status }`, so the two are distinguishable
by shape. The routing claim is now pinned more directly than before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent c4624f0 commit b16dcb4
5 files changed
Lines changed: 742 additions & 40 deletions
File tree
- .changeset
- packages/runtime/src
- domains
Lines changed: 38 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
Lines changed: 14 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
35 | 38 | | |
36 | 39 | | |
37 | 40 | | |
| |||
323 | 326 | | |
324 | 327 | | |
325 | 328 | | |
326 | | - | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
327 | 339 | | |
328 | 340 | | |
329 | 341 | | |
| |||
0 commit comments