@pikku/core 0.12.63 → 0.12.66

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 (225) hide show
  1. package/CHANGELOG.md +368 -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 +42 -27
  24. package/dist/permissions.js +101 -203
  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/testing/service-tests.js +37 -0
  46. package/dist/types/core.types.d.ts +34 -6
  47. package/dist/types/state.types.d.ts +20 -2
  48. package/dist/utils/hmac.d.ts +16 -0
  49. package/dist/utils/hmac.js +26 -0
  50. package/dist/utils/safe-fetch.d.ts +51 -0
  51. package/dist/utils/safe-fetch.js +192 -0
  52. package/dist/wirings/ai-agent/ai-agent-agui.d.ts +84 -0
  53. package/dist/wirings/ai-agent/ai-agent-agui.js +305 -0
  54. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +105 -1
  55. package/dist/wirings/ai-agent/ai-agent-prepare.js +249 -11
  56. package/dist/wirings/ai-agent/ai-agent-runner.js +10 -1
  57. package/dist/wirings/ai-agent/ai-agent-stream.js +88 -16
  58. package/dist/wirings/ai-agent/ai-agent.types.d.ts +56 -2
  59. package/dist/wirings/ai-agent/index.d.ts +2 -1
  60. package/dist/wirings/ai-agent/index.js +2 -1
  61. package/dist/wirings/ai-agent/voice-input.d.ts +1 -0
  62. package/dist/wirings/ai-agent/voice-input.js +5 -41
  63. package/dist/wirings/ai-agent/voice-output.d.ts +1 -0
  64. package/dist/wirings/channel/channel-common.js +0 -1
  65. package/dist/wirings/channel/channel-handler.js +1 -4
  66. package/dist/wirings/channel/channel-middleware-runner.d.ts +12 -0
  67. package/dist/wirings/channel/channel-middleware-runner.js +34 -19
  68. package/dist/wirings/channel/channel.types.d.ts +2 -6
  69. package/dist/wirings/cli/cli-runner.js +3 -2
  70. package/dist/wirings/cli/cli.types.d.ts +1 -2
  71. package/dist/wirings/cli/command-parser.d.ts +2 -0
  72. package/dist/wirings/cli/command-parser.js +59 -2
  73. package/dist/wirings/credential/credential.types.d.ts +14 -0
  74. package/dist/wirings/credential/validate-credential-definitions.js +1 -0
  75. package/dist/wirings/gateway/gateway-runner.js +100 -50
  76. package/dist/wirings/gateway/gateway.types.d.ts +8 -5
  77. package/dist/wirings/http/http-routes.js +0 -3
  78. package/dist/wirings/http/http-runner.d.ts +9 -29
  79. package/dist/wirings/http/http-runner.js +9 -34
  80. package/dist/wirings/http/http.types.d.ts +4 -12
  81. package/dist/wirings/mcp/mcp-runner.js +0 -2
  82. package/dist/wirings/mcp/mcp.types.d.ts +5 -11
  83. package/dist/wirings/oauth2/index.d.ts +0 -3
  84. package/dist/wirings/oauth2/index.js +1 -2
  85. package/dist/wirings/rpc/addon-runner.d.ts +28 -0
  86. package/dist/wirings/rpc/addon-runner.js +173 -0
  87. package/dist/wirings/rpc/index.d.ts +5 -1
  88. package/dist/wirings/rpc/index.js +3 -1
  89. package/dist/wirings/rpc/remote-addon-auth.d.ts +26 -0
  90. package/dist/wirings/rpc/remote-addon-auth.js +43 -0
  91. package/dist/wirings/rpc/rpc-runner.d.ts +19 -0
  92. package/dist/wirings/rpc/rpc-runner.js +109 -3
  93. package/dist/wirings/rpc/rpc-types.d.ts +15 -1
  94. package/dist/wirings/rpc/wire-addon.js +9 -0
  95. package/dist/wirings/rpc/wire-remote-addon.d.ts +47 -0
  96. package/dist/wirings/rpc/wire-remote-addon.js +19 -0
  97. package/dist/wirings/scope/index.d.ts +3 -0
  98. package/dist/wirings/scope/index.js +2 -0
  99. package/dist/wirings/scope/scope.types.d.ts +39 -0
  100. package/dist/wirings/scope/scope.types.js +1 -0
  101. package/dist/wirings/scope/validate-scope-definitions.d.ts +16 -0
  102. package/dist/wirings/scope/validate-scope-definitions.js +76 -0
  103. package/dist/wirings/scope/wire-scope.d.ts +33 -0
  104. package/dist/wirings/scope/wire-scope.js +32 -0
  105. package/dist/wirings/secret/secret.types.d.ts +14 -0
  106. package/dist/wirings/secret/validate-secret-definitions.js +2 -0
  107. package/dist/wirings/variable/validate-variable-definitions.js +2 -0
  108. package/dist/wirings/variable/variable.types.d.ts +14 -0
  109. package/dist/wirings/workflow/dsl/index.d.ts +1 -1
  110. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +89 -7
  111. package/dist/wirings/workflow/graph/graph-node.d.ts +2 -0
  112. package/dist/wirings/workflow/graph/graph-runner.js +3 -0
  113. package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +6 -0
  114. package/dist/wirings/workflow/graph/wire-workflow-graph.js +1 -0
  115. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +5 -0
  116. package/dist/wirings/workflow/index.d.ts +2 -2
  117. package/dist/wirings/workflow/index.js +1 -1
  118. package/dist/wirings/workflow/pikku-workflow-service.d.ts +59 -1
  119. package/dist/wirings/workflow/pikku-workflow-service.js +187 -0
  120. package/dist/wirings/workflow/workflow.types.d.ts +1 -3
  121. package/package.json +6 -1
  122. package/run-tests.sh +1 -0
  123. package/src/dev/hot-reload.ts +13 -68
  124. package/src/dev/module-runner.test.ts +169 -0
  125. package/src/dev/module-runner.ts +103 -0
  126. package/src/dev/reload-meta.test.ts +31 -2
  127. package/src/dev/reload-meta.ts +26 -0
  128. package/src/errors/errors.ts +24 -0
  129. package/src/function/function-runner.test.ts +276 -142
  130. package/src/function/function-runner.ts +52 -145
  131. package/src/function/functions.types.ts +28 -38
  132. package/src/index.ts +8 -6
  133. package/src/middleware/index.ts +1 -5
  134. package/src/permissions.test.ts +160 -373
  135. package/src/permissions.ts +137 -279
  136. package/src/pikku-state.ts +0 -2
  137. package/src/scopes.test.ts +167 -0
  138. package/src/scopes.ts +69 -0
  139. package/src/services/ai-embedding-service.ts +31 -0
  140. package/src/services/credential-wire-service.ts +10 -4
  141. package/src/services/index.ts +20 -0
  142. package/src/services/meta-service.ts +68 -3
  143. package/src/services/queue-webhook-service.test.ts +408 -0
  144. package/src/services/queue-webhook-service.ts +182 -0
  145. package/src/services/scope-service.ts +75 -0
  146. package/src/services/typed-secret-service.test.ts +35 -0
  147. package/src/services/typed-secret-service.ts +39 -4
  148. package/src/services/webhook-service.ts +180 -0
  149. package/src/services/workflow-service.ts +7 -0
  150. package/src/testing/service-tests.ts +49 -0
  151. package/src/types/core.types.ts +42 -14
  152. package/src/types/state.types.ts +23 -3
  153. package/src/utils/hmac.ts +27 -0
  154. package/src/utils/safe-fetch.test.ts +373 -0
  155. package/src/utils/safe-fetch.ts +213 -0
  156. package/src/wirings/ai-agent/ai-agent-agui.test.ts +1127 -0
  157. package/src/wirings/ai-agent/ai-agent-agui.ts +386 -0
  158. package/src/wirings/ai-agent/ai-agent-authorization.test.ts +204 -0
  159. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +483 -0
  160. package/src/wirings/ai-agent/ai-agent-prepare.ts +324 -13
  161. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +207 -0
  162. package/src/wirings/ai-agent/ai-agent-runner.test.ts +48 -1
  163. package/src/wirings/ai-agent/ai-agent-runner.ts +28 -0
  164. package/src/wirings/ai-agent/ai-agent-stream.test.ts +301 -10
  165. package/src/wirings/ai-agent/ai-agent-stream.ts +122 -16
  166. package/src/wirings/ai-agent/ai-agent.types.ts +57 -0
  167. package/src/wirings/ai-agent/index.ts +5 -0
  168. package/src/wirings/ai-agent/voice-input.test.ts +90 -0
  169. package/src/wirings/ai-agent/voice-input.ts +9 -40
  170. package/src/wirings/channel/channel-common.ts +0 -1
  171. package/src/wirings/channel/channel-handler.ts +1 -9
  172. package/src/wirings/channel/channel-middleware-runner.test.ts +28 -2
  173. package/src/wirings/channel/channel-middleware-runner.ts +39 -27
  174. package/src/wirings/channel/channel.types.ts +0 -6
  175. package/src/wirings/cli/cli-runner.ts +4 -2
  176. package/src/wirings/cli/cli.types.ts +0 -2
  177. package/src/wirings/cli/command-parser.test.ts +130 -0
  178. package/src/wirings/cli/command-parser.ts +80 -2
  179. package/src/wirings/credential/credential.types.ts +14 -0
  180. package/src/wirings/credential/validate-credential-definitions.ts +1 -0
  181. package/src/wirings/gateway/gateway-authorization.test.ts +444 -0
  182. package/src/wirings/gateway/gateway-runner.ts +114 -68
  183. package/src/wirings/gateway/gateway.types.ts +7 -9
  184. package/src/wirings/http/http-routes.ts +0 -3
  185. package/src/wirings/http/http-runner.test.ts +1 -11
  186. package/src/wirings/http/http-runner.ts +16 -39
  187. package/src/wirings/http/http.types.ts +7 -14
  188. package/src/wirings/mcp/mcp-runner.ts +0 -2
  189. package/src/wirings/mcp/mcp.types.ts +3 -11
  190. package/src/wirings/oauth2/index.ts +0 -3
  191. package/src/wirings/rpc/addon-runner.ts +233 -0
  192. package/src/wirings/rpc/index.ts +17 -1
  193. package/src/wirings/rpc/remote-addon-auth.ts +69 -0
  194. package/src/wirings/rpc/rpc-runner.test.ts +319 -16
  195. package/src/wirings/rpc/rpc-runner.ts +155 -3
  196. package/src/wirings/rpc/rpc-types.ts +16 -5
  197. package/src/wirings/rpc/wire-addon.test.ts +6 -3
  198. package/src/wirings/rpc/wire-addon.ts +9 -0
  199. package/src/wirings/rpc/wire-remote-addon.ts +57 -0
  200. package/src/wirings/scope/index.ts +14 -0
  201. package/src/wirings/scope/scope.test.ts +135 -0
  202. package/src/wirings/scope/scope.types.ts +44 -0
  203. package/src/wirings/scope/validate-scope-definitions.ts +110 -0
  204. package/src/wirings/scope/wire-scope.ts +34 -0
  205. package/src/wirings/secret/secret.types.ts +14 -0
  206. package/src/wirings/secret/validate-secret-definitions.ts +2 -0
  207. package/src/wirings/variable/validate-variable-definitions.ts +2 -0
  208. package/src/wirings/variable/variable.types.ts +14 -0
  209. package/src/wirings/workflow/dsl/index.ts +4 -0
  210. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +97 -6
  211. package/src/wirings/workflow/graph/graph-node.ts +2 -0
  212. package/src/wirings/workflow/graph/graph-runner.test.ts +58 -0
  213. package/src/wirings/workflow/graph/graph-runner.ts +3 -0
  214. package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
  215. package/src/wirings/workflow/graph/workflow-graph.types.ts +5 -0
  216. package/src/wirings/workflow/index.ts +5 -0
  217. package/src/wirings/workflow/pikku-workflow-service.test.ts +341 -0
  218. package/src/wirings/workflow/pikku-workflow-service.ts +272 -0
  219. package/src/wirings/workflow/workflow-on-error.test.ts +154 -0
  220. package/src/wirings/workflow/workflow-step-session.test.ts +21 -5
  221. package/src/wirings/workflow/workflow.types.ts +4 -2
  222. package/tsconfig.tsbuildinfo +1 -1
  223. package/src/wirings/oauth2/oauth2-client.test.ts +0 -930
  224. package/src/wirings/oauth2/oauth2-client.ts +0 -331
  225. package/src/wirings/oauth2/oauth2-routes.ts +0 -234
@@ -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: {
@@ -190,7 +211,7 @@ export type AIAgentMemoryConfig = {
190
211
  lastMessages?: number;
191
212
  workingMemory?: unknown;
192
213
  };
193
- export type CoreAIAgent<PikkuPermission = CorePikkuPermission<any, any>, PikkuMiddleware = CorePikkuMiddleware<any>> = {
214
+ export type CoreAIAgent<PikkuPermission = CorePikkuPermission<any, any>, PikkuMiddleware = CorePikkuMiddleware<any>, Scope extends string = string> = {
194
215
  name: string;
195
216
  description: string;
196
217
  summary?: string;
@@ -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;
@@ -220,6 +244,23 @@ export type CoreAIAgent<PikkuPermission = CorePikkuPermission<any, any>, PikkuMi
220
244
  middleware?: PikkuMiddleware[];
221
245
  channelMiddleware?: CorePikkuChannelMiddleware<any, any>[];
222
246
  aiMiddleware?: PikkuAIMiddlewareHooks<any, any>[];
247
+ /**
248
+ * Whether a session is required to run this agent. Defaults to `false`, since
249
+ * agents are commonly invoked from an already-authenticated `pikkuFunc` or
250
+ * from genuinely sessionless contexts (crons, queue workers). Set `true` to
251
+ * require a session at the agent itself. `scopes` and `permissions` are
252
+ * enforced either way.
253
+ */
254
+ auth?: boolean;
255
+ /**
256
+ * Scopes the session must hold to run this agent. All of them are required
257
+ * (AND), and they are checked before `permissions` — unlike permissions,
258
+ * which OR together, a scope can only narrow access.
259
+ *
260
+ * Narrowed to the generated `ScopeId` union in a project's own
261
+ * `pikku-types.gen.ts`, so an undeclared scope is a compile error.
262
+ */
263
+ scopes?: Scope[];
223
264
  permissions?: CorePermissionGroup<PikkuPermission>;
224
265
  };
225
266
  export type AIStreamEvent = {
@@ -267,6 +308,7 @@ export type AIStreamEvent = {
267
308
  toolName: string;
268
309
  args: unknown;
269
310
  reason?: string;
311
+ runId?: string;
270
312
  agent?: string;
271
313
  session?: string;
272
314
  } | {
@@ -388,6 +430,17 @@ export interface AgentRunService {
388
430
  listThreads(options?: {
389
431
  agentName?: string;
390
432
  resourceId?: string;
433
+ /**
434
+ * Restrict results to threads owned by one of these session principals. A
435
+ * thread matches when its `resourceId` is the principal itself or one of its
436
+ * `principal:` sub-partitions, mirroring the composition
437
+ * `resolveOwnerResourceId` writes.
438
+ *
439
+ * Unlike `resourceId`, which is an optional exact-match filter, this is an
440
+ * authorization constraint: an empty array matches nothing. Callers exposing
441
+ * threads over the wire must derive it from the session, never from input.
442
+ */
443
+ owners?: string[];
391
444
  limit?: number;
392
445
  offset?: number;
393
446
  }): Promise<AIThread[]>;
@@ -397,9 +450,10 @@ export interface AgentRunService {
397
450
  deleteThread(threadId: string): Promise<boolean>;
398
451
  getDistinctAgentNames(): Promise<string[]>;
399
452
  }
400
- export type AIAgentMeta = Record<string, Omit<CoreAIAgent, 'input' | 'output' | 'tools' | 'agents' | 'middleware' | 'channelMiddleware' | 'aiMiddleware' | 'permissions'> & {
453
+ export type AIAgentMeta = Record<string, Omit<CoreAIAgent, 'input' | 'output' | 'tools' | 'agents' | 'workflows' | 'middleware' | 'channelMiddleware' | 'aiMiddleware' | 'permissions'> & {
401
454
  tools?: string[];
402
455
  agents?: string[];
456
+ workflows?: string[];
403
457
  inputSchema: string | null;
404
458
  outputSchema: string | null;
405
459
  workingMemorySchema: string | null;
@@ -1,8 +1,9 @@
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';
5
6
  export { voiceOutput } from './voice-output.js';
6
- export { type RunAIAgentParams, type StreamAIAgentOptions, ToolApprovalRequired, ToolCredentialRequired, } from './ai-agent-prepare.js';
7
+ export { type RunAIAgentParams, type StreamAIAgentOptions, ToolApprovalRequired, ToolCredentialRequired, canAccessThread, isOwnedByPrincipal, sessionPrincipals, threadOwnerConstraint, } from './ai-agent-prepare.js';
7
8
  export { addAIAgent, approveAIAgent, getAIAgents, getAIAgentsMeta, } from './ai-agent-registry.js';
8
9
  export type { AIAgentInput, AIAgentInputAttachment, AIAgentMeta, AIAgentMemoryConfig, AIAgentStep, AIContentPart, AgentRunRow, AgentRunService, AgentRunState, AIMessage, AIStreamChannel, AIStreamEvent, AIThread, CoreAIAgent, PendingApproval, PikkuAIMiddlewareHooks, } from './ai-agent.types.js';
@@ -1,7 +1,8 @@
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';
5
6
  export { voiceOutput } from './voice-output.js';
6
- export { ToolApprovalRequired, ToolCredentialRequired, } from './ai-agent-prepare.js';
7
+ export { ToolApprovalRequired, ToolCredentialRequired, canAccessThread, isOwnedByPrincipal, sessionPrincipals, threadOwnerConstraint, } from './ai-agent-prepare.js';
7
8
  export { addAIAgent, approveAIAgent, getAIAgents, getAIAgentsMeta, } from './ai-agent-registry.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');
@@ -59,8 +23,8 @@ async function fetchAsUint8Array(url, allowedAudioHosts) {
59
23
  }
60
24
  export const voiceInput = (config) => pikkuAIMiddleware({
61
25
  modifyInput: async (services, { messages, instructions }) => {
62
- const transcribeAudio = services.aiAgentRunner?.transcribe;
63
- if (!transcribeAudio)
26
+ const aiAgentRunner = services.aiAgentRunner;
27
+ if (!aiAgentRunner?.transcribe)
64
28
  return { messages, instructions };
65
29
  const last = messages[messages.length - 1];
66
30
  if (!last || last.role !== 'user' || typeof last.content === 'string') {
@@ -87,7 +51,7 @@ export const voiceInput = (config) => pikkuAIMiddleware({
87
51
  const audioData = p.data
88
52
  ? base64ToUint8Array(p.data)
89
53
  : await fetchAsUint8Array(p.url, config.allowedAudioHosts);
90
- const result = await transcribeAudio({
54
+ const result = await aiAgentRunner.transcribe({
91
55
  model: config.model,
92
56
  audio: audioData,
93
57
  ...(config.language
@@ -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,
@@ -321,6 +319,9 @@ export async function executeCLI({ programName, args, createConfig, createSingle
321
319
  console.log(helpText);
322
320
  return;
323
321
  }
322
+ // Non-fatal diagnostics (unknown options are still accepted) go to stderr
323
+ // so they never pollute a command's machine-readable stdout.
324
+ parsed.warnings.forEach((warning) => console.error(`Warning: ${warning}`));
324
325
  if (parsed.errors.length > 0) {
325
326
  // Check if any error is about an unknown command
326
327
  const hasUnknownCommand = parsed.errors.some((error) => error.startsWith('Unknown command:') ||
@@ -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
  /**
@@ -8,6 +8,8 @@ export interface ParsedCommand {
8
8
  positionals: Record<string, any>;
9
9
  options: Record<string, any>;
10
10
  errors: string[];
11
+ /** Non-fatal diagnostics (e.g. unknown options that were accepted+ignored) */
12
+ warnings: string[];
11
13
  }
12
14
  /**
13
15
  * Parses raw CLI arguments into structured data for a specific program
@@ -8,6 +8,54 @@ function toCamelCase(str) {
8
8
  function toKebabCase(str) {
9
9
  return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
10
10
  }
11
+ /** Options the runner handles itself — never reported as unknown. */
12
+ const RESERVED_OPTIONS = new Set(['help']);
13
+ /** Levenshtein distance, capped-free and dependency-free. Used only to suggest
14
+ * a near-miss option name, so the naive O(n*m) implementation is fine. */
15
+ function levenshtein(a, b) {
16
+ if (a === b)
17
+ return 0;
18
+ if (a.length === 0)
19
+ return b.length;
20
+ if (b.length === 0)
21
+ return a.length;
22
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
23
+ for (let i = 1; i <= a.length; i++) {
24
+ const row = [i];
25
+ for (let j = 1; j <= b.length; j++) {
26
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
27
+ row[j] = Math.min(row[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
28
+ }
29
+ prev = row;
30
+ }
31
+ return prev[b.length];
32
+ }
33
+ /** Finds the closest declared option (distance <= 2) to what the user typed.
34
+ * Compares against the kebab-case rendering, since that is what is typed. */
35
+ function suggestOption(typed, availableOptions) {
36
+ let best = null;
37
+ let bestDistance = 3;
38
+ for (const name of Object.keys(availableOptions)) {
39
+ const kebab = toKebabCase(name);
40
+ const distance = Math.min(levenshtein(typed, kebab), levenshtein(typed, name));
41
+ if (distance < bestDistance) {
42
+ bestDistance = distance;
43
+ best = kebab;
44
+ }
45
+ }
46
+ return best;
47
+ }
48
+ /** Records a warning that an unknown long option was accepted but ignored.
49
+ * Unknown options stay non-fatal for forward compatibility (a newer command
50
+ * version may understand them) — they are just no longer silent. */
51
+ function warnUnknownOption(typed, availableOptions, result) {
52
+ if (RESERVED_OPTIONS.has(toCamelCase(typed))) {
53
+ return;
54
+ }
55
+ const suggestion = suggestOption(typed, availableOptions);
56
+ result.warnings.push(`Unknown option: --${typed} (ignored)` +
57
+ (suggestion ? ` Did you mean --${suggestion}?` : ''));
58
+ }
11
59
  /**
12
60
  * Parses raw CLI arguments into structured data for a specific program
13
61
  */
@@ -18,6 +66,7 @@ export function parseCLIArguments(args, programName, allMeta) {
18
66
  positionals: {},
19
67
  options: {},
20
68
  errors: [],
69
+ warnings: [],
21
70
  };
22
71
  const meta = allMeta.programs[programName];
23
72
  if (!meta) {
@@ -106,7 +155,11 @@ export function parseCLIArguments(args, programName, allMeta) {
106
155
  // --option=value format
107
156
  const key = toCamelCase(arg.slice(2, equalIndex));
108
157
  const optionDef = availableOptions[key];
109
- // Unknown options are allowed for forward compatibility
158
+ // Unknown options are allowed for forward compatibility, but warned
159
+ // about so they are not silently dropped by the input schema.
160
+ if (!optionDef) {
161
+ warnUnknownOption(arg.slice(2, equalIndex), availableOptions, result);
162
+ }
110
163
  const value = arg.slice(equalIndex + 1);
111
164
  optionArgs[key] = parseOptionValue(value, optionDef);
112
165
  }
@@ -114,7 +167,11 @@ export function parseCLIArguments(args, programName, allMeta) {
114
167
  // --option value format
115
168
  const key = toCamelCase(arg.slice(2));
116
169
  const optionDef = availableOptions[key];
117
- // Unknown options are allowed for forward compatibility
170
+ // Unknown options are allowed for forward compatibility, but warned
171
+ // about so they are not silently dropped by the input schema.
172
+ if (!optionDef) {
173
+ warnUnknownOption(arg.slice(2), availableOptions, result);
174
+ }
118
175
  if (optionDef && optionDef.array) {
119
176
  // Array option - collect all following non-flag values
120
177
  currentIndex++;
@@ -5,6 +5,13 @@ export type CoreCredential<T = unknown> = {
5
5
  description?: string;
6
6
  type: 'singleton' | 'wire';
7
7
  schema: T;
8
+ /**
9
+ * Link to documentation explaining how to obtain this value — a provider's
10
+ * API-key page, a setup guide, an internal runbook. Surfaced by consoles and
11
+ * deploy UIs so a user facing a missing value has somewhere to go instead of
12
+ * an opaque identifier.
13
+ */
14
+ docsUrl?: string;
8
15
  oauth2?: OAuth2CredentialConfig & {
9
16
  appCredentialSecretId: string;
10
17
  };
@@ -15,6 +22,13 @@ export type CredentialDefinitionMeta = {
15
22
  description?: string;
16
23
  type: 'singleton' | 'wire';
17
24
  schema?: Record<string, unknown> | string;
25
+ /**
26
+ * Link to documentation explaining how to obtain this value — a provider's
27
+ * API-key page, a setup guide, an internal runbook. Surfaced by consoles and
28
+ * deploy UIs so a user facing a missing value has somewhere to go instead of
29
+ * an opaque identifier.
30
+ */
31
+ docsUrl?: string;
18
32
  oauth2?: OAuth2CredentialConfig & {
19
33
  appCredentialSecretId: string;
20
34
  };
@@ -31,6 +31,7 @@ export function validateAndBuildCredentialDefinitionsMeta(definitions, schemaLoo
31
31
  type: def.type,
32
32
  schema: def.schema,
33
33
  oauth2: def.oauth2,
34
+ docsUrl: def.docsUrl,
34
35
  sourceFile: def.sourceFile,
35
36
  };
36
37
  }