@@ -1138,6 +1138,15 @@ export class AutomationEngine implements IAutomationService {
11381138 * and a module-level flag would report only whichever engine ran first.
11391139 */
11401140 private nodeTypeSealOmissionWarned = false ;
1141+ /**
1142+ * Node types already named by {@link warnIfResumeAuthorityUndeclared}
1143+ * (#5561). Per **instance** and per **type**, for the same reason
1144+ * {@link nodeTypeSealOmissionWarned} is per instance: a hot-reload or a
1145+ * multi-tenant host re-registers the same executor repeatedly, and one
1146+ * omission must read as one finding rather than as a log that grows with
1147+ * uptime.
1148+ */
1149+ private readonly resumeAuthorityOmissionWarned = new Set < string > ( ) ;
11411150 private triggers = new Map < string , FlowTrigger > ( ) ;
11421151 /**
11431152 * Flows currently wired to a trigger, keyed by flow name → the trigger
@@ -1334,11 +1343,60 @@ export class AutomationEngine implements IAutomationService {
13341343 ) ;
13351344 }
13361345 this . actionDescriptors . set ( descriptorType , executor . descriptor ) ;
1346+ this . warnIfResumeAuthorityUndeclared ( executor . descriptor ) ;
13371347 }
13381348
13391349 this . logger . info ( `Node executor registered: ${ executor . type } ` ) ;
13401350 }
13411351
1352+ /**
1353+ * Name a pausing node type that never declared WHO may resume the pauses it
1354+ * creates (#5561, the tracking item ADR-0044's amendment deferred).
1355+ *
1356+ * `resumeAuthority` carries no schema default precisely so this warning can
1357+ * exist: with `.default('any')` an omission parsed into a descriptor
1358+ * byte-identical to an author's explicit `'any'`, so the fact was gone
1359+ * before the engine ever saw the object. Absent now means absent, and a
1360+ * pausing type that leaves it absent is fail-open by omission rather than
1361+ * by decision — #3823 is what that costs (a revise pause standing in a
1362+ * service-owned position inherited `wait`'s legitimate `'any'`, and a raw
1363+ * resume walked past an unrecorded decision).
1364+ *
1365+ * **What it asserts, and why that is safe here.** Only the static fact that
1366+ * THIS descriptor omits the key — a property of the object being registered,
1367+ * fixed at authoring time, which no later registration can contradict. It
1368+ * reads no registry and draws no conclusion from anything being absent from
1369+ * one, so it is not the shape AGENTS.md "Startup registry reads" forbids and
1370+ * needs no seal flag (contrast {@link warnIfNodeTypeVocabularyNeverSealed},
1371+ * which reports a missing CALL for the same reason). Whether the omission
1372+ * *matters* at run time is deliberately not judged: the engine still
1373+ * resolves absent to `'any'` ({@link resolveResumeAuthority}), so nothing
1374+ * about today's behaviour changes.
1375+ *
1376+ * **Blind spot, stated up front:** the trigger is `supportsPause`, itself a
1377+ * declaration no execution path enforces (#5703) — a run pauses because
1378+ * `execute()` returned `suspend: true`. An executor that suspends while
1379+ * leaving `supportsPause` false is therefore fail-open AND silent here.
1380+ * `check:resume-authority-declared` catches this repo's own executors at
1381+ * authoring time; #5703 tracks the runtime half.
1382+ */
1383+ private warnIfResumeAuthorityUndeclared ( descriptor : ActionDescriptor ) : void {
1384+ if ( descriptor . supportsPause !== true ) return ;
1385+ if ( descriptor . resumeAuthority !== undefined ) return ;
1386+ if ( this . resumeAuthorityOmissionWarned . has ( descriptor . type ) ) return ;
1387+ this . resumeAuthorityOmissionWarned . add ( descriptor . type ) ;
1388+ this . logger . warn (
1389+ `[automation] node type '${ descriptor . type } ' declares supportsPause but never declares ` +
1390+ `resumeAuthority, so the #3801 resume gate treats every pause it creates as raw-resumable ` +
1391+ `through the generic route (POST /automation/:name/runs/:runId/resume) — fail-open by omission ` +
1392+ `rather than by decision, which is how #3823 walked past an unrecorded approval decision. ` +
1393+ `Declare it on the descriptor: 'any' if that route IS the intended door (a screen's collected ` +
1394+ `inputs, a signal wait's external producer), or 'service' if resuming is the tail of a decision ` +
1395+ `some service must authorize and record first. Declaring 'any' explicitly silences this and ` +
1396+ `changes no behaviour. Reported once per node type per engine.` ,
1397+ ) ;
1398+ }
1399+
13421400 /**
13431401 * Register a **deprecated alias** of a canonical node type (ADR-0018 M3).
13441402 *
@@ -2704,8 +2762,16 @@ export class AutomationEngine implements IAutomationService {
27042762 * snapshotting at alias-registration time) also keeps it correct whichever
27052763 * order the two register in. No alias of a pausing type exists today; this
27062764 * keeps it from becoming a hole the day one does.
2765+ *
2766+ * The `?? 'any'` is load-bearing in a second way since #5561: with no schema
2767+ * default on `resumeAuthority`, an undeclared descriptor arrives with the key
2768+ * absent and this is the one place that resolves it. It resolves fail-OPEN,
2769+ * exactly as the removed default did — step one of #5561 changed nothing
2770+ * here, it only made the omission audible at registration. Flipping this
2771+ * fallback to `'service'` is the breaking half still tracked on #5561, and
2772+ * it is this single expression.
27072773 */
2708- private resolveResumeAuthority ( nodeType : string ) : ActionDescriptor [ 'resumeAuthority' ] {
2774+ private resolveResumeAuthority ( nodeType : string ) : NonNullable < ActionDescriptor [ 'resumeAuthority' ] > {
27092775 let descriptor = this . actionDescriptors . get ( nodeType ) ;
27102776 for ( let hop = 0 ; descriptor ?. aliasOf && hop < AutomationEngine . MAX_ALIAS_HOPS ; hop ++ ) {
27112777 const canonical = this . actionDescriptors . get ( descriptor . aliasOf ) ;
0 commit comments