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
@@ -0,0 +1,358 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
2
+ function stableJsonStringify(value: unknown, depth = 0): string {
3
+ if (depth > 64) {
4
+ return JSON.stringify(value);
5
+ }
6
+
7
+ if (value === null || typeof value !== "object") {
8
+ return JSON.stringify(value);
9
+ }
10
+
11
+ if (Array.isArray(value)) {
12
+ return `[${value.map((item) => stableJsonStringify(item, depth + 1)).join(",")}]`;
13
+ }
14
+
15
+ const entries = Object.entries(value as Record<string, unknown>)
16
+ .sort(([left], [right]) => left.localeCompare(right))
17
+ .map(([key, item]) => `${JSON.stringify(key)}:${stableJsonStringify(item, depth + 1)}`);
18
+
19
+ return `{${entries.join(",")}}`;
20
+ }
21
+
22
+ function createToolResultKey(result: unknown, isError: boolean): string {
23
+ return `${isError ? "1" : "0"}:${stableJsonStringify(result)}`;
24
+ }
25
+
26
+ export class AgentRunCancelledError extends Error {
27
+ constructor(message = "Run cancelled") {
28
+ super(message);
29
+ this.name = "AgentRunCancelledError";
30
+ }
31
+ }
32
+
33
+ export class AgentRunAlreadyExistsError extends Error {
34
+ constructor(runId: string) {
35
+ super(`Run "${runId}" is already active`);
36
+ this.name = "AgentRunAlreadyExistsError";
37
+ }
38
+ }
39
+
40
+ export class RunNotActiveError extends Error {
41
+ constructor(runId: string) {
42
+ super(`Run "${runId}" is not active`);
43
+ this.name = "RunNotActiveError";
44
+ }
45
+ }
46
+
47
+ export class ToolResultNotWaitingError extends Error {
48
+ constructor(runId: string, toolCallId: string) {
49
+ super(`Run "${runId}" is not waiting for tool call "${toolCallId}"`);
50
+ this.name = "ToolResultNotWaitingError";
51
+ }
52
+ }
53
+
54
+ export class ToolResultConflictError extends Error {
55
+ constructor(runId: string, toolCallId: string) {
56
+ super(`Conflicting tool result for run "${runId}" and tool call "${toolCallId}"`);
57
+ this.name = "ToolResultConflictError";
58
+ }
59
+ }
60
+
61
+ const DEFAULT_WAITING_FOR_TOOL_TTL_MS = 5 * 60 * 1000;
62
+ const DEFAULT_SESSION_TTL_MS = 15 * 60 * 1000;
63
+ const DEFAULT_MAX_CONCURRENT_SESSIONS = 100;
64
+
65
+ type SessionStatus = "running" | "waiting" | "completed" | "cancelled" | "failed";
66
+
67
+ interface SubmittedToolResult {
68
+ toolCallId: string;
69
+ result: unknown;
70
+ isError: boolean;
71
+ key: string;
72
+ }
73
+
74
+ interface WaitingToolState {
75
+ toolCallId: string;
76
+ resolve: (value: SubmittedToolResult) => void;
77
+ reject: (reason?: unknown) => void;
78
+ }
79
+
80
+ interface AgentRunSession {
81
+ runId: string;
82
+ threadId: string;
83
+ status: SessionStatus;
84
+ abortController: AbortController;
85
+ waitingTool: WaitingToolState | null;
86
+ submittedResults: Map<string, SubmittedToolResult>;
87
+ waitingTimeoutId: number | null;
88
+ sessionTimeoutId: number | null;
89
+ }
90
+
91
+ export interface SubmitToolResultOutcome {
92
+ accepted: true;
93
+ duplicate?: true;
94
+ }
95
+
96
+ export class AgentRunSessionManager {
97
+ private readonly sessions = new Map<string, AgentRunSession>();
98
+
99
+ constructor(
100
+ private readonly options: {
101
+ waitingForToolTtlMs?: number;
102
+ sessionTtlMs?: number | null;
103
+ maxConcurrentSessions?: number;
104
+ setTimeoutFn?: typeof dntShim.setTimeout;
105
+ clearTimeoutFn?: typeof clearTimeout;
106
+ } = {},
107
+ ) {}
108
+
109
+ private get waitingForToolTtlMs(): number {
110
+ return this.options.waitingForToolTtlMs ?? DEFAULT_WAITING_FOR_TOOL_TTL_MS;
111
+ }
112
+
113
+ private get sessionTtlMs(): number | null {
114
+ return this.options.sessionTtlMs ?? null;
115
+ }
116
+
117
+ private get setTimeoutFn(): typeof dntShim.setTimeout {
118
+ return this.options.setTimeoutFn ?? dntShim.dntGlobalThis.setTimeout.bind(dntShim.dntGlobalThis);
119
+ }
120
+
121
+ private get clearTimeoutFn(): typeof clearTimeout {
122
+ return this.options.clearTimeoutFn ?? globalThis.clearTimeout.bind(dntShim.dntGlobalThis);
123
+ }
124
+
125
+ private clearWaitingTimeout(session: AgentRunSession): void {
126
+ if (session.waitingTimeoutId === null) {
127
+ return;
128
+ }
129
+
130
+ this.clearTimeoutFn(session.waitingTimeoutId);
131
+ session.waitingTimeoutId = null;
132
+ }
133
+
134
+ private clearSessionTimeout(session: AgentRunSession): void {
135
+ if (session.sessionTimeoutId === null) {
136
+ return;
137
+ }
138
+
139
+ this.clearTimeoutFn(session.sessionTimeoutId);
140
+ session.sessionTimeoutId = null;
141
+ }
142
+
143
+ private scheduleSessionTimeout(session: AgentRunSession): void {
144
+ if (this.sessionTtlMs === null) {
145
+ return;
146
+ }
147
+
148
+ this.clearSessionTimeout(session);
149
+ session.sessionTimeoutId = this.setTimeoutFn(() => {
150
+ this.cancelRun(session.runId);
151
+ }, this.sessionTtlMs) as unknown as number;
152
+ }
153
+
154
+ private scheduleWaitingTimeout(session: AgentRunSession): void {
155
+ this.clearWaitingTimeout(session);
156
+ session.waitingTimeoutId = this.setTimeoutFn(() => {
157
+ this.cancelRun(session.runId);
158
+ }, this.waitingForToolTtlMs) as unknown as number;
159
+ }
160
+
161
+ private touchSession(session: AgentRunSession): void {
162
+ if (
163
+ session.status === "running" || session.status === "waiting"
164
+ ) {
165
+ this.scheduleSessionTimeout(session);
166
+ }
167
+ }
168
+
169
+ private get maxConcurrentSessions(): number {
170
+ return this.options.maxConcurrentSessions ?? DEFAULT_MAX_CONCURRENT_SESSIONS;
171
+ }
172
+
173
+ startRun(input: { runId: string; threadId: string }): AbortSignal {
174
+ const existing = this.sessions.get(input.runId);
175
+ if (existing && (existing.status === "running" || existing.status === "waiting")) {
176
+ throw new AgentRunAlreadyExistsError(input.runId);
177
+ }
178
+
179
+ if (this.sessions.size >= this.maxConcurrentSessions) {
180
+ throw new Error(
181
+ `Maximum concurrent sessions (${this.maxConcurrentSessions}) reached`,
182
+ );
183
+ }
184
+
185
+ const session: AgentRunSession = {
186
+ runId: input.runId,
187
+ threadId: input.threadId,
188
+ status: "running",
189
+ abortController: new AbortController(),
190
+ waitingTool: null,
191
+ submittedResults: new Map(),
192
+ waitingTimeoutId: null,
193
+ sessionTimeoutId: null,
194
+ };
195
+
196
+ this.sessions.set(input.runId, session);
197
+ this.touchSession(session);
198
+ return session.abortController.signal;
199
+ }
200
+
201
+ async waitForToolResult(runId: string, toolCallId: string): Promise<{
202
+ result: unknown;
203
+ isError: boolean;
204
+ }> {
205
+ const session = this.sessions.get(runId);
206
+ if (!session || session.status === "completed" || session.status === "failed") {
207
+ throw new RunNotActiveError(runId);
208
+ }
209
+
210
+ if (session.abortController.signal.aborted || session.status === "cancelled") {
211
+ throw new AgentRunCancelledError();
212
+ }
213
+
214
+ const existingResult = session.submittedResults.get(toolCallId);
215
+ if (existingResult) {
216
+ session.status = "running";
217
+ this.touchSession(session);
218
+ return { result: existingResult.result, isError: existingResult.isError };
219
+ }
220
+
221
+ if (session.waitingTool && session.waitingTool.toolCallId !== toolCallId) {
222
+ throw new ToolResultNotWaitingError(runId, toolCallId);
223
+ }
224
+
225
+ session.status = "waiting";
226
+ this.scheduleWaitingTimeout(session);
227
+ this.touchSession(session);
228
+
229
+ return await new Promise<{ result: unknown; isError: boolean }>((resolve, reject) => {
230
+ const abortHandler = () => {
231
+ this.clearWaitingTimeout(session);
232
+ session.waitingTool = null;
233
+ session.status = "cancelled";
234
+ reject(new AgentRunCancelledError());
235
+ };
236
+
237
+ session.abortController.signal.addEventListener("abort", abortHandler, { once: true });
238
+ session.waitingTool = {
239
+ toolCallId,
240
+ resolve: (value) => {
241
+ session.abortController.signal.removeEventListener("abort", abortHandler);
242
+ this.clearWaitingTimeout(session);
243
+ session.waitingTool = null;
244
+ session.status = "running";
245
+ this.touchSession(session);
246
+ resolve({ result: value.result, isError: value.isError });
247
+ },
248
+ reject: (reason) => {
249
+ session.abortController.signal.removeEventListener("abort", abortHandler);
250
+ this.clearWaitingTimeout(session);
251
+ session.waitingTool = null;
252
+ reject(reason);
253
+ },
254
+ };
255
+ });
256
+ }
257
+
258
+ submitToolResult(
259
+ runId: string,
260
+ input: { toolCallId: string; result: unknown; isError?: boolean },
261
+ ): SubmitToolResultOutcome {
262
+ const session = this.sessions.get(runId);
263
+ if (!session) {
264
+ throw new RunNotActiveError(runId);
265
+ }
266
+
267
+ const normalized: SubmittedToolResult = {
268
+ toolCallId: input.toolCallId,
269
+ result: input.result,
270
+ isError: Boolean(input.isError),
271
+ key: createToolResultKey(input.result, Boolean(input.isError)),
272
+ };
273
+
274
+ const existingResult = session.submittedResults.get(input.toolCallId);
275
+ if (existingResult) {
276
+ if (existingResult.key === normalized.key) {
277
+ return { accepted: true, duplicate: true };
278
+ }
279
+
280
+ throw new ToolResultConflictError(runId, input.toolCallId);
281
+ }
282
+
283
+ if (
284
+ session.status === "completed" || session.status === "failed" ||
285
+ session.status === "cancelled"
286
+ ) {
287
+ throw new RunNotActiveError(runId);
288
+ }
289
+
290
+ if (!session.waitingTool || session.waitingTool.toolCallId !== input.toolCallId) {
291
+ throw new ToolResultNotWaitingError(runId, input.toolCallId);
292
+ }
293
+
294
+ session.submittedResults.set(input.toolCallId, normalized);
295
+ this.touchSession(session);
296
+ session.waitingTool.resolve(normalized);
297
+ return { accepted: true };
298
+ }
299
+
300
+ cancelRun(runId: string): boolean {
301
+ const session = this.sessions.get(runId);
302
+ if (!session) {
303
+ return false;
304
+ }
305
+
306
+ if (
307
+ session.status === "completed" || session.status === "failed" ||
308
+ session.status === "cancelled"
309
+ ) {
310
+ return false;
311
+ }
312
+
313
+ session.status = "cancelled";
314
+ this.clearWaitingTimeout(session);
315
+ this.clearSessionTimeout(session);
316
+ session.abortController.abort(new AgentRunCancelledError());
317
+ session.waitingTool?.reject(new AgentRunCancelledError());
318
+ session.waitingTool = null;
319
+ this.sessions.delete(runId);
320
+ return true;
321
+ }
322
+
323
+ completeRun(runId: string): void {
324
+ const session = this.sessions.get(runId);
325
+ if (!session) return;
326
+ session.status = "completed";
327
+ this.clearWaitingTimeout(session);
328
+ this.clearSessionTimeout(session);
329
+ session.waitingTool = null;
330
+ this.sessions.delete(runId);
331
+ }
332
+
333
+ failRun(runId: string): void {
334
+ const session = this.sessions.get(runId);
335
+ if (!session) return;
336
+ session.status = "failed";
337
+ this.clearWaitingTimeout(session);
338
+ this.clearSessionTimeout(session);
339
+ session.waitingTool = null;
340
+ this.sessions.delete(runId);
341
+ }
342
+
343
+ getRunStatus(runId: string): SessionStatus | null {
344
+ return this.sessions.get(runId)?.status ?? null;
345
+ }
346
+
347
+ reset(): void {
348
+ for (const session of this.sessions.values()) {
349
+ this.clearWaitingTimeout(session);
350
+ this.clearSessionTimeout(session);
351
+ }
352
+ this.sessions.clear();
353
+ }
354
+ }
355
+
356
+ export const agentRunSessionManager = new AgentRunSessionManager({
357
+ sessionTtlMs: DEFAULT_SESSION_TTL_MS,
358
+ });
@@ -17,7 +17,12 @@ import type {
17
17
  WebSocketUpgrade,
18
18
  } from "../../base.js";
19
19
  import { serverLogger } from "../../../../utils/index.js";
20
- import { getEnvOverlayStorage } from "../../../compat/process.js";
20
+ import {
21
+ env as getEnvObject,
22
+ getEnv,
23
+ getEnvOverlayStorage,
24
+ setEnv,
25
+ } from "../../../compat/process.js";
21
26
  import {
22
27
  createFileWatcher,
23
28
  createWatcherIterator,
@@ -272,22 +277,15 @@ class DenoFileSystemAdapter implements FileSystemAdapter {
272
277
 
273
278
  class DenoEnvironmentAdapter implements EnvironmentAdapter {
274
279
  get(key: string): string | undefined {
275
- if (typeof dntShim.Deno === "undefined" || typeof dntShim.Deno.env === "undefined") return undefined;
276
- return dntShim.Deno.env.get(key);
280
+ return getEnv(key);
277
281
  }
278
282
 
279
283
  set(key: string, value: string): void {
280
- if (typeof dntShim.Deno === "undefined" || typeof dntShim.Deno.env === "undefined") {
281
- throw NOT_SUPPORTED.create({
282
- detail: "DenoEnvironmentAdapter.set() can only be used in Deno runtime",
283
- });
284
- }
285
- dntShim.Deno.env.set(key, value);
284
+ setEnv(key, value);
286
285
  }
287
286
 
288
287
  toObject(): Record<string, string> {
289
- if (typeof dntShim.Deno === "undefined" || typeof dntShim.Deno.env === "undefined") return {};
290
- return dntShim.Deno.env.toObject();
288
+ return getEnvObject();
291
289
  }
292
290
  }
293
291
 
@@ -57,10 +57,46 @@ export function chdir(directory: string): void {
57
57
  throw new Error("chdir() is not supported in this runtime");
58
58
  }
59
59
 
60
+ type EnvOverlayValue = string | null;
61
+ type EnvOverlayStore = Map<string, EnvOverlayValue>;
62
+
63
+ function getEnvOverlayStore(): EnvOverlayStore | null {
64
+ const storage = getEnvOverlayStorage();
65
+ const store = storage?.getStore();
66
+ return store instanceof Map ? store as EnvOverlayStore : null;
67
+ }
68
+
69
+ function getOverlayEnvValue(
70
+ store: EnvOverlayStore | null,
71
+ key: string,
72
+ ): { hasValue: boolean; value: string | undefined } {
73
+ if (!store?.has(key)) {
74
+ return { hasValue: false, value: undefined };
75
+ }
76
+
77
+ const value = store.get(key);
78
+ return { hasValue: true, value: value ?? undefined };
79
+ }
80
+
60
81
  export function env(): Record<string, string> {
61
- if (IS_DENO) return dntShim.Deno.env.toObject();
62
- if (runtimeProcess) return runtimeProcess.env as Record<string, string>;
63
- return {};
82
+ const base = IS_DENO
83
+ ? dntShim.Deno.env.toObject()
84
+ : runtimeProcess
85
+ ? { ...runtimeProcess.env } as Record<string, string>
86
+ : {};
87
+
88
+ const overlay = getEnvOverlayStore();
89
+ if (!overlay) return base;
90
+
91
+ for (const [key, value] of overlay.entries()) {
92
+ if (value === null) {
93
+ delete base[key];
94
+ continue;
95
+ }
96
+ base[key] = value;
97
+ }
98
+
99
+ return base;
64
100
  }
65
101
 
66
102
  /**
@@ -68,6 +104,11 @@ export function env(): Record<string, string> {
68
104
  * Use this for framework-owned runtime configuration that should not be shadowed by tenant env.
69
105
  */
70
106
  export function getHostEnv(key: string): string | undefined {
107
+ const overlayResult = getOverlayEnvValue(getEnvOverlayStore(), key);
108
+ if (overlayResult.hasValue) {
109
+ return overlayResult.value;
110
+ }
111
+
71
112
  if (IS_DENO) return dntShim.Deno.env.get(key);
72
113
  if (runtimeProcess) return runtimeProcess.env[key];
73
114
  return undefined;
@@ -184,6 +225,12 @@ export function getEnvBoolean(
184
225
  }
185
226
 
186
227
  export function setEnv(key: string, value: string): void {
228
+ const overlay = getEnvOverlayStore();
229
+ if (overlay) {
230
+ overlay.set(key, value);
231
+ return;
232
+ }
233
+
187
234
  if (IS_DENO) {
188
235
  dntShim.Deno.env.set(key, value);
189
236
  return;
@@ -196,6 +243,12 @@ export function setEnv(key: string, value: string): void {
196
243
  }
197
244
 
198
245
  export function deleteEnv(key: string): void {
246
+ const overlay = getEnvOverlayStore();
247
+ if (overlay) {
248
+ overlay.set(key, null);
249
+ return;
250
+ }
251
+
199
252
  if (IS_DENO) {
200
253
  dntShim.Deno.env.delete(key);
201
254
  return;
@@ -10,7 +10,7 @@ import { getErrorMessage } from "../errors/veryfront-error.js";
10
10
  import { INVALID_ARGUMENT } from "../errors/index.js";
11
11
  import { enhanceAdapterWithFS } from "../platform/adapters/fs/integration.js";
12
12
  import { isExtendedFSAdapter } from "../platform/adapters/fs/wrapper.js";
13
- import { getEnv } from "../platform/compat/process.js";
13
+ import { getEnv, getHostEnv } from "../platform/compat/process.js";
14
14
  import { initializeEsbuild } from "../platform/compat/esbuild.js";
15
15
  import { logger } from "../utils/index.js";
16
16
  import { isDebugEnabled } from "../utils/constants/env.js";
@@ -256,6 +256,7 @@ export async function bootstrapProd(
256
256
  function validateProductionEnvironment(_adapter: RuntimeAdapter): void {
257
257
  const nodeEnv = getEnv("NODE_ENV") ?? getEnv("DENO_ENV");
258
258
  const proxyMode = getEnv("PROXY_MODE");
259
+ const controlPlanePublicKey = getHostEnv("CHANNEL_DISPATCH_SIGNING_PUBLIC_KEY");
259
260
 
260
261
  // In proxy mode (deployed pods), NODE_ENV must be explicitly set to production
261
262
  if (proxyMode === "1") {
@@ -276,6 +277,17 @@ function validateProductionEnvironment(_adapter: RuntimeAdapter): void {
276
277
  nodeEnv,
277
278
  );
278
279
  }
280
+
281
+ if (!controlPlanePublicKey) {
282
+ logger.error(
283
+ "[Bootstrap:Prod] CRITICAL: CHANNEL_DISPATCH_SIGNING_PUBLIC_KEY is not set in proxy mode. " +
284
+ "Hosted runtimes cannot verify control-plane requests without it.",
285
+ );
286
+ throw INVALID_ARGUMENT.create({
287
+ detail:
288
+ "CHANNEL_DISPATCH_SIGNING_PUBLIC_KEY must be set when running in proxy mode (PROXY_MODE=1)",
289
+ });
290
+ }
279
291
  }
280
292
 
281
293
  // Log effective configuration for debugging
@@ -0,0 +1,86 @@
1
+ import * as dntShim from "../../../../_dnt.shims.js";
2
+ import {
3
+ ControlPlaneRequestError,
4
+ verifyControlPlaneRequest,
5
+ } from "../../../internal-agents/control-plane-auth.js";
6
+ import {
7
+ type AgentRunSessionManager,
8
+ agentRunSessionManager,
9
+ } from "../../../internal-agents/session-manager.js";
10
+ import {
11
+ INTERNAL_AGENT_CONTROL_PLANE_MAX_BODY_BYTES,
12
+ InternalAgentRequestBodyTooLargeError,
13
+ readInternalAgentRequestBody,
14
+ } from "../../../internal-agents/request-body.js";
15
+ import { BaseHandler } from "../response/base.js";
16
+ import type { HandlerContext, HandlerMetadata, HandlerPriority, HandlerResult } from "../types.js";
17
+ import { PRIORITY_MEDIUM_API } from "../../../utils/constants/index.js";
18
+
19
+ const CANCEL_PATH_REGEX = /^\/internal\/agents\/runs\/([^/]+)$/;
20
+
21
+ function getRunId(pathname: string): string | null {
22
+ return CANCEL_PATH_REGEX.exec(pathname)?.[1] ?? null;
23
+ }
24
+
25
+ export class AgentRunCancelHandler extends BaseHandler {
26
+ metadata: HandlerMetadata = {
27
+ name: "AgentRunCancelHandler",
28
+ priority: PRIORITY_MEDIUM_API as HandlerPriority,
29
+ patterns: [{ pattern: "/internal/agents/runs/", prefix: true, method: "DELETE" }],
30
+ };
31
+
32
+ constructor(private readonly sessionManager: AgentRunSessionManager = agentRunSessionManager) {
33
+ super();
34
+ }
35
+
36
+ async handle(req: dntShim.Request, ctx: HandlerContext): Promise<HandlerResult> {
37
+ if (!this.shouldHandle(req, ctx)) {
38
+ return this.continue();
39
+ }
40
+
41
+ const runId = getRunId(new URL(req.url).pathname);
42
+ if (!runId) {
43
+ return this.continue();
44
+ }
45
+
46
+ return this.withProxyContext(ctx, async () => {
47
+ const builder = this.createResponseBuilder(ctx)
48
+ .withCORS(req, ctx.securityConfig?.cors)
49
+ .withSecurity(ctx.securityConfig ?? undefined, req);
50
+
51
+ try {
52
+ const rawBody = await readInternalAgentRequestBody(
53
+ req,
54
+ INTERNAL_AGENT_CONTROL_PLANE_MAX_BODY_BYTES,
55
+ );
56
+ await verifyControlPlaneRequest(req, ctx, rawBody, {
57
+ expectedSubject: runId,
58
+ expectedSurface: "studio",
59
+ });
60
+
61
+ const accepted = this.sessionManager.cancelRun(runId);
62
+ if (accepted) {
63
+ return this.respond(builder.json({ accepted: true }, 202));
64
+ }
65
+
66
+ return this.respond(builder.build(null, 204));
67
+ } catch (error) {
68
+ if (error instanceof InternalAgentRequestBodyTooLargeError) {
69
+ return this.respond(builder.json({ error: error.message }, error.status));
70
+ }
71
+
72
+ if (error instanceof ControlPlaneRequestError) {
73
+ return this.respond(builder.json({ error: error.message }, error.status));
74
+ }
75
+
76
+ this.logWarn("Internal agent run cancel failed", {
77
+ error: error instanceof Error ? error.message : String(error),
78
+ runId,
79
+ projectId: ctx.projectId,
80
+ projectSlug: ctx.projectSlug,
81
+ });
82
+ return this.respond(builder.json({ error: "Internal cancel failed" }, 500));
83
+ }
84
+ });
85
+ }
86
+ }