@pikku/core 0.12.63 → 0.12.64

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 (198) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/dist/dev/hot-reload.js +13 -55
  3. package/dist/dev/module-runner.d.ts +14 -0
  4. package/dist/dev/module-runner.js +59 -0
  5. package/dist/dev/reload-meta.d.ts +13 -0
  6. package/dist/dev/reload-meta.js +22 -0
  7. package/dist/errors/errors.d.ts +12 -0
  8. package/dist/errors/errors.js +19 -0
  9. package/dist/function/function-runner.d.ts +5 -5
  10. package/dist/function/function-runner.js +39 -111
  11. package/dist/function/functions.types.d.ts +17 -7
  12. package/dist/index.d.ts +5 -1
  13. package/dist/index.js +3 -1
  14. package/dist/middleware/auth-apikey.d.ts +1 -0
  15. package/dist/middleware/auth-bearer.d.ts +1 -0
  16. package/dist/middleware/auth-cookie.d.ts +1 -0
  17. package/dist/middleware/cors.d.ts +1 -0
  18. package/dist/middleware/index.d.ts +1 -1
  19. package/dist/middleware/index.js +1 -1
  20. package/dist/middleware/remote-auth.d.ts +1 -0
  21. package/dist/middleware/telemetry.d.ts +2 -0
  22. package/dist/middleware/timeout.d.ts +1 -0
  23. package/dist/permissions.d.ts +32 -25
  24. package/dist/permissions.js +104 -185
  25. package/dist/pikku-state.js +0 -2
  26. package/dist/scopes.d.ts +17 -0
  27. package/dist/scopes.js +59 -0
  28. package/dist/services/ai-embedding-service.d.ts +31 -0
  29. package/dist/services/ai-embedding-service.js +1 -0
  30. package/dist/services/credential-wire-service.d.ts +3 -1
  31. package/dist/services/credential-wire-service.js +10 -4
  32. package/dist/services/index.d.ts +4 -0
  33. package/dist/services/index.js +2 -0
  34. package/dist/services/meta-service.d.ts +15 -2
  35. package/dist/services/meta-service.js +51 -1
  36. package/dist/services/queue-webhook-service.d.ts +55 -0
  37. package/dist/services/queue-webhook-service.js +136 -0
  38. package/dist/services/scope-service.d.ts +67 -0
  39. package/dist/services/scope-service.js +1 -0
  40. package/dist/services/typed-secret-service.d.ts +8 -0
  41. package/dist/services/typed-secret-service.js +39 -4
  42. package/dist/services/webhook-service.d.ts +140 -0
  43. package/dist/services/webhook-service.js +44 -0
  44. package/dist/services/workflow-service.d.ts +7 -0
  45. package/dist/types/core.types.d.ts +34 -6
  46. package/dist/types/state.types.d.ts +20 -2
  47. package/dist/utils/hmac.d.ts +16 -0
  48. package/dist/utils/hmac.js +26 -0
  49. package/dist/utils/safe-fetch.d.ts +51 -0
  50. package/dist/utils/safe-fetch.js +192 -0
  51. package/dist/wirings/ai-agent/ai-agent-agui.d.ts +84 -0
  52. package/dist/wirings/ai-agent/ai-agent-agui.js +305 -0
  53. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +41 -1
  54. package/dist/wirings/ai-agent/ai-agent-prepare.js +146 -6
  55. package/dist/wirings/ai-agent/ai-agent-runner.js +6 -1
  56. package/dist/wirings/ai-agent/ai-agent-stream.js +61 -10
  57. package/dist/wirings/ai-agent/ai-agent.types.d.ts +27 -1
  58. package/dist/wirings/ai-agent/index.d.ts +1 -0
  59. package/dist/wirings/ai-agent/index.js +1 -0
  60. package/dist/wirings/ai-agent/voice-input.d.ts +1 -0
  61. package/dist/wirings/ai-agent/voice-input.js +2 -38
  62. package/dist/wirings/ai-agent/voice-output.d.ts +1 -0
  63. package/dist/wirings/channel/channel-common.js +0 -1
  64. package/dist/wirings/channel/channel-handler.js +1 -4
  65. package/dist/wirings/channel/channel-middleware-runner.d.ts +12 -0
  66. package/dist/wirings/channel/channel-middleware-runner.js +34 -19
  67. package/dist/wirings/channel/channel.types.d.ts +2 -6
  68. package/dist/wirings/cli/cli-runner.js +0 -2
  69. package/dist/wirings/cli/cli.types.d.ts +1 -2
  70. package/dist/wirings/http/http-routes.js +0 -3
  71. package/dist/wirings/http/http-runner.d.ts +9 -29
  72. package/dist/wirings/http/http-runner.js +9 -34
  73. package/dist/wirings/http/http.types.d.ts +1 -9
  74. package/dist/wirings/mcp/mcp-runner.js +0 -2
  75. package/dist/wirings/mcp/mcp.types.d.ts +5 -11
  76. package/dist/wirings/oauth2/index.d.ts +0 -3
  77. package/dist/wirings/oauth2/index.js +1 -2
  78. package/dist/wirings/rpc/addon-runner.d.ts +28 -0
  79. package/dist/wirings/rpc/addon-runner.js +173 -0
  80. package/dist/wirings/rpc/index.d.ts +5 -1
  81. package/dist/wirings/rpc/index.js +3 -1
  82. package/dist/wirings/rpc/remote-addon-auth.d.ts +26 -0
  83. package/dist/wirings/rpc/remote-addon-auth.js +43 -0
  84. package/dist/wirings/rpc/rpc-runner.d.ts +19 -0
  85. package/dist/wirings/rpc/rpc-runner.js +109 -3
  86. package/dist/wirings/rpc/rpc-types.d.ts +15 -1
  87. package/dist/wirings/rpc/wire-addon.js +9 -0
  88. package/dist/wirings/rpc/wire-remote-addon.d.ts +47 -0
  89. package/dist/wirings/rpc/wire-remote-addon.js +19 -0
  90. package/dist/wirings/scope/index.d.ts +3 -0
  91. package/dist/wirings/scope/index.js +2 -0
  92. package/dist/wirings/scope/scope.types.d.ts +39 -0
  93. package/dist/wirings/scope/scope.types.js +1 -0
  94. package/dist/wirings/scope/validate-scope-definitions.d.ts +16 -0
  95. package/dist/wirings/scope/validate-scope-definitions.js +76 -0
  96. package/dist/wirings/scope/wire-scope.d.ts +33 -0
  97. package/dist/wirings/scope/wire-scope.js +32 -0
  98. package/dist/wirings/workflow/dsl/index.d.ts +1 -1
  99. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +53 -1
  100. package/dist/wirings/workflow/graph/graph-node.d.ts +2 -0
  101. package/dist/wirings/workflow/graph/graph-runner.js +3 -0
  102. package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +6 -0
  103. package/dist/wirings/workflow/graph/wire-workflow-graph.js +1 -0
  104. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +5 -0
  105. package/dist/wirings/workflow/index.d.ts +2 -2
  106. package/dist/wirings/workflow/index.js +1 -1
  107. package/dist/wirings/workflow/pikku-workflow-service.d.ts +51 -1
  108. package/dist/wirings/workflow/pikku-workflow-service.js +171 -0
  109. package/dist/wirings/workflow/workflow.types.d.ts +1 -1
  110. package/package.json +5 -1
  111. package/run-tests.sh +1 -0
  112. package/src/dev/hot-reload.ts +13 -68
  113. package/src/dev/module-runner.test.ts +169 -0
  114. package/src/dev/module-runner.ts +103 -0
  115. package/src/dev/reload-meta.test.ts +31 -2
  116. package/src/dev/reload-meta.ts +26 -0
  117. package/src/errors/errors.ts +24 -0
  118. package/src/function/function-runner.test.ts +276 -142
  119. package/src/function/function-runner.ts +52 -145
  120. package/src/function/functions.types.ts +28 -38
  121. package/src/index.ts +8 -6
  122. package/src/middleware/index.ts +1 -5
  123. package/src/permissions.test.ts +153 -372
  124. package/src/permissions.ts +139 -259
  125. package/src/pikku-state.ts +0 -2
  126. package/src/scopes.test.ts +167 -0
  127. package/src/scopes.ts +69 -0
  128. package/src/services/ai-embedding-service.ts +31 -0
  129. package/src/services/credential-wire-service.ts +10 -4
  130. package/src/services/index.ts +20 -0
  131. package/src/services/meta-service.ts +68 -3
  132. package/src/services/queue-webhook-service.test.ts +408 -0
  133. package/src/services/queue-webhook-service.ts +182 -0
  134. package/src/services/scope-service.ts +75 -0
  135. package/src/services/typed-secret-service.test.ts +35 -0
  136. package/src/services/typed-secret-service.ts +39 -4
  137. package/src/services/webhook-service.ts +180 -0
  138. package/src/services/workflow-service.ts +7 -0
  139. package/src/types/core.types.ts +42 -14
  140. package/src/types/state.types.ts +23 -3
  141. package/src/utils/hmac.ts +27 -0
  142. package/src/utils/safe-fetch.test.ts +373 -0
  143. package/src/utils/safe-fetch.ts +213 -0
  144. package/src/wirings/ai-agent/ai-agent-agui.test.ts +1127 -0
  145. package/src/wirings/ai-agent/ai-agent-agui.ts +386 -0
  146. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +308 -0
  147. package/src/wirings/ai-agent/ai-agent-prepare.ts +192 -8
  148. package/src/wirings/ai-agent/ai-agent-runner.test.ts +48 -1
  149. package/src/wirings/ai-agent/ai-agent-runner.ts +21 -0
  150. package/src/wirings/ai-agent/ai-agent-stream.test.ts +198 -10
  151. package/src/wirings/ai-agent/ai-agent-stream.ts +84 -10
  152. package/src/wirings/ai-agent/ai-agent.types.ts +28 -0
  153. package/src/wirings/ai-agent/index.ts +1 -0
  154. package/src/wirings/ai-agent/voice-input.ts +6 -35
  155. package/src/wirings/channel/channel-common.ts +0 -1
  156. package/src/wirings/channel/channel-handler.ts +1 -9
  157. package/src/wirings/channel/channel-middleware-runner.test.ts +28 -2
  158. package/src/wirings/channel/channel-middleware-runner.ts +39 -27
  159. package/src/wirings/channel/channel.types.ts +0 -6
  160. package/src/wirings/cli/cli-runner.ts +0 -2
  161. package/src/wirings/cli/cli.types.ts +0 -2
  162. package/src/wirings/http/http-routes.ts +0 -3
  163. package/src/wirings/http/http-runner.test.ts +1 -11
  164. package/src/wirings/http/http-runner.ts +16 -39
  165. package/src/wirings/http/http.types.ts +1 -10
  166. package/src/wirings/mcp/mcp-runner.ts +0 -2
  167. package/src/wirings/mcp/mcp.types.ts +3 -11
  168. package/src/wirings/oauth2/index.ts +0 -3
  169. package/src/wirings/rpc/addon-runner.ts +233 -0
  170. package/src/wirings/rpc/index.ts +17 -1
  171. package/src/wirings/rpc/remote-addon-auth.ts +69 -0
  172. package/src/wirings/rpc/rpc-runner.test.ts +319 -16
  173. package/src/wirings/rpc/rpc-runner.ts +155 -3
  174. package/src/wirings/rpc/rpc-types.ts +16 -5
  175. package/src/wirings/rpc/wire-addon.test.ts +6 -3
  176. package/src/wirings/rpc/wire-addon.ts +9 -0
  177. package/src/wirings/rpc/wire-remote-addon.ts +57 -0
  178. package/src/wirings/scope/index.ts +14 -0
  179. package/src/wirings/scope/scope.test.ts +135 -0
  180. package/src/wirings/scope/scope.types.ts +44 -0
  181. package/src/wirings/scope/validate-scope-definitions.ts +110 -0
  182. package/src/wirings/scope/wire-scope.ts +34 -0
  183. package/src/wirings/workflow/dsl/index.ts +4 -0
  184. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +61 -0
  185. package/src/wirings/workflow/graph/graph-node.ts +2 -0
  186. package/src/wirings/workflow/graph/graph-runner.test.ts +58 -0
  187. package/src/wirings/workflow/graph/graph-runner.ts +3 -0
  188. package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
  189. package/src/wirings/workflow/graph/workflow-graph.types.ts +5 -0
  190. package/src/wirings/workflow/index.ts +5 -0
  191. package/src/wirings/workflow/pikku-workflow-service.test.ts +341 -0
  192. package/src/wirings/workflow/pikku-workflow-service.ts +236 -0
  193. package/src/wirings/workflow/workflow-step-session.test.ts +21 -5
  194. package/src/wirings/workflow/workflow.types.ts +4 -0
  195. package/tsconfig.tsbuildinfo +1 -1
  196. package/src/wirings/oauth2/oauth2-client.test.ts +0 -930
  197. package/src/wirings/oauth2/oauth2-client.ts +0 -331
  198. package/src/wirings/oauth2/oauth2-routes.ts +0 -234
@@ -3,7 +3,7 @@ import { AIProviderNotConfiguredError } from '../../errors/errors.js';
3
3
  import { randomUUID } from './ai-agent-utils.js';
4
4
  import { combineChannelMiddleware, wrapChannelWithMiddleware, } from '../channel/channel-middleware-runner.js';
5
5
  import { resolveMemoryServices, loadContextMessages, trimMessages, getWorkingMemoryMiddleware, } from './ai-agent-memory.js';
6
- import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, ToolApprovalRequired, ToolCredentialRequired, } from './ai-agent-prepare.js';
6
+ import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, resolveOwnerResourceId, agentSessionScope, assertResourceOwner, ToolApprovalRequired, ToolCredentialRequired, APPROVAL_REQUIRED, } from './ai-agent-prepare.js';
7
7
  import { resolveModelConfig } from './ai-agent-model-config.js';
8
8
  function createPersistingChannel(parent, storage, threadId) {
9
9
  let fullText = '';
@@ -213,14 +213,23 @@ export function checkForApprovals(stepResult, tools, runId) {
213
213
  approvals.push(new ToolApprovalRequired(tc.toolCallId, tc.toolName, tc.args));
214
214
  continue;
215
215
  }
216
+ // The approval marker is only trusted from a framework tool declared with
217
+ // `forwardsApproval` (the sub-agent delegating tools) AND carrying the
218
+ // non-forgeable `APPROVAL_REQUIRED` Symbol brand. A plain tool's output — or
219
+ // a delegating tool's LLM-shaped `result.object`, which an attacker can
220
+ // influence — is plain JSON and can never carry the Symbol, so it can never
221
+ // forge an approval/suspension.
222
+ if (!toolDef?.forwardsApproval) {
223
+ continue;
224
+ }
216
225
  const tr = stepResult.toolResults.find((r) => r.toolCallId === tc.toolCallId);
217
226
  if (tr?.result &&
218
227
  typeof tr.result === 'object' &&
219
- '__approvalRequired' in tr.result) {
228
+ APPROVAL_REQUIRED in tr.result) {
220
229
  const r = tr.result;
221
230
  if (r.subApprovals?.length) {
222
231
  for (const sub of r.subApprovals) {
223
- approvals.push(new ToolApprovalRequired(sub.toolCallId, r.toolName, r.args, undefined, sub.toolName, sub.args, r.agentRunId));
232
+ approvals.push(new ToolApprovalRequired(sub.toolCallId, r.toolName, r.args, sub.reason, sub.toolName, sub.args, r.agentRunId));
224
233
  }
225
234
  }
226
235
  else {
@@ -341,16 +350,32 @@ function handleCredentialRequests(requests, runId, channel, aiRunState, persisti
341
350
  suspendReason: 'credential',
342
351
  pendingApprovals,
343
352
  });
344
- // Don't send credential-request SSE events the tool result with
345
- // __credentialRequired was already streamed. The frontend detects it
346
- // from the tool result and shows Connect/Ignore buttons.
353
+ // The __credentialRequired tool result is suppressed from the stream, so
354
+ // credential-request events (with the runId needed for /resume) are the
355
+ // client's signal to show Connect/Ignore buttons — mirroring how
356
+ // approval-request suspensions work.
357
+ for (const req of requests) {
358
+ channel.send({
359
+ type: 'credential-request',
360
+ toolCallId: req.toolCallId,
361
+ toolName: req.toolName,
362
+ args: req.args,
363
+ credentialName: req.credentialName,
364
+ credentialType: req.credentialType,
365
+ connectUrl: req.connectUrl,
366
+ runId,
367
+ });
368
+ }
347
369
  channel.send({ type: 'done' });
348
370
  channel.close();
349
371
  })();
350
372
  }
351
373
  export async function streamAIAgent(agentName, input, channel, params, agentSessionMap, options) {
352
374
  const sessionMap = agentSessionMap ?? new Map();
353
- const normalizedInput = input;
375
+ const normalizedInput = {
376
+ ...input,
377
+ resourceId: resolveOwnerResourceId(params, agentSessionScope(agentName), input.resourceId),
378
+ };
354
379
  const streamContext = { channel, options };
355
380
  // delegateState is attached after prepareAgentRun resolves the agent config
356
381
  const { agent, packageName, resolvedName, agentRunner, storage, memoryConfig, threadId, userMessage, runnerParams, maxSteps, missingRpcs, workingMemorySchemaName, } = await prepareAgentRun(agentName, normalizedInput, params, sessionMap, streamContext);
@@ -408,6 +433,7 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
408
433
  createdAt: new Date(),
409
434
  updatedAt: new Date(),
410
435
  });
436
+ options?.onRunCreated?.(runId);
411
437
  if (storage) {
412
438
  await storage.saveMessages(threadId, [userMessage]);
413
439
  }
@@ -447,6 +473,22 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
447
473
  const wrappedChannel = allChannelMiddleware.length > 0
448
474
  ? wrapChannelWithMiddleware({ channel: persistingChannel }, singletonServices, allChannelMiddleware).channel
449
475
  : persistingChannel;
476
+ // Tool results carrying the __credentialRequired marker must never reach
477
+ // the client or persisted history: the run suspends with credential-request
478
+ // events instead (mirroring approvals), and leaving the tool call
479
+ // unresulted is what lets the client resume it after connecting.
480
+ const credentialFilteredChannel = {
481
+ ...wrappedChannel,
482
+ send: (event) => {
483
+ if (event.type === 'tool-result' &&
484
+ event.result !== null &&
485
+ typeof event.result === 'object' &&
486
+ '__credentialRequired' in event.result) {
487
+ return;
488
+ }
489
+ wrappedChannel.send(event);
490
+ },
491
+ };
450
492
  // In delegate mode (default), suppress parent's text from reaching the client
451
493
  // AFTER a sub-agent has been called. If the parent responds directly (no delegation),
452
494
  // its text goes through normally. Sub-agent text bypasses this path entirely
@@ -458,16 +500,16 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
458
500
  }
459
501
  const outputChannel = isDelegateMode
460
502
  ? {
461
- ...wrappedChannel,
503
+ ...credentialFilteredChannel,
462
504
  send: (event) => {
463
505
  if (delegateState.delegated &&
464
506
  (event.type === 'text-delta' || event.type === 'reasoning-delta'))
465
507
  return;
466
- wrappedChannel.send(event);
508
+ credentialFilteredChannel.send(event);
467
509
  },
468
510
  delegateState,
469
511
  }
470
- : wrappedChannel;
512
+ : credentialFilteredChannel;
471
513
  try {
472
514
  const loopResult = await runStreamStepLoop({
473
515
  agent,
@@ -531,6 +573,7 @@ export async function resumeAIAgent(input, channel, params, options) {
531
573
  if (!run) {
532
574
  throw new Error(`No run found for runId ${input.runId}`);
533
575
  }
576
+ assertResourceOwner(resolveOwnerResourceId(params, agentSessionScope(run.agentName), run.resourceId), run.resourceId, 'run');
534
577
  const pending = run.pendingApprovals?.find((p) => p.toolCallId === input.toolCallId);
535
578
  if (!pending) {
536
579
  throw new Error(`No pending approval for toolCallId ${input.toolCallId} on run ${input.runId}`);
@@ -567,6 +610,14 @@ export async function resumeAIAgent(input, channel, params, options) {
567
610
  },
568
611
  ]);
569
612
  }
613
+ channel.send({
614
+ type: 'tool-result',
615
+ toolCallId: input.toolCallId,
616
+ toolName: pending.type === 'tool-call' || pending.type === 'credential-request'
617
+ ? pending.toolName
618
+ : pending.agentName,
619
+ result: denialResult,
620
+ });
570
621
  // Check remaining pending approvals
571
622
  const updatedRun = await aiRunState.getRun(run.runId);
572
623
  const remaining = updatedRun?.pendingApprovals ?? [];
@@ -100,6 +100,19 @@ export interface AIAgentOutput {
100
100
  runId: string;
101
101
  }>;
102
102
  }
103
+ /**
104
+ * How an agent's threads/runs are owned and partitioned.
105
+ * - `'user'` (default): owner is the authenticated `session.userId`; the caller's
106
+ * `resourceId` becomes a sub-partition within that user (`userId:resourceId`).
107
+ * - `'org'`: owner is the authenticated `session.orgId` (`orgId:resourceId`), so
108
+ * threads are shared across everyone in the org. Requires a session with an org
109
+ * (e.g. Better Auth's `organization` plugin) — otherwise access is denied.
110
+ *
111
+ * The trusted principal is always the prefix, so a client-supplied `resourceId`
112
+ * can sub-divide within the caller's own boundary but can never widen access to
113
+ * another user's or org's threads.
114
+ */
115
+ export type SessionScope = 'user' | 'org';
103
116
  export interface AIAgentToolDef {
104
117
  name: string;
105
118
  description: string;
@@ -107,6 +120,14 @@ export interface AIAgentToolDef {
107
120
  execute: (input: unknown) => Promise<unknown>;
108
121
  needsApproval?: boolean;
109
122
  approvalDescriptionFn?: (input: unknown) => Promise<string>;
123
+ /**
124
+ * Set only by the framework on sub-agent delegating tools. Such a tool may
125
+ * legitimately return an `__approvalRequired` marker to forward a nested
126
+ * sub-agent approval. The marker is honored ONLY from a tool with this flag —
127
+ * a plain tool's output (which an attacker may influence) can never forge an
128
+ * approval request. See `checkForApprovals`.
129
+ */
130
+ forwardsApproval?: boolean;
110
131
  }
111
132
  export interface PikkuAIMiddlewareHooks<State extends Record<string, unknown> = Record<string, unknown>, Services = any> {
112
133
  modifyInput?: (services: Services, ctx: {
@@ -200,8 +221,11 @@ export type CoreAIAgent<PikkuPermission = CorePikkuPermission<any, any>, PikkuMi
200
221
  goal: string;
201
222
  model: string;
202
223
  temperature?: number;
224
+ /** Ownership/partitioning of this agent's threads and runs. Defaults to `'user'`. */
225
+ sessionScope?: SessionScope;
203
226
  tools?: unknown[];
204
227
  agents?: unknown[];
228
+ workflows?: unknown[];
205
229
  agentMode?: 'delegate' | 'supervise';
206
230
  memory?: AIAgentMemoryConfig;
207
231
  maxSteps?: number;
@@ -267,6 +291,7 @@ export type AIStreamEvent = {
267
291
  toolName: string;
268
292
  args: unknown;
269
293
  reason?: string;
294
+ runId?: string;
270
295
  agent?: string;
271
296
  session?: string;
272
297
  } | {
@@ -397,9 +422,10 @@ export interface AgentRunService {
397
422
  deleteThread(threadId: string): Promise<boolean>;
398
423
  getDistinctAgentNames(): Promise<string[]>;
399
424
  }
400
- export type AIAgentMeta = Record<string, Omit<CoreAIAgent, 'input' | 'output' | 'tools' | 'agents' | 'middleware' | 'channelMiddleware' | 'aiMiddleware' | 'permissions'> & {
425
+ export type AIAgentMeta = Record<string, Omit<CoreAIAgent, 'input' | 'output' | 'tools' | 'agents' | 'workflows' | 'middleware' | 'channelMiddleware' | 'aiMiddleware' | 'permissions'> & {
401
426
  tools?: string[];
402
427
  agents?: string[];
428
+ workflows?: string[];
403
429
  inputSchema: string | null;
404
430
  outputSchema: string | null;
405
431
  workingMemorySchema: string | null;
@@ -1,4 +1,5 @@
1
1
  export { agent, agentStream, agentResume, agentApprove, } from './ai-agent-helpers.js';
2
+ export { wrapChannelWithAGUI, type AGUIEvent } from './ai-agent-agui.js';
2
3
  export { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js';
3
4
  export { streamAIAgent, resumeAIAgent } from './ai-agent-stream.js';
4
5
  export { voiceInput } from './voice-input.js';
@@ -1,4 +1,5 @@
1
1
  export { agent, agentStream, agentResume, agentApprove, } from './ai-agent-helpers.js';
2
+ export { wrapChannelWithAGUI } from './ai-agent-agui.js';
2
3
  export { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js';
3
4
  export { streamAIAgent, resumeAIAgent } from './ai-agent-stream.js';
4
5
  export { voiceInput } from './voice-input.js';
@@ -6,5 +6,6 @@ export declare const voiceInput: (config?: {
6
6
  logLevel?: import("../../services/logger.js").LogLevel;
7
7
  secrets?: {};
8
8
  workflow?: import("../workflow/workflow.types.js").WorkflowServiceConfig;
9
+ webhook?: import("../../services/webhook-service.js").WebhookServiceConfig;
9
10
  postgres?: import("../../types/core.types.js").PostgresConfig;
10
11
  }>>;
@@ -1,4 +1,5 @@
1
1
  import { pikkuAIMiddleware } from '../../types/core.types.js';
2
+ import { safeFetch } from '../../utils/safe-fetch.js';
2
3
  function base64ToUint8Array(base64) {
3
4
  const binary = atob(base64);
4
5
  const bytes = new Uint8Array(binary.length);
@@ -8,45 +9,8 @@ function base64ToUint8Array(base64) {
8
9
  return bytes;
9
10
  }
10
11
  const MAX_AUDIO_SIZE = 50 * 1024 * 1024;
11
- // Portable SSRF guard: @pikku/core runs in edge runtimes (CF Workers) with no
12
- // Node `dns`, so we cannot resolve hostnames to check for private targets.
13
- // Reject the obvious internal literals; callers wanting stricter control pass
14
- // an explicit `allowedAudioHosts` allowlist. (Does not defend against a public
15
- // hostname that resolves to a private IP / DNS rebinding — out of reach here.)
16
- function isPrivateHost(hostname) {
17
- const host = hostname.replace(/^\[|\]$/g, '').toLowerCase();
18
- if (host === 'localhost' || host === '0.0.0.0' || host === '::1')
19
- return true;
20
- if (host.startsWith('fe80:') || host.startsWith('fc') || host.startsWith('fd'))
21
- return true;
22
- const v4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.\d{1,3}$/);
23
- if (v4) {
24
- const [a, b] = [Number(v4[1]), Number(v4[2])];
25
- if (a === 127 || a === 10 || a === 0)
26
- return true;
27
- if (a === 169 && b === 254)
28
- return true; // link-local incl. cloud metadata
29
- if (a === 172 && b >= 16 && b <= 31)
30
- return true;
31
- if (a === 192 && b === 168)
32
- return true;
33
- }
34
- return false;
35
- }
36
12
  async function fetchAsUint8Array(url, allowedAudioHosts) {
37
- const parsed = new URL(url);
38
- if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
39
- throw new Error('Only HTTP(S) URLs are supported for audio');
40
- }
41
- if (allowedAudioHosts) {
42
- if (!allowedAudioHosts.includes(parsed.hostname)) {
43
- throw new Error(`Audio URL host is not allowed: ${parsed.hostname}`);
44
- }
45
- }
46
- else if (isPrivateHost(parsed.hostname)) {
47
- throw new Error(`Refusing to fetch audio from a private/internal host: ${parsed.hostname}`);
48
- }
49
- const response = await fetch(url);
13
+ const response = await safeFetch(url, {}, { allowedHosts: allowedAudioHosts });
50
14
  const contentLength = response.headers.get('content-length');
51
15
  if (contentLength && parseInt(contentLength, 10) > MAX_AUDIO_SIZE) {
52
16
  throw new Error('Audio file exceeds maximum size');
@@ -12,6 +12,7 @@ export declare const voiceOutput: (config?: {
12
12
  logLevel?: import("../../services/logger.js").LogLevel;
13
13
  secrets?: {};
14
14
  workflow?: import("../workflow/workflow.types.js").WorkflowServiceConfig;
15
+ webhook?: import("../../services/webhook-service.js").WebhookServiceConfig;
15
16
  postgres?: import("../../types/core.types.js").PostgresConfig;
16
17
  }>>;
17
18
  export {};
@@ -42,7 +42,6 @@ export const runChannelLifecycleWithMiddleware = async ({ channelConfig, meta, l
42
42
  data: () => data,
43
43
  wire,
44
44
  tags: meta.tags ?? [],
45
- inheritedPermissions: meta.permissions,
46
45
  packageName: meta.packageName ?? null,
47
46
  });
48
47
  };
@@ -49,7 +49,7 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
49
49
  return;
50
50
  }
51
51
  const routeMeta = getRouteMeta(channelConfig.name, routingProperty, routerValue);
52
- const { packageName, middleware: routeInheritedMiddleware, permissions: inheritedPermissions, } = routeMeta;
52
+ const { packageName, middleware: routeInheritedMiddleware } = routeMeta;
53
53
  const pikkuFuncId = routeMeta.pikkuFuncId;
54
54
  // Get wire middleware: channel-level middleware + message-specific middleware
55
55
  const channelWireMiddleware = channelConfig.middleware || [];
@@ -68,7 +68,6 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
68
68
  const wireMiddleware = [...channelWireMiddleware, ...messageWireMiddleware];
69
69
  // Inherited middleware comes from metadata (tag groups, non-inline wire)
70
70
  const inheritedMiddleware = routeInheritedMiddleware || [];
71
- const wirePermissions = isWrapper ? onMessage.permissions : undefined;
72
71
  // Create unique cache key that includes routing info to avoid cache collisions
73
72
  // when multiple message handlers use the same function
74
73
  const cacheKey = routingProperty
@@ -83,8 +82,6 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
83
82
  wireMiddleware,
84
83
  inheritedChannelMiddleware: channelMeta?.channelMiddleware,
85
84
  wireChannelMiddleware: wireChannelMiddleware,
86
- inheritedPermissions,
87
- wirePermissions,
88
85
  coerceDataFromSchema: true,
89
86
  tags: channelConfig.tags,
90
87
  sessionService: userSession,
@@ -2,6 +2,18 @@ import type { CoreSingletonServices, MiddlewareMetadata, PikkuRawWire } from '..
2
2
  import type { CorePikkuChannelMiddleware } from './channel.types.js';
3
3
  export declare const addChannelMiddleware: (tag: string, middleware: CorePikkuChannelMiddleware[], packageName?: string | null) => CorePikkuChannelMiddleware[];
4
4
  export declare const clearChannelMiddlewareCache: () => void;
5
+ /**
6
+ * Combine the inherited (tag/named) channel middleware with any per-run
7
+ * middleware for a wiring.
8
+ *
9
+ * Only the statically-resolved inherited middleware is deterministic per `uid`
10
+ * and safe to cache. `wireChannelMiddleware` is a per-run set of closures (e.g.
11
+ * an AI agent's per-invocation stream middleware holding that run's
12
+ * thread/session state) and MUST NOT be cached — caching it lets a later run of
13
+ * the same `uid` reuse an earlier run's closures, leaking that run's state (and
14
+ * growing memory) across invocations. It is therefore appended fresh on every
15
+ * call after the cached inherited slice.
16
+ */
5
17
  export declare const combineChannelMiddleware: (wireType: string, uid: string, { wireInheritedChannelMiddleware, wireChannelMiddleware, packageName, }?: {
6
18
  wireInheritedChannelMiddleware?: MiddlewareMetadata[];
7
19
  wireChannelMiddleware?: CorePikkuChannelMiddleware[];
@@ -16,33 +16,48 @@ export const clearChannelMiddlewareCache = () => {
16
16
  delete channelMiddlewareCache[key];
17
17
  }
18
18
  };
19
+ /**
20
+ * Combine the inherited (tag/named) channel middleware with any per-run
21
+ * middleware for a wiring.
22
+ *
23
+ * Only the statically-resolved inherited middleware is deterministic per `uid`
24
+ * and safe to cache. `wireChannelMiddleware` is a per-run set of closures (e.g.
25
+ * an AI agent's per-invocation stream middleware holding that run's
26
+ * thread/session state) and MUST NOT be cached — caching it lets a later run of
27
+ * the same `uid` reuse an earlier run's closures, leaking that run's state (and
28
+ * growing memory) across invocations. It is therefore appended fresh on every
29
+ * call after the cached inherited slice.
30
+ */
19
31
  export const combineChannelMiddleware = (wireType, uid, { wireInheritedChannelMiddleware, wireChannelMiddleware, packageName = null, } = {}) => {
20
32
  const cacheKey = `${wireType}:${uid}`;
21
- if (channelMiddlewareCache[cacheKey]) {
22
- return channelMiddlewareCache[cacheKey];
23
- }
24
- const resolved = [];
25
- if (wireInheritedChannelMiddleware) {
26
- for (const meta of wireInheritedChannelMiddleware) {
27
- if (meta.type === 'tag') {
28
- const groups = getTagGroups(pikkuState(packageName, 'channelMiddleware', 'tagGroup'), meta.tag);
29
- for (const group of groups) {
30
- resolved.push(...group);
33
+ let inherited = channelMiddlewareCache[cacheKey];
34
+ if (!inherited) {
35
+ const resolved = [];
36
+ if (wireInheritedChannelMiddleware) {
37
+ for (const meta of wireInheritedChannelMiddleware) {
38
+ if (meta.type === 'tag') {
39
+ const groups = getTagGroups(pikkuState(packageName, 'channelMiddleware', 'tagGroup'), meta.tag);
40
+ for (const group of groups) {
41
+ resolved.push(...group);
42
+ }
31
43
  }
32
- }
33
- else if (meta.type === 'wire') {
34
- const middleware = getChannelMiddlewareByName(meta.name);
35
- if (middleware) {
36
- resolved.push(middleware);
44
+ else if (meta.type === 'wire') {
45
+ const middleware = getChannelMiddlewareByName(meta.name);
46
+ if (middleware) {
47
+ resolved.push(middleware);
48
+ }
37
49
  }
38
50
  }
39
51
  }
52
+ inherited = channelMiddlewareCache[cacheKey] = freezeDedupe(resolved);
40
53
  }
41
- if (wireChannelMiddleware) {
42
- resolved.push(...wireChannelMiddleware);
54
+ if (!wireChannelMiddleware?.length) {
55
+ return inherited;
43
56
  }
44
- channelMiddlewareCache[cacheKey] = freezeDedupe(resolved);
45
- return channelMiddlewareCache[cacheKey];
57
+ return freezeDedupe([
58
+ ...inherited,
59
+ ...wireChannelMiddleware,
60
+ ]);
46
61
  };
47
62
  export function wrapChannelWithMiddleware(wire, services, middlewares) {
48
63
  if (middlewares.length === 0 || !wire.channel)
@@ -1,9 +1,9 @@
1
1
  import type { HTTPFunctionMetaInputTypes, PikkuHTTPRequest, PikkuHTTPResponse } from '../http/http.types.js';
2
- import type { CorePikkuMiddleware, MiddlewareMetadata, PermissionMetadata } from '../../types/core.types.js';
2
+ import type { CorePikkuMiddleware, MiddlewareMetadata } from '../../types/core.types.js';
3
3
  export type BinaryData = ArrayBuffer | Uint8Array;
4
4
  export type CorePikkuChannelMiddleware<Services = any, Event = unknown> = (services: Services, event: Event, next: (event: Event | Event[] | null) => Promise<void> | void) => Promise<void> | void;
5
5
  export type CorePikkuChannelMiddlewareFactory<In = any, Services = any, Event = unknown> = (input: In) => CorePikkuChannelMiddleware<Services, Event>;
6
- import type { CorePermissionGroup, CorePikkuFunction, CorePikkuFunctionConfig, CorePikkuFunctionSessionless, CorePikkuPermission } from '../../function/functions.types.js';
6
+ import type { CorePikkuFunction, CorePikkuFunctionConfig, CorePikkuFunctionSessionless, CorePikkuPermission } from '../../function/functions.types.js';
7
7
  export type RunChannelOptions = Partial<{
8
8
  skipUserSession: boolean;
9
9
  respondWith404: boolean;
@@ -24,7 +24,6 @@ export interface ChannelMessageMeta {
24
24
  errors?: string[];
25
25
  tags?: string[];
26
26
  middleware?: MiddlewareMetadata[];
27
- permissions?: PermissionMetadata[];
28
27
  }
29
28
  export interface ChannelMeta {
30
29
  name: string;
@@ -43,7 +42,6 @@ export interface ChannelMeta {
43
42
  errors?: string[];
44
43
  tags?: string[];
45
44
  middleware?: MiddlewareMetadata[];
46
- permissions?: PermissionMetadata[];
47
45
  channelMiddleware?: MiddlewareMetadata[];
48
46
  }
49
47
  export type ChannelsMeta = Record<string, ChannelMeta>;
@@ -61,13 +59,11 @@ export type CoreChannel<ChannelData, Channel extends string, ChannelConnect = Co
61
59
  onMessage?: ChannelFunctionMessage;
62
60
  onMessageWiring?: Record<string, Record<string, ChannelFunctionMessage | {
63
61
  func: ChannelFunctionMessage;
64
- permissions?: CorePermissionGroup<PikkuPermission>;
65
62
  auth?: boolean;
66
63
  middleware?: PikkuMiddleware[];
67
64
  }>>;
68
65
  middleware?: PikkuMiddleware[];
69
66
  channelMiddleware?: Array<CorePikkuChannelMiddleware | CorePikkuChannelMiddlewareFactory>;
70
- permissions?: CorePermissionGroup<PikkuPermission>;
71
67
  auth?: boolean;
72
68
  binary?: boolean | null;
73
69
  onBinaryMessage?: (services: any, data: BinaryData, channel: PikkuChannel<ChannelData, any>) => Promise<BinaryData | void> | BinaryData | void;
@@ -253,8 +253,6 @@ export async function runCLICommand({ program, commandPath, data, singletonServi
253
253
  auth: false,
254
254
  inheritedMiddleware: currentCommand.middleware,
255
255
  wireMiddleware: allWireMiddleware,
256
- inheritedPermissions: currentCommand.permissions,
257
- wirePermissions: undefined,
258
256
  coerceDataFromSchema: true,
259
257
  tags: programData?.tags,
260
258
  wire,
@@ -1,4 +1,4 @@
1
- import type { CorePikkuMiddleware, CoreSingletonServices, CoreUserSession, CoreServices, MiddlewareMetadata, PermissionMetadata } from '../../types/core.types.js';
1
+ import type { CorePikkuMiddleware, CoreSingletonServices, CoreUserSession, CoreServices, MiddlewareMetadata } from '../../types/core.types.js';
2
2
  import type { CorePikkuFunctionConfig, CorePikkuPermission, CorePikkuFunction, CorePikkuFunctionSessionless } from '../../function/functions.types.js';
3
3
  import type { PikkuChannel } from '../channel/channel.types.js';
4
4
  /**
@@ -62,7 +62,6 @@ export interface CLICommandMeta {
62
62
  tags?: string[];
63
63
  subcommands?: Record<string, CLICommandMeta>;
64
64
  middleware?: MiddlewareMetadata[];
65
- permissions?: PermissionMetadata[];
66
65
  isDefault?: boolean;
67
66
  }
68
67
  /**
@@ -83,7 +83,6 @@ function processRouteMap(map, parentConfig) {
83
83
  * - tags: Merges (parent + child)
84
84
  * - middleware: Merges (parent runs first)
85
85
  * - auth: Inner overrides outer
86
- * - permissions: Inner overrides outer
87
86
  */
88
87
  function mergeGroupConfig(parent, child) {
89
88
  return {
@@ -91,7 +90,6 @@ function mergeGroupConfig(parent, child) {
91
90
  tags: [...(parent.tags || []), ...(child.tags || [])],
92
91
  middleware: [...(parent.middleware || []), ...(child.middleware || [])],
93
92
  auth: child.auth ?? parent.auth,
94
- permissions: child.permissions ?? parent.permissions,
95
93
  };
96
94
  }
97
95
  /**
@@ -109,7 +107,6 @@ function registerRoute(route, groupConfig) {
109
107
  ...(groupConfig.middleware || []),
110
108
  ...(route.middleware || []),
111
109
  ],
112
- permissions: route.permissions ?? groupConfig.permissions,
113
110
  contentType: route.contentType,
114
111
  timeout: route.timeout,
115
112
  headers: route.headers,
@@ -1,5 +1,5 @@
1
1
  import type { CoreHTTPFunctionWiring, RunHTTPWiringOptions, PikkuHTTP, PikkuHTTPRequest, PikkuHTTPResponse } from './http.types.js';
2
- import type { CorePikkuFunction, CorePikkuFunctionSessionless, CorePikkuPermission, CorePermissionGroup } from '../../function/functions.types.js';
2
+ import type { CorePikkuFunction, CorePikkuFunctionSessionless, CorePikkuPermission } from '../../function/functions.types.js';
3
3
  import type { CorePikkuMiddleware, CorePikkuMiddlewareGroup } from '../../types/core.types.js';
4
4
  import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js';
5
5
  /**
@@ -33,35 +33,15 @@ import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js';
33
33
  */
34
34
  export declare const addHTTPMiddleware: <PikkuMiddleware extends CorePikkuMiddleware>(pattern: string, middleware: CorePikkuMiddlewareGroup, packageName?: string | null) => CorePikkuMiddlewareGroup;
35
35
  /**
36
- * Registers HTTP permissions for a specific route pattern.
37
- *
38
- * This function registers permissions at runtime that will be applied to
39
- * HTTP routes matching the specified pattern.
40
- *
41
- * For tree-shaking benefits, wrap in a factory function:
42
- * `export const x = () => addHTTPPermission('pattern', [...])`
43
- *
44
- * @template PikkuPermission The permission type.
45
- * @param {string} pattern - Route pattern (e.g., '*' for all routes, '/api/*' for specific routes).
46
- * @param {CorePermissionGroup | CorePikkuPermission[]} permissions - Permissions for this route pattern.
47
- *
48
- * @returns {CorePermissionGroup | CorePikkuPermission[]} The permissions (for chaining/wrapping).
49
- *
50
- * @example
51
- * ```typescript
52
- * // Recommended: tree-shakeable
53
- * export const httpGlobalPermissions = () => addHTTPPermission('*', [
54
- * authenticatedPermission,
55
- * rateLimitPermission
56
- * ])
57
- *
58
- * // Also works: no tree-shaking
59
- * export const apiPermissions = addHTTPPermission('/api/*', [
60
- * adminPermission
61
- * ])
62
- * ```
36
+ * @deprecated HTTP-route-level permissions were removed in #972 — permissions
37
+ * are now function-scoped only (declare them on the function via
38
+ * `pikkuFunc({ permissions })`). This throwing stub exists solely so the pinned
39
+ * bootstrap CLI (which still generates an `addHTTPPermission` wrapper) can
40
+ * resolve the import at build time; it is never called. Delete once
41
+ * `PIKKU_CLI_VERSION` in the CLI build is bumped past the release that removes
42
+ * HTTP-route permissions.
63
43
  */
64
- export declare const addHTTPPermission: <PikkuPermission extends CorePikkuPermission>(pattern: string, permissions: CorePermissionGroup | CorePikkuPermission[], packageName?: string | null) => CorePermissionGroup | CorePikkuPermission[];
44
+ export declare const addHTTPPermission: (_pattern: string, _permissions: Record<string, CorePikkuPermission | CorePikkuPermission[]> | CorePikkuPermission[]) => never;
65
45
  /**
66
46
  * Adds a new route to the global HTTP route registry.
67
47
  *
@@ -64,38 +64,16 @@ export const addHTTPMiddleware = (pattern, middleware, packageName = null) => {
64
64
  return middleware;
65
65
  };
66
66
  /**
67
- * Registers HTTP permissions for a specific route pattern.
68
- *
69
- * This function registers permissions at runtime that will be applied to
70
- * HTTP routes matching the specified pattern.
71
- *
72
- * For tree-shaking benefits, wrap in a factory function:
73
- * `export const x = () => addHTTPPermission('pattern', [...])`
74
- *
75
- * @template PikkuPermission The permission type.
76
- * @param {string} pattern - Route pattern (e.g., '*' for all routes, '/api/*' for specific routes).
77
- * @param {CorePermissionGroup | CorePikkuPermission[]} permissions - Permissions for this route pattern.
78
- *
79
- * @returns {CorePermissionGroup | CorePikkuPermission[]} The permissions (for chaining/wrapping).
80
- *
81
- * @example
82
- * ```typescript
83
- * // Recommended: tree-shakeable
84
- * export const httpGlobalPermissions = () => addHTTPPermission('*', [
85
- * authenticatedPermission,
86
- * rateLimitPermission
87
- * ])
88
- *
89
- * // Also works: no tree-shaking
90
- * export const apiPermissions = addHTTPPermission('/api/*', [
91
- * adminPermission
92
- * ])
93
- * ```
67
+ * @deprecated HTTP-route-level permissions were removed in #972 — permissions
68
+ * are now function-scoped only (declare them on the function via
69
+ * `pikkuFunc({ permissions })`). This throwing stub exists solely so the pinned
70
+ * bootstrap CLI (which still generates an `addHTTPPermission` wrapper) can
71
+ * resolve the import at build time; it is never called. Delete once
72
+ * `PIKKU_CLI_VERSION` in the CLI build is bumped past the release that removes
73
+ * HTTP-route permissions.
94
74
  */
95
- export const addHTTPPermission = (pattern, permissions, packageName = null) => {
96
- const httpGroups = pikkuState(packageName, 'permissions', 'httpGroup');
97
- httpGroups[pattern] = permissions;
98
- return permissions;
75
+ export const addHTTPPermission = (_pattern, _permissions) => {
76
+ throw new Error('addHTTPPermission was removed in #972 — HTTP-route-level permissions no longer exist. Declare permissions on the function definition instead: pikkuFunc({ permissions }).');
99
77
  };
100
78
  /**
101
79
  * Adds a new route to the global HTTP route registry.
@@ -156,7 +134,6 @@ const getMatchingRoute = (requestType, requestPath) => {
156
134
  matchedPath,
157
135
  params: matchedPath.params,
158
136
  route,
159
- permissions: route.permissions,
160
137
  meta: meta,
161
138
  };
162
139
  }
@@ -307,8 +284,6 @@ const executeRoute = async (services, matchedRoute, http, options) => {
307
284
  data,
308
285
  inheritedMiddleware: meta.middleware,
309
286
  wireMiddleware: route.middleware,
310
- inheritedPermissions: meta.permissions,
311
- wirePermissions: route.permissions,
312
287
  coerceDataFromSchema: options.coerceDataFromSchema,
313
288
  tags: route.tags,
314
289
  wire,