|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +"@objectstack/plugin-approvals": minor |
| 4 | +"@objectstack/lint": minor |
| 5 | +--- |
| 6 | + |
| 7 | +fix(approvals): the ADR-0044 revise window is a service-owned node type, not a bare `wait` (#3823) |
| 8 | + |
| 9 | +#3801 gated `POST /api/v1/automation/:name/runs/:runId/resume` on the **node type** |
| 10 | +that produced the suspension: an `approval` pause declares |
| 11 | +`resumeAuthority: 'service'`, so it continues only through `ApprovalService`. |
| 12 | +ADR-0044's **revise window** was the same trust boundary in a shape that key |
| 13 | +could not see. Send-back parked the run on an ordinary `wait` node the flow |
| 14 | +author placed — correctly `resumeAuthority: 'any'`, because a signal wait is |
| 15 | +*meant* to be resumable by an external producer — and `ApprovalService.resubmit` |
| 16 | +was the only thing that checked anything about continuing it. |
| 17 | + |
| 18 | +Demonstrated (not reasoned) against the real engine: a raw `resume(runId)` with |
| 19 | +an **empty body**, from any caller, walked the `resubmit` back-edge into the |
| 20 | +approval node and opened round N+1 with **no submitter check and no `resubmit` |
| 21 | +audit row** (`['submit','revise']` — no third row, ever). Worse, when another |
| 22 | +request was already pending on the record — the exact case `resubmit` refuses |
| 23 | +with `DUPLICATE_REQUEST` *specifically to keep the run alive* — the raw resume |
| 24 | +went around that guard: the approval node's re-entry failed **after** the engine |
| 25 | +consumed the suspension, and the run was **permanently destroyed** with its |
| 26 | +round-N request stuck `returned` and no resubmit able to reach it. |
| 27 | + |
| 28 | +The revise pause is therefore its own node type: |
| 29 | + |
| 30 | +- **`approval_revise`** (`APPROVAL_REVISE_NODE_TYPE`), registered by |
| 31 | + `@objectstack/plugin-approvals` alongside the `approval` node, declaring |
| 32 | + `resumeAuthority: 'service'`. It stays a first-class box on the canvas, in the |
| 33 | + run log and in the suspended-run store — only the *reuse* of `wait` was wrong. |
| 34 | + It takes **no config**: the window ends on the submitter's explicit resubmit, |
| 35 | + never on a signal or timer. The `resumeAuthority` gate itself is unchanged. |
| 36 | +- `sendBack` refuses a `revise` edge whose target is not an `approval_revise` |
| 37 | + node, **before any mutation** (like the existing missing-`revise`-edge check), |
| 38 | + so no run can be parked in a window something else can advance. |
| 39 | +- New gating lint `flow-approval-revise-target-not-service-owned` |
| 40 | + (severity `error`, on `os build` / `os validate` / `os lint` and the runtime |
| 41 | + metadata publish gate) rejects the old shape at authoring time. |
| 42 | + |
| 43 | +**Upgrading a flow authored against the original ADR-0044 D3.** One token: |
| 44 | + |
| 45 | +- **FROM:** `{ id: 'wait_revision', type: 'wait', waitEventConfig: { eventType: 'signal', … } }` |
| 46 | +- **TO:** `{ id: 'wait_revision', type: 'approval_revise' }` — drop |
| 47 | + `waitEventConfig` / any `config`; the window has no event to wait on. |
| 48 | + |
| 49 | +Until you do, such a flow keeps registering and running and its approvals stay |
| 50 | +decidable (`approve` / `reject` / `recall` / `reassign` are untouched), but |
| 51 | +**send-back is refused** with a message naming the node and this fix, and |
| 52 | +re-publishing it reports the lint error. A run *already parked* in a legacy |
| 53 | +revise window keeps its recorded node type (a republish never re-types a live |
| 54 | +pause) and is drained by `resubmit` or `recall` as usual. |
| 55 | + |
| 56 | +ADR-0044's 2026-07-28 amendment records the reversal of its D3 and of its |
| 57 | +`Alternatives` rejection of a service-owned revise pause, with the evidence |
| 58 | +above; the implementation section there records what shipped, why the approval |
| 59 | +node does not re-suspend itself instead, and why no ADR-0087 conversion was |
| 60 | +added for the old shape. |
0 commit comments