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.
- 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 +3 -40
- package/esm/src/channels/invoke.d.ts.map +1 -1
- package/esm/src/channels/invoke.js +9 -106
- 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/bootstrap.js +9 -1
- 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/{channel-assistants.handler.d.ts → internal-agents-list.handler.d.ts} +4 -4
- 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 -2
- 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 +12 -157
- 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/bootstrap.ts +13 -1
- 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 -2
- package/esm/src/server/handlers/request/channel-assistants.handler.d.ts.map +0 -1
- package/esm/src/server/handlers/request/channel-assistants.handler.js +0 -71
- 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 {
|
|
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
|
-
|
|
276
|
-
return dntShim.Deno.env.get(key);
|
|
280
|
+
return getEnv(key);
|
|
277
281
|
}
|
|
278
282
|
|
|
279
283
|
set(key: string, value: string): void {
|
|
280
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
}
|