veryfront 0.1.62 → 0.1.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 (73) hide show
  1. package/esm/cli/templates/manifest.js +37 -37
  2. package/esm/deno.d.ts +3 -0
  3. package/esm/deno.js +6 -3
  4. package/esm/src/agent/composition/composition.d.ts.map +1 -1
  5. package/esm/src/agent/composition/composition.js +13 -3
  6. package/esm/src/agent/factory.d.ts.map +1 -1
  7. package/esm/src/agent/factory.js +3 -3
  8. package/esm/src/agent/middleware/security/validator.d.ts +92 -0
  9. package/esm/src/agent/middleware/security/validator.d.ts.map +1 -0
  10. package/esm/src/agent/middleware/security/validator.js +187 -0
  11. package/esm/src/agent/runtime/index.d.ts +3 -2
  12. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  13. package/esm/src/agent/runtime/index.js +16 -8
  14. package/esm/src/agent/types.d.ts +4 -0
  15. package/esm/src/agent/types.d.ts.map +1 -1
  16. package/esm/src/channels/invoke.d.ts +491 -0
  17. package/esm/src/channels/invoke.d.ts.map +1 -0
  18. package/esm/src/channels/invoke.js +417 -0
  19. package/esm/src/embedding/embedding.js +2 -2
  20. package/esm/src/integrations/endpoint-executor.d.ts +1 -0
  21. package/esm/src/integrations/endpoint-executor.d.ts.map +1 -1
  22. package/esm/src/integrations/endpoint-executor.js +44 -0
  23. package/esm/src/integrations/schema.d.ts +2 -2
  24. package/esm/src/oauth/handlers/init-handler.d.ts +6 -2
  25. package/esm/src/oauth/handlers/init-handler.d.ts.map +1 -1
  26. package/esm/src/oauth/handlers/init-handler.js +8 -2
  27. package/esm/src/platform/compat/opaque-deps.d.ts.map +1 -1
  28. package/esm/src/platform/compat/opaque-deps.js +10 -1
  29. package/esm/src/prompt/factory.d.ts.map +1 -1
  30. package/esm/src/prompt/factory.js +9 -1
  31. package/esm/src/react/components/ai/markdown.d.ts.map +1 -1
  32. package/esm/src/react/components/ai/markdown.js +4 -4
  33. package/esm/src/server/handlers/dev/framework-candidates.generated.d.ts.map +1 -1
  34. package/esm/src/server/handlers/dev/framework-candidates.generated.js +5 -4
  35. package/esm/src/server/handlers/preview/markdown-html-generator.js +1 -1
  36. package/esm/src/server/handlers/request/api/api-handler-wrapper.d.ts.map +1 -1
  37. package/esm/src/server/handlers/request/api/api-handler-wrapper.js +1 -74
  38. package/esm/src/server/handlers/request/api/project-discovery.d.ts +9 -0
  39. package/esm/src/server/handlers/request/api/project-discovery.d.ts.map +1 -0
  40. package/esm/src/server/handlers/request/api/project-discovery.js +74 -0
  41. package/esm/src/server/handlers/request/channel-assistants.handler.d.ts +11 -0
  42. package/esm/src/server/handlers/request/channel-assistants.handler.d.ts.map +1 -0
  43. package/esm/src/server/handlers/request/channel-assistants.handler.js +71 -0
  44. package/esm/src/server/handlers/request/channel-invoke.handler.d.ts +11 -0
  45. package/esm/src/server/handlers/request/channel-invoke.handler.d.ts.map +1 -0
  46. package/esm/src/server/handlers/request/channel-invoke.handler.js +72 -0
  47. package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
  48. package/esm/src/server/runtime-handler/index.js +4 -0
  49. package/esm/src/transforms/md/compiler/md-compiler.d.ts.map +1 -1
  50. package/esm/src/transforms/md/compiler/md-compiler.js +25 -1
  51. package/package.json +3 -1
  52. package/src/cli/templates/manifest.js +37 -37
  53. package/src/deno.js +6 -3
  54. package/src/src/agent/composition/composition.ts +15 -3
  55. package/src/src/agent/factory.ts +19 -6
  56. package/src/src/agent/middleware/security/validator.ts +288 -0
  57. package/src/src/agent/runtime/index.ts +26 -3
  58. package/src/src/agent/types.ts +4 -0
  59. package/src/src/channels/invoke.ts +546 -0
  60. package/src/src/embedding/embedding.ts +2 -2
  61. package/src/src/integrations/endpoint-executor.ts +51 -0
  62. package/src/src/oauth/handlers/init-handler.ts +20 -5
  63. package/src/src/platform/compat/opaque-deps.ts +19 -4
  64. package/src/src/prompt/factory.ts +10 -1
  65. package/src/src/react/components/ai/markdown.tsx +5 -4
  66. package/src/src/server/handlers/dev/framework-candidates.generated.ts +5 -4
  67. package/src/src/server/handlers/preview/markdown-html-generator.ts +1 -1
  68. package/src/src/server/handlers/request/api/api-handler-wrapper.ts +1 -85
  69. package/src/src/server/handlers/request/api/project-discovery.ts +86 -0
  70. package/src/src/server/handlers/request/channel-assistants.handler.ts +94 -0
  71. package/src/src/server/handlers/request/channel-invoke.handler.ts +95 -0
  72. package/src/src/server/runtime-handler/index.ts +4 -0
  73. package/src/src/transforms/md/compiler/md-compiler.ts +27 -1
@@ -0,0 +1,546 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
2
+ import type { Agent, AgentMessage as Message, AgentResponse } from "../agent/index.js";
3
+ import { fromError } from "../errors/veryfront-error.js";
4
+ import type { HandlerContext } from "../types/index.js";
5
+ import { serverLogger } from "../utils/index.js";
6
+ import { base64urlEncodeBytes } from "../utils/base64url.js";
7
+ import { z } from "zod";
8
+ import {
9
+ getAgent as getRegisteredAgent,
10
+ getAllAgentIds as getRegisteredAgentIds,
11
+ } from "../agent/composition/composition.js";
12
+ import { ensureProjectDiscovery as ensureProjectDiscoveryForProject } from "../server/handlers/request/api/project-discovery.js";
13
+
14
+ const logger = serverLogger.component("channels-invoke");
15
+ const SIGNATURE_SKEW_SECONDS = 5;
16
+
17
+ const rawHistoryPartSchema = z.object({
18
+ type: z.string(),
19
+ }).passthrough();
20
+
21
+ const channelAttachmentSchema = z.object({
22
+ id: z.string(),
23
+ kind: z.enum(["image", "file"]),
24
+ filename: z.string().optional(),
25
+ mediaType: z.string().optional(),
26
+ privateUrl: z.string().optional(),
27
+ });
28
+
29
+ const channelInvokeHistoryMessageSchema = z.object({
30
+ id: z.string(),
31
+ role: z.enum(["user", "assistant", "system", "tool"]),
32
+ parts: z.array(rawHistoryPartSchema),
33
+ metadata: z.record(z.unknown()).optional(),
34
+ createdAt: z.string().optional(),
35
+ });
36
+
37
+ const channelInvokeRequestWireSchema = z.object({
38
+ dispatchId: z.string().min(1),
39
+ conversationId: z.string().min(1),
40
+ projectId: z.string().min(1),
41
+ assistantId: z.string().min(1),
42
+ platform: z.literal("slack"),
43
+ inboundMessage: z.object({
44
+ text: z.string(),
45
+ userId: z.string(),
46
+ userName: z.string(),
47
+ isDirectMessage: z.boolean(),
48
+ attachments: z.array(channelAttachmentSchema).optional(),
49
+ }),
50
+ conversationHistory: z.array(channelInvokeHistoryMessageSchema),
51
+ generation: z.object({
52
+ maxResponseTokens: z.number().int().positive().max(16384).optional(),
53
+ }).optional(),
54
+ });
55
+
56
+ export const ChannelInvokeRequestSchema = channelInvokeRequestWireSchema;
57
+
58
+ export const ChannelAssistantsRequestSchema = z.object({
59
+ requestId: z.string().min(1),
60
+ projectId: z.string().min(1),
61
+ platform: z.literal("slack"),
62
+ });
63
+
64
+ export const ChannelAssistantSchema = z.object({
65
+ id: z.string().min(1),
66
+ name: z.string().min(1),
67
+ description: z.string().nullable().optional(),
68
+ model: z.string().nullable().optional(),
69
+ });
70
+
71
+ export const ChannelAssistantsResponseSchema = z.object({
72
+ assistants: z.array(ChannelAssistantSchema),
73
+ });
74
+
75
+ const channelTextPartSchema = z.object({
76
+ type: z.literal("text"),
77
+ text: z.string(),
78
+ });
79
+
80
+ const channelToolCallPartSchema = z.object({
81
+ type: z.literal("tool_call"),
82
+ id: z.string(),
83
+ name: z.string(),
84
+ input: z.record(z.unknown()),
85
+ state: z.enum(["streaming", "pending", "completed", "error"]),
86
+ });
87
+
88
+ const channelToolResultPartSchema = z.object({
89
+ type: z.literal("tool_result"),
90
+ tool_call_id: z.string(),
91
+ output: z.unknown(),
92
+ is_error: z.boolean().optional(),
93
+ });
94
+
95
+ const channelReasoningPartSchema = z.object({
96
+ type: z.literal("reasoning"),
97
+ text: z.string(),
98
+ });
99
+
100
+ const channelErrorPartSchema = z.object({
101
+ type: z.literal("error"),
102
+ code: z.string(),
103
+ message: z.string(),
104
+ });
105
+
106
+ export const ChannelResponsePartSchema = z.discriminatedUnion("type", [
107
+ channelTextPartSchema,
108
+ channelToolCallPartSchema,
109
+ channelToolResultPartSchema,
110
+ channelReasoningPartSchema,
111
+ channelErrorPartSchema,
112
+ ]);
113
+
114
+ export const ChannelInvokeResponseSchema = z.object({
115
+ ignored: z.boolean(),
116
+ responseParts: z.array(ChannelResponsePartSchema).optional(),
117
+ tokenUsage: z.object({
118
+ inputTokens: z.number().int().nonnegative().optional(),
119
+ outputTokens: z.number().int().nonnegative().optional(),
120
+ totalTokens: z.number().int().nonnegative().optional(),
121
+ }).optional(),
122
+ error: z.object({
123
+ code: z.enum(["provider_error", "internal_error"]),
124
+ retryable: z.boolean(),
125
+ }).optional(),
126
+ });
127
+
128
+ const dispatchHeaderSchema = z.object({
129
+ alg: z.literal("EdDSA"),
130
+ typ: z.string().optional(),
131
+ kid: z.string().optional(),
132
+ });
133
+
134
+ const dispatchClaimsSchema = z.object({
135
+ iss: z.string(),
136
+ aud: z.string(),
137
+ sub: z.string(),
138
+ project_id: z.string(),
139
+ platform: z.string(),
140
+ body_sha256: z.string(),
141
+ iat: z.number().int(),
142
+ exp: z.number().int(),
143
+ });
144
+
145
+ export type ChannelInvokeRequest = z.infer<typeof ChannelInvokeRequestSchema>;
146
+ export type ChannelInvokeResponse = z.infer<typeof ChannelInvokeResponseSchema>;
147
+ export type ChannelAssistantsRequest = z.infer<typeof ChannelAssistantsRequestSchema>;
148
+ export type ChannelAssistantsResponse = z.infer<typeof ChannelAssistantsResponseSchema>;
149
+
150
+ type ChannelResponsePart = z.infer<typeof ChannelResponsePartSchema>;
151
+ type DispatchClaims = z.infer<typeof dispatchClaimsSchema>;
152
+ type ChannelAssistant = z.infer<typeof ChannelAssistantSchema>;
153
+
154
+ export interface ChannelInvokeDeps {
155
+ ensureProjectDiscovery: (ctx: HandlerContext) => Promise<void>;
156
+ getAgent: (id: string) => Agent | undefined;
157
+ getAllAgentIds: () => string[];
158
+ }
159
+
160
+ export const defaultChannelInvokeDeps: ChannelInvokeDeps = {
161
+ ensureProjectDiscovery: ensureProjectDiscoveryForProject,
162
+ getAgent: getRegisteredAgent,
163
+ getAllAgentIds: getRegisteredAgentIds,
164
+ };
165
+
166
+ function getAssistantMetadata(agent: Agent): ChannelAssistant {
167
+ const rawConfig = agent.config as unknown as Record<string, unknown>;
168
+
169
+ return ChannelAssistantSchema.parse({
170
+ id: agent.id,
171
+ name: typeof rawConfig.name === "string" && rawConfig.name.trim().length > 0
172
+ ? rawConfig.name
173
+ : agent.id,
174
+ description: typeof rawConfig.description === "string" ? rawConfig.description : null,
175
+ model: agent.config.model ?? null,
176
+ });
177
+ }
178
+
179
+ export async function listChannelAssistants(
180
+ ctx: HandlerContext,
181
+ deps: ChannelInvokeDeps,
182
+ ): Promise<ChannelAssistantsResponse> {
183
+ await deps.ensureProjectDiscovery(ctx);
184
+
185
+ const assistants = deps.getAllAgentIds()
186
+ .map((id) => deps.getAgent(id))
187
+ .filter((agent): agent is Agent => Boolean(agent))
188
+ .map(getAssistantMetadata)
189
+ .sort((left, right) => left.name.localeCompare(right.name));
190
+
191
+ return ChannelAssistantsResponseSchema.parse({ assistants });
192
+ }
193
+
194
+ function base64urlDecodeToBytes(input: string): ArrayBuffer {
195
+ const normalized = input
196
+ .replaceAll("-", "+")
197
+ .replaceAll("_", "/")
198
+ .padEnd(Math.ceil(input.length / 4) * 4, "=");
199
+
200
+ return toArrayBuffer(Uint8Array.from(atob(normalized), (char) => char.charCodeAt(0)));
201
+ }
202
+
203
+ function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
204
+ const buffer = new ArrayBuffer(bytes.byteLength);
205
+ new Uint8Array(buffer).set(bytes);
206
+ return buffer;
207
+ }
208
+
209
+ function pemToDer(pem: string, label: string): ArrayBuffer {
210
+ const body = pem
211
+ .replace(`-----BEGIN ${label}-----`, "")
212
+ .replace(`-----END ${label}-----`, "")
213
+ .replace(/\s/g, "");
214
+
215
+ return toArrayBuffer(Uint8Array.from(atob(body), (char) => char.charCodeAt(0)));
216
+ }
217
+
218
+ async function importEd25519PublicKey(pem: string): Promise<dntShim.CryptoKey> {
219
+ return dntShim.crypto.subtle.importKey(
220
+ "spki",
221
+ pemToDer(pem, "PUBLIC KEY"),
222
+ "Ed25519",
223
+ false,
224
+ ["verify"],
225
+ );
226
+ }
227
+
228
+ async function sha256Base64url(body: string): Promise<string> {
229
+ const hash = await dntShim.crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
230
+ return base64urlEncodeBytes(new Uint8Array(hash));
231
+ }
232
+
233
+ export async function verifyDispatchJws(
234
+ jws: string,
235
+ body: string,
236
+ options: {
237
+ audience: string;
238
+ publicKeyPem: string;
239
+ maxAgeSeconds: number;
240
+ expectedProjectId?: string;
241
+ },
242
+ ): Promise<DispatchClaims> {
243
+ const parts = jws.split(".");
244
+ if (parts.length !== 3) {
245
+ throw new Error("Channel dispatch signature must be a compact JWS");
246
+ }
247
+
248
+ const encodedHeader = parts[0];
249
+ const encodedPayload = parts[1];
250
+ const encodedSignature = parts[2];
251
+ if (!encodedHeader || !encodedPayload || !encodedSignature) {
252
+ throw new Error("Channel dispatch signature must include header, payload, and signature");
253
+ }
254
+ const header = dispatchHeaderSchema.parse(
255
+ JSON.parse(new TextDecoder().decode(base64urlDecodeToBytes(encodedHeader))),
256
+ );
257
+ const claims = dispatchClaimsSchema.parse(
258
+ JSON.parse(new TextDecoder().decode(base64urlDecodeToBytes(encodedPayload))),
259
+ );
260
+
261
+ if (header.alg !== "EdDSA") {
262
+ throw new Error("Unsupported channel dispatch JWS algorithm");
263
+ }
264
+
265
+ const signingInput = new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`);
266
+ const signature = base64urlDecodeToBytes(encodedSignature);
267
+ const publicKey = await importEd25519PublicKey(options.publicKeyPem);
268
+ const verified = await dntShim.crypto.subtle.verify("Ed25519", publicKey, signature, signingInput);
269
+
270
+ if (!verified) {
271
+ throw new Error("Channel dispatch signature verification failed");
272
+ }
273
+
274
+ if (claims.aud !== options.audience) {
275
+ throw new Error("Channel dispatch audience mismatch");
276
+ }
277
+
278
+ if (options.expectedProjectId && claims.project_id !== options.expectedProjectId) {
279
+ throw new Error("Channel dispatch project mismatch");
280
+ }
281
+
282
+ const now = Math.floor(Date.now() / 1000);
283
+ if (claims.exp <= now) {
284
+ throw new Error("Channel dispatch signature expired");
285
+ }
286
+
287
+ if (claims.iat > now + SIGNATURE_SKEW_SECONDS) {
288
+ throw new Error("Channel dispatch signature issued in the future");
289
+ }
290
+
291
+ if (now - claims.iat > options.maxAgeSeconds) {
292
+ throw new Error("Channel dispatch signature is too old");
293
+ }
294
+
295
+ const bodySha256 = await sha256Base64url(body);
296
+ if (claims.body_sha256 !== bodySha256) {
297
+ throw new Error("Channel dispatch body hash mismatch");
298
+ }
299
+
300
+ return claims;
301
+ }
302
+
303
+ function normalizeConversationPart(
304
+ part: z.infer<typeof rawHistoryPartSchema>,
305
+ ): Message["parts"][number] | null {
306
+ if (part.type === "text" && typeof part.text === "string") {
307
+ return { type: "text", text: part.text };
308
+ }
309
+
310
+ if (
311
+ part.type === "tool_call" &&
312
+ typeof part.id === "string" &&
313
+ typeof part.name === "string" &&
314
+ part.input &&
315
+ typeof part.input === "object" &&
316
+ !Array.isArray(part.input)
317
+ ) {
318
+ return {
319
+ type: `tool-${part.name}`,
320
+ toolCallId: part.id,
321
+ toolName: part.name,
322
+ args: part.input as Record<string, unknown>,
323
+ };
324
+ }
325
+
326
+ if (part.type === "tool_result" && typeof part.tool_call_id === "string") {
327
+ return {
328
+ type: "tool-result",
329
+ toolCallId: part.tool_call_id,
330
+ toolName: typeof part.tool_name === "string" ? part.tool_name : "unknown",
331
+ result: "output" in part ? part.output : undefined,
332
+ };
333
+ }
334
+
335
+ return null;
336
+ }
337
+
338
+ export function normalizeConversationHistoryForRuntime(
339
+ messages: ChannelInvokeRequest["conversationHistory"],
340
+ ): Message[] {
341
+ return messages.map((message) => ({
342
+ id: message.id,
343
+ role: message.role,
344
+ parts: message.parts
345
+ .map((part) => normalizeConversationPart(part))
346
+ .filter((part): part is NonNullable<typeof part> => part !== null),
347
+ ...(message.createdAt ? { timestamp: Date.parse(message.createdAt) || undefined } : {}),
348
+ ...(message.metadata ? { metadata: message.metadata } : {}),
349
+ }));
350
+ }
351
+
352
+ export function resolveChannelInvokeAgent(
353
+ assistantId: string,
354
+ deps: Pick<ChannelInvokeDeps, "getAgent" | "getAllAgentIds">,
355
+ ): Agent | undefined {
356
+ return deps.getAgent(assistantId);
357
+ }
358
+
359
+ function normalizeToolCallState(status: string): "pending" | "completed" | "error" {
360
+ switch (status) {
361
+ case "completed":
362
+ return "completed";
363
+ case "error":
364
+ return "error";
365
+ default:
366
+ return "pending";
367
+ }
368
+ }
369
+
370
+ function convertAssistantPartToChannelResponsePart(
371
+ part: Message["parts"][number],
372
+ knownToolCallIds: Set<string>,
373
+ ): ChannelResponsePart | null {
374
+ if (part.type === "text" && "text" in part) {
375
+ return channelTextPartSchema.parse({
376
+ type: "text",
377
+ text: part.text,
378
+ });
379
+ }
380
+
381
+ const isToolCallPart = part.type === "tool-call" ||
382
+ (part.type.startsWith("tool-") && part.type !== "tool-result");
383
+ if (
384
+ isToolCallPart &&
385
+ "toolCallId" in part &&
386
+ "toolName" in part &&
387
+ !knownToolCallIds.has(part.toolCallId)
388
+ ) {
389
+ return channelToolCallPartSchema.parse({
390
+ type: "tool_call",
391
+ id: part.toolCallId,
392
+ name: part.toolName,
393
+ input: "args" in part ? part.args : ("input" in part ? part.input : {}),
394
+ state: "pending",
395
+ });
396
+ }
397
+
398
+ return null;
399
+ }
400
+
401
+ function findLastAssistantMessage(messages: Message[]): Message | undefined {
402
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
403
+ if (messages[index]?.role === "assistant") {
404
+ return messages[index];
405
+ }
406
+ }
407
+
408
+ return undefined;
409
+ }
410
+
411
+ export function buildChannelResponseParts(response: AgentResponse): ChannelResponsePart[] {
412
+ const responseParts: ChannelResponsePart[] = [];
413
+ const knownToolCallIds = new Set<string>();
414
+
415
+ if (response.thinking?.trim()) {
416
+ responseParts.push(channelReasoningPartSchema.parse({
417
+ type: "reasoning",
418
+ text: response.thinking,
419
+ }));
420
+ }
421
+
422
+ for (const toolCall of response.toolCalls) {
423
+ knownToolCallIds.add(toolCall.id);
424
+ responseParts.push(channelToolCallPartSchema.parse({
425
+ type: "tool_call",
426
+ id: toolCall.id,
427
+ name: toolCall.name,
428
+ input: toolCall.args,
429
+ state: normalizeToolCallState(toolCall.status),
430
+ }));
431
+
432
+ if (toolCall.status === "completed" || toolCall.status === "error") {
433
+ responseParts.push(channelToolResultPartSchema.parse({
434
+ type: "tool_result",
435
+ tool_call_id: toolCall.id,
436
+ output: toolCall.status === "error"
437
+ ? { error: toolCall.error ?? "Tool execution failed" }
438
+ : toolCall.result,
439
+ ...(toolCall.status === "error" ? { is_error: true } : {}),
440
+ }));
441
+ }
442
+ }
443
+
444
+ const lastAssistantMessage = findLastAssistantMessage(response.messages);
445
+ if (lastAssistantMessage) {
446
+ for (const part of lastAssistantMessage.parts) {
447
+ const converted = convertAssistantPartToChannelResponsePart(part, knownToolCallIds);
448
+ if (converted) {
449
+ responseParts.push(converted);
450
+ }
451
+ }
452
+ } else if (response.text.trim()) {
453
+ responseParts.push(channelTextPartSchema.parse({
454
+ type: "text",
455
+ text: response.text,
456
+ }));
457
+ }
458
+
459
+ return responseParts;
460
+ }
461
+
462
+ function classifyRuntimeError(error: unknown): ChannelInvokeResponse["error"] {
463
+ const veryfrontError = fromError(error);
464
+
465
+ if (veryfrontError?.type === "no_ai_available") {
466
+ return { code: "provider_error", retryable: false };
467
+ }
468
+
469
+ if (veryfrontError?.type === "api" || veryfrontError?.type === "network") {
470
+ return { code: "provider_error", retryable: true };
471
+ }
472
+
473
+ return { code: "internal_error", retryable: true };
474
+ }
475
+
476
+ export async function executeChannelInvoke(
477
+ payload: ChannelInvokeRequest,
478
+ ctx: HandlerContext,
479
+ deps: ChannelInvokeDeps,
480
+ ): Promise<ChannelInvokeResponse> {
481
+ await deps.ensureProjectDiscovery(ctx);
482
+
483
+ const agent = resolveChannelInvokeAgent(payload.assistantId, deps);
484
+ if (!agent) {
485
+ logger.error("Channel invoke could not resolve a runtime agent for the request", {
486
+ requestedAssistantId: payload.assistantId,
487
+ discoveredAgentIds: deps.getAllAgentIds(),
488
+ projectSlug: ctx.projectSlug,
489
+ projectId: ctx.projectId,
490
+ });
491
+ return {
492
+ ignored: false,
493
+ error: {
494
+ code: "internal_error",
495
+ retryable: false,
496
+ },
497
+ };
498
+ }
499
+
500
+ const messages = normalizeConversationHistoryForRuntime(payload.conversationHistory);
501
+ await agent.clearMemory();
502
+
503
+ try {
504
+ const result = await agent.generate({
505
+ input: messages,
506
+ context: {
507
+ requestId: payload.dispatchId,
508
+ dispatchId: payload.dispatchId,
509
+ conversationId: payload.conversationId,
510
+ projectId: payload.projectId,
511
+ assistantId: payload.assistantId,
512
+ channel: payload.inboundMessage,
513
+ },
514
+ ...(payload.generation?.maxResponseTokens
515
+ ? {
516
+ maxOutputTokens: payload.generation.maxResponseTokens,
517
+ }
518
+ : {}),
519
+ });
520
+
521
+ return ChannelInvokeResponseSchema.parse({
522
+ ignored: false,
523
+ responseParts: buildChannelResponseParts(result),
524
+ tokenUsage: result.usage
525
+ ? {
526
+ inputTokens: result.usage.promptTokens,
527
+ outputTokens: result.usage.completionTokens,
528
+ totalTokens: result.usage.totalTokens,
529
+ }
530
+ : undefined,
531
+ });
532
+ } catch (error) {
533
+ logger.error("Channel invoke runtime execution failed", {
534
+ error: error instanceof Error ? error.message : String(error),
535
+ stack: error instanceof Error ? error.stack : undefined,
536
+ projectSlug: ctx.projectSlug,
537
+ projectId: ctx.projectId,
538
+ dispatchId: payload.dispatchId,
539
+ });
540
+
541
+ return {
542
+ ignored: false,
543
+ error: classifyRuntimeError(error),
544
+ };
545
+ }
546
+ }
@@ -35,10 +35,10 @@ export function embedding(config: EmbeddingConfig): Embedding {
35
35
  model: modelId,
36
36
 
37
37
  async embed(text: string): Promise<number[]> {
38
- const value = queryPrefix + text;
39
- if (!value.trim()) {
38
+ if (!text.trim()) {
40
39
  throw new Error("Cannot embed an empty string");
41
40
  }
41
+ const value = queryPrefix + text;
42
42
  const result = await embed({ model, value });
43
43
  return result.embedding;
44
44
  },
@@ -12,6 +12,53 @@ import { logger } from "../utils/index.js";
12
12
  import type { IntegrationEndpoint } from "./types.js";
13
13
  import { INVALID_ARGUMENT } from "../errors/index.js";
14
14
 
15
+ const PRIVATE_IP_RANGES = [
16
+ /^127\./, // 127.0.0.0/8
17
+ /^10\./, // 10.0.0.0/8
18
+ /^172\.(1[6-9]|2\d|3[01])\./, // 172.16.0.0/12
19
+ /^192\.168\./, // 192.168.0.0/16
20
+ /^169\.254\./, // 169.254.0.0/16
21
+ /^0\./, // 0.0.0.0/8
22
+ /^::1$/, // IPv6 loopback
23
+ /^f[cd][0-9a-f]{2}:/i, // IPv6 unique local (fc00::/7)
24
+ /^fe80:/i, // IPv6 link-local (fe80::/10)
25
+ ];
26
+
27
+ export function validateEndpointUrl(url: string): void {
28
+ let parsed: URL;
29
+ try {
30
+ parsed = new URL(url);
31
+ } catch {
32
+ throw INVALID_ARGUMENT.create({ detail: `Invalid endpoint URL: ${url}` });
33
+ }
34
+
35
+ if (parsed.protocol !== "https:") {
36
+ throw INVALID_ARGUMENT.create({
37
+ detail: `Endpoint URL must use HTTPS: ${parsed.protocol}`,
38
+ });
39
+ }
40
+
41
+ const hostname = parsed.hostname.toLowerCase();
42
+ if (hostname === "localhost") {
43
+ throw INVALID_ARGUMENT.create({
44
+ detail: "Endpoint URL must not target localhost",
45
+ });
46
+ }
47
+
48
+ // Strip IPv6 brackets for regex matching
49
+ const bare = hostname.startsWith("[") && hostname.endsWith("]")
50
+ ? hostname.slice(1, -1)
51
+ : hostname;
52
+
53
+ for (const range of PRIVATE_IP_RANGES) {
54
+ if (range.test(bare)) {
55
+ throw INVALID_ARGUMENT.create({
56
+ detail: "Endpoint URL must not target private/internal networks",
57
+ });
58
+ }
59
+ }
60
+ }
61
+
15
62
  interface ExecutionContext {
16
63
  integration: string;
17
64
  toolId: string;
@@ -63,6 +110,8 @@ async function executeGraphQL(
63
110
  }
64
111
  }
65
112
 
113
+ validateEndpointUrl(endpoint.url);
114
+
66
115
  logger.debug("Executing GraphQL endpoint", {
67
116
  integration: ctx.integration,
68
117
  tool: ctx.toolId,
@@ -149,6 +198,8 @@ async function executeRest(
149
198
  headers["Content-Type"] = endpoint.contentType ?? "application/json";
150
199
  }
151
200
 
201
+ validateEndpointUrl(urlObj.toString());
202
+
152
203
  logger.debug("Executing REST endpoint", {
153
204
  integration: ctx.integration,
154
205
  tool: ctx.toolId,
@@ -77,16 +77,23 @@ export interface OAuthStatusHandlerOptions {
77
77
 
78
78
  /** EnvReader for dynamic env vars (defaults to getEnv) */
79
79
  envReader?: EnvReader;
80
+
81
+ /** Optional authentication check — return true if the request is authenticated */
82
+ isAuthenticated?: (req: dntShim.Request) => boolean | Promise<boolean>;
80
83
  }
81
84
 
82
85
  export function createOAuthStatusHandler(
83
86
  config: OAuthServiceConfig,
84
87
  options: OAuthStatusHandlerOptions = {},
85
- ): () => Promise<dntShim.Response> {
88
+ ): (req: dntShim.Request) => Promise<dntShim.Response> {
86
89
  const tokenStore = options.tokenStore ?? memoryTokenStore;
87
90
  const envReader = options.envReader ?? getEnv;
88
91
 
89
- return async function handler(): Promise<dntShim.Response> {
92
+ return async function handler(req: dntShim.Request): Promise<dntShim.Response> {
93
+ if (options.isAuthenticated && !(await options.isAuthenticated(req))) {
94
+ return dntShim.Response.json({ error: "Unauthorized" }, { status: 401 });
95
+ }
96
+
90
97
  const tokens = await tokenStore.getTokens(config.serviceId);
91
98
 
92
99
  const isConnected = !!tokens?.accessToken;
@@ -106,11 +113,19 @@ export function createOAuthStatusHandler(
106
113
 
107
114
  export function createOAuthDisconnectHandler(
108
115
  config: OAuthServiceConfig,
109
- options: { tokenStore?: TokenStore } = {},
110
- ): () => Promise<dntShim.Response> {
116
+ options: {
117
+ tokenStore?: TokenStore;
118
+ /** Optional authentication check — return true if the request is authenticated */
119
+ isAuthenticated?: (req: dntShim.Request) => boolean | Promise<boolean>;
120
+ } = {},
121
+ ): (req: dntShim.Request) => Promise<dntShim.Response> {
111
122
  const tokenStore = options.tokenStore ?? memoryTokenStore;
112
123
 
113
- return async function handler(): Promise<dntShim.Response> {
124
+ return async function handler(req: dntShim.Request): Promise<dntShim.Response> {
125
+ if (options.isAuthenticated && !(await options.isAuthenticated(req))) {
126
+ return dntShim.Response.json({ error: "Unauthorized" }, { status: 401 });
127
+ }
128
+
114
129
  await tokenStore.clearTokens(config.serviceId);
115
130
 
116
131
  return dntShim.Response.json({