veryfront 0.1.64 → 0.1.67

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 (69) 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 +3 -40
  9. package/esm/src/channels/invoke.d.ts.map +1 -1
  10. package/esm/src/channels/invoke.js +9 -106
  11. package/esm/src/internal-agents/ag-ui-sse.d.ts +35 -0
  12. package/esm/src/internal-agents/ag-ui-sse.d.ts.map +1 -0
  13. package/esm/src/internal-agents/ag-ui-sse.js +263 -0
  14. package/esm/src/internal-agents/control-plane-auth.d.ts +20 -0
  15. package/esm/src/internal-agents/control-plane-auth.d.ts.map +1 -0
  16. package/esm/src/internal-agents/control-plane-auth.js +56 -0
  17. package/esm/src/internal-agents/request-body.d.ts +9 -0
  18. package/esm/src/internal-agents/request-body.d.ts.map +1 -0
  19. package/esm/src/internal-agents/request-body.js +28 -0
  20. package/esm/src/internal-agents/run-stream.d.ts +14 -0
  21. package/esm/src/internal-agents/run-stream.d.ts.map +1 -0
  22. package/esm/src/internal-agents/run-stream.js +259 -0
  23. package/esm/src/internal-agents/schema.d.ts +268 -0
  24. package/esm/src/internal-agents/schema.d.ts.map +1 -0
  25. package/esm/src/internal-agents/schema.js +71 -0
  26. package/esm/src/internal-agents/session-manager.d.ts +63 -0
  27. package/esm/src/internal-agents/session-manager.d.ts.map +1 -0
  28. package/esm/src/internal-agents/session-manager.js +258 -0
  29. package/esm/src/platform/adapters/runtime/deno/adapter.d.ts.map +1 -1
  30. package/esm/src/platform/adapters/runtime/deno/adapter.js +4 -13
  31. package/esm/src/platform/compat/process.d.ts.map +1 -1
  32. package/esm/src/platform/compat/process.js +42 -5
  33. package/esm/src/server/bootstrap.js +9 -1
  34. package/esm/src/server/handlers/request/agent-run-cancel.handler.d.ts +11 -0
  35. package/esm/src/server/handlers/request/agent-run-cancel.handler.d.ts.map +1 -0
  36. package/esm/src/server/handlers/request/agent-run-cancel.handler.js +62 -0
  37. package/esm/src/server/handlers/request/agent-run-resume.handler.d.ts +11 -0
  38. package/esm/src/server/handlers/request/agent-run-resume.handler.d.ts.map +1 -0
  39. package/esm/src/server/handlers/request/agent-run-resume.handler.js +77 -0
  40. package/esm/src/server/handlers/request/agent-stream.handler.d.ts +14 -0
  41. package/esm/src/server/handlers/request/agent-stream.handler.d.ts.map +1 -0
  42. package/esm/src/server/handlers/request/agent-stream.handler.js +86 -0
  43. package/esm/src/server/handlers/request/{channel-assistants.handler.d.ts → internal-agents-list.handler.d.ts} +4 -4
  44. package/esm/src/server/handlers/request/internal-agents-list.handler.d.ts.map +1 -0
  45. package/esm/src/server/handlers/request/internal-agents-list.handler.js +73 -0
  46. package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
  47. package/esm/src/server/runtime-handler/index.js +8 -2
  48. package/package.json +1 -1
  49. package/src/deno.js +1 -1
  50. package/src/src/agent/runtime/index.ts +12 -2
  51. package/src/src/channels/control-plane.ts +332 -0
  52. package/src/src/channels/invoke.ts +12 -157
  53. package/src/src/internal-agents/ag-ui-sse.ts +327 -0
  54. package/src/src/internal-agents/control-plane-auth.ts +82 -0
  55. package/src/src/internal-agents/request-body.ts +42 -0
  56. package/src/src/internal-agents/run-stream.ts +354 -0
  57. package/src/src/internal-agents/schema.ts +102 -0
  58. package/src/src/internal-agents/session-manager.ts +358 -0
  59. package/src/src/platform/adapters/runtime/deno/adapter.ts +9 -11
  60. package/src/src/platform/compat/process.ts +56 -3
  61. package/src/src/server/bootstrap.ts +13 -1
  62. package/src/src/server/handlers/request/agent-run-cancel.handler.ts +86 -0
  63. package/src/src/server/handlers/request/agent-run-resume.handler.ts +108 -0
  64. package/src/src/server/handlers/request/agent-stream.handler.ts +125 -0
  65. package/src/src/server/handlers/request/internal-agents-list.handler.ts +100 -0
  66. package/src/src/server/runtime-handler/index.ts +8 -2
  67. package/esm/src/server/handlers/request/channel-assistants.handler.d.ts.map +0 -1
  68. package/esm/src/server/handlers/request/channel-assistants.handler.js +0 -71
  69. package/src/src/server/handlers/request/channel-assistants.handler.ts +0 -94
@@ -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(),
@@ -125,37 +123,13 @@ export const ChannelInvokeResponseSchema = z.object({
125
123
  }).optional(),
126
124
  });
127
125
 
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
126
  export type ChannelInvokeRequest = z.infer<typeof ChannelInvokeRequestSchema>;
146
127
  export type ChannelInvokeResponse = z.infer<typeof ChannelInvokeResponseSchema>;
147
128
  export type ChannelAssistantsRequest = z.infer<typeof ChannelAssistantsRequestSchema>;
148
129
  export type ChannelAssistantsResponse = z.infer<typeof ChannelAssistantsResponseSchema>;
149
130
 
150
131
  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
- }
132
+ export interface ChannelInvokeDeps extends RuntimeAgentDiscoveryDeps {}
159
133
 
160
134
  export const defaultChannelInvokeDeps: ChannelInvokeDeps = {
161
135
  ensureProjectDiscovery: ensureProjectDiscoveryForProject,
@@ -163,142 +137,23 @@ export const defaultChannelInvokeDeps: ChannelInvokeDeps = {
163
137
  getAllAgentIds: getRegisteredAgentIds,
164
138
  };
165
139
 
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
140
  export async function listChannelAssistants(
180
141
  ctx: HandlerContext,
181
142
  deps: ChannelInvokeDeps,
182
143
  ): 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"],
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
+ })
225
152
  );
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
153
 
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;
154
+ return ChannelAssistantsResponseSchema.parse({ assistants });
301
155
  }
156
+ export { verifyDispatchJws } from "./control-plane.js";
302
157
 
303
158
  function normalizeConversationPart(
304
159
  part: z.infer<typeof rawHistoryPartSchema>,
@@ -0,0 +1,327 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
2
+ import type { AgentResponse } from "../agent/index.js";
3
+ import { serverLogger } from "../utils/index.js";
4
+ import { z } from "zod";
5
+
6
+ const encoder = new TextEncoder();
7
+ const logger = serverLogger.component("internal-agents-ag-ui-sse");
8
+
9
+ type RuntimeDataEvent = Record<string, unknown> & { type: string };
10
+
11
+ export interface RunFinishedMetadata {
12
+ provider?: string;
13
+ model?: string;
14
+ inputTokens?: number;
15
+ outputTokens?: number;
16
+ totalTokens?: number;
17
+ finishReason?: string;
18
+ }
19
+
20
+ export interface StreamTransformState {
21
+ messageId: string | null;
22
+ textOpen: boolean;
23
+ stepIndex: number;
24
+ sawTerminalError: boolean;
25
+ metadata: RunFinishedMetadata;
26
+ }
27
+
28
+ export function createStreamTransformState(): StreamTransformState {
29
+ return {
30
+ messageId: null,
31
+ textOpen: false,
32
+ stepIndex: 0,
33
+ sawTerminalError: false,
34
+ metadata: {},
35
+ };
36
+ }
37
+
38
+ const agUiEventPayloadSchemas = {
39
+ RunStarted: z.object({
40
+ runId: z.string().min(1),
41
+ threadId: z.string().min(1),
42
+ agentId: z.string().min(1),
43
+ }),
44
+ TextMessageStart: z.object({
45
+ messageId: z.string().min(1),
46
+ role: z.literal("assistant"),
47
+ }),
48
+ TextMessageContent: z.object({
49
+ messageId: z.string().min(1),
50
+ delta: z.string(),
51
+ }),
52
+ TextMessageEnd: z.object({
53
+ messageId: z.string().min(1),
54
+ }),
55
+ ToolCallStart: z.object({
56
+ toolCallId: z.string().min(1),
57
+ toolCallName: z.string().min(1),
58
+ }),
59
+ ToolCallArgs: z.object({
60
+ toolCallId: z.string().min(1),
61
+ delta: z.string(),
62
+ }),
63
+ ToolCallEnd: z.object({
64
+ toolCallId: z.string().min(1),
65
+ }),
66
+ ToolCallResult: z.object({
67
+ toolCallId: z.string().min(1),
68
+ result: z.unknown(),
69
+ isError: z.boolean().optional(),
70
+ }),
71
+ StepStarted: z.object({
72
+ stepIndex: z.number().int().positive(),
73
+ }),
74
+ StepFinished: z.object({
75
+ stepIndex: z.number().int().positive(),
76
+ }),
77
+ RunError: z.object({
78
+ code: z.string().min(1).optional(),
79
+ message: z.string().min(1),
80
+ }),
81
+ RunFinished: z.object({
82
+ metadata: z.object({
83
+ provider: z.string().optional(),
84
+ model: z.string().optional(),
85
+ inputTokens: z.number().int().nonnegative().optional(),
86
+ outputTokens: z.number().int().nonnegative().optional(),
87
+ totalTokens: z.number().int().nonnegative().optional(),
88
+ finishReason: z.string().optional(),
89
+ }),
90
+ }),
91
+ } as const satisfies Record<string, z.ZodType<Record<string, unknown>>>;
92
+
93
+ type AgUiEventName = keyof typeof agUiEventPayloadSchemas;
94
+
95
+ export function formatAgUiEvent(event: string, payload: Record<string, unknown>): Uint8Array {
96
+ const schema = agUiEventPayloadSchemas[event as AgUiEventName];
97
+ const validatedPayload = schema ? schema.parse(payload) : payload;
98
+ return encoder.encode(`event: ${event}\ndata: ${JSON.stringify(validatedPayload)}\n\n`);
99
+ }
100
+
101
+ export function parseSseJsonEvents(chunk: string): {
102
+ events: RuntimeDataEvent[];
103
+ remainder: string;
104
+ } {
105
+ const blocks = chunk.split("\n\n");
106
+ const remainder = blocks.pop() ?? "";
107
+ const events = blocks.flatMap((block) => {
108
+ const dataLines = block.split("\n")
109
+ .filter((line) => line.startsWith("data:"))
110
+ .map((line) => line.slice(5).trimStart());
111
+
112
+ if (!dataLines.length) {
113
+ return [];
114
+ }
115
+
116
+ try {
117
+ const payload = JSON.parse(dataLines.join("\n")) as RuntimeDataEvent;
118
+ return [payload];
119
+ } catch (error) {
120
+ logger.warn("Skipping malformed runtime SSE event payload", {
121
+ error,
122
+ dataLength: dataLines.join("\n").length,
123
+ });
124
+ return [];
125
+ }
126
+ });
127
+
128
+ return { events, remainder };
129
+ }
130
+
131
+ function getMessageId(state: StreamTransformState, event: RuntimeDataEvent): string {
132
+ if (typeof event.messageId === "string") {
133
+ state.messageId = event.messageId;
134
+ return event.messageId;
135
+ }
136
+
137
+ if (!state.messageId && typeof event.id === "string") {
138
+ state.messageId = event.id;
139
+ }
140
+
141
+ if (!state.messageId) {
142
+ state.messageId = dntShim.crypto.randomUUID();
143
+ }
144
+
145
+ return state.messageId;
146
+ }
147
+
148
+ function applyDataMetadata(state: StreamTransformState, event: RuntimeDataEvent): void {
149
+ const data = event.data && typeof event.data === "object" && !Array.isArray(event.data)
150
+ ? event.data as Record<string, unknown>
151
+ : event;
152
+
153
+ if (typeof data.model === "string") {
154
+ state.metadata.model = data.model;
155
+ const provider = data.model.split("/")[0];
156
+ if (provider) {
157
+ state.metadata.provider = provider;
158
+ }
159
+ }
160
+ }
161
+
162
+ function applyResponseMetadata(state: StreamTransformState, response: AgentResponse | null): void {
163
+ if (!response) return;
164
+
165
+ if (response.usage) {
166
+ state.metadata.inputTokens = response.usage.promptTokens;
167
+ state.metadata.outputTokens = response.usage.completionTokens;
168
+ state.metadata.totalTokens = response.usage.totalTokens;
169
+ }
170
+
171
+ const finishReason = response.metadata && typeof response.metadata === "object"
172
+ ? response.metadata.finishReason
173
+ : undefined;
174
+ if (typeof finishReason === "string") {
175
+ state.metadata.finishReason = finishReason;
176
+ }
177
+ }
178
+
179
+ export function mapRuntimeEventToAgUi(
180
+ state: StreamTransformState,
181
+ event: RuntimeDataEvent,
182
+ ): Array<{ event: string; payload: Record<string, unknown> }> {
183
+ switch (event.type) {
184
+ case "message-start":
185
+ getMessageId(state, event);
186
+ return [];
187
+
188
+ case "text-start": {
189
+ if (state.textOpen) return [];
190
+ const messageId = getMessageId(state, event);
191
+ state.textOpen = true;
192
+ return [{
193
+ event: "TextMessageStart",
194
+ payload: { messageId, role: "assistant" },
195
+ }];
196
+ }
197
+
198
+ case "text-delta": {
199
+ const messageId = getMessageId(state, event);
200
+ if (!state.textOpen) {
201
+ state.textOpen = true;
202
+ return [
203
+ { event: "TextMessageStart", payload: { messageId, role: "assistant" } },
204
+ {
205
+ event: "TextMessageContent",
206
+ payload: { messageId, delta: typeof event.delta === "string" ? event.delta : "" },
207
+ },
208
+ ];
209
+ }
210
+
211
+ return [{
212
+ event: "TextMessageContent",
213
+ payload: { messageId, delta: typeof event.delta === "string" ? event.delta : "" },
214
+ }];
215
+ }
216
+
217
+ case "text-end": {
218
+ if (!state.textOpen) return [];
219
+ state.textOpen = false;
220
+ return [{
221
+ event: "TextMessageEnd",
222
+ payload: { messageId: getMessageId(state, event) },
223
+ }];
224
+ }
225
+
226
+ case "tool-input-start":
227
+ return [{
228
+ event: "ToolCallStart",
229
+ payload: {
230
+ toolCallId: event.toolCallId,
231
+ toolCallName: event.toolName,
232
+ },
233
+ }];
234
+
235
+ case "tool-input-delta":
236
+ return [{
237
+ event: "ToolCallArgs",
238
+ payload: {
239
+ toolCallId: event.toolCallId,
240
+ delta: typeof event.inputTextDelta === "string" ? event.inputTextDelta : "",
241
+ },
242
+ }];
243
+
244
+ case "tool-input-available":
245
+ return [{
246
+ event: "ToolCallEnd",
247
+ payload: { toolCallId: event.toolCallId },
248
+ }];
249
+
250
+ case "tool-output-available":
251
+ return [{
252
+ event: "ToolCallResult",
253
+ payload: {
254
+ toolCallId: event.toolCallId,
255
+ result: event.output,
256
+ },
257
+ }];
258
+
259
+ case "tool-output-error":
260
+ return [{
261
+ event: "ToolCallResult",
262
+ payload: {
263
+ toolCallId: event.toolCallId,
264
+ result: { error: event.errorText },
265
+ isError: true,
266
+ },
267
+ }];
268
+
269
+ case "step-start":
270
+ state.stepIndex += 1;
271
+ return [{
272
+ event: "StepStarted",
273
+ payload: { stepIndex: state.stepIndex },
274
+ }];
275
+
276
+ case "step-end":
277
+ return [{
278
+ event: "StepFinished",
279
+ payload: { stepIndex: state.stepIndex },
280
+ }];
281
+
282
+ case "data":
283
+ applyDataMetadata(state, event);
284
+ return [];
285
+
286
+ case "error":
287
+ state.sawTerminalError = true;
288
+ return [{
289
+ event: "RunError",
290
+ payload: {
291
+ message: typeof event.error === "string" ? event.error : "Agent run failed",
292
+ },
293
+ }];
294
+
295
+ default:
296
+ return [];
297
+ }
298
+ }
299
+
300
+ export function finalizeRunEvents(
301
+ state: StreamTransformState,
302
+ response: AgentResponse | null,
303
+ ): Array<{ event: string; payload: Record<string, unknown> }> {
304
+ applyResponseMetadata(state, response);
305
+
306
+ if (state.sawTerminalError) {
307
+ return [];
308
+ }
309
+
310
+ const events: Array<{ event: string; payload: Record<string, unknown> }> = [];
311
+ if (state.textOpen) {
312
+ state.textOpen = false;
313
+ events.push({
314
+ event: "TextMessageEnd",
315
+ payload: { messageId: getMessageId(state, { type: "text-end" }) },
316
+ });
317
+ }
318
+
319
+ events.push({
320
+ event: "RunFinished",
321
+ payload: {
322
+ metadata: state.metadata,
323
+ },
324
+ });
325
+
326
+ return events;
327
+ }
@@ -0,0 +1,82 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
2
+ import { verifyControlPlaneJws } from "../channels/control-plane.js";
3
+ import type { HandlerContext } from "../types/index.js";
4
+ import { serverLogger } from "../utils/index.js";
5
+ import { HTTP_INTERNAL_SERVER_ERROR } from "../utils/constants/index.js";
6
+
7
+ const CONTROL_PLANE_JWS_HEADER = "x-veryfront-control-plane-jws";
8
+ const MAX_CONTROL_PLANE_SIGNATURE_AGE_SECONDS = 60;
9
+ const logger = serverLogger.component("internal-agents-auth");
10
+
11
+ export class ControlPlaneRequestError extends Error {
12
+ readonly status: number;
13
+
14
+ constructor(status: number, message: string) {
15
+ super(message);
16
+ this.status = status;
17
+ this.name = "ControlPlaneRequestError";
18
+ }
19
+ }
20
+
21
+ export async function verifyControlPlaneRequest(
22
+ req: dntShim.Request,
23
+ ctx: HandlerContext,
24
+ rawBody: string,
25
+ options: {
26
+ expectedSubject?: string;
27
+ expectedSurface?: "studio" | "channels" | "a2a" | "mcp";
28
+ } = {},
29
+ ) {
30
+ const publicKeyPem = ctx.adapter.env.get("CHANNEL_DISPATCH_SIGNING_PUBLIC_KEY");
31
+ if (!publicKeyPem) {
32
+ throw new ControlPlaneRequestError(
33
+ HTTP_INTERNAL_SERVER_ERROR,
34
+ "Control-plane verification is not configured",
35
+ );
36
+ }
37
+
38
+ const projectSlug = ctx.projectSlug;
39
+ if (!projectSlug) {
40
+ throw new ControlPlaneRequestError(400, "Project context is unavailable");
41
+ }
42
+
43
+ const controlPlaneJws = req.headers.get(CONTROL_PLANE_JWS_HEADER);
44
+ if (!controlPlaneJws) {
45
+ throw new ControlPlaneRequestError(401, "Missing control-plane signature");
46
+ }
47
+
48
+ try {
49
+ return await verifyControlPlaneJws(controlPlaneJws, rawBody, {
50
+ audience: projectSlug,
51
+ expectedProjectId: ctx.projectId,
52
+ expectedSubject: options.expectedSubject,
53
+ expectedSurface: options.expectedSurface,
54
+ publicKeyPem,
55
+ maxAgeSeconds: MAX_CONTROL_PLANE_SIGNATURE_AGE_SECONDS,
56
+ });
57
+ } catch (error) {
58
+ const isAuthError = error instanceof Error && (
59
+ error.message.includes("Control-plane") ||
60
+ error.message.includes("Unsupported")
61
+ );
62
+
63
+ if (isAuthError) {
64
+ logger.warn("Invalid control-plane signature", {
65
+ error,
66
+ projectSlug,
67
+ expectedSubject: options.expectedSubject,
68
+ expectedSurface: options.expectedSurface,
69
+ });
70
+ throw new ControlPlaneRequestError(401, "Invalid control-plane signature");
71
+ }
72
+
73
+ logger.error("Unexpected error during control-plane verification", {
74
+ error,
75
+ projectSlug,
76
+ });
77
+ throw new ControlPlaneRequestError(
78
+ HTTP_INTERNAL_SERVER_ERROR,
79
+ "Internal error during request verification",
80
+ );
81
+ }
82
+ }
@@ -0,0 +1,42 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
2
+ import { VeryfrontError } from "../errors/types.js";
3
+ import { readBodyWithLimit } from "../security/index.js";
4
+ import {
5
+ DEFAULT_MAX_BODY_SIZE_BYTES,
6
+ HTTP_PAYLOAD_TOO_LARGE,
7
+ } from "../utils/constants/index.js";
8
+
9
+ export const INTERNAL_AGENT_STREAM_MAX_BODY_BYTES = DEFAULT_MAX_BODY_SIZE_BYTES;
10
+ export const INTERNAL_AGENT_CONTROL_PLANE_MAX_BODY_BYTES = 128 * 1024;
11
+
12
+ export class InternalAgentRequestBodyTooLargeError extends Error {
13
+ readonly status = HTTP_PAYLOAD_TOO_LARGE;
14
+
15
+ constructor(message = "Payload too large") {
16
+ super(message);
17
+ this.name = "InternalAgentRequestBodyTooLargeError";
18
+ }
19
+ }
20
+
21
+ export async function readInternalAgentRequestBody(
22
+ request: dntShim.Request,
23
+ maxBodyBytes = INTERNAL_AGENT_STREAM_MAX_BODY_BYTES,
24
+ ): Promise<string> {
25
+ if (!request.body) {
26
+ return "";
27
+ }
28
+
29
+ try {
30
+ return await readBodyWithLimit(request, maxBodyBytes);
31
+ } catch (error) {
32
+ if (
33
+ error instanceof VeryfrontError &&
34
+ error.slug === "input-validation-failed" &&
35
+ error.detail === "Request body exceeds size limit"
36
+ ) {
37
+ throw new InternalAgentRequestBodyTooLargeError();
38
+ }
39
+
40
+ throw error;
41
+ }
42
+ }