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.
- package/esm/cli/templates/manifest.js +37 -37
- package/esm/deno.d.ts +3 -0
- package/esm/deno.js +6 -3
- package/esm/src/agent/composition/composition.d.ts.map +1 -1
- package/esm/src/agent/composition/composition.js +13 -3
- package/esm/src/agent/factory.d.ts.map +1 -1
- package/esm/src/agent/factory.js +3 -3
- package/esm/src/agent/middleware/security/validator.d.ts +92 -0
- package/esm/src/agent/middleware/security/validator.d.ts.map +1 -0
- package/esm/src/agent/middleware/security/validator.js +187 -0
- package/esm/src/agent/runtime/index.d.ts +3 -2
- package/esm/src/agent/runtime/index.d.ts.map +1 -1
- package/esm/src/agent/runtime/index.js +16 -8
- package/esm/src/agent/types.d.ts +4 -0
- package/esm/src/agent/types.d.ts.map +1 -1
- package/esm/src/channels/invoke.d.ts +491 -0
- package/esm/src/channels/invoke.d.ts.map +1 -0
- package/esm/src/channels/invoke.js +417 -0
- package/esm/src/embedding/embedding.js +2 -2
- 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/integrations/schema.d.ts +2 -2
- package/esm/src/oauth/handlers/init-handler.d.ts +6 -2
- package/esm/src/oauth/handlers/init-handler.d.ts.map +1 -1
- package/esm/src/oauth/handlers/init-handler.js +8 -2
- package/esm/src/platform/compat/opaque-deps.d.ts.map +1 -1
- package/esm/src/platform/compat/opaque-deps.js +10 -1
- package/esm/src/prompt/factory.d.ts.map +1 -1
- package/esm/src/prompt/factory.js +9 -1
- package/esm/src/react/components/ai/markdown.d.ts.map +1 -1
- package/esm/src/react/components/ai/markdown.js +4 -4
- package/esm/src/server/handlers/dev/framework-candidates.generated.d.ts.map +1 -1
- package/esm/src/server/handlers/dev/framework-candidates.generated.js +5 -4
- package/esm/src/server/handlers/preview/markdown-html-generator.js +1 -1
- package/esm/src/server/handlers/request/api/api-handler-wrapper.d.ts.map +1 -1
- package/esm/src/server/handlers/request/api/api-handler-wrapper.js +1 -74
- package/esm/src/server/handlers/request/api/project-discovery.d.ts +9 -0
- package/esm/src/server/handlers/request/api/project-discovery.d.ts.map +1 -0
- package/esm/src/server/handlers/request/api/project-discovery.js +74 -0
- package/esm/src/server/handlers/request/channel-assistants.handler.d.ts +11 -0
- package/esm/src/server/handlers/request/channel-assistants.handler.d.ts.map +1 -0
- package/esm/src/server/handlers/request/channel-assistants.handler.js +71 -0
- package/esm/src/server/handlers/request/channel-invoke.handler.d.ts +11 -0
- package/esm/src/server/handlers/request/channel-invoke.handler.d.ts.map +1 -0
- package/esm/src/server/handlers/request/channel-invoke.handler.js +72 -0
- package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/index.js +4 -0
- package/esm/src/transforms/md/compiler/md-compiler.d.ts.map +1 -1
- package/esm/src/transforms/md/compiler/md-compiler.js +25 -1
- package/package.json +3 -1
- package/src/cli/templates/manifest.js +37 -37
- package/src/deno.js +6 -3
- package/src/src/agent/composition/composition.ts +15 -3
- package/src/src/agent/factory.ts +19 -6
- package/src/src/agent/middleware/security/validator.ts +288 -0
- package/src/src/agent/runtime/index.ts +26 -3
- package/src/src/agent/types.ts +4 -0
- package/src/src/channels/invoke.ts +546 -0
- package/src/src/embedding/embedding.ts +2 -2
- package/src/src/integrations/endpoint-executor.ts +51 -0
- package/src/src/oauth/handlers/init-handler.ts +20 -5
- package/src/src/platform/compat/opaque-deps.ts +19 -4
- package/src/src/prompt/factory.ts +10 -1
- package/src/src/react/components/ai/markdown.tsx +5 -4
- package/src/src/server/handlers/dev/framework-candidates.generated.ts +5 -4
- package/src/src/server/handlers/preview/markdown-html-generator.ts +1 -1
- package/src/src/server/handlers/request/api/api-handler-wrapper.ts +1 -85
- package/src/src/server/handlers/request/api/project-discovery.ts +86 -0
- package/src/src/server/handlers/request/channel-assistants.handler.ts +94 -0
- package/src/src/server/handlers/request/channel-invoke.handler.ts +95 -0
- package/src/src/server/runtime-handler/index.ts +4 -0
- 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
|
-
|
|
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: {
|
|
110
|
-
|
|
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({
|