veryfront 0.1.63 → 0.1.65

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 (68) hide show
  1. package/esm/deno.js +1 -1
  2. package/esm/src/agent/runtime/index.d.ts +1 -0
  3. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  4. package/esm/src/agent/runtime/index.js +10 -2
  5. package/esm/src/channels/control-plane.d.ts +259 -0
  6. package/esm/src/channels/control-plane.d.ts.map +1 -0
  7. package/esm/src/channels/control-plane.js +212 -0
  8. package/esm/src/channels/invoke.d.ts +71 -44
  9. package/esm/src/channels/invoke.d.ts.map +1 -1
  10. package/esm/src/channels/invoke.js +33 -114
  11. package/esm/src/integrations/endpoint-executor.d.ts +1 -0
  12. package/esm/src/integrations/endpoint-executor.d.ts.map +1 -1
  13. package/esm/src/integrations/endpoint-executor.js +44 -0
  14. package/esm/src/internal-agents/ag-ui-sse.d.ts +35 -0
  15. package/esm/src/internal-agents/ag-ui-sse.d.ts.map +1 -0
  16. package/esm/src/internal-agents/ag-ui-sse.js +263 -0
  17. package/esm/src/internal-agents/control-plane-auth.d.ts +20 -0
  18. package/esm/src/internal-agents/control-plane-auth.d.ts.map +1 -0
  19. package/esm/src/internal-agents/control-plane-auth.js +56 -0
  20. package/esm/src/internal-agents/request-body.d.ts +9 -0
  21. package/esm/src/internal-agents/request-body.d.ts.map +1 -0
  22. package/esm/src/internal-agents/request-body.js +28 -0
  23. package/esm/src/internal-agents/run-stream.d.ts +14 -0
  24. package/esm/src/internal-agents/run-stream.d.ts.map +1 -0
  25. package/esm/src/internal-agents/run-stream.js +259 -0
  26. package/esm/src/internal-agents/schema.d.ts +268 -0
  27. package/esm/src/internal-agents/schema.d.ts.map +1 -0
  28. package/esm/src/internal-agents/schema.js +71 -0
  29. package/esm/src/internal-agents/session-manager.d.ts +63 -0
  30. package/esm/src/internal-agents/session-manager.d.ts.map +1 -0
  31. package/esm/src/internal-agents/session-manager.js +258 -0
  32. package/esm/src/platform/adapters/runtime/deno/adapter.d.ts.map +1 -1
  33. package/esm/src/platform/adapters/runtime/deno/adapter.js +4 -13
  34. package/esm/src/platform/compat/process.d.ts.map +1 -1
  35. package/esm/src/platform/compat/process.js +42 -5
  36. package/esm/src/server/handlers/request/agent-run-cancel.handler.d.ts +11 -0
  37. package/esm/src/server/handlers/request/agent-run-cancel.handler.d.ts.map +1 -0
  38. package/esm/src/server/handlers/request/agent-run-cancel.handler.js +62 -0
  39. package/esm/src/server/handlers/request/agent-run-resume.handler.d.ts +11 -0
  40. package/esm/src/server/handlers/request/agent-run-resume.handler.d.ts.map +1 -0
  41. package/esm/src/server/handlers/request/agent-run-resume.handler.js +77 -0
  42. package/esm/src/server/handlers/request/agent-stream.handler.d.ts +14 -0
  43. package/esm/src/server/handlers/request/agent-stream.handler.d.ts.map +1 -0
  44. package/esm/src/server/handlers/request/agent-stream.handler.js +86 -0
  45. package/esm/src/server/handlers/request/internal-agents-list.handler.d.ts +11 -0
  46. package/esm/src/server/handlers/request/internal-agents-list.handler.d.ts.map +1 -0
  47. package/esm/src/server/handlers/request/internal-agents-list.handler.js +73 -0
  48. package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
  49. package/esm/src/server/runtime-handler/index.js +8 -0
  50. package/package.json +1 -1
  51. package/src/deno.js +1 -1
  52. package/src/src/agent/runtime/index.ts +12 -2
  53. package/src/src/channels/control-plane.ts +332 -0
  54. package/src/src/channels/invoke.ts +44 -164
  55. package/src/src/integrations/endpoint-executor.ts +51 -0
  56. package/src/src/internal-agents/ag-ui-sse.ts +327 -0
  57. package/src/src/internal-agents/control-plane-auth.ts +82 -0
  58. package/src/src/internal-agents/request-body.ts +42 -0
  59. package/src/src/internal-agents/run-stream.ts +354 -0
  60. package/src/src/internal-agents/schema.ts +102 -0
  61. package/src/src/internal-agents/session-manager.ts +358 -0
  62. package/src/src/platform/adapters/runtime/deno/adapter.ts +9 -11
  63. package/src/src/platform/compat/process.ts +56 -3
  64. package/src/src/server/handlers/request/agent-run-cancel.handler.ts +86 -0
  65. package/src/src/server/handlers/request/agent-run-resume.handler.ts +108 -0
  66. package/src/src/server/handlers/request/agent-stream.handler.ts +125 -0
  67. package/src/src/server/handlers/request/internal-agents-list.handler.ts +100 -0
  68. package/src/src/server/runtime-handler/index.ts +8 -0
@@ -0,0 +1,332 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
2
+ import type { Agent } from "../agent/index.js";
3
+ import type { HandlerContext } from "../types/index.js";
4
+ import { skillRegistry } from "../skill/registry.js";
5
+ import { base64urlEncodeBytes } from "../utils/base64url.js";
6
+ import { z } from "zod";
7
+
8
+ const SIGNATURE_SKEW_SECONDS = 5;
9
+
10
+ const compactJwsHeaderSchema = z.object({
11
+ alg: z.literal("EdDSA"),
12
+ typ: z.string().optional(),
13
+ kid: z.string().optional(),
14
+ });
15
+
16
+ export const ControlPlaneSurfaceSchema = z.enum(["studio", "channels", "a2a", "mcp"]);
17
+
18
+ export const ControlPlaneAgentsListRequestSchema = z.object({
19
+ requestId: z.string().min(1),
20
+ projectId: z.string().min(1),
21
+ surface: ControlPlaneSurfaceSchema,
22
+ });
23
+
24
+ export const RuntimeAgentSkillSchema = z.object({
25
+ id: z.string().min(1),
26
+ name: z.string().min(1),
27
+ description: z.string().optional(),
28
+ tags: z.array(z.string()).optional(),
29
+ examples: z.array(z.string()).optional(),
30
+ });
31
+
32
+ export const RuntimeAgentSchema = z.object({
33
+ id: z.string().min(1),
34
+ name: z.string().min(1),
35
+ description: z.string().nullable().optional(),
36
+ model: z.string().nullable().optional(),
37
+ version: z.string().nullable().optional(),
38
+ skills: z.array(RuntimeAgentSkillSchema).optional(),
39
+ });
40
+
41
+ export const RuntimeAgentListResponseSchema = z.object({
42
+ agents: z.array(RuntimeAgentSchema),
43
+ });
44
+
45
+ const dispatchClaimsSchema = z.object({
46
+ iss: z.string(),
47
+ aud: z.string(),
48
+ sub: z.string(),
49
+ project_id: z.string(),
50
+ platform: z.string(),
51
+ body_sha256: z.string(),
52
+ iat: z.number().int(),
53
+ exp: z.number().int(),
54
+ });
55
+
56
+ const controlPlaneClaimsSchema = z.object({
57
+ iss: z.string(),
58
+ aud: z.string(),
59
+ sub: z.string(),
60
+ surface: ControlPlaneSurfaceSchema,
61
+ project_id: z.string(),
62
+ request_hash: z.string(),
63
+ iat: z.number().int(),
64
+ exp: z.number().int(),
65
+ });
66
+
67
+ export type ControlPlaneSurface = z.infer<typeof ControlPlaneSurfaceSchema>;
68
+ export type ControlPlaneAgentsListRequest = z.infer<typeof ControlPlaneAgentsListRequestSchema>;
69
+ export type RuntimeAgentSkill = z.infer<typeof RuntimeAgentSkillSchema>;
70
+ export type RuntimeAgent = z.infer<typeof RuntimeAgentSchema>;
71
+ export type RuntimeAgentListResponse = z.infer<typeof RuntimeAgentListResponseSchema>;
72
+ export type DispatchClaims = z.infer<typeof dispatchClaimsSchema>;
73
+ export type ControlPlaneClaims = z.infer<typeof controlPlaneClaimsSchema>;
74
+
75
+ export interface RuntimeAgentDiscoveryDeps {
76
+ ensureProjectDiscovery: (ctx: HandlerContext) => Promise<void>;
77
+ getAgent: (id: string) => Agent | undefined;
78
+ getAllAgentIds: () => string[];
79
+ }
80
+
81
+ type SignedRequestClaims = {
82
+ aud: string;
83
+ exp: number;
84
+ iat: number;
85
+ project_id: string;
86
+ sub: string;
87
+ } & Record<string, unknown>;
88
+
89
+ function base64urlDecodeToBytes(input: string): ArrayBuffer {
90
+ const normalized = input
91
+ .replaceAll("-", "+")
92
+ .replaceAll("_", "/")
93
+ .padEnd(Math.ceil(input.length / 4) * 4, "=");
94
+
95
+ return toArrayBuffer(Uint8Array.from(atob(normalized), (char) => char.charCodeAt(0)));
96
+ }
97
+
98
+ function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
99
+ const buffer = new ArrayBuffer(bytes.byteLength);
100
+ new Uint8Array(buffer).set(bytes);
101
+ return buffer;
102
+ }
103
+
104
+ function pemToDer(pem: string, label: string): ArrayBuffer {
105
+ const body = pem
106
+ .replace(`-----BEGIN ${label}-----`, "")
107
+ .replace(`-----END ${label}-----`, "")
108
+ .replace(/\s/g, "");
109
+
110
+ return toArrayBuffer(Uint8Array.from(atob(body), (char) => char.charCodeAt(0)));
111
+ }
112
+
113
+ async function importEd25519PublicKey(pem: string): Promise<dntShim.CryptoKey> {
114
+ return dntShim.crypto.subtle.importKey(
115
+ "spki",
116
+ pemToDer(pem, "PUBLIC KEY"),
117
+ "Ed25519",
118
+ false,
119
+ ["verify"],
120
+ );
121
+ }
122
+
123
+ async function sha256Base64url(body: string): Promise<string> {
124
+ const hash = await dntShim.crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
125
+ return base64urlEncodeBytes(new Uint8Array(hash));
126
+ }
127
+
128
+ async function verifySignedRequestJws<TClaims extends SignedRequestClaims>(
129
+ jws: string,
130
+ body: string,
131
+ options: {
132
+ audience: string;
133
+ claimsSchema: z.ZodType<TClaims>;
134
+ expectedProjectId?: string;
135
+ expectedSubject?: string;
136
+ hashClaimKey: keyof TClaims & string;
137
+ maxAgeSeconds: number;
138
+ publicKeyPem: string;
139
+ scopedClaim?: {
140
+ key: keyof TClaims & string;
141
+ label: string;
142
+ value: string;
143
+ };
144
+ },
145
+ ): Promise<TClaims> {
146
+ const parts = jws.split(".");
147
+ if (parts.length !== 3) {
148
+ throw new Error("Control-plane signature must be a compact JWS");
149
+ }
150
+
151
+ const encodedHeader = parts[0];
152
+ const encodedPayload = parts[1];
153
+ const encodedSignature = parts[2];
154
+ if (!encodedHeader || !encodedPayload || !encodedSignature) {
155
+ throw new Error("Control-plane signature must include header, payload, and signature");
156
+ }
157
+
158
+ const header = compactJwsHeaderSchema.parse(
159
+ JSON.parse(new TextDecoder().decode(base64urlDecodeToBytes(encodedHeader))),
160
+ );
161
+ const claims = options.claimsSchema.parse(
162
+ JSON.parse(new TextDecoder().decode(base64urlDecodeToBytes(encodedPayload))),
163
+ );
164
+
165
+ if (header.alg !== "EdDSA") {
166
+ throw new Error("Unsupported control-plane JWS algorithm");
167
+ }
168
+
169
+ const signingInput = new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`);
170
+ const signature = base64urlDecodeToBytes(encodedSignature);
171
+ const publicKey = await importEd25519PublicKey(options.publicKeyPem);
172
+ const verified = await dntShim.crypto.subtle.verify("Ed25519", publicKey, signature, signingInput);
173
+
174
+ if (!verified) {
175
+ throw new Error("Control-plane signature verification failed");
176
+ }
177
+
178
+ if (claims.iss !== "veryfront-api") {
179
+ throw new Error("Control-plane issuer mismatch");
180
+ }
181
+
182
+ if (claims.aud !== options.audience) {
183
+ throw new Error("Control-plane audience mismatch");
184
+ }
185
+
186
+ if (options.expectedProjectId && claims.project_id !== options.expectedProjectId) {
187
+ throw new Error("Control-plane project mismatch");
188
+ }
189
+
190
+ if (options.expectedSubject && claims.sub !== options.expectedSubject) {
191
+ throw new Error("Control-plane subject mismatch");
192
+ }
193
+
194
+ if (options.scopedClaim && claims[options.scopedClaim.key] !== options.scopedClaim.value) {
195
+ throw new Error(`Control-plane ${options.scopedClaim.label} mismatch`);
196
+ }
197
+
198
+ const now = Math.floor(Date.now() / 1000);
199
+ if (claims.exp <= now) {
200
+ throw new Error("Control-plane signature expired");
201
+ }
202
+
203
+ if (claims.iat > now + SIGNATURE_SKEW_SECONDS) {
204
+ throw new Error("Control-plane signature issued in the future");
205
+ }
206
+
207
+ if (now - claims.iat > options.maxAgeSeconds) {
208
+ throw new Error("Control-plane signature is too old");
209
+ }
210
+
211
+ const requestHash = claims[options.hashClaimKey];
212
+ if (typeof requestHash !== "string") {
213
+ throw new Error("Control-plane request hash is missing");
214
+ }
215
+
216
+ const bodyHash = await sha256Base64url(body);
217
+ if (requestHash !== bodyHash) {
218
+ throw new Error("Control-plane body hash mismatch");
219
+ }
220
+
221
+ return claims;
222
+ }
223
+
224
+ function resolveAgentSkills(agent: Agent): RuntimeAgentSkill[] {
225
+ if (!agent.config.skills) {
226
+ return [];
227
+ }
228
+
229
+ return Array.from(skillRegistry.resolveForAgent(agent.config.skills).values())
230
+ .map((skill) =>
231
+ RuntimeAgentSkillSchema.parse({
232
+ id: skill.id,
233
+ name: skill.metadata.name || skill.id,
234
+ ...(skill.metadata.description ? { description: skill.metadata.description } : {}),
235
+ })
236
+ )
237
+ .sort((left, right) => left.name.localeCompare(right.name));
238
+ }
239
+
240
+ function getRuntimeAgentMetadata(agent: Agent): RuntimeAgent {
241
+ const rawConfig = agent.config as unknown as Record<string, unknown>;
242
+
243
+ return RuntimeAgentSchema.parse({
244
+ id: agent.id,
245
+ name: typeof rawConfig.name === "string" && rawConfig.name.trim().length > 0
246
+ ? rawConfig.name
247
+ : agent.id,
248
+ description: typeof rawConfig.description === "string" ? rawConfig.description : null,
249
+ model: agent.config.model ?? null,
250
+ version: typeof rawConfig.version === "string" ? rawConfig.version : null,
251
+ skills: resolveAgentSkills(agent),
252
+ });
253
+ }
254
+
255
+ export async function listRuntimeAgents(
256
+ ctx: HandlerContext,
257
+ deps: RuntimeAgentDiscoveryDeps,
258
+ ): Promise<RuntimeAgentListResponse> {
259
+ await deps.ensureProjectDiscovery(ctx);
260
+
261
+ const agents = deps.getAllAgentIds()
262
+ .map((id) => deps.getAgent(id))
263
+ .filter((agent): agent is Agent => Boolean(agent))
264
+ .map(getRuntimeAgentMetadata)
265
+ .sort((left, right) => left.name.localeCompare(right.name));
266
+
267
+ return RuntimeAgentListResponseSchema.parse({ agents });
268
+ }
269
+
270
+ export async function verifyDispatchJws(
271
+ jws: string,
272
+ body: string,
273
+ options: {
274
+ audience: string;
275
+ expectedPlatform?: string;
276
+ expectedProjectId?: string;
277
+ expectedSubject?: string;
278
+ maxAgeSeconds: number;
279
+ publicKeyPem: string;
280
+ },
281
+ ): Promise<DispatchClaims> {
282
+ return verifySignedRequestJws(jws, body, {
283
+ audience: options.audience,
284
+ claimsSchema: dispatchClaimsSchema,
285
+ expectedProjectId: options.expectedProjectId,
286
+ ...(options.expectedSubject ? { expectedSubject: options.expectedSubject } : {}),
287
+ hashClaimKey: "body_sha256",
288
+ maxAgeSeconds: options.maxAgeSeconds,
289
+ publicKeyPem: options.publicKeyPem,
290
+ ...(options.expectedPlatform
291
+ ? {
292
+ scopedClaim: {
293
+ key: "platform" as const,
294
+ label: "platform",
295
+ value: options.expectedPlatform,
296
+ },
297
+ }
298
+ : {}),
299
+ });
300
+ }
301
+
302
+ export async function verifyControlPlaneJws(
303
+ jws: string,
304
+ body: string,
305
+ options: {
306
+ audience: string;
307
+ expectedProjectId?: string;
308
+ expectedSubject?: string;
309
+ expectedSurface?: ControlPlaneSurface;
310
+ maxAgeSeconds: number;
311
+ publicKeyPem: string;
312
+ },
313
+ ): Promise<ControlPlaneClaims> {
314
+ return verifySignedRequestJws(jws, body, {
315
+ audience: options.audience,
316
+ claimsSchema: controlPlaneClaimsSchema,
317
+ expectedProjectId: options.expectedProjectId,
318
+ ...(options.expectedSubject ? { expectedSubject: options.expectedSubject } : {}),
319
+ hashClaimKey: "request_hash",
320
+ maxAgeSeconds: options.maxAgeSeconds,
321
+ publicKeyPem: options.publicKeyPem,
322
+ ...(options.expectedSurface
323
+ ? {
324
+ scopedClaim: {
325
+ key: "surface" as const,
326
+ label: "surface",
327
+ value: options.expectedSurface,
328
+ },
329
+ }
330
+ : {}),
331
+ });
332
+ }
@@ -1,18 +1,16 @@
1
- import * as dntShim from "../../_dnt.shims.js";
2
1
  import type { Agent, AgentMessage as Message, AgentResponse } from "../agent/index.js";
3
2
  import { fromError } from "../errors/veryfront-error.js";
4
3
  import type { HandlerContext } from "../types/index.js";
5
4
  import { serverLogger } from "../utils/index.js";
6
- import { base64urlEncodeBytes } from "../utils/base64url.js";
7
5
  import { z } from "zod";
8
6
  import {
9
7
  getAgent as getRegisteredAgent,
10
8
  getAllAgentIds as getRegisteredAgentIds,
11
9
  } from "../agent/composition/composition.js";
10
+ import { listRuntimeAgents, type RuntimeAgentDiscoveryDeps } from "./control-plane.js";
12
11
  import { ensureProjectDiscovery as ensureProjectDiscoveryForProject } from "../server/handlers/request/api/project-discovery.js";
13
12
 
14
13
  const logger = serverLogger.component("channels-invoke");
15
- const SIGNATURE_SKEW_SECONDS = 5;
16
14
 
17
15
  const rawHistoryPartSchema = z.object({
18
16
  type: z.string(),
@@ -34,11 +32,11 @@ const channelInvokeHistoryMessageSchema = z.object({
34
32
  createdAt: z.string().optional(),
35
33
  });
36
34
 
37
- export const ChannelInvokeRequestSchema = z.object({
35
+ const channelInvokeRequestWireSchema = z.object({
38
36
  dispatchId: z.string().min(1),
39
37
  conversationId: z.string().min(1),
40
38
  projectId: z.string().min(1),
41
- agentConfigId: z.string().min(1),
39
+ assistantId: z.string().min(1),
42
40
  platform: z.literal("slack"),
43
41
  inboundMessage: z.object({
44
42
  text: z.string(),
@@ -53,6 +51,25 @@ export const ChannelInvokeRequestSchema = z.object({
53
51
  }).optional(),
54
52
  });
55
53
 
54
+ export const ChannelInvokeRequestSchema = channelInvokeRequestWireSchema;
55
+
56
+ export const ChannelAssistantsRequestSchema = z.object({
57
+ requestId: z.string().min(1),
58
+ projectId: z.string().min(1),
59
+ platform: z.literal("slack"),
60
+ });
61
+
62
+ export const ChannelAssistantSchema = z.object({
63
+ id: z.string().min(1),
64
+ name: z.string().min(1),
65
+ description: z.string().nullable().optional(),
66
+ model: z.string().nullable().optional(),
67
+ });
68
+
69
+ export const ChannelAssistantsResponseSchema = z.object({
70
+ assistants: z.array(ChannelAssistantSchema),
71
+ });
72
+
56
73
  const channelTextPartSchema = z.object({
57
74
  type: z.literal("text"),
58
75
  text: z.string(),
@@ -106,34 +123,13 @@ export const ChannelInvokeResponseSchema = z.object({
106
123
  }).optional(),
107
124
  });
108
125
 
109
- const dispatchHeaderSchema = z.object({
110
- alg: z.literal("EdDSA"),
111
- typ: z.string().optional(),
112
- kid: z.string().optional(),
113
- });
114
-
115
- const dispatchClaimsSchema = z.object({
116
- iss: z.string(),
117
- aud: z.string(),
118
- sub: z.string(),
119
- project_id: z.string(),
120
- platform: z.string(),
121
- body_sha256: z.string(),
122
- iat: z.number().int(),
123
- exp: z.number().int(),
124
- });
125
-
126
126
  export type ChannelInvokeRequest = z.infer<typeof ChannelInvokeRequestSchema>;
127
127
  export type ChannelInvokeResponse = z.infer<typeof ChannelInvokeResponseSchema>;
128
+ export type ChannelAssistantsRequest = z.infer<typeof ChannelAssistantsRequestSchema>;
129
+ export type ChannelAssistantsResponse = z.infer<typeof ChannelAssistantsResponseSchema>;
128
130
 
129
131
  type ChannelResponsePart = z.infer<typeof ChannelResponsePartSchema>;
130
- type DispatchClaims = z.infer<typeof dispatchClaimsSchema>;
131
-
132
- export interface ChannelInvokeDeps {
133
- ensureProjectDiscovery: (ctx: HandlerContext) => Promise<void>;
134
- getAgent: (id: string) => Agent | undefined;
135
- getAllAgentIds: () => string[];
136
- }
132
+ export interface ChannelInvokeDeps extends RuntimeAgentDiscoveryDeps {}
137
133
 
138
134
  export const defaultChannelInvokeDeps: ChannelInvokeDeps = {
139
135
  ensureProjectDiscovery: ensureProjectDiscoveryForProject,
@@ -141,114 +137,23 @@ export const defaultChannelInvokeDeps: ChannelInvokeDeps = {
141
137
  getAllAgentIds: getRegisteredAgentIds,
142
138
  };
143
139
 
144
- function base64urlDecodeToBytes(input: string): ArrayBuffer {
145
- const normalized = input
146
- .replaceAll("-", "+")
147
- .replaceAll("_", "/")
148
- .padEnd(Math.ceil(input.length / 4) * 4, "=");
149
-
150
- return toArrayBuffer(Uint8Array.from(atob(normalized), (char) => char.charCodeAt(0)));
151
- }
152
-
153
- function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
154
- const buffer = new ArrayBuffer(bytes.byteLength);
155
- new Uint8Array(buffer).set(bytes);
156
- return buffer;
157
- }
158
-
159
- function pemToDer(pem: string, label: string): ArrayBuffer {
160
- const body = pem
161
- .replace(`-----BEGIN ${label}-----`, "")
162
- .replace(`-----END ${label}-----`, "")
163
- .replace(/\s/g, "");
164
-
165
- return toArrayBuffer(Uint8Array.from(atob(body), (char) => char.charCodeAt(0)));
166
- }
167
-
168
- async function importEd25519PublicKey(pem: string): Promise<dntShim.CryptoKey> {
169
- return dntShim.crypto.subtle.importKey(
170
- "spki",
171
- pemToDer(pem, "PUBLIC KEY"),
172
- "Ed25519",
173
- false,
174
- ["verify"],
175
- );
176
- }
177
-
178
- async function sha256Base64url(body: string): Promise<string> {
179
- const hash = await dntShim.crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
180
- return base64urlEncodeBytes(new Uint8Array(hash));
181
- }
182
-
183
- export async function verifyDispatchJws(
184
- jws: string,
185
- body: string,
186
- options: {
187
- audience: string;
188
- publicKeyPem: string;
189
- maxAgeSeconds: number;
190
- expectedProjectId?: string;
191
- },
192
- ): Promise<DispatchClaims> {
193
- const parts = jws.split(".");
194
- if (parts.length !== 3) {
195
- throw new Error("Channel dispatch signature must be a compact JWS");
196
- }
197
-
198
- const encodedHeader = parts[0];
199
- const encodedPayload = parts[1];
200
- const encodedSignature = parts[2];
201
- if (!encodedHeader || !encodedPayload || !encodedSignature) {
202
- throw new Error("Channel dispatch signature must include header, payload, and signature");
203
- }
204
- const header = dispatchHeaderSchema.parse(
205
- JSON.parse(new TextDecoder().decode(base64urlDecodeToBytes(encodedHeader))),
206
- );
207
- const claims = dispatchClaimsSchema.parse(
208
- JSON.parse(new TextDecoder().decode(base64urlDecodeToBytes(encodedPayload))),
140
+ export async function listChannelAssistants(
141
+ ctx: HandlerContext,
142
+ deps: ChannelInvokeDeps,
143
+ ): Promise<ChannelAssistantsResponse> {
144
+ const response = await listRuntimeAgents(ctx, deps);
145
+ const assistants = response.agents.map((agent) =>
146
+ ChannelAssistantSchema.parse({
147
+ id: agent.id,
148
+ name: agent.name,
149
+ description: agent.description ?? null,
150
+ model: agent.model ?? null,
151
+ })
209
152
  );
210
153
 
211
- if (header.alg !== "EdDSA") {
212
- throw new Error("Unsupported channel dispatch JWS algorithm");
213
- }
214
-
215
- const signingInput = new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`);
216
- const signature = base64urlDecodeToBytes(encodedSignature);
217
- const publicKey = await importEd25519PublicKey(options.publicKeyPem);
218
- const verified = await dntShim.crypto.subtle.verify("Ed25519", publicKey, signature, signingInput);
219
-
220
- if (!verified) {
221
- throw new Error("Channel dispatch signature verification failed");
222
- }
223
-
224
- if (claims.aud !== options.audience) {
225
- throw new Error("Channel dispatch audience mismatch");
226
- }
227
-
228
- if (options.expectedProjectId && claims.project_id !== options.expectedProjectId) {
229
- throw new Error("Channel dispatch project mismatch");
230
- }
231
-
232
- const now = Math.floor(Date.now() / 1000);
233
- if (claims.exp <= now) {
234
- throw new Error("Channel dispatch signature expired");
235
- }
236
-
237
- if (claims.iat > now + SIGNATURE_SKEW_SECONDS) {
238
- throw new Error("Channel dispatch signature issued in the future");
239
- }
240
-
241
- if (now - claims.iat > options.maxAgeSeconds) {
242
- throw new Error("Channel dispatch signature is too old");
243
- }
244
-
245
- const bodySha256 = await sha256Base64url(body);
246
- if (claims.body_sha256 !== bodySha256) {
247
- throw new Error("Channel dispatch body hash mismatch");
248
- }
249
-
250
- return claims;
154
+ return ChannelAssistantsResponseSchema.parse({ assistants });
251
155
  }
156
+ export { verifyDispatchJws } from "./control-plane.js";
252
157
 
253
158
  function normalizeConversationPart(
254
159
  part: z.infer<typeof rawHistoryPartSchema>,
@@ -300,35 +205,10 @@ export function normalizeConversationHistoryForRuntime(
300
205
  }
301
206
 
302
207
  export function resolveChannelInvokeAgent(
303
- agentConfigId: string,
208
+ assistantId: string,
304
209
  deps: Pick<ChannelInvokeDeps, "getAgent" | "getAllAgentIds">,
305
210
  ): Agent | undefined {
306
- const exactAgent = deps.getAgent(agentConfigId);
307
- if (exactAgent) {
308
- return exactAgent;
309
- }
310
-
311
- const agentIds = deps.getAllAgentIds();
312
- if (agentIds.length !== 1) {
313
- return undefined;
314
- }
315
-
316
- const onlyAgentId = agentIds[0];
317
- if (!onlyAgentId) {
318
- return undefined;
319
- }
320
- const onlyAgent = deps.getAgent(onlyAgentId);
321
- if (onlyAgent) {
322
- logger.warn(
323
- "Channel invoke fell back to the only discovered runtime agent because agentConfigId did not match a registry id",
324
- {
325
- requestedAgentConfigId: agentConfigId,
326
- resolvedAgentId: onlyAgentId,
327
- },
328
- );
329
- }
330
-
331
- return onlyAgent;
211
+ return deps.getAgent(assistantId);
332
212
  }
333
213
 
334
214
  function normalizeToolCallState(status: string): "pending" | "completed" | "error" {
@@ -455,10 +335,10 @@ export async function executeChannelInvoke(
455
335
  ): Promise<ChannelInvokeResponse> {
456
336
  await deps.ensureProjectDiscovery(ctx);
457
337
 
458
- const agent = resolveChannelInvokeAgent(payload.agentConfigId, deps);
338
+ const agent = resolveChannelInvokeAgent(payload.assistantId, deps);
459
339
  if (!agent) {
460
340
  logger.error("Channel invoke could not resolve a runtime agent for the request", {
461
- requestedAgentConfigId: payload.agentConfigId,
341
+ requestedAssistantId: payload.assistantId,
462
342
  discoveredAgentIds: deps.getAllAgentIds(),
463
343
  projectSlug: ctx.projectSlug,
464
344
  projectId: ctx.projectId,
@@ -483,7 +363,7 @@ export async function executeChannelInvoke(
483
363
  dispatchId: payload.dispatchId,
484
364
  conversationId: payload.conversationId,
485
365
  projectId: payload.projectId,
486
- agentConfigId: payload.agentConfigId,
366
+ assistantId: payload.assistantId,
487
367
  channel: payload.inboundMessage,
488
368
  },
489
369
  ...(payload.generation?.maxResponseTokens
@@ -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,