@pikku/core 0.12.79 → 0.12.82

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 (479) hide show
  1. package/CHANGELOG.md +473 -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/errors/index.d.ts +1 -1
  7. package/dist/errors/index.js +1 -1
  8. package/dist/function/function-runner.js +22 -47
  9. package/dist/function/functions.types.d.ts +9 -5
  10. package/dist/function/index.d.ts +1 -1
  11. package/dist/index.d.ts +14 -19
  12. package/dist/index.js +6 -11
  13. package/dist/middleware/auth-cookie.d.ts +0 -4
  14. package/dist/middleware/auth-cookie.js +38 -2
  15. package/dist/middleware/cors.js +1 -0
  16. package/dist/middleware/index.d.ts +1 -1
  17. package/dist/middleware/index.js +1 -1
  18. package/dist/middleware/remote-auth.js +14 -3
  19. package/dist/permissions.d.ts +8 -10
  20. package/dist/permissions.js +0 -11
  21. package/dist/pikku-state.js +16 -5
  22. package/dist/schema.js +35 -1
  23. package/dist/services/ai-agent-runner-service.d.ts +7 -0
  24. package/dist/services/ai-run-state-service.d.ts +17 -1
  25. package/dist/services/in-memory-ai-run-state-service.d.ts +6 -2
  26. package/dist/services/in-memory-ai-run-state-service.js +11 -1
  27. package/dist/services/in-memory-workflow-service.js +2 -0
  28. package/dist/services/index.d.ts +15 -15
  29. package/dist/services/index.js +5 -5
  30. package/dist/services/local-content-request-handler.d.ts +21 -0
  31. package/dist/services/local-content-request-handler.js +72 -53
  32. package/dist/services/local-content.d.ts +6 -0
  33. package/dist/services/local-content.js +14 -1
  34. package/dist/services/scoped-credential-service.d.ts +21 -0
  35. package/dist/services/scoped-credential-service.js +53 -0
  36. package/dist/services/workflow-service.d.ts +4 -2
  37. package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
  38. package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
  39. package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
  40. package/dist/testing/service-tests/ai-storage-service-tests.js +302 -0
  41. package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
  42. package/dist/testing/service-tests/channel-store-tests.js +72 -0
  43. package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
  44. package/dist/testing/service-tests/credential-service-tests.js +109 -0
  45. package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
  46. package/dist/testing/service-tests/deployment-service-tests.js +21 -0
  47. package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
  48. package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
  49. package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
  50. package/dist/testing/service-tests/secret-service-tests.js +80 -0
  51. package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
  52. package/dist/testing/service-tests/session-store-tests.js +43 -0
  53. package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
  54. package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
  55. package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
  56. package/dist/testing/service-tests/workflow-service-tests.js +150 -0
  57. package/dist/testing/service-tests.d.ts +6 -0
  58. package/dist/testing/service-tests.js +26 -791
  59. package/dist/types/core.types.d.ts +11 -3
  60. package/dist/types/state.types.d.ts +14 -1
  61. package/dist/wirings/actor-flow/index.d.ts +1 -1
  62. package/dist/wirings/actor-flow/run-conversation.js +1 -6
  63. package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
  64. package/dist/wirings/ai-agent/agent-rpc.js +53 -0
  65. package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
  66. package/dist/wirings/ai-agent/ai-agent-finalize.d.ts +58 -0
  67. package/dist/wirings/ai-agent/ai-agent-finalize.js +138 -0
  68. package/dist/wirings/ai-agent/ai-agent-interrupt.js +1 -0
  69. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +2 -8
  70. package/dist/wirings/ai-agent/ai-agent-memory.js +37 -18
  71. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +7 -0
  72. package/dist/wirings/ai-agent/ai-agent-model-config.js +44 -1
  73. package/dist/wirings/ai-agent/ai-agent-prepare.js +5 -9
  74. package/dist/wirings/ai-agent/ai-agent-runner.js +89 -147
  75. package/dist/wirings/ai-agent/ai-agent-stream.js +109 -97
  76. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +57 -0
  77. package/dist/wirings/ai-agent/ai-agent-turn.js +82 -0
  78. package/dist/wirings/ai-agent/ai-agent.types.d.ts +47 -2
  79. package/dist/wirings/ai-agent/index.d.ts +8 -7
  80. package/dist/wirings/ai-agent/index.js +5 -4
  81. package/dist/wirings/ai-agent/voice-input.js +1 -6
  82. package/dist/wirings/ai-agent/voice-output.js +2 -12
  83. package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +26 -0
  84. package/dist/wirings/ai-scorer/ai-scorer-grade.js +33 -0
  85. package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +17 -0
  86. package/dist/wirings/ai-scorer/ai-scorer-judge.js +92 -0
  87. package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +15 -0
  88. package/dist/wirings/ai-scorer/ai-scorer-live.js +38 -0
  89. package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +18 -0
  90. package/dist/wirings/ai-scorer/ai-scorer-registry.js +46 -0
  91. package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +8 -0
  92. package/dist/wirings/ai-scorer/ai-scorer-sampling.js +31 -0
  93. package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +10 -0
  94. package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +40 -0
  95. package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +15 -0
  96. package/dist/wirings/ai-scorer/ai-scorer-worker.js +58 -0
  97. package/dist/wirings/ai-scorer/ai-scorer.d.ts +39 -0
  98. package/dist/wirings/ai-scorer/ai-scorer.js +40 -0
  99. package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +90 -0
  100. package/dist/wirings/ai-scorer/ai-scorer.types.js +4 -0
  101. package/dist/wirings/ai-scorer/index.d.ts +6 -0
  102. package/dist/wirings/ai-scorer/index.js +5 -0
  103. package/dist/wirings/channel/channel-common.js +1 -0
  104. package/dist/wirings/channel/channel-handler.js +3 -5
  105. package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
  106. package/dist/wirings/channel/channel-rpc-service.js +0 -8
  107. package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
  108. package/dist/wirings/channel/channel-rpc.types.js +8 -0
  109. package/dist/wirings/channel/channel-runner.d.ts +1 -3
  110. package/dist/wirings/channel/channel-runner.js +16 -8
  111. package/dist/wirings/channel/channel.types.d.ts +2 -0
  112. package/dist/wirings/channel/index.d.ts +5 -6
  113. package/dist/wirings/channel/index.js +3 -4
  114. package/dist/wirings/channel/local/local-channel-runner.js +8 -1
  115. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  116. package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
  117. package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
  118. package/dist/wirings/cli/channel/cli-raw-channel-runner.js +9 -1
  119. package/dist/wirings/cli/channel/index.d.ts +1 -2
  120. package/dist/wirings/cli/channel/index.js +0 -1
  121. package/dist/wirings/cli/cli-runner.js +17 -3
  122. package/dist/wirings/cli/cli.types.d.ts +0 -8
  123. package/dist/wirings/cli/command-parser.js +13 -0
  124. package/dist/wirings/credential/index.d.ts +1 -1
  125. package/dist/wirings/gateway/gateway-runner.js +9 -2
  126. package/dist/wirings/gateway/index.d.ts +1 -1
  127. package/dist/wirings/http/http-routes.js +2 -0
  128. package/dist/wirings/http/http-runner.d.ts +0 -10
  129. package/dist/wirings/http/http-runner.js +14 -15
  130. package/dist/wirings/http/http.types.d.ts +0 -10
  131. package/dist/wirings/http/index.d.ts +2 -3
  132. package/dist/wirings/http/index.js +1 -1
  133. package/dist/wirings/mcp/index.d.ts +1 -1
  134. package/dist/wirings/mcp/mcp-runner.d.ts +15 -7
  135. package/dist/wirings/mcp/mcp-runner.js +18 -11
  136. package/dist/wirings/persona/index.d.ts +3 -4
  137. package/dist/wirings/persona/index.js +2 -3
  138. package/dist/wirings/queue/index.d.ts +1 -3
  139. package/dist/wirings/queue/index.js +1 -3
  140. package/dist/wirings/rpc/addon-runner.d.ts +4 -0
  141. package/dist/wirings/rpc/addon-runner.js +19 -3
  142. package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
  143. package/dist/wirings/rpc/rpc-runner.js +55 -86
  144. package/dist/wirings/rpc/rpc-types.d.ts +7 -0
  145. package/dist/wirings/rpc/wire-addon.d.ts +13 -0
  146. package/dist/wirings/rpc/wire-addon.js +4 -0
  147. package/dist/wirings/scheduler/index.d.ts +1 -1
  148. package/dist/wirings/secret/validate-secret-definitions.js +2 -2
  149. package/dist/wirings/trigger/index.d.ts +1 -1
  150. package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
  151. package/dist/wirings/trigger/trigger-runner.js +1 -0
  152. package/dist/wirings/virtual-user/index.d.ts +5 -6
  153. package/dist/wirings/virtual-user/index.js +2 -4
  154. package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
  155. package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
  156. package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
  157. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +100 -3
  158. package/dist/wirings/workflow/feature.js +7 -4
  159. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
  160. package/dist/wirings/workflow/graph/graph-runner.js +8 -7
  161. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
  162. package/dist/wirings/workflow/index.d.ts +12 -7
  163. package/dist/wirings/workflow/index.js +7 -3
  164. package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
  165. package/dist/wirings/workflow/pikku-scenario-service.js +64 -10
  166. package/dist/wirings/workflow/pikku-workflow-service.d.ts +28 -147
  167. package/dist/wirings/workflow/pikku-workflow-service.js +85 -493
  168. package/dist/wirings/workflow/scenario-step.types.d.ts +8 -0
  169. package/dist/wirings/workflow/workflow-approval-audit.d.ts +16 -0
  170. package/dist/wirings/workflow/workflow-approval-audit.js +40 -0
  171. package/dist/wirings/workflow/workflow-approval-policy.d.ts +20 -0
  172. package/dist/wirings/workflow/workflow-approval-policy.js +48 -0
  173. package/dist/wirings/workflow/workflow-approval.d.ts +67 -0
  174. package/dist/wirings/workflow/workflow-approval.js +177 -0
  175. package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
  176. package/dist/wirings/workflow/workflow-constants.js +35 -0
  177. package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
  178. package/dist/wirings/workflow/workflow-errors.js +112 -0
  179. package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
  180. package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
  181. package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
  182. package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
  183. package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
  184. package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
  185. package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
  186. package/dist/wirings/workflow/workflow-recovery.js +101 -0
  187. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
  188. package/dist/wirings/workflow/workflow-run-ownership.d.ts +17 -0
  189. package/dist/wirings/workflow/workflow-run-ownership.js +30 -0
  190. package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
  191. package/dist/wirings/workflow/workflow-suspend.js +33 -0
  192. package/dist/wirings/workflow/workflow.types.d.ts +8 -1
  193. package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
  194. package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
  195. package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
  196. package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
  197. package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
  198. package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
  199. package/knowledge/decisions/internals/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md +32 -0
  200. package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
  201. package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
  202. package/knowledge/decisions/internals/an-addon-scope-root-loses-to-a-root-the-host-already-declares.md +39 -0
  203. package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
  204. package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
  205. package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
  206. package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
  207. package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
  208. package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
  209. package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
  210. package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
  211. package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
  212. package/knowledge/decisions/internals/index.md +42 -3
  213. package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
  214. package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
  215. package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
  216. package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
  217. package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
  218. package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
  219. package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
  220. package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
  221. package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
  222. package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
  223. package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
  224. package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
  225. package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
  226. package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
  227. package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
  228. package/knowledge/decisions/internals/validate-runs-checks-by-precondition.md +115 -0
  229. package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
  230. package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
  231. package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
  232. package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
  233. package/knowledge/decisions/security/a-function-never-receives-the-secret-service.md +37 -0
  234. package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
  235. package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
  236. package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
  237. package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
  238. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +50 -0
  239. package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
  240. package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +59 -0
  241. package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
  242. package/knowledge/decisions/security/index.md +9 -0
  243. package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
  244. package/knowledge/questions/index.md +2 -1
  245. package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
  246. package/package.json +15 -3
  247. package/scripts/generate-api-report.d.mts +1 -0
  248. package/scripts/generate-api-report.mjs +89 -0
  249. package/scripts/generate-api-report.mts +343 -0
  250. package/src/api-report.test.ts +69 -0
  251. package/src/column-form.ts +1 -4
  252. package/src/crypto-utils.test.ts +15 -2
  253. package/src/dev/hot-reload.ts +2 -7
  254. package/src/ecosystem.ts +40 -0
  255. package/src/errors/error-handler.ts +5 -3
  256. package/src/errors/error.test.ts +4 -1
  257. package/src/errors/index.ts +1 -1
  258. package/src/function/function-runner.test.ts +53 -1
  259. package/src/function/function-runner.ts +33 -48
  260. package/src/function/functions.types.ts +15 -9
  261. package/src/function/index.ts +0 -2
  262. package/src/handle-error.ts +1 -1
  263. package/src/index.ts +3 -57
  264. package/src/middleware/auth-cookie.test.ts +50 -0
  265. package/src/middleware/auth-cookie.ts +38 -2
  266. package/src/middleware/cors.ts +2 -1
  267. package/src/middleware/index.ts +1 -1
  268. package/src/middleware/remote-auth.test.ts +43 -0
  269. package/src/middleware/remote-auth.ts +17 -3
  270. package/src/middleware-runner.ts +4 -2
  271. package/src/no-any-casts.test.ts +57 -0
  272. package/src/permissions.test.ts +3 -1
  273. package/src/permissions.ts +22 -24
  274. package/src/pikku-state.ts +22 -6
  275. package/src/public-surface.json +554 -0
  276. package/src/public-surface.json.README +25 -0
  277. package/src/public-surface.test.ts +105 -0
  278. package/src/removed-legacy-exports.test.ts +62 -0
  279. package/src/schema.test.ts +78 -0
  280. package/src/schema.ts +36 -1
  281. package/src/services/ai-agent-runner-service.ts +12 -1
  282. package/src/services/ai-run-state-service.ts +18 -1
  283. package/src/services/audit-service.ts +2 -2
  284. package/src/services/in-memory-ai-run-state-service.ts +16 -2
  285. package/src/services/in-memory-workflow-service.ts +2 -0
  286. package/src/services/index.ts +4 -47
  287. package/src/services/local-content-request-handler.test.ts +43 -5
  288. package/src/services/local-content-request-handler.ts +103 -74
  289. package/src/services/local-content.ts +15 -1
  290. package/src/services/local-email-service.ts +5 -1
  291. package/src/services/scoped-credential-service.test.ts +86 -0
  292. package/src/services/scoped-credential-service.ts +63 -0
  293. package/src/services/system-role-guard.test.ts +4 -1
  294. package/src/services/workflow-service.ts +9 -2
  295. package/src/side-effects-are-declared.test.ts +84 -0
  296. package/src/source-files-stay-composable.test.ts +41 -0
  297. package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
  298. package/src/testing/service-tests/ai-storage-service-tests.ts +379 -0
  299. package/src/testing/service-tests/channel-store-tests.ts +98 -0
  300. package/src/testing/service-tests/credential-service-tests.ts +143 -0
  301. package/src/testing/service-tests/deployment-service-tests.ts +33 -0
  302. package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
  303. package/src/testing/service-tests/secret-service-tests.ts +105 -0
  304. package/src/testing/service-tests/session-store-tests.ts +62 -0
  305. package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
  306. package/src/testing/service-tests/workflow-service-tests.ts +308 -0
  307. package/src/testing/service-tests.ts +31 -1111
  308. package/src/types/core.types.ts +14 -3
  309. package/src/types/state.types.ts +17 -1
  310. package/src/wirings/actor-flow/index.ts +0 -3
  311. package/src/wirings/actor-flow/run-conversation.ts +1 -6
  312. package/src/wirings/ai-agent/agent-rpc.ts +120 -0
  313. package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
  314. package/src/wirings/ai-agent/ai-agent-finalize.test.ts +186 -0
  315. package/src/wirings/ai-agent/ai-agent-finalize.ts +197 -0
  316. package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
  317. package/src/wirings/ai-agent/ai-agent-interrupt.ts +1 -0
  318. package/src/wirings/ai-agent/ai-agent-memory.ts +62 -39
  319. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +72 -3
  320. package/src/wirings/ai-agent/ai-agent-model-config.ts +49 -1
  321. package/src/wirings/ai-agent/ai-agent-prepare.ts +12 -12
  322. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
  323. package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
  324. package/src/wirings/ai-agent/ai-agent-runner.ts +120 -160
  325. package/src/wirings/ai-agent/ai-agent-stream-output-hooks.test.ts +353 -0
  326. package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
  327. package/src/wirings/ai-agent/ai-agent-stream.ts +152 -117
  328. package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
  329. package/src/wirings/ai-agent/ai-agent-turn.test.ts +67 -0
  330. package/src/wirings/ai-agent/ai-agent-turn.ts +122 -0
  331. package/src/wirings/ai-agent/ai-agent.types.ts +65 -5
  332. package/src/wirings/ai-agent/index.ts +2 -16
  333. package/src/wirings/ai-agent/voice-input.ts +1 -6
  334. package/src/wirings/ai-agent/voice-output.ts +2 -12
  335. package/src/wirings/ai-scorer/ai-scorer-grade.test.ts +106 -0
  336. package/src/wirings/ai-scorer/ai-scorer-grade.ts +55 -0
  337. package/src/wirings/ai-scorer/ai-scorer-judge.test.ts +143 -0
  338. package/src/wirings/ai-scorer/ai-scorer-judge.ts +120 -0
  339. package/src/wirings/ai-scorer/ai-scorer-live.test.ts +174 -0
  340. package/src/wirings/ai-scorer/ai-scorer-live.ts +56 -0
  341. package/src/wirings/ai-scorer/ai-scorer-registry.ts +63 -0
  342. package/src/wirings/ai-scorer/ai-scorer-sampling.test.ts +34 -0
  343. package/src/wirings/ai-scorer/ai-scorer-sampling.ts +36 -0
  344. package/src/wirings/ai-scorer/ai-scorer-snapshots.test.ts +49 -0
  345. package/src/wirings/ai-scorer/ai-scorer-snapshots.ts +46 -0
  346. package/src/wirings/ai-scorer/ai-scorer-worker.test.ts +122 -0
  347. package/src/wirings/ai-scorer/ai-scorer-worker.ts +69 -0
  348. package/src/wirings/ai-scorer/ai-scorer.ts +76 -0
  349. package/src/wirings/ai-scorer/ai-scorer.types.ts +107 -0
  350. package/src/wirings/ai-scorer/index.ts +24 -0
  351. package/src/wirings/channel/channel-common.ts +5 -2
  352. package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
  353. package/src/wirings/channel/channel-handler.ts +7 -7
  354. package/src/wirings/channel/channel-rpc-service.ts +0 -14
  355. package/src/wirings/channel/channel-rpc.test.ts +25 -4
  356. package/src/wirings/channel/channel-rpc.types.ts +14 -0
  357. package/src/wirings/channel/channel-runner.ts +33 -20
  358. package/src/wirings/channel/channel.types.ts +2 -0
  359. package/src/wirings/channel/index.ts +1 -20
  360. package/src/wirings/channel/local/local-channel-runner.test.ts +68 -0
  361. package/src/wirings/channel/local/local-channel-runner.ts +9 -2
  362. package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
  363. package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
  364. package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
  365. package/src/wirings/cli/channel/cli-raw-channel-runner.test.ts +23 -0
  366. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +12 -1
  367. package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
  368. package/src/wirings/cli/channel/index.ts +0 -7
  369. package/src/wirings/cli/cli-runner.test.ts +68 -0
  370. package/src/wirings/cli/cli-runner.ts +25 -5
  371. package/src/wirings/cli/cli.types.ts +0 -39
  372. package/src/wirings/cli/command-parser.test.ts +19 -0
  373. package/src/wirings/cli/command-parser.ts +17 -1
  374. package/src/wirings/credential/index.ts +0 -1
  375. package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
  376. package/src/wirings/gateway/gateway-runner.ts +27 -19
  377. package/src/wirings/gateway/index.ts +0 -3
  378. package/src/wirings/http/http-routes.ts +4 -1
  379. package/src/wirings/http/http-runner.test.ts +66 -0
  380. package/src/wirings/http/http-runner.ts +21 -28
  381. package/src/wirings/http/http.types.ts +0 -15
  382. package/src/wirings/http/index.ts +2 -8
  383. package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
  384. package/src/wirings/http/web-request.ts +1 -1
  385. package/src/wirings/mcp/index.ts +0 -1
  386. package/src/wirings/mcp/mcp-runner.test.ts +181 -0
  387. package/src/wirings/mcp/mcp-runner.ts +37 -21
  388. package/src/wirings/persona/index.ts +0 -8
  389. package/src/wirings/persona/persona-environments.test.ts +14 -3
  390. package/src/wirings/persona/persona.test.ts +13 -3
  391. package/src/wirings/persona/validate-personas.ts +5 -1
  392. package/src/wirings/queue/index.ts +0 -14
  393. package/src/wirings/queue/queue-runner.ts +1 -1
  394. package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
  395. package/src/wirings/rpc/addon-runner.ts +34 -3
  396. package/src/wirings/rpc/addon-secrets.test.ts +261 -0
  397. package/src/wirings/rpc/rpc-runner.test.ts +2 -0
  398. package/src/wirings/rpc/rpc-runner.ts +60 -136
  399. package/src/wirings/rpc/rpc-types.ts +11 -0
  400. package/src/wirings/rpc/wire-addon.ts +20 -1
  401. package/src/wirings/scheduler/index.ts +0 -1
  402. package/src/wirings/secret/validate-secret-definitions.test.ts +22 -0
  403. package/src/wirings/secret/validate-secret-definitions.ts +2 -2
  404. package/src/wirings/trigger/index.ts +0 -1
  405. package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
  406. package/src/wirings/trigger/trigger-runner.ts +7 -5
  407. package/src/wirings/virtual-user/index.ts +0 -16
  408. package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
  409. package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
  410. package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
  411. package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
  412. package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
  413. package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
  414. package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
  415. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +117 -4
  416. package/src/wirings/workflow/feature.ts +7 -4
  417. package/src/wirings/workflow/graph/graph-node.ts +1 -1
  418. package/src/wirings/workflow/graph/graph-runner.test.ts +76 -2
  419. package/src/wirings/workflow/graph/graph-runner.ts +13 -16
  420. package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
  421. package/src/wirings/workflow/index.ts +14 -24
  422. package/src/wirings/workflow/pikku-scenario-service.ts +101 -27
  423. package/src/wirings/workflow/pikku-workflow-service.test.ts +15 -14
  424. package/src/wirings/workflow/pikku-workflow-service.ts +214 -743
  425. package/src/wirings/workflow/scenario-expectations.test.ts +75 -0
  426. package/src/wirings/workflow/scenario-hooks.test.ts +52 -0
  427. package/src/wirings/workflow/scenario-step.types.ts +8 -0
  428. package/src/wirings/workflow/workflow-approval-audit.ts +47 -0
  429. package/src/wirings/workflow/workflow-approval-policy.test.ts +524 -0
  430. package/src/wirings/workflow/workflow-approval-policy.ts +68 -0
  431. package/src/wirings/workflow/workflow-approval.ts +289 -0
  432. package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
  433. package/src/wirings/workflow/workflow-constants.ts +47 -0
  434. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  435. package/src/wirings/workflow/workflow-errors.ts +128 -0
  436. package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
  437. package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
  438. package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
  439. package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
  440. package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
  441. package/src/wirings/workflow/workflow-recovery.ts +162 -0
  442. package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
  443. package/src/wirings/workflow/workflow-run-authority.test.ts +212 -0
  444. package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
  445. package/src/wirings/workflow/workflow-run-ownership.ts +37 -0
  446. package/src/wirings/workflow/workflow-suspend.ts +61 -0
  447. package/src/wirings/workflow/workflow.types.ts +7 -9
  448. package/src/wirings-stay-decoupled.test.ts +127 -0
  449. package/tsconfig.json +1 -1
  450. package/tsconfig.tsbuildinfo +1 -1
  451. package/dist/internal.d.ts +0 -3
  452. package/dist/internal.js +0 -2
  453. package/dist/middleware/timeout.d.ts +0 -9
  454. package/dist/middleware/timeout.js +0 -15
  455. package/dist/pikku-response.d.ts +0 -6
  456. package/dist/pikku-response.js +0 -6
  457. package/dist/services/gopass-secrets.d.ts +0 -15
  458. package/dist/services/gopass-secrets.js +0 -76
  459. package/dist/services/http-scenario-actors.d.ts +0 -75
  460. package/dist/services/http-scenario-actors.js +0 -195
  461. package/dist/services/http-user-flow-actors.d.ts +0 -67
  462. package/dist/services/http-user-flow-actors.js +0 -193
  463. package/dist/services/scenario-actors-service.d.ts +0 -127
  464. package/dist/services/scenario-actors-service.js +0 -40
  465. package/dist/services/user-flow-actors-service.d.ts +0 -39
  466. package/dist/wirings/credential/wire-credential.d.ts +0 -48
  467. package/dist/wirings/credential/wire-credential.js +0 -47
  468. package/dist/wirings/oauth2/oauth2-client.d.ts +0 -47
  469. package/dist/wirings/oauth2/oauth2-client.js +0 -263
  470. package/dist/wirings/oauth2/oauth2-routes.d.ts +0 -35
  471. package/dist/wirings/oauth2/oauth2-routes.js +0 -146
  472. package/dist/wirings/scope/wire-scope.d.ts +0 -33
  473. package/dist/wirings/scope/wire-scope.js +0 -32
  474. package/dist/wirings/workflow/dsl/index.d.ts +0 -5
  475. package/dist/wirings/workflow/dsl/index.js +0 -4
  476. package/dist/wirings/workflow/graph/index.d.ts +0 -5
  477. package/dist/wirings/workflow/graph/index.js +0 -4
  478. package/src/internal.ts +0 -10
  479. /package/dist/{services/user-flow-actors-service.js → wirings/workflow/workflow-run-engine.types.js} +0 -0
@@ -0,0 +1,524 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import type { StandardSchemaV1 } from '@standard-schema/spec'
4
+
5
+ import { InMemoryWorkflowService } from '../../services/in-memory-workflow-service.js'
6
+ import { pikkuState } from '../../pikku-state.js'
7
+ import { addWorkflow } from './dsl/workflow-runner.js'
8
+ import { WorkflowSuspendedException } from './workflow-errors.js'
9
+ import {
10
+ approvalPolicyRefusal,
11
+ WorkflowApprovalForbiddenError,
12
+ } from './workflow-approval-policy.js'
13
+ import { approvalStateKey, approvalStepNameFor } from './workflow-approval.js'
14
+ import type {
15
+ PikkuWorkflowWire,
16
+ WorkflowApprovalOptions,
17
+ } from './workflow.types.js'
18
+
19
+ const anySchema: StandardSchemaV1<unknown, unknown> = {
20
+ '~standard': {
21
+ version: 1,
22
+ vendor: 'test',
23
+ validate: (value) => ({ value }),
24
+ },
25
+ }
26
+
27
+ const registerApprovalWorkflow = (
28
+ workflowName: string,
29
+ graphHash: string,
30
+ options: Omit<WorkflowApprovalOptions, 'schema'>,
31
+ audit?: { audit: (event: unknown) => Promise<void> }
32
+ ) => {
33
+ pikkuState(null, 'package', 'singletonServices', {
34
+ logger: { error() {}, info() {}, warn() {}, debug() {} },
35
+ queueService: { add: async () => {} },
36
+ audit,
37
+ } as any)
38
+
39
+ const metaState = pikkuState(null, 'workflows', 'meta')
40
+ metaState[workflowName] = {
41
+ name: workflowName,
42
+ pikkuFuncId: workflowName,
43
+ source: 'dsl',
44
+ graphHash,
45
+ }
46
+ const functionMetaState = pikkuState(null, 'function', 'meta')
47
+ functionMetaState[workflowName] = {
48
+ name: workflowName,
49
+ sessionless: true,
50
+ permissions: [],
51
+ } as any
52
+
53
+ addWorkflow(workflowName, {
54
+ func: async (
55
+ _services: any,
56
+ _data: any,
57
+ { workflow }: { workflow: PikkuWorkflowWire }
58
+ ) => {
59
+ const decision = await workflow.approval('Release funds', {
60
+ schema: anySchema,
61
+ ...options,
62
+ })
63
+ return { decision }
64
+ },
65
+ })
66
+
67
+ return () => {
68
+ delete metaState[workflowName]
69
+ delete functionMetaState[workflowName]
70
+ pikkuState(null, 'workflows', 'registrations').delete(workflowName)
71
+ }
72
+ }
73
+
74
+ /** Run until the gate suspends, so the gate has published its policy. */
75
+ const runToGate = async (ws: InMemoryWorkflowService, runId: string) => {
76
+ await assert.rejects(
77
+ ws.runWorkflowJob(runId, {}),
78
+ (error: unknown) => error instanceof WorkflowSuspendedException
79
+ )
80
+ }
81
+
82
+ describe('approval policy', () => {
83
+ test('the default lets anyone the entrypoint admitted answer', () => {
84
+ assert.equal(approvalPolicyRefusal({}, 'owner', undefined), undefined)
85
+ assert.equal(
86
+ approvalPolicyRefusal({}, 'owner', { userId: 'someone-else' }),
87
+ undefined
88
+ )
89
+ })
90
+
91
+ test('owner admits only the user who started the run', () => {
92
+ const policy = { approvers: 'owner' as const }
93
+ assert.equal(
94
+ approvalPolicyRefusal(policy, 'owner', { userId: 'owner' }),
95
+ undefined
96
+ )
97
+ assert.match(
98
+ approvalPolicyRefusal(policy, 'owner', { userId: 'other' })!,
99
+ /Only the user who started this run/
100
+ )
101
+ assert.match(
102
+ approvalPolicyRefusal(policy, 'owner', undefined)!,
103
+ /Only the user who started this run/
104
+ )
105
+ })
106
+
107
+ test('a run with no initiator has no owner to compare against', () => {
108
+ assert.equal(
109
+ approvalPolicyRefusal({ approvers: 'owner' }, undefined, {
110
+ userId: 'anyone',
111
+ }),
112
+ undefined
113
+ )
114
+ })
115
+
116
+ test('not-initiator excludes the user who started the run', () => {
117
+ const policy = { approvers: 'not-initiator' as const }
118
+ assert.match(
119
+ approvalPolicyRefusal(policy, 'owner', { userId: 'owner' })!,
120
+ /may not answer/
121
+ )
122
+ assert.equal(
123
+ approvalPolicyRefusal(policy, 'owner', { userId: 'reviewer' }),
124
+ undefined
125
+ )
126
+ })
127
+
128
+ test('not-initiator still requires a signed-in decider on an unowned run', () => {
129
+ assert.match(
130
+ approvalPolicyRefusal({ approvers: 'not-initiator' }, undefined, {})!,
131
+ /requires a signed-in user/
132
+ )
133
+ })
134
+
135
+ test('approverScope is required on top of the approvers rule', () => {
136
+ const policy = { approvers: 'not-initiator' as const, approverScope: 'sre' }
137
+ assert.match(
138
+ approvalPolicyRefusal(policy, 'owner', { userId: 'reviewer' })!,
139
+ /requires the 'sre' scope/
140
+ )
141
+ assert.equal(
142
+ approvalPolicyRefusal(policy, 'owner', {
143
+ userId: 'reviewer',
144
+ scopes: ['sre'],
145
+ }),
146
+ undefined
147
+ )
148
+ assert.match(
149
+ approvalPolicyRefusal(policy, 'owner', {
150
+ userId: 'owner',
151
+ scopes: ['sre'],
152
+ })!,
153
+ /may not answer/
154
+ )
155
+ })
156
+ })
157
+
158
+ describe('approval policy enforcement', () => {
159
+ test('a gate with no declared policy accepts a caller who does not own the run', async () => {
160
+ const ws = new InMemoryWorkflowService()
161
+ const cleanup = registerApprovalWorkflow('openGate', 'open-gate', {})
162
+
163
+ const runId = await ws.createRun('openGate', {}, false, 'open-gate', {
164
+ type: 'http',
165
+ pikkuUserId: 'initiator',
166
+ })
167
+ await runToGate(ws, runId)
168
+
169
+ await ws.approveStep(runId, 'Release funds', { ok: true }, {
170
+ userId: 'someone-else',
171
+ })
172
+ await ws.runWorkflowJob(runId, {})
173
+
174
+ assert.equal((await ws.getRun(runId))?.status, 'completed')
175
+ cleanup()
176
+ })
177
+
178
+ test('a four-eyes gate refuses the initiator and admits a second user', async () => {
179
+ const ws = new InMemoryWorkflowService()
180
+ const cleanup = registerApprovalWorkflow('fourEyes', 'four-eyes', {
181
+ approvers: 'not-initiator',
182
+ })
183
+
184
+ const runId = await ws.createRun('fourEyes', {}, false, 'four-eyes', {
185
+ type: 'http',
186
+ pikkuUserId: 'initiator',
187
+ })
188
+ await runToGate(ws, runId)
189
+
190
+ await assert.rejects(
191
+ ws.approveStep(runId, 'Release funds', { ok: true }, {
192
+ userId: 'initiator',
193
+ }),
194
+ WorkflowApprovalForbiddenError
195
+ )
196
+
197
+ await ws.approveStep(runId, 'Release funds', { ok: true }, {
198
+ userId: 'reviewer',
199
+ })
200
+ await ws.runWorkflowJob(runId, {})
201
+
202
+ assert.equal((await ws.getRun(runId))?.status, 'completed')
203
+ cleanup()
204
+ })
205
+
206
+ test('an owner-only gate refuses a caller who did not start the run', async () => {
207
+ const ws = new InMemoryWorkflowService()
208
+ const cleanup = registerApprovalWorkflow('ownerGate', 'owner-gate', {
209
+ approvers: 'owner',
210
+ })
211
+
212
+ const runId = await ws.createRun('ownerGate', {}, false, 'owner-gate', {
213
+ type: 'http',
214
+ pikkuUserId: 'initiator',
215
+ })
216
+ await runToGate(ws, runId)
217
+
218
+ await assert.rejects(
219
+ ws.approveStep(runId, 'Release funds', { ok: true }, {
220
+ userId: 'attacker',
221
+ }),
222
+ WorkflowApprovalForbiddenError
223
+ )
224
+
225
+ await ws.approveStep(runId, 'Release funds', { ok: true }, {
226
+ userId: 'initiator',
227
+ })
228
+ await ws.runWorkflowJob(runId, {})
229
+
230
+ assert.equal((await ws.getRun(runId))?.status, 'completed')
231
+ cleanup()
232
+ })
233
+
234
+ test('a scoped gate refuses a decider without the scope', async () => {
235
+ const ws = new InMemoryWorkflowService()
236
+ const cleanup = registerApprovalWorkflow('scopedGate', 'scoped-gate', {
237
+ approverScope: 'payments:approve',
238
+ })
239
+
240
+ const runId = await ws.createRun('scopedGate', {}, false, 'scoped-gate', {
241
+ type: 'http',
242
+ pikkuUserId: 'initiator',
243
+ })
244
+ await runToGate(ws, runId)
245
+
246
+ await assert.rejects(
247
+ ws.approveStep(runId, 'Release funds', { ok: true }, {
248
+ userId: 'reviewer',
249
+ scopes: ['payments:read'],
250
+ }),
251
+ WorkflowApprovalForbiddenError
252
+ )
253
+
254
+ await ws.approveStep(runId, 'Release funds', { ok: true }, {
255
+ userId: 'reviewer',
256
+ scopes: ['payments:approve'],
257
+ })
258
+ await ws.runWorkflowJob(runId, {})
259
+
260
+ assert.equal((await ws.getRun(runId))?.status, 'completed')
261
+ cleanup()
262
+ })
263
+
264
+ /**
265
+ * Run state is where a decision waits, not where it is kept: the record is
266
+ * overwritten by the next write to that key and cleared outright whenever a
267
+ * decision is refused. Four-eyes exists to be audited, so who signed and when
268
+ * has to reach the step result, which is append-only and carried into
269
+ * workflowStepHistory.
270
+ */
271
+ test('the decider and the moment reach the step result, not just run state', async () => {
272
+ const ws = new InMemoryWorkflowService()
273
+ const cleanup = registerApprovalWorkflow('signedGate', 'signed-gate', {
274
+ approvers: 'not-initiator',
275
+ })
276
+
277
+ const runId = await ws.createRun('signedGate', {}, false, 'signed-gate', {
278
+ type: 'http',
279
+ pikkuUserId: 'initiator',
280
+ })
281
+ await runToGate(ws, runId)
282
+
283
+ await ws.approveStep(runId, 'Release funds', { ok: true }, {
284
+ userId: 'reviewer',
285
+ scopes: ['payments:approve'],
286
+ })
287
+ await ws.runWorkflowJob(runId, {})
288
+
289
+ const step = await ws.getStepState(
290
+ runId,
291
+ approvalStepNameFor('Release funds')
292
+ )
293
+ const outcome = step.result as {
294
+ status: string
295
+ decidedBy?: { userId?: string; scopes?: string[] }
296
+ decidedAt?: string
297
+ }
298
+
299
+ assert.equal(outcome.status, 'decided')
300
+ assert.equal(outcome.decidedBy?.userId, 'reviewer')
301
+ assert.deepEqual(outcome.decidedBy?.scopes, ['payments:approve'])
302
+ assert.ok(
303
+ outcome.decidedAt && !Number.isNaN(Date.parse(outcome.decidedAt)),
304
+ 'decidedAt is an ISO timestamp'
305
+ )
306
+
307
+ cleanup()
308
+ })
309
+
310
+ test('an expired gate has nobody to record', async () => {
311
+ const ws = new InMemoryWorkflowService()
312
+ const cleanup = registerApprovalWorkflow('expiredGate', 'expired-gate', {
313
+ expiry: 100,
314
+ })
315
+
316
+ const runId = await ws.createRun('expiredGate', {}, false, 'expired-gate', {
317
+ type: 'http',
318
+ pikkuUserId: 'initiator',
319
+ })
320
+ await runToGate(ws, runId)
321
+ await new Promise((resolve) => setTimeout(resolve, 150))
322
+ await ws.runWorkflowJob(runId, {})
323
+
324
+ const step = await ws.getStepState(
325
+ runId,
326
+ approvalStepNameFor('Release funds')
327
+ )
328
+ assert.deepEqual(step.result, { status: 'expired' })
329
+
330
+ cleanup()
331
+ })
332
+
333
+ test('a decision recorded before the run reaches the gate is still judged', async () => {
334
+ const ws = new InMemoryWorkflowService()
335
+ const cleanup = registerApprovalWorkflow('earlyGate', 'early-gate', {
336
+ approvers: 'not-initiator',
337
+ })
338
+
339
+ const runId = await ws.createRun('earlyGate', {}, false, 'early-gate', {
340
+ type: 'http',
341
+ pikkuUserId: 'initiator',
342
+ })
343
+
344
+ // The gate has not been reached, so no policy has been published and the
345
+ // submission cannot be judged here — it is accepted and judged on replay.
346
+ await ws.approveStep(runId, 'Release funds', { ok: true }, {
347
+ userId: 'initiator',
348
+ })
349
+
350
+ await runToGate(ws, runId)
351
+
352
+ const stateKey = approvalStateKey(approvalStepNameFor('Release funds'))
353
+ const record = (await ws.getRunState(runId))[stateKey] as {
354
+ decision?: unknown
355
+ error?: Array<{ message: string }>
356
+ }
357
+ assert.equal(record.decision, undefined, 'the decision was cleared')
358
+ assert.match(record.error?.[0]?.message ?? '', /may not answer/)
359
+ assert.equal((await ws.getRun(runId))?.status, 'suspended')
360
+
361
+ cleanup()
362
+ })
363
+ })
364
+
365
+ /**
366
+ * The step result is deleted with the run — `deleteRun` cascades to steps and
367
+ * to history — and a refused attempt never reaches a step at all. An approval
368
+ * is asked for so it can be answered for afterwards, so it also goes to the
369
+ * audit sink, which holds no foreign key to the run.
370
+ */
371
+ describe('approval audit trail', () => {
372
+ const collectingAudit = () => {
373
+ const events: any[] = []
374
+ return {
375
+ events,
376
+ sink: { audit: async (event: any) => void events.push(event) },
377
+ }
378
+ }
379
+
380
+ test('an accepted decision is recorded against the user who made it', async () => {
381
+ const ws = new InMemoryWorkflowService()
382
+ const { events, sink } = collectingAudit()
383
+ const cleanup = registerApprovalWorkflow(
384
+ 'auditedGate',
385
+ 'audited-gate',
386
+ { approvers: 'not-initiator' },
387
+ sink
388
+ )
389
+
390
+ const runId = await ws.createRun('auditedGate', {}, false, 'audited-gate', {
391
+ type: 'http',
392
+ pikkuUserId: 'initiator',
393
+ })
394
+ await runToGate(ws, runId)
395
+ await ws.approveStep(runId, 'Release funds', { ok: true }, {
396
+ userId: 'reviewer',
397
+ scopes: ['payments:approve'],
398
+ })
399
+ await ws.runWorkflowJob(runId, {})
400
+
401
+ assert.equal(events.length, 1)
402
+ assert.equal(events[0].type, 'workflow.approval.decided')
403
+ assert.equal(events[0].outcome, 'success')
404
+ assert.equal(events[0].wireType, 'workflow')
405
+ assert.equal(events[0].userIdentity.pikkuUserId, 'reviewer')
406
+ assert.equal(events[0].metadata.runId, runId)
407
+ assert.equal(events[0].metadata.reason, 'Release funds')
408
+ assert.deepEqual(events[0].metadata.scopes, ['payments:approve'])
409
+ assert.ok(!Number.isNaN(Date.parse(events[0].occurredAt)))
410
+
411
+ cleanup()
412
+ })
413
+
414
+ test('a refused attempt is recorded too — that is the one worth keeping', async () => {
415
+ const ws = new InMemoryWorkflowService()
416
+ const { events, sink } = collectingAudit()
417
+ const cleanup = registerApprovalWorkflow(
418
+ 'refusedGate',
419
+ 'refused-gate',
420
+ { approvers: 'not-initiator' },
421
+ sink
422
+ )
423
+
424
+ const runId = await ws.createRun('refusedGate', {}, false, 'refused-gate', {
425
+ type: 'http',
426
+ pikkuUserId: 'initiator',
427
+ })
428
+ await runToGate(ws, runId)
429
+
430
+ await assert.rejects(
431
+ ws.approveStep(runId, 'Release funds', { ok: true }, {
432
+ userId: 'initiator',
433
+ }),
434
+ WorkflowApprovalForbiddenError
435
+ )
436
+
437
+ assert.equal(events.length, 1)
438
+ assert.equal(events[0].outcome, 'denied')
439
+ assert.equal(events[0].userIdentity.pikkuUserId, 'initiator')
440
+ assert.match(events[0].metadata.refusal, /may not answer/)
441
+
442
+ cleanup()
443
+ })
444
+
445
+ test('a decision refused on replay is recorded, having been refused nowhere else', async () => {
446
+ const ws = new InMemoryWorkflowService()
447
+ const { events, sink } = collectingAudit()
448
+ const cleanup = registerApprovalWorkflow(
449
+ 'replayRefused',
450
+ 'replay-refused',
451
+ { approvers: 'not-initiator' },
452
+ sink
453
+ )
454
+
455
+ const runId = await ws.createRun(
456
+ 'replayRefused',
457
+ {},
458
+ false,
459
+ 'replay-refused',
460
+ { type: 'http', pikkuUserId: 'initiator' }
461
+ )
462
+
463
+ // Submitted before the gate published its policy, so it is accepted here
464
+ // and only refused on replay.
465
+ await ws.approveStep(runId, 'Release funds', { ok: true }, {
466
+ userId: 'initiator',
467
+ })
468
+ assert.equal(events[0].outcome, 'success')
469
+
470
+ await runToGate(ws, runId)
471
+
472
+ assert.equal(events.length, 2)
473
+ assert.equal(events[1].outcome, 'denied')
474
+ assert.equal(events[1].userIdentity.pikkuUserId, 'initiator')
475
+ assert.match(events[1].metadata.refusal, /may not answer/)
476
+
477
+ cleanup()
478
+ })
479
+
480
+ test('a failing sink does not cost the decision', async () => {
481
+ const ws = new InMemoryWorkflowService()
482
+ const cleanup = registerApprovalWorkflow(
483
+ 'brokenSink',
484
+ 'broken-sink',
485
+ {},
486
+ {
487
+ audit: async () => {
488
+ throw new Error('sink is down')
489
+ },
490
+ }
491
+ )
492
+
493
+ const runId = await ws.createRun('brokenSink', {}, false, 'broken-sink', {
494
+ type: 'http',
495
+ pikkuUserId: 'initiator',
496
+ })
497
+ await runToGate(ws, runId)
498
+ await ws.approveStep(runId, 'Release funds', { ok: true }, {
499
+ userId: 'reviewer',
500
+ })
501
+ await ws.runWorkflowJob(runId, {})
502
+
503
+ assert.equal((await ws.getRun(runId))?.status, 'completed')
504
+ cleanup()
505
+ })
506
+
507
+ test('a project with no audit sink wired is unaffected', async () => {
508
+ const ws = new InMemoryWorkflowService()
509
+ const cleanup = registerApprovalWorkflow('noSink', 'no-sink', {})
510
+
511
+ const runId = await ws.createRun('noSink', {}, false, 'no-sink', {
512
+ type: 'http',
513
+ pikkuUserId: 'initiator',
514
+ })
515
+ await runToGate(ws, runId)
516
+ await ws.approveStep(runId, 'Release funds', { ok: true }, {
517
+ userId: 'reviewer',
518
+ })
519
+ await ws.runWorkflowJob(runId, {})
520
+
521
+ assert.equal((await ws.getRun(runId))?.status, 'completed')
522
+ cleanup()
523
+ })
524
+ })
@@ -0,0 +1,68 @@
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 {
5
+ ApprovalDecider,
6
+ WorkflowApprovalPolicy,
7
+ } from './dsl/workflow-dsl.types.js'
8
+
9
+ export class WorkflowApprovalForbiddenError extends ForbiddenError {
10
+ public payload: { reason: string; detail: string }
11
+ constructor(reason: string, detail: string) {
12
+ super(detail)
13
+ this.payload = { reason, detail }
14
+ }
15
+ }
16
+ addError(WorkflowApprovalForbiddenError, {
17
+ status: 403,
18
+ message: 'Not authorized to answer this approval.',
19
+ })
20
+
21
+ export const approvalDeciderFrom = (
22
+ session: CoreUserSession | undefined
23
+ ): ApprovalDecider | undefined =>
24
+ session ? { userId: session.userId, scopes: session.scopes } : undefined
25
+
26
+ /**
27
+ * Judge a decision against the gate's declared policy, returning the reason it
28
+ * is refused or `undefined` if it stands.
29
+ *
30
+ * Returns a message rather than throwing because the same judgement is needed
31
+ * in two places with different outcomes: refusing a live submission with a 403,
32
+ * and clearing an already-recorded decision on replay.
33
+ */
34
+ export const approvalPolicyRefusal = (
35
+ policy: WorkflowApprovalPolicy,
36
+ owner: string | undefined,
37
+ decider: ApprovalDecider | undefined
38
+ ): string | undefined => {
39
+ if (
40
+ policy.approverScope &&
41
+ !decider?.scopes?.includes(policy.approverScope)
42
+ ) {
43
+ return `Answering this approval requires the '${policy.approverScope}' scope`
44
+ }
45
+
46
+ switch (policy.approvers ?? 'any') {
47
+ case 'owner':
48
+ if (!owner) {
49
+ return undefined
50
+ }
51
+ if (!decider?.userId || decider.userId !== owner) {
52
+ return 'Only the user who started this run may answer this approval'
53
+ }
54
+ return undefined
55
+
56
+ case 'not-initiator':
57
+ if (!decider?.userId) {
58
+ return 'Answering this approval requires a signed-in user'
59
+ }
60
+ if (owner && decider.userId === owner) {
61
+ return 'The user who started this run may not answer this approval'
62
+ }
63
+ return undefined
64
+
65
+ case 'any':
66
+ return undefined
67
+ }
68
+ }