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.
- package/esm/deno.js +1 -1
- package/esm/src/agent/runtime/index.d.ts +1 -0
- package/esm/src/agent/runtime/index.d.ts.map +1 -1
- package/esm/src/agent/runtime/index.js +10 -2
- package/esm/src/channels/control-plane.d.ts +259 -0
- package/esm/src/channels/control-plane.d.ts.map +1 -0
- package/esm/src/channels/control-plane.js +212 -0
- package/esm/src/channels/invoke.d.ts +71 -44
- package/esm/src/channels/invoke.d.ts.map +1 -1
- package/esm/src/channels/invoke.js +33 -114
- package/esm/src/integrations/endpoint-executor.d.ts +1 -0
- package/esm/src/integrations/endpoint-executor.d.ts.map +1 -1
- package/esm/src/integrations/endpoint-executor.js +44 -0
- package/esm/src/internal-agents/ag-ui-sse.d.ts +35 -0
- package/esm/src/internal-agents/ag-ui-sse.d.ts.map +1 -0
- package/esm/src/internal-agents/ag-ui-sse.js +263 -0
- package/esm/src/internal-agents/control-plane-auth.d.ts +20 -0
- package/esm/src/internal-agents/control-plane-auth.d.ts.map +1 -0
- package/esm/src/internal-agents/control-plane-auth.js +56 -0
- package/esm/src/internal-agents/request-body.d.ts +9 -0
- package/esm/src/internal-agents/request-body.d.ts.map +1 -0
- package/esm/src/internal-agents/request-body.js +28 -0
- package/esm/src/internal-agents/run-stream.d.ts +14 -0
- package/esm/src/internal-agents/run-stream.d.ts.map +1 -0
- package/esm/src/internal-agents/run-stream.js +259 -0
- package/esm/src/internal-agents/schema.d.ts +268 -0
- package/esm/src/internal-agents/schema.d.ts.map +1 -0
- package/esm/src/internal-agents/schema.js +71 -0
- package/esm/src/internal-agents/session-manager.d.ts +63 -0
- package/esm/src/internal-agents/session-manager.d.ts.map +1 -0
- package/esm/src/internal-agents/session-manager.js +258 -0
- package/esm/src/platform/adapters/runtime/deno/adapter.d.ts.map +1 -1
- package/esm/src/platform/adapters/runtime/deno/adapter.js +4 -13
- package/esm/src/platform/compat/process.d.ts.map +1 -1
- package/esm/src/platform/compat/process.js +42 -5
- package/esm/src/server/handlers/request/agent-run-cancel.handler.d.ts +11 -0
- package/esm/src/server/handlers/request/agent-run-cancel.handler.d.ts.map +1 -0
- package/esm/src/server/handlers/request/agent-run-cancel.handler.js +62 -0
- package/esm/src/server/handlers/request/agent-run-resume.handler.d.ts +11 -0
- package/esm/src/server/handlers/request/agent-run-resume.handler.d.ts.map +1 -0
- package/esm/src/server/handlers/request/agent-run-resume.handler.js +77 -0
- package/esm/src/server/handlers/request/agent-stream.handler.d.ts +14 -0
- package/esm/src/server/handlers/request/agent-stream.handler.d.ts.map +1 -0
- package/esm/src/server/handlers/request/agent-stream.handler.js +86 -0
- package/esm/src/server/handlers/request/internal-agents-list.handler.d.ts +11 -0
- package/esm/src/server/handlers/request/internal-agents-list.handler.d.ts.map +1 -0
- package/esm/src/server/handlers/request/internal-agents-list.handler.js +73 -0
- package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/index.js +8 -0
- package/package.json +1 -1
- package/src/deno.js +1 -1
- package/src/src/agent/runtime/index.ts +12 -2
- package/src/src/channels/control-plane.ts +332 -0
- package/src/src/channels/invoke.ts +44 -164
- package/src/src/integrations/endpoint-executor.ts +51 -0
- package/src/src/internal-agents/ag-ui-sse.ts +327 -0
- package/src/src/internal-agents/control-plane-auth.ts +82 -0
- package/src/src/internal-agents/request-body.ts +42 -0
- package/src/src/internal-agents/run-stream.ts +354 -0
- package/src/src/internal-agents/schema.ts +102 -0
- package/src/src/internal-agents/session-manager.ts +358 -0
- package/src/src/platform/adapters/runtime/deno/adapter.ts +9 -11
- package/src/src/platform/compat/process.ts +56 -3
- package/src/src/server/handlers/request/agent-run-cancel.handler.ts +86 -0
- package/src/src/server/handlers/request/agent-run-resume.handler.ts +108 -0
- package/src/src/server/handlers/request/agent-stream.handler.ts +125 -0
- package/src/src/server/handlers/request/internal-agents-list.handler.ts +100 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
+
assistantId: string,
|
|
304
209
|
deps: Pick<ChannelInvokeDeps, "getAgent" | "getAllAgentIds">,
|
|
305
210
|
): Agent | undefined {
|
|
306
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|