@pikku/core 0.12.13 → 0.12.15

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 (150) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/errors/errors.d.ts +18 -0
  3. package/dist/errors/errors.js +33 -0
  4. package/dist/function/function-runner.d.ts +1 -1
  5. package/dist/function/function-runner.js +10 -5
  6. package/dist/function/functions.types.js +1 -0
  7. package/dist/function/index.d.ts +2 -1
  8. package/dist/function/index.js +1 -0
  9. package/dist/handle-error.d.ts +2 -2
  10. package/dist/handle-error.js +8 -8
  11. package/dist/index.d.ts +1 -2
  12. package/dist/index.js +1 -2
  13. package/dist/middleware/index.d.ts +2 -0
  14. package/dist/middleware/index.js +2 -0
  15. package/dist/middleware/remote-auth.js +5 -2
  16. package/dist/permissions.d.ts +13 -1
  17. package/dist/permissions.js +51 -0
  18. package/dist/pikku-state.d.ts +0 -1
  19. package/dist/pikku-state.js +2 -4
  20. package/dist/remote.d.ts +10 -0
  21. package/dist/remote.js +32 -0
  22. package/dist/schema.js +2 -0
  23. package/dist/services/credential-service.d.ts +11 -0
  24. package/dist/services/credential-wire-service.d.ts +14 -3
  25. package/dist/services/credential-wire-service.js +49 -6
  26. package/dist/services/deployment-service.d.ts +12 -1
  27. package/dist/services/in-memory-queue-service.d.ts +7 -0
  28. package/dist/services/in-memory-queue-service.js +28 -0
  29. package/dist/services/index.d.ts +4 -1
  30. package/dist/services/index.js +2 -1
  31. package/dist/services/local-credential-service.d.ts +2 -0
  32. package/dist/services/local-credential-service.js +20 -0
  33. package/dist/services/logger-console.d.ts +26 -40
  34. package/dist/services/logger-console.js +102 -46
  35. package/dist/services/logger.d.ts +5 -0
  36. package/dist/services/meta-service.d.ts +175 -0
  37. package/dist/services/meta-service.js +310 -0
  38. package/dist/services/pikku-user-id.d.ts +3 -0
  39. package/dist/services/pikku-user-id.js +16 -0
  40. package/dist/services/typed-credential-service.d.ts +2 -0
  41. package/dist/services/typed-credential-service.js +6 -0
  42. package/dist/services/workflow-service.d.ts +6 -1
  43. package/dist/testing/service-tests.js +0 -8
  44. package/dist/types/core.types.d.ts +14 -2
  45. package/dist/types/state.types.d.ts +9 -2
  46. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +21 -0
  47. package/dist/wirings/ai-agent/ai-agent-prepare.js +90 -21
  48. package/dist/wirings/ai-agent/ai-agent-registry.js +2 -2
  49. package/dist/wirings/ai-agent/ai-agent-runner.js +24 -2
  50. package/dist/wirings/ai-agent/ai-agent-stream.d.ts +2 -1
  51. package/dist/wirings/ai-agent/ai-agent-stream.js +61 -3
  52. package/dist/wirings/ai-agent/ai-agent.types.d.ts +25 -4
  53. package/dist/wirings/ai-agent/index.d.ts +1 -1
  54. package/dist/wirings/ai-agent/index.js +1 -1
  55. package/dist/wirings/channel/channel-runner.js +3 -3
  56. package/dist/wirings/cli/cli-runner.js +4 -3
  57. package/dist/wirings/cli/command-parser.js +7 -3
  58. package/dist/wirings/http/http-runner.d.ts +1 -1
  59. package/dist/wirings/http/http-runner.js +23 -15
  60. package/dist/wirings/http/http.types.d.ts +2 -0
  61. package/dist/wirings/mcp/mcp-runner.js +5 -3
  62. package/dist/wirings/queue/queue-runner.d.ts +3 -1
  63. package/dist/wirings/queue/queue-runner.js +8 -3
  64. package/dist/wirings/queue/queue.types.d.ts +6 -0
  65. package/dist/wirings/rpc/rpc-runner.js +57 -90
  66. package/dist/wirings/scheduler/scheduler-runner.d.ts +3 -1
  67. package/dist/wirings/scheduler/scheduler-runner.js +9 -5
  68. package/dist/wirings/trigger/trigger-runner.js +4 -2
  69. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +2 -0
  70. package/dist/wirings/workflow/dsl/workflow-runner.d.ts +1 -1
  71. package/dist/wirings/workflow/dsl/workflow-runner.js +6 -9
  72. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
  73. package/dist/wirings/workflow/graph/graph-runner.js +18 -4
  74. package/dist/wirings/workflow/index.d.ts +4 -2
  75. package/dist/wirings/workflow/index.js +4 -2
  76. package/dist/wirings/workflow/pikku-workflow-service.d.ts +16 -4
  77. package/dist/wirings/workflow/pikku-workflow-service.js +218 -24
  78. package/dist/wirings/workflow/workflow-queue-workers.d.ts +40 -0
  79. package/dist/wirings/workflow/workflow-queue-workers.js +41 -0
  80. package/dist/wirings/workflow/workflow.types.d.ts +19 -1
  81. package/package.json +4 -1
  82. package/src/errors/errors.ts +44 -0
  83. package/src/function/function-runner.ts +24 -13
  84. package/src/function/functions.types.ts +1 -0
  85. package/src/function/index.ts +10 -0
  86. package/src/handle-error.test.ts +2 -2
  87. package/src/handle-error.ts +8 -8
  88. package/src/index.ts +1 -2
  89. package/src/middleware/index.ts +2 -0
  90. package/src/middleware/remote-auth.test.ts +4 -4
  91. package/src/middleware/remote-auth.ts +7 -2
  92. package/src/permissions.ts +70 -0
  93. package/src/pikku-state.test.ts +0 -26
  94. package/src/pikku-state.ts +2 -5
  95. package/src/remote.ts +46 -0
  96. package/src/schema.ts +1 -0
  97. package/src/services/credential-service.ts +13 -0
  98. package/src/services/credential-wire-service.test.ts +174 -0
  99. package/src/services/credential-wire-service.ts +52 -8
  100. package/src/services/deployment-service.ts +17 -1
  101. package/src/services/in-memory-queue-service.ts +46 -0
  102. package/src/services/index.ts +21 -1
  103. package/src/services/local-credential-service.ts +22 -0
  104. package/src/services/logger-console.ts +127 -47
  105. package/src/services/logger.ts +6 -0
  106. package/src/services/meta-service.ts +523 -0
  107. package/src/services/pikku-user-id.test.ts +62 -0
  108. package/src/services/pikku-user-id.ts +17 -0
  109. package/src/services/typed-credential-service.ts +8 -0
  110. package/src/services/workflow-service.ts +8 -0
  111. package/src/testing/service-tests.ts +0 -10
  112. package/src/types/core.types.ts +16 -2
  113. package/src/types/state.types.ts +7 -2
  114. package/src/wirings/ai-agent/ai-agent-prepare.ts +122 -41
  115. package/src/wirings/ai-agent/ai-agent-registry.ts +3 -3
  116. package/src/wirings/ai-agent/ai-agent-runner.ts +22 -3
  117. package/src/wirings/ai-agent/ai-agent-stream.ts +115 -3
  118. package/src/wirings/ai-agent/ai-agent.types.ts +27 -4
  119. package/src/wirings/ai-agent/index.ts +1 -0
  120. package/src/wirings/channel/channel-handler.ts +0 -1
  121. package/src/wirings/channel/channel-runner.ts +4 -5
  122. package/src/wirings/cli/cli-runner.test.ts +8 -7
  123. package/src/wirings/cli/cli-runner.ts +5 -4
  124. package/src/wirings/cli/command-parser.ts +8 -3
  125. package/src/wirings/http/http-runner.ts +27 -18
  126. package/src/wirings/http/http.types.ts +2 -0
  127. package/src/wirings/mcp/mcp-runner.ts +7 -10
  128. package/src/wirings/queue/queue-runner.test.ts +5 -11
  129. package/src/wirings/queue/queue-runner.ts +12 -4
  130. package/src/wirings/queue/queue.types.ts +6 -0
  131. package/src/wirings/rpc/rpc-runner.ts +91 -118
  132. package/src/wirings/scheduler/scheduler-runner.test.ts +5 -11
  133. package/src/wirings/scheduler/scheduler-runner.ts +13 -5
  134. package/src/wirings/trigger/trigger-runner.test.ts +10 -11
  135. package/src/wirings/trigger/trigger-runner.ts +6 -4
  136. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +2 -0
  137. package/src/wirings/workflow/dsl/workflow-runner.ts +11 -10
  138. package/src/wirings/workflow/graph/graph-runner.ts +19 -4
  139. package/src/wirings/workflow/index.ts +17 -6
  140. package/src/wirings/workflow/pikku-workflow-service.ts +286 -24
  141. package/src/wirings/workflow/workflow-queue-workers.ts +85 -0
  142. package/src/wirings/workflow/workflow.types.ts +18 -1
  143. package/tsconfig.tsbuildinfo +1 -1
  144. package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +0 -6
  145. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +0 -468
  146. package/dist/wirings/workflow/workflow-helpers.d.ts +0 -36
  147. package/dist/wirings/workflow/workflow-helpers.js +0 -38
  148. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +0 -610
  149. package/src/wirings/workflow/workflow-helpers.test.ts +0 -129
  150. package/src/wirings/workflow/workflow-helpers.ts +0 -79
@@ -369,15 +369,16 @@ describe('CLI Runner', () => {
369
369
  assert.strictEqual(programs['my-cli'].middleware[0], middleware)
370
370
  })
371
371
 
372
- test('should throw error when CLI metadata not found', () => {
372
+ test('should skip when CLI metadata not found', () => {
373
373
  pikkuState(null, 'cli', 'meta', { programs: {}, renderers: {} })
374
374
 
375
- assert.throws(() => {
376
- wireCLI({
377
- program: 'nonexistent',
378
- commands: {},
379
- })
380
- }, /CLI metadata not found for program 'nonexistent'/)
375
+ wireCLI({
376
+ program: 'nonexistent',
377
+ commands: {},
378
+ })
379
+
380
+ const programs = pikkuState(null, 'cli', 'programs')
381
+ assert.strictEqual(programs['nonexistent'], undefined)
381
382
  })
382
383
  })
383
384
 
@@ -1,4 +1,4 @@
1
- import { NotFoundError, PikkuMissingMetaError } from '../../errors/errors.js'
1
+ import { NotFoundError } from '../../errors/errors.js'
2
2
  import { addFunction, runPikkuFunc } from '../../function/function-runner.js'
3
3
  import { pikkuState } from '../../pikku-state.js'
4
4
  import type {
@@ -66,9 +66,10 @@ export const wireCLI = <
66
66
  const cliMeta = pikkuState(null, 'cli', 'meta') || {}
67
67
 
68
68
  if (!cliMeta.programs?.[cli.program]) {
69
- throw new PikkuMissingMetaError(
70
- `CLI metadata not found for program '${cli.program}'`
69
+ console.warn(
70
+ `[pikku] Skipping CLI program '${cli.program}' — metadata not found. Consider moving this wiring to its own file.`
71
71
  )
72
+ return
72
73
  }
73
74
 
74
75
  // Get existing programs state and add this program
@@ -191,7 +192,7 @@ function registerCLICommands(
191
192
  }
192
193
  }
193
194
 
194
- addFunction(funcName, unwrapFunc(command))
195
+ addFunction(funcName, unwrapFunc(command), currentMeta?.packageName)
195
196
 
196
197
  // Register renderer if provided
197
198
  if (typeof command === 'object' && command.render) {
@@ -6,6 +6,11 @@ import type {
6
6
  CLIOption,
7
7
  } from './cli.types.js'
8
8
 
9
+ /** Convert kebab-case to camelCase: "from-plan" → "fromPlan" */
10
+ function toCamelCase(str: string): string {
11
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
12
+ }
13
+
9
14
  /**
10
15
  * Result of parsing CLI arguments
11
16
  */
@@ -117,11 +122,11 @@ export function parseCLIArguments(
117
122
  const arg = args[currentIndex]
118
123
 
119
124
  if (arg.startsWith('--')) {
120
- // Long option
125
+ // Long option (--from-plan → fromPlan)
121
126
  const equalIndex = arg.indexOf('=')
122
127
  if (equalIndex > 0) {
123
128
  // --option=value format
124
- const key = arg.slice(2, equalIndex)
129
+ const key = toCamelCase(arg.slice(2, equalIndex))
125
130
  const optionDef = availableOptions[key]
126
131
 
127
132
  // Unknown options are allowed for forward compatibility
@@ -129,7 +134,7 @@ export function parseCLIArguments(
129
134
  optionArgs[key] = parseOptionValue(value, optionDef)
130
135
  } else {
131
136
  // --option value format
132
- const key = arg.slice(2)
137
+ const key = toCamelCase(arg.slice(2))
133
138
  const optionDef = availableOptions[key]
134
139
 
135
140
  // Unknown options are allowed for forward compatibility
@@ -21,7 +21,7 @@ import type {
21
21
  PikkuWire,
22
22
  PikkuWiringTypes,
23
23
  } from '../../types/core.types.js'
24
- import { NotFoundError, PikkuMissingMetaError } from '../../errors/errors.js'
24
+ import { NotFoundError } from '../../errors/errors.js'
25
25
  import {
26
26
  closeWireServices,
27
27
  createWeakUID,
@@ -33,10 +33,6 @@ import {
33
33
  } from '../../pikku-state.js'
34
34
  import { PikkuSessionService } from '../../services/user-session-service.js'
35
35
  import { getErrorResponse } from '../../errors/error-handler.js'
36
- import {
37
- PikkuCredentialWireService,
38
- createMiddlewareCredentialWireProps,
39
- } from '../../services/credential-wire-service.js'
40
36
  import { handleHTTPError } from '../../handle-error.js'
41
37
  import { pikkuState } from '../../pikku-state.js'
42
38
  import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js'
@@ -184,9 +180,14 @@ export const wireHTTP = <
184
180
  const httpMeta = pikkuState(null, 'http', 'meta')
185
181
  const routeMeta = httpMeta[httpWiring.method][httpWiring.route]
186
182
  if (!routeMeta) {
187
- throw new PikkuMissingMetaError(
188
- `Missing generated metadata for HTTP route '${httpWiring.method.toUpperCase()} ${httpWiring.route}'`
183
+ // In deploy units with filtered metadata, wiring files may include
184
+ // routes for functions not in this unit. This happens when multiple
185
+ // wirings share a file — split them into separate files for better
186
+ // tree-shaking.
187
+ console.warn(
188
+ `[pikku] Skipping HTTP route '${httpWiring.method.toUpperCase()} ${httpWiring.route}' — metadata not found. Consider moving this wiring to its own file.`
189
189
  )
190
+ return
190
191
  }
191
192
  if (httpWiring.func) {
192
193
  addFunction(routeMeta.pikkuFuncId, httpWiring.func)
@@ -303,7 +304,6 @@ const executeRoute = async (
303
304
  }
304
305
  ) => {
305
306
  const userSession = new PikkuSessionService<CoreUserSession>()
306
- const credentialWire = new PikkuCredentialWireService()
307
307
  const { params, route, meta } = matchedRoute
308
308
  const { singletonServices, createWireServices, skipUserSession, requestId } =
309
309
  services
@@ -376,13 +376,13 @@ const executeRoute = async (
376
376
  }
377
377
 
378
378
  const wire: PikkuWire = {
379
+ traceId: requestId,
379
380
  http,
380
381
  channel,
381
382
  session: userSession.get() as CoreUserSession | undefined,
382
383
  setSession: (s: any) => userSession.setInitial(s),
383
384
  getSession: () => userSession.get(),
384
385
  hasSessionChanged: () => userSession.sessionChanged,
385
- ...createMiddlewareCredentialWireProps(credentialWire),
386
386
  }
387
387
 
388
388
  result = await runPikkuFunc(
@@ -402,7 +402,6 @@ const executeRoute = async (
402
402
  tags: route.tags,
403
403
  wire,
404
404
  sessionService: userSession,
405
- credentialWireService: credentialWire,
406
405
  packageName: meta.packageName,
407
406
  }
408
407
  )
@@ -496,6 +495,7 @@ export const fetchData = async <In, Out>(
496
495
  bubbleErrors = false,
497
496
  exposeErrors = false,
498
497
  generateRequestId,
498
+ traceId: externalTraceId,
499
499
  }: RunHTTPWiringOptions = {}
500
500
  ): Promise<Out | void> => {
501
501
  const singletonServices = getSingletonServices()
@@ -506,11 +506,20 @@ export const fetchData = async <In, Out>(
506
506
  // Combine the request and response into one wire object
507
507
  const pikkuRequest =
508
508
  request instanceof Request ? new PikkuFetchHTTPRequest(request) : request
509
- let requestId: string | null = null
510
- try {
511
- requestId = pikkuRequest.header('x-request-id')
512
- } catch {}
509
+
510
+ // Resolve traceId: external (e.g. CF-Ray) > x-request-id header > generated
511
+ let requestId: string | null = externalTraceId ?? null
512
+ if (!requestId) {
513
+ try {
514
+ requestId = pikkuRequest.header('x-request-id')
515
+ } catch {}
516
+ }
513
517
  requestId = requestId || generateRequestId?.() || createWeakUID()
518
+
519
+ // Scoped logger for HTTP runner internal logging (error handling, route matching)
520
+ // Functions/middleware receive singletonServices.logger directly for compatibility
521
+ const scopedLogger =
522
+ singletonServices.logger.scope?.(requestId) ?? singletonServices.logger
514
523
  const http = createHTTPWire(pikkuRequest, response)
515
524
  const apiType = http!.request!.method()
516
525
  const apiRoute = http!.request!.path()
@@ -533,7 +542,7 @@ export const fetchData = async <In, Out>(
533
542
  response.status(204).json(undefined as any)
534
543
  return
535
544
  }
536
- singletonServices.logger.info({
545
+ scopedLogger.info({
537
546
  message: 'Route not found',
538
547
  apiRoute,
539
548
  apiType,
@@ -558,7 +567,7 @@ export const fetchData = async <In, Out>(
558
567
  } catch (e: any) {
559
568
  if (matchedRoute?.route.sse) {
560
569
  // For SSE routes, send error through the stream since the response is already in stream mode
561
- singletonServices.logger.error(e instanceof Error ? e.message : e)
570
+ scopedLogger.error(e instanceof Error ? e.message : e)
562
571
  try {
563
572
  const errorResponse = getErrorResponse(e)
564
573
  response.arrayBuffer(
@@ -569,7 +578,7 @@ export const fetchData = async <In, Out>(
569
578
  )
570
579
  response.arrayBuffer(JSON.stringify({ type: 'done' }))
571
580
  } catch (streamErr: any) {
572
- singletonServices.logger.error(
581
+ scopedLogger.error(
573
582
  `SSE error while sending error payload: ${streamErr instanceof Error ? streamErr.message : String(streamErr)}`
574
583
  )
575
584
  }
@@ -579,7 +588,7 @@ export const fetchData = async <In, Out>(
579
588
  e,
580
589
  http,
581
590
  requestId,
582
- singletonServices.logger,
591
+ scopedLogger,
583
592
  logWarningsForStatusCodes,
584
593
  respondWith404,
585
594
  bubbleErrors,
@@ -37,6 +37,8 @@ export type RunHTTPWiringOptions = Partial<{
37
37
  bubbleErrors: boolean
38
38
  exposeErrors: boolean
39
39
  generateRequestId: () => string
40
+ /** Pre-resolved trace ID (e.g. CF-Ray). Falls back to x-request-id header or generated ID. */
41
+ traceId: string
40
42
  }>
41
43
 
42
44
  /**
@@ -20,16 +20,11 @@ import {
20
20
  } from '../../pikku-state.js'
21
21
  import { addFunction, runPikkuFunc } from '../../function/function-runner.js'
22
22
  import { resolveNamespace } from '../rpc/rpc-runner.js'
23
- import {
24
- BadRequestError,
25
- NotFoundError,
26
- PikkuMissingMetaError,
27
- } from '../../errors/errors.js'
23
+ import { BadRequestError, NotFoundError } from '../../errors/errors.js'
28
24
  import {
29
25
  PikkuSessionService,
30
26
  createMiddlewareSessionWireProps,
31
27
  } from '../../services/user-session-service.js'
32
-
33
28
  export class MCPError extends Error {
34
29
  constructor(public readonly error: JsonRpcErrorResponse) {
35
30
  super(error?.message || 'MCP Error')
@@ -58,9 +53,10 @@ export const wireMCPResource = <
58
53
  const resourcesMeta = pikkuState(null, 'mcp', 'resourcesMeta')
59
54
  const mcpResourceMeta = resourcesMeta[mcpResource.uri]
60
55
  if (!mcpResourceMeta) {
61
- throw new PikkuMissingMetaError(
62
- `Missing generated metadata for MCP resource '${mcpResource.uri}'`
56
+ console.warn(
57
+ `[pikku] Skipping MCP resource '${mcpResource.uri}' — metadata not found. Consider moving this wiring to its own file.`
63
58
  )
59
+ return
64
60
  }
65
61
  addFunction(mcpResourceMeta.pikkuFuncId, mcpResource.func as any)
66
62
  const resources = pikkuState(null, 'mcp', 'resources')
@@ -80,9 +76,10 @@ export const wireMCPPrompt = <
80
76
  const promptsMeta = pikkuState(null, 'mcp', 'promptsMeta')
81
77
  const mcpPromptMeta = promptsMeta[mcpPrompt.name]
82
78
  if (!mcpPromptMeta) {
83
- throw new PikkuMissingMetaError(
84
- `Missing generated metadata for MCP prompt '${mcpPrompt.name}'`
79
+ console.warn(
80
+ `[pikku] Skipping MCP prompt '${mcpPrompt.name}' — metadata not found. Consider moving this wiring to its own file.`
85
81
  )
82
+ return
86
83
  }
87
84
  addFunction(mcpPromptMeta.pikkuFuncId, mcpPrompt.func as any)
88
85
  const prompts = pikkuState(null, 'mcp', 'prompts')
@@ -72,7 +72,7 @@ describe('wireQueueWorker', () => {
72
72
  assert.equal(registrations.get('test-queue'), mockWorker)
73
73
  })
74
74
 
75
- test('should throw error when processor metadata not found', () => {
75
+ test('should skip when queue worker metadata not found', () => {
76
76
  const mockWorker: CoreQueueWorker = {
77
77
  name: 'missing-meta-queue',
78
78
  func: {
@@ -81,16 +81,10 @@ describe('wireQueueWorker', () => {
81
81
  },
82
82
  }
83
83
 
84
- assert.throws(
85
- () => wireQueueWorker(mockWorker),
86
- (error: any) => {
87
- assert(
88
- error.message.includes('Missing generated metadata for queue worker')
89
- )
90
- assert(error.message.includes('missing-meta-queue'))
91
- return true
92
- }
93
- )
84
+ wireQueueWorker(mockWorker)
85
+
86
+ const registrations = pikkuState(null, 'queue', 'registrations')
87
+ assert.equal(registrations.has('missing-meta-queue'), false)
94
88
  })
95
89
 
96
90
  test('should wire worker with middleware and tags', () => {
@@ -12,7 +12,6 @@ import {
12
12
  pikkuState,
13
13
  } from '../../pikku-state.js'
14
14
  import { addFunction, runPikkuFunc } from '../../function/function-runner.js'
15
-
16
15
  /**
17
16
  * Error class for queue processor not found
18
17
  */
@@ -59,9 +58,10 @@ export const wireQueueWorker = <
59
58
  const meta = pikkuState(null, 'queue', 'meta')
60
59
  const processorMeta = meta[queueWorker.name]
61
60
  if (!processorMeta) {
62
- throw new PikkuMissingMetaError(
63
- `Missing generated metadata for queue worker '${queueWorker.name}'`
61
+ console.warn(
62
+ `[pikku] Skipping queue worker '${queueWorker.name}' — metadata not found. Consider moving this wiring to its own file.`
64
63
  )
64
+ return
65
65
  }
66
66
 
67
67
  // Register the function with pikku
@@ -105,13 +105,19 @@ export async function removeQueueWorker(name: string): Promise<void> {
105
105
  export async function runQueueJob({
106
106
  job,
107
107
  updateProgress,
108
+ traceId,
108
109
  }: {
109
110
  job: QueueJob
110
111
  updateProgress?: (progress: number | string | object) => Promise<void>
112
+ /** Pre-resolved trace ID (e.g. from queue message metadata) */
113
+ traceId?: string
111
114
  }): Promise<void> {
112
115
  const singletonServices = getSingletonServices()
113
116
  const createWireServices = getCreateWireServices()
114
- const logger = singletonServices.logger
117
+ const resolvedTraceId = traceId ?? `q-${job.id}`
118
+ const logger =
119
+ singletonServices.logger.scope?.(resolvedTraceId) ??
120
+ singletonServices.logger
115
121
 
116
122
  const meta = pikkuState(null, 'queue', 'meta')
117
123
  const processorMeta = meta[job.queueName]
@@ -132,6 +138,7 @@ export async function runQueueJob({
132
138
  const queue: PikkuQueue = {
133
139
  queueName: job.queueName,
134
140
  jobId: job.id,
141
+ pikkuUserId: job.pikkuUserId,
135
142
  updateProgress:
136
143
  updateProgress ||
137
144
  (async (progress: number | string | object) => {
@@ -150,6 +157,7 @@ export async function runQueueJob({
150
157
  logger.info(`Processing job ${job.id} in queue ${job.queueName}`)
151
158
 
152
159
  const wire: PikkuWire = {
160
+ traceId: resolvedTraceId,
153
161
  queue,
154
162
  }
155
163
 
@@ -96,6 +96,8 @@ export interface QueueJob<T = any, R = any> {
96
96
  result?: R
97
97
  waitForCompletion?: (ttl?: number) => Promise<R>
98
98
  metadata?: () => Promise<QueueJobMetadata> | QueueJobMetadata
99
+ /** Pikku user ID propagated from the job producer */
100
+ pikkuUserId?: string
99
101
  }
100
102
 
101
103
  /**
@@ -109,6 +111,8 @@ export interface JobOptions {
109
111
  removeOnComplete?: number
110
112
  removeOnFail?: number
111
113
  jobId?: string
114
+ /** Pikku user ID to propagate to the queue worker for credential resolution */
115
+ pikkuUserId?: string
112
116
  }
113
117
 
114
118
  /**
@@ -180,6 +184,8 @@ export interface PikkuQueue {
180
184
  queueName: string
181
185
  /** The current job ID */
182
186
  jobId: string
187
+ /** Pikku user ID propagated from the job producer for credential resolution */
188
+ pikkuUserId?: string
183
189
  /** Update job progress (0-100 or custom value) */
184
190
  updateProgress: (progress: number | string | object) => Promise<void>
185
191
  /** Fail the current job with optional reason */
@@ -7,7 +7,6 @@ import { ForbiddenError } from '../../errors/errors.js'
7
7
  import { PikkuError, addError } from '../../errors/error-handler.js'
8
8
  import type { PikkuRPC, ResolvedFunction } from './rpc-types.js'
9
9
  import { parseVersionedId } from '../../version.js'
10
- import { encryptJSON } from '../../crypto-utils.js'
11
10
 
12
11
  export class RPCNotFoundError extends PikkuError {
13
12
  public readonly rpcName: string
@@ -118,23 +117,51 @@ export class ContextAwareRPCService {
118
117
  : undefined,
119
118
  }
120
119
 
121
- // Check if it's a namespaced function call (e.g., 'stripe:createCharge')
120
+ // Check addon namespace first (e.g. 'stripe:createCharge')
122
121
  if (funcName.includes(':')) {
123
- return this.invokeAddonFunction<In, Out>(funcName, data, updatedWire)
122
+ try {
123
+ return await this.invokeAddonFunction<In, Out>(
124
+ funcName,
125
+ data,
126
+ updatedWire
127
+ )
128
+ } catch (addonErr) {
129
+ if (!(addonErr instanceof RPCNotFoundError)) throw addonErr
130
+ // Not an addon — fall through to local lookup
131
+ }
124
132
  }
125
133
 
126
- // Main package function
127
- return runPikkuFunc<In, Out>(
128
- 'rpc',
129
- funcName,
130
- getPikkuFunctionName(funcName),
131
- {
132
- auth: this.options.requiresAuth,
133
- singletonServices: this.services,
134
- data: () => data,
135
- wire: updatedWire,
134
+ // Try local function, then fall back to deployment service (remote)
135
+ try {
136
+ return await runPikkuFunc<In, Out>(
137
+ 'rpc',
138
+ funcName,
139
+ getPikkuFunctionName(funcName),
140
+ {
141
+ auth: this.options.requiresAuth,
142
+ singletonServices: this.services,
143
+ data: () => data,
144
+ wire: updatedWire,
145
+ }
146
+ )
147
+ } catch (e) {
148
+ if (e instanceof RPCNotFoundError) {
149
+ // Fall back to deployment service (e.g. CF service binding, Lambda Invoke)
150
+ if (this.services.deploymentService) {
151
+ const session =
152
+ this.wire.getSession && typeof this.wire.getSession === 'function'
153
+ ? await this.wire.getSession()
154
+ : (this.wire as any).session
155
+ return this.services.deploymentService.invoke(
156
+ funcName,
157
+ data,
158
+ session,
159
+ this.wire.traceId
160
+ ) as Promise<Out>
161
+ }
136
162
  }
137
- )
163
+ throw e
164
+ }
138
165
  }
139
166
 
140
167
  /**
@@ -151,10 +178,7 @@ export class ContextAwareRPCService {
151
178
  // Resolve namespace to package name
152
179
  const resolved = resolveNamespace(namespacedFunction)
153
180
  if (!resolved) {
154
- throw new Error(
155
- `Unknown namespace in function reference: ${namespacedFunction}. ` +
156
- `Make sure the package is registered in addons config.`
157
- )
181
+ throw new RPCNotFoundError(namespacedFunction)
158
182
  }
159
183
 
160
184
  // Get the function meta from the addon package
@@ -162,10 +186,7 @@ export class ContextAwareRPCService {
162
186
  const addonFunctionMeta = pikkuState(resolved.package, 'function', 'meta')
163
187
  const funcMeta = addonFunctionMeta[resolved.function]
164
188
  if (!funcMeta) {
165
- throw new Error(
166
- `Function '${resolved.function}' not found in package '${resolved.package}'. ` +
167
- `Available functions: ${Object.keys(addonFunctionMeta).join(', ') || '(none)'}`
168
- )
189
+ throw new RPCNotFoundError(namespacedFunction)
169
190
  }
170
191
  const funcName = funcMeta.pikkuFuncId || resolved.function
171
192
 
@@ -210,17 +231,33 @@ export class ContextAwareRPCService {
210
231
  return this.invokeAddonFunction<In, Out>(rpcName, data, mergedWire)
211
232
  }
212
233
 
213
- return runPikkuFunc<In, Out>(
214
- 'rpc',
215
- rpcName,
216
- getPikkuFunctionName(rpcName),
217
- {
218
- auth: this.options.requiresAuth,
219
- singletonServices: this.services,
220
- data: () => data,
221
- wire: mergedWire,
234
+ 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
+ )
246
+ } catch (e) {
247
+ if (e instanceof RPCNotFoundError && this.services.deploymentService) {
248
+ const session =
249
+ this.wire.getSession && typeof this.wire.getSession === 'function'
250
+ ? await this.wire.getSession()
251
+ : (this.wire as any).session
252
+ return this.services.deploymentService.invoke(
253
+ rpcName,
254
+ data,
255
+ session,
256
+ this.wire.traceId
257
+ ) as Promise<Out>
222
258
  }
223
- )
259
+ throw e
260
+ }
224
261
  }
225
262
 
226
263
  public async startWorkflow<In = any>(
@@ -239,6 +276,7 @@ export class ContextAwareRPCService {
239
276
  type: this.wire.wireType ?? 'unknown',
240
277
  id: this.wire.wireId,
241
278
  ...(parentRunId ? { parentRunId } : {}),
279
+ ...(this.wire.pikkuUserId ? { pikkuUserId: this.wire.pikkuUserId } : {}),
242
280
  }
243
281
  return this.services.workflowService.startWorkflow(
244
282
  workflowName,
@@ -306,9 +344,14 @@ export class ContextAwareRPCService {
306
344
  approvals: { toolCallId: string; approved: boolean }[],
307
345
  expectedAgentName?: string
308
346
  ) => {
309
- const result = await resumeAIAgentSync(runId, approvals, {
310
- sessionService: this.options.sessionService,
311
- }, expectedAgentName)
347
+ const result = await resumeAIAgentSync(
348
+ runId,
349
+ approvals,
350
+ {
351
+ sessionService: this.options.sessionService,
352
+ },
353
+ expectedAgentName
354
+ )
312
355
  return {
313
356
  runId: result.runId,
314
357
  result: result.object ?? result.text,
@@ -326,94 +369,24 @@ export class ContextAwareRPCService {
326
369
  funcName: string,
327
370
  data: In
328
371
  ): Promise<Out> {
329
- let endpoint: string | undefined
330
- const headers: Record<string, string> = {
331
- 'Content-Type': 'application/json',
332
- }
333
-
334
- const colonIndex = funcName.indexOf(':')
335
- if (colonIndex !== -1) {
336
- const namespace = funcName.substring(0, colonIndex)
337
- const addons = pikkuState(null, 'addons', 'packages')
338
- const pkgConfig = addons.get(namespace)
339
- endpoint = pkgConfig?.rpcEndpoint
340
- }
341
-
342
- if (!endpoint && this.services.deploymentService) {
343
- const deployments =
344
- await this.services.deploymentService.findFunction(funcName)
345
- if (deployments.length > 0) {
346
- endpoint = deployments[0].endpoint
347
- }
348
- }
349
-
350
- if (!endpoint) {
372
+ if (!this.services.deploymentService) {
351
373
  throw new Error(
352
- `No endpoint configured for remote RPC: ${funcName}. ` +
353
- `Configure rpcEndpoint in addons config or set up a DeploymentService.`
374
+ `No DeploymentService configured for remote RPC: ${funcName}. ` +
375
+ `Set up a DeploymentService to enable remote function calls.`
354
376
  )
355
377
  }
356
378
 
357
- let secret: string | undefined
358
- if (this.services.secrets) {
359
- try {
360
- secret = await this.services.secrets.getSecret('PIKKU_REMOTE_SECRET')
361
- } catch {}
362
- }
363
- if (secret) {
364
- if (!this.services.jwt) {
365
- throw new Error(
366
- 'PIKKU_REMOTE_SECRET is set but JWT service is not available'
367
- )
368
- }
369
- const session =
370
- this.wire.getSession && typeof this.wire.getSession === 'function'
371
- ? await this.wire.getSession()
372
- : (this.wire as any).session
373
- const sessionEnc = session
374
- ? await encryptJSON(secret, { session })
375
- : undefined
376
- const token = await this.services.jwt.encode(
377
- { value: 5, unit: 'minute' },
378
- {
379
- aud: 'pikku-remote',
380
- fn: funcName,
381
- iat: Date.now(),
382
- session: sessionEnc,
383
- }
384
- )
385
- headers.Authorization = `Bearer ${token}`
386
- }
379
+ const session =
380
+ this.wire.getSession && typeof this.wire.getSession === 'function'
381
+ ? await this.wire.getSession()
382
+ : (this.wire as any).session
387
383
 
388
- const url = `${endpoint}/rpc/${encodeURIComponent(funcName)}`
389
- const timeoutMs = 30_000
390
- const response = await fetch(url, {
391
- method: 'POST',
392
- headers,
393
- body: JSON.stringify({ data }),
394
- signal: AbortSignal.timeout(timeoutMs),
395
- })
396
-
397
- if (!response.ok) {
398
- let errorBody: string
399
- try {
400
- errorBody = JSON.stringify(await response.json())
401
- } catch {
402
- errorBody = await response.text()
403
- }
404
- throw new Error(
405
- `Remote RPC call failed: ${response.status} ${response.statusText}. ${errorBody}`
406
- )
407
- }
408
-
409
- try {
410
- return (await response.json()) as Out
411
- } catch {
412
- const text = await response.text()
413
- throw new Error(
414
- `Remote RPC call returned non-JSON response: ${response.status} ${response.statusText}. ${text}`
415
- )
416
- }
384
+ return this.services.deploymentService.invoke(
385
+ funcName,
386
+ data,
387
+ session,
388
+ this.wire.traceId
389
+ ) as Promise<Out>
417
390
  }
418
391
  }
419
392