@pikku/core 0.12.78 → 0.12.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/CHANGELOG.md +234 -0
  2. package/dist/dev/hot-reload.js +1 -6
  3. package/dist/ecosystem.d.ts +27 -0
  4. package/dist/ecosystem.js +26 -0
  5. package/dist/errors/error-handler.js +4 -2
  6. package/dist/function/function-runner.js +20 -42
  7. package/dist/function/functions.types.d.ts +9 -5
  8. package/dist/index.d.ts +4 -9
  9. package/dist/index.js +3 -8
  10. package/dist/middleware/auth-cookie.d.ts +0 -4
  11. package/dist/middleware/auth-cookie.js +38 -2
  12. package/dist/middleware/cors.js +1 -0
  13. package/dist/middleware/index.d.ts +1 -1
  14. package/dist/middleware/index.js +1 -1
  15. package/dist/middleware/remote-auth.js +14 -3
  16. package/dist/permissions.d.ts +8 -10
  17. package/dist/permissions.js +0 -11
  18. package/dist/pikku-state.js +12 -5
  19. package/dist/schema.js +35 -1
  20. package/dist/services/ai-run-state-service.d.ts +7 -1
  21. package/dist/services/in-memory-ai-run-state-service.d.ts +1 -1
  22. package/dist/services/in-memory-ai-run-state-service.js +2 -1
  23. package/dist/services/in-memory-workflow-service.d.ts +10 -0
  24. package/dist/services/in-memory-workflow-service.js +25 -0
  25. package/dist/services/local-content-request-handler.d.ts +21 -0
  26. package/dist/services/local-content-request-handler.js +72 -53
  27. package/dist/services/local-content.d.ts +6 -0
  28. package/dist/services/local-content.js +14 -1
  29. package/dist/services/workflow-service.d.ts +4 -2
  30. package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
  31. package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
  32. package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
  33. package/dist/testing/service-tests/ai-storage-service-tests.js +226 -0
  34. package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
  35. package/dist/testing/service-tests/channel-store-tests.js +72 -0
  36. package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
  37. package/dist/testing/service-tests/credential-service-tests.js +109 -0
  38. package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
  39. package/dist/testing/service-tests/deployment-service-tests.js +21 -0
  40. package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
  41. package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
  42. package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
  43. package/dist/testing/service-tests/secret-service-tests.js +80 -0
  44. package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
  45. package/dist/testing/service-tests/session-store-tests.js +43 -0
  46. package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
  47. package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
  48. package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
  49. package/dist/testing/service-tests/workflow-service-tests.js +150 -0
  50. package/dist/testing/service-tests.d.ts +6 -0
  51. package/dist/testing/service-tests.js +26 -791
  52. package/dist/types/core.types.d.ts +11 -1
  53. package/dist/types/state.types.d.ts +1 -1
  54. package/dist/utils/node-host-resolver.d.ts +12 -0
  55. package/dist/utils/node-host-resolver.js +16 -0
  56. package/dist/utils/safe-fetch.d.ts +18 -0
  57. package/dist/utils/safe-fetch.js +167 -29
  58. package/dist/wirings/actor-flow/run-conversation.js +1 -6
  59. package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
  60. package/dist/wirings/ai-agent/agent-rpc.js +53 -0
  61. package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
  62. package/dist/wirings/ai-agent/ai-agent-memory.js +3 -1
  63. package/dist/wirings/ai-agent/ai-agent-prepare.js +3 -9
  64. package/dist/wirings/ai-agent/ai-agent-runner.js +28 -107
  65. package/dist/wirings/ai-agent/ai-agent-stream.js +20 -61
  66. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +56 -0
  67. package/dist/wirings/ai-agent/ai-agent-turn.js +81 -0
  68. package/dist/wirings/ai-agent/ai-agent.types.d.ts +1 -1
  69. package/dist/wirings/ai-agent/voice-input.js +1 -6
  70. package/dist/wirings/ai-agent/voice-output.js +2 -12
  71. package/dist/wirings/channel/channel-common.js +1 -0
  72. package/dist/wirings/channel/channel-handler.js +3 -5
  73. package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
  74. package/dist/wirings/channel/channel-rpc-service.js +0 -8
  75. package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
  76. package/dist/wirings/channel/channel-rpc.types.js +8 -0
  77. package/dist/wirings/channel/channel-runner.d.ts +1 -3
  78. package/dist/wirings/channel/channel-runner.js +16 -8
  79. package/dist/wirings/channel/channel.types.d.ts +2 -0
  80. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  81. package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
  82. package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
  83. package/dist/wirings/cli/cli-runner.js +4 -2
  84. package/dist/wirings/cli/cli.types.d.ts +0 -8
  85. package/dist/wirings/cli/command-parser.js +13 -0
  86. package/dist/wirings/gateway/gateway-runner.js +41 -5
  87. package/dist/wirings/http/http-routes.js +2 -0
  88. package/dist/wirings/http/http-runner.d.ts +0 -10
  89. package/dist/wirings/http/http-runner.js +6 -13
  90. package/dist/wirings/http/http.types.d.ts +0 -10
  91. package/dist/wirings/http/index.d.ts +1 -1
  92. package/dist/wirings/http/index.js +1 -1
  93. package/dist/wirings/mcp/mcp-runner.d.ts +0 -7
  94. package/dist/wirings/mcp/mcp-runner.js +0 -6
  95. package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
  96. package/dist/wirings/rpc/rpc-runner.js +53 -86
  97. package/dist/wirings/rpc/rpc-types.d.ts +3 -0
  98. package/dist/wirings/secret/validate-secret-definitions.js +2 -0
  99. package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
  100. package/dist/wirings/trigger/trigger-runner.js +1 -0
  101. package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
  102. package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
  103. package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
  104. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +29 -2
  105. package/dist/wirings/workflow/feature.js +7 -4
  106. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
  107. package/dist/wirings/workflow/graph/graph-runner.js +8 -7
  108. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
  109. package/dist/wirings/workflow/index.d.ts +7 -2
  110. package/dist/wirings/workflow/index.js +5 -1
  111. package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
  112. package/dist/wirings/workflow/pikku-scenario-service.js +38 -10
  113. package/dist/wirings/workflow/pikku-workflow-service.d.ts +44 -111
  114. package/dist/wirings/workflow/pikku-workflow-service.js +86 -404
  115. package/dist/wirings/workflow/workflow-approval.d.ts +39 -0
  116. package/dist/wirings/workflow/workflow-approval.js +114 -0
  117. package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
  118. package/dist/wirings/workflow/workflow-constants.js +35 -0
  119. package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
  120. package/dist/wirings/workflow/workflow-errors.js +112 -0
  121. package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
  122. package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
  123. package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
  124. package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
  125. package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
  126. package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
  127. package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
  128. package/dist/wirings/workflow/workflow-recovery.js +101 -0
  129. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
  130. package/dist/wirings/workflow/workflow-run-engine.types.js +1 -0
  131. package/dist/wirings/workflow/workflow-run-ownership.d.ts +16 -0
  132. package/dist/wirings/workflow/workflow-run-ownership.js +29 -0
  133. package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
  134. package/dist/wirings/workflow/workflow-suspend.js +33 -0
  135. package/dist/wirings/workflow/workflow.types.d.ts +7 -0
  136. package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
  137. package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
  138. package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
  139. package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
  140. package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
  141. package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
  142. package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
  143. package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
  144. package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
  145. package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
  146. package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
  147. package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
  148. package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
  149. package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
  150. package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
  151. package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
  152. package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
  153. package/knowledge/decisions/internals/index.md +38 -26
  154. package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
  155. package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
  156. package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
  157. package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
  158. package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
  159. package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
  160. package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
  161. package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
  162. package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
  163. package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
  164. package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
  165. package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
  166. package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
  167. package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
  168. package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
  169. package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
  170. package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
  171. package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
  172. package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
  173. package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
  174. package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
  175. package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
  176. package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
  177. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +34 -0
  178. package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
  179. package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
  180. package/knowledge/decisions/security/gateway-handlers-run-through-the-function-runner-gate.md +13 -3
  181. package/knowledge/decisions/security/index.md +7 -0
  182. package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
  183. package/knowledge/questions/index.md +2 -1
  184. package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
  185. package/package.json +14 -3
  186. package/scripts/generate-api-report.d.mts +1 -0
  187. package/scripts/generate-api-report.mjs +89 -0
  188. package/scripts/generate-api-report.mts +218 -0
  189. package/src/api-report.test.ts +69 -0
  190. package/src/column-form.ts +1 -4
  191. package/src/crypto-utils.test.ts +15 -2
  192. package/src/dev/hot-reload.ts +2 -7
  193. package/src/ecosystem.ts +40 -0
  194. package/src/errors/error-handler.ts +5 -3
  195. package/src/errors/error.test.ts +4 -1
  196. package/src/function/function-runner.test.ts +1 -1
  197. package/src/function/function-runner.ts +28 -39
  198. package/src/function/functions.types.ts +15 -9
  199. package/src/handle-error.ts +1 -1
  200. package/src/index.ts +3 -22
  201. package/src/middleware/auth-cookie.test.ts +50 -0
  202. package/src/middleware/auth-cookie.ts +38 -2
  203. package/src/middleware/cors.ts +2 -1
  204. package/src/middleware/index.ts +1 -1
  205. package/src/middleware/remote-auth.test.ts +43 -0
  206. package/src/middleware/remote-auth.ts +17 -3
  207. package/src/middleware-runner.ts +4 -2
  208. package/src/no-any-casts.test.ts +57 -0
  209. package/src/permissions.test.ts +3 -1
  210. package/src/permissions.ts +22 -24
  211. package/src/pikku-state.ts +17 -6
  212. package/src/public-surface.json +578 -0
  213. package/src/public-surface.json.README +25 -0
  214. package/src/public-surface.test.ts +105 -0
  215. package/src/removed-legacy-exports.test.ts +62 -0
  216. package/src/schema.test.ts +78 -0
  217. package/src/schema.ts +36 -1
  218. package/src/services/ai-run-state-service.ts +7 -1
  219. package/src/services/audit-service.ts +2 -2
  220. package/src/services/in-memory-ai-run-state-service.ts +3 -2
  221. package/src/services/in-memory-workflow-service.ts +25 -0
  222. package/src/services/index.ts +1 -4
  223. package/src/services/local-content-request-handler.test.ts +43 -5
  224. package/src/services/local-content-request-handler.ts +103 -74
  225. package/src/services/local-content.ts +15 -1
  226. package/src/services/local-email-service.ts +5 -1
  227. package/src/services/system-role-guard.test.ts +4 -1
  228. package/src/services/workflow-service.ts +9 -2
  229. package/src/side-effects-are-declared.test.ts +84 -0
  230. package/src/source-files-stay-composable.test.ts +41 -0
  231. package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
  232. package/src/testing/service-tests/ai-storage-service-tests.ts +286 -0
  233. package/src/testing/service-tests/channel-store-tests.ts +98 -0
  234. package/src/testing/service-tests/credential-service-tests.ts +143 -0
  235. package/src/testing/service-tests/deployment-service-tests.ts +33 -0
  236. package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
  237. package/src/testing/service-tests/secret-service-tests.ts +105 -0
  238. package/src/testing/service-tests/session-store-tests.ts +62 -0
  239. package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
  240. package/src/testing/service-tests/workflow-service-tests.ts +308 -0
  241. package/src/testing/service-tests.ts +31 -1111
  242. package/src/types/core.types.ts +15 -1
  243. package/src/types/state.types.ts +1 -1
  244. package/src/utils/node-host-resolver.ts +20 -0
  245. package/src/utils/safe-fetch.test.ts +143 -1
  246. package/src/utils/safe-fetch.ts +200 -25
  247. package/src/wirings/actor-flow/run-conversation.ts +1 -6
  248. package/src/wirings/ai-agent/agent-rpc.ts +120 -0
  249. package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
  250. package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
  251. package/src/wirings/ai-agent/ai-agent-memory.ts +8 -1
  252. package/src/wirings/ai-agent/ai-agent-prepare.ts +10 -12
  253. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
  254. package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
  255. package/src/wirings/ai-agent/ai-agent-runner.ts +49 -120
  256. package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
  257. package/src/wirings/ai-agent/ai-agent-stream.ts +36 -63
  258. package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
  259. package/src/wirings/ai-agent/ai-agent-turn.ts +121 -0
  260. package/src/wirings/ai-agent/ai-agent.types.ts +1 -1
  261. package/src/wirings/ai-agent/voice-input.ts +1 -6
  262. package/src/wirings/ai-agent/voice-output.ts +2 -12
  263. package/src/wirings/channel/channel-common.ts +5 -2
  264. package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
  265. package/src/wirings/channel/channel-handler.ts +7 -7
  266. package/src/wirings/channel/channel-rpc-service.ts +0 -14
  267. package/src/wirings/channel/channel-rpc.test.ts +25 -4
  268. package/src/wirings/channel/channel-rpc.types.ts +14 -0
  269. package/src/wirings/channel/channel-runner.ts +33 -20
  270. package/src/wirings/channel/channel.types.ts +2 -0
  271. package/src/wirings/channel/local/local-channel-runner.ts +1 -1
  272. package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
  273. package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
  274. package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
  275. package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
  276. package/src/wirings/cli/cli-runner.ts +7 -4
  277. package/src/wirings/cli/cli.types.ts +0 -39
  278. package/src/wirings/cli/command-parser.test.ts +19 -0
  279. package/src/wirings/cli/command-parser.ts +17 -1
  280. package/src/wirings/gateway/gateway-authorization.test.ts +131 -0
  281. package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
  282. package/src/wirings/gateway/gateway-runner.ts +62 -22
  283. package/src/wirings/http/http-routes.ts +4 -1
  284. package/src/wirings/http/http-runner.ts +11 -26
  285. package/src/wirings/http/http.types.ts +0 -15
  286. package/src/wirings/http/index.ts +1 -7
  287. package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
  288. package/src/wirings/http/web-request.ts +1 -1
  289. package/src/wirings/mcp/mcp-runner.ts +2 -16
  290. package/src/wirings/persona/persona-environments.test.ts +14 -3
  291. package/src/wirings/persona/persona.test.ts +13 -3
  292. package/src/wirings/persona/validate-personas.ts +5 -1
  293. package/src/wirings/queue/queue-runner.ts +1 -1
  294. package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
  295. package/src/wirings/rpc/rpc-runner.ts +58 -136
  296. package/src/wirings/rpc/rpc-types.ts +7 -0
  297. package/src/wirings/rpc/wire-addon.ts +3 -1
  298. package/src/wirings/secret/validate-secret-definitions.test.ts +69 -0
  299. package/src/wirings/secret/validate-secret-definitions.ts +2 -0
  300. package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
  301. package/src/wirings/trigger/trigger-runner.ts +7 -5
  302. package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
  303. package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
  304. package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
  305. package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
  306. package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
  307. package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
  308. package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
  309. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -2
  310. package/src/wirings/workflow/feature.ts +7 -4
  311. package/src/wirings/workflow/graph/graph-node.ts +1 -1
  312. package/src/wirings/workflow/graph/graph-runner.test.ts +4 -2
  313. package/src/wirings/workflow/graph/graph-runner.ts +13 -16
  314. package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
  315. package/src/wirings/workflow/index.ts +12 -4
  316. package/src/wirings/workflow/pikku-scenario-service.ts +54 -25
  317. package/src/wirings/workflow/pikku-workflow-service.test.ts +2 -2
  318. package/src/wirings/workflow/pikku-workflow-service.ts +208 -630
  319. package/src/wirings/workflow/scenario-hooks.test.ts +51 -0
  320. package/src/wirings/workflow/workflow-approval.ts +185 -0
  321. package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
  322. package/src/wirings/workflow/workflow-constants.ts +47 -0
  323. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  324. package/src/wirings/workflow/workflow-dispatch-relay.test.ts +128 -0
  325. package/src/wirings/workflow/workflow-errors.ts +128 -0
  326. package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
  327. package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
  328. package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
  329. package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
  330. package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
  331. package/src/wirings/workflow/workflow-recovery.ts +162 -0
  332. package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
  333. package/src/wirings/workflow/workflow-run-authority.test.ts +215 -0
  334. package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
  335. package/src/wirings/workflow/workflow-run-ownership.ts +36 -0
  336. package/src/wirings/workflow/workflow-suspend.ts +61 -0
  337. package/src/wirings/workflow/workflow.types.ts +7 -0
  338. package/src/wirings-stay-decoupled.test.ts +123 -0
  339. package/tsconfig.json +1 -1
  340. package/tsconfig.tsbuildinfo +1 -1
  341. package/src/internal.ts +0 -10
@@ -0,0 +1,40 @@
1
+ ---
2
+ type: decision
3
+ title: The middleware resolution cache is deliberately unbounded
4
+ description: Its keyspace is the set of registered wires, not request traffic, and middleware is dynamic — so eviction would buy nothing and cost the dedupe guarantee
5
+ tags: core, middleware
6
+ ---
7
+
8
+ # The middleware resolution cache is deliberately unbounded
9
+
10
+ `middlewareCache` in `packages/core/src/middleware-runner.ts` is a plain
11
+ `Record<PikkuWiringTypes, Record<string, readonly CorePikkuMiddleware[]>>` with
12
+ no size limit and no eviction. That reads like an unbounded-growth bug and is
13
+ not one.
14
+
15
+ **The keyspace is finite by construction.** The cache is keyed by wire type and
16
+ wire id, and wire ids come from registration, not from traffic — an HTTP route
17
+ pattern, a channel name, a queue name. A million requests to the same route
18
+ produce one entry. Nothing a caller sends creates a key: a channel message key
19
+ is `${channel}:${routingProperty}:${routerValue}`, and `routerValue` is only
20
+ reached after matching `routes[routerValue]`, so an unregistered value never
21
+ gets that far.
22
+
23
+ **Middleware is dynamic, and the cache is invalidated rather than aged out.**
24
+ `addGlobalMiddleware` and `addTagMiddleware` can run after startup, and dev
25
+ hot-reload rewires wholesale — so correctness comes from `clearMiddlewareCache()`
26
+ at the points where the middleware set actually changes, not from entries
27
+ expiring. An LRU would evict entries that are still current while doing nothing
28
+ about entries that are stale.
29
+
30
+ **Eviction would also weaken the chain itself.** `combineMiddleware` returns a
31
+ `freezeDedupe`d array, and dedupe is by function identity — a middleware
32
+ reachable through both a tag group and a direct wire registration runs exactly
33
+ once. Callers hold that array. Recomputing it under memory pressure hands out a
34
+ second array for the same wire, which is churn at best and, for anything
35
+ comparing chains by identity, a silent behaviour change.
36
+
37
+ **What this rules out:** adding a `MIDDLEWARE_CACHE_MAX` with LRU eviction as a
38
+ denial-of-service mitigation. There is no traffic-driven growth to mitigate. If
39
+ a future change ever keys this cache by something a caller controls, that is the
40
+ bug — bound the key, not the cache.
@@ -0,0 +1,40 @@
1
+ ---
2
+ type: decision
3
+ title: The per-invocation rpc view is a class, because an object literal with a getter is slow to build
4
+ description: An accessor declared on an object literal is defined per instance, which drops the literal off V8's fast construction path — measured at 1.15µs against 0.47µs, on every request
5
+ tags: core, rpc, performance
6
+ ---
7
+
8
+ # The per-invocation rpc view is a class
9
+
10
+ `getContextRPCService` builds `wire.rpc` once per invocation. It used to return
11
+ an object literal, and that literal declared `get agent()` so the agent facade
12
+ stayed lazy — most requests never touch it, and reading it builds five closures.
13
+
14
+ The laziness is right. Declaring the accessor *on the literal* was not: a
15
+ literal containing an accessor needs a real property descriptor per instance,
16
+ which takes it off V8's fast object-literal construction path and slows the
17
+ whole object, not just the accessor.
18
+
19
+ Measured with `benchmarks/bench-profile-granular.ts`, three runs each:
20
+
21
+ | shape | per call |
22
+ | --- | --- |
23
+ | object literal with `get agent()` | 1.106 / 1.433 / 1.147 µs |
24
+ | class with `agent` on the prototype | 0.523 / 0.461 / 0.525 µs |
25
+
26
+ Roughly 2.4×, well outside the run-to-run variance, on a path every request
27
+ takes. On the same machine a full `fetchData` measures 12–19µs, so this was on
28
+ the order of a tenth of a request spent constructing one object.
29
+
30
+ The obvious alternative is worse. Making `agent` an eager property removes the
31
+ accessor but builds those five closures unconditionally, and measured *slower*
32
+ than the original at 1.967µs. A prototype accessor is the only shape that keeps
33
+ the laziness and the fast construction path.
34
+
35
+ **What this rules out:** "simplifying" `ContextRPCView` back to an object
36
+ literal. It reads as the plainer option and costs double. It also rules out
37
+ trusting a synthetic microbenchmark here — a standalone benchmark of these
38
+ shapes pointed at the eager version, because its stand-in for the agent getter
39
+ was too cheap to represent the real one. The numbers above come from the
40
+ profiler running the real code.
@@ -0,0 +1,28 @@
1
+ ---
2
+ type: decision
3
+ title: The persona runtime is exported from @pikku/core/persona, never from services
4
+ description: Those values reach the actor-flow and agent runners, which no production server runs — and an unbundled deploy loads whatever the import graph names
5
+ tags: core, services
6
+ ---
7
+
8
+ # The persona runtime lives on `@pikku/core/persona`
9
+
10
+ `HttpPersona`, `createHttpPersonas`, `readScenarioHttpResponse` and
11
+ `postScenarioJson` are exported from `@pikku/core/persona`, not from
12
+ `@pikku/core/services`. Their *types* are re-exported freely — TypeScript erases
13
+ those and they cost a bundle nothing.
14
+
15
+ The values are different. They reach `http-personas`, which reaches the
16
+ actor-flow conversation runner, which reaches the agent runner: an entire
17
+ scenario and virtual-user runtime that no production server executes. An
18
+ unbundled Node or Lambda deploy does no tree-shaking — it loads whatever the
19
+ import graph names — so exporting one value from the services barrel pulls all
20
+ of it into every application that imports a service.
21
+
22
+ This is enforced, not merely intended: `production-barrels-stay-lean.test.ts`
23
+ walks the value-import graph from `services/index.ts` and
24
+ `wirings/workflow/index.ts` and fails if any of those modules is reachable.
25
+
26
+ **What this rules out:** re-exporting a persona value from the services barrel
27
+ for convenience, and "just one small helper" — the graph is transitive, and one
28
+ value is enough to pull the whole runtime.
@@ -0,0 +1,25 @@
1
+ ---
2
+ type: decision
3
+ title: A voice turn's transcript is sent before the run starts, on the raw channel
4
+ description: The client does not know what it said, and an answer starts streaming within a few hundred milliseconds — a question arriving after its answer reads as the wrong question
5
+ tags: core, ai-agent
6
+ ---
7
+
8
+ # A voice turn's transcript is sent ahead of the run
9
+
10
+ A voice client sends audio, so it does not know what it said. Until the
11
+ transcript reaches it, its own message renders as a blank bubble.
12
+
13
+ The event is sent *before* the run rather than alongside it because the answer
14
+ begins streaming within a few hundred milliseconds. Sent concurrently, the
15
+ transcript routinely lands after the first tokens of its own answer — and a
16
+ question that appears beneath its answer reads as a question about something
17
+ else entirely.
18
+
19
+ It goes on the raw channel rather than through the stream middleware because it
20
+ is not part of the reply: it is what the user said, and the stream hooks are
21
+ built to transform what the agent says.
22
+
23
+ **What this rules out:** folding the transcript into the run's event stream for
24
+ uniformity. Ordering is the whole point, and the stream is where ordering is
25
+ least under this code's control.
@@ -0,0 +1,21 @@
1
+ ---
2
+ type: decision
3
+ title: The catalogue is the only gate on what a virtual user may call
4
+ description: Narrowing happens once at derivation, so there is no second check in the run loop that could drift out of step with it
5
+ tags: core, virtual-user
6
+ ---
7
+
8
+ # The catalogue is the only gate on what a virtual user may call
9
+
10
+ What a virtual user may invoke is decided once, when the catalogue is narrowed
11
+ for its disposition: a read-only disposition is never offered a mutation, and an
12
+ approval-gated endpoint is never offered at all.
13
+
14
+ The run loop then indexes into that catalogue and calls what it finds. There is
15
+ deliberately no second permission check at call time, because a second check is
16
+ a second thing to keep correct — and the failure mode of the two disagreeing is
17
+ that one of them silently stops mattering.
18
+
19
+ **What this rules out:** adding a "defence in depth" guard in the run loop. It
20
+ would not be defence in depth; it would be a duplicate of the derivation rules,
21
+ maintained separately, and the one that runs first wins.
@@ -0,0 +1,22 @@
1
+ ---
2
+ type: decision
3
+ title: The worker disposition is the only one not testing anything
4
+ description: It is doing the job, so it abandons rarely, runs cool, and is told to stop and say so rather than guess
5
+ tags: core, virtual-user
6
+ ---
7
+
8
+ # The worker disposition is the only one not testing anything
9
+
10
+ Every other disposition exists to probe the product. The worker exists to *use*
11
+ it, which changes every parameter: it abandons rarely, runs at a low temperature,
12
+ and is instructed to stop and report rather than guess its way past an obstacle.
13
+
14
+ The failure mode being guarded against is different too. For the probing
15
+ dispositions the risk is a missed bug. For this one it is a wrong action nobody
16
+ asked for — a real mutation performed because the model improvised when it
17
+ should have stopped.
18
+
19
+ **What this rules out:** tuning this disposition toward the others for
20
+ consistency, in particular raising its temperature or its tolerance for
21
+ guessing. Those settings are what make the others useful and what would make this
22
+ one dangerous.
@@ -0,0 +1,25 @@
1
+ ---
2
+ type: decision
3
+ title: Thread history records what the model was asked, which for a spoken turn is the transcript
4
+ description: The wire carried a base64 audio blob; persisting it writes megabytes of unreadable data and loses the only readable record of the turn
5
+ tags: core, ai-agent
6
+ ---
7
+
8
+ # Thread history records the transcript, not the audio that arrived
9
+
10
+ What goes into thread history is what the model was actually asked. For a typed
11
+ turn that is what arrived over the wire. For a spoken turn it is not: the wire
12
+ carried a base64 audio blob, and `voiceInput` replaced it with a transcript
13
+ before the model ever saw it.
14
+
15
+ Persisting the blob would write megabytes of unreadable data into the history
16
+ *and* discard the only readable record of what was said — the worst of both.
17
+
18
+ Both the streaming and non-streaming paths do this, and both check identity
19
+ rather than assuming: a middleware is free to rewrite the message list into
20
+ something with no relation to this turn, and in that case there is no transcript
21
+ to substitute and the list is persisted as-is.
22
+
23
+ **What this rules out:** persisting the inbound message verbatim on the grounds
24
+ that it is the ground truth. For voice it is the least useful representation
25
+ available, and the substitution is exactly what makes the history readable.
@@ -0,0 +1,21 @@
1
+ ---
2
+ type: decision
3
+ title: A scenario's step order for a virtual user is insertion order, not a graph traversal
4
+ description: The CLI writes nodes in declaration order, so following `next` would buy an ordering that is already true and cost a traversal that has to interpret branches
5
+ tags: core, virtual-user
6
+ ---
7
+
8
+ # Step order comes from insertion order, not a graph traversal
9
+
10
+ When a scenario becomes catalogue entries, its steps are read in the order the
11
+ nodes appear — which is the order the CLI wrote them, which is the order the
12
+ scenario declares them.
13
+
14
+ Following `next` edges instead would produce the same ordering in the ordinary
15
+ case, and in the branching case it would force a decision nobody needs: which
16
+ arm of a branch to present to a reader who will never take one. The traversal
17
+ costs work to reproduce a fact that already holds.
18
+
19
+ **What this rules out:** "correcting" this to a topological walk because a graph
20
+ is present. The graph describes execution; the catalogue describes what the
21
+ scenario *says*, and those are read in declaration order.
@@ -0,0 +1,24 @@
1
+ ---
2
+ type: decision
3
+ title: Only an explicit `false` silences a spoken reply
4
+ description: The key being absent means no voice input is wired and nothing reported either way, so those callers keep the behaviour they had before the option existed
5
+ tags: core, ai-agent
6
+ ---
7
+
8
+ # Only an explicit `false` silences a spoken reply
9
+
10
+ `voiceOutput` speaks a reply unless the turn is explicitly marked as not having
11
+ arrived by voice. The distinction is between three states, not two:
12
+
13
+ - `true` — `voiceInput` handled this turn and it carried speech.
14
+ - `false` — `voiceInput` handled this turn and a real user really typed it.
15
+ - absent — no voice input is wired at all, so nothing reported either way.
16
+
17
+ Treating absent as `false` would silence every caller who wired `voiceOutput`
18
+ without `voiceInput`, changing behaviour that worked before the option existed.
19
+ Treating absent as `true` would speak replies to typists on any stack where the
20
+ flag never gets set.
21
+
22
+ **What this rules out:** normalising the flag to a boolean at any point between
23
+ `voiceInput` and `voiceOutput` — that collapses absent into one of the other two
24
+ and picks a wrong answer for somebody.
@@ -0,0 +1,35 @@
1
+ ---
2
+ type: decision
3
+ title: Wiring registries erase the generics their wire* functions capture
4
+ description: A registry is typed at unknown so every wiring shares one map; storing a generic config in it needs an assertion, because function parameters are contravariant
5
+ tags: core
6
+ ---
7
+
8
+ # Wiring registries erase the generics their `wire*` functions capture
9
+
10
+ Every `wire*` entry point — `wireTrigger`, `wireTriggerSource`, `wireChannel`,
11
+ `wireQueueWorker`, and the rest — is generic in the shape a caller declares, so
12
+ the caller gets a checked `func`. The registry each one writes to is not:
13
+ `pikkuState(null, 'trigger', 'triggers')` is a
14
+ `Map<string, CoreTriggerSource>` where `CoreTriggerSource` fixes input and
15
+ output at `unknown`, because one map has to hold every trigger in the app.
16
+
17
+ Handing `CoreTriggerSource<TInput, TOutput>` to a slot typed
18
+ `CoreTriggerSource<unknown, unknown>` is not an upcast. `func` takes its input
19
+ as a *parameter*, and parameters are contravariant: a function that accepts
20
+ `TInput` cannot stand in for one that accepts `unknown`, since `unknown` admits
21
+ values `TInput` does not. TypeScript is right to reject it, and no variance
22
+ annotation makes it go away — the registry genuinely holds functions whose input
23
+ types it cannot name.
24
+
25
+ The runtime is nonetheless sound, because the only thing that ever invokes a
26
+ registered `func` is the function runner, which validates the incoming data
27
+ against that function's own generated schema before the call. The type the
28
+ registry lost is re-established at the call boundary by the schema, not by the
29
+ type system.
30
+
31
+ **What this rules out:** deleting these assertions as if they were noise — the
32
+ code does not compile without them. It equally rules out `as any` in their
33
+ place: `as any` discards the target type too, so a genuinely wrong config would
34
+ also slip through. Assert to the registry's own element type and let everything
35
+ except the erased generic stay checked.
@@ -13,8 +13,10 @@ Its `write` discards the event — but before it does, it warns once, and it loo
13
13
  for a logger in two places: the wire's own, then the singleton passed to the
14
14
  constructor.
15
15
 
16
- The fallback is the point. Wires frequently do not carry a logger, and with only
17
- `this.wire.logger` the warning would itself be dropped whenever the wire is bare.
16
+ The fallback is the point. `wire.logger` is an optional hook for a host that
17
+ wants invocation-scoped logging core never sets it, so on every path core
18
+ itself drives, the singleton is the only source there is. With only
19
+ `this.wire.logger` the warning would be dropped every time.
18
20
  An audit call that silently does nothing is the worst available outcome: the
19
21
  function believes it is producing an audit trail, the trail does not exist, and
20
22
  nothing anywhere says so. The warning names the function and the fix, and fires
@@ -0,0 +1,33 @@
1
+ ---
2
+ type: decision
3
+ title: A graph run starts at an entry node the graph declared
4
+ description: startNode may only name a node in meta.entryNodeIds, and no generated route offers it, because otherwise a caller picks which half of the graph to skip
5
+ tags: workflow
6
+ ---
7
+
8
+ # A graph run starts at an entry node the graph declared
9
+
10
+ `runWorkflowGraph` in
11
+ `packages/core/src/wirings/workflow/graph/graph-runner.ts` takes an optional
12
+ `startNode`. It used to accept any node id the graph contained, validated only
13
+ by `validateGraphReferences` (does the node exist) and `areDependenciesSatisfied`
14
+ (does its input reference another node). A node whose input is a literal or comes
15
+ from the trigger passes both — including a node that is only ever reached after a
16
+ validation, payment or approval node. Choosing it as the start does not skip the
17
+ gate so much as never reach it.
18
+
19
+ `startNode` is now checked against `meta.entryNodeIds`, the set the graph itself
20
+ declared, and the generated `POST /workflow/:workflowName/graph/:nodeId` route
21
+ that offered it to HTTP callers is gone.
22
+
23
+ The parameter stays, because it has a real internal user: `PikkuTriggerService`
24
+ (`packages/core/src/wirings/trigger/pikku-trigger-service.ts`) passes a target's
25
+ `startNode` when a trigger fires. That caller names a node the graph declared, so
26
+ the restriction costs it nothing.
27
+
28
+ **What this rules out:** re-exposing entry-node choice on a generated route, and
29
+ "validating" a `startNode` by existence or by dependency satisfaction — neither
30
+ says the graph meant that node to be an entry point. It does not rule out a graph
31
+ declaring several entry nodes; that is exactly how a graph says which starts are
32
+ legitimate. If a graph needs a start that is not an entry node, the answer is to
33
+ declare it as one, not to widen the check.
@@ -0,0 +1,33 @@
1
+ ---
2
+ type: decision
3
+ title: A permission gets a wire it cannot reply on
4
+ description: The permission wire is typed with Out = never so a permission cannot send on the channel; that narrowing is not a subtype, so the call site asserts
5
+ tags: core, permissions
6
+ ---
7
+
8
+ # A permission gets a wire it cannot reply on
9
+
10
+ `CorePikkuPermission` in `packages/core/src/function/functions.types.ts` types
11
+ its wire as `PikkuWire<In, never, false, any, PikkuRPC, never, never>`. The
12
+ second parameter is `Out`, and `never` there is deliberate: it makes
13
+ `wire.channel` a `PikkuChannel<unknown, never, …>`, whose `send` accepts
14
+ nothing. A permission is a gate — it answers `true` or `false` — and must not
15
+ be able to write a reply to the caller it is deciding about. A permission that
16
+ could `send` would be able to leak the very data the gate exists to withhold,
17
+ and it would do so before the function it guards has run.
18
+
19
+ `runPikkuFunc` holds an ordinary `PikkuWire`, whose default `Out` is `unknown`.
20
+ `unknown` is not assignable to `never`, and `send` is contravariant in its
21
+ argument, so the narrowing cannot be expressed as a subtype relation — the call
22
+ into `runPermissions` asserts to the exact permission-wire type rather than
23
+ relying on assignability.
24
+
25
+ **What this rules out:** widening `Out` on the permission wire to `unknown` to
26
+ delete the assertion. That silently hands every permission function a working
27
+ `channel.send`. It equally rules out replacing the assertion with `as any`,
28
+ which erases the target type and hides the fact that a specific, intentional
29
+ narrowing is happening.
30
+
31
+ The same reasoning explains the `never` in the fifth and sixth positions
32
+ (`MCPTools`, `IsChannel`): a permission is not an MCP tool host and does not own
33
+ the channel lifecycle.
@@ -0,0 +1,37 @@
1
+ ---
2
+ type: decision
3
+ title: A step runs the function the workflow dispatched it with
4
+ description: StepState records the step's function name so the worker can reject a queue message naming a different one, because the step executes under the run owner's identity
5
+ tags: workflow
6
+ ---
7
+
8
+ # A step runs the function the workflow dispatched it with
9
+
10
+ `pikkuWorkflowStepWorker` (`workflow-queue-workers.ts`) takes `rpcName` straight
11
+ off the queue message and hands it to `executeWorkflowStep`. That call runs as
12
+ the run's owner — `invokeStepRpc` copies `run.wire.pikkuUserId` onto the wire —
13
+ and `rpcWithWire` does not apply the `expose` gate that the public `/rpc` route
14
+ applies. So the message decided both *what* ran and *as whom*.
15
+
16
+ The claim in `executeWorkflowStepInner` read the step's status and nothing else;
17
+ there was no stored function name to compare against. `StepState` now carries
18
+ `rpcName`, written by `insertStepState` (which already received it) and returned
19
+ by every backend, and the claim rejects a message naming anything else with
20
+ `WorkflowStepFunctionMismatchError` — before any status is mutated, so a forged
21
+ message leaves the run untouched. The graph path takes the same value from
22
+ `nodes[nodeId].rpcName` rather than from the message.
23
+
24
+ `rpcName: undefined` means a store that never recorded one and cannot be
25
+ compared; `null` is a step with no function of its own. Only a recorded value is
26
+ checked.
27
+
28
+ **Reachability, stated plainly:** the step-worker queue is not reachable from the
29
+ public `/rpc/:rpcName` route — the worker is registered without `expose`. The
30
+ exposure this closes is write access to the queue backend, and any in-process
31
+ caller reaching `executeWorkflowStep` directly.
32
+
33
+ **What this rules out:** trusting a queue payload to name the function it
34
+ executes, here or in any future worker, and dropping `rpcName` from `StepState`
35
+ as redundant with the step data — the comparison is the only thing standing
36
+ between queue-write access and running any registered function as the run's
37
+ owner.
@@ -0,0 +1,28 @@
1
+ ---
2
+ type: decision
3
+ title: A virtual user is never offered a scenario, platform or addon step
4
+ description: Being able to invoke "the webhook arrives" lets the user manufacture the outcome it exists to discover, which invalidates every finding downstream
5
+ tags: core, virtual-user
6
+ ---
7
+
8
+ # A virtual user is never offered a scenario, platform or addon step
9
+
10
+ `deriveVirtualUserCatalogue` excludes scenario bodies and their steps, and
11
+ platform and addon steps, from what a virtual user may call.
12
+
13
+ For scenario bodies the argument is only efficiency: they are held out of every
14
+ deployed unit and are not network-callable, so offering one wastes a turn on a
15
+ 404.
16
+
17
+ For a platform or addon step the argument is the oracle itself. A virtual user's
18
+ findings are worth something *because* it cannot manufacture the outcomes it is
19
+ meant to discover. A user that could invoke "Stripe's webhook arrives" forges its
20
+ own payment success, and every finding downstream of that forgery is worthless —
21
+ not merely unreliable, but actively misleading, because it looks like evidence.
22
+
23
+ This is the same class of argument as `allowApprovalRequired` defaulting to
24
+ false, and it is enforced here at derivation rather than left to convention.
25
+
26
+ **What this rules out:** exposing platform steps behind a flag "for
27
+ convenience", or filtering them later in the run loop where a caller could skip
28
+ the filter.
@@ -0,0 +1,34 @@
1
+ ---
2
+ type: decision
3
+ title: A workflow run is read and approved by its owner
4
+ description: A run started through a session records that user and only that user may read it or answer its approval gates; a run with no recorded owner has no ownership to enforce
5
+ tags: workflow
6
+ ---
7
+
8
+ # A workflow run is read and approved by its owner
9
+
10
+ `WorkflowRunWire.pikkuUserId` has always been recorded on every run started
11
+ through a session — `RPCService.startWorkflow` copies it off the wire — and
12
+ nothing read it back. Run ids were the only secret protecting both the status
13
+ routes (which stream `output` and `error`) and `approveStep`, which took no
14
+ session at all and rejected only an already-resolved gate.
15
+
16
+ `assertWorkflowRunOwner`
17
+ (`packages/core/src/wirings/workflow/workflow-run-ownership.ts`) is the one check
18
+ both paths share. `approveStep` takes the caller's session and asserts it, and
19
+ the generated status routes assert it against the run they were already reading.
20
+
21
+ **A run with no recorded owner is not gated.** Triggers, schedulers and routes
22
+ wired without auth start runs with no `pikkuUserId`; there is nobody to compare a
23
+ caller against, and inventing one would reject the framework's own callers rather
24
+ than secure anything. Gate those at the entrypoint with `auth` or `permissions`.
25
+
26
+ This is ownership, not an approver model. It answers "is this your run", not "are
27
+ you entitled to approve this particular gate" — `WorkflowApprovalOptions` still
28
+ carries no approver, role or permission, and a second approver on someone else's
29
+ run is still a matter for the route's own `permissions`.
30
+
31
+ **What this rules out:** treating a run id as a capability, and adding a new run
32
+ read path that does not take a session. It does not rule out a richer approver
33
+ model on `WorkflowApprovalOptions` later; that would narrow this gate, never
34
+ replace it.
@@ -0,0 +1,33 @@
1
+ ---
2
+ type: decision
3
+ title: An agent approval is claimed before the tool runs
4
+ description: resolveApproval is a compare-and-swap returning whether this caller won, because the read that precedes it is not a claim and ten concurrent approvals would otherwise mean ten refunds
5
+ tags: ai-agent
6
+ ---
7
+
8
+ # An agent approval is claimed before the tool runs
9
+
10
+ Resuming a suspended agent run read the run, checked `status === 'suspended'`,
11
+ snapshotted `pendingApprovals`, called `resolveApproval`, executed the tool, and
12
+ only then wrote `status: 'running'`. Nothing spanned that sequence — no
13
+ transaction, no row lock, no conditional update — so concurrent approvals of the
14
+ same tool call all observed `suspended`, all snapshotted the same list, and all
15
+ reached `execute`. `resolveApproval` returned `void`, so a loser could not even
16
+ tell.
17
+
18
+ `resolveApproval` is now the claim, and returns whether *this* caller made it.
19
+ The stores implement it as a compare-and-swap: Kysely updates the run row only
20
+ while `status = 'suspended'` and `pendingApprovals` still equals the list it
21
+ read; the tool-call stores move the row off `approvalStatus = 'pending'` and
22
+ count the rows they changed. Both resume paths run the tool only for the ids they
23
+ claimed, and a caller that claimed nothing gets an error rather than a silent
24
+ re-run.
25
+
26
+ The claim is per tool call, not per run, so concurrent approvals of *different*
27
+ tool calls on one run all proceed — which is the case that made a run-level
28
+ `claimSuspendedRun` the worse fit.
29
+
30
+ **What this rules out:** treating `getRun` as a claim, adding a resume path that
31
+ executes a tool without a `true` from `resolveApproval`, and implementing
32
+ `resolveApproval` as a read-modify-write in any new store — the return value is
33
+ a promise about atomicity, not a convenience.
@@ -0,0 +1,24 @@
1
+ ---
2
+ type: decision
3
+ title: An upload is counted as it arrives, not buffered and then measured
4
+ description: Reading the whole body before checking its size hands an unauthenticated caller a way to spend the server's memory
5
+ tags: core, content
6
+ ---
7
+
8
+ # An upload is counted as it arrives, not buffered and then measured
9
+
10
+ The local content request handler accumulates the request body chunk by chunk,
11
+ tracking the running total, and abandons the read the moment it crosses the
12
+ limit. An oversized upload therefore costs the limit, not its own size.
13
+
14
+ The obvious alternative — `await request.arrayBuffer()` and then check
15
+ `byteLength` — has to hold the entire body in memory before it can decide the
16
+ body is too large. For an endpoint reachable before authentication that is a
17
+ way to spend the server's memory for the price of one request, and no size limit
18
+ configured anywhere prevents it.
19
+
20
+ `readRequestBody` in the node HTTP server aborts on the same terms, for the same
21
+ reason.
22
+
23
+ **What this rules out:** replacing the streaming accumulation with a single
24
+ buffered read because the limit check "still happens".
@@ -17,7 +17,16 @@ runner's gate is the only thing that evaluates a function's declared `auth`,
17
17
  `scopes` and `permissions`; a direct call runs the handler with none of them
18
18
  checked.
19
19
 
20
- The handler is registered with `sessionless: true`. Gateway inbound traffic is
20
+ The synthetic registration **inherits the wired function's own metadata**
21
+ `sessionless`, input and output schema names, `scopes`, tag middleware — read
22
+ back from `pikkuState(null, 'gateway', 'meta')[name].pikkuFuncId`, which the
23
+ inspector already records. The synthetic id exists so the gate runs; it is not a
24
+ licence to run the handler under metadata its author never wrote. A handler
25
+ declared with `pikkuFunc` says session-required in meta rather than through an
26
+ `auth` property, and that declaration is honoured.
27
+
28
+ When nothing was declared — no inspector entry, as when a gateway is wired by
29
+ hand — the handler falls back to `sessionless: true`. Gateway inbound traffic is
21
30
  authenticated by the platform adapter (webhook signature verification, platform
22
31
  tokens), not by a user session, so defaulting to session-required would reject
23
32
  every legitimate webhook. `CoreGateway.auth` and the handler's own `auth: true`
@@ -26,6 +35,7 @@ whenever declared, session or not.
26
35
 
27
36
  **What this rules out:** invoking `config.func` directly from any gateway
28
37
  transport as an optimisation, and "simplifying" the synthetic function
29
- registration away. It also rules out flipping the sessionless default to
30
- session-required as a hardening measure that breaks every webhook rather than
38
+ registration away. It also rules out fabricating the handler's metadata rather
39
+ than inheriting it, and flipping the _fallback_ to session-required as a
40
+ hardening measure — that breaks every webhook that declared nothing, rather than
31
41
  securing it; require auth per gateway via `CoreGateway.auth` instead.
@@ -10,6 +10,11 @@ A rule about who may do what, and which way it fails when it is unsure.
10
10
 
11
11
  <!-- pikku:knowledge-index -->
12
12
  - [A dropped audit write is always logged](a-dropped-audit-write-is-always-logged.md) — The no-op audit service falls back to the singleton logger when the wire carries none, so an unconfigured audit call is never silent
13
+ - [A graph run starts at an entry node the graph declared](a-graph-run-starts-at-an-entry-node-the-graph-declared.md) — startNode may only name a node in meta.entryNodeIds, and no generated route offers it, because otherwise a caller picks which half of the graph to skip
14
+ - [A permission gets a wire it cannot reply on](a-permission-gets-a-wire-it-cannot-reply-on.md) — The permission wire is typed with Out = never so a permission cannot send on the channel; that narrowing is not a subtype, so the call site asserts
15
+ - [A step runs the function the workflow dispatched it with](a-step-runs-the-function-the-workflow-dispatched-it-with.md) — StepState records the step's function name so the worker can reject a queue message naming a different one, because the step executes under the run owner's identity
16
+ - [A virtual user is never offered a scenario, platform or addon step](a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md) — Being able to invoke "the webhook arrives" lets the user manufacture the outcome it exists to discover, which invalidates every finding downstream
17
+ - [A workflow run is read and approved by its owner](a-workflow-run-is-read-and-approved-by-its-owner.md) — A run started through a session records that user and only that user may read it or answer its approval gates; a run with no recorded owner has no ownership to enforce
13
18
  - [An actor's missing approval decision defaults to denied](actor-flow-missing-approval-decisions-default-to-denied.md) — Every pending tool call gets an explicit decision; an id the persona LLM omitted is denied, so a dropped field can never read as consent
14
19
  - [Actor sign-in is proven by Set-Cookie, not a non-empty jar](actor-sign-in-is-proven-by-set-cookie-not-a-non-empty-jar.md) — HttpScenarioActor tracks its own signedIn flag and requires the sign-in response itself to set a cookie, because a populated jar proves nothing
15
20
  - [Actor sign-in only works for actor-flagged users](actor-sign-in-only-works-for-actor-flagged-users.md) — The scenario actor secret mints sessions for user rows flagged actor and nothing else, so holding it never impersonates a real user
@@ -24,8 +29,10 @@ A rule about who may do what, and which way it fails when it is unsure.
24
29
  - [Agent thread ownership fails closed when there is no principal](ai-agent-sessionless-deployments-have-no-thread-ownership.md) — A sessionless caller gets an ephemeral owner and reaches no stored thread, rather than reaching all of them
25
30
  - [An agent thread key is always prefixed with the trusted principal](ai-agent-thread-ownership-composes-the-session-principal.md) — Ownership keys are composed as principal:resourceId, so a client id can sub-divide its own boundary but never widen it
26
31
  - [Agent tool permission filtering reads the live function config, not the metadata](ai-agent-tool-filtering-reads-the-live-function-config.md) — The pikkuAuth brand survives only on live permission objects, so a metadata-driven check would silently admit every gated tool
32
+ - [An agent approval is claimed before the tool runs](an-agent-approval-is-claimed-before-the-tool-runs.md) — resolveApproval is a compare-and-swap returning whether this caller won, because the read that precedes it is not a claim and ten concurrent approvals would otherwise mean ten refunds
27
33
  - [An empty owners constraint matches nothing](an-empty-owners-constraint-matches-nothing.md) — owners is an authorization boundary, so every storage backend must treat [] as no rows rather than no filter
28
34
  - [An exposed function with no gate is reported at codegen, not at boot](an-exposed-ungated-function-is-a-codegen-warning.md) — The check runs in the inspector where function meta and every wireAddon declaration are both in hand, because neither source alone can tell a gated function from an ungated one
35
+ - [An upload is counted as it arrives, not buffered and then measured](an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md) — Reading the whole body before checking its size hands an unauthenticated caller a way to spend the server's memory
29
36
  - [The console addon's privileged functions gate themselves](console-addon-privileged-functions-gate-themselves.md) — Thread listing is owner-scoped unless the caller holds admin, and addon installation requires an admin session, rather than trusting the host to register a global permission
30
37
  - [Core's SSRF guard matches host literals because edge runtimes have no DNS](core-safe-fetch-blocks-ssrf-by-host-literal-not-dns.md) — safeFetch rejects internal address literals and re-validates every redirect hop; it cannot stop DNS rebinding
31
38
  - [Core secrets are encrypted with a per-secret DEK wrapped by a KEK](core-secrets-use-a-per-secret-dek-wrapped-by-a-kek.md) — Envelope encryption keeps ciphertext untouched during key rotation, at the cost of storing two blobs per secret
@@ -0,0 +1,35 @@
1
+ ---
2
+ type: question
3
+ title: A channel's middleware list accepts bare factories that nothing ever resolves
4
+ description: CoreChannel.channelMiddleware admits CorePikkuChannelMiddlewareFactory, but no runner calls one, so a bare factory would stall the chain
5
+ tags: core, channel
6
+ ---
7
+
8
+ # A channel's middleware list accepts bare factories that nothing resolves
9
+
10
+ `CoreChannel.channelMiddleware` is typed as
11
+ `(CorePikkuChannelMiddleware | CorePikkuChannelMiddlewareFactory)[]`, but
12
+ `runPikkuFunc`'s `wireChannelMiddleware` parameter admits only
13
+ `CorePikkuChannelMiddleware[]`. The two call sites that bridge them —
14
+ `channel-common.ts` and `channel-handler.ts` — assert across the gap.
15
+
16
+ A factory is `(input: In) => CorePikkuChannelMiddleware`: the author is meant to
17
+ *call* it at wiring time and put the result in the array.
18
+ `combineChannelMiddleware` contains no factory-resolution branch, and neither
19
+ does `combineMiddleware` for ordinary middleware — that is consistent and
20
+ deliberate. So a bare, uncalled factory in `channelMiddleware` would be pushed
21
+ into the chain and then invoked as if it were middleware: it would receive
22
+ `(services, channel, next)` as its single `input` argument, return a middleware
23
+ function that nothing runs, and never call `next` — stalling the chain silently.
24
+
25
+ Nothing in this repository puts a bare factory there, so the failure is
26
+ hypothetical today. The type is what invites it.
27
+
28
+ **What would settle it:** deciding whether `channelMiddleware` should accept
29
+ only resolved middleware — in which case the union is simply wrong and should
30
+ lose its factory arm, and both assertions disappear — or whether the runner
31
+ should resolve factories, in which case `combineChannelMiddleware` needs a
32
+ branch and the ordinary middleware path probably needs the matching one.
33
+
34
+ Until then the assertions name the exact target type rather than `as any`, so
35
+ the gap is visible at both call sites.