@pikku/core 0.12.19 → 0.12.21

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 (146) hide show
  1. package/CHANGELOG.md +77 -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/function-runner.js +53 -3
  6. package/dist/function/functions.types.d.ts +3 -2
  7. package/dist/index.d.ts +3 -3
  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/content-service.d.ts +66 -37
  21. package/dist/services/in-memory-queue-service.js +1 -1
  22. package/dist/services/in-memory-workflow-service.d.ts +15 -13
  23. package/dist/services/in-memory-workflow-service.js +31 -13
  24. package/dist/services/index.d.ts +1 -1
  25. package/dist/services/local-content.d.ts +10 -12
  26. package/dist/services/local-content.js +54 -38
  27. package/dist/services/user-session-service.d.ts +1 -1
  28. package/dist/testing/service-tests.js +24 -0
  29. package/dist/types/core.types.d.ts +4 -0
  30. package/dist/types/state.types.d.ts +2 -18
  31. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
  32. package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
  33. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
  34. package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
  35. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
  36. package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
  37. package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
  38. package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
  39. package/dist/wirings/channel/channel-handler.js +1 -1
  40. package/dist/wirings/channel/channel-store.d.ts +13 -0
  41. package/dist/wirings/channel/channel.types.d.ts +3 -0
  42. package/dist/wirings/channel/local/local-channel-runner.js +23 -5
  43. package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
  44. package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
  45. package/dist/wirings/cli/cli-runner.js +24 -5
  46. package/dist/wirings/cli/command-parser.js +19 -4
  47. package/dist/wirings/http/http-runner.js +72 -36
  48. package/dist/wirings/http/http.types.d.ts +1 -0
  49. package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
  50. package/dist/wirings/http/web-request.js +32 -0
  51. package/dist/wirings/rpc/rpc-runner.d.ts +3 -2
  52. package/dist/wirings/rpc/rpc-runner.js +45 -11
  53. package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
  54. package/dist/wirings/workflow/graph/graph-node.js +4 -0
  55. package/dist/wirings/workflow/graph/graph-runner.js +12 -10
  56. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
  57. package/dist/wirings/workflow/index.d.ts +1 -1
  58. package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
  59. package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
  60. package/dist/wirings/workflow/workflow.types.d.ts +34 -0
  61. package/package.json +1 -1
  62. package/run-tests.sh +4 -1
  63. package/src/dev/hot-reload.test.ts +62 -11
  64. package/src/dev/hot-reload.ts +28 -7
  65. package/src/errors/error-handler.ts +8 -2
  66. package/src/errors/error.test.ts +2 -0
  67. package/src/function/function-runner.test.ts +500 -10
  68. package/src/function/function-runner.ts +68 -3
  69. package/src/function/functions.types.ts +3 -2
  70. package/src/index.ts +22 -3
  71. package/src/middleware/index.ts +11 -2
  72. package/src/middleware/telemetry.ts +2 -2
  73. package/src/middleware-runner.test.ts +16 -16
  74. package/src/middleware-runner.ts +42 -30
  75. package/src/permissions.test.ts +27 -24
  76. package/src/permissions.ts +41 -25
  77. package/src/pikku-request.test.ts +35 -0
  78. package/src/pikku-request.ts +1 -1
  79. package/src/pikku-state.ts +2 -3
  80. package/src/run-tests-script.test.ts +18 -0
  81. package/src/services/ai-agent-runner-service.ts +1 -0
  82. package/src/services/content-service.ts +79 -51
  83. package/src/services/in-memory-queue-service.ts +1 -1
  84. package/src/services/in-memory-session-store.ts +1 -2
  85. package/src/services/in-memory-workflow-service.test.ts +33 -11
  86. package/src/services/in-memory-workflow-service.ts +49 -13
  87. package/src/services/index.ts +10 -1
  88. package/src/services/local-content.test.ts +54 -0
  89. package/src/services/local-content.ts +80 -53
  90. package/src/services/typed-credential-service.ts +3 -3
  91. package/src/services/typed-secret-service.ts +3 -3
  92. package/src/services/typed-variables-service.ts +3 -3
  93. package/src/services/user-session-service.ts +4 -4
  94. package/src/testing/service-tests.ts +30 -0
  95. package/src/types/core.types.ts +6 -2
  96. package/src/types/state.types.ts +2 -13
  97. package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
  98. package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
  99. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
  100. package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
  101. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
  102. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
  103. package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
  104. package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
  105. package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
  106. package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
  107. package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
  108. package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
  109. package/src/wirings/channel/channel-handler.test.ts +272 -0
  110. package/src/wirings/channel/channel-handler.ts +1 -1
  111. package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
  112. package/src/wirings/channel/channel-store.ts +19 -0
  113. package/src/wirings/channel/channel.types.ts +4 -0
  114. package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
  115. package/src/wirings/channel/local/local-channel-runner.ts +41 -5
  116. package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
  117. package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
  118. package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
  119. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
  120. package/src/wirings/cli/cli-runner.test.ts +255 -2
  121. package/src/wirings/cli/cli-runner.ts +31 -10
  122. package/src/wirings/cli/cli.types.ts +4 -8
  123. package/src/wirings/cli/command-parser.test.ts +83 -0
  124. package/src/wirings/cli/command-parser.ts +23 -4
  125. package/src/wirings/http/http-runner.test.ts +296 -1
  126. package/src/wirings/http/http-runner.ts +87 -57
  127. package/src/wirings/http/http.types.ts +11 -26
  128. package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
  129. package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
  130. package/src/wirings/http/web-request.test.ts +115 -0
  131. package/src/wirings/http/web-request.ts +43 -5
  132. package/src/wirings/mcp/mcp-runner.test.ts +367 -0
  133. package/src/wirings/queue/queue.types.ts +2 -5
  134. package/src/wirings/rpc/rpc-runner.test.ts +511 -0
  135. package/src/wirings/rpc/rpc-runner.ts +60 -21
  136. package/src/wirings/rpc/wire-addon.test.ts +57 -0
  137. package/src/wirings/workflow/graph/graph-node.ts +12 -0
  138. package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
  139. package/src/wirings/workflow/graph/graph-runner.ts +28 -10
  140. package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
  141. package/src/wirings/workflow/index.ts +1 -0
  142. package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
  143. package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
  144. package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
  145. package/src/wirings/workflow/workflow.types.ts +68 -5
  146. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,511 @@
1
+ import { beforeEach, describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { addFunction } from '../../function/function-runner.js'
5
+ import { pikkuState, resetPikkuState } from '../../pikku-state.js'
6
+ import {
7
+ ContextAwareRPCService,
8
+ RPCNotFoundError,
9
+ resolveNamespace,
10
+ rpcService,
11
+ } from './rpc-runner.js'
12
+
13
+ const createLogger = () => ({
14
+ debug: () => {},
15
+ info: () => {},
16
+ warn: () => {},
17
+ error: () => {},
18
+ })
19
+
20
+ const registerFunction = (
21
+ funcName: string,
22
+ func: (services: any, data: any, wire: any) => Promise<unknown> | unknown,
23
+ {
24
+ packageName = null,
25
+ expose,
26
+ pikkuFuncId,
27
+ tags,
28
+ }: {
29
+ packageName?: string | null
30
+ expose?: boolean
31
+ pikkuFuncId?: string
32
+ tags?: string[]
33
+ } = {}
34
+ ) => {
35
+ addFunction(funcName, { func } as never, packageName)
36
+ pikkuState(packageName, 'function', 'meta')[funcName] = {
37
+ name: funcName,
38
+ pikkuFuncId,
39
+ sessionless: true,
40
+ permissions: [],
41
+ expose,
42
+ tags,
43
+ } as never
44
+ }
45
+
46
+ const createServices = (overrides: Record<string, unknown> = {}) =>
47
+ ({
48
+ logger: createLogger(),
49
+ ...overrides,
50
+ }) as never
51
+
52
+ beforeEach(() => {
53
+ resetPikkuState()
54
+ pikkuState(null, 'package', 'singletonServices', createServices())
55
+ })
56
+
57
+ describe('resolveNamespace', () => {
58
+ test('returns null for non-namespaced functions', () => {
59
+ assert.equal(resolveNamespace('plainFunction'), null)
60
+ })
61
+
62
+ test('returns null when namespace is not registered', () => {
63
+ assert.equal(resolveNamespace('missing:func'), null)
64
+ })
65
+
66
+ test('resolves addon namespace to package and function', () => {
67
+ pikkuState(null, 'addons', 'packages').set('stripe', {
68
+ package: '@addon/stripe',
69
+ auth: true,
70
+ tags: ['addon-tag'],
71
+ } as never)
72
+
73
+ assert.deepEqual(resolveNamespace('stripe:createCharge'), {
74
+ package: '@addon/stripe',
75
+ function: 'createCharge',
76
+ addonConfig: {
77
+ package: '@addon/stripe',
78
+ auth: true,
79
+ tags: ['addon-tag'],
80
+ },
81
+ })
82
+ })
83
+ })
84
+
85
+ describe('ContextAwareRPCService.rpc', () => {
86
+ test('runs bare rpc functions through root rpc metadata', async () => {
87
+ pikkuState(null, 'rpc', 'meta').echo = 'echoFunc'
88
+ registerFunction('echoFunc', async (_services, data, wire) => ({
89
+ data,
90
+ traceId: wire.traceId,
91
+ }))
92
+
93
+ const service = new ContextAwareRPCService(
94
+ createServices(),
95
+ { traceId: 'trace-1' } as never,
96
+ { requiresAuth: false }
97
+ )
98
+
99
+ const result = await service.rpc('echo', { hello: 'world' })
100
+
101
+ assert.deepEqual(result, {
102
+ data: { hello: 'world' },
103
+ traceId: 'trace-1',
104
+ })
105
+ })
106
+
107
+ test('uses package-scoped function meta before root rpc metadata', async () => {
108
+ pikkuState(null, 'rpc', 'meta').echo = 'rootEchoFunc'
109
+ registerFunction('rootEchoFunc', async () => ({ source: 'root' }))
110
+ registerFunction(
111
+ 'packageEchoFunc',
112
+ async () => ({ source: '@addon/pkg' }),
113
+ {
114
+ packageName: '@addon/pkg',
115
+ }
116
+ )
117
+ pikkuState('@addon/pkg', 'function', 'meta').echo = {
118
+ name: 'echo',
119
+ pikkuFuncId: 'packageEchoFunc',
120
+ sessionless: true,
121
+ permissions: [],
122
+ } as never
123
+
124
+ const service = new ContextAwareRPCService(
125
+ createServices(),
126
+ {} as never,
127
+ {},
128
+ '@addon/pkg'
129
+ )
130
+
131
+ const result = await service.rpc('echo', {})
132
+
133
+ assert.deepEqual(result, { source: '@addon/pkg' })
134
+ })
135
+
136
+ test('falls back from versioned rpc names to the base metadata entry', async () => {
137
+ pikkuState(null, 'rpc', 'meta').echo = 'echoFunc'
138
+ registerFunction('echoFunc', async (_services, data) => data)
139
+
140
+ const service = new ContextAwareRPCService(
141
+ createServices(),
142
+ {} as never,
143
+ {}
144
+ )
145
+
146
+ const result = await service.rpc('echo@v2', { ok: true })
147
+
148
+ assert.deepEqual(result, { ok: true })
149
+ })
150
+
151
+ test('resolves internal root functions that are registered outside rpc metadata', async () => {
152
+ registerFunction('pikkuWorkflowSleeper', async (_services, data) => ({
153
+ sleeper: true,
154
+ data,
155
+ }))
156
+
157
+ const service = new ContextAwareRPCService(
158
+ createServices(),
159
+ {} as never,
160
+ {}
161
+ )
162
+
163
+ const result = await service.rpc('pikkuWorkflowSleeper', {
164
+ runId: 'run-1',
165
+ stepId: 'step-1',
166
+ })
167
+
168
+ assert.deepEqual(result, {
169
+ sleeper: true,
170
+ data: {
171
+ runId: 'run-1',
172
+ stepId: 'step-1',
173
+ },
174
+ })
175
+ })
176
+
177
+ test('invokes addon functions with addon auth and tags', async () => {
178
+ pikkuState(null, 'addons', 'packages').set('stripe', {
179
+ package: '@addon/stripe',
180
+ auth: false,
181
+ tags: ['addon-tag'],
182
+ } as never)
183
+ registerFunction(
184
+ 'createCharge',
185
+ async (_services, data, wire) => ({
186
+ data,
187
+ auth: wire.wireType,
188
+ }),
189
+ {
190
+ packageName: '@addon/stripe',
191
+ tags: ['function-tag'],
192
+ }
193
+ )
194
+
195
+ pikkuState('@addon/stripe', 'function', 'meta').createCharge = {
196
+ name: 'createCharge',
197
+ sessionless: true,
198
+ permissions: [],
199
+ tags: ['function-tag'],
200
+ } as never
201
+
202
+ const service = new ContextAwareRPCService(
203
+ createServices({
204
+ logger: createLogger(),
205
+ }),
206
+ { wireType: 'http', wireId: 'wire-1' } as never,
207
+ { requiresAuth: true }
208
+ )
209
+
210
+ const seenTags: string[][] = []
211
+ const addonTagGroup = pikkuState('@addon/stripe', 'permissions', 'tagGroup')
212
+ addonTagGroup['addon-tag'] = [
213
+ async (_services: any, _data: any, wire: any) => {
214
+ seenTags.push(['addon-tag', wire.wireType])
215
+ return true
216
+ },
217
+ ] as never
218
+ addonTagGroup['function-tag'] = [
219
+ async (_services: any, _data: any, wire: any) => {
220
+ seenTags.push(['function-tag', wire.wireType])
221
+ return true
222
+ },
223
+ ] as never
224
+
225
+ const result = await service.rpc('stripe:createCharge', { amount: 10 })
226
+
227
+ assert.deepEqual(result, {
228
+ data: { amount: 10 },
229
+ auth: 'http',
230
+ })
231
+ assert.deepEqual(seenTags, [['addon-tag', 'http']])
232
+ })
233
+
234
+ test('falls back to deploymentService when rpc meta is missing', async () => {
235
+ const calls: unknown[][] = []
236
+ const service = new ContextAwareRPCService(
237
+ createServices({
238
+ deploymentService: {
239
+ invoke: async (...args: unknown[]) => {
240
+ calls.push(args)
241
+ return { remote: true }
242
+ },
243
+ },
244
+ }),
245
+ {
246
+ traceId: 'trace-2',
247
+ getSession: async () => ({ userId: 'user-1' }),
248
+ } as never,
249
+ {}
250
+ )
251
+
252
+ const result = await service.rpc('missingRpc', { value: 1 })
253
+
254
+ assert.deepEqual(result, { remote: true })
255
+ assert.deepEqual(calls, [
256
+ ['missingRpc', { value: 1 }, { userId: 'user-1' }, 'trace-2'],
257
+ ])
258
+ })
259
+
260
+ test('throws RPCNotFoundError when rpc is missing and no deploymentService exists', async () => {
261
+ const service = new ContextAwareRPCService(
262
+ createServices(),
263
+ {} as never,
264
+ {}
265
+ )
266
+
267
+ await assert.rejects(() => service.rpc('missingRpc', {}), RPCNotFoundError)
268
+ })
269
+ })
270
+
271
+ describe('ContextAwareRPCService.rpcExposed', () => {
272
+ test('resolves unversioned exposed rpc names through latest rpc metadata', async () => {
273
+ pikkuState(null, 'rpc', 'meta').listCards = 'listCards@v2'
274
+ registerFunction(
275
+ 'listCards@v2',
276
+ async (_services, data) => ({ data, version: 2 }),
277
+ {
278
+ expose: true,
279
+ }
280
+ )
281
+
282
+ const service = new ContextAwareRPCService(
283
+ createServices(),
284
+ {} as never,
285
+ {}
286
+ )
287
+
288
+ const result = await service.rpcExposed('listCards', { ok: true })
289
+
290
+ assert.deepEqual(result, {
291
+ data: { ok: true },
292
+ version: 2,
293
+ })
294
+ })
295
+
296
+ test('throws when the function does not exist', async () => {
297
+ const service = new ContextAwareRPCService(
298
+ createServices(),
299
+ {} as never,
300
+ {}
301
+ )
302
+
303
+ await assert.rejects(
304
+ () => service.rpcExposed('missingRpc', {}),
305
+ RPCNotFoundError
306
+ )
307
+ })
308
+
309
+ test('throws RPCNotFoundError when the function is not exposed', async () => {
310
+ pikkuState(null, 'rpc', 'meta').hiddenFunc = 'hiddenFunc'
311
+ registerFunction('hiddenFunc', async () => ({ ok: true }), {
312
+ expose: false,
313
+ })
314
+
315
+ const service = new ContextAwareRPCService(
316
+ createServices(),
317
+ {} as never,
318
+ {}
319
+ )
320
+
321
+ await assert.rejects(
322
+ () => service.rpcExposed('hiddenFunc', {}),
323
+ RPCNotFoundError
324
+ )
325
+ })
326
+
327
+ test('runs exposed addon functions', async () => {
328
+ pikkuState(null, 'addons', 'packages').set('addon', {
329
+ package: '@addon/pkg',
330
+ } as never)
331
+ registerFunction(
332
+ 'visibleFunc',
333
+ async (_services, data) => ({ data, source: 'addon' }),
334
+ {
335
+ packageName: '@addon/pkg',
336
+ expose: true,
337
+ }
338
+ )
339
+
340
+ const service = new ContextAwareRPCService(
341
+ createServices(),
342
+ {} as never,
343
+ {}
344
+ )
345
+
346
+ const result = await service.rpcExposed('addon:visibleFunc', {
347
+ ok: true,
348
+ })
349
+
350
+ assert.deepEqual(result, { data: { ok: true }, source: 'addon' })
351
+ })
352
+ })
353
+
354
+ describe('ContextAwareRPCService.rpcWithWire', () => {
355
+ test('merges wire data for local rpc calls', async () => {
356
+ pikkuState(null, 'rpc', 'meta').echo = 'echoFunc'
357
+ registerFunction('echoFunc', async (_services, _data, wire) => ({
358
+ traceId: wire.traceId,
359
+ custom: wire.custom,
360
+ }))
361
+
362
+ const service = new ContextAwareRPCService(
363
+ createServices(),
364
+ { traceId: 'base-trace' } as never,
365
+ {}
366
+ )
367
+
368
+ const result = await service.rpcWithWire('echo', {}, {
369
+ custom: 'merged',
370
+ } as never)
371
+
372
+ assert.deepEqual(result, {
373
+ traceId: 'base-trace',
374
+ custom: 'merged',
375
+ })
376
+ })
377
+
378
+ test('falls back to deploymentService for rpcWithWire when local rpc is missing', async () => {
379
+ const calls: unknown[][] = []
380
+ const service = new ContextAwareRPCService(
381
+ createServices({
382
+ deploymentService: {
383
+ invoke: async (...args: unknown[]) => {
384
+ calls.push(args)
385
+ return { remote: true }
386
+ },
387
+ },
388
+ }),
389
+ { traceId: 'trace-3', session: { userId: 'user-2' } } as never,
390
+ {}
391
+ )
392
+
393
+ const result = await service.rpcWithWire('missingRpc', { value: 2 }, {
394
+ custom: 'wire',
395
+ } as never)
396
+
397
+ assert.deepEqual(result, { remote: true })
398
+ assert.deepEqual(calls, [
399
+ ['missingRpc', { value: 2 }, { userId: 'user-2' }, 'trace-3'],
400
+ ])
401
+ })
402
+ })
403
+
404
+ describe('ContextAwareRPCService.startWorkflow', () => {
405
+ test('throws when workflowService is unavailable', async () => {
406
+ const service = new ContextAwareRPCService(
407
+ createServices(),
408
+ {} as never,
409
+ {}
410
+ )
411
+
412
+ await assert.rejects(
413
+ () => service.startWorkflow('workflowA', {}),
414
+ /WorkflowService service not available/
415
+ )
416
+ })
417
+
418
+ test('passes derived wire metadata into workflowService.startWorkflow', async () => {
419
+ const calls: unknown[][] = []
420
+ const service = new ContextAwareRPCService(
421
+ createServices({
422
+ workflowService: {
423
+ startWorkflow: async (...args: unknown[]) => {
424
+ calls.push(args)
425
+ return { runId: 'run-1' }
426
+ },
427
+ },
428
+ }),
429
+ {
430
+ wireType: 'rpc',
431
+ wireId: 'rpc-1',
432
+ pikkuUserId: 'user-3',
433
+ workflowStep: { runId: 'parent-run-1' },
434
+ } as never,
435
+ {}
436
+ )
437
+
438
+ const result = await service.startWorkflow('workflowA', { ok: true })
439
+
440
+ assert.deepEqual(result, { runId: 'run-1' })
441
+ assert.deepEqual(calls[0]?.slice(0, 3), [
442
+ 'workflowA',
443
+ { ok: true },
444
+ {
445
+ type: 'rpc',
446
+ id: 'rpc-1',
447
+ parentRunId: 'parent-run-1',
448
+ pikkuUserId: 'user-3',
449
+ },
450
+ ])
451
+ })
452
+ })
453
+
454
+ describe('ContextAwareRPCService.remote', () => {
455
+ test('throws when deploymentService is unavailable', async () => {
456
+ const service = new ContextAwareRPCService(
457
+ createServices(),
458
+ {} as never,
459
+ {}
460
+ )
461
+
462
+ await assert.rejects(
463
+ () => service.remote('remoteFunc', {}),
464
+ /No DeploymentService configured for remote RPC: remoteFunc/
465
+ )
466
+ })
467
+
468
+ test('uses deploymentService with session and trace id', async () => {
469
+ const calls: unknown[][] = []
470
+ const service = new ContextAwareRPCService(
471
+ createServices({
472
+ deploymentService: {
473
+ invoke: async (...args: unknown[]) => {
474
+ calls.push(args)
475
+ return { ok: true }
476
+ },
477
+ },
478
+ }),
479
+ {
480
+ traceId: 'trace-4',
481
+ getSession: async () => ({ userId: 'user-4' }),
482
+ } as never,
483
+ {}
484
+ )
485
+
486
+ const result = await service.remote('remoteFunc', { hi: true })
487
+
488
+ assert.deepEqual(result, { ok: true })
489
+ assert.deepEqual(calls, [
490
+ ['remoteFunc', { hi: true }, { userId: 'user-4' }, 'trace-4'],
491
+ ])
492
+ })
493
+ })
494
+
495
+ describe('rpcService.getContextRPCService', () => {
496
+ test('builds a typed rpc facade with depth and bound methods', async () => {
497
+ pikkuState(null, 'rpc', 'meta').echo = 'echoFunc'
498
+ registerFunction('echoFunc', async (_services, data) => data)
499
+
500
+ const rpc = rpcService.getContextRPCService(
501
+ createServices(),
502
+ {} as never,
503
+ false,
504
+ 7
505
+ )
506
+
507
+ assert.equal(rpc.depth, 7)
508
+ assert.equal(rpc.global, false)
509
+ assert.deepEqual(await rpc.invoke('echo', { ok: true }), { ok: true })
510
+ })
511
+ })
@@ -3,7 +3,6 @@ import type { SessionService } from '../../services/user-session-service.js'
3
3
  import type { CoreUserSession } from '../../types/core.types.js'
4
4
  import { runPikkuFunc } from '../../function/function-runner.js'
5
5
  import { pikkuState } from '../../pikku-state.js'
6
- import { ForbiddenError } from '../../errors/errors.js'
7
6
  import { PikkuError, addError } from '../../errors/error-handler.js'
8
7
  import type { PikkuRPC, ResolvedFunction } from './rpc-types.js'
9
8
  import { parseVersionedId } from '../../version.js'
@@ -54,7 +53,25 @@ export const resolveNamespace = (
54
53
  }
55
54
  }
56
55
 
57
- const getPikkuFunctionName = (rpcName: string): string => {
56
+ /**
57
+ * Resolve a bare (non-namespaced) RPC name to its pikkuFuncId, preferring
58
+ * the caller's addon package when provided. Returns the function name plus
59
+ * the package scope that resolved it (null = root), so callers can thread
60
+ * the scope into runPikkuFunc without a second lookup.
61
+ */
62
+ const resolvePikkuFunction = (
63
+ rpcName: string,
64
+ packageName: string | null = null
65
+ ): { pikkuFuncId: string; packageName: string | null } => {
66
+ // Addon-scoped calls: try the caller's package function meta first.
67
+ // (RPC meta only lives in root; addon functions are registered under their package.)
68
+ if (packageName) {
69
+ const pkgFunctions = pikkuState(packageName, 'function', 'meta')
70
+ const pkgMeta = pkgFunctions?.[rpcName]
71
+ if (pkgMeta) {
72
+ return { pikkuFuncId: pkgMeta.pikkuFuncId || rpcName, packageName }
73
+ }
74
+ }
58
75
  const rpc = pikkuState(null, 'rpc', 'meta')
59
76
  let rpcMeta = rpc[rpcName]
60
77
  if (!rpcMeta) {
@@ -63,10 +80,20 @@ const getPikkuFunctionName = (rpcName: string): string => {
63
80
  rpcMeta = rpc[baseName]
64
81
  }
65
82
  }
83
+ if (!rpcMeta) {
84
+ const rootFunctions = pikkuState(null, 'function', 'meta')
85
+ const rootFunctionMeta = rootFunctions?.[rpcName]
86
+ if (rootFunctionMeta) {
87
+ return {
88
+ pikkuFuncId: rootFunctionMeta.pikkuFuncId || rpcName,
89
+ packageName: null,
90
+ }
91
+ }
92
+ }
66
93
  if (!rpcMeta) {
67
94
  throw new RPCNotFoundError(rpcName)
68
95
  }
69
- return rpcMeta
96
+ return { pikkuFuncId: rpcMeta, packageName: null }
70
97
  }
71
98
 
72
99
  // Context-aware RPC client for use within services
@@ -77,7 +104,8 @@ export class ContextAwareRPCService {
77
104
  private options: {
78
105
  requiresAuth?: boolean
79
106
  sessionService?: SessionService<CoreUserSession>
80
- }
107
+ },
108
+ private packageName: string | null = null
81
109
  ) {}
82
110
 
83
111
  public async rpcExposed(funcName: string, data: any): Promise<any> {
@@ -90,13 +118,16 @@ export class ContextAwareRPCService {
90
118
  ]
91
119
  }
92
120
  } else {
93
- functionMeta = pikkuState(null, 'function', 'meta')[funcName]
121
+ const resolved = resolvePikkuFunction(funcName, this.packageName)
122
+ functionMeta = pikkuState(resolved.packageName, 'function', 'meta')[
123
+ resolved.pikkuFuncId
124
+ ]
94
125
  }
95
126
  if (!functionMeta) {
96
127
  throw new RPCNotFoundError(funcName)
97
128
  }
98
129
  if (!functionMeta.expose) {
99
- throw new ForbiddenError()
130
+ throw new RPCNotFoundError(funcName)
100
131
  }
101
132
  return await this.rpc(funcName, data)
102
133
  }
@@ -131,17 +162,22 @@ export class ContextAwareRPCService {
131
162
  }
132
163
  }
133
164
 
134
- // Try local function, then fall back to deployment service (remote)
165
+ // Bare name: resolve via caller's package scope first (if any), then root.
166
+ // Note: intra-addon bare calls do NOT re-apply the addon's external
167
+ // addonConfig.auth/tags — those gates are only applied on the external
168
+ // 'namespace:func' boundary via invokeAddonFunction.
135
169
  try {
170
+ const resolved = resolvePikkuFunction(funcName, this.packageName)
136
171
  return await runPikkuFunc<In, Out>(
137
172
  'rpc',
138
173
  funcName,
139
- getPikkuFunctionName(funcName),
174
+ resolved.pikkuFuncId,
140
175
  {
141
176
  auth: this.options.requiresAuth,
142
177
  singletonServices: this.services,
143
178
  data: () => data,
144
179
  wire: updatedWire,
180
+ packageName: resolved.packageName,
145
181
  }
146
182
  )
147
183
  } catch (e) {
@@ -232,17 +268,14 @@ export class ContextAwareRPCService {
232
268
  }
233
269
 
234
270
  try {
235
- return await runPikkuFunc<In, Out>(
236
- 'rpc',
237
- rpcName,
238
- getPikkuFunctionName(rpcName),
239
- {
240
- auth: this.options.requiresAuth,
241
- singletonServices: this.services,
242
- data: () => data,
243
- wire: mergedWire,
244
- }
245
- )
271
+ const resolved = resolvePikkuFunction(rpcName, this.packageName)
272
+ return await runPikkuFunc<In, Out>('rpc', rpcName, resolved.pikkuFuncId, {
273
+ auth: this.options.requiresAuth,
274
+ singletonServices: this.services,
275
+ data: () => data,
276
+ wire: mergedWire,
277
+ packageName: resolved.packageName,
278
+ })
246
279
  } catch (e) {
247
280
  if (e instanceof RPCNotFoundError && this.services.deploymentService) {
248
281
  const session =
@@ -406,14 +439,20 @@ export class PikkuRPCService<
406
439
  sessionService?: SessionService<CoreUserSession>
407
440
  }
408
441
  | undefined,
409
- depth: number = 0
442
+ depth: number = 0,
443
+ packageName: string | null = null
410
444
  ): TypedRPC {
411
445
  const options =
412
446
  typeof requiresAuthOrOptions === 'object' &&
413
447
  requiresAuthOrOptions !== null
414
448
  ? requiresAuthOrOptions
415
449
  : { requiresAuth: requiresAuthOrOptions }
416
- const serviceRPC = new ContextAwareRPCService(services, wire, options)
450
+ const serviceRPC = new ContextAwareRPCService(
451
+ services,
452
+ wire,
453
+ options,
454
+ packageName
455
+ )
417
456
  return {
418
457
  depth,
419
458
  global: false,