@pikku/core 0.12.78 → 0.12.80

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 (341) hide show
  1. package/CHANGELOG.md +234 -0
  2. package/dist/dev/hot-reload.js +1 -6
  3. package/dist/ecosystem.d.ts +27 -0
  4. package/dist/ecosystem.js +26 -0
  5. package/dist/errors/error-handler.js +4 -2
  6. package/dist/function/function-runner.js +20 -42
  7. package/dist/function/functions.types.d.ts +9 -5
  8. package/dist/index.d.ts +4 -9
  9. package/dist/index.js +3 -8
  10. package/dist/middleware/auth-cookie.d.ts +0 -4
  11. package/dist/middleware/auth-cookie.js +38 -2
  12. package/dist/middleware/cors.js +1 -0
  13. package/dist/middleware/index.d.ts +1 -1
  14. package/dist/middleware/index.js +1 -1
  15. package/dist/middleware/remote-auth.js +14 -3
  16. package/dist/permissions.d.ts +8 -10
  17. package/dist/permissions.js +0 -11
  18. package/dist/pikku-state.js +12 -5
  19. package/dist/schema.js +35 -1
  20. package/dist/services/ai-run-state-service.d.ts +7 -1
  21. package/dist/services/in-memory-ai-run-state-service.d.ts +1 -1
  22. package/dist/services/in-memory-ai-run-state-service.js +2 -1
  23. package/dist/services/in-memory-workflow-service.d.ts +10 -0
  24. package/dist/services/in-memory-workflow-service.js +25 -0
  25. package/dist/services/local-content-request-handler.d.ts +21 -0
  26. package/dist/services/local-content-request-handler.js +72 -53
  27. package/dist/services/local-content.d.ts +6 -0
  28. package/dist/services/local-content.js +14 -1
  29. package/dist/services/workflow-service.d.ts +4 -2
  30. package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
  31. package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
  32. package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
  33. package/dist/testing/service-tests/ai-storage-service-tests.js +226 -0
  34. package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
  35. package/dist/testing/service-tests/channel-store-tests.js +72 -0
  36. package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
  37. package/dist/testing/service-tests/credential-service-tests.js +109 -0
  38. package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
  39. package/dist/testing/service-tests/deployment-service-tests.js +21 -0
  40. package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
  41. package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
  42. package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
  43. package/dist/testing/service-tests/secret-service-tests.js +80 -0
  44. package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
  45. package/dist/testing/service-tests/session-store-tests.js +43 -0
  46. package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
  47. package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
  48. package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
  49. package/dist/testing/service-tests/workflow-service-tests.js +150 -0
  50. package/dist/testing/service-tests.d.ts +6 -0
  51. package/dist/testing/service-tests.js +26 -791
  52. package/dist/types/core.types.d.ts +11 -1
  53. package/dist/types/state.types.d.ts +1 -1
  54. package/dist/utils/node-host-resolver.d.ts +12 -0
  55. package/dist/utils/node-host-resolver.js +16 -0
  56. package/dist/utils/safe-fetch.d.ts +18 -0
  57. package/dist/utils/safe-fetch.js +167 -29
  58. package/dist/wirings/actor-flow/run-conversation.js +1 -6
  59. package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
  60. package/dist/wirings/ai-agent/agent-rpc.js +53 -0
  61. package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
  62. package/dist/wirings/ai-agent/ai-agent-memory.js +3 -1
  63. package/dist/wirings/ai-agent/ai-agent-prepare.js +3 -9
  64. package/dist/wirings/ai-agent/ai-agent-runner.js +28 -107
  65. package/dist/wirings/ai-agent/ai-agent-stream.js +20 -61
  66. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +56 -0
  67. package/dist/wirings/ai-agent/ai-agent-turn.js +81 -0
  68. package/dist/wirings/ai-agent/ai-agent.types.d.ts +1 -1
  69. package/dist/wirings/ai-agent/voice-input.js +1 -6
  70. package/dist/wirings/ai-agent/voice-output.js +2 -12
  71. package/dist/wirings/channel/channel-common.js +1 -0
  72. package/dist/wirings/channel/channel-handler.js +3 -5
  73. package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
  74. package/dist/wirings/channel/channel-rpc-service.js +0 -8
  75. package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
  76. package/dist/wirings/channel/channel-rpc.types.js +8 -0
  77. package/dist/wirings/channel/channel-runner.d.ts +1 -3
  78. package/dist/wirings/channel/channel-runner.js +16 -8
  79. package/dist/wirings/channel/channel.types.d.ts +2 -0
  80. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  81. package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
  82. package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
  83. package/dist/wirings/cli/cli-runner.js +4 -2
  84. package/dist/wirings/cli/cli.types.d.ts +0 -8
  85. package/dist/wirings/cli/command-parser.js +13 -0
  86. package/dist/wirings/gateway/gateway-runner.js +41 -5
  87. package/dist/wirings/http/http-routes.js +2 -0
  88. package/dist/wirings/http/http-runner.d.ts +0 -10
  89. package/dist/wirings/http/http-runner.js +6 -13
  90. package/dist/wirings/http/http.types.d.ts +0 -10
  91. package/dist/wirings/http/index.d.ts +1 -1
  92. package/dist/wirings/http/index.js +1 -1
  93. package/dist/wirings/mcp/mcp-runner.d.ts +0 -7
  94. package/dist/wirings/mcp/mcp-runner.js +0 -6
  95. package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
  96. package/dist/wirings/rpc/rpc-runner.js +53 -86
  97. package/dist/wirings/rpc/rpc-types.d.ts +3 -0
  98. package/dist/wirings/secret/validate-secret-definitions.js +2 -0
  99. package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
  100. package/dist/wirings/trigger/trigger-runner.js +1 -0
  101. package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
  102. package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
  103. package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
  104. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +29 -2
  105. package/dist/wirings/workflow/feature.js +7 -4
  106. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
  107. package/dist/wirings/workflow/graph/graph-runner.js +8 -7
  108. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
  109. package/dist/wirings/workflow/index.d.ts +7 -2
  110. package/dist/wirings/workflow/index.js +5 -1
  111. package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
  112. package/dist/wirings/workflow/pikku-scenario-service.js +38 -10
  113. package/dist/wirings/workflow/pikku-workflow-service.d.ts +44 -111
  114. package/dist/wirings/workflow/pikku-workflow-service.js +86 -404
  115. package/dist/wirings/workflow/workflow-approval.d.ts +39 -0
  116. package/dist/wirings/workflow/workflow-approval.js +114 -0
  117. package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
  118. package/dist/wirings/workflow/workflow-constants.js +35 -0
  119. package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
  120. package/dist/wirings/workflow/workflow-errors.js +112 -0
  121. package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
  122. package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
  123. package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
  124. package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
  125. package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
  126. package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
  127. package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
  128. package/dist/wirings/workflow/workflow-recovery.js +101 -0
  129. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
  130. package/dist/wirings/workflow/workflow-run-engine.types.js +1 -0
  131. package/dist/wirings/workflow/workflow-run-ownership.d.ts +16 -0
  132. package/dist/wirings/workflow/workflow-run-ownership.js +29 -0
  133. package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
  134. package/dist/wirings/workflow/workflow-suspend.js +33 -0
  135. package/dist/wirings/workflow/workflow.types.d.ts +7 -0
  136. package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
  137. package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
  138. package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
  139. package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
  140. package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
  141. package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
  142. package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
  143. package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
  144. package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
  145. package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
  146. package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
  147. package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
  148. package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
  149. package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
  150. package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
  151. package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
  152. package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
  153. package/knowledge/decisions/internals/index.md +38 -26
  154. package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
  155. package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
  156. package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
  157. package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
  158. package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
  159. package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
  160. package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
  161. package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
  162. package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
  163. package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
  164. package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
  165. package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
  166. package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
  167. package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
  168. package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
  169. package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
  170. package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
  171. package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
  172. package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
  173. package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
  174. package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
  175. package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
  176. package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
  177. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +34 -0
  178. package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
  179. package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
  180. package/knowledge/decisions/security/gateway-handlers-run-through-the-function-runner-gate.md +13 -3
  181. package/knowledge/decisions/security/index.md +7 -0
  182. package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
  183. package/knowledge/questions/index.md +2 -1
  184. package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
  185. package/package.json +14 -3
  186. package/scripts/generate-api-report.d.mts +1 -0
  187. package/scripts/generate-api-report.mjs +89 -0
  188. package/scripts/generate-api-report.mts +218 -0
  189. package/src/api-report.test.ts +69 -0
  190. package/src/column-form.ts +1 -4
  191. package/src/crypto-utils.test.ts +15 -2
  192. package/src/dev/hot-reload.ts +2 -7
  193. package/src/ecosystem.ts +40 -0
  194. package/src/errors/error-handler.ts +5 -3
  195. package/src/errors/error.test.ts +4 -1
  196. package/src/function/function-runner.test.ts +1 -1
  197. package/src/function/function-runner.ts +28 -39
  198. package/src/function/functions.types.ts +15 -9
  199. package/src/handle-error.ts +1 -1
  200. package/src/index.ts +3 -22
  201. package/src/middleware/auth-cookie.test.ts +50 -0
  202. package/src/middleware/auth-cookie.ts +38 -2
  203. package/src/middleware/cors.ts +2 -1
  204. package/src/middleware/index.ts +1 -1
  205. package/src/middleware/remote-auth.test.ts +43 -0
  206. package/src/middleware/remote-auth.ts +17 -3
  207. package/src/middleware-runner.ts +4 -2
  208. package/src/no-any-casts.test.ts +57 -0
  209. package/src/permissions.test.ts +3 -1
  210. package/src/permissions.ts +22 -24
  211. package/src/pikku-state.ts +17 -6
  212. package/src/public-surface.json +578 -0
  213. package/src/public-surface.json.README +25 -0
  214. package/src/public-surface.test.ts +105 -0
  215. package/src/removed-legacy-exports.test.ts +62 -0
  216. package/src/schema.test.ts +78 -0
  217. package/src/schema.ts +36 -1
  218. package/src/services/ai-run-state-service.ts +7 -1
  219. package/src/services/audit-service.ts +2 -2
  220. package/src/services/in-memory-ai-run-state-service.ts +3 -2
  221. package/src/services/in-memory-workflow-service.ts +25 -0
  222. package/src/services/index.ts +1 -4
  223. package/src/services/local-content-request-handler.test.ts +43 -5
  224. package/src/services/local-content-request-handler.ts +103 -74
  225. package/src/services/local-content.ts +15 -1
  226. package/src/services/local-email-service.ts +5 -1
  227. package/src/services/system-role-guard.test.ts +4 -1
  228. package/src/services/workflow-service.ts +9 -2
  229. package/src/side-effects-are-declared.test.ts +84 -0
  230. package/src/source-files-stay-composable.test.ts +41 -0
  231. package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
  232. package/src/testing/service-tests/ai-storage-service-tests.ts +286 -0
  233. package/src/testing/service-tests/channel-store-tests.ts +98 -0
  234. package/src/testing/service-tests/credential-service-tests.ts +143 -0
  235. package/src/testing/service-tests/deployment-service-tests.ts +33 -0
  236. package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
  237. package/src/testing/service-tests/secret-service-tests.ts +105 -0
  238. package/src/testing/service-tests/session-store-tests.ts +62 -0
  239. package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
  240. package/src/testing/service-tests/workflow-service-tests.ts +308 -0
  241. package/src/testing/service-tests.ts +31 -1111
  242. package/src/types/core.types.ts +15 -1
  243. package/src/types/state.types.ts +1 -1
  244. package/src/utils/node-host-resolver.ts +20 -0
  245. package/src/utils/safe-fetch.test.ts +143 -1
  246. package/src/utils/safe-fetch.ts +200 -25
  247. package/src/wirings/actor-flow/run-conversation.ts +1 -6
  248. package/src/wirings/ai-agent/agent-rpc.ts +120 -0
  249. package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
  250. package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
  251. package/src/wirings/ai-agent/ai-agent-memory.ts +8 -1
  252. package/src/wirings/ai-agent/ai-agent-prepare.ts +10 -12
  253. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
  254. package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
  255. package/src/wirings/ai-agent/ai-agent-runner.ts +49 -120
  256. package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
  257. package/src/wirings/ai-agent/ai-agent-stream.ts +36 -63
  258. package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
  259. package/src/wirings/ai-agent/ai-agent-turn.ts +121 -0
  260. package/src/wirings/ai-agent/ai-agent.types.ts +1 -1
  261. package/src/wirings/ai-agent/voice-input.ts +1 -6
  262. package/src/wirings/ai-agent/voice-output.ts +2 -12
  263. package/src/wirings/channel/channel-common.ts +5 -2
  264. package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
  265. package/src/wirings/channel/channel-handler.ts +7 -7
  266. package/src/wirings/channel/channel-rpc-service.ts +0 -14
  267. package/src/wirings/channel/channel-rpc.test.ts +25 -4
  268. package/src/wirings/channel/channel-rpc.types.ts +14 -0
  269. package/src/wirings/channel/channel-runner.ts +33 -20
  270. package/src/wirings/channel/channel.types.ts +2 -0
  271. package/src/wirings/channel/local/local-channel-runner.ts +1 -1
  272. package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
  273. package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
  274. package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
  275. package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
  276. package/src/wirings/cli/cli-runner.ts +7 -4
  277. package/src/wirings/cli/cli.types.ts +0 -39
  278. package/src/wirings/cli/command-parser.test.ts +19 -0
  279. package/src/wirings/cli/command-parser.ts +17 -1
  280. package/src/wirings/gateway/gateway-authorization.test.ts +131 -0
  281. package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
  282. package/src/wirings/gateway/gateway-runner.ts +62 -22
  283. package/src/wirings/http/http-routes.ts +4 -1
  284. package/src/wirings/http/http-runner.ts +11 -26
  285. package/src/wirings/http/http.types.ts +0 -15
  286. package/src/wirings/http/index.ts +1 -7
  287. package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
  288. package/src/wirings/http/web-request.ts +1 -1
  289. package/src/wirings/mcp/mcp-runner.ts +2 -16
  290. package/src/wirings/persona/persona-environments.test.ts +14 -3
  291. package/src/wirings/persona/persona.test.ts +13 -3
  292. package/src/wirings/persona/validate-personas.ts +5 -1
  293. package/src/wirings/queue/queue-runner.ts +1 -1
  294. package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
  295. package/src/wirings/rpc/rpc-runner.ts +58 -136
  296. package/src/wirings/rpc/rpc-types.ts +7 -0
  297. package/src/wirings/rpc/wire-addon.ts +3 -1
  298. package/src/wirings/secret/validate-secret-definitions.test.ts +69 -0
  299. package/src/wirings/secret/validate-secret-definitions.ts +2 -0
  300. package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
  301. package/src/wirings/trigger/trigger-runner.ts +7 -5
  302. package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
  303. package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
  304. package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
  305. package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
  306. package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
  307. package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
  308. package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
  309. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -2
  310. package/src/wirings/workflow/feature.ts +7 -4
  311. package/src/wirings/workflow/graph/graph-node.ts +1 -1
  312. package/src/wirings/workflow/graph/graph-runner.test.ts +4 -2
  313. package/src/wirings/workflow/graph/graph-runner.ts +13 -16
  314. package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
  315. package/src/wirings/workflow/index.ts +12 -4
  316. package/src/wirings/workflow/pikku-scenario-service.ts +54 -25
  317. package/src/wirings/workflow/pikku-workflow-service.test.ts +2 -2
  318. package/src/wirings/workflow/pikku-workflow-service.ts +208 -630
  319. package/src/wirings/workflow/scenario-hooks.test.ts +51 -0
  320. package/src/wirings/workflow/workflow-approval.ts +185 -0
  321. package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
  322. package/src/wirings/workflow/workflow-constants.ts +47 -0
  323. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  324. package/src/wirings/workflow/workflow-dispatch-relay.test.ts +128 -0
  325. package/src/wirings/workflow/workflow-errors.ts +128 -0
  326. package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
  327. package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
  328. package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
  329. package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
  330. package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
  331. package/src/wirings/workflow/workflow-recovery.ts +162 -0
  332. package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
  333. package/src/wirings/workflow/workflow-run-authority.test.ts +215 -0
  334. package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
  335. package/src/wirings/workflow/workflow-run-ownership.ts +36 -0
  336. package/src/wirings/workflow/workflow-suspend.ts +61 -0
  337. package/src/wirings/workflow/workflow.types.ts +7 -0
  338. package/src/wirings-stay-decoupled.test.ts +123 -0
  339. package/tsconfig.json +1 -1
  340. package/tsconfig.tsbuildinfo +1 -1
  341. package/src/internal.ts +0 -10
@@ -0,0 +1,215 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { InMemoryWorkflowService } from '../../services/in-memory-workflow-service.js'
5
+ import { pikkuState } from '../../pikku-state.js'
6
+ import { runWorkflowGraph } from './graph/graph-runner.js'
7
+ import { assertWorkflowRunOwner } from './workflow-run-ownership.js'
8
+ import { WorkflowStepFunctionMismatchError } from './workflow-errors.js'
9
+ import { ForbiddenError } from '../../errors/errors.js'
10
+
11
+ const silentLogger = { error() {}, info() {}, warn() {}, debug() {} }
12
+
13
+ const seedSingletons = () =>
14
+ pikkuState(null, 'package', 'singletonServices', {
15
+ logger: silentLogger,
16
+ // `resumeWorkflow` hands the run back to the orchestrator queue; the tests
17
+ // here assert on what happened before that hand-off.
18
+ queueService: { add: async () => 'job-1' },
19
+ } as any)
20
+
21
+ describe('a graph run starts at an entry node the graph declared', () => {
22
+ const meta = {
23
+ name: 'payout',
24
+ pikkuFuncId: 'payout',
25
+ source: 'graph' as const,
26
+ entryNodeIds: ['validate'],
27
+ graphHash: 'payout-hash',
28
+ nodes: {
29
+ validate: { nodeId: 'validate', rpcName: 'validatePayout' },
30
+ // Reachable only after validation, but its input is a literal — so
31
+ // `areDependenciesSatisfied` alone would happily start the run here.
32
+ transfer: {
33
+ nodeId: 'transfer',
34
+ rpcName: 'transferFunds',
35
+ input: { amount: 1000 },
36
+ },
37
+ },
38
+ }
39
+
40
+ test('a startNode outside entryNodeIds is rejected', async () => {
41
+ seedSingletons()
42
+ const ws = new InMemoryWorkflowService()
43
+
44
+ await assert.rejects(
45
+ runWorkflowGraph(
46
+ ws,
47
+ 'payout',
48
+ {},
49
+ { rpcWithWire: async () => ({}) },
50
+ true,
51
+ 'transfer',
52
+ undefined,
53
+ meta
54
+ ),
55
+ /'transfer' is not an entry node/
56
+ )
57
+ })
58
+
59
+ test('a declared entry node still starts the run', async () => {
60
+ seedSingletons()
61
+ const ws = new InMemoryWorkflowService()
62
+
63
+ const { runId } = await runWorkflowGraph(
64
+ ws,
65
+ 'payout',
66
+ {},
67
+ { rpcWithWire: async () => ({}) },
68
+ true,
69
+ 'validate',
70
+ undefined,
71
+ meta
72
+ )
73
+
74
+ assert.ok(runId, 'the graph ran from the entry node it declared')
75
+ })
76
+ })
77
+
78
+ describe('a step runs the function the workflow dispatched it with', () => {
79
+ test('a queue message naming another function is rejected', async () => {
80
+ seedSingletons()
81
+ const ws = new InMemoryWorkflowService()
82
+ const runId = await ws.createRun('flow', {}, false, 'hash', {
83
+ type: 'queue',
84
+ })
85
+ await ws.insertStepState(runId, 'notify', 'sendReceipt', {})
86
+
87
+ const invoked: string[] = []
88
+ await assert.rejects(
89
+ ws.executeWorkflowStep(runId, 'notify', 'deleteAllUsers', {}, {
90
+ rpcWithWire: async (rpcName: string) => {
91
+ invoked.push(rpcName)
92
+ return {}
93
+ },
94
+ } as any),
95
+ WorkflowStepFunctionMismatchError
96
+ )
97
+ assert.deepEqual(invoked, [], 'nothing was invoked')
98
+
99
+ const stepState = await ws.getStepState(runId, 'notify')
100
+ assert.equal(
101
+ stepState.status,
102
+ 'pending',
103
+ 'the forged message left the step untouched'
104
+ )
105
+ })
106
+
107
+ test('the dispatched function still runs', async () => {
108
+ seedSingletons()
109
+ const ws = new InMemoryWorkflowService()
110
+ const runId = await ws.createRun('flow', {}, false, 'hash', {
111
+ type: 'queue',
112
+ })
113
+ await ws.insertStepState(runId, 'notify', 'sendReceipt', {})
114
+
115
+ const invoked: string[] = []
116
+ await ws.executeWorkflowStep(runId, 'notify', 'sendReceipt', {}, {
117
+ rpcWithWire: async (rpcName: string) => {
118
+ invoked.push(rpcName)
119
+ return {}
120
+ },
121
+ } as any)
122
+
123
+ assert.deepEqual(invoked, ['sendReceipt'])
124
+ })
125
+ })
126
+
127
+ describe('workflow run ownership', () => {
128
+ test('a run started by a session is readable only by that session', () => {
129
+ assert.throws(
130
+ () =>
131
+ assertWorkflowRunOwner(
132
+ { type: 'http', pikkuUserId: 'user-a' },
133
+ {
134
+ userId: 'user-b',
135
+ }
136
+ ),
137
+ ForbiddenError
138
+ )
139
+ assert.throws(
140
+ () =>
141
+ assertWorkflowRunOwner(
142
+ { type: 'http', pikkuUserId: 'user-a' },
143
+ undefined
144
+ ),
145
+ ForbiddenError
146
+ )
147
+ assert.doesNotThrow(() =>
148
+ assertWorkflowRunOwner(
149
+ { type: 'http', pikkuUserId: 'user-a' },
150
+ {
151
+ userId: 'user-a',
152
+ }
153
+ )
154
+ )
155
+ })
156
+
157
+ test('a run with no recorded owner has no ownership to enforce', () => {
158
+ assert.doesNotThrow(() =>
159
+ assertWorkflowRunOwner({ type: 'trigger' }, { userId: 'user-b' })
160
+ )
161
+ })
162
+
163
+ test('approveStep refuses a caller who does not own the run', async () => {
164
+ seedSingletons()
165
+ const ws = new InMemoryWorkflowService()
166
+ const runId = await ws.createRun('flow', {}, false, 'hash', {
167
+ type: 'http',
168
+ pikkuUserId: 'owner',
169
+ })
170
+
171
+ await assert.rejects(
172
+ ws.approveStep(
173
+ runId,
174
+ 'release-funds',
175
+ { ok: true },
176
+ {
177
+ userId: 'attacker',
178
+ }
179
+ ),
180
+ ForbiddenError
181
+ )
182
+
183
+ const state = await ws.getRunState(runId)
184
+ assert.deepEqual(
185
+ Object.keys(state),
186
+ [],
187
+ 'no decision was recorded for the run'
188
+ )
189
+ })
190
+
191
+ test('approveStep accepts the run owner', async () => {
192
+ seedSingletons()
193
+ const ws = new InMemoryWorkflowService()
194
+ const runId = await ws.createRun('flow', {}, false, 'hash', {
195
+ type: 'http',
196
+ pikkuUserId: 'owner',
197
+ })
198
+
199
+ await ws.approveStep(
200
+ runId,
201
+ 'release-funds',
202
+ { ok: true },
203
+ {
204
+ userId: 'owner',
205
+ }
206
+ )
207
+
208
+ const state = await ws.getRunState(runId)
209
+ assert.equal(
210
+ Object.keys(state).length,
211
+ 1,
212
+ 'the owner’s decision was recorded'
213
+ )
214
+ })
215
+ })
@@ -0,0 +1,91 @@
1
+ import type { PikkuRawWire, SerializedError } from '../../types/core.types.js'
2
+ import type {
3
+ CoreWorkflow,
4
+ PikkuWorkflowWire,
5
+ StepState,
6
+ WorkflowRun,
7
+ WorkflowStatus,
8
+ WorkflowStepOptions,
9
+ } from './workflow.types.js'
10
+
11
+ export interface RunLifecycleContext {
12
+ runId: string
13
+ run: WorkflowRun
14
+ workflowMeta: any
15
+ workflow: CoreWorkflow
16
+ wire: PikkuRawWire
17
+ packageName: string | null
18
+ }
19
+
20
+ /**
21
+ * The subset of the workflow service a step executor is allowed to call back
22
+ * into.
23
+ */
24
+ export interface WorkflowRunEngine {
25
+ inlineStep(
26
+ runId: string,
27
+ logicalStepName: string,
28
+ fn: Function,
29
+ stepOptions?: WorkflowStepOptions,
30
+ data?: any,
31
+ funcName?: string
32
+ ): Promise<any>
33
+ updateRunStatus(
34
+ runId: string,
35
+ status: WorkflowStatus,
36
+ output?: any,
37
+ error?: SerializedError
38
+ ): Promise<void>
39
+ onChildWorkflowFailed(run: WorkflowRun, error: unknown): Promise<void>
40
+ verifyStepName(stepName: unknown): void
41
+ }
42
+
43
+ /**
44
+ * Hooks a host may install to decorate runs it did not start. Used by the
45
+ * scenario service to attach its own per-run state.
46
+ */
47
+ export interface WorkflowRunExtension {
48
+ attachRunContext(
49
+ runId: string,
50
+ workflowMeta: any,
51
+ options?: Record<string, any>
52
+ ): Promise<void>
53
+ detachRunContext(runId: string): void
54
+ decorateRunWire(
55
+ wire: PikkuRawWire,
56
+ context: {
57
+ runId: string
58
+ workflowMeta: any
59
+ workflowWire: PikkuWorkflowWire
60
+ }
61
+ ): void
62
+ decorateWorkflowWire(
63
+ workflowWire: PikkuWorkflowWire,
64
+ context: {
65
+ name: string
66
+ runId: string
67
+ rpcService: any
68
+ addonNamespace?: string | null
69
+ }
70
+ ): void
71
+ onBeforeRunFunc(context: RunLifecycleContext): Promise<void>
72
+ onAfterRunFunc(
73
+ context: RunLifecycleContext,
74
+ outcome: 'completed' | 'failed' | 'interrupted',
75
+ failure: unknown
76
+ ): Promise<void>
77
+ }
78
+
79
+ /**
80
+ * Per-run bookkeeping held only for the lifetime of an in-process execution.
81
+ */
82
+ export type RunContext = {
83
+ activeExecutions: number
84
+ inline?: boolean
85
+ ordinals: Map<string, number>
86
+ lastStep?: string
87
+ replay?: {
88
+ steps?: Map<string, StepState>
89
+ run?: WorkflowRun
90
+ }
91
+ }
@@ -0,0 +1,36 @@
1
+ import { ForbiddenError } from '../../errors/errors.js'
2
+ import { addError } from '../../errors/error-handler.js'
3
+ import type { CoreUserSession } from '../../types/core.types.js'
4
+ import type { WorkflowRunWire } from './workflow.types.js'
5
+
6
+ export class WorkflowRunForbiddenError extends ForbiddenError {
7
+ constructor() {
8
+ super('Not authorized to access this workflow run')
9
+ }
10
+ }
11
+ addError(WorkflowRunForbiddenError, {
12
+ status: 403,
13
+ message: 'Not authorized to access this workflow run.',
14
+ })
15
+
16
+ /**
17
+ * A run started through a session records that session's user as its owner, and
18
+ * only that user may read it or answer its approval gates.
19
+ *
20
+ * A run with no recorded owner — started by a trigger, a scheduler, or a route
21
+ * wired without auth — has nobody to compare a caller against, so ownership is
22
+ * not a control that exists for it. Gate those with `auth` or `permissions` on
23
+ * the entrypoint instead.
24
+ */
25
+ export const assertWorkflowRunOwner = (
26
+ wire: WorkflowRunWire | undefined,
27
+ session: CoreUserSession | undefined
28
+ ): void => {
29
+ const owner = wire?.pikkuUserId
30
+ if (!owner) {
31
+ return
32
+ }
33
+ if (!session?.userId || session.userId !== owner) {
34
+ throw new WorkflowRunForbiddenError()
35
+ }
36
+ }
@@ -0,0 +1,61 @@
1
+ import { WorkflowSuspendedException } from './workflow-errors.js'
2
+ import type { ApprovalStore } from './workflow-approval.js'
3
+ import type { StepState } from './workflow.types.js'
4
+
5
+ /** The durable step name a suspension point is recorded under. */
6
+ export const suspendStepNameFor = (reason: string): string =>
7
+ `__workflow_suspend:${reason}`
8
+
9
+ /** What the suspend gate needs from the workflow service. */
10
+ export type SuspendStore = Pick<
11
+ ApprovalStore,
12
+ 'getStepState' | 'insertStepState' | 'setStepRunning' | 'setStepResult'
13
+ >
14
+
15
+ /**
16
+ * Record a suspension point and unwind the run.
17
+ *
18
+ * A suspension that has already succeeded returns instead of throwing, so a
19
+ * replay walks past a gate the run has already passed through.
20
+ */
21
+ export const recordSuspension = async (
22
+ store: SuspendStore,
23
+ runId: string,
24
+ reason: string,
25
+ stepName: string,
26
+ fromStepName: string | undefined
27
+ ): Promise<void> => {
28
+ const insert = () =>
29
+ store.insertStepState(
30
+ runId,
31
+ stepName,
32
+ 'pikkuWorkflowSuspend',
33
+ { reason },
34
+ undefined,
35
+ fromStepName
36
+ )
37
+
38
+ let stepState: StepState
39
+ try {
40
+ stepState = await store.getStepState(runId, stepName)
41
+ } catch {
42
+ stepState = await insert()
43
+ }
44
+ if (!stepState.stepId) {
45
+ stepState = await insert()
46
+ }
47
+
48
+ if (stepState.status === 'succeeded') {
49
+ return
50
+ }
51
+
52
+ if (stepState.status === 'pending') {
53
+ await store.setStepRunning(stepState.stepId)
54
+ }
55
+
56
+ await store.setStepResult(stepState.stepId, {
57
+ reason,
58
+ suspendedAt: new Date().toISOString(),
59
+ })
60
+ throw new WorkflowSuspendedException(runId, reason)
61
+ }
@@ -120,6 +120,13 @@ export interface WorkflowRun {
120
120
  export interface StepState {
121
121
  stepId: string
122
122
  status: StepStatus
123
+ /**
124
+ * The function the workflow dispatched this step with, recorded so a worker
125
+ * can reject a queue message naming anything else. `null` is a step with no
126
+ * function of its own (inline work); `undefined` is a store that never
127
+ * recorded one, and cannot be compared against.
128
+ */
129
+ rpcName?: string | null
123
130
  result?: any
124
131
  error?: SerializedError
125
132
  attemptCount: number
@@ -0,0 +1,123 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { readFileSync, readdirSync } from 'node:fs'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { join, dirname, resolve, relative, sep } from 'node:path'
6
+
7
+ const srcRoot = dirname(fileURLToPath(import.meta.url))
8
+ const wiringsRoot = join(srcRoot, 'wirings')
9
+
10
+ /** Type-only statements are erased, so they couple nothing at runtime. */
11
+ const TYPE_ONLY = /^\s*(?:import|export)\s+type\b/
12
+
13
+ /**
14
+ * Which wire may reach which, at runtime.
15
+ *
16
+ * A wire should be self-contained apart from what is genuinely common, so every
17
+ * entry here is a deliberate crossover point rather than an accident. Adding one
18
+ * is a visible diff — and worth asking whether the dependency could be
19
+ * type-only, which couples nothing at runtime.
20
+ */
21
+ const ALLOWED: Record<string, string[]> = {
22
+ // A channel is an HTTP upgrade, so it builds on the HTTP wire.
23
+ channel: ['http'],
24
+ // `remote` on a wire with no peer — one leaf module, not the channel runtime.
25
+ http: ['channel'],
26
+ cli: ['channel'],
27
+ // An agent streams over a channel and is invoked over rpc.
28
+ 'ai-agent': ['channel', 'rpc'],
29
+ // `wire.rpc.agent` — the facade lives with the agent runtime it delegates to.
30
+ rpc: ['ai-agent'],
31
+ gateway: ['http'],
32
+ mcp: ['rpc'],
33
+ trigger: ['rpc'],
34
+ // A workflow step is dispatched to a queue, invoked over rpc, or slept on.
35
+ workflow: ['queue', 'rpc', 'scheduler'],
36
+ // A virtual user drives a scenario, which is a workflow, as a persona.
37
+ 'virtual-user': ['role', 'workflow'],
38
+ persona: ['virtual-user'],
39
+ }
40
+
41
+ const sourceFiles = (directory: string): string[] =>
42
+ readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
43
+ const entryPath = join(directory, entry.name)
44
+ if (entry.isDirectory()) {
45
+ return entry.name === 'dist' ? [] : sourceFiles(entryPath)
46
+ }
47
+ return entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')
48
+ ? [entryPath]
49
+ : []
50
+ })
51
+
52
+ /** Every wire each wire reaches at runtime, following value imports only. */
53
+ const crossWireEdges = (): Map<string, Set<string>> => {
54
+ const edges = new Map<string, Set<string>>()
55
+
56
+ for (const file of sourceFiles(wiringsRoot)) {
57
+ const from = relative(wiringsRoot, file).split(sep)[0]!
58
+
59
+ for (const line of readFileSync(file, 'utf-8').split('\n')) {
60
+ if (TYPE_ONLY.test(line)) continue
61
+ const specifier = line.match(/\bfrom\s+'(\.[^']+)'/)?.[1]
62
+ if (!specifier) continue
63
+
64
+ const target = resolve(dirname(file), specifier)
65
+ if (!target.startsWith(wiringsRoot + sep)) continue
66
+
67
+ const to = relative(wiringsRoot, target).split(sep)[0]!
68
+ if (to === from) continue
69
+ if (!edges.has(from)) edges.set(from, new Set())
70
+ edges.get(from)!.add(to)
71
+ }
72
+ }
73
+
74
+ return edges
75
+ }
76
+
77
+ describe('wirings stay decoupled', () => {
78
+ test('no wire reaches a wire it is not declared to', () => {
79
+ const undeclared = [...crossWireEdges()]
80
+ .flatMap(([from, tos]) =>
81
+ [...tos]
82
+ .filter((to) => !(ALLOWED[from] ?? []).includes(to))
83
+ .map((to) => `${from} -> ${to}`)
84
+ )
85
+ .sort()
86
+
87
+ assert.deepEqual(
88
+ undeclared,
89
+ [],
90
+ 'a wire gained a runtime dependency on another wire:\n' +
91
+ `${undeclared.join('\n')}\n` +
92
+ 'Add it to ALLOWED only if it is genuinely necessary — a type-only ' +
93
+ 'import couples nothing.'
94
+ )
95
+ })
96
+
97
+ test('every declared crossover still exists', () => {
98
+ const actual = crossWireEdges()
99
+ const stale = Object.entries(ALLOWED)
100
+ .flatMap(([from, tos]) =>
101
+ tos
102
+ .filter((to) => !actual.get(from)?.has(to))
103
+ .map((to) => `${from} -> ${to}`)
104
+ )
105
+ .sort()
106
+
107
+ assert.deepEqual(
108
+ stale,
109
+ [],
110
+ `these crossovers are declared but no longer real — delete them:\n${stale.join('\n')}`
111
+ )
112
+ })
113
+
114
+ test('the walker finds the edges it is meant to', () => {
115
+ // Guards both tests above: a walker that resolved nothing would pass
116
+ // whatever the wirings imported.
117
+ const edges = crossWireEdges()
118
+ assert.ok(
119
+ edges.get('channel')?.has('http'),
120
+ `expected channel -> http; walker found ${edges.size} wires with edges`
121
+ )
122
+ })
123
+ })
package/tsconfig.json CHANGED
@@ -9,6 +9,6 @@
9
9
  "moduleResolution": "node16",
10
10
  "tsBuildInfoFile": null
11
11
  },
12
- "exclude": ["**/*.test.ts", "node_modules", "dist"],
12
+ "exclude": ["**/*.test.ts", "node_modules", "dist", "scripts"],
13
13
  "references": []
14
14
  }