@pikku/core 0.12.80 → 0.12.83

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 (237) hide show
  1. package/CHANGELOG.md +345 -0
  2. package/dist/errors/index.d.ts +1 -1
  3. package/dist/errors/index.js +1 -1
  4. package/dist/function/function-runner.js +2 -5
  5. package/dist/function/index.d.ts +1 -1
  6. package/dist/index.d.ts +11 -11
  7. package/dist/index.js +3 -3
  8. package/dist/pikku-state.js +4 -0
  9. package/dist/services/ai-agent-runner-service.d.ts +7 -0
  10. package/dist/services/ai-run-state-service.d.ts +10 -0
  11. package/dist/services/in-memory-ai-run-state-service.d.ts +5 -1
  12. package/dist/services/in-memory-ai-run-state-service.js +9 -0
  13. package/dist/services/index.d.ts +15 -16
  14. package/dist/services/index.js +5 -5
  15. package/dist/services/meta-service.d.ts +2 -1
  16. package/dist/services/scoped-credential-service.d.ts +21 -0
  17. package/dist/services/scoped-credential-service.js +53 -0
  18. package/dist/testing/service-tests/ai-storage-service-tests.js +76 -0
  19. package/dist/types/core.types.d.ts +2 -3
  20. package/dist/types/state.types.d.ts +19 -1
  21. package/dist/wirings/actor-flow/index.d.ts +1 -1
  22. package/dist/wirings/ai-agent/ai-agent-finalize.d.ts +58 -0
  23. package/dist/wirings/ai-agent/ai-agent-finalize.js +138 -0
  24. package/dist/wirings/ai-agent/ai-agent-interrupt.js +1 -0
  25. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +2 -8
  26. package/dist/wirings/ai-agent/ai-agent-memory.js +34 -17
  27. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +7 -0
  28. package/dist/wirings/ai-agent/ai-agent-model-config.js +44 -1
  29. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -0
  30. package/dist/wirings/ai-agent/ai-agent-runner.js +61 -40
  31. package/dist/wirings/ai-agent/ai-agent-stream.js +89 -36
  32. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +1 -0
  33. package/dist/wirings/ai-agent/ai-agent-turn.js +1 -0
  34. package/dist/wirings/ai-agent/ai-agent.types.d.ts +46 -1
  35. package/dist/wirings/ai-agent/index.d.ts +8 -7
  36. package/dist/wirings/ai-agent/index.js +5 -4
  37. package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +26 -0
  38. package/dist/wirings/ai-scorer/ai-scorer-grade.js +33 -0
  39. package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +17 -0
  40. package/dist/wirings/ai-scorer/ai-scorer-judge.js +92 -0
  41. package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +15 -0
  42. package/dist/wirings/ai-scorer/ai-scorer-live.js +38 -0
  43. package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +18 -0
  44. package/dist/wirings/ai-scorer/ai-scorer-registry.js +46 -0
  45. package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +8 -0
  46. package/dist/wirings/ai-scorer/ai-scorer-sampling.js +31 -0
  47. package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +10 -0
  48. package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +40 -0
  49. package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +15 -0
  50. package/dist/wirings/ai-scorer/ai-scorer-worker.js +58 -0
  51. package/dist/wirings/ai-scorer/ai-scorer.d.ts +39 -0
  52. package/dist/wirings/ai-scorer/ai-scorer.js +40 -0
  53. package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +90 -0
  54. package/dist/wirings/ai-scorer/ai-scorer.types.js +4 -0
  55. package/dist/wirings/ai-scorer/index.d.ts +6 -0
  56. package/dist/wirings/ai-scorer/index.js +5 -0
  57. package/dist/wirings/channel/index.d.ts +5 -6
  58. package/dist/wirings/channel/index.js +3 -4
  59. package/dist/wirings/channel/local/local-channel-runner.js +8 -1
  60. package/dist/wirings/cli/channel/cli-raw-channel-runner.js +9 -1
  61. package/dist/wirings/cli/channel/index.d.ts +1 -2
  62. package/dist/wirings/cli/channel/index.js +0 -1
  63. package/dist/wirings/cli/cli-runner.js +13 -1
  64. package/dist/wirings/credential/index.d.ts +1 -1
  65. package/dist/wirings/gateway/index.d.ts +1 -1
  66. package/dist/wirings/http/http-runner.js +8 -2
  67. package/dist/wirings/http/index.d.ts +1 -2
  68. package/dist/wirings/mcp/index.d.ts +1 -1
  69. package/dist/wirings/mcp/mcp-runner.d.ts +15 -0
  70. package/dist/wirings/mcp/mcp-runner.js +18 -5
  71. package/dist/wirings/persona/index.d.ts +3 -4
  72. package/dist/wirings/persona/index.js +2 -3
  73. package/dist/wirings/queue/index.d.ts +1 -3
  74. package/dist/wirings/queue/index.js +1 -3
  75. package/dist/wirings/rpc/addon-runner.d.ts +8 -0
  76. package/dist/wirings/rpc/addon-runner.js +31 -3
  77. package/dist/wirings/rpc/rpc-runner.js +4 -0
  78. package/dist/wirings/rpc/rpc-types.d.ts +8 -0
  79. package/dist/wirings/rpc/wire-addon.d.ts +25 -0
  80. package/dist/wirings/rpc/wire-addon.js +8 -0
  81. package/dist/wirings/scheduler/index.d.ts +1 -1
  82. package/dist/wirings/trigger/index.d.ts +1 -1
  83. package/dist/wirings/virtual-user/index.d.ts +5 -6
  84. package/dist/wirings/virtual-user/index.js +2 -4
  85. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +85 -15
  86. package/dist/wirings/workflow/feature.d.ts +2 -1
  87. package/dist/wirings/workflow/index.d.ts +5 -16
  88. package/dist/wirings/workflow/index.js +1 -9
  89. package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -7
  90. package/dist/wirings/workflow/pikku-scenario-service.js +48 -13
  91. package/dist/wirings/workflow/pikku-workflow-service.js +17 -3
  92. package/dist/wirings/workflow/scenario-step.types.d.ts +8 -0
  93. package/dist/wirings/workflow/scenario.types.d.ts +37 -0
  94. package/dist/wirings/workflow/workflow-approval-audit.d.ts +16 -0
  95. package/dist/wirings/workflow/workflow-approval-audit.js +40 -0
  96. package/dist/wirings/workflow/workflow-approval-policy.d.ts +20 -0
  97. package/dist/wirings/workflow/workflow-approval-policy.js +48 -0
  98. package/dist/wirings/workflow/workflow-approval.d.ts +29 -1
  99. package/dist/wirings/workflow/workflow-approval.js +65 -2
  100. package/dist/wirings/workflow/workflow-run-ownership.d.ts +2 -1
  101. package/dist/wirings/workflow/workflow-run-ownership.js +2 -1
  102. package/dist/wirings/workflow/workflow.types.d.ts +2 -37
  103. package/knowledge/decisions/internals/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md +32 -0
  104. package/knowledge/decisions/internals/an-addon-scope-root-loses-to-a-root-the-host-already-declares.md +39 -0
  105. package/knowledge/decisions/internals/index.md +30 -3
  106. package/knowledge/decisions/internals/validate-runs-checks-by-precondition.md +115 -0
  107. package/knowledge/decisions/security/a-function-never-receives-the-secret-service.md +37 -0
  108. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +30 -14
  109. package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +59 -0
  110. package/knowledge/decisions/security/index.md +3 -1
  111. package/knowledge/questions/index.md +1 -1
  112. package/package.json +3 -2
  113. package/scripts/generate-api-report.mts +143 -18
  114. package/src/api-report.test.ts +2 -2
  115. package/src/errors/index.ts +1 -1
  116. package/src/function/function-runner.test.ts +52 -0
  117. package/src/function/function-runner.ts +5 -9
  118. package/src/function/index.ts +0 -2
  119. package/src/index.ts +0 -35
  120. package/src/pikku-state.ts +5 -0
  121. package/src/public-surface.json +81 -118
  122. package/src/services/ai-agent-runner-service.ts +12 -1
  123. package/src/services/ai-run-state-service.ts +11 -0
  124. package/src/services/in-memory-ai-run-state-service.ts +13 -0
  125. package/src/services/index.ts +7 -58
  126. package/src/services/meta-service.ts +2 -4
  127. package/src/services/scoped-credential-service.test.ts +86 -0
  128. package/src/services/scoped-credential-service.ts +63 -0
  129. package/src/testing/service-tests/ai-storage-service-tests.ts +93 -0
  130. package/src/types/core.types.ts +4 -7
  131. package/src/types/state.types.ts +21 -1
  132. package/src/wirings/actor-flow/index.ts +0 -3
  133. package/src/wirings/ai-agent/ai-agent-finalize.test.ts +186 -0
  134. package/src/wirings/ai-agent/ai-agent-finalize.ts +197 -0
  135. package/src/wirings/ai-agent/ai-agent-interrupt.ts +1 -0
  136. package/src/wirings/ai-agent/ai-agent-memory.ts +54 -38
  137. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +72 -3
  138. package/src/wirings/ai-agent/ai-agent-model-config.ts +49 -1
  139. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -0
  140. package/src/wirings/ai-agent/ai-agent-runner.ts +71 -40
  141. package/src/wirings/ai-agent/ai-agent-stream-output-hooks.test.ts +353 -0
  142. package/src/wirings/ai-agent/ai-agent-stream.ts +116 -54
  143. package/src/wirings/ai-agent/ai-agent-turn.test.ts +67 -0
  144. package/src/wirings/ai-agent/ai-agent-turn.ts +1 -0
  145. package/src/wirings/ai-agent/ai-agent.types.ts +64 -4
  146. package/src/wirings/ai-agent/index.ts +2 -16
  147. package/src/wirings/ai-scorer/ai-scorer-grade.test.ts +106 -0
  148. package/src/wirings/ai-scorer/ai-scorer-grade.ts +55 -0
  149. package/src/wirings/ai-scorer/ai-scorer-judge.test.ts +143 -0
  150. package/src/wirings/ai-scorer/ai-scorer-judge.ts +120 -0
  151. package/src/wirings/ai-scorer/ai-scorer-live.test.ts +174 -0
  152. package/src/wirings/ai-scorer/ai-scorer-live.ts +56 -0
  153. package/src/wirings/ai-scorer/ai-scorer-registry.ts +63 -0
  154. package/src/wirings/ai-scorer/ai-scorer-sampling.test.ts +34 -0
  155. package/src/wirings/ai-scorer/ai-scorer-sampling.ts +36 -0
  156. package/src/wirings/ai-scorer/ai-scorer-snapshots.test.ts +49 -0
  157. package/src/wirings/ai-scorer/ai-scorer-snapshots.ts +46 -0
  158. package/src/wirings/ai-scorer/ai-scorer-worker.test.ts +122 -0
  159. package/src/wirings/ai-scorer/ai-scorer-worker.ts +69 -0
  160. package/src/wirings/ai-scorer/ai-scorer.ts +76 -0
  161. package/src/wirings/ai-scorer/ai-scorer.types.ts +107 -0
  162. package/src/wirings/ai-scorer/index.ts +24 -0
  163. package/src/wirings/channel/index.ts +1 -20
  164. package/src/wirings/channel/local/local-channel-runner.test.ts +68 -0
  165. package/src/wirings/channel/local/local-channel-runner.ts +8 -1
  166. package/src/wirings/cli/channel/cli-raw-channel-runner.test.ts +23 -0
  167. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +12 -1
  168. package/src/wirings/cli/channel/index.ts +0 -7
  169. package/src/wirings/cli/cli-runner.test.ts +68 -0
  170. package/src/wirings/cli/cli-runner.ts +18 -1
  171. package/src/wirings/credential/index.ts +0 -1
  172. package/src/wirings/gateway/index.ts +0 -3
  173. package/src/wirings/http/http-runner.test.ts +66 -0
  174. package/src/wirings/http/http-runner.ts +10 -2
  175. package/src/wirings/http/index.ts +1 -1
  176. package/src/wirings/mcp/index.ts +0 -1
  177. package/src/wirings/mcp/mcp-runner.test.ts +181 -0
  178. package/src/wirings/mcp/mcp-runner.ts +35 -5
  179. package/src/wirings/persona/index.ts +0 -8
  180. package/src/wirings/queue/index.ts +0 -14
  181. package/src/wirings/rpc/addon-runner.ts +62 -3
  182. package/src/wirings/rpc/addon-secrets.test.ts +391 -0
  183. package/src/wirings/rpc/rpc-runner.test.ts +2 -0
  184. package/src/wirings/rpc/rpc-runner.ts +4 -0
  185. package/src/wirings/rpc/rpc-types.ts +8 -0
  186. package/src/wirings/rpc/wire-addon.ts +33 -0
  187. package/src/wirings/scheduler/index.ts +0 -1
  188. package/src/wirings/trigger/index.ts +0 -1
  189. package/src/wirings/virtual-user/index.ts +0 -16
  190. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +96 -16
  191. package/src/wirings/workflow/feature.ts +2 -5
  192. package/src/wirings/workflow/graph/graph-runner.test.ts +72 -0
  193. package/src/wirings/workflow/index.ts +2 -68
  194. package/src/wirings/workflow/pikku-scenario-service.ts +81 -16
  195. package/src/wirings/workflow/pikku-workflow-service.test.ts +13 -12
  196. package/src/wirings/workflow/pikku-workflow-service.ts +28 -4
  197. package/src/wirings/workflow/scenario-expectations.test.ts +75 -0
  198. package/src/wirings/workflow/scenario-hooks.test.ts +3 -2
  199. package/src/wirings/workflow/scenario-step.types.ts +8 -0
  200. package/src/wirings/workflow/scenario.types.ts +63 -0
  201. package/src/wirings/workflow/workflow-approval-audit.ts +47 -0
  202. package/src/wirings/workflow/workflow-approval-policy.test.ts +524 -0
  203. package/src/wirings/workflow/workflow-approval-policy.ts +68 -0
  204. package/src/wirings/workflow/workflow-approval.ts +113 -9
  205. package/src/wirings/workflow/workflow-run-authority.test.ts +12 -15
  206. package/src/wirings/workflow/workflow-run-ownership.ts +2 -1
  207. package/src/wirings/workflow/workflow.types.ts +1 -63
  208. package/src/wirings-stay-decoupled.test.ts +6 -2
  209. package/tsconfig.tsbuildinfo +1 -1
  210. package/dist/internal.d.ts +0 -3
  211. package/dist/internal.js +0 -2
  212. package/dist/middleware/timeout.d.ts +0 -9
  213. package/dist/middleware/timeout.js +0 -15
  214. package/dist/pikku-response.d.ts +0 -6
  215. package/dist/pikku-response.js +0 -6
  216. package/dist/services/gopass-secrets.d.ts +0 -15
  217. package/dist/services/gopass-secrets.js +0 -76
  218. package/dist/services/http-scenario-actors.d.ts +0 -75
  219. package/dist/services/http-scenario-actors.js +0 -195
  220. package/dist/services/http-user-flow-actors.d.ts +0 -67
  221. package/dist/services/http-user-flow-actors.js +0 -193
  222. package/dist/services/scenario-actors-service.d.ts +0 -127
  223. package/dist/services/scenario-actors-service.js +0 -40
  224. package/dist/services/user-flow-actors-service.d.ts +0 -39
  225. package/dist/wirings/credential/wire-credential.d.ts +0 -48
  226. package/dist/wirings/credential/wire-credential.js +0 -47
  227. package/dist/wirings/oauth2/oauth2-client.d.ts +0 -47
  228. package/dist/wirings/oauth2/oauth2-client.js +0 -263
  229. package/dist/wirings/oauth2/oauth2-routes.d.ts +0 -35
  230. package/dist/wirings/oauth2/oauth2-routes.js +0 -146
  231. package/dist/wirings/scope/wire-scope.d.ts +0 -33
  232. package/dist/wirings/scope/wire-scope.js +0 -32
  233. package/dist/wirings/workflow/dsl/index.d.ts +0 -5
  234. package/dist/wirings/workflow/dsl/index.js +0 -4
  235. package/dist/wirings/workflow/graph/index.d.ts +0 -5
  236. package/dist/wirings/workflow/graph/index.js +0 -4
  237. /package/dist/{services/user-flow-actors-service.js → wirings/workflow/scenario.types.js} +0 -0
@@ -396,6 +396,74 @@ describe('CLI Runner', () => {
396
396
  const programs = pikkuState(null, 'cli', 'programs')
397
397
  assert.strictEqual(programs['nonexistent'], undefined)
398
398
  })
399
+
400
+ test('enforces command-level permissions declared on a CLI command', async () => {
401
+ let functionRan = false
402
+
403
+ pikkuState(null, 'cli', 'meta', {
404
+ programs: {
405
+ 'sec-cli': {
406
+ program: 'sec-cli',
407
+ commands: {
408
+ secret: {
409
+ command: 'secret',
410
+ pikkuFuncId: 'secretFunc',
411
+ positionals: [],
412
+ options: {},
413
+ },
414
+ },
415
+ options: {},
416
+ },
417
+ },
418
+ renderers: {},
419
+ })
420
+ pikkuState(null, 'function', 'meta', {
421
+ secretFunc: {
422
+ pikkuFuncId: 'secretFunc',
423
+ inputSchemaName: null,
424
+ outputSchemaName: null,
425
+ sessionless: true,
426
+ },
427
+ })
428
+
429
+ wireCLI({
430
+ program: 'sec-cli',
431
+ commands: {
432
+ // command.func is a function-config object, so unwrapFunc reads the
433
+ // inner func's auth/permissions and previously dropped the
434
+ // command-level ones — the branch this fix restores.
435
+ secret: {
436
+ func: {
437
+ func: async () => {
438
+ functionRan = true
439
+ return {}
440
+ },
441
+ },
442
+ // A permission that always denies.
443
+ permissions: { deny: async () => false },
444
+ } as any,
445
+ },
446
+ })
447
+
448
+ const result = await runCLICommand({
449
+ program: 'sec-cli',
450
+ commandPath: ['secret'],
451
+ data: {},
452
+ singletonServices,
453
+ createWireServices,
454
+ }).catch((error) => ({ success: false, error }))
455
+
456
+ assert.equal(
457
+ functionRan,
458
+ false,
459
+ 'the function must not run when a declared permission denies'
460
+ )
461
+ assert.notEqual(
462
+ (result as any).success,
463
+ true,
464
+ 'the command must not report success when permission is denied'
465
+ )
466
+ })
399
467
  })
400
468
 
401
469
  describe('pikkuCLIRender', () => {
@@ -169,7 +169,24 @@ function registerCLICommands(
169
169
  }
170
170
  }
171
171
 
172
- addFunction(funcName, unwrapFunc(command), currentMeta?.packageName)
172
+ // Merge the command-level auth/permissions into the registered config so
173
+ // they are actually enforced by the function runner. They were accepted by
174
+ // the types but dropped here, making a command's declared access control a
175
+ // silent no-op. Command-level wins over the handler's own, then falls back
176
+ // to it.
177
+ const unwrapped = unwrapFunc(command)
178
+ const commandAuth = typeof command === 'object' ? command.auth : undefined
179
+ const commandPermissions =
180
+ typeof command === 'object' ? command.permissions : undefined
181
+ addFunction(
182
+ funcName,
183
+ {
184
+ ...unwrapped,
185
+ auth: commandAuth ?? unwrapped.auth,
186
+ permissions: commandPermissions ?? unwrapped.permissions,
187
+ },
188
+ currentMeta?.packageName
189
+ )
173
190
 
174
191
  if (typeof command === 'object' && command.render) {
175
192
  if (programs[program]) {
@@ -2,7 +2,6 @@ export { defineCredential } from './define-credential.js'
2
2
  export { validateAndBuildCredentialDefinitionsMeta } from './validate-credential-definitions.js'
3
3
  export type {
4
4
  CoreCredential,
5
- CredentialDefinitionMeta,
6
5
  CredentialDefinitionsMeta,
7
6
  CredentialDefinitions,
8
7
  } from './credential.types.js'
@@ -6,13 +6,10 @@ export {
6
6
  export type {
7
7
  GatewayAdapter,
8
8
  GatewayAdapterFactory,
9
- GatewayAttachment,
10
9
  GatewayInboundMessage,
11
10
  GatewayOutboundMessage,
12
- GatewayMeta,
13
11
  GatewaysMeta,
14
12
  GatewayTransportType,
15
13
  CoreGateway,
16
- PikkuGateway,
17
14
  WebhookVerificationResult,
18
15
  } from './gateway.types.js'
@@ -70,6 +70,11 @@ class TestResponse extends PikkuMockResponse {
70
70
  return this
71
71
  }
72
72
 
73
+ redirect(location: string, status: number = 302): this {
74
+ this.status(status)
75
+ return this.header('Location', location)
76
+ }
77
+
73
78
  setMode(mode: 'stream') {
74
79
  this.mode = mode
75
80
  }
@@ -365,6 +370,67 @@ describe('http-runner helpers', () => {
365
370
  assert.equal(response.statusCode, 204)
366
371
  })
367
372
 
373
+ test('fetchData keeps a redirect status when the route returns undefined', async () => {
374
+ setRouteMeta('/redirect')
375
+ addFunction('pikku_func_name', {
376
+ func: async (_services: any, _data: any, { http }: any) => {
377
+ http?.response?.redirect('https://example.com/done', 302)
378
+ },
379
+ })
380
+ wireHTTP({
381
+ route: '/redirect',
382
+ method: 'get',
383
+ auth: false,
384
+ func: {
385
+ func: async (_services: any, _data: any, { http }: any) => {
386
+ http?.response?.redirect('https://example.com/done', 302)
387
+ },
388
+ },
389
+ })
390
+ httpRouter.initialize()
391
+
392
+ const request = new TestRequest('/redirect', 'get')
393
+ const response = new TestResponse()
394
+
395
+ await fetchData(request, response)
396
+
397
+ assert.equal(response.statusCode, 302)
398
+ assert.equal(
399
+ response.headersMap.get('Location'),
400
+ 'https://example.com/done'
401
+ )
402
+ })
403
+
404
+ test('fetchData keeps a status the route set alongside a body', async () => {
405
+ setRouteMeta('/created')
406
+ addFunction('pikku_func_name', {
407
+ func: async (_services: any, _data: any, { http }: any) => {
408
+ http?.response?.status(201)
409
+ return { id: 'thing-1' }
410
+ },
411
+ })
412
+ wireHTTP({
413
+ route: '/created',
414
+ method: 'get',
415
+ auth: false,
416
+ func: {
417
+ func: async (_services: any, _data: any, { http }: any) => {
418
+ http?.response?.status(201)
419
+ return { id: 'thing-1' }
420
+ },
421
+ },
422
+ })
423
+ httpRouter.initialize()
424
+
425
+ const request = new TestRequest('/created', 'get')
426
+ const response = new TestResponse()
427
+
428
+ await fetchData(request, response)
429
+
430
+ assert.equal(response.statusCode, 201)
431
+ assert.deepEqual(response.jsonBody, { id: 'thing-1' })
432
+ })
433
+
368
434
  test('fetchData writes binary responses when returnsJSON is false', async () => {
369
435
  setRouteMeta('/binary')
370
436
  wireHTTP({
@@ -283,6 +283,8 @@ const executeRoute = async (
283
283
  hasSessionChanged: () => userSession.sessionChanged,
284
284
  }
285
285
 
286
+ const statusBeforeRoute = http?.response?.statusCode
287
+
286
288
  try {
287
289
  result = await runPikkuFunc(
288
290
  'http',
@@ -323,14 +325,20 @@ const executeRoute = async (
323
325
  if (matchedRoute.route.sse) {
324
326
  http?.response?.flushHeaders?.()
325
327
  } else {
328
+ const statusSetByRoute = http?.response?.statusCode !== statusBeforeRoute
329
+
326
330
  if (result instanceof Response) {
327
331
  await applyWebResponse(http!.response!, result)
328
332
  } else if (result === undefined || result === null) {
329
- http?.response?.status(204)
333
+ if (!statusSetByRoute) {
334
+ http?.response?.status(204)
335
+ }
330
336
  } else if (route.returnsJSON === false) {
331
337
  http?.response?.arrayBuffer(result)
332
338
  } else {
333
- http?.response?.status(200)
339
+ if (!statusSetByRoute) {
340
+ http?.response?.status(200)
341
+ }
334
342
  http?.response?.json(result)
335
343
  }
336
344
  }
@@ -2,7 +2,6 @@ export {
2
2
  PikkuFetchHTTPRequest,
3
3
  DEFAULT_MAX_BODY_SIZE,
4
4
  } from './pikku-fetch-http-request.js'
5
- export type { PikkuFetchHTTPRequestOptions } from './pikku-fetch-http-request.js'
6
5
  export { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js'
7
6
  export { logRoutes } from './log-http-routes.js'
8
7
 
@@ -19,6 +18,7 @@ export type {
19
18
  HTTPRouteContract,
20
19
  HTTPRouteMap,
21
20
  HTTPWiringsMeta,
21
+ PikkuHTTP,
22
22
  PikkuHTTPRequest,
23
23
  PikkuHTTPResponse,
24
24
  PikkuQuery,
@@ -16,7 +16,6 @@ export type {
16
16
  AssertMCPResourceURIParams,
17
17
  CoreMCPPrompt,
18
18
  CoreMCPResource,
19
- CoreMCPTool,
20
19
  MCPPromptResponse,
21
20
  MCPResourceMeta,
22
21
  MCPResourceResponse,
@@ -9,8 +9,11 @@ import {
9
9
  wireMCPPrompt,
10
10
  wireMCPResource,
11
11
  } from './mcp-runner.js'
12
+ import { addGlobalMiddleware } from '../../middleware-runner.js'
12
13
  import { addFunction } from '../../function/function-runner.js'
13
14
  import { pikkuState, resetPikkuState } from '../../pikku-state.js'
15
+ import { pikkuMiddleware } from '../../types/core.types.js'
16
+ import { addTagMiddleware } from '../../middleware-runner.js'
14
17
 
15
18
  const logger = {
16
19
  debug: () => {},
@@ -166,6 +169,49 @@ describe('runMCPResource', () => {
166
169
  },
167
170
  })
168
171
  })
172
+
173
+ test('runs a templated resource middleware when invoked by concrete uri', async () => {
174
+ let middlewareRan = false
175
+ // A tag-derived middleware group (this is where a tag-based auth gate lands).
176
+ addTagMiddleware('secure', [
177
+ pikkuMiddleware(async (_services, _wire, next) => {
178
+ middlewareRan = true
179
+ await next()
180
+ }),
181
+ ])
182
+ pikkuState(null, 'mcp', 'resourcesMeta')['resource://secure/{id}'] = {
183
+ uri: 'resource://secure/{id}',
184
+ title: 'Secure',
185
+ description: 'Secure',
186
+ pikkuFuncId: 'secureResourceFunc',
187
+ inputSchema: null,
188
+ outputSchema: null,
189
+ // The resource's merged middleware metadata. It must resolve for a concrete
190
+ // request against the template, or the resource is reachable with its gate
191
+ // skipped.
192
+ middleware: [{ type: 'tag', tag: 'secure' }],
193
+ } as never
194
+ registerFunction('secureResourceFunc', async () => ({ ok: true }))
195
+
196
+ wireMCPResource({
197
+ uri: 'resource://secure/{id}',
198
+ title: 'Secure',
199
+ description: 'Secure',
200
+ func: { func: async () => ({ ok: true }) } as never,
201
+ })
202
+
203
+ await runMCPResource(
204
+ { jsonrpc: '2.0', id: 'req-2', params: {} },
205
+ { mcp: mcpWire as never },
206
+ 'resource://secure/99'
207
+ )
208
+
209
+ assert.equal(
210
+ middlewareRan,
211
+ true,
212
+ 'the templated resource middleware must run for a concrete uri'
213
+ )
214
+ })
169
215
  })
170
216
 
171
217
  describe('runMCPTool', () => {
@@ -371,6 +417,141 @@ describe('runMCPTool', () => {
371
417
  })
372
418
  })
373
419
 
420
+ describe("MCP carries the caller's HTTP request", () => {
421
+ // Without this, every auth middleware bails on its first line — each one
422
+ // opens with `if (!http?.request) return`, so an MCP call arrived at the
423
+ // function with no session, and a tool fronting a session-requiring function
424
+ // answered 'Authentication required' to every caller. Nothing else was
425
+ // missing: global middleware already runs for MCP, and the runner already
426
+ // builds a session service. Only the request was being dropped.
427
+ test('exposes http on the wire so session middleware can read it', async () => {
428
+ pikkuState(null, 'mcp', 'toolsMeta').whoami = {
429
+ name: 'whoami',
430
+ title: 'Who am I',
431
+ description: 'Who am I',
432
+ pikkuFuncId: 'whoamiFunc',
433
+ inputSchema: null,
434
+ outputSchema: 'MCPToolResponse',
435
+ } as never
436
+
437
+ let seenAuthorization: string | undefined
438
+ registerFunction('whoamiFunc', async (_services, _data, wire) => {
439
+ seenAuthorization = wire.http?.request?.header('authorization')
440
+ return [{ type: 'text', text: seenAuthorization ?? 'anonymous' }]
441
+ })
442
+
443
+ const http = {
444
+ request: {
445
+ header: (name: string) =>
446
+ name.toLowerCase() === 'authorization'
447
+ ? 'Bearer token-abc'
448
+ : undefined,
449
+ },
450
+ }
451
+
452
+ const response = await runMCPTool(
453
+ { jsonrpc: '2.0', id: 'req-http', params: {} },
454
+ { mcp: mcpWire as never, http: http as never },
455
+ 'whoami'
456
+ )
457
+
458
+ assert.equal(seenAuthorization, 'Bearer token-abc')
459
+ assert.deepEqual(response, {
460
+ id: 'req-http',
461
+ result: [{ type: 'text', text: 'Bearer token-abc' }],
462
+ })
463
+ })
464
+
465
+ test('omitting http leaves the wire without one rather than inventing it', async () => {
466
+ pikkuState(null, 'mcp', 'toolsMeta').anon = {
467
+ name: 'anon',
468
+ title: 'Anon',
469
+ description: 'Anon',
470
+ pikkuFuncId: 'anonFunc',
471
+ inputSchema: null,
472
+ outputSchema: 'MCPToolResponse',
473
+ } as never
474
+
475
+ let sawHttp: unknown = 'unset'
476
+ registerFunction('anonFunc', async (_services, _data, wire) => {
477
+ sawHttp = wire.http
478
+ return [{ type: 'text', text: 'ok' }]
479
+ })
480
+
481
+ await runMCPTool(
482
+ { jsonrpc: '2.0', id: 'req-anon', params: {} },
483
+ { mcp: mcpWire as never },
484
+ 'anon'
485
+ )
486
+
487
+ assert.equal(sawHttp, undefined)
488
+ })
489
+ })
490
+
491
+ describe('MCP tools fronting a session-requiring function', () => {
492
+ // The whole point of carrying the request: this is the template failure,
493
+ // reproduced. `listRoomsTool` and five others front a `pikkuFunc`, which
494
+ // requires a session, and answered 'Authentication required' to every caller
495
+ // because no request ever reached the middleware that would have set one.
496
+ test('receive the session set by global middleware from the request', async () => {
497
+ addGlobalMiddleware([
498
+ async (_services, wire, next) => {
499
+ const header = wire.http?.request?.header('authorization')
500
+ if (header === 'Bearer token-abc') {
501
+ wire.setSession?.({ userId: 'usr_1', name: 'Rafa' } as never)
502
+ }
503
+ await next()
504
+ },
505
+ ] as never)
506
+
507
+ pikkuState(null, 'mcp', 'toolsMeta').listRooms = {
508
+ name: 'listRooms',
509
+ title: 'List rooms',
510
+ description: 'List rooms',
511
+ pikkuFuncId: 'listRoomsFunc',
512
+ inputSchema: null,
513
+ outputSchema: 'MCPToolResponse',
514
+ } as never
515
+
516
+ // sessionless: false — the distinction that made these tools fail.
517
+ addFunction(
518
+ 'listRoomsFunc',
519
+ {
520
+ func: async (_services: any, _data: any, wire: any) => [
521
+ { type: 'text', text: wire.session?.userId ?? 'anonymous' },
522
+ ],
523
+ } as never,
524
+ null
525
+ )
526
+ pikkuState(null, 'function', 'meta').listRoomsFunc = {
527
+ name: 'listRoomsFunc',
528
+ sessionless: false,
529
+ permissions: [],
530
+ } as never
531
+
532
+ const response = await runMCPTool(
533
+ { jsonrpc: '2.0', id: 'req-session', params: {} },
534
+ {
535
+ mcp: mcpWire as never,
536
+ http: {
537
+ request: {
538
+ header: (name: string) =>
539
+ name.toLowerCase() === 'authorization'
540
+ ? 'Bearer token-abc'
541
+ : undefined,
542
+ },
543
+ } as never,
544
+ },
545
+ 'listRooms'
546
+ )
547
+
548
+ assert.deepEqual(response, {
549
+ id: 'req-session',
550
+ result: [{ type: 'text', text: 'usr_1' }],
551
+ })
552
+ })
553
+ })
554
+
374
555
  describe('runMCPPrompt', () => {
375
556
  test('maps missing prompt registration to not found MCP errors', async () => {
376
557
  pikkuState(null, 'mcp', 'promptsMeta').missingPrompt = {
@@ -1,4 +1,5 @@
1
1
  import type { PikkuRawWire } from '../../types/core.types.js'
2
+ import type { PikkuHTTP } from '../http/http.types.js'
2
3
  import type {
3
4
  CoreMCPResource,
4
5
  CoreMCPPrompt,
@@ -35,6 +36,20 @@ export class MCPError extends Error {
35
36
 
36
37
  export type RunMCPEndpointParams<Tools extends string = any> = {
37
38
  mcp?: PikkuMCP<Tools>
39
+ /**
40
+ * The HTTP request the MCP call arrived on, when it arrived over HTTP.
41
+ *
42
+ * Auth middleware reads the session off `wire.http.request` — every
43
+ * implementation opens with `if (!http?.request) return` — so without this an
44
+ * MCP call reaches the function unauthenticated no matter what middleware the
45
+ * app has registered, and a tool fronting a session-requiring function can
46
+ * only ever answer 'Authentication required'.
47
+ *
48
+ * Left undefined for transports that have no request to offer, such as stdio.
49
+ * Those remain anonymous, which is a property of the transport rather than a
50
+ * default chosen here.
51
+ */
52
+ http?: PikkuHTTP
38
53
  /** Defaults to enabled outside production. */
39
54
  exposeErrors?: boolean
40
55
  }
@@ -97,6 +112,12 @@ export async function runMCPResource(
97
112
  const metas = pikkuState(null, 'mcp', 'resourcesMeta')
98
113
  const endpoints = pikkuState(null, 'mcp', 'resources')
99
114
 
115
+ // The key the resource's meta (and its middleware) is stored under. For a
116
+ // templated resource this is the template, not the concrete request URI — so
117
+ // it must be carried forward, or the meta lookup downstream misses and the
118
+ // resource's declared middleware (including auth) is silently skipped.
119
+ let metaKey = uri
120
+
100
121
  if (endpoints.has(uri)) {
101
122
  endpoint = endpoints.get(uri)
102
123
  pikkuFuncId = metas[uri]?.pikkuFuncId
@@ -113,6 +134,7 @@ export async function runMCPResource(
113
134
 
114
135
  if (match) {
115
136
  endpoint = value
137
+ metaKey = uriTemplate
116
138
  pikkuFuncId = metas[uriTemplate]?.pikkuFuncId
117
139
 
118
140
  for (let i = 0; i < paramNames.length; i++) {
@@ -134,7 +156,8 @@ export async function runMCPResource(
134
156
  pikkuFuncId,
135
157
  { ...params, mcp: { ...params.mcp, uri } } as RunMCPEndpointParams<
136
158
  keyof CoreMCPResource
137
- >
159
+ >,
160
+ metaKey
138
161
  )
139
162
  }
140
163
 
@@ -177,7 +200,11 @@ async function runMCPPikkuFunc(
177
200
  name: string,
178
201
  mcp: CoreMCPResource | CoreMCPPrompt | undefined,
179
202
  pikkuFuncId: string | undefined,
180
- { mcp: mcpWire, exposeErrors = !isProduction() }: RunMCPEndpointParams
203
+ { mcp: mcpWire, http, exposeErrors = !isProduction() }: RunMCPEndpointParams,
204
+ // The key the endpoint's meta is stored under. Differs from `name` only for a
205
+ // templated resource, where `name` is the concrete URI and the meta lives
206
+ // under the template. Defaults to `name` for tools and prompts.
207
+ metaKey: string = name
181
208
  ): Promise<JsonRpcResponse> {
182
209
  const singletonServices = getSingletonServices()
183
210
  const createWireServices = getCreateWireServices()
@@ -208,16 +235,19 @@ async function runMCPPikkuFunc(
208
235
  )
209
236
  const wire: PikkuRawWire = {
210
237
  mcp: mcpWire,
238
+ // Only when the transport supplied one. Setting `http: undefined`
239
+ // explicitly would be the same to a reader and different to a spread.
240
+ ...(http ? { http } : {}),
211
241
  ...createMiddlewareSessionWireProps(mcpSessionService),
212
242
  }
213
243
 
214
244
  let meta: any
215
245
  if (type === 'resource') {
216
- meta = pikkuState(null, 'mcp', 'resourcesMeta')[name]
246
+ meta = pikkuState(null, 'mcp', 'resourcesMeta')[metaKey]
217
247
  } else if (type === 'tool') {
218
- meta = pikkuState(null, 'mcp', 'toolsMeta')[name]
248
+ meta = pikkuState(null, 'mcp', 'toolsMeta')[metaKey]
219
249
  } else if (type === 'prompt') {
220
- meta = pikkuState(null, 'mcp', 'promptsMeta')[name]
250
+ meta = pikkuState(null, 'mcp', 'promptsMeta')[metaKey]
221
251
  }
222
252
 
223
253
  let resolvedFuncName = pikkuFuncId
@@ -10,7 +10,6 @@ export type { RoleVerification } from './validate-personas.js'
10
10
  export {
11
11
  personaEnvironmentErrors,
12
12
  personaEnvironmentRefusal,
13
- resolvePersonaEnvironments,
14
13
  } from './persona-environments.js'
15
14
  export type {
16
15
  PersonaEnvironment,
@@ -18,14 +17,8 @@ export type {
18
17
  } from './persona-environments.js'
19
18
  export {
20
19
  personaEmail,
21
- personaEmailLabel,
22
20
  personaEmails,
23
21
  } from './persona-email.js'
24
- export {
25
- allowedLinks,
26
- applyMailboxAllowlist,
27
- isAllowedSender,
28
- } from './persona-mailbox.js'
29
22
  export type {
30
23
  MailboxAllowlist,
31
24
  PersonaMailbox,
@@ -33,7 +26,6 @@ export type {
33
26
  } from './persona-mailbox.js'
34
27
  export type {
35
28
  CorePersona,
36
- CorePersonaAccount,
37
29
  CorePersonas,
38
30
  PersonaAccountMeta,
39
31
  PersonaDefinitions,
@@ -17,26 +17,12 @@ export type {
17
17
  export {
18
18
  wireQueueWorker,
19
19
  runQueueJob,
20
- getQueueWorkers,
21
- removeQueueWorker,
22
20
  QueueJobDiscardedError,
23
21
  QueueJobFailedError,
24
22
  } from './queue-runner.js'
25
-
26
- export {
27
- QUEUE_IDENTITY_CLAIM_VERSION,
28
- QUEUE_IDENTITY_INFO,
29
- QUEUE_IDENTITY_SECRET_NAME,
30
- getQueueIdentitySecret,
31
- resolveQueueJobIdentity,
32
- signQueueIdentity,
33
- verifyQueueIdentity,
34
- } from './queue-identity.js'
35
23
  export type { QueueIdentityBinding } from './queue-identity.js'
36
24
 
37
25
  export { SignedQueueService } from './signed-queue-service.js'
38
-
39
- export { validateWorkerConfig } from './validate-worker-config.js'
40
26
  export type { QueueConfigMapping } from './validate-worker-config.js'
41
27
 
42
28
  export { registerQueueWorkers } from './register-queue-helper.js'