@xdelivered/emberflow 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (414) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/bin/commands.test.ts +185 -0
  4. package/bin/commands.ts +306 -0
  5. package/bin/emberflow.mjs +38 -0
  6. package/bin/init.test.ts +165 -0
  7. package/bin/init.ts +297 -0
  8. package/bin/runtime.mjs +105 -0
  9. package/bin/runtime.test.ts +167 -0
  10. package/bin/skillTargets.test.ts +108 -0
  11. package/bin/skillTargets.ts +36 -0
  12. package/bin/skillTemplates.test.ts +37 -0
  13. package/bin/tsconfig.json +23 -0
  14. package/dist/bin/commands.d.ts +24 -0
  15. package/dist/bin/commands.js +252 -0
  16. package/dist/bin/init.d.ts +17 -0
  17. package/dist/bin/init.js +241 -0
  18. package/dist/bin/runtime.d.mts +39 -0
  19. package/dist/bin/runtime.mjs +96 -0
  20. package/dist/bin/skillTargets.d.ts +13 -0
  21. package/dist/bin/skillTargets.js +22 -0
  22. package/dist/server/agents/claudeAdapter.d.ts +8 -0
  23. package/dist/server/agents/claudeAdapter.js +190 -0
  24. package/dist/server/agents/claudeParse.d.ts +2 -0
  25. package/dist/server/agents/claudeParse.js +162 -0
  26. package/dist/server/agents/codexAdapter.d.ts +13 -0
  27. package/dist/server/agents/codexAdapter.js +174 -0
  28. package/dist/server/agents/codexParse.d.ts +2 -0
  29. package/dist/server/agents/codexParse.js +74 -0
  30. package/dist/server/agents/detect.d.ts +23 -0
  31. package/dist/server/agents/detect.js +35 -0
  32. package/dist/server/agents/gitScope.d.ts +24 -0
  33. package/dist/server/agents/gitScope.js +95 -0
  34. package/dist/server/agents/modelRejectionHint.d.ts +8 -0
  35. package/dist/server/agents/modelRejectionHint.js +12 -0
  36. package/dist/server/agents/prompt.d.ts +79 -0
  37. package/dist/server/agents/prompt.js +364 -0
  38. package/dist/server/agents/runManager.d.ts +90 -0
  39. package/dist/server/agents/runManager.js +259 -0
  40. package/dist/server/agents/types.d.ts +15 -0
  41. package/dist/server/agents/types.js +1 -0
  42. package/dist/server/apiStore.d.ts +60 -0
  43. package/dist/server/apiStore.js +249 -0
  44. package/dist/server/auth/enforce.d.ts +1 -0
  45. package/dist/server/auth/enforce.js +6 -0
  46. package/dist/server/auth/types.d.ts +1 -0
  47. package/dist/server/auth/types.js +1 -0
  48. package/dist/server/auth/verifiers.d.ts +1 -0
  49. package/dist/server/auth/verifiers.js +4 -0
  50. package/dist/server/authAttach.d.ts +19 -0
  51. package/dist/server/authAttach.js +79 -0
  52. package/dist/server/cli.d.ts +7 -0
  53. package/dist/server/cli.js +804 -0
  54. package/dist/server/client.d.ts +130 -0
  55. package/dist/server/client.js +247 -0
  56. package/dist/server/environments.d.ts +103 -0
  57. package/dist/server/environments.js +486 -0
  58. package/dist/server/flowStore.d.ts +37 -0
  59. package/dist/server/flowStore.js +117 -0
  60. package/dist/server/httpOperations.d.ts +32 -0
  61. package/dist/server/httpOperations.js +62 -0
  62. package/dist/server/index.d.ts +1 -0
  63. package/dist/server/index.js +1022 -0
  64. package/dist/server/infrastructure.d.ts +51 -0
  65. package/dist/server/infrastructure.js +116 -0
  66. package/dist/server/login.d.ts +21 -0
  67. package/dist/server/login.js +44 -0
  68. package/dist/server/mcp.d.ts +1 -0
  69. package/dist/server/mcp.js +182 -0
  70. package/dist/server/migrateFlows.d.ts +8 -0
  71. package/dist/server/migrateFlows.js +25 -0
  72. package/dist/server/mockHandler.d.ts +15 -0
  73. package/dist/server/mockHandler.js +35 -0
  74. package/dist/server/nodesPayload.d.ts +14 -0
  75. package/dist/server/nodesPayload.js +14 -0
  76. package/dist/server/normalizeFlow.d.ts +21 -0
  77. package/dist/server/normalizeFlow.js +48 -0
  78. package/dist/server/openBrowser.d.ts +8 -0
  79. package/dist/server/openBrowser.js +16 -0
  80. package/dist/server/operationResult.d.ts +12 -0
  81. package/dist/server/operationResult.js +26 -0
  82. package/dist/server/pathGuard.d.ts +1 -0
  83. package/dist/server/pathGuard.js +16 -0
  84. package/dist/server/pathSafety.d.ts +20 -0
  85. package/dist/server/pathSafety.js +35 -0
  86. package/dist/server/projectConfig.d.ts +47 -0
  87. package/dist/server/projectConfig.js +61 -0
  88. package/dist/server/projectMode.d.ts +26 -0
  89. package/dist/server/projectMode.js +44 -0
  90. package/dist/server/redact.d.ts +6 -0
  91. package/dist/server/redact.js +37 -0
  92. package/dist/server/runRegistry.d.ts +84 -0
  93. package/dist/server/runRegistry.js +200 -0
  94. package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
  95. package/dist/server/runtime/RuntimeAdapter.js +1 -0
  96. package/dist/server/runtime/expressAdapter.d.ts +19 -0
  97. package/dist/server/runtime/expressAdapter.js +56 -0
  98. package/dist/server/scenarioTest.d.ts +14 -0
  99. package/dist/server/scenarioTest.js +66 -0
  100. package/dist/server/subflowRunner.d.ts +32 -0
  101. package/dist/server/subflowRunner.js +72 -0
  102. package/dist/server/testRunner.d.ts +70 -0
  103. package/dist/server/testRunner.js +151 -0
  104. package/dist/src/config.d.ts +3 -0
  105. package/dist/src/config.js +2 -0
  106. package/dist/src/engine/authVerify.d.ts +33 -0
  107. package/dist/src/engine/authVerify.js +63 -0
  108. package/dist/src/engine/diagnostics.d.ts +62 -0
  109. package/dist/src/engine/diagnostics.js +148 -0
  110. package/dist/src/engine/executor.d.ts +151 -0
  111. package/dist/src/engine/executor.js +684 -0
  112. package/dist/src/engine/httpError.d.ts +7 -0
  113. package/dist/src/engine/httpError.js +14 -0
  114. package/dist/src/engine/index.d.ts +11 -0
  115. package/dist/src/engine/index.js +11 -0
  116. package/dist/src/engine/output.d.ts +11 -0
  117. package/dist/src/engine/output.js +20 -0
  118. package/dist/src/engine/publish.d.ts +30 -0
  119. package/dist/src/engine/publish.js +95 -0
  120. package/dist/src/engine/registry.d.ts +34 -0
  121. package/dist/src/engine/registry.js +67 -0
  122. package/dist/src/engine/scenarios.d.ts +12 -0
  123. package/dist/src/engine/scenarios.js +19 -0
  124. package/dist/src/engine/schemaCheck.d.ts +4 -0
  125. package/dist/src/engine/schemaCheck.js +34 -0
  126. package/dist/src/engine/trace.d.ts +12 -0
  127. package/dist/src/engine/trace.js +12 -0
  128. package/dist/src/engine/types.d.ts +308 -0
  129. package/dist/src/engine/types.js +1 -0
  130. package/dist/src/engine/validation.d.ts +26 -0
  131. package/dist/src/engine/validation.js +318 -0
  132. package/dist/src/flows/anomaly-flows.d.ts +23 -0
  133. package/dist/src/flows/anomaly-flows.js +151 -0
  134. package/dist/src/flows/city-sweep-flow.d.ts +10 -0
  135. package/dist/src/flows/city-sweep-flow.js +127 -0
  136. package/dist/src/flows/login-flow.d.ts +13 -0
  137. package/dist/src/flows/login-flow.js +145 -0
  138. package/dist/src/flows/pradar-flows.d.ts +5 -0
  139. package/dist/src/flows/pradar-flows.js +375 -0
  140. package/dist/src/flows/weather-flow.d.ts +12 -0
  141. package/dist/src/flows/weather-flow.js +150 -0
  142. package/dist/src/lib/pathParams.d.ts +3 -0
  143. package/dist/src/lib/pathParams.js +6 -0
  144. package/dist/src/nodes/anomaly.d.ts +2 -0
  145. package/dist/src/nodes/anomaly.js +556 -0
  146. package/dist/src/nodes/flow-control.d.ts +7 -0
  147. package/dist/src/nodes/flow-control.js +270 -0
  148. package/dist/src/nodes/index.d.ts +3 -0
  149. package/dist/src/nodes/index.js +20 -0
  150. package/dist/src/nodes/login.d.ts +7 -0
  151. package/dist/src/nodes/login.js +158 -0
  152. package/dist/src/nodes/loops.d.ts +11 -0
  153. package/dist/src/nodes/loops.js +60 -0
  154. package/dist/src/nodes/pradar/index.d.ts +7 -0
  155. package/dist/src/nodes/pradar/index.js +9 -0
  156. package/dist/src/nodes/pradar/logic.d.ts +189 -0
  157. package/dist/src/nodes/pradar/logic.js +370 -0
  158. package/dist/src/nodes/pradar/nodes.d.ts +42 -0
  159. package/dist/src/nodes/pradar/nodes.js +801 -0
  160. package/dist/src/nodes/requireAuth.d.ts +20 -0
  161. package/dist/src/nodes/requireAuth.js +48 -0
  162. package/dist/src/nodes/response.d.ts +8 -0
  163. package/dist/src/nodes/response.js +33 -0
  164. package/dist/src/nodes/weather.d.ts +33 -0
  165. package/dist/src/nodes/weather.js +260 -0
  166. package/package.json +92 -0
  167. package/server/agentRoute.test.ts +157 -0
  168. package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
  169. package/server/agents/__fixtures__/fake-claude.mjs +55 -0
  170. package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
  171. package/server/agents/__fixtures__/fake-codex.mjs +52 -0
  172. package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
  173. package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
  174. package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
  175. package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
  176. package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
  177. package/server/agents/claudeAdapter.test.ts +127 -0
  178. package/server/agents/claudeAdapter.ts +212 -0
  179. package/server/agents/claudeParse.ts +193 -0
  180. package/server/agents/codexAdapter.test.ts +125 -0
  181. package/server/agents/codexAdapter.ts +198 -0
  182. package/server/agents/codexParse.test.ts +58 -0
  183. package/server/agents/codexParse.ts +126 -0
  184. package/server/agents/detect.test.ts +65 -0
  185. package/server/agents/detect.ts +43 -0
  186. package/server/agents/gitScope.test.ts +129 -0
  187. package/server/agents/gitScope.ts +110 -0
  188. package/server/agents/modelRejectionHint.test.ts +30 -0
  189. package/server/agents/modelRejectionHint.ts +14 -0
  190. package/server/agents/prompt.test.ts +613 -0
  191. package/server/agents/prompt.ts +527 -0
  192. package/server/agents/runManager.test.ts +251 -0
  193. package/server/agents/runManager.ts +286 -0
  194. package/server/agents/types.ts +17 -0
  195. package/server/apiMount.test.ts +591 -0
  196. package/server/apiStore.test.ts +324 -0
  197. package/server/apiStore.ts +241 -0
  198. package/server/auth/enforce.test.ts +61 -0
  199. package/server/auth/enforce.ts +6 -0
  200. package/server/auth/types.ts +5 -0
  201. package/server/auth/verifiers.test.ts +37 -0
  202. package/server/auth/verifiers.ts +9 -0
  203. package/server/authAttach.test.ts +175 -0
  204. package/server/authAttach.ts +99 -0
  205. package/server/cli.ts +847 -0
  206. package/server/cliAuth.test.ts +229 -0
  207. package/server/cliCreate.test.ts +131 -0
  208. package/server/cliDoctor.test.ts +326 -0
  209. package/server/cliTest.test.ts +735 -0
  210. package/server/client.ts +314 -0
  211. package/server/demoProject.test.ts +29 -0
  212. package/server/diagnosticsRoute.test.ts +169 -0
  213. package/server/environments.test.ts +620 -0
  214. package/server/environments.ts +603 -0
  215. package/server/environmentsRoute.test.ts +385 -0
  216. package/server/errorWorkflow.test.ts +205 -0
  217. package/server/flowStore.test.ts +84 -0
  218. package/server/flowStore.ts +125 -0
  219. package/server/httpOperations.test.ts +377 -0
  220. package/server/httpOperations.ts +96 -0
  221. package/server/index.ts +1111 -0
  222. package/server/infrastructure.test.ts +154 -0
  223. package/server/infrastructure.ts +161 -0
  224. package/server/infrastructureRoute.test.ts +105 -0
  225. package/server/login.test.ts +153 -0
  226. package/server/login.ts +67 -0
  227. package/server/mcp.ts +278 -0
  228. package/server/migrateFlows.test.ts +55 -0
  229. package/server/migrateFlows.ts +23 -0
  230. package/server/mockHandler.test.ts +138 -0
  231. package/server/mockHandler.ts +52 -0
  232. package/server/mockRun.test.ts +367 -0
  233. package/server/mockServe.test.ts +467 -0
  234. package/server/nodesPayload.test.ts +29 -0
  235. package/server/nodesPayload.ts +26 -0
  236. package/server/normalizeFlow.test.ts +80 -0
  237. package/server/normalizeFlow.ts +59 -0
  238. package/server/openBrowser.test.ts +20 -0
  239. package/server/openBrowser.ts +22 -0
  240. package/server/operationResult.test.ts +67 -0
  241. package/server/operationResult.ts +27 -0
  242. package/server/pathGuard.test.ts +19 -0
  243. package/server/pathGuard.ts +12 -0
  244. package/server/pathSafety.test.ts +38 -0
  245. package/server/pathSafety.ts +33 -0
  246. package/server/projectConfig.test.ts +67 -0
  247. package/server/projectConfig.ts +101 -0
  248. package/server/projectMode.test.ts +106 -0
  249. package/server/projectMode.ts +59 -0
  250. package/server/redact.test.ts +88 -0
  251. package/server/redact.ts +35 -0
  252. package/server/redactEgress.test.ts +281 -0
  253. package/server/runRegistry.ts +270 -0
  254. package/server/runsAuth.test.ts +119 -0
  255. package/server/runtime/RuntimeAdapter.ts +24 -0
  256. package/server/runtime/expressAdapter.test.ts +38 -0
  257. package/server/runtime/expressAdapter.ts +70 -0
  258. package/server/scenarioTest.test.ts +148 -0
  259. package/server/scenarioTest.ts +88 -0
  260. package/server/setupStatus.test.ts +127 -0
  261. package/server/subflowRunner.ts +104 -0
  262. package/server/testRunner.ts +216 -0
  263. package/server/tsconfig.json +20 -0
  264. package/server/workflowTestRoute.test.ts +297 -0
  265. package/src/App.tsx +233 -0
  266. package/src/components/AgentConsole.tsx +461 -0
  267. package/src/components/BranchRulesEditor.tsx +156 -0
  268. package/src/components/CreateModal.tsx +275 -0
  269. package/src/components/Dock.tsx +487 -0
  270. package/src/components/EnvironmentDialog.tsx +569 -0
  271. package/src/components/EnvironmentPicker.tsx +357 -0
  272. package/src/components/EnvironmentsDialog.tsx +144 -0
  273. package/src/components/ExecutionPager.tsx +67 -0
  274. package/src/components/InfraPanel.test.tsx +103 -0
  275. package/src/components/InfraPanel.tsx +171 -0
  276. package/src/components/Inspector.tsx +1182 -0
  277. package/src/components/Json.tsx +50 -0
  278. package/src/components/JsonModal.tsx +52 -0
  279. package/src/components/LogMessage.tsx +61 -0
  280. package/src/components/NodeRunModal.tsx +213 -0
  281. package/src/components/RunLogPanel.tsx +397 -0
  282. package/src/components/RunbookView.tsx +1085 -0
  283. package/src/components/Scenarios.tsx +476 -0
  284. package/src/components/SettingsDialog.tsx +229 -0
  285. package/src/components/Sidebar.tsx +436 -0
  286. package/src/components/StatusBar.tsx +302 -0
  287. package/src/components/Toolbar.tsx +663 -0
  288. package/src/components/WelcomeDialog.test.tsx +107 -0
  289. package/src/components/WelcomeDialog.tsx +412 -0
  290. package/src/components/agentMarkdown.test.tsx +91 -0
  291. package/src/components/agentMarkdown.tsx +158 -0
  292. package/src/components/ui/badge.tsx +31 -0
  293. package/src/components/ui/button.tsx +46 -0
  294. package/src/components/ui/combobox.tsx +114 -0
  295. package/src/components/ui/command.tsx +82 -0
  296. package/src/components/ui/dialog.tsx +73 -0
  297. package/src/components/ui/input.tsx +20 -0
  298. package/src/components/ui/popover.tsx +32 -0
  299. package/src/components/ui/resizable.tsx +44 -0
  300. package/src/components/ui/tabs.tsx +36 -0
  301. package/src/config.ts +4 -0
  302. package/src/engine/authVerify.ts +81 -0
  303. package/src/engine/diagnostics.test.ts +437 -0
  304. package/src/engine/diagnostics.ts +211 -0
  305. package/src/engine/executor.test.ts +1339 -0
  306. package/src/engine/executor.ts +840 -0
  307. package/src/engine/httpError.test.ts +12 -0
  308. package/src/engine/httpError.ts +16 -0
  309. package/src/engine/index.ts +11 -0
  310. package/src/engine/output.ts +21 -0
  311. package/src/engine/publish.test.ts +176 -0
  312. package/src/engine/publish.ts +127 -0
  313. package/src/engine/registry.test.ts +64 -0
  314. package/src/engine/registry.ts +80 -0
  315. package/src/engine/scenarios.test.ts +55 -0
  316. package/src/engine/scenarios.ts +29 -0
  317. package/src/engine/schemaCheck.test.ts +21 -0
  318. package/src/engine/schemaCheck.ts +35 -0
  319. package/src/engine/trace.test.ts +19 -0
  320. package/src/engine/trace.ts +23 -0
  321. package/src/engine/types.test.ts +22 -0
  322. package/src/engine/types.ts +316 -0
  323. package/src/engine/validation.test.ts +314 -0
  324. package/src/engine/validation.ts +354 -0
  325. package/src/flows/anomaly-flows.test.ts +81 -0
  326. package/src/flows/anomaly-flows.ts +202 -0
  327. package/src/flows/city-sweep-flow.ts +131 -0
  328. package/src/flows/flows.test.ts +30 -0
  329. package/src/flows/login-flow.ts +150 -0
  330. package/src/flows/pradar-flows.test.ts +204 -0
  331. package/src/flows/pradar-flows.ts +419 -0
  332. package/src/flows/weather-flow.ts +154 -0
  333. package/src/globals.css +147 -0
  334. package/src/lib/pathParams.test.ts +24 -0
  335. package/src/lib/pathParams.ts +6 -0
  336. package/src/lib/registerLens.test.ts +79 -0
  337. package/src/lib/registerLens.ts +59 -0
  338. package/src/lib/runbookModel.test.ts +61 -0
  339. package/src/lib/runbookModel.ts +290 -0
  340. package/src/lib/runbookProjection.test.ts +417 -0
  341. package/src/lib/runbookProjection.ts +254 -0
  342. package/src/lib/useTailAnchor.ts +53 -0
  343. package/src/lib/utils.ts +7 -0
  344. package/src/main.tsx +13 -0
  345. package/src/nodes/anomaly.test.ts +98 -0
  346. package/src/nodes/anomaly.ts +637 -0
  347. package/src/nodes/effects-scan.test.ts +44 -0
  348. package/src/nodes/flow-control.test.ts +280 -0
  349. package/src/nodes/flow-control.ts +305 -0
  350. package/src/nodes/index.ts +22 -0
  351. package/src/nodes/login.test.ts +81 -0
  352. package/src/nodes/login.ts +186 -0
  353. package/src/nodes/loops.ts +69 -0
  354. package/src/nodes/pradar/index.ts +11 -0
  355. package/src/nodes/pradar/logic.test.ts +288 -0
  356. package/src/nodes/pradar/logic.ts +582 -0
  357. package/src/nodes/pradar/nodes.test.ts +146 -0
  358. package/src/nodes/pradar/nodes.ts +955 -0
  359. package/src/nodes/requireAuth.test.ts +75 -0
  360. package/src/nodes/requireAuth.ts +55 -0
  361. package/src/nodes/response.test.ts +33 -0
  362. package/src/nodes/response.ts +39 -0
  363. package/src/nodes/traceDetail.test.ts +39 -0
  364. package/src/nodes/weather.test.ts +65 -0
  365. package/src/nodes/weather.ts +306 -0
  366. package/src/store/agentClient.test.ts +103 -0
  367. package/src/store/agentClient.ts +131 -0
  368. package/src/store/apiTree.test.ts +17 -0
  369. package/src/store/apiTree.ts +73 -0
  370. package/src/store/builderStore.test.ts +1234 -0
  371. package/src/store/builderStore.ts +1879 -0
  372. package/src/store/infraClient.ts +63 -0
  373. package/src/store/nodeMeta.test.ts +24 -0
  374. package/src/store/nodeMeta.ts +21 -0
  375. package/src/store/persistence.test.ts +15 -0
  376. package/src/store/persistence.ts +54 -0
  377. package/src/store/serverRunner.ts +381 -0
  378. package/src/store/setupClient.ts +57 -0
  379. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  380. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  381. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  382. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  383. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  384. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  385. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  386. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  387. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  388. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  389. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  390. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  391. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  392. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  393. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  394. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  395. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  396. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  397. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  398. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  399. package/studio-dist/assets/index-DNJwW-hM.css +1 -0
  400. package/studio-dist/assets/index-O26dKRjW.js +65 -0
  401. package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  402. package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  403. package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  404. package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  405. package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  406. package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  407. package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  408. package/studio-dist/favicon.svg +1 -0
  409. package/studio-dist/icons.svg +24 -0
  410. package/studio-dist/index.html +14 -0
  411. package/templates/skills/emberflow-basics/SKILL.md +316 -0
  412. package/templates/skills/emberflow-model-process/SKILL.md +209 -0
  413. package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
  414. package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Pure value-based secret redaction. Deep-clones a JSON-safe payload,
3
+ * replacing every occurrence of each secret VALUE (raw and its
4
+ * encodeURIComponent form) inside any string leaf with «secret:KEY».
5
+ */
6
+ export function redactSecrets<T>(payload: T, secrets: Record<string, string>): T {
7
+ const replacements: Array<[string, string]> = [];
8
+ for (const [key, value] of Object.entries(secrets)) {
9
+ if (value.length < 6) continue;
10
+ const token = `«secret:${key}»`;
11
+ const encoded = encodeURIComponent(value);
12
+ const base64 = Buffer.from(value).toString('base64');
13
+ replacements.push([value, token]);
14
+ if (encoded !== value) replacements.push([encoded, token]);
15
+ if (base64 !== value && base64 !== encoded) replacements.push([base64, token]);
16
+ }
17
+ // Longest value first, so an encoded superstring is replaced before the raw substring it contains.
18
+ replacements.sort((a, b) => b[0].length - a[0].length);
19
+
20
+ const redactString = (input: string): string =>
21
+ replacements.reduce((acc, [value, token]) => acc.split(value).join(token), input);
22
+
23
+ const walk = (node: unknown): unknown => {
24
+ if (typeof node === 'string') return redactString(node);
25
+ if (Array.isArray(node)) return node.map(walk);
26
+ if (node !== null && typeof node === 'object') {
27
+ const out: Record<string, unknown> = {};
28
+ for (const [k, v] of Object.entries(node)) out[k] = walk(v);
29
+ return out;
30
+ }
31
+ return node;
32
+ };
33
+
34
+ return walk(payload) as T;
35
+ }
@@ -0,0 +1,281 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import { spawn, type ChildProcess } from 'node:child_process';
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ // Verifies secret values never reach the studio via SSE run events or
8
+ // GET /samples, per Task 2 (egress-choke-point redaction). Boots the actual
9
+ // runner subprocess (server/index.ts) against scratch project dirs, mirroring
10
+ // the harness in runsAuth.test.ts.
11
+
12
+ const PORT = 8130;
13
+ const base = `http://127.0.0.1:${PORT}`;
14
+ const SECRET = 'supersecretvalue123';
15
+
16
+ async function waitHealthy(url: string, tries = 40): Promise<void> {
17
+ for (let i = 0; i < tries; i++) {
18
+ try {
19
+ if ((await fetch(url)).ok) return;
20
+ } catch {
21
+ /* not up yet */
22
+ }
23
+ await new Promise((r) => setTimeout(r, 150));
24
+ }
25
+ throw new Error('runner did not become healthy');
26
+ }
27
+
28
+ function bootRunner(port: number, env: Record<string, string | undefined>): ChildProcess {
29
+ return spawn('npx', ['tsx', 'server/index.ts'], {
30
+ env: { ...process.env, EMBERFLOW_RUNNER_PORT: String(port), ...env },
31
+ stdio: 'ignore',
32
+ });
33
+ }
34
+
35
+ // Single-node flow: an Input node that just echoes whatever input the run
36
+ // was invoked with (including `headers`).
37
+ const echoFlow = {
38
+ id: 'echo-input',
39
+ name: 'Echo Input',
40
+ version: 1,
41
+ createdAt: '2026-01-01T00:00:00Z',
42
+ updatedAt: '2026-01-01T00:00:00Z',
43
+ nodes: [
44
+ {
45
+ id: 'input',
46
+ type: 'Input',
47
+ label: 'Input',
48
+ position: { x: 0, y: 0 },
49
+ config: { fields: [], defaults: {} },
50
+ },
51
+ ],
52
+ edges: [],
53
+ };
54
+
55
+ /** Collects every SSE event emitted for a run until it finishes. */
56
+ async function collectSseEvents(runId: string): Promise<Array<Record<string, unknown>>> {
57
+ const res = await fetch(`${base}/api/runs/${runId}/events`);
58
+ if (!res.body) throw new Error('no SSE body');
59
+ const reader = res.body.getReader();
60
+ const decoder = new TextDecoder();
61
+ let buf = '';
62
+ const events: Array<Record<string, unknown>> = [];
63
+ while (true) {
64
+ const { value, done } = await reader.read();
65
+ if (done) break;
66
+ buf += decoder.decode(value, { stream: true });
67
+ let idx: number;
68
+ while ((idx = buf.indexOf('\n\n')) !== -1) {
69
+ const chunk = buf.slice(0, idx);
70
+ buf = buf.slice(idx + 2);
71
+ const dataLine = chunk.split('\n').find((l) => l.startsWith('data: '));
72
+ if (dataLine) {
73
+ const parsed = JSON.parse(dataLine.slice('data: '.length));
74
+ events.push(parsed);
75
+ if (parsed.type === 'finished') {
76
+ reader.cancel();
77
+ return events;
78
+ }
79
+ }
80
+ }
81
+ }
82
+ return events;
83
+ }
84
+
85
+ describe('secret redaction at egress choke points', () => {
86
+ describe('with a configured auth secret', () => {
87
+ let projectDir: string;
88
+ let proc: ChildProcess;
89
+
90
+ beforeAll(async () => {
91
+ projectDir = mkdtempSync(join(tmpdir(), 'redactegress-'));
92
+ mkdirSync(join(projectDir, 'emberflow', 'apis', 'default'), { recursive: true });
93
+ writeFileSync(join(projectDir, 'emberflow.config.mjs'), 'export default {};\n');
94
+ writeFileSync(
95
+ join(projectDir, 'emberflow.environments.json'),
96
+ JSON.stringify({
97
+ defaultEnvironment: 'local',
98
+ environments: {
99
+ local: {
100
+ vars: {},
101
+ secrets: { sessionCookie: SECRET },
102
+ auth: { attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' } },
103
+ },
104
+ },
105
+ }),
106
+ );
107
+ proc = bootRunner(PORT, { EMBERFLOW_PROJECT: projectDir });
108
+ await waitHealthy(`${base}/healthz`);
109
+ }, 20_000);
110
+
111
+ afterAll(() => {
112
+ proc?.kill();
113
+ if (projectDir) rmSync(projectDir, { recursive: true, force: true });
114
+ });
115
+
116
+ it('never leaks the raw secret value over SSE, and redacts the attached cookie', async () => {
117
+ const createRes = await fetch(`${base}/api/runs`, {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify({ flow: echoFlow, mode: 'run', input: {} }),
121
+ });
122
+ expect(createRes.status).toBe(201);
123
+ const { runId } = await createRes.json();
124
+
125
+ const events = await collectSseEvents(runId);
126
+ expect(events.length).toBeGreaterThan(0);
127
+
128
+ const raw = JSON.stringify(events);
129
+ expect(raw).not.toContain(SECRET);
130
+
131
+ const nodeStateEvents = events.filter(
132
+ (e) => e.type === 'nodeState' && (e as { nodeId?: string }).nodeId === 'input',
133
+ );
134
+ const succeeded = nodeStateEvents.find(
135
+ (e) => (e.state as { status?: string } | undefined)?.status === 'succeeded',
136
+ );
137
+ expect(succeeded).toBeDefined();
138
+ const output = (succeeded!.state as { output?: { headers?: { cookie?: string } } }).output;
139
+ expect(output?.headers?.cookie).toContain('session=«secret:sessionCookie»');
140
+ });
141
+
142
+ it('redacts the sample output returned via GET /samples', async () => {
143
+ const createRes = await fetch(`${base}/api/runs`, {
144
+ method: 'POST',
145
+ headers: { 'Content-Type': 'application/json' },
146
+ body: JSON.stringify({ flow: echoFlow, mode: 'step', input: {} }),
147
+ });
148
+ expect(createRes.status).toBe(201);
149
+ const { runId } = await createRes.json();
150
+
151
+ const stepRes = await fetch(`${base}/api/runs/${runId}/step`, { method: 'POST' });
152
+ expect(stepRes.status).toBe(200);
153
+
154
+ const samplesRes = await fetch(`${base}/api/samples?nodeId=input`);
155
+ expect(samplesRes.status).toBe(200);
156
+ const body = await samplesRes.json();
157
+ const raw = JSON.stringify(body);
158
+ expect(raw).not.toContain(SECRET);
159
+
160
+ const sample = body.samples.find((s: { runId: string }) => s.runId === runId);
161
+ expect(sample).toBeDefined();
162
+ expect(sample.output.headers.cookie).toContain('session=«secret:sessionCookie»');
163
+ });
164
+ });
165
+
166
+ describe('with two environments sharing a secret key (different values)', () => {
167
+ let projectDir: string;
168
+ let proc: ChildProcess;
169
+ const port = 8141;
170
+ const base3 = `http://127.0.0.1:${port}`;
171
+ const DEV_SECRET = 'devsecretvalue-abc123';
172
+ const PROD_SECRET = 'prodsecretvalue-xyz789';
173
+
174
+ beforeAll(async () => {
175
+ projectDir = mkdtempSync(join(tmpdir(), 'redactegress-multienv-'));
176
+ mkdirSync(join(projectDir, 'emberflow', 'apis', 'default'), { recursive: true });
177
+ writeFileSync(join(projectDir, 'emberflow.config.mjs'), 'export default {};\n');
178
+ writeFileSync(
179
+ join(projectDir, 'emberflow.environments.json'),
180
+ JSON.stringify({
181
+ defaultEnvironment: 'dev',
182
+ environments: {
183
+ // Both envs define the SAME key with DIFFERENT values. A merged
184
+ // redaction map keyed by name would keep only prod's value and
185
+ // let dev's pass through /samples raw.
186
+ dev: {
187
+ vars: {},
188
+ secrets: { sessionCookie: DEV_SECRET },
189
+ auth: { attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' } },
190
+ },
191
+ prod: {
192
+ vars: {},
193
+ secrets: { sessionCookie: PROD_SECRET },
194
+ auth: { attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' } },
195
+ },
196
+ },
197
+ }),
198
+ );
199
+ proc = bootRunner(port, { EMBERFLOW_PROJECT: projectDir });
200
+ await waitHealthy(`${base3}/healthz`);
201
+ }, 20_000);
202
+
203
+ afterAll(() => {
204
+ proc?.kill();
205
+ if (projectDir) rmSync(projectDir, { recursive: true, force: true });
206
+ });
207
+
208
+ it('GET /samples leaks NEITHER environment value raw, even on key collision', async () => {
209
+ // Run against dev (the default env): dev's value lands in the sample
210
+ // via the auto-attached cookie.
211
+ const createRes = await fetch(`${base3}/api/runs`, {
212
+ method: 'POST',
213
+ headers: { 'Content-Type': 'application/json' },
214
+ body: JSON.stringify({ flow: echoFlow, mode: 'step', input: {} }),
215
+ });
216
+ expect(createRes.status).toBe(201);
217
+ const { runId } = await createRes.json();
218
+
219
+ const stepRes = await fetch(`${base3}/api/runs/${runId}/step`, { method: 'POST' });
220
+ expect(stepRes.status).toBe(200);
221
+
222
+ const samplesRes = await fetch(`${base3}/api/samples?nodeId=input`);
223
+ expect(samplesRes.status).toBe(200);
224
+ const body = await samplesRes.json();
225
+ const raw = JSON.stringify(body);
226
+ expect(raw).not.toContain(DEV_SECRET);
227
+ expect(raw).not.toContain(PROD_SECRET);
228
+
229
+ const sample = body.samples.find((s: { runId: string }) => s.runId === runId);
230
+ expect(sample).toBeDefined();
231
+ expect(sample.output.headers.cookie).toContain('session=«secret:sessionCookie»');
232
+ });
233
+ });
234
+
235
+ describe('without any secrets configured', () => {
236
+ let projectDir: string;
237
+ let proc: ChildProcess;
238
+ const port = 8140;
239
+ const base2 = `http://127.0.0.1:${port}`;
240
+
241
+ beforeAll(async () => {
242
+ projectDir = mkdtempSync(join(tmpdir(), 'redactegress-nosecrets-'));
243
+ mkdirSync(join(projectDir, 'emberflow', 'apis', 'default'), { recursive: true });
244
+ writeFileSync(join(projectDir, 'emberflow.config.mjs'), 'export default {};\n');
245
+ writeFileSync(
246
+ join(projectDir, 'emberflow.environments.json'),
247
+ JSON.stringify({
248
+ defaultEnvironment: 'local',
249
+ environments: { local: { vars: {}, secrets: {} } },
250
+ }),
251
+ );
252
+ proc = bootRunner(port, { EMBERFLOW_PROJECT: projectDir });
253
+ await waitHealthy(`${base2}/healthz`);
254
+ }, 20_000);
255
+
256
+ afterAll(() => {
257
+ proc?.kill();
258
+ if (projectDir) rmSync(projectDir, { recursive: true, force: true });
259
+ });
260
+
261
+ it('run output is untouched: no redaction tokens appear anywhere', async () => {
262
+ const createRes = await fetch(`${base2}/api/runs`, {
263
+ method: 'POST',
264
+ headers: { 'Content-Type': 'application/json' },
265
+ body: JSON.stringify({ flow: echoFlow, mode: 'step', input: { headers: { 'x-foo': 'bar' } } }),
266
+ });
267
+ expect(createRes.status).toBe(201);
268
+ const { runId } = await createRes.json();
269
+
270
+ const stepRes = await fetch(`${base2}/api/runs/${runId}/step`, { method: 'POST' });
271
+ expect(stepRes.status).toBe(200);
272
+
273
+ const samplesRes = await fetch(`${base2}/api/samples?nodeId=input`);
274
+ const { samples } = await samplesRes.json();
275
+ const sample = samples.find((s: { runId: string }) => s.runId === runId);
276
+ expect(sample).toBeDefined();
277
+ expect(sample.output.headers['x-foo']).toBe('bar');
278
+ expect(JSON.stringify(sample)).not.toContain('«secret:');
279
+ });
280
+ });
281
+ });
@@ -0,0 +1,270 @@
1
+ import type {
2
+ LogLine, NodeExecutionSample, NodeRunState,
3
+ WorkflowDefinition, WorkflowRun,
4
+ } from '../src/engine';
5
+ import { InMemoryTraceSink, startRun, type FlowRun, type NodeRegistry } from '../src/engine';
6
+ import { createDefaultRegistry } from '../src/nodes';
7
+ import { redactSecrets } from './redact';
8
+ import { makeSubflowRunner } from './subflowRunner';
9
+
10
+ /** SSE-shaped events, mirroring ExecutorEvents 1:1. */
11
+ export type RunEvent =
12
+ | { type: 'nodeState'; nodeId: string; state: NodeRunState }
13
+ | { type: 'log'; line: LogLine }
14
+ | {
15
+ type: 'finished';
16
+ run: WorkflowRun;
17
+ /** Present only when this run is an error-handler invocation: the
18
+ * workflowId of the run whose failure fired it. */
19
+ errorHandler?: { firedBy: string };
20
+ };
21
+
22
+ type Listener = (event: RunEvent) => void;
23
+
24
+ interface LiveRun {
25
+ handle: FlowRun;
26
+ buffer: RunEvent[];
27
+ listeners: Set<Listener>;
28
+ finished: boolean;
29
+ cleanupTimer?: ReturnType<typeof setTimeout>;
30
+ }
31
+
32
+ const RETAIN_FINISHED_MS = 10 * 60 * 1000; // 10 minutes
33
+ const MAX_LIVE_RUNS = 100;
34
+
35
+ /** Manages live runs: buffers engine events per run and fans them out to SSE subscribers. */
36
+ export class RunRegistry {
37
+ private readonly registry: NodeRegistry;
38
+ private readonly runs = new Map<string, LiveRun>();
39
+ readonly sink = new InMemoryTraceSink();
40
+
41
+ /**
42
+ * @param loadFlow Resolves a workflow by id for Subflow nodes (server hosts
43
+ * this via flowStore.load). Absent → Subflow nodes fail with a clear message.
44
+ * @param registry Execution registry to run against. Defaults to the
45
+ * built-in node set; project mode passes one preloaded with consumer nodes.
46
+ * @param errorOperation Op id (resolved via `loadFlow`, same as Subflow
47
+ * nodes) to run whenever a run finishes with status 'failed'. Best-effort:
48
+ * never throws, never affects the failed run. Absent → disabled.
49
+ */
50
+ constructor(
51
+ private readonly loadFlow?: (id: string) => WorkflowDefinition | undefined,
52
+ registry: NodeRegistry = createDefaultRegistry(),
53
+ private readonly errorOperation?: string,
54
+ ) {
55
+ this.registry = registry;
56
+ }
57
+
58
+ /** Number of registered node types (for boot logging). */
59
+ get nodeCount(): number {
60
+ return this.registry.list().length;
61
+ }
62
+
63
+ /** The registry runs execute against — used to verify artifact node hashes. */
64
+ get executionRegistry(): NodeRegistry {
65
+ return this.registry;
66
+ }
67
+
68
+ /** Samples recorded for a node across all runs on this registry (newest first, capped). */
69
+ samplesFor(nodeId: string): NodeExecutionSample[] {
70
+ return this.sink.samplesFor(nodeId).slice(0, 100);
71
+ }
72
+
73
+ /** Create a run; wires engine events into a per-run buffer + attached subscribers. */
74
+ create(
75
+ flow: WorkflowDefinition,
76
+ opts: {
77
+ secrets: Record<string, string>;
78
+ vars: Record<string, string>;
79
+ environment: string;
80
+ safeMode: boolean;
81
+ pins?: Record<string, unknown>;
82
+ input?: Record<string, unknown>;
83
+ /** When true, runs in Mock mode: mocked nodes return their canned
84
+ * output, unmocked infra nodes fail loud. See `StartRunOptions.mockRun`. */
85
+ mockRun?: boolean;
86
+ /** nodeId -> canned output, consulted only when `mockRun` is true. */
87
+ mocks?: Record<string, unknown>;
88
+ /** Internal: marks this run as an error-workflow invocation so it never
89
+ * itself triggers `errorOperation` (recursion guard — a flag, not an id
90
+ * comparison, since an error op can legitimately fail on its own). */
91
+ isErrorHandler?: boolean;
92
+ /** Internal: the workflowId of the run that failed and fired this
93
+ * error-handler run — stamped onto this run's `finished` event as
94
+ * `errorHandler: { firedBy }`. Only meaningful alongside isErrorHandler. */
95
+ errorHandlerFiredBy?: string;
96
+ },
97
+ ): { runId: string; handle: FlowRun } {
98
+ this.evictIfNeeded();
99
+
100
+ // Placeholder entry so the ExecutorEvents closures can reach the live run.
101
+ const entry: LiveRun = { handle: undefined as unknown as FlowRun, buffer: [], listeners: new Set(), finished: false };
102
+
103
+ // Redact secret values from every event before it reaches the buffer
104
+ // (replayed to SSE subscribers) or any listener — internal run state
105
+ // (`entry.handle`, `handle.run.nodeStates`) is never routed through
106
+ // `emit` and stays raw.
107
+ const scrub = <T>(e: T): T => redactSecrets(e, opts.secrets ?? {});
108
+
109
+ const emit = (event: RunEvent): void => {
110
+ const scrubbed = scrub(event);
111
+ entry.buffer.push(scrubbed);
112
+ for (const listener of entry.listeners) listener(scrubbed);
113
+ };
114
+
115
+ // Subflow runner (shared factory — see server/subflowRunner.ts): resolves
116
+ // the child via loadFlow, runs it to completion on the same registry
117
+ // sharing the parent's environment, and forwards its (prefixed) logs into
118
+ // this run's SSE stream via `onLog`.
119
+ const subflowRunner = makeSubflowRunner(
120
+ {
121
+ loadFlow: (id) => this.loadFlow?.(id),
122
+ registry: this.registry,
123
+ secrets: opts.secrets,
124
+ vars: opts.vars,
125
+ environment: opts.environment,
126
+ safeMode: opts.safeMode,
127
+ mockRun: opts.mockRun,
128
+ trace: this.sink,
129
+ onLog: (line) => emit({ type: 'log', line }),
130
+ },
131
+ [flow.id],
132
+ );
133
+
134
+ const handle = startRun({
135
+ flow,
136
+ registry: this.registry,
137
+ secrets: opts.secrets,
138
+ vars: opts.vars,
139
+ environment: opts.environment,
140
+ safeMode: opts.safeMode,
141
+ pins: opts.pins,
142
+ input: opts.input,
143
+ mockRun: opts.mockRun,
144
+ mocks: opts.mocks,
145
+ trace: this.sink,
146
+ subflowRunner,
147
+ events: {
148
+ onNodeStateChange: (nodeId, state) => emit({ type: 'nodeState', nodeId, state }),
149
+ onLog: (line) => emit({ type: 'log', line }),
150
+ onRunFinished: (run) => {
151
+ emit({
152
+ type: 'finished',
153
+ run,
154
+ ...(opts.isErrorHandler && opts.errorHandlerFiredBy
155
+ ? { errorHandler: { firedBy: opts.errorHandlerFiredBy } }
156
+ : {}),
157
+ });
158
+ entry.finished = true;
159
+ this.scheduleCleanup(run.id);
160
+ if (run.status === 'failed' && !opts.isErrorHandler) this.fireErrorWorkflow(run, flow, opts);
161
+ },
162
+ },
163
+ });
164
+
165
+ entry.handle = handle;
166
+ this.runs.set(handle.run.id, entry);
167
+ return { runId: handle.run.id, handle };
168
+ }
169
+
170
+ /**
171
+ * Best-effort: fires `errorOperation` (resolved via the same `loadFlow`
172
+ * callback Subflow nodes use) as a new run when `failedRun` finished with
173
+ * status 'failed'. Never throws and never affects the failed run — any
174
+ * problem (unset config, unknown op id, the error op itself failing to
175
+ * start) is logged and swallowed.
176
+ */
177
+ private fireErrorWorkflow(
178
+ failedRun: WorkflowRun,
179
+ failedFlow: WorkflowDefinition,
180
+ opts: {
181
+ secrets: Record<string, string>;
182
+ vars: Record<string, string>;
183
+ environment: string;
184
+ safeMode: boolean;
185
+ /** A failed MOCK run's error op must also run mocked — otherwise the
186
+ * most common mock failure (the fail-loud infra boundary) would fire
187
+ * an error op that touches real infrastructure. */
188
+ mockRun?: boolean;
189
+ },
190
+ ): void {
191
+ if (!this.errorOperation) return;
192
+ try {
193
+ const errorFlow = this.loadFlow?.(this.errorOperation);
194
+ if (!errorFlow) {
195
+ console.error(`[error-workflow] unknown errorOperation "${this.errorOperation}"`);
196
+ return;
197
+ }
198
+ // Report the failure that HALTED the run: an `optional` (fail-soft) node
199
+ // can fail earlier without aborting, so prefer the first failed state
200
+ // whose node is not optional. Fall back to the first failed state only
201
+ // when every failed node is optional.
202
+ const failedEntries = Object.entries(failedRun.nodeStates).filter(([, s]) => s.status === 'failed');
203
+ const failedEntry =
204
+ failedEntries.find(([id]) => !failedFlow.nodes.find((n) => n.id === id)?.optional)
205
+ ?? failedEntries[0];
206
+ const failedNodeId = failedEntry?.[0];
207
+ const error = failedEntry?.[1] && 'error' in failedEntry[1] ? failedEntry[1].error : undefined;
208
+ const { handle } = this.create(errorFlow, {
209
+ secrets: opts.secrets,
210
+ vars: opts.vars,
211
+ environment: opts.environment,
212
+ safeMode: opts.safeMode,
213
+ // Mock mode propagates with the ERROR op's own op-level mocks
214
+ // (nodeIds are flow-scoped — the failed run's map doesn't apply).
215
+ mockRun: opts.mockRun,
216
+ mocks: (errorFlow as { mocks?: Record<string, unknown> }).mocks ?? {},
217
+ input: {
218
+ failedRunId: failedRun.id,
219
+ failedWorkflowId: failedRun.workflowId,
220
+ failedNodeId,
221
+ error,
222
+ environment: opts.environment,
223
+ },
224
+ isErrorHandler: true,
225
+ errorHandlerFiredBy: failedRun.workflowId,
226
+ });
227
+ handle.runToEnd().catch((err) => {
228
+ console.error('[error-workflow] error op run failed', err instanceof Error ? err.message : String(err));
229
+ });
230
+ } catch (err) {
231
+ console.error('[error-workflow] failed to fire error operation', err instanceof Error ? err.message : String(err));
232
+ }
233
+ }
234
+
235
+ get(runId: string): FlowRun | undefined {
236
+ return this.runs.get(runId)?.handle;
237
+ }
238
+
239
+ /** Replay the buffer to the listener, then stream live events. Returns an unsubscribe fn. */
240
+ subscribe(runId: string, listener: Listener): (() => void) | undefined {
241
+ const entry = this.runs.get(runId);
242
+ if (!entry) return undefined;
243
+ for (const event of entry.buffer) listener(event);
244
+ // A run that already finished has its full buffer replayed above; no live events follow.
245
+ entry.listeners.add(listener);
246
+ return () => entry.listeners.delete(listener);
247
+ }
248
+
249
+ private scheduleCleanup(runId: string): void {
250
+ const entry = this.runs.get(runId);
251
+ if (!entry) return;
252
+ if (entry.cleanupTimer) clearTimeout(entry.cleanupTimer);
253
+ entry.cleanupTimer = setTimeout(() => this.runs.delete(runId), RETAIN_FINISHED_MS);
254
+ }
255
+
256
+ /** Cap live entries: drop the oldest finished run, or the oldest run outright. */
257
+ private evictIfNeeded(): void {
258
+ if (this.runs.size < MAX_LIVE_RUNS) return;
259
+ let victim: string | undefined;
260
+ for (const [id, entry] of this.runs) {
261
+ if (entry.finished) { victim = id; break; }
262
+ }
263
+ if (!victim) victim = this.runs.keys().next().value;
264
+ if (victim) {
265
+ const entry = this.runs.get(victim);
266
+ if (entry?.cleanupTimer) clearTimeout(entry.cleanupTimer);
267
+ this.runs.delete(victim);
268
+ }
269
+ }
270
+ }