@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
@@ -1,6 +1,15 @@
1
1
  import { UnauthorizedError } from '../errors/errors.js';
2
2
  import { pikkuMiddleware } from '../types/core.types.js';
3
3
  import { assertStrongKeyMaterial, decryptWithKeyMaterial, REMOTE_SESSION_INFO, } from '../crypto-utils.js';
4
+ const REMOTE_RPC_PREFIX = '/remote/rpc/';
5
+ /**
6
+ * Whether this request targets the remote-RPC surface. Compared case-insensitively
7
+ * because the router matches routes that way (path-to-regexp defaults to
8
+ * `sensitive: false`), so `/Remote/RPC/fn` reaches the same handler as
9
+ * `/remote/rpc/fn`. A case-sensitive check here let a case-varied path slip
10
+ * past the trust gate and the token's `fn` binding while still being dispatched.
11
+ */
12
+ const isRemoteRpcPath = (path) => path.toLowerCase().startsWith(REMOTE_RPC_PREFIX);
4
13
  export const pikkuRemoteAuthMiddleware = pikkuMiddleware(async ({ secrets, jwt }, { http, setSession }, next) => {
5
14
  if (!http?.request || !secrets) {
6
15
  return next();
@@ -10,7 +19,7 @@ export const pikkuRemoteAuthMiddleware = pikkuMiddleware(async ({ secrets, jwt }
10
19
  secret = (await secrets.getSecret('PIKKU_REMOTE_SECRET')).reveal();
11
20
  }
12
21
  catch {
13
- if (http.request.path().startsWith('/remote/rpc/')) {
22
+ if (isRemoteRpcPath(http.request.path())) {
14
23
  throw new UnauthorizedError();
15
24
  }
16
25
  return next();
@@ -38,8 +47,10 @@ export const pikkuRemoteAuthMiddleware = pikkuMiddleware(async ({ secrets, jwt }
38
47
  if (payload?.aud !== 'pikku-remote') {
39
48
  throw new UnauthorizedError();
40
49
  }
41
- if (payload?.fn && http.request.path().startsWith('/remote/rpc/')) {
42
- const fn = decodeURIComponent(http.request.path().slice('/remote/rpc/'.length));
50
+ if (payload?.fn && isRemoteRpcPath(http.request.path())) {
51
+ // The prefix length is the same in any case, so slicing the raw path
52
+ // keeps the requested function name in its original case for the compare.
53
+ const fn = decodeURIComponent(http.request.path().slice(REMOTE_RPC_PREFIX.length));
43
54
  if (fn && payload.fn !== fn) {
44
55
  throw new UnauthorizedError();
45
56
  }
@@ -1,20 +1,18 @@
1
1
  import type { CoreServices, CoreUserSession, PikkuWire } from './types/core.types.js';
2
+ import type { PikkuRPC } from './wirings/rpc/rpc-types.js';
3
+ /**
4
+ * The wire a permission function sees. `Out = never` denies it `channel.send`.
5
+ *
6
+ * knowledge: decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md
7
+ */
8
+ export type PermissionWire = PikkuWire<any, never, false, any, PikkuRPC, never, never>;
2
9
  import type { CorePermissionGroup, CorePikkuPermission } from './function/functions.types.js';
3
10
  export declare const clearPermissionsCache: () => void;
4
11
  export declare const addGlobalPermission: (permissions: CorePermissionGroup | CorePikkuPermission[], packageName?: string | null) => CorePermissionGroup | CorePikkuPermission[];
5
- /**
6
- * @deprecated Tag-level permissions were removed in #972 — permissions are now
7
- * function-scoped only (declare them on the function via `pikkuFunc({ permissions })`).
8
- * This throwing stub exists solely so the pinned bootstrap CLI (which still
9
- * generates an `addTagPermission` wrapper) can resolve the import at build time;
10
- * it is never called. Delete once `PIKKU_CLI_VERSION` in the CLI build is bumped
11
- * past the release that removes tag permissions.
12
- */
13
- export declare const addTagPermission: (_tag: string, _permissions: CorePermissionGroup | CorePikkuPermission[], _packageName?: string | null) => never;
14
12
  export declare const runPermissions: ({ funcPermissions, services, wire, data, packageName, label, }: {
15
13
  funcPermissions?: CorePermissionGroup | CorePikkuPermission[];
16
14
  services: CoreServices;
17
- wire: PikkuWire<any, never, any, CoreUserSession, never, never, never>;
15
+ wire: PermissionWire;
18
16
  data: any;
19
17
  packageName?: string | null;
20
18
  /** What the non-global gate is called in debug logs, e.g. 'function', 'agent'. */
@@ -40,17 +40,6 @@ export const addGlobalPermission = (permissions, packageName = null) => {
40
40
  clearPermissionsCache();
41
41
  return permissions;
42
42
  };
43
- /**
44
- * @deprecated Tag-level permissions were removed in #972 — permissions are now
45
- * function-scoped only (declare them on the function via `pikkuFunc({ permissions })`).
46
- * This throwing stub exists solely so the pinned bootstrap CLI (which still
47
- * generates an `addTagPermission` wrapper) can resolve the import at build time;
48
- * it is never called. Delete once `PIKKU_CLI_VERSION` in the CLI build is bumped
49
- * past the release that removes tag permissions.
50
- */
51
- export const addTagPermission = (_tag, _permissions, _packageName = null) => {
52
- throw new Error('addTagPermission was removed in #972 — tag-level permissions no longer exist. Declare permissions on the function definition instead: pikkuFunc({ permissions }).');
53
- };
54
43
  const bucket = (packageName) => pikkuState(packageName, 'permissions', 'global') ?? [];
55
44
  /**
56
45
  * Global permissions in effect for a function, root ones first.
@@ -1,10 +1,17 @@
1
1
  const PIKKU_STATE_KEY = Symbol.for('@pikku/core/state');
2
+ /**
3
+ * State is parked on `globalThis` under a registered symbol so separate copies
4
+ * of @pikku/core in one process share it. Typed once here rather than at each
5
+ * access.
6
+ */
7
+ const stateHost = globalThis;
2
8
  export const getAllPackageStates = () => {
3
- if (!globalThis[PIKKU_STATE_KEY]) {
4
- ;
5
- globalThis[PIKKU_STATE_KEY] = new Map();
9
+ let states = stateHost[PIKKU_STATE_KEY];
10
+ if (!states) {
11
+ states = new Map();
12
+ stateHost[PIKKU_STATE_KEY] = states;
6
13
  }
7
- return globalThis[PIKKU_STATE_KEY];
14
+ return states;
8
15
  };
9
16
  export const pikkuState = (packageName, type, content, value) => {
10
17
  const resolvedPackageName = packageName ?? '__main__';
@@ -120,7 +127,7 @@ export const initializePikkuState = (packageName) => {
120
127
  export const resetPikkuState = () => {
121
128
  // Error definitions come from module-import side effects that never re-run.
122
129
  const existingErrors = getAllPackageStates().get('__main__')?.misc.errors;
123
- globalThis[PIKKU_STATE_KEY] = new Map();
130
+ stateHost[PIKKU_STATE_KEY] = new Map();
124
131
  initializePikkuState('__main__');
125
132
  if (existingErrors) {
126
133
  const mainState = getAllPackageStates().get('__main__');
package/dist/schema.js CHANGED
@@ -98,6 +98,40 @@ export const coerceTopLevelDataFromSchema = (schemaName, data, packageName = nul
98
98
  }
99
99
  }
100
100
  };
101
+ /**
102
+ * Drops object properties whose value is literally `undefined`.
103
+ *
104
+ * JSON Schema has no way to describe such a property, and the validator refuses
105
+ * the whole instance rather than reporting it — `{ a: undefined }` throws
106
+ * "Instances of \"undefined\" type are not supported" instead of validating.
107
+ *
108
+ * That matters because whether a payload can contain one depends on how the
109
+ * call travelled. Over HTTP `JSON.stringify` drops these keys before the
110
+ * request is built, so they never arrive. Dispatched in-process — an inline
111
+ * workflow step calling an RPC — the object is handed over as-is, and
112
+ * `{ retries: data.maybeRetries }` with the field omitted by the caller is
113
+ * enough to kill the step. Normalising here makes the two paths agree, and an
114
+ * explicitly-undefined *required* field now reports as the missing property it
115
+ * is rather than as an internal error.
116
+ *
117
+ * Deliberately not a JSON round-trip: `coerceTopLevelDataFromSchema` puts real
118
+ * `Date` instances on the data first, and stringifying would flatten them back
119
+ * to strings the validator has never been given.
120
+ */
121
+ const stripUndefinedValues = (value) => {
122
+ if (Array.isArray(value))
123
+ return value.map(stripUndefinedValues);
124
+ if (value === null || typeof value !== 'object')
125
+ return value;
126
+ // Only plain objects: a Date, Map or class instance is data, not a bag of
127
+ // properties to be rewritten.
128
+ const prototype = Object.getPrototypeOf(value);
129
+ if (prototype !== Object.prototype && prototype !== null)
130
+ return value;
131
+ return Object.fromEntries(Object.entries(value)
132
+ .filter(([, entry]) => entry !== undefined)
133
+ .map(([key, entry]) => [key, stripUndefinedValues(entry)]));
134
+ };
101
135
  export const validateSchema = async (logger, schemaService, schemaName, data, packageName = null) => {
102
136
  if (schemaService) {
103
137
  if (!schemaName) {
@@ -118,6 +152,6 @@ export const validateSchema = async (logger, schemaService, schemaName, data, pa
118
152
  throw new MissingSchemaError(`Schema '${schemaName}' not found. Ensure schema generation has been run.`);
119
153
  }
120
154
  await schemaService.compileSchema(key, schema);
121
- await schemaService.validateSchema(key, data ?? {});
155
+ await schemaService.validateSchema(key, stripUndefinedValues(data ?? {}));
122
156
  }
123
157
  };
@@ -5,7 +5,13 @@ export interface AIRunStateService {
5
5
  updateRun(runId: string, updates: Partial<AgentRunState>): Promise<void>;
6
6
  getRun(runId: string): Promise<AgentRunState | null>;
7
7
  getRunsByThread(threadId: string): Promise<AgentRunState[]>;
8
- resolveApproval(toolCallId: string, status: 'approved' | 'denied'): Promise<void>;
8
+ /**
9
+ * Claims an approval: removes it from the run's pending list and records the
10
+ * decision, as one atomic step. Returns whether THIS caller made the claim —
11
+ * concurrent approvals of the same tool call produce exactly one `true`, and
12
+ * only that caller may run the tool.
13
+ */
14
+ resolveApproval(toolCallId: string, status: 'approved' | 'denied'): Promise<boolean>;
9
15
  findRunByToolCallId(toolCallId: string): Promise<{
10
16
  run: AgentRunState;
11
17
  approval: PendingApproval;
@@ -7,7 +7,7 @@ export declare class InMemoryAIRunStateService implements AIRunStateService {
7
7
  updateRun(runId: string, updates: Partial<AgentRunState>): Promise<void>;
8
8
  getRun(runId: string): Promise<AgentRunState | null>;
9
9
  getRunsByThread(threadId: string): Promise<AgentRunState[]>;
10
- resolveApproval(toolCallId: string, status: 'approved' | 'denied'): Promise<void>;
10
+ resolveApproval(toolCallId: string, status: 'approved' | 'denied'): Promise<boolean>;
11
11
  findRunByToolCallId(toolCallId: string): Promise<{
12
12
  run: AgentRunState;
13
13
  approval: PendingApproval;
@@ -25,10 +25,11 @@ export class InMemoryAIRunStateService {
25
25
  if (approval) {
26
26
  run.pendingApprovals = run.pendingApprovals.filter((a) => a.toolCallId !== toolCallId);
27
27
  run.updatedAt = new Date();
28
- return;
28
+ return true;
29
29
  }
30
30
  }
31
31
  }
32
+ return false;
32
33
  }
33
34
  async findRunByToolCallId(toolCallId) {
34
35
  for (const run of this.runs.values()) {
@@ -29,6 +29,16 @@ export declare class InMemoryWorkflowService extends PikkuWorkflowService implem
29
29
  protected setStepErrorImpl(stepId: string, error: Error): Promise<void>;
30
30
  protected setStepChildRunIdImpl(stepId: string, childRunId: string): Promise<void>;
31
31
  protected createRetryAttemptImpl(failedStepId: string, status: 'pending' | 'running'): Promise<StepState>;
32
+ /**
33
+ * Safe to opt in despite the pass-through `withStepLock`: this service wires
34
+ * no queues and runs every step inline in one process, so the relay's
35
+ * redundant dispatch has no second holder to race with. See
36
+ * knowledge: decisions/internals/the-in-memory-workflow-service-is-inline-only-and-single-process.md
37
+ */
38
+ protected findUndispatchedSteps(before: Date, limit: number): Promise<Array<{
39
+ runId: string;
40
+ stepId: string;
41
+ }>>;
32
42
  protected findStalledRunIds(before: Date, limit: number): Promise<string[]>;
33
43
  listRuns(options?: {
34
44
  workflowName?: string;
@@ -74,6 +74,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
74
74
  const step = {
75
75
  stepId,
76
76
  status: 'pending',
77
+ rpcName,
77
78
  attemptCount: 1,
78
79
  retries: stepOptions?.retries,
79
80
  retryDelay: stepOptions?.retryDelay,
@@ -177,6 +178,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
177
178
  const newStep = {
178
179
  stepId: newStepId,
179
180
  status,
181
+ rpcName: failedStep.rpcName,
180
182
  attemptCount: failedStep.attemptCount + 1,
181
183
  retries: failedStep.retries,
182
184
  retryDelay: failedStep.retryDelay,
@@ -199,6 +201,29 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
199
201
  this.stepHistory.set(runId, history);
200
202
  return newStep;
201
203
  }
204
+ /**
205
+ * Safe to opt in despite the pass-through `withStepLock`: this service wires
206
+ * no queues and runs every step inline in one process, so the relay's
207
+ * redundant dispatch has no second holder to race with. See
208
+ * knowledge: decisions/internals/the-in-memory-workflow-service-is-inline-only-and-single-process.md
209
+ */
210
+ async findUndispatchedSteps(before, limit) {
211
+ const undispatched = [];
212
+ for (const [runId, run] of this.runs) {
213
+ if (run.status !== 'running')
214
+ continue;
215
+ for (const step of this.stepHistory.get(runId) ?? []) {
216
+ if (step.status !== 'pending')
217
+ continue;
218
+ if (step.updatedAt >= before)
219
+ continue;
220
+ undispatched.push({ runId, stepId: step.stepId });
221
+ if (undispatched.length >= limit)
222
+ return undispatched;
223
+ }
224
+ }
225
+ return undispatched;
226
+ }
202
227
  async findStalledRunIds(before, limit) {
203
228
  const stalled = [];
204
229
  for (const [runId, run] of this.runs) {
@@ -26,4 +26,25 @@ export type LocalContentRequestHandlerOptions = {
26
26
  */
27
27
  getJWT: () => JWTService | undefined;
28
28
  };
29
+ export type SignedContentVerification = {
30
+ ok: true;
31
+ } | {
32
+ ok: false;
33
+ status: number;
34
+ body: string;
35
+ };
36
+ /**
37
+ * Verifies a signed content URL — the one function that decides whether a
38
+ * signed read or a signed upload is genuine.
39
+ *
40
+ * Exported and shared so the runtimes that serve content (the core handler here,
41
+ * the node server, the express adapter) verify identically. They used to each
42
+ * carry their own copy, which is how the express upload path ended up with no
43
+ * check at all: one copy can rot or be forgotten in isolation.
44
+ *
45
+ * `onMissingJWT` is called at most where the caller wants to log the
46
+ * misconfiguration once; a signed request with no verifier is treated as
47
+ * invalid, never as trusted.
48
+ */
49
+ export declare const verifySignedContentRequest: (requestUrl: URL, jwt: JWTService | undefined, onMissingJWT?: () => void) => Promise<SignedContentVerification>;
29
50
  export declare const createLocalContentRequestHandler: ({ content, logger, getJWT, }: LocalContentRequestHandlerOptions) => LocalContentRequestHandler;
@@ -36,69 +36,88 @@ const text = (status, body) => new Response(body, {
36
36
  status,
37
37
  headers: { 'content-type': 'text/plain; charset=utf-8' },
38
38
  });
39
+ /**
40
+ * Verifies a signed content URL — the one function that decides whether a
41
+ * signed read or a signed upload is genuine.
42
+ *
43
+ * Exported and shared so the runtimes that serve content (the core handler here,
44
+ * the node server, the express adapter) verify identically. They used to each
45
+ * carry their own copy, which is how the express upload path ended up with no
46
+ * check at all: one copy can rot or be forgotten in isolation.
47
+ *
48
+ * `onMissingJWT` is called at most where the caller wants to log the
49
+ * misconfiguration once; a signed request with no verifier is treated as
50
+ * invalid, never as trusted.
51
+ */
52
+ export const verifySignedContentRequest = async (requestUrl, jwt, onMissingJWT) => {
53
+ const signedAtValue = requestUrl.searchParams.get('signedAt');
54
+ const expiresAtValue = requestUrl.searchParams.get('expiresAt');
55
+ const notBeforeValue = requestUrl.searchParams.get('notBefore');
56
+ const signature = requestUrl.searchParams.get('signature');
57
+ if (!signedAtValue || !expiresAtValue) {
58
+ return { ok: false, status: 403, body: 'Signed URL required' };
59
+ }
60
+ const signedAt = Number(signedAtValue);
61
+ const expiresAt = Number(expiresAtValue);
62
+ const notBefore = notBeforeValue == null ? undefined : Number(notBeforeValue);
63
+ if (!Number.isFinite(signedAt) ||
64
+ !Number.isFinite(expiresAt) ||
65
+ (notBefore != null && !Number.isFinite(notBefore))) {
66
+ return { ok: false, status: 403, body: 'Invalid signed URL' };
67
+ }
68
+ const now = Date.now();
69
+ if (now > expiresAt || (notBefore != null && now < notBefore)) {
70
+ return { ok: false, status: 403, body: 'Signed URL expired' };
71
+ }
72
+ if (!jwt) {
73
+ onMissingJWT?.();
74
+ return { ok: false, status: 403, body: 'Invalid signed URL' };
75
+ }
76
+ if (!signature) {
77
+ return { ok: false, status: 403, body: 'Signed URL signature required' };
78
+ }
79
+ try {
80
+ const payload = await jwt.decode(signature);
81
+ // Every claim is compared, the path included: without it a signature
82
+ // minted for one asset would read any other.
83
+ if (payload.signedAt !== signedAt ||
84
+ payload.expiresAt !== expiresAt ||
85
+ payload.notBefore !== notBefore ||
86
+ payload.path !== signedContentPath(requestUrl.pathname)) {
87
+ return { ok: false, status: 403, body: 'Invalid signed URL' };
88
+ }
89
+ }
90
+ catch {
91
+ return { ok: false, status: 403, body: 'Invalid signed URL' };
92
+ }
93
+ return { ok: true };
94
+ };
39
95
  export const createLocalContentRequestHandler = ({ content, logger, getJWT, }) => {
40
96
  // Logged at most once. An unverifiable request is attacker-triggerable, so
41
97
  // this reports a startup misconfiguration rather than per-request news.
42
98
  let loggedMissingJWT = false;
43
- const validateSignedAssetRequest = async (requestUrl) => {
44
- const signedAtValue = requestUrl.searchParams.get('signedAt');
45
- const expiresAtValue = requestUrl.searchParams.get('expiresAt');
46
- const notBeforeValue = requestUrl.searchParams.get('notBefore');
47
- const signature = requestUrl.searchParams.get('signature');
48
- if (!signedAtValue || !expiresAtValue) {
49
- return { ok: false, status: 403, body: 'Signed URL required' };
50
- }
51
- const signedAt = Number(signedAtValue);
52
- const expiresAt = Number(expiresAtValue);
53
- const notBefore = notBeforeValue == null ? undefined : Number(notBeforeValue);
54
- if (!Number.isFinite(signedAt) ||
55
- !Number.isFinite(expiresAt) ||
56
- (notBefore != null && !Number.isFinite(notBefore))) {
57
- return { ok: false, status: 403, body: 'Invalid signed URL' };
58
- }
59
- const now = Date.now();
60
- if (now > expiresAt || (notBefore != null && now < notBefore)) {
61
- return { ok: false, status: 403, body: 'Signed URL expired' };
62
- }
63
- const jwt = getJWT();
64
- if (!jwt) {
65
- if (!loggedMissingJWT) {
66
- loggedMissingJWT = true;
67
- logger.error('pikku: refusing signed asset reads — no JWTService is available to verify them. Pass `contentSigningJWT` (the same service LocalContent signs with) or expose it as `singletonServices.jwt`.');
68
- }
69
- return { ok: false, status: 403, body: 'Invalid signed URL' };
70
- }
71
- if (!signature) {
72
- return { ok: false, status: 403, body: 'Signed URL signature required' };
73
- }
74
- try {
75
- const payload = await jwt.decode(signature);
76
- // Every claim is compared, the path included: without it a signature
77
- // minted for one asset would read any other.
78
- if (payload.signedAt !== signedAt ||
79
- payload.expiresAt !== expiresAt ||
80
- payload.notBefore !== notBefore ||
81
- payload.path !== signedContentPath(requestUrl.pathname)) {
82
- return { ok: false, status: 403, body: 'Invalid signed URL' };
83
- }
84
- }
85
- catch {
86
- return { ok: false, status: 403, body: 'Invalid signed URL' };
99
+ const validateSignedAssetRequest = (requestUrl) => verifySignedContentRequest(requestUrl, getJWT(), () => {
100
+ if (loggedMissingJWT)
101
+ return;
102
+ loggedMissingJWT = true;
103
+ logger.error('pikku: refusing signed asset reads — no JWTService is available to verify them. Pass `contentSigningJWT` (the same service LocalContent signs with) or expose it as `singletonServices.jwt`.');
104
+ });
105
+ const handleUpload = async (request, requestUrl, pathname) => {
106
+ // Uploads are presigned exactly like reads: getUploadURL mints a signed URL
107
+ // and this verifies it before writing. Without this an unauthenticated PUT
108
+ // could write arbitrary bytes to any key under the content root — the write
109
+ // side had none of the signature checking the read side has always had.
110
+ const signed = await validateSignedAssetRequest(requestUrl);
111
+ if (!signed.ok) {
112
+ return text(signed.status, signed.body);
87
113
  }
88
- return { ok: true };
89
- };
90
- const handleUpload = async (request, pathname) => {
91
114
  const key = contentKey(pathname, content.uploadUrlPrefix);
92
115
  const targetPath = toTargetPath(content.localFileUploadPath, key);
93
116
  if (!targetPath) {
94
117
  return text(400, 'Invalid path');
95
118
  }
96
119
  const maxBytes = parseSizeLimit(content.sizeLimit ?? '1mb');
97
- // Counted as it arrives and abandoned the moment it goes over, so an
98
- // oversized upload costs the limit rather than its own size — `arrayBuffer()`
99
- // would have to hold all of it first, which hands an unauthenticated caller
100
- // a way to spend the server's memory. Mirrors node-http-server's
101
- // `readRequestBody`, which aborts the same way.
120
+ // knowledge: decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md
102
121
  const chunks = [];
103
122
  let bytesRead = 0;
104
123
  const reader = request.body?.getReader();
@@ -165,7 +184,7 @@ export const createLocalContentRequestHandler = ({ content, logger, getJWT, }) =
165
184
  const pathname = decodeURIComponent(requestUrl.pathname);
166
185
  if (request.method === 'PUT' &&
167
186
  matchesPrefix(pathname, content.uploadUrlPrefix)) {
168
- return handleUpload(request, pathname);
187
+ return handleUpload(request, requestUrl, pathname);
169
188
  }
170
189
  if ((request.method === 'GET' || request.method === 'HEAD') &&
171
190
  matchesPrefix(pathname, content.assetUrlPrefix)) {
@@ -28,6 +28,12 @@ export declare class LocalContent implements ContentService {
28
28
  private signParams;
29
29
  signURL(args: SignURLArgs): Promise<string>;
30
30
  signContentKey(args: SignContentKeyArgs): Promise<string>;
31
+ /**
32
+ * Presigned upload, mirroring the read side: the URL carries a short-lived,
33
+ * path-bound signature that {@link createLocalContentRequestHandler} verifies
34
+ * before writing. An unsigned upload URL cannot be verified, and the handler
35
+ * now refuses one — so this must sign, or every upload 403s.
36
+ */
31
37
  getUploadURL(args: GetUploadURLArgs): Promise<UploadURLResult>;
32
38
  writeFile(args: WriteFileArgs): Promise<boolean>;
33
39
  copyFile(args: CopyFileArgs): Promise<boolean>;
@@ -27,6 +27,8 @@ export const signedContentPath = (urlOrPath) => {
27
27
  return pathname;
28
28
  }
29
29
  };
30
+ /** How long a presigned upload URL stays valid. Short, like an S3 PUT URL. */
31
+ const UPLOAD_URL_TTL_MS = 15 * 60 * 1000;
30
32
  export class LocalContent {
31
33
  config;
32
34
  logger;
@@ -90,13 +92,24 @@ export class LocalContent {
90
92
  dateGreaterThan: args.dateGreaterThan,
91
93
  });
92
94
  }
95
+ /**
96
+ * Presigned upload, mirroring the read side: the URL carries a short-lived,
97
+ * path-bound signature that {@link createLocalContentRequestHandler} verifies
98
+ * before writing. An unsigned upload URL cannot be verified, and the handler
99
+ * now refuses one — so this must sign, or every upload 403s.
100
+ */
93
101
  async getUploadURL(args) {
94
102
  void args.visibility;
95
103
  const fullKey = this.joinKey(args.bucket, args.fileKey);
96
104
  this.logger.debug(`Going to upload with key: ${fullKey}`);
105
+ const uploadUrl = await this.signURL({
106
+ url: `${this.config.uploadUrlPrefix}/${fullKey}`,
107
+ dateLessThan: new Date(Date.now() + UPLOAD_URL_TTL_MS),
108
+ });
97
109
  return {
98
- uploadUrl: `${this.config.uploadUrlPrefix}/${fullKey}`,
110
+ uploadUrl,
99
111
  assetKey: fullKey,
112
+ uploadMethod: 'PUT',
100
113
  };
101
114
  }
102
115
  async writeFile(args) {
@@ -1,4 +1,4 @@
1
- import type { SerializedError } from '../types/core.types.js';
1
+ import type { CoreUserSession, SerializedError } from '../types/core.types.js';
2
2
  import type { WorkflowRun, WorkflowPlannedStep, WorkflowRunWire, WorkflowRunStatus, StepState, WorkflowStatus, WorkflowVersionStatus } from '../wirings/workflow/workflow.types.js';
3
3
  import type { RunTimeline, ReconstructedRunState } from '../wirings/workflow/run-timeline.js';
4
4
  export interface WorkflowService {
@@ -21,8 +21,10 @@ export interface WorkflowService {
21
21
  /**
22
22
  * The payload is stored as given and validated on replay inside the workflow
23
23
  * body, so an invalid one re-closes the gate rather than failing the run.
24
+ *
25
+ * The session must belong to the run's owner — see `assertWorkflowRunOwner`.
24
26
  */
25
- approveStep(runId: string, reason: string, decision: unknown): Promise<void>;
27
+ approveStep(runId: string, reason: string, decision: unknown, session?: CoreUserSession): Promise<void>;
26
28
  startWorkflow<I>(name: string, input: I, wire: WorkflowRunWire, rpcService: any, options?: {
27
29
  inline?: boolean;
28
30
  startNode?: string;
@@ -0,0 +1,10 @@
1
+ import type { ServiceTestConfig } from '../service-tests.js';
2
+ /** Conformance suite for `agentRunService`. Runs only when a backend supplies one. */
3
+ export declare const defineAgentRunServiceTests: (name: string, agentRunService: NonNullable<ServiceTestConfig["services"]["agentRunService"]>,
4
+ /**
5
+ * Thread-ownership is a property of the two services together — the run
6
+ * service lists what the storage service owns — so that half of the suite
7
+ * only runs when a backend supplies both. Passed explicitly rather than
8
+ * closed over, so the dependency is visible in the signature.
9
+ */
10
+ aiStorageService?: ServiceTestConfig["services"]["aiStorageService"]) => void;
@@ -0,0 +1,72 @@
1
+ import { describe, test, before } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ /** Conformance suite for `agentRunService`. Runs only when a backend supplies one. */
4
+ export const defineAgentRunServiceTests = (name, agentRunService,
5
+ /**
6
+ * Thread-ownership is a property of the two services together — the run
7
+ * service lists what the storage service owns — so that half of the suite
8
+ * only runs when a backend supplies both. Passed explicitly rather than
9
+ * closed over, so the dependency is visible in the signature.
10
+ */
11
+ aiStorageService) => {
12
+ const factory = agentRunService;
13
+ describe(`AgentRunService [${name}]`, () => {
14
+ let agentService;
15
+ before(async () => {
16
+ agentService = await factory();
17
+ });
18
+ test('listThreads', async () => {
19
+ const threads = await agentService.listThreads();
20
+ assert.ok(Array.isArray(threads));
21
+ });
22
+ if (aiStorageService) {
23
+ const storageFactory = aiStorageService;
24
+ // The `owners` constraint is what keeps the generated thread-management
25
+ // functions from leaking across tenants: a caller may only list threads
26
+ // owned by one of their session principals, matching the
27
+ // `principal:sub-partition` composition resolveOwnerResourceId writes.
28
+ test('listThreads scopes to the given owners, including sub-partitions', async () => {
29
+ const storage = await storageFactory();
30
+ await storage.createThread('owner-alice');
31
+ await storage.createThread('owner-alice:project-1');
32
+ await storage.createThread('owner-bob:secret');
33
+ const threads = await agentService.listThreads({
34
+ owners: ['owner-alice'],
35
+ });
36
+ const ids = threads.map((t) => t.resourceId);
37
+ assert.ok(ids.includes('owner-alice'));
38
+ assert.ok(ids.includes('owner-alice:project-1'));
39
+ assert.ok(!ids.some((id) => id.startsWith('owner-bob')), "another owner's threads must not be listed");
40
+ });
41
+ test('listThreads with an owner does not match a lookalike prefix', async () => {
42
+ const storage = await storageFactory();
43
+ await storage.createThread('owner-al');
44
+ await storage.createThread('owner-alice-evil:p');
45
+ const threads = await agentService.listThreads({
46
+ owners: ['owner-al'],
47
+ });
48
+ const ids = threads.map((t) => t.resourceId);
49
+ assert.ok(ids.includes('owner-al'));
50
+ assert.ok(!ids.includes('owner-alice-evil:p'));
51
+ });
52
+ test('listThreads with an empty owners list returns nothing', async () => {
53
+ const storage = await storageFactory();
54
+ await storage.createThread('owner-empty-check');
55
+ const threads = await agentService.listThreads({ owners: [] });
56
+ assert.deepEqual(threads, []);
57
+ });
58
+ }
59
+ test('getThread returns null for missing', async () => {
60
+ const thread = await agentService.getThread('missing-thread');
61
+ assert.equal(thread, null);
62
+ });
63
+ test('getDistinctAgentNames', async () => {
64
+ const names = await agentService.getDistinctAgentNames();
65
+ assert.ok(Array.isArray(names));
66
+ });
67
+ test('deleteThread returns false for missing', async () => {
68
+ const result = await agentService.deleteThread('missing-thread');
69
+ assert.equal(result, false);
70
+ });
71
+ });
72
+ };
@@ -0,0 +1,3 @@
1
+ import type { ServiceTestConfig } from '../service-tests.js';
2
+ /** Conformance suite for `aiStorageService`. Runs only when a backend supplies one. */
3
+ export declare const defineAiStorageServiceTests: (name: string, aiStorageService: NonNullable<ServiceTestConfig["services"]["aiStorageService"]>) => void;