@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
@@ -77,89 +77,122 @@ const text = (status: number, body: string) =>
77
77
  headers: { 'content-type': 'text/plain; charset=utf-8' },
78
78
  })
79
79
 
80
- export const createLocalContentRequestHandler = ({
81
- content,
82
- logger,
83
- getJWT,
84
- }: LocalContentRequestHandlerOptions): LocalContentRequestHandler => {
85
- // Logged at most once. An unverifiable request is attacker-triggerable, so
86
- // this reports a startup misconfiguration rather than per-request news.
87
- let loggedMissingJWT = false
80
+ export type SignedContentVerification =
81
+ | { ok: true }
82
+ | { ok: false; status: number; body: string }
88
83
 
89
- const validateSignedAssetRequest = async (
90
- requestUrl: URL
91
- ): Promise<{ ok: true } | { ok: false; status: number; body: string }> => {
92
- const signedAtValue = requestUrl.searchParams.get('signedAt')
93
- const expiresAtValue = requestUrl.searchParams.get('expiresAt')
94
- const notBeforeValue = requestUrl.searchParams.get('notBefore')
95
- const signature = requestUrl.searchParams.get('signature')
96
-
97
- if (!signedAtValue || !expiresAtValue) {
98
- return { ok: false, status: 403, body: 'Signed URL required' }
99
- }
84
+ /**
85
+ * Verifies a signed content URL — the one function that decides whether a
86
+ * signed read or a signed upload is genuine.
87
+ *
88
+ * Exported and shared so the runtimes that serve content (the core handler here,
89
+ * the node server, the express adapter) verify identically. They used to each
90
+ * carry their own copy, which is how the express upload path ended up with no
91
+ * check at all: one copy can rot or be forgotten in isolation.
92
+ *
93
+ * `onMissingJWT` is called at most where the caller wants to log the
94
+ * misconfiguration once; a signed request with no verifier is treated as
95
+ * invalid, never as trusted.
96
+ */
97
+ export const verifySignedContentRequest = async (
98
+ requestUrl: URL,
99
+ jwt: JWTService | undefined,
100
+ onMissingJWT?: () => void
101
+ ): Promise<SignedContentVerification> => {
102
+ const signedAtValue = requestUrl.searchParams.get('signedAt')
103
+ const expiresAtValue = requestUrl.searchParams.get('expiresAt')
104
+ const notBeforeValue = requestUrl.searchParams.get('notBefore')
105
+ const signature = requestUrl.searchParams.get('signature')
100
106
 
101
- const signedAt = Number(signedAtValue)
102
- const expiresAt = Number(expiresAtValue)
103
- const notBefore =
104
- notBeforeValue == null ? undefined : Number(notBeforeValue)
107
+ if (!signedAtValue || !expiresAtValue) {
108
+ return { ok: false, status: 403, body: 'Signed URL required' }
109
+ }
105
110
 
106
- if (
107
- !Number.isFinite(signedAt) ||
108
- !Number.isFinite(expiresAt) ||
109
- (notBefore != null && !Number.isFinite(notBefore))
110
- ) {
111
- return { ok: false, status: 403, body: 'Invalid signed URL' }
112
- }
111
+ const signedAt = Number(signedAtValue)
112
+ const expiresAt = Number(expiresAtValue)
113
+ const notBefore = notBeforeValue == null ? undefined : Number(notBeforeValue)
113
114
 
114
- const now = Date.now()
115
- if (now > expiresAt || (notBefore != null && now < notBefore)) {
116
- return { ok: false, status: 403, body: 'Signed URL expired' }
117
- }
115
+ if (
116
+ !Number.isFinite(signedAt) ||
117
+ !Number.isFinite(expiresAt) ||
118
+ (notBefore != null && !Number.isFinite(notBefore))
119
+ ) {
120
+ return { ok: false, status: 403, body: 'Invalid signed URL' }
121
+ }
118
122
 
119
- const jwt = getJWT()
120
- if (!jwt) {
121
- if (!loggedMissingJWT) {
122
- loggedMissingJWT = true
123
- logger.error(
124
- 'pikku: refusing signed asset reads — no JWTService is available to verify them. Pass `contentSigningJWT` (the same service LocalContent signs with) or expose it as `singletonServices.jwt`.'
125
- )
126
- }
127
- return { ok: false, status: 403, body: 'Invalid signed URL' }
128
- }
123
+ const now = Date.now()
124
+ if (now > expiresAt || (notBefore != null && now < notBefore)) {
125
+ return { ok: false, status: 403, body: 'Signed URL expired' }
126
+ }
129
127
 
130
- if (!signature) {
131
- return { ok: false, status: 403, body: 'Signed URL signature required' }
132
- }
128
+ if (!jwt) {
129
+ onMissingJWT?.()
130
+ return { ok: false, status: 403, body: 'Invalid signed URL' }
131
+ }
133
132
 
134
- try {
135
- const payload = await jwt.decode<{
136
- signedAt?: number
137
- expiresAt?: number
138
- notBefore?: number
139
- path?: string
140
- }>(signature)
141
-
142
- // Every claim is compared, the path included: without it a signature
143
- // minted for one asset would read any other.
144
- if (
145
- payload.signedAt !== signedAt ||
146
- payload.expiresAt !== expiresAt ||
147
- payload.notBefore !== notBefore ||
148
- payload.path !== signedContentPath(requestUrl.pathname)
149
- ) {
150
- return { ok: false, status: 403, body: 'Invalid signed URL' }
151
- }
152
- } catch {
133
+ if (!signature) {
134
+ return { ok: false, status: 403, body: 'Signed URL signature required' }
135
+ }
136
+
137
+ try {
138
+ const payload = await jwt.decode<{
139
+ signedAt?: number
140
+ expiresAt?: number
141
+ notBefore?: number
142
+ path?: string
143
+ }>(signature)
144
+
145
+ // Every claim is compared, the path included: without it a signature
146
+ // minted for one asset would read any other.
147
+ if (
148
+ payload.signedAt !== signedAt ||
149
+ payload.expiresAt !== expiresAt ||
150
+ payload.notBefore !== notBefore ||
151
+ payload.path !== signedContentPath(requestUrl.pathname)
152
+ ) {
153
153
  return { ok: false, status: 403, body: 'Invalid signed URL' }
154
154
  }
155
-
156
- return { ok: true }
155
+ } catch {
156
+ return { ok: false, status: 403, body: 'Invalid signed URL' }
157
157
  }
158
158
 
159
+ return { ok: true }
160
+ }
161
+
162
+ export const createLocalContentRequestHandler = ({
163
+ content,
164
+ logger,
165
+ getJWT,
166
+ }: LocalContentRequestHandlerOptions): LocalContentRequestHandler => {
167
+ // Logged at most once. An unverifiable request is attacker-triggerable, so
168
+ // this reports a startup misconfiguration rather than per-request news.
169
+ let loggedMissingJWT = false
170
+
171
+ const validateSignedAssetRequest = (
172
+ requestUrl: URL
173
+ ): Promise<SignedContentVerification> =>
174
+ verifySignedContentRequest(requestUrl, getJWT(), () => {
175
+ if (loggedMissingJWT) return
176
+ loggedMissingJWT = true
177
+ logger.error(
178
+ 'pikku: refusing signed asset reads — no JWTService is available to verify them. Pass `contentSigningJWT` (the same service LocalContent signs with) or expose it as `singletonServices.jwt`.'
179
+ )
180
+ })
181
+
159
182
  const handleUpload = async (
160
183
  request: Request,
184
+ requestUrl: URL,
161
185
  pathname: string
162
186
  ): Promise<Response> => {
187
+ // Uploads are presigned exactly like reads: getUploadURL mints a signed URL
188
+ // and this verifies it before writing. Without this an unauthenticated PUT
189
+ // could write arbitrary bytes to any key under the content root — the write
190
+ // side had none of the signature checking the read side has always had.
191
+ const signed = await validateSignedAssetRequest(requestUrl)
192
+ if (!signed.ok) {
193
+ return text(signed.status, signed.body)
194
+ }
195
+
163
196
  const key = contentKey(pathname, content.uploadUrlPrefix)
164
197
  const targetPath = toTargetPath(content.localFileUploadPath, key)
165
198
  if (!targetPath) {
@@ -168,11 +201,7 @@ export const createLocalContentRequestHandler = ({
168
201
 
169
202
  const maxBytes = parseSizeLimit(content.sizeLimit ?? '1mb')
170
203
 
171
- // Counted as it arrives and abandoned the moment it goes over, so an
172
- // oversized upload costs the limit rather than its own size — `arrayBuffer()`
173
- // would have to hold all of it first, which hands an unauthenticated caller
174
- // a way to spend the server's memory. Mirrors node-http-server's
175
- // `readRequestBody`, which aborts the same way.
204
+ // knowledge: decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md
176
205
  const chunks: Buffer[] = []
177
206
  let bytesRead = 0
178
207
  const reader = request.body?.getReader()
@@ -252,7 +281,7 @@ export const createLocalContentRequestHandler = ({
252
281
  request.method === 'PUT' &&
253
282
  matchesPrefix(pathname, content.uploadUrlPrefix)
254
283
  ) {
255
- return handleUpload(request, pathname)
284
+ return handleUpload(request, requestUrl, pathname)
256
285
  }
257
286
 
258
287
  if (
@@ -48,6 +48,9 @@ export const signedContentPath = (urlOrPath: string): string => {
48
48
  }
49
49
  }
50
50
 
51
+ /** How long a presigned upload URL stays valid. Short, like an S3 PUT URL. */
52
+ const UPLOAD_URL_TTL_MS = 15 * 60 * 1000
53
+
51
54
  export class LocalContent implements ContentService {
52
55
  constructor(
53
56
  private config: LocalContentConfig,
@@ -137,13 +140,24 @@ export class LocalContent implements ContentService {
137
140
  })
138
141
  }
139
142
 
143
+ /**
144
+ * Presigned upload, mirroring the read side: the URL carries a short-lived,
145
+ * path-bound signature that {@link createLocalContentRequestHandler} verifies
146
+ * before writing. An unsigned upload URL cannot be verified, and the handler
147
+ * now refuses one — so this must sign, or every upload 403s.
148
+ */
140
149
  public async getUploadURL(args: GetUploadURLArgs): Promise<UploadURLResult> {
141
150
  void args.visibility
142
151
  const fullKey = this.joinKey(args.bucket, args.fileKey)
143
152
  this.logger.debug(`Going to upload with key: ${fullKey}`)
153
+ const uploadUrl = await this.signURL({
154
+ url: `${this.config.uploadUrlPrefix}/${fullKey}`,
155
+ dateLessThan: new Date(Date.now() + UPLOAD_URL_TTL_MS),
156
+ })
144
157
  return {
145
- uploadUrl: `${this.config.uploadUrlPrefix}/${fullKey}`,
158
+ uploadUrl,
146
159
  assetKey: fullKey,
160
+ uploadMethod: 'PUT',
147
161
  }
148
162
  }
149
163
 
@@ -1,4 +1,8 @@
1
- import type { EmailService, SendEmailInput, SendEmailResult } from './email-service.js'
1
+ import type {
2
+ EmailService,
3
+ SendEmailInput,
4
+ SendEmailResult,
5
+ } from './email-service.js'
2
6
 
3
7
  export class LocalEmailService implements EmailService {
4
8
  async send(input: SendEmailInput): Promise<SendEmailResult> {
@@ -11,7 +11,10 @@ import {
11
11
  SystemRoleShadowedError,
12
12
  } from '../errors/errors.js'
13
13
 
14
- const system = (...names: string[]) => (name: string) => names.includes(name)
14
+ const system =
15
+ (...names: string[]) =>
16
+ (name: string) =>
17
+ names.includes(name)
15
18
 
16
19
  describe('mutating a role', () => {
17
20
  test('a declared role refuses to be deleted or re-scoped', async () => {
@@ -1,4 +1,4 @@
1
- import type { SerializedError } from '../types/core.types.js'
1
+ import type { CoreUserSession, SerializedError } from '../types/core.types.js'
2
2
  import type {
3
3
  WorkflowRun,
4
4
  WorkflowPlannedStep,
@@ -47,8 +47,15 @@ export interface WorkflowService {
47
47
  /**
48
48
  * The payload is stored as given and validated on replay inside the workflow
49
49
  * body, so an invalid one re-closes the gate rather than failing the run.
50
+ *
51
+ * The session must belong to the run's owner — see `assertWorkflowRunOwner`.
50
52
  */
51
- approveStep(runId: string, reason: string, decision: unknown): Promise<void>
53
+ approveStep(
54
+ runId: string,
55
+ reason: string,
56
+ decision: unknown,
57
+ session?: CoreUserSession
58
+ ): Promise<void>
52
59
  startWorkflow<I>(
53
60
  name: string,
54
61
  input: I,
@@ -0,0 +1,84 @@
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, relative } from 'node:path'
6
+
7
+ const srcRoot = dirname(fileURLToPath(import.meta.url))
8
+ const packageRoot = join(srcRoot, '..')
9
+
10
+ /**
11
+ * A statement that runs when the module is imported, rather than when
12
+ * something in it is called. Today every one is an `addError(...)` registering
13
+ * an error class in the runtime registry.
14
+ */
15
+ const TOP_LEVEL_CALL = /^[a-zA-Z_$][\w.$]*\(/
16
+
17
+ const collectSourceFiles = (directory: string): string[] =>
18
+ readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
19
+ const entryPath = join(directory, entry.name)
20
+ if (entry.isDirectory()) {
21
+ return entry.name === 'dist' ? [] : collectSourceFiles(entryPath)
22
+ }
23
+ return entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')
24
+ ? [entryPath]
25
+ : []
26
+ })
27
+
28
+ /** Source modules that do something merely by being imported. */
29
+ const modulesWithSideEffects = (): string[] =>
30
+ collectSourceFiles(srcRoot)
31
+ .filter((file) =>
32
+ readFileSync(file, 'utf-8')
33
+ .split('\n')
34
+ .some((line) => TOP_LEVEL_CALL.test(line))
35
+ )
36
+ .map((file) => `./dist/${relative(srcRoot, file).replace(/\.ts$/, '.js')}`)
37
+ .sort()
38
+
39
+ const declared = (): string[] => {
40
+ const pkg = JSON.parse(
41
+ readFileSync(join(packageRoot, 'package.json'), 'utf-8')
42
+ )
43
+ return [...(pkg.sideEffects ?? [])].sort()
44
+ }
45
+
46
+ /**
47
+ * `sideEffects` tells a bundler which modules it may not drop. Claiming
48
+ * `false` outright would be untrue — the error registry is built by
49
+ * `addError(...)` calls that run on import, and a bundler that dropped
50
+ * `errors/errors.js` would leave `getErrorResponse` unable to find any of
51
+ * them. Naming the modules exactly lets everything else be tree-shaken.
52
+ *
53
+ * knowledge: decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md
54
+ */
55
+ describe('every module with import-time side effects is declared', () => {
56
+ test('the allowlist matches the modules that actually have them', () => {
57
+ const actual = modulesWithSideEffects()
58
+ const listed = declared()
59
+
60
+ assert.deepEqual(
61
+ { missing: actual.filter((m) => !listed.includes(m)) },
62
+ { missing: [] },
63
+ 'a module runs code on import but is not in package.json sideEffects. ' +
64
+ 'A bundler may drop it, and whatever it registered will be absent at ' +
65
+ 'runtime.'
66
+ )
67
+
68
+ assert.deepEqual(
69
+ { stale: listed.filter((m) => !actual.includes(m)) },
70
+ { stale: [] },
71
+ 'package.json sideEffects names a module that no longer has any — ' +
72
+ 'remove it so bundlers can tree-shake it.'
73
+ )
74
+ })
75
+
76
+ test('the detector finds a known side effect', () => {
77
+ // Guards the test above: a detector matching nothing would pass whatever
78
+ // package.json claimed.
79
+ assert.ok(
80
+ modulesWithSideEffects().includes('./dist/errors/errors.js'),
81
+ 'errors/errors.js registers every error class at import and must match'
82
+ )
83
+ })
84
+ })
@@ -0,0 +1,41 @@
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, relative } from 'node:path'
6
+
7
+ const srcRoot = dirname(fileURLToPath(import.meta.url))
8
+
9
+ /**
10
+ * The ceiling a single module may reach before it has to be composed out of
11
+ * smaller ones.
12
+ */
13
+ const MAX_LINES = 2000
14
+
15
+ const collectSourceFiles = (directory: string): string[] =>
16
+ readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
17
+ const entryPath = join(directory, entry.name)
18
+ if (entry.isDirectory()) {
19
+ return entry.name === 'dist' ? [] : collectSourceFiles(entryPath)
20
+ }
21
+ return entry.name.endsWith('.ts') ? [entryPath] : []
22
+ })
23
+
24
+ describe('source files stay composable', () => {
25
+ test(`no module in @pikku/core exceeds ${MAX_LINES} lines`, () => {
26
+ const offenders = collectSourceFiles(srcRoot)
27
+ .map((file) => ({
28
+ file: relative(srcRoot, file),
29
+ lines: readFileSync(file, 'utf-8').split('\n').length,
30
+ }))
31
+ .filter(({ lines }) => lines > MAX_LINES)
32
+ .sort((a, b) => b.lines - a.lines)
33
+
34
+ assert.deepEqual(
35
+ offenders,
36
+ [],
37
+ `modules over ${MAX_LINES} lines:\n` +
38
+ offenders.map(({ file, lines }) => ` ${lines} ${file}`).join('\n')
39
+ )
40
+ })
41
+ })
@@ -0,0 +1,98 @@
1
+ import { describe, test, before } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import type { AgentRunService } from '../../wirings/ai-agent/ai-agent.types.js'
5
+ import type { ServiceTestConfig } from '../service-tests.js'
6
+
7
+ /** Conformance suite for `agentRunService`. Runs only when a backend supplies one. */
8
+ export const defineAgentRunServiceTests = (
9
+ name: string,
10
+ agentRunService: NonNullable<
11
+ ServiceTestConfig['services']['agentRunService']
12
+ >,
13
+ /**
14
+ * Thread-ownership is a property of the two services together — the run
15
+ * service lists what the storage service owns — so that half of the suite
16
+ * only runs when a backend supplies both. Passed explicitly rather than
17
+ * closed over, so the dependency is visible in the signature.
18
+ */
19
+ aiStorageService?: ServiceTestConfig['services']['aiStorageService']
20
+ ): void => {
21
+ const factory = agentRunService
22
+ describe(`AgentRunService [${name}]`, () => {
23
+ let agentService: AgentRunService
24
+
25
+ before(async () => {
26
+ agentService = await factory()
27
+ })
28
+
29
+ test('listThreads', async () => {
30
+ const threads = await agentService.listThreads()
31
+ assert.ok(Array.isArray(threads))
32
+ })
33
+
34
+ if (aiStorageService) {
35
+ const storageFactory = aiStorageService
36
+
37
+ // The `owners` constraint is what keeps the generated thread-management
38
+ // functions from leaking across tenants: a caller may only list threads
39
+ // owned by one of their session principals, matching the
40
+ // `principal:sub-partition` composition resolveOwnerResourceId writes.
41
+ test('listThreads scopes to the given owners, including sub-partitions', async () => {
42
+ const storage = await storageFactory()
43
+ await storage.createThread('owner-alice')
44
+ await storage.createThread('owner-alice:project-1')
45
+ await storage.createThread('owner-bob:secret')
46
+
47
+ const threads = await agentService.listThreads({
48
+ owners: ['owner-alice'],
49
+ })
50
+ const ids = threads.map((t) => t.resourceId)
51
+
52
+ assert.ok(ids.includes('owner-alice'))
53
+ assert.ok(ids.includes('owner-alice:project-1'))
54
+ assert.ok(
55
+ !ids.some((id) => id.startsWith('owner-bob')),
56
+ "another owner's threads must not be listed"
57
+ )
58
+ })
59
+
60
+ test('listThreads with an owner does not match a lookalike prefix', async () => {
61
+ const storage = await storageFactory()
62
+ await storage.createThread('owner-al')
63
+ await storage.createThread('owner-alice-evil:p')
64
+
65
+ const threads = await agentService.listThreads({
66
+ owners: ['owner-al'],
67
+ })
68
+ const ids = threads.map((t) => t.resourceId)
69
+
70
+ assert.ok(ids.includes('owner-al'))
71
+ assert.ok(!ids.includes('owner-alice-evil:p'))
72
+ })
73
+
74
+ test('listThreads with an empty owners list returns nothing', async () => {
75
+ const storage = await storageFactory()
76
+ await storage.createThread('owner-empty-check')
77
+
78
+ const threads = await agentService.listThreads({ owners: [] })
79
+ assert.deepEqual(threads, [])
80
+ })
81
+ }
82
+
83
+ test('getThread returns null for missing', async () => {
84
+ const thread = await agentService.getThread('missing-thread')
85
+ assert.equal(thread, null)
86
+ })
87
+
88
+ test('getDistinctAgentNames', async () => {
89
+ const names = await agentService.getDistinctAgentNames()
90
+ assert.ok(Array.isArray(names))
91
+ })
92
+
93
+ test('deleteThread returns false for missing', async () => {
94
+ const result = await agentService.deleteThread('missing-thread')
95
+ assert.equal(result, false)
96
+ })
97
+ })
98
+ }