@assemblyline-agents/node 5.0.10 → 6.0.1
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/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +6 -5
- package/dist/adapters.js.map +1 -1
- package/dist/admin-http-routes.d.ts +3 -0
- package/dist/admin-http-routes.d.ts.map +1 -0
- package/dist/admin-http-routes.js +285 -0
- package/dist/admin-http-routes.js.map +1 -0
- package/dist/auth.d.ts +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +2 -16
- package/dist/auth.js.map +1 -1
- package/dist/conversation-http-routes.d.ts +3 -0
- package/dist/conversation-http-routes.d.ts.map +1 -0
- package/dist/conversation-http-routes.js +206 -0
- package/dist/conversation-http-routes.js.map +1 -0
- package/dist/core-http-routes.d.ts +3 -0
- package/dist/core-http-routes.d.ts.map +1 -0
- package/dist/core-http-routes.js +112 -0
- package/dist/core-http-routes.js.map +1 -0
- package/dist/http-io.d.ts +1 -1
- package/dist/http-io.d.ts.map +1 -1
- package/dist/index.d.ts +9 -39
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -882
- package/dist/index.js.map +1 -1
- package/dist/ingress-http-routes.d.ts +3 -0
- package/dist/ingress-http-routes.d.ts.map +1 -0
- package/dist/ingress-http-routes.js +210 -0
- package/dist/ingress-http-routes.js.map +1 -0
- package/dist/model-auth.d.ts.map +1 -1
- package/dist/model-auth.js +22 -21
- package/dist/model-auth.js.map +1 -1
- package/dist/node-types.d.ts +24 -0
- package/dist/node-types.d.ts.map +1 -0
- package/dist/node-types.js +2 -0
- package/dist/node-types.js.map +1 -0
- package/dist/provider-resolver.d.ts +2 -2
- package/dist/provider-resolver.d.ts.map +1 -1
- package/dist/provider-resolver.js +2 -2
- package/dist/provider-resolver.js.map +1 -1
- package/dist/run-http-routes.d.ts +3 -0
- package/dist/run-http-routes.d.ts.map +1 -0
- package/dist/run-http-routes.js +252 -0
- package/dist/run-http-routes.js.map +1 -0
- package/dist/runtime-http-route.d.ts +14 -0
- package/dist/runtime-http-route.d.ts.map +1 -0
- package/dist/runtime-http-route.js +2 -0
- package/dist/runtime-http-route.js.map +1 -0
- package/dist/secrets.d.ts +3 -13
- package/dist/secrets.d.ts.map +1 -1
- package/dist/secrets.js +6 -16
- package/dist/secrets.js.map +1 -1
- package/dist/usage-http.d.ts +2 -2
- package/dist/usage-http.d.ts.map +1 -1
- package/dist/usage-http.js +14 -78
- package/dist/usage-http.js.map +1 -1
- package/package.json +11 -11
package/dist/index.js
CHANGED
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { adapter
|
|
4
|
-
import { AssemblyLineRuntime,
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { HttpRequestError, firstHeaderValue, maxRequestBodyBytes, normalizeHeaders, readBody, send, summarizeRun } from "./http-io.js";
|
|
9
|
-
import { serveRunEventStream, serveStreamingRun } from "./run-stream-http.js";
|
|
3
|
+
import { adapter } from "@assemblyline-agents/core";
|
|
4
|
+
import { AssemblyLineRuntime, loadRuntimeBundle, } from "@assemblyline-agents/runtime";
|
|
5
|
+
import { createBlobAdapter, createConnectionStores, createMediaProcessors, createModelCredentialStore, createSandboxAdapter, createSelfImprovementStores, createStateAdapter, } from "./adapters.js";
|
|
6
|
+
import { logHttpRequest, validateRuntimeHttpSecurity } from "./auth.js";
|
|
7
|
+
import { HttpRequestError, send } from "./http-io.js";
|
|
10
8
|
import { createCredentialBoundary, resolveGatewayCredentials, resolveGatewayEnvironment } from "./secrets.js";
|
|
11
|
-
import { startRunReplay } from "./run-replay-http.js";
|
|
12
|
-
import { matchRoute, parseAgentControlRoute, parseRunFeedbackRoute, parseRunReplayRoute, parseRunResumeRoute, parseRunRoute, parseSelfImprovementRoute } from "./route-match.js";
|
|
13
9
|
import { resolveIngressRateLimit } from "./rate-limit.js";
|
|
14
|
-
import { createNodeRuntimeHandle, installSignalHandlers } from "./lifecycle.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
10
|
+
import { createNodeRuntimeHandle, installSignalHandlers, } from "./lifecycle.js";
|
|
11
|
+
import { handleAdminHttpRoutes } from "./admin-http-routes.js";
|
|
12
|
+
import { handleCoreHttpRoutes } from "./core-http-routes.js";
|
|
13
|
+
import { handleConversationHttpRoutes } from "./conversation-http-routes.js";
|
|
14
|
+
import { handleIngressHttpRoutes } from "./ingress-http-routes.js";
|
|
15
|
+
import { handleRunHttpRoutes } from "./run-http-routes.js";
|
|
16
|
+
export { resolveProvider, } from "./provider-resolver.js";
|
|
17
|
+
export { createCredentialBoundary, resolveCredentialInventory, resolveGatewayCredentials, resolveGatewayEnvironment, } from "./secrets.js";
|
|
19
18
|
export { createModelCredentialStore } from "./adapters.js";
|
|
19
|
+
export { createMemoryRateLimiterStore, parseRateLimitSpec, resolveIngressRateLimit, } from "./rate-limit.js";
|
|
20
|
+
export { createNodeRuntimeHandle, installSignalHandlers, } from "./lifecycle.js";
|
|
20
21
|
export { runModelProviderAuth } from "./model-auth.js";
|
|
21
|
-
export { createCredentialBoundary, resolveCredentialInventory, resolveGatewayCredentials, resolveGatewayEnvironment } from "./secrets.js";
|
|
22
|
-
export { createMemoryRateLimiterStore, parseRateLimitSpec, resolveIngressRateLimit } from "./rate-limit.js";
|
|
23
|
-
export { createNodeRuntimeHandle, installSignalHandlers } from "./lifecycle.js";
|
|
24
22
|
export function nodeAdapter(options = {}) {
|
|
25
23
|
return {
|
|
26
24
|
...adapter("node", options),
|
|
27
25
|
kind: "node",
|
|
28
|
-
capabilities: ["http-runtime", "hosted-container-runtime", "local-server"]
|
|
26
|
+
capabilities: ["http-runtime", "hosted-container-runtime", "local-server"],
|
|
29
27
|
};
|
|
30
28
|
}
|
|
31
29
|
export async function createProductionRuntimeOptions(options) {
|
|
@@ -36,7 +34,7 @@ export async function createProductionRuntimeOptions(options) {
|
|
|
36
34
|
manifest: runtimeOptions.manifest,
|
|
37
35
|
env: options.env ?? process.env,
|
|
38
36
|
artifactRoot,
|
|
39
|
-
devMode: options.devMode ?? false
|
|
37
|
+
devMode: options.devMode ?? false,
|
|
40
38
|
});
|
|
41
39
|
const gatewayEnv = await resolveGatewayEnvironment(boundary, runtimeOptions.manifest);
|
|
42
40
|
const gatewayCredentials = await resolveGatewayCredentials(boundary, runtimeOptions.manifest);
|
|
@@ -64,13 +62,15 @@ export async function createProductionRuntimeOptions(options) {
|
|
|
64
62
|
credentialStore: boundary.store,
|
|
65
63
|
gatewayCredentials,
|
|
66
64
|
env: gatewayEnv,
|
|
67
|
-
devMode: options.devMode ?? false
|
|
65
|
+
devMode: options.devMode ?? false,
|
|
68
66
|
};
|
|
69
|
-
const callbackBaseUrl = boundary.config.ASSEMBLY_LINE_CONNECTION_CALLBACK_BASE_URL ??
|
|
67
|
+
const callbackBaseUrl = boundary.config.ASSEMBLY_LINE_CONNECTION_CALLBACK_BASE_URL ??
|
|
68
|
+
boundary.config.ASSEMBLY_LINE_PUBLIC_URL;
|
|
70
69
|
if (callbackBaseUrl)
|
|
71
70
|
out.connectionCallbackBaseUrl = callbackBaseUrl;
|
|
72
71
|
// Production default: bound run concurrency unless the operator chose a limit.
|
|
73
|
-
if (!(options.devMode ?? false) &&
|
|
72
|
+
if (!(options.devMode ?? false) &&
|
|
73
|
+
boundary.config.ASSEMBLY_LINE_MAX_CONCURRENT_RUNS === undefined) {
|
|
74
74
|
out.maxConcurrentRuns = 16;
|
|
75
75
|
}
|
|
76
76
|
return out;
|
|
@@ -95,854 +95,20 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
const url = new URL(request.url, "http://localhost");
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
if (request.method === "GET" && url.pathname === "/readyz") {
|
|
116
|
-
// Readiness (vs /health liveness): 503 while draining so load
|
|
117
|
-
// balancers stop routing new traffic during graceful shutdown.
|
|
118
|
-
if (!await authorizeOrSend(request, response, runtime, options, "public-health", url.pathname))
|
|
119
|
-
return;
|
|
120
|
-
if (drain.draining) {
|
|
121
|
-
send(response, 503, { ...revisionHealth(options.manifest), ok: false, draining: true });
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
send(response, 200, revisionHealth(options.manifest));
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (request.method === "GET" && url.pathname === "/manifest") {
|
|
128
|
-
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
129
|
-
return;
|
|
130
|
-
send(response, 200, options.manifest);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
if (request.method === "GET" && url.pathname === "/routes") {
|
|
134
|
-
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
135
|
-
return;
|
|
136
|
-
send(response, 200, options.manifest.routeTable);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
const agentControlRoute = parseAgentControlRoute(url.pathname);
|
|
140
|
-
if (agentControlRoute && request.method === "GET" && agentControlRoute.kind === "control") {
|
|
141
|
-
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
142
|
-
return;
|
|
143
|
-
send(response, 200, await runtime.getAgentControl());
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (agentControlRoute && request.method === "POST" && agentControlRoute.kind !== "control") {
|
|
147
|
-
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
148
|
-
return;
|
|
149
|
-
if (!await rateLimitOrSend(request, response, rateLimit, "agent-control", url.pathname))
|
|
150
|
-
return;
|
|
151
|
-
const requestId = firstHeaderValue(request.headers["x-request-id"] ?? request.headers["x-railway-request-id"]);
|
|
152
|
-
const metadata = {
|
|
153
|
-
source: "http",
|
|
154
|
-
path: url.pathname,
|
|
155
|
-
...(requestId ? { requestId } : {})
|
|
156
|
-
};
|
|
157
|
-
const result = agentControlRoute.kind === "quiesce" || agentControlRoute.kind === "resume"
|
|
158
|
-
? await runtime.setAgentMaintenance(agentControlRoute.kind === "quiesce", metadata)
|
|
159
|
-
: await runtime.setAgentEnabled(agentControlRoute.kind === "enable", metadata);
|
|
160
|
-
send(response, 200, result);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
if (isWorkspaceAdminPath(url.pathname)) {
|
|
164
|
-
const method = request.method ?? "GET";
|
|
165
|
-
if (!["GET", "POST", "DELETE"].includes(method)) {
|
|
166
|
-
send(response, 405, { error: "Method not allowed." });
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
if (!await authorizeOrSend(request, response, runtime, options, method === "GET" ? "admin-read" : "agent-control", url.pathname))
|
|
170
|
-
return;
|
|
171
|
-
try {
|
|
172
|
-
const body = method === "POST"
|
|
173
|
-
? (await readBody(request, maxRequestBodyBytes(runtime, options))).body
|
|
174
|
-
: {};
|
|
175
|
-
send(response, 200, await workspaceAdminHttpResult(runtime, method, url.pathname, body, url.searchParams));
|
|
176
|
-
}
|
|
177
|
-
catch (error) {
|
|
178
|
-
send(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
179
|
-
}
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
const conversationRoute = parseConversationHttpRoute(url.pathname);
|
|
183
|
-
if (conversationRoute?.kind === "collection" && request.method === "GET") {
|
|
184
|
-
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
185
|
-
return;
|
|
186
|
-
if (!runtime.state.listConversations) {
|
|
187
|
-
send(response, 501, { error: "The configured state adapter does not support conversation listing." });
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
const cursorValue = url.searchParams.get("cursor");
|
|
191
|
-
const before = decodeConversationCursor(cursorValue);
|
|
192
|
-
if (cursorValue && !before) {
|
|
193
|
-
send(response, 400, { error: "Invalid conversation cursor." });
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
const limitParam = Number(url.searchParams.get("limit"));
|
|
197
|
-
const limit = Number.isFinite(limitParam) && limitParam > 0 ? Math.min(Math.floor(limitParam), 100) : 40;
|
|
198
|
-
const archivedValue = url.searchParams.get("archived");
|
|
199
|
-
const archived = archivedValue === "true" ? true : archivedValue === "all" ? undefined : false;
|
|
200
|
-
const listed = await runtime.state.listConversations({
|
|
201
|
-
agentScope: directAgentScope(options.manifest),
|
|
202
|
-
channel: url.searchParams.get("channel") || DIRECT_CHANNEL,
|
|
203
|
-
...(url.searchParams.get("subject") ? { subject: url.searchParams.get("subject") } : {}),
|
|
204
|
-
...(archived !== undefined ? { archived } : {}),
|
|
205
|
-
...(before ? { before } : {}),
|
|
206
|
-
limit: limit + 1
|
|
207
|
-
});
|
|
208
|
-
const hasMore = listed.length > limit;
|
|
209
|
-
const conversations = listed.slice(0, limit);
|
|
210
|
-
send(response, 200, {
|
|
211
|
-
conversations,
|
|
212
|
-
nextCursor: hasMore && conversations.length > 0
|
|
213
|
-
? encodeConversationCursor(conversations[conversations.length - 1])
|
|
214
|
-
: null
|
|
215
|
-
});
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
if (conversationRoute?.kind === "messages" && request.method === "GET") {
|
|
219
|
-
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
220
|
-
return;
|
|
221
|
-
const conversation = await runtime.state.getConversation(conversationRoute.conversationId);
|
|
222
|
-
if (!conversation || !conversationBelongsToAgent(conversation, directAgentScope(options.manifest))) {
|
|
223
|
-
send(response, 404, { error: "Conversation not found." });
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
const cursorValue = url.searchParams.get("before");
|
|
227
|
-
const before = decodeMessageCursor(cursorValue);
|
|
228
|
-
if (cursorValue && !before) {
|
|
229
|
-
send(response, 400, { error: "Invalid message cursor." });
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
const limitParam = Number(url.searchParams.get("limit"));
|
|
233
|
-
const limit = Number.isFinite(limitParam) && limitParam > 0 ? Math.min(Math.floor(limitParam), 100) : 50;
|
|
234
|
-
const listed = runtime.state.listMessages
|
|
235
|
-
? await runtime.state.listMessages({
|
|
236
|
-
conversationId: conversation.id,
|
|
237
|
-
...(before ? { before } : {}),
|
|
238
|
-
limit: limit + 1
|
|
239
|
-
})
|
|
240
|
-
: before
|
|
241
|
-
? []
|
|
242
|
-
: await runtime.state.listRecentMessages(conversation.id, limit + 1);
|
|
243
|
-
if (before && !runtime.state.listMessages) {
|
|
244
|
-
send(response, 501, { error: "The configured state adapter does not support message pagination." });
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
const hasMore = listed.length > limit;
|
|
248
|
-
const messages = hasMore ? listed.slice(1) : listed;
|
|
249
|
-
send(response, 200, {
|
|
250
|
-
messages,
|
|
251
|
-
nextCursor: hasMore && messages.length > 0 ? encodeMessageCursor(messages[0]) : null
|
|
252
|
-
});
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
if (conversationRoute?.kind === "record" && request.method === "PATCH") {
|
|
256
|
-
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
257
|
-
return;
|
|
258
|
-
if (!runtime.state.updateConversation) {
|
|
259
|
-
send(response, 501, { error: "The configured state adapter does not support conversation updates." });
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
const existing = await runtime.state.getConversation(conversationRoute.conversationId);
|
|
263
|
-
if (!existing || !conversationBelongsToAgent(existing, directAgentScope(options.manifest))) {
|
|
264
|
-
send(response, 404, { error: "Conversation not found." });
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
268
|
-
const patch = directConversationPatch(body);
|
|
269
|
-
if (Object.keys(patch).length === 0) {
|
|
270
|
-
send(response, 400, { error: "Provide a title, archived flag, or metadata patch." });
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
send(response, 200, await runtime.state.updateConversation(existing.id, patch));
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
if (conversationRoute?.kind === "turns" && request.method === "POST") {
|
|
277
|
-
channel = DIRECT_CHANNEL;
|
|
278
|
-
if (!apiRunCreationEnabled(runtime, options)) {
|
|
279
|
-
send(response, 403, { error: "API run creation is disabled." });
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
if (!await authorizeOrSend(request, response, runtime, options, "run-create", url.pathname))
|
|
283
|
-
return;
|
|
284
|
-
if (!await rateLimitOrSend(request, response, rateLimit, "run-create", url.pathname, DIRECT_CHANNEL))
|
|
285
|
-
return;
|
|
286
|
-
const existing = await runtime.state.getConversation(conversationRoute.conversationId);
|
|
287
|
-
if (existing && !conversationBelongsToAgent(existing, directAgentScope(options.manifest))) {
|
|
288
|
-
send(response, 404, { error: "Conversation not found." });
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
if (existing?.channel && existing.channel !== DIRECT_CHANNEL) {
|
|
292
|
-
send(response, 409, { error: `Conversation belongs to channel ${existing.channel}.` });
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
if (existing?.archivedAt) {
|
|
296
|
-
send(response, 409, { error: "Archived conversations cannot accept new turns." });
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
300
|
-
const idempotencyKey = firstHeaderValue(request.headers["idempotency-key"]);
|
|
301
|
-
const direct = directTurnFromBody({
|
|
302
|
-
conversationId: conversationRoute.conversationId,
|
|
303
|
-
body,
|
|
304
|
-
...(idempotencyKey ? { idempotencyKey } : {})
|
|
305
|
-
});
|
|
306
|
-
if (!direct.turn.message && !(direct.turn.attachments?.length)) {
|
|
307
|
-
send(response, 400, { error: "A message or attachment is required." });
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
const workspaceId = stringBodyValue(body.workspaceId);
|
|
311
|
-
const projectId = stringBodyValue(body.projectId);
|
|
312
|
-
const tenantId = stringBodyValue(body.tenantId);
|
|
313
|
-
const runOptions = {
|
|
314
|
-
message: direct.turn.message,
|
|
315
|
-
channel: DIRECT_CHANNEL,
|
|
316
|
-
approve: direct.approve,
|
|
317
|
-
turn: direct.turn,
|
|
318
|
-
...(workspaceId ? { workspaceId } : {}),
|
|
319
|
-
...(projectId ? { projectId } : {}),
|
|
320
|
-
...(tenantId ? { tenantId } : {})
|
|
321
|
-
};
|
|
322
|
-
if (body.sandboxCredentials && typeof body.sandboxCredentials === "object" && !Array.isArray(body.sandboxCredentials)) {
|
|
323
|
-
Object.assign(runOptions, { sandboxCredentials: body.sandboxCredentials });
|
|
324
|
-
}
|
|
325
|
-
try {
|
|
326
|
-
if (direct.stream) {
|
|
327
|
-
await serveStreamingRun(runtime, response, (onRunCreated) => runtime.run({ ...runOptions, onRunCreated }), (result) => summarizeRun(result));
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
const result = await runtime.run(runOptions);
|
|
331
|
-
send(response, 200, summarizeRun(result));
|
|
332
|
-
}
|
|
333
|
-
catch (error) {
|
|
334
|
-
if (error instanceof RunCapacityError) {
|
|
335
|
-
send(response, 429, { error: "Run capacity exhausted. Retry later.", retryAfterMs: error.retryAfterMs }, {
|
|
336
|
-
"retry-after": String(retryAfterSeconds(error.retryAfterMs))
|
|
337
|
-
});
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
throw error;
|
|
341
|
-
}
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
if (request.method === "GET" && url.pathname === "/runs") {
|
|
345
|
-
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
346
|
-
return;
|
|
347
|
-
// Light list: no per-run detail (avoids the 1+4N fan-out that hangs on
|
|
348
|
-
// busy agents). Bounded to the most recent runs; ?limit overrides.
|
|
349
|
-
const limitParam = Number(url.searchParams.get("limit"));
|
|
350
|
-
const limit = Number.isFinite(limitParam) && limitParam > 0 ? Math.min(limitParam, 1000) : 200;
|
|
351
|
-
send(response, 200, await queryAgentRuns(runtime.state, options.manifest, { includeDetails: false, limit }));
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
const memoryAdminRoute = parseMemoryAdminRoute(url.pathname);
|
|
355
|
-
if (memoryAdminRoute) {
|
|
356
|
-
const agentScope = durableAgentScopeForManifest(options.manifest);
|
|
357
|
-
if (request.method === "GET") {
|
|
358
|
-
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
359
|
-
return;
|
|
360
|
-
const result = memoryAdminRoute.kind === "list"
|
|
361
|
-
? await listMemoryAdmin(runtime.state, agentScope, url.searchParams)
|
|
362
|
-
: await readMemoryAdmin(runtime.state, agentScope, memoryAdminRoute.path);
|
|
363
|
-
send(response, result.status, result.body);
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
if (request.method === "DELETE" && memoryAdminRoute.kind === "document") {
|
|
367
|
-
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
368
|
-
return;
|
|
369
|
-
if (!await rateLimitOrSend(request, response, rateLimit, "agent-control", url.pathname))
|
|
370
|
-
return;
|
|
371
|
-
const result = await deleteMemoryAdmin(runtime.state, agentScope, memoryAdminRoute.path, url.searchParams);
|
|
372
|
-
send(response, result.status, result.body);
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
send(response, 405, { error: "Method not allowed." });
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
const selfImprovementRoute = parseSelfImprovementRoute(url.pathname);
|
|
379
|
-
const selfImprovementSurface = url.searchParams.get("surface")?.trim() || "root";
|
|
380
|
-
if (selfImprovementRoute && request.method === "GET") {
|
|
381
|
-
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
382
|
-
return;
|
|
383
|
-
if (selfImprovementRoute.kind === "pending") {
|
|
384
|
-
send(response, 200, { surface: selfImprovementSurface, changes: await runtime.listPendingSkillChanges(selfImprovementSurface) });
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
if (selfImprovementRoute.kind === "reviews") {
|
|
388
|
-
send(response, 200, { surface: selfImprovementSurface, reviews: await runtime.listBackgroundReviews(selfImprovementSurface) });
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (selfImprovementRoute.kind === "revisions") {
|
|
392
|
-
send(response, 200, { surface: selfImprovementSurface, name: selfImprovementRoute.name, revisions: await runtime.listSkillRevisions(selfImprovementRoute.name, selfImprovementSurface) });
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
send(response, 405, { error: "Method not allowed." });
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
if (selfImprovementRoute && request.method === "POST") {
|
|
399
|
-
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
400
|
-
return;
|
|
401
|
-
if (!await rateLimitOrSend(request, response, rateLimit, "agent-control", url.pathname))
|
|
402
|
-
return;
|
|
403
|
-
if (selfImprovementRoute.kind === "approve") {
|
|
404
|
-
try {
|
|
405
|
-
send(response, 200, await runtime.approveSkillChange(selfImprovementRoute.id, selfImprovementSurface));
|
|
406
|
-
}
|
|
407
|
-
catch (error) {
|
|
408
|
-
if (error instanceof Error && /not found/i.test(error.message)) {
|
|
409
|
-
send(response, 404, { error: error.message });
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
throw error;
|
|
413
|
-
}
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
if (selfImprovementRoute.kind === "reject") {
|
|
417
|
-
try {
|
|
418
|
-
await runtime.rejectSkillChange(selfImprovementRoute.id, selfImprovementSurface);
|
|
419
|
-
send(response, 200, { rejected: true, id: selfImprovementRoute.id, surface: selfImprovementSurface });
|
|
420
|
-
}
|
|
421
|
-
catch (error) {
|
|
422
|
-
if (error instanceof Error && /not found/i.test(error.message)) {
|
|
423
|
-
send(response, 404, { error: error.message });
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
throw error;
|
|
427
|
-
}
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
if (selfImprovementRoute.kind === "restore") {
|
|
431
|
-
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
432
|
-
const revision = numberBodyValue(body.revision);
|
|
433
|
-
if (revision === undefined || revision < 1 || !Number.isInteger(revision)) {
|
|
434
|
-
send(response, 400, { error: "A positive integer revision is required." });
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
try {
|
|
438
|
-
send(response, 200, await runtime.restoreSkillRevision(selfImprovementRoute.name, revision, selfImprovementSurface));
|
|
439
|
-
}
|
|
440
|
-
catch (error) {
|
|
441
|
-
if (error instanceof Error && /not found/i.test(error.message)) {
|
|
442
|
-
send(response, 404, { error: error.message });
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
throw error;
|
|
446
|
-
}
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
send(response, 405, { error: "Method not allowed." });
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
if (request.method === "GET" && url.pathname === "/usage") {
|
|
453
|
-
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
454
|
-
return;
|
|
455
|
-
try {
|
|
456
|
-
send(response, 200, await usageHttpResult(runtime.state, url));
|
|
457
|
-
}
|
|
458
|
-
catch (error) {
|
|
459
|
-
send(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
460
|
-
}
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
if (request.method === "POST" && url.pathname === "/usage/reconcile") {
|
|
464
|
-
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
465
|
-
return;
|
|
466
|
-
try {
|
|
467
|
-
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
468
|
-
send(response, 200, await usageReconciliationHttpResult(runtime.state, runtime.env, body));
|
|
469
|
-
}
|
|
470
|
-
catch (error) {
|
|
471
|
-
send(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
472
|
-
}
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
if ((request.method === "GET" || request.method === "POST")
|
|
476
|
-
&& url.pathname === "/assembly-line/automations/tick") {
|
|
477
|
-
if (!await authorizeOrSend(request, response, runtime, options, "scheduler", url.pathname))
|
|
478
|
-
return;
|
|
479
|
-
if (!await rateLimitOrSend(request, response, rateLimit, "scheduler", url.pathname))
|
|
480
|
-
return;
|
|
481
|
-
const body = request.method === "POST" ? (await readBody(request, maxRequestBodyBytes(runtime, options))).body : {};
|
|
482
|
-
const nowValue = stringBodyValue(body.now) ?? url.searchParams.get("now") ?? undefined;
|
|
483
|
-
const lookbackValue = numberBodyValue(body.lookbackMs) ?? numberQueryValue(url.searchParams.get("lookbackMs"));
|
|
484
|
-
const tickOptions = {};
|
|
485
|
-
if (nowValue)
|
|
486
|
-
tickOptions.now = new Date(nowValue);
|
|
487
|
-
if (lookbackValue !== undefined)
|
|
488
|
-
tickOptions.lookbackMs = lookbackValue;
|
|
489
|
-
if (tickOptions.now && Number.isNaN(tickOptions.now.getTime())) {
|
|
490
|
-
send(response, 400, { error: "Invalid scheduler now timestamp." });
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
const results = await runtime.runDueAutomations(tickOptions);
|
|
494
|
-
send(response, 200, { ok: true, results });
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
if (request.method === "POST" && url.pathname === "/assembly-line/automations/events") {
|
|
498
|
-
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
499
|
-
return;
|
|
500
|
-
if (!await rateLimitOrSend(request, response, rateLimit, "run-create", url.pathname))
|
|
501
|
-
return;
|
|
502
|
-
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
503
|
-
const source = stringBodyValue(body.source);
|
|
504
|
-
const event = stringBodyValue(body.event);
|
|
505
|
-
const eventId = stringBodyValue(body.eventId);
|
|
506
|
-
const payload = body.payload;
|
|
507
|
-
if (!source || !event || !eventId || !payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
508
|
-
send(response, 400, { error: "Automation events require source, event, eventId, and an object payload." });
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
const automationEvent = {
|
|
512
|
-
source,
|
|
513
|
-
event,
|
|
514
|
-
eventId,
|
|
515
|
-
payload: payload
|
|
516
|
-
};
|
|
517
|
-
const occurredAt = stringBodyValue(body.occurredAt);
|
|
518
|
-
const userId = stringBodyValue(body.userId);
|
|
519
|
-
const conversationId = stringBodyValue(body.conversationId);
|
|
520
|
-
const projectId = stringBodyValue(body.projectId);
|
|
521
|
-
if (occurredAt && Number.isNaN(new Date(occurredAt).getTime())) {
|
|
522
|
-
send(response, 400, { error: "Automation event occurredAt must be a valid ISO timestamp." });
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
if (occurredAt)
|
|
526
|
-
automationEvent.occurredAt = occurredAt;
|
|
527
|
-
if (userId)
|
|
528
|
-
automationEvent.userId = userId;
|
|
529
|
-
if (conversationId)
|
|
530
|
-
automationEvent.conversationId = conversationId;
|
|
531
|
-
if (projectId)
|
|
532
|
-
automationEvent.projectId = projectId;
|
|
533
|
-
if (body.delivery && typeof body.delivery === "object" && !Array.isArray(body.delivery)) {
|
|
534
|
-
automationEvent.delivery = body.delivery;
|
|
535
|
-
}
|
|
536
|
-
if (body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)) {
|
|
537
|
-
automationEvent.metadata = body.metadata;
|
|
538
|
-
}
|
|
539
|
-
const results = await runtime.dispatchAutomationEvent(automationEvent);
|
|
540
|
-
send(response, 200, { ok: true, matched: results.length, results });
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
if (request.method === "POST" && url.pathname === "/assembly-line/connections/events/reconcile") {
|
|
544
|
-
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
545
|
-
return;
|
|
546
|
-
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
547
|
-
const principal = stringBodyValue(body.userId) && stringBodyValue(body.issuer)
|
|
548
|
-
? { type: "user", id: stringBodyValue(body.userId), issuer: stringBodyValue(body.issuer) }
|
|
549
|
-
: undefined;
|
|
550
|
-
const connection = stringBodyValue(body.connection);
|
|
551
|
-
const publicUrl = stringBodyValue(body.publicUrl);
|
|
552
|
-
const results = await runtime.reconcileConnectionEvents({
|
|
553
|
-
...(connection ? { connection } : {}),
|
|
554
|
-
...(publicUrl ? { publicUrl } : {}),
|
|
555
|
-
...(principal ? { principal } : {}),
|
|
556
|
-
force: body.force === true
|
|
557
|
-
});
|
|
558
|
-
send(response, 200, { ok: true, results });
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
if (request.method === "GET" && url.pathname === "/assembly-line/connections/events") {
|
|
562
|
-
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
563
|
-
return;
|
|
564
|
-
const results = await runtime.checkConnectionEvents(url.searchParams.get("connection") ?? undefined);
|
|
565
|
-
send(response, 200, { ok: true, results });
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
if (request.method === "GET" && url.pathname.startsWith("/runs/")) {
|
|
569
|
-
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
570
|
-
return;
|
|
571
|
-
const runRoute = parseRunRoute(url.pathname);
|
|
572
|
-
if (!runRoute) {
|
|
573
|
-
send(response, 404, { error: "Not found." });
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
if (runRoute.kind === "stream") {
|
|
577
|
-
if (!await runtime.state.getRun(runRoute.runId)) {
|
|
578
|
-
send(response, 404, { error: `Run not found: ${runRoute.runId}` });
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
const lastEventId = firstHeaderValue(request.headers["last-event-id"]) ?? url.searchParams.get("lastEventId") ?? undefined;
|
|
582
|
-
await serveRunEventStream(runtime, response, runRoute.runId, lastEventId);
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
const paginatedTimeline = runRoute.kind === "timeline" && url.searchParams.has("limit");
|
|
586
|
-
const before = numberQueryValue(url.searchParams.get("before"));
|
|
587
|
-
const after = numberQueryValue(url.searchParams.get("after"));
|
|
588
|
-
const limit = numberQueryValue(url.searchParams.get("limit"));
|
|
589
|
-
if (paginatedTimeline && limit === undefined) {
|
|
590
|
-
send(response, 400, { error: "Run timeline limit must be a number." });
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
let timeline;
|
|
594
|
-
try {
|
|
595
|
-
timeline = paginatedTimeline
|
|
596
|
-
? await loadRunTimelinePage(runtime, runRoute.runId, {
|
|
597
|
-
...(before !== undefined ? { before } : {}),
|
|
598
|
-
...(after !== undefined ? { after } : {}),
|
|
599
|
-
limit: limit
|
|
600
|
-
})
|
|
601
|
-
: await loadRunTimeline(runtime, runRoute.runId);
|
|
602
|
-
}
|
|
603
|
-
catch (error) {
|
|
604
|
-
if (!paginatedTimeline)
|
|
605
|
-
throw error;
|
|
606
|
-
send(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
if (!timeline) {
|
|
610
|
-
send(response, 404, { error: `Run not found: ${runRoute.runId}` });
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
if (runRoute.kind === "events") {
|
|
614
|
-
send(response, 200, { runId: runRoute.runId, events: timeline.events });
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
if (runRoute.kind === "timeline") {
|
|
618
|
-
send(response, 200, timeline);
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
send(response, 200, timeline);
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
if (request.method === "POST" && url.pathname.startsWith("/runs/")) {
|
|
625
|
-
const replayRoute = parseRunReplayRoute(url.pathname);
|
|
626
|
-
if (replayRoute) {
|
|
627
|
-
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
628
|
-
return;
|
|
629
|
-
if (!await rateLimitOrSend(request, response, rateLimit, "run-create", url.pathname))
|
|
630
|
-
return;
|
|
631
|
-
try {
|
|
632
|
-
const replay = await startRunReplay(runtime, replayRoute.runId);
|
|
633
|
-
void replay.completion.catch((error) => {
|
|
634
|
-
runtime.logger.error("replay run failed after durable creation", {
|
|
635
|
-
sourceRunId: replayRoute.runId,
|
|
636
|
-
runId: replay.run.id,
|
|
637
|
-
error: error instanceof Error ? error.message : String(error)
|
|
638
|
-
});
|
|
639
|
-
});
|
|
640
|
-
send(response, 202, { runId: replay.run.id, status: replay.run.status });
|
|
641
|
-
}
|
|
642
|
-
catch (error) {
|
|
643
|
-
if (error instanceof RunReplayError) {
|
|
644
|
-
send(response, error.code === "run_not_found" ? 404 : 409, { error: error.message, code: error.code });
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
if (error instanceof RunCapacityError) {
|
|
648
|
-
send(response, 429, { error: "Run capacity exhausted. Retry later.", retryAfterMs: error.retryAfterMs }, {
|
|
649
|
-
"retry-after": String(retryAfterSeconds(error.retryAfterMs))
|
|
650
|
-
});
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
throw error;
|
|
654
|
-
}
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
const feedbackRoute = parseRunFeedbackRoute(url.pathname);
|
|
658
|
-
if (feedbackRoute) {
|
|
659
|
-
if (!await authorizeOrSend(request, response, runtime, options, "run-control", url.pathname))
|
|
660
|
-
return;
|
|
661
|
-
if (!await rateLimitOrSend(request, response, rateLimit, "run-control", url.pathname))
|
|
662
|
-
return;
|
|
663
|
-
if (!await runtime.state.getRun(feedbackRoute.runId)) {
|
|
664
|
-
send(response, 404, { error: `Run not found: ${feedbackRoute.runId}` });
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
668
|
-
const rating = stringBodyValue(body.rating);
|
|
669
|
-
const comment = stringBodyValue(body.comment);
|
|
670
|
-
if (rating !== "positive" && rating !== "negative") {
|
|
671
|
-
send(response, 400, { error: "Feedback rating must be 'positive' or 'negative'." });
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
const result = await runtime.recordRunFeedback(feedbackRoute.runId, {
|
|
675
|
-
rating,
|
|
676
|
-
...(comment ? { comment } : {})
|
|
677
|
-
});
|
|
678
|
-
send(response, 202, result);
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
const resumeRoute = parseRunResumeRoute(url.pathname);
|
|
682
|
-
if (resumeRoute) {
|
|
683
|
-
const isHitlResume = resumeRoute.kind === "approve" || resumeRoute.kind === "answer";
|
|
684
|
-
const routeClass = isHitlResume || resumeRoute.kind === "resume" ? "run-resume" : "run-control";
|
|
685
|
-
if (!await authorizeOrSend(request, response, runtime, options, routeClass, url.pathname))
|
|
686
|
-
return;
|
|
687
|
-
if (!await rateLimitOrSend(request, response, rateLimit, routeClass, url.pathname))
|
|
688
|
-
return;
|
|
689
|
-
const run = await runtime.state.getRun(resumeRoute.runId);
|
|
690
|
-
if (!run) {
|
|
691
|
-
send(response, 404, { error: `Run not found: ${resumeRoute.runId}` });
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
const expected = resumeRoute.kind === "approve"
|
|
695
|
-
? "waiting_for_approval"
|
|
696
|
-
: resumeRoute.kind === "answer"
|
|
697
|
-
? "waiting_for_input"
|
|
698
|
-
: resumeRoute.kind === "suspend"
|
|
699
|
-
? "running"
|
|
700
|
-
: resumeRoute.kind === "resume"
|
|
701
|
-
? "suspended"
|
|
702
|
-
: undefined;
|
|
703
|
-
if (expected && run.status !== expected) {
|
|
704
|
-
send(response, 409, { error: `Run ${resumeRoute.runId} is not ${expected} (status: ${run.status}).` });
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
707
|
-
if (resumeRoute.kind === "cancel" && run.status === "cancelled") {
|
|
708
|
-
// Cancellation is an idempotent operator command. A retry may be
|
|
709
|
-
// the first response the client receives even when an earlier
|
|
710
|
-
// request already committed the terminal state.
|
|
711
|
-
send(response, 200, {
|
|
712
|
-
runId: run.id,
|
|
713
|
-
status: run.status,
|
|
714
|
-
controlIntent: run.controlIntent ?? null
|
|
715
|
-
});
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
if (resumeRoute.kind === "cancel" && ["completed", "failed"].includes(run.status)) {
|
|
719
|
-
send(response, 409, { error: `Run ${resumeRoute.runId} is already terminal (status: ${run.status}).` });
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
let answer = "";
|
|
723
|
-
if (resumeRoute.kind === "answer") {
|
|
724
|
-
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
725
|
-
answer = typeof body.answer === "string" ? body.answer : "";
|
|
726
|
-
if (!answer) {
|
|
727
|
-
send(response, 400, { error: "An 'answer' string is required to resume a waiting run." });
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
try {
|
|
732
|
-
if (resumeRoute.kind === "cancel" || resumeRoute.kind === "suspend") {
|
|
733
|
-
const controlled = resumeRoute.kind === "cancel"
|
|
734
|
-
? await runtime.cancelRun(resumeRoute.runId)
|
|
735
|
-
: await runtime.suspendRun(resumeRoute.runId);
|
|
736
|
-
send(response, controlled.status === "running" ? 202 : 200, {
|
|
737
|
-
runId: controlled.id,
|
|
738
|
-
status: controlled.status,
|
|
739
|
-
controlIntent: controlled.controlIntent ?? null
|
|
740
|
-
});
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
const result = resumeRoute.kind === "approve"
|
|
744
|
-
? await runtime.resumeApproval(resumeRoute.runId)
|
|
745
|
-
: resumeRoute.kind === "answer"
|
|
746
|
-
? await runtime.resumeInput(resumeRoute.runId, answer)
|
|
747
|
-
: await runtime.resumeSuspended(resumeRoute.runId);
|
|
748
|
-
send(response, 200, summarizeRun(result));
|
|
749
|
-
}
|
|
750
|
-
catch (error) {
|
|
751
|
-
if (error instanceof RunCapacityError) {
|
|
752
|
-
send(response, 429, { error: "Run capacity exhausted. Retry later.", retryAfterMs: error.retryAfterMs }, {
|
|
753
|
-
"retry-after": String(retryAfterSeconds(error.retryAfterMs))
|
|
754
|
-
});
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
// The resume idempotency claim rejects a concurrent/replayed resume.
|
|
758
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
759
|
-
if (resumeRoute.kind === "cancel" && /already terminal/i.test(message)) {
|
|
760
|
-
const latest = await runtime.state.getRun(resumeRoute.runId);
|
|
761
|
-
if (latest?.status === "cancelled") {
|
|
762
|
-
send(response, 200, {
|
|
763
|
-
runId: latest.id,
|
|
764
|
-
status: latest.status,
|
|
765
|
-
controlIntent: latest.controlIntent ?? null
|
|
766
|
-
});
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
if (/already in progress|not waiting|not suspended|not running|already terminal/i.test(message)) {
|
|
771
|
-
send(response, 409, { error: message });
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
throw error;
|
|
775
|
-
}
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
// Fall through: unknown POST /runs/... path.
|
|
779
|
-
}
|
|
780
|
-
if (request.method === "GET" && url.pathname === "/assembly-line/connections/authorize") {
|
|
781
|
-
// Authenticated JSON, not a public 302: the operator surface fetches
|
|
782
|
-
// the consent URL and opens the browser itself, so unauthenticated
|
|
783
|
-
// callers can neither trigger consent flows nor enumerate connection
|
|
784
|
-
// names. The provider's redirect lands on the public callback below.
|
|
785
|
-
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
786
|
-
return;
|
|
787
|
-
if (!await rateLimitOrSend(request, response, rateLimit, "agent-control", url.pathname))
|
|
788
|
-
return;
|
|
789
|
-
const connectionName = url.searchParams.get("connection") ?? "";
|
|
790
|
-
if (!connectionName) {
|
|
791
|
-
send(response, 400, { error: "A 'connection' query parameter is required." });
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
const session = {};
|
|
795
|
-
const sessionChannel = url.searchParams.get("channel");
|
|
796
|
-
const sessionUserId = url.searchParams.get("userId");
|
|
797
|
-
const sessionIssuer = url.searchParams.get("issuer");
|
|
798
|
-
const sessionConversationId = url.searchParams.get("conversationId");
|
|
799
|
-
if (sessionChannel)
|
|
800
|
-
session.channel = sessionChannel;
|
|
801
|
-
if (sessionUserId)
|
|
802
|
-
session.userId = sessionUserId;
|
|
803
|
-
if (sessionUserId && sessionIssuer) {
|
|
804
|
-
session.principal = { type: "user", id: sessionUserId, issuer: sessionIssuer };
|
|
805
|
-
}
|
|
806
|
-
if (sessionConversationId)
|
|
807
|
-
session.conversationId = sessionConversationId;
|
|
808
|
-
const result = await runtime.beginConnectionAuthorization(connectionName, session);
|
|
809
|
-
send(response, result.status === "unavailable" ? 409 : 200, result);
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
if ((request.method === "GET" || request.method === "POST") && url.pathname === "/assembly-line/connections/callback") {
|
|
813
|
-
if (!await authorizeOrSend(request, response, runtime, options, "connection-callback", url.pathname))
|
|
814
|
-
return;
|
|
815
|
-
const { rawBody, body } = request.method === "POST" ? await readBody(request, maxRequestBodyBytes(runtime, options)) : { rawBody: "", body: {} };
|
|
816
|
-
const params = Object.fromEntries(url.searchParams.entries());
|
|
817
|
-
if (Object.keys(body).length > 0) {
|
|
818
|
-
for (const [key, value] of Object.entries(body)) {
|
|
819
|
-
if (typeof value === "string")
|
|
820
|
-
params[key] = value;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
const callbackInput = {
|
|
824
|
-
params,
|
|
825
|
-
method: request.method
|
|
826
|
-
};
|
|
827
|
-
if (rawBody)
|
|
828
|
-
callbackInput.body = rawBody;
|
|
829
|
-
const result = await runtime.completeConnectionAuthorizationCallback(callbackInput);
|
|
830
|
-
send(response, result.status, result.body);
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
const connectionEvent = /^\/assembly-line\/connections\/([^/]+)\/events\/([^/]+)$/u.exec(url.pathname);
|
|
834
|
-
if ((request.method === "GET" || request.method === "POST") && connectionEvent?.[1] && connectionEvent[2]) {
|
|
835
|
-
if (!await authorizeOrSend(request, response, runtime, options, "connection-event", url.pathname))
|
|
836
|
-
return;
|
|
837
|
-
if (!await rateLimitOrSend(request, response, rateLimit, "connection-event", url.pathname))
|
|
838
|
-
return;
|
|
839
|
-
const rawBody = request.method === "POST"
|
|
840
|
-
? (await readBody(request, maxRequestBodyBytes(runtime, options))).rawBody
|
|
841
|
-
: "";
|
|
842
|
-
try {
|
|
843
|
-
const result = await runtime.acceptConnectionEvent(decodeURIComponent(connectionEvent[1]), decodeURIComponent(connectionEvent[2]), {
|
|
844
|
-
method: request.method,
|
|
845
|
-
headers: normalizeHeaders(request.headers),
|
|
846
|
-
body: rawBody,
|
|
847
|
-
query: Object.fromEntries(url.searchParams.entries())
|
|
848
|
-
});
|
|
849
|
-
send(response, result.status, result.body ?? { ok: true, accepted: result.accepted, duplicates: result.duplicates, ignored: result.ignored }, result.headers);
|
|
850
|
-
}
|
|
851
|
-
catch (error) {
|
|
852
|
-
if (error instanceof ConnectionEventError) {
|
|
853
|
-
send(response, error.status, { error: error.message });
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
throw error;
|
|
857
|
-
}
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
if (request.method === "POST" && url.pathname === "/runs") {
|
|
861
|
-
if (!apiRunCreationEnabled(runtime, options)) {
|
|
862
|
-
send(response, 403, { error: "API run creation is disabled." });
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
if (!await authorizeOrSend(request, response, runtime, options, "run-create", url.pathname))
|
|
866
|
-
return;
|
|
867
|
-
if (!await rateLimitOrSend(request, response, rateLimit, "run-create", url.pathname))
|
|
868
|
-
return;
|
|
869
|
-
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
870
|
-
const runOptions = {
|
|
871
|
-
message: typeof body.message === "string" ? body.message : "",
|
|
872
|
-
approve: body.approve === true,
|
|
873
|
-
channel: typeof body.channel === "string" ? body.channel : "http"
|
|
874
|
-
};
|
|
875
|
-
if (typeof body.toolName === "string")
|
|
876
|
-
Object.assign(runOptions, { toolName: body.toolName });
|
|
877
|
-
if (body.input !== undefined)
|
|
878
|
-
Object.assign(runOptions, { input: body.input });
|
|
879
|
-
if (typeof body.conversationId === "string")
|
|
880
|
-
Object.assign(runOptions, { conversationId: body.conversationId });
|
|
881
|
-
if (typeof body.projectId === "string")
|
|
882
|
-
Object.assign(runOptions, { projectId: body.projectId });
|
|
883
|
-
if (typeof body.workspaceId === "string")
|
|
884
|
-
Object.assign(runOptions, { workspaceId: body.workspaceId });
|
|
885
|
-
if (typeof body.tenantId === "string")
|
|
886
|
-
Object.assign(runOptions, { tenantId: body.tenantId });
|
|
887
|
-
if (typeof body.userId === "string")
|
|
888
|
-
Object.assign(runOptions, { userId: body.userId });
|
|
889
|
-
if (body.sandboxCredentials && typeof body.sandboxCredentials === "object" && !Array.isArray(body.sandboxCredentials)) {
|
|
890
|
-
Object.assign(runOptions, { sandboxCredentials: body.sandboxCredentials });
|
|
891
|
-
}
|
|
892
|
-
if (body.eval !== undefined) {
|
|
893
|
-
if (!evalRunsEnabled(runtime, options)) {
|
|
894
|
-
send(response, 403, { error: "Eval run options are disabled. Set ASSEMBLY_LINE_ENABLE_EVAL_RUNS=true or allowEvalRuns." });
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
try {
|
|
898
|
-
Object.assign(runOptions, { eval: validateEvalRunOptions(body.eval) });
|
|
899
|
-
}
|
|
900
|
-
catch (error) {
|
|
901
|
-
send(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
try {
|
|
906
|
-
if (body.stream === true) {
|
|
907
|
-
await serveStreamingRun(runtime, response, (onRunCreated) => runtime.run({ ...runOptions, onRunCreated }), (result) => summarizeRun(result));
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
const result = await runtime.run(runOptions);
|
|
911
|
-
send(response, 200, summarizeRun(result));
|
|
912
|
-
}
|
|
913
|
-
catch (error) {
|
|
914
|
-
if (error instanceof RunCapacityError) {
|
|
915
|
-
send(response, 429, { error: "Run capacity exhausted. Retry later.", retryAfterMs: error.retryAfterMs }, {
|
|
916
|
-
"retry-after": String(retryAfterSeconds(error.retryAfterMs))
|
|
917
|
-
});
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
throw error;
|
|
921
|
-
}
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
|
-
const requestMethod = request.method;
|
|
925
|
-
const routeMatch = requestMethod ? matchRoute(options.manifest.routeTable, requestMethod, url.pathname) : undefined;
|
|
926
|
-
if (routeMatch && requestMethod) {
|
|
927
|
-
channel = routeMatch.route.channel;
|
|
928
|
-
const authorization = await authorizeOrSend(request, response, runtime, options, "provider-channel", url.pathname, channel);
|
|
929
|
-
if (!authorization)
|
|
930
|
-
return;
|
|
931
|
-
if (!await rateLimitOrSend(request, response, rateLimit, "provider-channel", url.pathname, channel))
|
|
932
|
-
return;
|
|
933
|
-
const { rawBody, rawBodyBytes, body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
934
|
-
const result = await runtime.dispatchHttpChannel(routeMatch.route.channel, {
|
|
935
|
-
method: requestMethod,
|
|
936
|
-
url: request.url,
|
|
937
|
-
path: url.pathname,
|
|
938
|
-
params: routeMatch.params,
|
|
939
|
-
headers: normalizeHeaders(request.headers),
|
|
940
|
-
rawBody,
|
|
941
|
-
rawBodyBytes,
|
|
942
|
-
body,
|
|
943
|
-
trusted: authorization.trusted === true
|
|
944
|
-
});
|
|
945
|
-
send(response, result.status, result.body, result.headers);
|
|
98
|
+
const routeContext = {
|
|
99
|
+
runtime,
|
|
100
|
+
options,
|
|
101
|
+
rateLimit,
|
|
102
|
+
drain,
|
|
103
|
+
setChannel(value) {
|
|
104
|
+
channel = value;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
if ((await handleCoreHttpRoutes(request, response, url, routeContext)) ||
|
|
108
|
+
(await handleConversationHttpRoutes(request, response, url, routeContext)) ||
|
|
109
|
+
(await handleAdminHttpRoutes(request, response, url, routeContext)) ||
|
|
110
|
+
(await handleRunHttpRoutes(request, response, url, routeContext)) ||
|
|
111
|
+
(await handleIngressHttpRoutes(request, response, url, routeContext))) {
|
|
946
112
|
return;
|
|
947
113
|
}
|
|
948
114
|
send(response, 404, { error: "Not found." });
|
|
@@ -957,7 +123,9 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
957
123
|
// to callers.
|
|
958
124
|
runtime.logger.error("unhandled runtime http error", {
|
|
959
125
|
error: error instanceof Error ? error.message : String(error),
|
|
960
|
-
...(error instanceof Error && error.stack
|
|
126
|
+
...(error instanceof Error && error.stack
|
|
127
|
+
? { stack: error.stack }
|
|
128
|
+
: {}),
|
|
961
129
|
});
|
|
962
130
|
send(response, 500, { error: "Internal server error." });
|
|
963
131
|
}
|
|
@@ -982,11 +150,12 @@ export async function listenNodeRuntime(options) {
|
|
|
982
150
|
if (scheduler)
|
|
983
151
|
server.once("close", stopScheduler);
|
|
984
152
|
const env = options.env ?? runtime.env ?? process.env;
|
|
985
|
-
if (env.ASSEMBLY_LINE_RUN_RECOVERY !== "false" &&
|
|
153
|
+
if (env.ASSEMBLY_LINE_RUN_RECOVERY !== "false" &&
|
|
154
|
+
env.ASSEMBLY_LINE_RUN_RECOVERY !== "0") {
|
|
986
155
|
// Repair runs orphaned by a previous process before accepting new traffic.
|
|
987
156
|
await runtime.recoverIncompleteRuns().catch((error) => {
|
|
988
157
|
runtime.logger.warn("boot run recovery failed", {
|
|
989
|
-
error: error instanceof Error ? error.message : String(error)
|
|
158
|
+
error: error instanceof Error ? error.message : String(error),
|
|
990
159
|
});
|
|
991
160
|
});
|
|
992
161
|
}
|
|
@@ -1007,14 +176,14 @@ export async function listenNodeRuntime(options) {
|
|
|
1007
176
|
env,
|
|
1008
177
|
stopIngress,
|
|
1009
178
|
stopScheduler,
|
|
1010
|
-
stopWorkers
|
|
179
|
+
stopWorkers,
|
|
1011
180
|
};
|
|
1012
181
|
if (scheduler)
|
|
1013
182
|
handleInput.scheduler = scheduler;
|
|
1014
183
|
if (ingress)
|
|
1015
184
|
handleInput.ingress = ingress;
|
|
1016
185
|
const handle = createNodeRuntimeHandle(handleInput);
|
|
1017
|
-
//
|
|
186
|
+
// Hosted orchestrators send SIGTERM on redeploy/restart. With no handler
|
|
1018
187
|
// installed, Node terminates immediately and the graceful drain never runs, so
|
|
1019
188
|
// in-flight runs are orphaned. Install the handlers by default (opt-out via
|
|
1020
189
|
// option or ASSEMBLY_LINE_SIGNAL_HANDLERS) so redeploys drain instead of hard-kill.
|
|
@@ -1025,13 +194,6 @@ export async function listenNodeRuntime(options) {
|
|
|
1025
194
|
}
|
|
1026
195
|
return handle;
|
|
1027
196
|
}
|
|
1028
|
-
function revisionHealth(manifest) {
|
|
1029
|
-
return {
|
|
1030
|
-
ok: true,
|
|
1031
|
-
agentRevision: manifest.agentRevision,
|
|
1032
|
-
buildRevision: manifest.buildRevision ?? manifest.agentRevision
|
|
1033
|
-
};
|
|
1034
|
-
}
|
|
1035
197
|
function once(fn) {
|
|
1036
198
|
let done = false;
|
|
1037
199
|
return () => {
|