@pikku/core 0.12.20 → 0.12.22

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 (141) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/dev/hot-reload.js +20 -6
  3. package/dist/errors/error-handler.d.ts +1 -1
  4. package/dist/errors/error-handler.js +2 -2
  5. package/dist/function/functions.types.d.ts +3 -2
  6. package/dist/handle-error.js +3 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +2 -2
  9. package/dist/middleware/index.d.ts +2 -2
  10. package/dist/middleware/index.js +2 -2
  11. package/dist/middleware/telemetry.d.ts +2 -2
  12. package/dist/middleware/telemetry.js +2 -2
  13. package/dist/middleware-runner.d.ts +15 -27
  14. package/dist/middleware-runner.js +25 -30
  15. package/dist/permissions.d.ts +15 -22
  16. package/dist/permissions.js +30 -25
  17. package/dist/pikku-request.js +1 -1
  18. package/dist/pikku-state.js +2 -3
  19. package/dist/services/ai-agent-runner-service.d.ts +1 -0
  20. package/dist/services/in-memory-queue-service.js +1 -1
  21. package/dist/services/in-memory-workflow-service.d.ts +15 -13
  22. package/dist/services/in-memory-workflow-service.js +31 -13
  23. package/dist/services/local-content.js +8 -1
  24. package/dist/services/user-session-service.d.ts +1 -1
  25. package/dist/testing/service-tests.js +24 -0
  26. package/dist/types/core.types.d.ts +4 -0
  27. package/dist/types/state.types.d.ts +2 -18
  28. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
  29. package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
  30. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
  31. package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
  32. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
  33. package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
  34. package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
  35. package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
  36. package/dist/wirings/channel/channel-handler.js +1 -1
  37. package/dist/wirings/channel/channel-store.d.ts +13 -0
  38. package/dist/wirings/channel/channel.types.d.ts +3 -0
  39. package/dist/wirings/channel/local/local-channel-runner.js +23 -5
  40. package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
  41. package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
  42. package/dist/wirings/cli/cli-runner.js +24 -5
  43. package/dist/wirings/cli/command-parser.js +19 -4
  44. package/dist/wirings/http/http-runner.js +72 -36
  45. package/dist/wirings/http/http.types.d.ts +1 -0
  46. package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
  47. package/dist/wirings/http/web-request.js +32 -0
  48. package/dist/wirings/rpc/rpc-runner.js +13 -3
  49. package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
  50. package/dist/wirings/workflow/graph/graph-node.js +4 -0
  51. package/dist/wirings/workflow/graph/graph-runner.js +12 -10
  52. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
  53. package/dist/wirings/workflow/index.d.ts +1 -1
  54. package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
  55. package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
  56. package/dist/wirings/workflow/workflow.types.d.ts +34 -0
  57. package/package.json +1 -1
  58. package/run-tests.sh +4 -1
  59. package/src/dev/hot-reload.test.ts +62 -11
  60. package/src/dev/hot-reload.ts +28 -7
  61. package/src/errors/error-handler.ts +8 -2
  62. package/src/errors/error.test.ts +2 -0
  63. package/src/function/function-runner.test.ts +500 -10
  64. package/src/function/functions.types.ts +3 -2
  65. package/src/handle-error.test.ts +1 -1
  66. package/src/handle-error.ts +4 -1
  67. package/src/index.ts +12 -2
  68. package/src/middleware/index.ts +11 -2
  69. package/src/middleware/telemetry.ts +2 -2
  70. package/src/middleware-runner.test.ts +16 -16
  71. package/src/middleware-runner.ts +42 -30
  72. package/src/permissions.test.ts +27 -24
  73. package/src/permissions.ts +41 -25
  74. package/src/pikku-request.test.ts +35 -0
  75. package/src/pikku-request.ts +1 -1
  76. package/src/pikku-state.ts +2 -3
  77. package/src/run-tests-script.test.ts +18 -0
  78. package/src/services/ai-agent-runner-service.ts +1 -0
  79. package/src/services/in-memory-queue-service.ts +1 -1
  80. package/src/services/in-memory-session-store.ts +1 -2
  81. package/src/services/in-memory-workflow-service.test.ts +33 -11
  82. package/src/services/in-memory-workflow-service.ts +49 -13
  83. package/src/services/local-content.test.ts +54 -0
  84. package/src/services/local-content.ts +12 -1
  85. package/src/services/typed-credential-service.ts +3 -3
  86. package/src/services/typed-secret-service.ts +3 -3
  87. package/src/services/typed-variables-service.ts +3 -3
  88. package/src/services/user-session-service.ts +4 -4
  89. package/src/testing/service-tests.ts +30 -0
  90. package/src/types/core.types.ts +6 -2
  91. package/src/types/state.types.ts +2 -13
  92. package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
  93. package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
  94. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
  95. package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
  96. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
  97. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
  98. package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
  99. package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
  100. package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
  101. package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
  102. package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
  103. package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
  104. package/src/wirings/channel/channel-handler.test.ts +272 -0
  105. package/src/wirings/channel/channel-handler.ts +1 -1
  106. package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
  107. package/src/wirings/channel/channel-store.ts +19 -0
  108. package/src/wirings/channel/channel.types.ts +4 -0
  109. package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
  110. package/src/wirings/channel/local/local-channel-runner.ts +41 -5
  111. package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
  112. package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
  113. package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
  114. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
  115. package/src/wirings/cli/cli-runner.test.ts +255 -2
  116. package/src/wirings/cli/cli-runner.ts +31 -10
  117. package/src/wirings/cli/cli.types.ts +4 -8
  118. package/src/wirings/cli/command-parser.test.ts +83 -0
  119. package/src/wirings/cli/command-parser.ts +23 -4
  120. package/src/wirings/http/http-runner.test.ts +296 -1
  121. package/src/wirings/http/http-runner.ts +87 -57
  122. package/src/wirings/http/http.types.ts +11 -26
  123. package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
  124. package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
  125. package/src/wirings/http/web-request.test.ts +115 -0
  126. package/src/wirings/http/web-request.ts +43 -5
  127. package/src/wirings/mcp/mcp-runner.test.ts +367 -0
  128. package/src/wirings/queue/queue.types.ts +2 -5
  129. package/src/wirings/rpc/rpc-runner.test.ts +511 -0
  130. package/src/wirings/rpc/rpc-runner.ts +15 -3
  131. package/src/wirings/rpc/wire-addon.test.ts +57 -0
  132. package/src/wirings/workflow/graph/graph-node.ts +12 -0
  133. package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
  134. package/src/wirings/workflow/graph/graph-runner.ts +28 -10
  135. package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
  136. package/src/wirings/workflow/index.ts +1 -0
  137. package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
  138. package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
  139. package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
  140. package/src/wirings/workflow/workflow.types.ts +68 -5
  141. package/tsconfig.tsbuildinfo +1 -1
package/src/index.ts CHANGED
@@ -123,8 +123,18 @@ export { createGraph } from './wirings/workflow/graph/graph-node.js'
123
123
  export { wireAddon } from './wirings/rpc/wire-addon.js'
124
124
  export type { WireAddonConfig } from './wirings/rpc/wire-addon.js'
125
125
  export type { PikkuPackageState } from './types/state.types.js'
126
- export { runMiddleware, addMiddleware } from './middleware-runner.js'
127
- export { addPermission, checkAuthPermissions } from './permissions.js'
126
+ export {
127
+ runMiddleware,
128
+ addTagMiddleware,
129
+ addTagMiddleware as addMiddleware,
130
+ addGlobalMiddleware,
131
+ } from './middleware-runner.js'
132
+ export {
133
+ addTagPermission,
134
+ addTagPermission as addPermission,
135
+ addGlobalPermission,
136
+ checkAuthPermissions,
137
+ } from './permissions.js'
128
138
  export { isSerializable, stopSingletonServices } from './utils.js'
129
139
  export { getSingletonServices, getCreateWireServices } from './pikku-state.js'
130
140
  export {
@@ -4,5 +4,14 @@ export { authBearer } from './auth-bearer.js'
4
4
  export { pikkuRemoteAuthMiddleware } from './remote-auth.js'
5
5
  export { cors } from './cors.js'
6
6
  export { telemetryOuter, telemetryInner } from './telemetry.js'
7
- export { addMiddleware, runMiddleware } from '../middleware-runner.js'
8
- export { addPermission } from '../permissions.js'
7
+ export {
8
+ addTagMiddleware,
9
+ addTagMiddleware as addMiddleware,
10
+ addGlobalMiddleware,
11
+ runMiddleware,
12
+ } from '../middleware-runner.js'
13
+ export {
14
+ addTagPermission,
15
+ addTagPermission as addPermission,
16
+ addGlobalPermission,
17
+ } from '../permissions.js'
@@ -13,7 +13,7 @@ import { pikkuMiddleware, pikkuMiddlewareFactory } from '../types/core.types.js'
13
13
  * @example
14
14
  * ```typescript
15
15
  * import { telemetryOuter } from '@pikku/core/middleware'
16
- * addMiddleware('myTag', [telemetryOuter()])
16
+ * addTagMiddleware('myTag', [telemetryOuter()])
17
17
  * ```
18
18
  */
19
19
  export const telemetryOuter = pikkuMiddlewareFactory<{
@@ -70,7 +70,7 @@ export const telemetryOuter = pikkuMiddlewareFactory<{
70
70
  * @example
71
71
  * ```typescript
72
72
  * import { telemetryInner } from '@pikku/core/middleware'
73
- * addMiddleware('myTag', [telemetryInner()])
73
+ * addTagMiddleware('myTag', [telemetryInner()])
74
74
  * ```
75
75
  */
76
76
  export const telemetryInner = pikkuMiddlewareFactory<{
@@ -2,7 +2,7 @@ import { describe, test, beforeEach } from 'node:test'
2
2
  import assert from 'node:assert'
3
3
  import {
4
4
  combineMiddleware,
5
- addMiddleware,
5
+ addTagMiddleware,
6
6
  runMiddleware,
7
7
  } from './middleware-runner.js'
8
8
  import { resetPikkuState } from './pikku-state.js'
@@ -115,8 +115,8 @@ describe('combineMiddleware', () => {
115
115
  await next()
116
116
  }
117
117
 
118
- addMiddleware('wiringTag', [wiringTagMiddleware])
119
- addMiddleware('funcTag', [funcTagMiddleware])
118
+ addTagMiddleware('wiringTag', [wiringTagMiddleware])
119
+ addTagMiddleware('funcTag', [funcTagMiddleware])
120
120
 
121
121
  const wiringMiddleware: CorePikkuMiddleware = async (
122
122
  services,
@@ -157,7 +157,7 @@ describe('combineMiddleware', () => {
157
157
  await next()
158
158
  }
159
159
 
160
- addMiddleware('testTag', [taggedMiddleware])
160
+ addTagMiddleware('testTag', [taggedMiddleware])
161
161
 
162
162
  const result = combineMiddleware('http', Math.random().toString(), {
163
163
  wireInheritedMiddleware: [{ type: 'tag', tag: 'testTag' }],
@@ -176,7 +176,7 @@ describe('combineMiddleware', () => {
176
176
  await next()
177
177
  }
178
178
 
179
- addMiddleware('funcTestTag', [taggedMiddleware])
179
+ addTagMiddleware('funcTestTag', [taggedMiddleware])
180
180
 
181
181
  const result = combineMiddleware('http', Math.random().toString(), {
182
182
  funcInheritedMiddleware: [{ type: 'tag', tag: 'funcTestTag' }],
@@ -194,8 +194,8 @@ describe('combineMiddleware', () => {
194
194
  await next()
195
195
  }
196
196
 
197
- addMiddleware('tag1', [middleware1])
198
- addMiddleware('tag2', [middleware2])
197
+ addTagMiddleware('tag1', [middleware1])
198
+ addTagMiddleware('tag2', [middleware2])
199
199
 
200
200
  const result = combineMiddleware('http', Math.random().toString(), {
201
201
  wireInheritedMiddleware: [
@@ -218,7 +218,7 @@ describe('combineMiddleware', () => {
218
218
  await next()
219
219
  }
220
220
 
221
- addMiddleware('billing', [billingMiddleware])
221
+ addTagMiddleware('billing', [billingMiddleware])
222
222
 
223
223
  const result = combineMiddleware('http', Math.random().toString(), {
224
224
  wireInheritedMiddleware: [{ type: 'tag', tag: 'billing:read' }],
@@ -237,7 +237,7 @@ describe('combineMiddleware', () => {
237
237
  await next()
238
238
  }
239
239
 
240
- addMiddleware('billing', [billingMiddleware])
240
+ addTagMiddleware('billing', [billingMiddleware])
241
241
 
242
242
  const result = combineMiddleware('http', Math.random().toString(), {
243
243
  wireInheritedMiddleware: [{ type: 'tag', tag: 'billing' }],
@@ -263,8 +263,8 @@ describe('combineMiddleware', () => {
263
263
  await next()
264
264
  }
265
265
 
266
- addMiddleware('billing', [billingMiddleware])
267
- addMiddleware('billing:read', [billingReadMiddleware])
266
+ addTagMiddleware('billing', [billingMiddleware])
267
+ addTagMiddleware('billing:read', [billingReadMiddleware])
268
268
 
269
269
  const result = combineMiddleware('http', Math.random().toString(), {
270
270
  wireInheritedMiddleware: [{ type: 'tag', tag: 'billing:read' }],
@@ -298,9 +298,9 @@ describe('combineMiddleware', () => {
298
298
  await next()
299
299
  }
300
300
 
301
- addMiddleware('billing', [billingMiddleware])
302
- addMiddleware('billing:read', [billingReadMiddleware])
303
- addMiddleware('billing:read:admin', [billingReadAdminMiddleware])
301
+ addTagMiddleware('billing', [billingMiddleware])
302
+ addTagMiddleware('billing:read', [billingReadMiddleware])
303
+ addTagMiddleware('billing:read:admin', [billingReadAdminMiddleware])
304
304
 
305
305
  const result = combineMiddleware('http', Math.random().toString(), {
306
306
  wireInheritedMiddleware: [{ type: 'tag', tag: 'billing:read:admin' }],
@@ -321,7 +321,7 @@ describe('combineMiddleware', () => {
321
321
  await next()
322
322
  }
323
323
 
324
- addMiddleware('billing', [billingMiddleware])
324
+ addTagMiddleware('billing', [billingMiddleware])
325
325
 
326
326
  const result = combineMiddleware('http', Math.random().toString(), {
327
327
  funcInheritedMiddleware: [{ type: 'tag', tag: 'billing:write' }],
@@ -336,7 +336,7 @@ describe('combineMiddleware', () => {
336
336
  await next()
337
337
  }
338
338
 
339
- addMiddleware('existingTag', [middleware])
339
+ addTagMiddleware('existingTag', [middleware])
340
340
 
341
341
  const result = combineMiddleware('http', Math.random().toString(), {
342
342
  wireInheritedMiddleware: [
@@ -91,39 +91,16 @@ const isSortedByPriority = (
91
91
  }
92
92
 
93
93
  /**
94
- * Registers global middleware for a specific tag.
94
+ * Registers tag-scoped middleware. Applies to any wiring (HTTP, Channel,
95
+ * Queue, Scheduler, MCP, CLI, Workflow, Agent) that carries the matching tag.
95
96
  *
96
- * This function registers middleware at runtime that will be applied to
97
- * any wiring (HTTP, Channel, Queue, Scheduler, MCP, CLI) that includes the matching tag.
98
- *
99
- * For tree-shaking benefits, wrap in a factory function:
100
- * `export const x = () => addMiddleware('tag', [...])`
101
- *
102
- * Accepts an array that can contain:
103
- * - Direct middleware functions (CorePikkuMiddleware)
104
- * - Factory middleware functions (CorePikkuMiddlewareFactory)
105
- *
106
- * @template PikkuMiddleware The middleware type.
107
- * @param {string} tag - The tag that the middleware should apply to.
108
- * @param {CorePikkuMiddlewareGroup} middleware - Array of middleware for this tag.
109
- *
110
- * @returns {CorePikkuMiddlewareGroup} The middleware array (for chaining/wrapping).
97
+ * For tree-shaking, wrap in a factory:
98
+ * `export const x = () => addTagMiddleware('tag', [...])`
111
99
  *
112
100
  * @example
113
- * ```typescript
114
- * // Recommended: tree-shakeable
115
- * export const adminMiddleware = () => addMiddleware('admin', [
116
- * authMiddleware,
117
- * loggingMiddleware({ level: 'info' })
118
- * ])
119
- *
120
- * // Also works: no tree-shaking
121
- * export const apiMiddleware = addMiddleware('api', [
122
- * rateLimitMiddleware
123
- * ])
124
- * ```
101
+ * export const adminMiddleware = () => addTagMiddleware('admin', [authMiddleware])
125
102
  */
126
- export const addMiddleware = <PikkuMiddleware extends CorePikkuMiddleware>(
103
+ export const addTagMiddleware = <PikkuMiddleware extends CorePikkuMiddleware>(
127
104
  tag: string,
128
105
  middleware: CorePikkuMiddlewareGroup,
129
106
  packageName: string | null = null
@@ -133,6 +110,33 @@ export const addMiddleware = <PikkuMiddleware extends CorePikkuMiddleware>(
133
110
  return middleware
134
111
  }
135
112
 
113
+ /**
114
+ * Registers wire-agnostic global middleware. Runs at the top of every
115
+ * wiring's middleware chain — before wire-, tag-, and function-level
116
+ * entries — across HTTP, Channel, Queue, Scheduler, MCP, CLI, Workflow, Agent.
117
+ *
118
+ * Resolution order: global → wire → tag → function.
119
+ *
120
+ * @example
121
+ * addGlobalMiddleware([telemetryMiddleware])
122
+ */
123
+ export const addGlobalMiddleware = <
124
+ PikkuMiddleware extends CorePikkuMiddleware,
125
+ >(
126
+ middleware: CorePikkuMiddlewareGroup,
127
+ packageName: string | null = null
128
+ ): CorePikkuMiddlewareGroup => {
129
+ const state = pikkuState(
130
+ packageName,
131
+ 'middleware',
132
+ 'global'
133
+ ) as unknown as CorePikkuMiddlewareGroup
134
+ ;(state as CorePikkuMiddleware[]).push(
135
+ ...(middleware as CorePikkuMiddleware[])
136
+ )
137
+ return middleware
138
+ }
139
+
136
140
  /**
137
141
  * Retrieves a registered middleware function by its name.
138
142
  *
@@ -218,7 +222,15 @@ export const combineMiddleware = (
218
222
 
219
223
  const resolved: CorePikkuMiddleware[] = []
220
224
 
221
- // 1. Resolve wire inherited middleware (HTTP + tag groups + individual wire middleware)
225
+ const globals = pikkuState(
226
+ packageName,
227
+ 'middleware',
228
+ 'global'
229
+ ) as unknown as CorePikkuMiddleware[]
230
+ if (globals && globals.length > 0) {
231
+ resolved.push(...globals)
232
+ }
233
+
222
234
  if (wireInheritedMiddleware) {
223
235
  for (const meta of wireInheritedMiddleware) {
224
236
  if (meta.type === 'http') {
@@ -1,6 +1,6 @@
1
1
  import { describe, test, beforeEach } from 'node:test'
2
2
  import * as assert from 'node:assert'
3
- import { addPermission, runPermissions } from './permissions.js'
3
+ import { addTagPermission, runPermissions } from './permissions.js'
4
4
  import { pikkuState, resetPikkuState } from './pikku-state.js'
5
5
  import type { CoreServices, CoreUserSession } from './types/core.types.js'
6
6
  import type {
@@ -47,11 +47,11 @@ const mockSession: CoreUserSession = {
47
47
  userId: 'test-user',
48
48
  } as any
49
49
 
50
- describe('addPermission', () => {
50
+ describe('addTagPermission', () => {
51
51
  test('should add single permission for a tag', () => {
52
52
  const mockPermission = async () => true
53
53
 
54
- addPermission('testTag', [mockPermission])
54
+ addTagPermission('testTag', [mockPermission])
55
55
 
56
56
  const permissions = getPermissionsForTags(['testTag'])
57
57
  assert.equal(permissions.length, 1)
@@ -62,7 +62,7 @@ describe('addPermission', () => {
62
62
  const mockPermission1 = async () => true
63
63
  const mockPermission2 = async () => false
64
64
 
65
- addPermission('multiTestTag', [mockPermission1, mockPermission2])
65
+ addTagPermission('multiTestTag', [mockPermission1, mockPermission2])
66
66
 
67
67
  const permissions = getPermissionsForTags(['multiTestTag'])
68
68
  assert.equal(permissions.length, 2)
@@ -75,7 +75,7 @@ describe('addPermission', () => {
75
75
  permissions: [async () => true, async () => false],
76
76
  }
77
77
 
78
- addPermission('groupTestTag', mockPermissionGroup)
78
+ addTagPermission('groupTestTag', mockPermissionGroup)
79
79
 
80
80
  const permissions = getPermissionsForTags(['groupTestTag'])
81
81
  assert.equal(permissions.length, 1)
@@ -85,7 +85,7 @@ describe('addPermission', () => {
85
85
  test('should apply parent tag permission to namespaced tag via runPermissions', async () => {
86
86
  const billingPermission = async () => true
87
87
 
88
- addPermission('billing', [billingPermission])
88
+ addTagPermission('billing', [billingPermission])
89
89
 
90
90
  await runPermissions('rpc', Math.random().toString(), {
91
91
  wireInheritedPermissions: [{ type: 'tag', tag: 'billing:write' }],
@@ -106,8 +106,8 @@ describe('addPermission', () => {
106
106
  return true
107
107
  }
108
108
 
109
- addPermission('billing', [billingPermission])
110
- addPermission('billing:read', [billingReadPermission])
109
+ addTagPermission('billing', [billingPermission])
110
+ addTagPermission('billing:read', [billingReadPermission])
111
111
 
112
112
  await runPermissions('rpc', Math.random().toString(), {
113
113
  wireInheritedPermissions: [{ type: 'tag', tag: 'billing:read' }],
@@ -134,9 +134,9 @@ describe('addPermission', () => {
134
134
  return true
135
135
  }
136
136
 
137
- addPermission('billing', [billingPermission])
138
- addPermission('billing:read', [billingReadPermission])
139
- addPermission('billing:read:admin', [billingReadAdminPermission])
137
+ addTagPermission('billing', [billingPermission])
138
+ addTagPermission('billing:read', [billingReadPermission])
139
+ addTagPermission('billing:read:admin', [billingReadAdminPermission])
140
140
 
141
141
  await runPermissions('rpc', Math.random().toString(), {
142
142
  wireInheritedPermissions: [{ type: 'tag', tag: 'billing:read:admin' }],
@@ -151,7 +151,7 @@ describe('addPermission', () => {
151
151
  test('should apply parent tag permission via funcInheritedPermissions', async () => {
152
152
  const billingPermission = async () => true
153
153
 
154
- addPermission('billing', [billingPermission])
154
+ addTagPermission('billing', [billingPermission])
155
155
 
156
156
  await runPermissions('rpc', Math.random().toString(), {
157
157
  funcInheritedPermissions: [{ type: 'tag', tag: 'billing:delete' }],
@@ -164,11 +164,11 @@ describe('addPermission', () => {
164
164
  test('should throw error when tag already exists', () => {
165
165
  const mockPermission = async () => true
166
166
 
167
- addPermission('duplicateTag', [mockPermission])
167
+ addTagPermission('duplicateTag', [mockPermission])
168
168
 
169
169
  assert.throws(
170
170
  () => {
171
- addPermission('duplicateTag', [mockPermission])
171
+ addTagPermission('duplicateTag', [mockPermission])
172
172
  },
173
173
  {
174
174
  message:
@@ -195,7 +195,7 @@ describe('runPermissions', () => {
195
195
  return true
196
196
  }
197
197
 
198
- addPermission('wiringTag', [wiringTagPermission])
198
+ addTagPermission('wiringTag', [wiringTagPermission])
199
199
 
200
200
  await runPermissions('rpc', Math.random().toString(), {
201
201
  wireInheritedPermissions: [{ type: 'tag', tag: 'wiringTag' }],
@@ -219,8 +219,8 @@ describe('runPermissions', () => {
219
219
  return true
220
220
  }
221
221
 
222
- addPermission('wiringTag', [wiringTagPermission])
223
- addPermission('funcTag', [funcTagPermission])
222
+ addTagPermission('wiringTag', [wiringTagPermission])
223
+ addTagPermission('funcTag', [funcTagPermission])
224
224
 
225
225
  const wirePermissions: CorePermissionGroup = {
226
226
  permissions: [
@@ -258,7 +258,10 @@ describe('runPermissions', () => {
258
258
  const failingPermission = async () => false
259
259
  const passingPermission = async () => true
260
260
 
261
- addPermission('atLeastOneTestTag', [failingPermission, passingPermission])
261
+ addTagPermission('atLeastOneTestTag', [
262
+ failingPermission,
263
+ passingPermission,
264
+ ])
262
265
 
263
266
  // Should not throw because at least one permission passes
264
267
  await runPermissions('rpc', Math.random().toString(), {
@@ -273,7 +276,7 @@ describe('runPermissions', () => {
273
276
  const failingPermission1 = async () => false
274
277
  const failingPermission2 = async () => false
275
278
 
276
- addPermission('allFailTestTag', [failingPermission1, failingPermission2])
279
+ addTagPermission('allFailTestTag', [failingPermission1, failingPermission2])
277
280
 
278
281
  await assert.rejects(
279
282
  runPermissions('rpc', Math.random().toString(), {
@@ -309,7 +312,7 @@ describe('runPermissions', () => {
309
312
  test('should throw error when all function tag permissions fail', async () => {
310
313
  const failingPermission = async () => false
311
314
 
312
- addPermission('funcTag', [failingPermission])
315
+ addTagPermission('funcTag', [failingPermission])
313
316
 
314
317
  await assert.rejects(
315
318
  runPermissions('rpc', Math.random().toString(), {
@@ -350,7 +353,7 @@ describe('runPermissions', () => {
350
353
  return false
351
354
  }
352
355
 
353
- addPermission('failingWiringTag', [failingWiringTagPermission])
356
+ addTagPermission('failingWiringTag', [failingWiringTagPermission])
354
357
 
355
358
  const wirePermissions: CorePermissionGroup = {
356
359
  permissions: [
@@ -377,7 +380,7 @@ describe('runPermissions', () => {
377
380
  test('should handle array permissions in tag-based permissions', async () => {
378
381
  const arrayPermission = [async () => true, async () => false]
379
382
 
380
- addPermission('arrayTestTag', arrayPermission)
383
+ addTagPermission('arrayTestTag', arrayPermission)
381
384
 
382
385
  // Should not throw because verifyPermissions handles array properly
383
386
  await runPermissions('rpc', Math.random().toString(), {
@@ -393,7 +396,7 @@ describe('runPermissions', () => {
393
396
  permissions: [async () => true],
394
397
  }
395
398
 
396
- addPermission('objectTestTag', permissionGroup)
399
+ addTagPermission('objectTestTag', permissionGroup)
397
400
 
398
401
  // Should not throw because verifyPermissions handles objects properly
399
402
  await runPermissions('rpc', Math.random().toString(), {
@@ -418,7 +421,7 @@ describe('runPermissions', () => {
418
421
 
419
422
  const testData = { test: 'data' }
420
423
 
421
- addPermission('paramTestTag', [testPermission])
424
+ addTagPermission('paramTestTag', [testPermission])
422
425
 
423
426
  await runPermissions('rpc', Math.random().toString(), {
424
427
  wireInheritedPermissions: [{ type: 'tag', tag: 'paramTestTag' }],
@@ -75,34 +75,16 @@ const getPermissionByName = (name: string): CorePikkuPermission | undefined => {
75
75
  }
76
76
 
77
77
  /**
78
- * Adds global permissions for a specific tag.
78
+ * Registers tag-scoped permissions. Applies to any wiring that carries the
79
+ * matching tag.
79
80
  *
80
- * This function allows you to register permissions that will be applied to
81
- * any wiring (HTTP, Channel, Queue, Scheduler, MCP) that includes the matching tag.
82
- *
83
- * For tree-shaking benefits, wrap in a factory function:
84
- * `export const x = () => addPermission('tag', [...])`
85
- *
86
- * @param {string} tag - The tag that the permissions should apply to.
87
- * @param {any[]} permissions - The permissions array to apply for the specified tag.
88
- *
89
- * @returns {CorePermissionGroup | CorePikkuPermission[]} The permissions (for chaining/wrapping).
81
+ * For tree-shaking, wrap in a factory:
82
+ * `export const x = () => addTagPermission('tag', [...])`
90
83
  *
91
84
  * @example
92
- * ```typescript
93
- * // Recommended: tree-shakeable
94
- * export const adminPermissions = () => addPermission('admin', [
95
- * adminPermission,
96
- * rolePermission({ role: 'admin' })
97
- * ])
98
- *
99
- * // Also works: no tree-shaking
100
- * export const apiPermissions = addPermission('api', [
101
- * readPermission
102
- * ])
103
- * ```
85
+ * export const adminPermissions = () => addTagPermission('admin', [adminPermission])
104
86
  */
105
- export const addPermission = (
87
+ export const addTagPermission = (
106
88
  tag: string,
107
89
  permissions: CorePermissionGroup | CorePikkuPermission[],
108
90
  packageName: string | null = null
@@ -117,6 +99,32 @@ export const addPermission = (
117
99
  return permissions
118
100
  }
119
101
 
102
+ /**
103
+ * Registers wire-agnostic global permissions. Runs at the top of every
104
+ * wiring's permission resolution — before wire-, tag-, and function-level
105
+ * entries — across all wire types.
106
+ *
107
+ * Resolution order: global → wire → tag → function.
108
+ *
109
+ * @example
110
+ * addGlobalPermission([signedInUser])
111
+ */
112
+ export const addGlobalPermission = (
113
+ permissions: CorePermissionGroup | CorePikkuPermission[],
114
+ packageName: string | null = null
115
+ ): CorePermissionGroup | CorePikkuPermission[] => {
116
+ const state = pikkuState(packageName, 'permissions', 'global') as unknown as (
117
+ | CorePermissionGroup
118
+ | CorePikkuPermission
119
+ )[]
120
+ if (Array.isArray(permissions)) {
121
+ state.push(...(permissions as CorePikkuPermission[]))
122
+ } else {
123
+ state.push(permissions)
124
+ }
125
+ return permissions
126
+ }
127
+
120
128
  const combinedPermissionsCache: Record<
121
129
  PikkuWiringTypes,
122
130
  Record<string, readonly (CorePermissionGroup | CorePikkuPermission)[]>
@@ -189,7 +197,15 @@ const combinePermissions = (
189
197
 
190
198
  const resolved: (CorePermissionGroup | CorePikkuPermission)[] = []
191
199
 
192
- // 1. Resolve wire inherited permissions (HTTP + tag groups + individual wire permissions)
200
+ const globals = pikkuState(
201
+ packageName,
202
+ 'permissions',
203
+ 'global'
204
+ ) as unknown as (CorePermissionGroup | CorePikkuPermission)[]
205
+ if (globals && globals.length > 0) {
206
+ resolved.push(...globals)
207
+ }
208
+
193
209
  if (wireInheritedPermissions) {
194
210
  for (const meta of wireInheritedPermissions) {
195
211
  if (meta.type === 'http') {
@@ -0,0 +1,35 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { PikkuRequest } from './pikku-request.js'
5
+
6
+ class TestPikkuRequest<In> extends PikkuRequest<In> {}
7
+
8
+ describe('PikkuRequest', () => {
9
+ test('returns stored data', async () => {
10
+ const request = new TestPikkuRequest({ hello: 'world' })
11
+
12
+ assert.deepEqual(await request.data(), { hello: 'world' })
13
+ })
14
+
15
+ test('preserves falsey payloads', async () => {
16
+ await assert.doesNotReject(async () => {
17
+ const zeroRequest = new TestPikkuRequest(0)
18
+ assert.equal(await zeroRequest.data(), 0)
19
+
20
+ const falseRequest = new TestPikkuRequest(false)
21
+ assert.equal(await falseRequest.data(), false)
22
+
23
+ const emptyStringRequest = new TestPikkuRequest('')
24
+ assert.equal(await emptyStringRequest.data(), '')
25
+ })
26
+ })
27
+
28
+ test('throws when data is undefined', async () => {
29
+ const request = new TestPikkuRequest(undefined as never)
30
+
31
+ await assert.rejects(() => request.data(), {
32
+ message: 'Data not found',
33
+ })
34
+ })
35
+ })
@@ -15,7 +15,7 @@ export abstract class PikkuRequest<In = any> {
15
15
  * @returns A promise that resolves to an object containing the combined data.
16
16
  */
17
17
  public async data(): Promise<In> {
18
- if (!this.#data) {
18
+ if (this.#data === undefined) {
19
19
  throw new Error('Data not found')
20
20
  }
21
21
  return this.#data
@@ -135,6 +135,7 @@ const createEmptyPackageState = (): PikkuPackageState => ({
135
135
  middleware: {
136
136
  tagGroup: {},
137
137
  httpGroup: {},
138
+ global: [] as any,
138
139
  },
139
140
  channelMiddleware: {
140
141
  tagGroup: {},
@@ -142,6 +143,7 @@ const createEmptyPackageState = (): PikkuPackageState => ({
142
143
  permissions: {
143
144
  tagGroup: {},
144
145
  httpGroup: {},
146
+ global: [] as any,
145
147
  },
146
148
  misc: {
147
149
  errors: new Map(),
@@ -150,9 +152,6 @@ const createEmptyPackageState = (): PikkuPackageState => ({
150
152
  channelMiddleware: {},
151
153
  permissions: {},
152
154
  },
153
- models: {
154
- config: null,
155
- },
156
155
  package: {
157
156
  factories: null,
158
157
  singletonServices: null,
@@ -29,6 +29,24 @@ describe('run-tests.sh discovery behavior', () => {
29
29
  fs.rmSync(tempDir, { recursive: true, force: true })
30
30
  })
31
31
 
32
+ test('returns success in CI when no tests are found', () => {
33
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pikku-core-tests-'))
34
+ const srcDir = path.join(tempDir, 'src')
35
+ fs.mkdirSync(srcDir, { recursive: true })
36
+ fs.copyFileSync(runTestsScriptPath, path.join(tempDir, 'run-tests.sh'))
37
+
38
+ const result = spawnSync('bash', ['run-tests.sh'], {
39
+ cwd: tempDir,
40
+ encoding: 'utf8',
41
+ env: { ...process.env, CI: 'true' },
42
+ })
43
+
44
+ assert.equal(result.status, 0)
45
+ assert.match(result.stdout, /No test files found matching pattern/)
46
+
47
+ fs.rmSync(tempDir, { recursive: true, force: true })
48
+ })
49
+
32
50
  test('returns failure in strict mode when no tests are found', () => {
33
51
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pikku-core-tests-'))
34
52
  const srcDir = path.join(tempDir, 'src')
@@ -30,6 +30,7 @@ export type AIAgentStepResult = {
30
30
  toolResults: { toolCallId: string; toolName: string; result: unknown }[]
31
31
  usage: { inputTokens: number; outputTokens: number }
32
32
  finishReason: 'stop' | 'tool-calls' | 'length' | 'error' | 'unknown'
33
+ reasoningContent?: string
33
34
  }
34
35
 
35
36
  export interface AIAgentRunnerService {
@@ -24,7 +24,7 @@ export class InMemoryQueueService implements QueueService {
24
24
  pikkuUserId: options?.pikkuUserId,
25
25
  }
26
26
 
27
- const delay = options?.delay ?? 0
27
+ const delay = options?.delay ?? 100 + Math.floor(Math.random() * 201)
28
28
 
29
29
  setTimeout(async () => {
30
30
  try {
@@ -3,8 +3,7 @@ import type { SessionStore } from './session-store.js'
3
3
 
4
4
  export class InMemorySessionStore<
5
5
  UserSession extends CoreUserSession = CoreUserSession,
6
- > implements SessionStore<UserSession>
7
- {
6
+ > implements SessionStore<UserSession> {
8
7
  private sessions = new Map<string, UserSession>()
9
8
 
10
9
  async get(pikkuUserId: string): Promise<UserSession | undefined> {