@assemblyline-agents/node 0.1.0
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/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/adapters.d.ts +17 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +122 -0
- package/dist/adapters.js.map +1 -0
- package/dist/auth.d.ts +45 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +196 -0
- package/dist/auth.js.map +1 -0
- package/dist/direct-conversations-http.d.ts +40 -0
- package/dist/direct-conversations-http.d.ts.map +1 -0
- package/dist/direct-conversations-http.js +140 -0
- package/dist/direct-conversations-http.js.map +1 -0
- package/dist/http-io.d.ts +20 -0
- package/dist/http-io.d.ts.map +1 -0
- package/dist/http-io.js +101 -0
- package/dist/http-io.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +750 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle.d.ts +56 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +112 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/multipart.d.ts +3 -0
- package/dist/multipart.d.ts.map +1 -0
- package/dist/multipart.js +125 -0
- package/dist/multipart.js.map +1 -0
- package/dist/provider-resolver.d.ts +17 -0
- package/dist/provider-resolver.d.ts.map +1 -0
- package/dist/provider-resolver.js +94 -0
- package/dist/provider-resolver.js.map +1 -0
- package/dist/rate-limit.d.ts +60 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +98 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/route-match.d.ts +21 -0
- package/dist/route-match.d.ts.map +1 -0
- package/dist/route-match.js +89 -0
- package/dist/route-match.js.map +1 -0
- package/dist/run-stream-http.d.ts +13 -0
- package/dist/run-stream-http.d.ts.map +1 -0
- package/dist/run-stream-http.js +145 -0
- package/dist/run-stream-http.js.map +1 -0
- package/dist/usage-http.d.ts +9 -0
- package/dist/usage-http.d.ts.map +1 -0
- package/dist/usage-http.js +184 -0
- package/dist/usage-http.js.map +1 -0
- package/package.json +96 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { codexAgentHarness } from "@assemblyline-agents/codex";
|
|
4
|
+
import { INTERNAL_ROUTE_PREFIX, LEGACY_INTERNAL_ROUTE_PREFIX, adapter, applyEnvAliases, mirrorEnvAliasesInPlace } from "@assemblyline-agents/core";
|
|
5
|
+
import { OpenEveRuntime, RunCapacityError, loadRuntimeBundle, queryAgentRuns, retryAfterSeconds, validateEvalRunOptions } from "@assemblyline-agents/runtime";
|
|
6
|
+
import { createBlobAdapter, createConnectionStores, createSandboxAdapter, createSelfImprovementStores, createStateAdapter, loadRunTimeline } from "./adapters.js";
|
|
7
|
+
import { apiRunCreationEnabled, evalRunsEnabled, authorizeOrSend, logHttpRequest, numberBodyValue, numberQueryValue, rateLimitOrSend, stringBodyValue, validateRuntimeHttpSecurity } from "./auth.js";
|
|
8
|
+
import { HttpRequestError, firstHeaderValue, maxRequestBodyBytes, normalizeHeaders, readBody, send, summarizeRun } from "./http-io.js";
|
|
9
|
+
import { serveRunEventStream, serveStreamingRun } from "./run-stream-http.js";
|
|
10
|
+
import { matchRoute, parseAgentControlRoute, parseRunResumeRoute, parseRunRoute } from "./route-match.js";
|
|
11
|
+
import { resolveIngressRateLimit } from "./rate-limit.js";
|
|
12
|
+
import { createNodeRuntimeHandle, installSignalHandlers } from "./lifecycle.js";
|
|
13
|
+
import { DIRECT_CHANNEL, conversationBelongsToAgent, decodeConversationCursor, decodeMessageCursor, directAgentScope, directConversationPatch, directTurnFromBody, encodeConversationCursor, encodeMessageCursor, parseConversationHttpRoute } from "./direct-conversations-http.js";
|
|
14
|
+
import { usageHttpResult, usageReconciliationHttpResult } from "./usage-http.js";
|
|
15
|
+
export { resolveProvider } from "./provider-resolver.js";
|
|
16
|
+
export { createMemoryRateLimiterStore, parseRateLimitSpec, resolveIngressRateLimit } from "./rate-limit.js";
|
|
17
|
+
export { createNodeRuntimeHandle, installSignalHandlers } from "./lifecycle.js";
|
|
18
|
+
export function nodeAdapter(options = {}) {
|
|
19
|
+
return {
|
|
20
|
+
...adapter("node", options),
|
|
21
|
+
kind: "node",
|
|
22
|
+
capabilities: ["http-runtime", "hosted-container-runtime", "local-server"]
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Install Node-hosted model harnesses without changing Pi's provider defaults. */
|
|
26
|
+
export function withNodeModelHarnesses(options) {
|
|
27
|
+
return {
|
|
28
|
+
...options,
|
|
29
|
+
modelHarnesses: {
|
|
30
|
+
"openai-codex": codexAgentHarness(),
|
|
31
|
+
...(options.modelHarnesses ?? {})
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function createProductionRuntimeOptions(options) {
|
|
36
|
+
const artifactRoot = resolve(options.artifactRoot);
|
|
37
|
+
const env = options.env ?? process.env;
|
|
38
|
+
const runtimeOptions = loadRuntimeBundle(artifactRoot);
|
|
39
|
+
runtimeOptions.devMode = options.devMode ?? false;
|
|
40
|
+
const state = await createStateAdapter(runtimeOptions, artifactRoot, env);
|
|
41
|
+
const blob = await createBlobAdapter(runtimeOptions, artifactRoot, env);
|
|
42
|
+
const connectionStores = createConnectionStores(runtimeOptions, artifactRoot, env, state, options.devMode ?? false);
|
|
43
|
+
const selfImprovementStores = createSelfImprovementStores(artifactRoot, env, state);
|
|
44
|
+
const sandbox = await createSandboxAdapter(runtimeOptions, artifactRoot, env);
|
|
45
|
+
const out = {
|
|
46
|
+
...runtimeOptions,
|
|
47
|
+
state,
|
|
48
|
+
blob,
|
|
49
|
+
sandbox,
|
|
50
|
+
connectionGrantStore: connectionStores.grantStore,
|
|
51
|
+
connectionAuthorizationSessionStore: connectionStores.authorizationSessionStore,
|
|
52
|
+
skillStore: selfImprovementStores.skillStore,
|
|
53
|
+
scheduleStore: selfImprovementStores.scheduleStore,
|
|
54
|
+
connectionDefinitionStore: selfImprovementStores.connectionDefinitionStore,
|
|
55
|
+
env,
|
|
56
|
+
devMode: options.devMode ?? false
|
|
57
|
+
};
|
|
58
|
+
const callbackBaseUrl = env.OPENEVE_CONNECTION_CALLBACK_BASE_URL ?? env.OPENEVE_PUBLIC_URL;
|
|
59
|
+
if (callbackBaseUrl)
|
|
60
|
+
out.connectionCallbackBaseUrl = callbackBaseUrl;
|
|
61
|
+
// Production default: bound run concurrency unless the operator chose a limit.
|
|
62
|
+
if (!(options.devMode ?? false) && env.OPENEVE_MAX_CONCURRENT_RUNS === undefined) {
|
|
63
|
+
out.maxConcurrentRuns = 16;
|
|
64
|
+
}
|
|
65
|
+
return withNodeModelHarnesses(out);
|
|
66
|
+
}
|
|
67
|
+
export function createNodeRuntimeServer(options) {
|
|
68
|
+
const runtime = options.runtime ?? new OpenEveRuntime(withNodeModelHarnesses(options));
|
|
69
|
+
runtime.boot();
|
|
70
|
+
validateRuntimeHttpSecurity(runtime, options);
|
|
71
|
+
return createNodeRuntimeServerForRuntime(runtime, options);
|
|
72
|
+
}
|
|
73
|
+
function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining: false }) {
|
|
74
|
+
const rateLimit = resolveIngressRateLimit(options.rateLimit, options.env ?? runtime.env ?? process.env);
|
|
75
|
+
return createServer(async (request, response) => {
|
|
76
|
+
const startedAt = Date.now();
|
|
77
|
+
let channel;
|
|
78
|
+
response.on("finish", () => {
|
|
79
|
+
logHttpRequest(runtime, request, response, startedAt, channel);
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
if (!request.url) {
|
|
83
|
+
send(response, 400, { error: "Missing URL." });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const url = new URL(request.url, "http://localhost");
|
|
87
|
+
// Rename compatibility: /assembly-line/* aliases the legacy /openeve/*
|
|
88
|
+
// internal route family. Normalizing before dispatch gives every branch
|
|
89
|
+
// identical auth/rate-limit behavior and one shared rate-limit bucket.
|
|
90
|
+
if (url.pathname === INTERNAL_ROUTE_PREFIX || url.pathname.startsWith(`${INTERNAL_ROUTE_PREFIX}/`)) {
|
|
91
|
+
url.pathname = LEGACY_INTERNAL_ROUTE_PREFIX + url.pathname.slice(INTERNAL_ROUTE_PREFIX.length);
|
|
92
|
+
}
|
|
93
|
+
if (request.method === "GET" && url.pathname === "/health") {
|
|
94
|
+
if (!await authorizeOrSend(request, response, runtime, options, "public-health", url.pathname))
|
|
95
|
+
return;
|
|
96
|
+
send(response, 200, { ok: true, agentRevision: options.manifest.agentRevision });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (request.method === "GET" && url.pathname === "/healthz") {
|
|
100
|
+
if (!await authorizeOrSend(request, response, runtime, options, "public-health", url.pathname))
|
|
101
|
+
return;
|
|
102
|
+
send(response, 200, {
|
|
103
|
+
ok: true,
|
|
104
|
+
agentRevision: options.manifest.agentRevision,
|
|
105
|
+
capabilities: evalRunsEnabled(runtime, options) ? ["eval-runs"] : []
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (request.method === "GET" && url.pathname === "/readyz") {
|
|
110
|
+
// Readiness (vs /health liveness): 503 while draining so load
|
|
111
|
+
// balancers stop routing new traffic during graceful shutdown.
|
|
112
|
+
if (!await authorizeOrSend(request, response, runtime, options, "public-health", url.pathname))
|
|
113
|
+
return;
|
|
114
|
+
if (drain.draining) {
|
|
115
|
+
send(response, 503, { ok: false, draining: true });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
send(response, 200, { ok: true, agentRevision: options.manifest.agentRevision });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (request.method === "GET" && url.pathname === "/manifest") {
|
|
122
|
+
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
123
|
+
return;
|
|
124
|
+
send(response, 200, options.manifest);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (request.method === "GET" && url.pathname === "/routes") {
|
|
128
|
+
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
129
|
+
return;
|
|
130
|
+
send(response, 200, options.manifest.routeTable);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const agentControlRoute = parseAgentControlRoute(url.pathname);
|
|
134
|
+
if (agentControlRoute && request.method === "GET" && agentControlRoute.kind === "control") {
|
|
135
|
+
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
136
|
+
return;
|
|
137
|
+
send(response, 200, await runtime.getAgentControl());
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (agentControlRoute && request.method === "POST" && agentControlRoute.kind !== "control") {
|
|
141
|
+
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
142
|
+
return;
|
|
143
|
+
if (!await rateLimitOrSend(request, response, rateLimit, "agent-control", url.pathname))
|
|
144
|
+
return;
|
|
145
|
+
const requestId = firstHeaderValue(request.headers["x-request-id"] ?? request.headers["x-railway-request-id"]);
|
|
146
|
+
const metadata = {
|
|
147
|
+
source: "http",
|
|
148
|
+
path: url.pathname,
|
|
149
|
+
...(requestId ? { requestId } : {})
|
|
150
|
+
};
|
|
151
|
+
const result = agentControlRoute.kind === "quiesce" || agentControlRoute.kind === "resume"
|
|
152
|
+
? await runtime.setAgentMaintenance(agentControlRoute.kind === "quiesce", metadata)
|
|
153
|
+
: await runtime.setAgentEnabled(agentControlRoute.kind === "enable", metadata);
|
|
154
|
+
send(response, 200, result);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const conversationRoute = parseConversationHttpRoute(url.pathname);
|
|
158
|
+
if (conversationRoute?.kind === "collection" && request.method === "GET") {
|
|
159
|
+
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
160
|
+
return;
|
|
161
|
+
if (!runtime.state.listConversations) {
|
|
162
|
+
send(response, 501, { error: "The configured state adapter does not support conversation listing." });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const cursorValue = url.searchParams.get("cursor");
|
|
166
|
+
const before = decodeConversationCursor(cursorValue);
|
|
167
|
+
if (cursorValue && !before) {
|
|
168
|
+
send(response, 400, { error: "Invalid conversation cursor." });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const limitParam = Number(url.searchParams.get("limit"));
|
|
172
|
+
const limit = Number.isFinite(limitParam) && limitParam > 0 ? Math.min(Math.floor(limitParam), 100) : 40;
|
|
173
|
+
const archivedValue = url.searchParams.get("archived");
|
|
174
|
+
const archived = archivedValue === "true" ? true : archivedValue === "all" ? undefined : false;
|
|
175
|
+
const listed = await runtime.state.listConversations({
|
|
176
|
+
agentScope: directAgentScope(options.manifest),
|
|
177
|
+
channel: url.searchParams.get("channel") || DIRECT_CHANNEL,
|
|
178
|
+
...(url.searchParams.get("subject") ? { subject: url.searchParams.get("subject") } : {}),
|
|
179
|
+
...(archived !== undefined ? { archived } : {}),
|
|
180
|
+
...(before ? { before } : {}),
|
|
181
|
+
limit: limit + 1
|
|
182
|
+
});
|
|
183
|
+
const hasMore = listed.length > limit;
|
|
184
|
+
const conversations = listed.slice(0, limit);
|
|
185
|
+
send(response, 200, {
|
|
186
|
+
conversations,
|
|
187
|
+
nextCursor: hasMore && conversations.length > 0
|
|
188
|
+
? encodeConversationCursor(conversations[conversations.length - 1])
|
|
189
|
+
: null
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (conversationRoute?.kind === "messages" && request.method === "GET") {
|
|
194
|
+
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
195
|
+
return;
|
|
196
|
+
const conversation = await runtime.state.getConversation(conversationRoute.conversationId);
|
|
197
|
+
if (!conversation || !conversationBelongsToAgent(conversation, directAgentScope(options.manifest))) {
|
|
198
|
+
send(response, 404, { error: "Conversation not found." });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const cursorValue = url.searchParams.get("before");
|
|
202
|
+
const before = decodeMessageCursor(cursorValue);
|
|
203
|
+
if (cursorValue && !before) {
|
|
204
|
+
send(response, 400, { error: "Invalid message cursor." });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const limitParam = Number(url.searchParams.get("limit"));
|
|
208
|
+
const limit = Number.isFinite(limitParam) && limitParam > 0 ? Math.min(Math.floor(limitParam), 100) : 50;
|
|
209
|
+
const listed = runtime.state.listMessages
|
|
210
|
+
? await runtime.state.listMessages({
|
|
211
|
+
conversationId: conversation.id,
|
|
212
|
+
...(before ? { before } : {}),
|
|
213
|
+
limit: limit + 1
|
|
214
|
+
})
|
|
215
|
+
: before
|
|
216
|
+
? []
|
|
217
|
+
: await runtime.state.listRecentMessages(conversation.id, limit + 1);
|
|
218
|
+
if (before && !runtime.state.listMessages) {
|
|
219
|
+
send(response, 501, { error: "The configured state adapter does not support message pagination." });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const hasMore = listed.length > limit;
|
|
223
|
+
const messages = hasMore ? listed.slice(1) : listed;
|
|
224
|
+
send(response, 200, {
|
|
225
|
+
messages,
|
|
226
|
+
nextCursor: hasMore && messages.length > 0 ? encodeMessageCursor(messages[0]) : null
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (conversationRoute?.kind === "record" && request.method === "PATCH") {
|
|
231
|
+
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
232
|
+
return;
|
|
233
|
+
if (!runtime.state.updateConversation) {
|
|
234
|
+
send(response, 501, { error: "The configured state adapter does not support conversation updates." });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const existing = await runtime.state.getConversation(conversationRoute.conversationId);
|
|
238
|
+
if (!existing || !conversationBelongsToAgent(existing, directAgentScope(options.manifest))) {
|
|
239
|
+
send(response, 404, { error: "Conversation not found." });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
243
|
+
const patch = directConversationPatch(body);
|
|
244
|
+
if (Object.keys(patch).length === 0) {
|
|
245
|
+
send(response, 400, { error: "Provide a title, archived flag, or metadata patch." });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
send(response, 200, await runtime.state.updateConversation(existing.id, patch));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (conversationRoute?.kind === "turns" && request.method === "POST") {
|
|
252
|
+
channel = DIRECT_CHANNEL;
|
|
253
|
+
if (!apiRunCreationEnabled(runtime, options)) {
|
|
254
|
+
send(response, 403, { error: "API run creation is disabled." });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (!await authorizeOrSend(request, response, runtime, options, "run-create", url.pathname))
|
|
258
|
+
return;
|
|
259
|
+
if (!await rateLimitOrSend(request, response, rateLimit, "run-create", url.pathname, DIRECT_CHANNEL))
|
|
260
|
+
return;
|
|
261
|
+
const existing = await runtime.state.getConversation(conversationRoute.conversationId);
|
|
262
|
+
if (existing && !conversationBelongsToAgent(existing, directAgentScope(options.manifest))) {
|
|
263
|
+
send(response, 404, { error: "Conversation not found." });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (existing?.channel && existing.channel !== DIRECT_CHANNEL) {
|
|
267
|
+
send(response, 409, { error: `Conversation belongs to channel ${existing.channel}.` });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (existing?.archivedAt) {
|
|
271
|
+
send(response, 409, { error: "Archived conversations cannot accept new turns." });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
275
|
+
const idempotencyKey = firstHeaderValue(request.headers["idempotency-key"]);
|
|
276
|
+
const direct = directTurnFromBody({
|
|
277
|
+
conversationId: conversationRoute.conversationId,
|
|
278
|
+
body,
|
|
279
|
+
...(idempotencyKey ? { idempotencyKey } : {})
|
|
280
|
+
});
|
|
281
|
+
if (!direct.turn.message && !(direct.turn.attachments?.length)) {
|
|
282
|
+
send(response, 400, { error: "A message or attachment is required." });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const runOptions = {
|
|
286
|
+
message: direct.turn.message,
|
|
287
|
+
channel: DIRECT_CHANNEL,
|
|
288
|
+
approve: direct.approve,
|
|
289
|
+
turn: direct.turn
|
|
290
|
+
};
|
|
291
|
+
if (direct.turn.model)
|
|
292
|
+
Object.assign(runOptions, { model: direct.turn.model });
|
|
293
|
+
try {
|
|
294
|
+
if (direct.stream) {
|
|
295
|
+
await serveStreamingRun(runtime, response, (onRunCreated) => runtime.run({ ...runOptions, onRunCreated }), (result) => summarizeRun(result));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const result = await runtime.run(runOptions);
|
|
299
|
+
send(response, 200, summarizeRun(result));
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (error instanceof RunCapacityError) {
|
|
303
|
+
send(response, 429, { error: "Run capacity exhausted. Retry later.", retryAfterMs: error.retryAfterMs }, {
|
|
304
|
+
"retry-after": String(retryAfterSeconds(error.retryAfterMs))
|
|
305
|
+
});
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (request.method === "GET" && url.pathname === "/runs") {
|
|
313
|
+
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
314
|
+
return;
|
|
315
|
+
// Light list: no per-run detail (avoids the 1+4N fan-out that hangs on
|
|
316
|
+
// busy agents). Bounded to the most recent runs; ?limit overrides.
|
|
317
|
+
const limitParam = Number(url.searchParams.get("limit"));
|
|
318
|
+
const limit = Number.isFinite(limitParam) && limitParam > 0 ? Math.min(limitParam, 1000) : 200;
|
|
319
|
+
send(response, 200, await queryAgentRuns(runtime.state, options.manifest, { includeDetails: false, limit }));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (request.method === "GET" && url.pathname === "/usage") {
|
|
323
|
+
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
324
|
+
return;
|
|
325
|
+
try {
|
|
326
|
+
send(response, 200, await usageHttpResult(runtime.state, url));
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
send(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (request.method === "POST" && url.pathname === "/usage/reconcile") {
|
|
334
|
+
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
335
|
+
return;
|
|
336
|
+
try {
|
|
337
|
+
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
338
|
+
send(response, 200, await usageReconciliationHttpResult(runtime.state, runtime.env, body));
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
send(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if ((request.method === "GET" || request.method === "POST")
|
|
346
|
+
&& (url.pathname === "/openeve/automations/tick" || url.pathname === "/openeve/scheduler/tick")) {
|
|
347
|
+
if (!await authorizeOrSend(request, response, runtime, options, "scheduler", url.pathname))
|
|
348
|
+
return;
|
|
349
|
+
if (!await rateLimitOrSend(request, response, rateLimit, "scheduler", url.pathname))
|
|
350
|
+
return;
|
|
351
|
+
const body = request.method === "POST" ? (await readBody(request, maxRequestBodyBytes(runtime, options))).body : {};
|
|
352
|
+
const nowValue = stringBodyValue(body.now) ?? url.searchParams.get("now") ?? undefined;
|
|
353
|
+
const lookbackValue = numberBodyValue(body.lookbackMs) ?? numberQueryValue(url.searchParams.get("lookbackMs"));
|
|
354
|
+
const tickOptions = {};
|
|
355
|
+
if (nowValue)
|
|
356
|
+
tickOptions.now = new Date(nowValue);
|
|
357
|
+
if (lookbackValue !== undefined)
|
|
358
|
+
tickOptions.lookbackMs = lookbackValue;
|
|
359
|
+
if (tickOptions.now && Number.isNaN(tickOptions.now.getTime())) {
|
|
360
|
+
send(response, 400, { error: "Invalid scheduler now timestamp." });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const results = await runtime.runDueAutomations(tickOptions);
|
|
364
|
+
send(response, 200, { ok: true, results });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (request.method === "POST" && url.pathname === "/openeve/automations/events") {
|
|
368
|
+
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
369
|
+
return;
|
|
370
|
+
if (!await rateLimitOrSend(request, response, rateLimit, "run-create", url.pathname))
|
|
371
|
+
return;
|
|
372
|
+
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
373
|
+
const source = stringBodyValue(body.source);
|
|
374
|
+
const event = stringBodyValue(body.event);
|
|
375
|
+
const eventId = stringBodyValue(body.eventId);
|
|
376
|
+
const payload = body.payload;
|
|
377
|
+
if (!source || !event || !eventId || !payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
378
|
+
send(response, 400, { error: "Automation events require source, event, eventId, and an object payload." });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const automationEvent = {
|
|
382
|
+
source,
|
|
383
|
+
event,
|
|
384
|
+
eventId,
|
|
385
|
+
payload: payload
|
|
386
|
+
};
|
|
387
|
+
const occurredAt = stringBodyValue(body.occurredAt);
|
|
388
|
+
const userId = stringBodyValue(body.userId);
|
|
389
|
+
const conversationId = stringBodyValue(body.conversationId);
|
|
390
|
+
const projectId = stringBodyValue(body.projectId);
|
|
391
|
+
if (occurredAt && Number.isNaN(new Date(occurredAt).getTime())) {
|
|
392
|
+
send(response, 400, { error: "Automation event occurredAt must be a valid ISO timestamp." });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (occurredAt)
|
|
396
|
+
automationEvent.occurredAt = occurredAt;
|
|
397
|
+
if (userId)
|
|
398
|
+
automationEvent.userId = userId;
|
|
399
|
+
if (conversationId)
|
|
400
|
+
automationEvent.conversationId = conversationId;
|
|
401
|
+
if (projectId)
|
|
402
|
+
automationEvent.projectId = projectId;
|
|
403
|
+
if (body.delivery && typeof body.delivery === "object" && !Array.isArray(body.delivery)) {
|
|
404
|
+
automationEvent.delivery = body.delivery;
|
|
405
|
+
}
|
|
406
|
+
if (body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)) {
|
|
407
|
+
automationEvent.metadata = body.metadata;
|
|
408
|
+
}
|
|
409
|
+
const results = await runtime.dispatchAutomationEvent(automationEvent);
|
|
410
|
+
send(response, 200, { ok: true, matched: results.length, results });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (request.method === "GET" && url.pathname.startsWith("/runs/")) {
|
|
414
|
+
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
415
|
+
return;
|
|
416
|
+
const runRoute = parseRunRoute(url.pathname);
|
|
417
|
+
if (!runRoute) {
|
|
418
|
+
send(response, 404, { error: "Not found." });
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (runRoute.kind === "stream") {
|
|
422
|
+
if (!await runtime.state.getRun(runRoute.runId)) {
|
|
423
|
+
send(response, 404, { error: `Run not found: ${runRoute.runId}` });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const lastEventId = firstHeaderValue(request.headers["last-event-id"]) ?? url.searchParams.get("lastEventId") ?? undefined;
|
|
427
|
+
await serveRunEventStream(runtime, response, runRoute.runId, lastEventId);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const timeline = await loadRunTimeline(runtime, runRoute.runId);
|
|
431
|
+
if (!timeline) {
|
|
432
|
+
send(response, 404, { error: `Run not found: ${runRoute.runId}` });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (runRoute.kind === "events") {
|
|
436
|
+
send(response, 200, { runId: runRoute.runId, events: timeline.events });
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (runRoute.kind === "timeline") {
|
|
440
|
+
send(response, 200, timeline);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
send(response, 200, timeline);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (request.method === "POST" && url.pathname.startsWith("/runs/")) {
|
|
447
|
+
const resumeRoute = parseRunResumeRoute(url.pathname);
|
|
448
|
+
if (resumeRoute) {
|
|
449
|
+
const isHitlResume = resumeRoute.kind === "approve" || resumeRoute.kind === "answer";
|
|
450
|
+
const routeClass = isHitlResume || resumeRoute.kind === "resume" ? "run-resume" : "run-control";
|
|
451
|
+
if (!await authorizeOrSend(request, response, runtime, options, routeClass, url.pathname))
|
|
452
|
+
return;
|
|
453
|
+
if (!await rateLimitOrSend(request, response, rateLimit, routeClass, url.pathname))
|
|
454
|
+
return;
|
|
455
|
+
const run = await runtime.state.getRun(resumeRoute.runId);
|
|
456
|
+
if (!run) {
|
|
457
|
+
send(response, 404, { error: `Run not found: ${resumeRoute.runId}` });
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const expected = resumeRoute.kind === "approve"
|
|
461
|
+
? "waiting_for_approval"
|
|
462
|
+
: resumeRoute.kind === "answer"
|
|
463
|
+
? "waiting_for_input"
|
|
464
|
+
: resumeRoute.kind === "suspend"
|
|
465
|
+
? "running"
|
|
466
|
+
: resumeRoute.kind === "resume"
|
|
467
|
+
? "suspended"
|
|
468
|
+
: undefined;
|
|
469
|
+
if (expected && run.status !== expected) {
|
|
470
|
+
send(response, 409, { error: `Run ${resumeRoute.runId} is not ${expected} (status: ${run.status}).` });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (resumeRoute.kind === "cancel" && ["completed", "failed", "cancelled"].includes(run.status)) {
|
|
474
|
+
send(response, 409, { error: `Run ${resumeRoute.runId} is already terminal (status: ${run.status}).` });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
let answer = "";
|
|
478
|
+
if (resumeRoute.kind === "answer") {
|
|
479
|
+
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
480
|
+
answer = typeof body.answer === "string" ? body.answer : "";
|
|
481
|
+
if (!answer) {
|
|
482
|
+
send(response, 400, { error: "An 'answer' string is required to resume a waiting run." });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
if (resumeRoute.kind === "cancel" || resumeRoute.kind === "suspend") {
|
|
488
|
+
const controlled = resumeRoute.kind === "cancel"
|
|
489
|
+
? await runtime.cancelRun(resumeRoute.runId)
|
|
490
|
+
: await runtime.suspendRun(resumeRoute.runId);
|
|
491
|
+
send(response, controlled.status === "running" ? 202 : 200, {
|
|
492
|
+
runId: controlled.id,
|
|
493
|
+
status: controlled.status,
|
|
494
|
+
controlIntent: controlled.controlIntent ?? null
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const result = resumeRoute.kind === "approve"
|
|
499
|
+
? await runtime.resumeApproval(resumeRoute.runId)
|
|
500
|
+
: resumeRoute.kind === "answer"
|
|
501
|
+
? await runtime.resumeInput(resumeRoute.runId, answer)
|
|
502
|
+
: await runtime.resumeSuspended(resumeRoute.runId);
|
|
503
|
+
send(response, 200, summarizeRun(result));
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
if (error instanceof RunCapacityError) {
|
|
507
|
+
send(response, 429, { error: "Run capacity exhausted. Retry later.", retryAfterMs: error.retryAfterMs }, {
|
|
508
|
+
"retry-after": String(retryAfterSeconds(error.retryAfterMs))
|
|
509
|
+
});
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
// The resume idempotency claim rejects a concurrent/replayed resume.
|
|
513
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
514
|
+
if (/already in progress|not waiting|not suspended|not running|already terminal/i.test(message)) {
|
|
515
|
+
send(response, 409, { error: message });
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
// Fall through: unknown POST /runs/... path.
|
|
523
|
+
}
|
|
524
|
+
if (request.method === "GET" && url.pathname === "/openeve/connections/authorize") {
|
|
525
|
+
// Authenticated JSON, not a public 302: the operator surface fetches
|
|
526
|
+
// the consent URL and opens the browser itself, so unauthenticated
|
|
527
|
+
// callers can neither trigger consent flows nor enumerate connection
|
|
528
|
+
// names. The provider's redirect lands on the public callback below.
|
|
529
|
+
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
530
|
+
return;
|
|
531
|
+
if (!await rateLimitOrSend(request, response, rateLimit, "agent-control", url.pathname))
|
|
532
|
+
return;
|
|
533
|
+
const connectionName = url.searchParams.get("connection") ?? "";
|
|
534
|
+
if (!connectionName) {
|
|
535
|
+
send(response, 400, { error: "A 'connection' query parameter is required." });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const session = {};
|
|
539
|
+
const sessionChannel = url.searchParams.get("channel");
|
|
540
|
+
const sessionUserId = url.searchParams.get("userId");
|
|
541
|
+
const sessionConversationId = url.searchParams.get("conversationId");
|
|
542
|
+
if (sessionChannel)
|
|
543
|
+
session.channel = sessionChannel;
|
|
544
|
+
if (sessionUserId)
|
|
545
|
+
session.userId = sessionUserId;
|
|
546
|
+
if (sessionConversationId)
|
|
547
|
+
session.conversationId = sessionConversationId;
|
|
548
|
+
const result = await runtime.beginConnectionAuthorization(connectionName, session);
|
|
549
|
+
send(response, result.status === "unavailable" ? 409 : 200, result);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if ((request.method === "GET" || request.method === "POST") && url.pathname === "/openeve/connections/callback") {
|
|
553
|
+
if (!await authorizeOrSend(request, response, runtime, options, "connection-callback", url.pathname))
|
|
554
|
+
return;
|
|
555
|
+
const { rawBody, body } = request.method === "POST" ? await readBody(request, maxRequestBodyBytes(runtime, options)) : { rawBody: "", body: {} };
|
|
556
|
+
const params = Object.fromEntries(url.searchParams.entries());
|
|
557
|
+
if (Object.keys(body).length > 0) {
|
|
558
|
+
for (const [key, value] of Object.entries(body)) {
|
|
559
|
+
if (typeof value === "string")
|
|
560
|
+
params[key] = value;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const callbackInput = {
|
|
564
|
+
params,
|
|
565
|
+
method: request.method
|
|
566
|
+
};
|
|
567
|
+
if (rawBody)
|
|
568
|
+
callbackInput.body = rawBody;
|
|
569
|
+
const result = await runtime.completeConnectionAuthorizationCallback(callbackInput);
|
|
570
|
+
send(response, result.status, result.body);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (request.method === "POST" && url.pathname === "/runs") {
|
|
574
|
+
if (!apiRunCreationEnabled(runtime, options)) {
|
|
575
|
+
send(response, 403, { error: "API run creation is disabled." });
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (!await authorizeOrSend(request, response, runtime, options, "run-create", url.pathname))
|
|
579
|
+
return;
|
|
580
|
+
if (!await rateLimitOrSend(request, response, rateLimit, "run-create", url.pathname))
|
|
581
|
+
return;
|
|
582
|
+
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
583
|
+
const runOptions = {
|
|
584
|
+
message: typeof body.message === "string" ? body.message : "",
|
|
585
|
+
approve: body.approve === true,
|
|
586
|
+
channel: typeof body.channel === "string" ? body.channel : "http"
|
|
587
|
+
};
|
|
588
|
+
if (typeof body.model === "string")
|
|
589
|
+
Object.assign(runOptions, { model: body.model });
|
|
590
|
+
if (typeof body.toolName === "string")
|
|
591
|
+
Object.assign(runOptions, { toolName: body.toolName });
|
|
592
|
+
if (body.input !== undefined)
|
|
593
|
+
Object.assign(runOptions, { input: body.input });
|
|
594
|
+
if (body.eval !== undefined) {
|
|
595
|
+
if (!evalRunsEnabled(runtime, options)) {
|
|
596
|
+
send(response, 403, { error: "Eval run options are disabled. Set OPENEVE_ENABLE_EVAL_RUNS=true or allowEvalRuns." });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
Object.assign(runOptions, { eval: validateEvalRunOptions(body.eval) });
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
send(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
if (body.stream === true) {
|
|
609
|
+
await serveStreamingRun(runtime, response, (onRunCreated) => runtime.run({ ...runOptions, onRunCreated }), (result) => summarizeRun(result));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const result = await runtime.run(runOptions);
|
|
613
|
+
send(response, 200, summarizeRun(result));
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
if (error instanceof RunCapacityError) {
|
|
617
|
+
send(response, 429, { error: "Run capacity exhausted. Retry later.", retryAfterMs: error.retryAfterMs }, {
|
|
618
|
+
"retry-after": String(retryAfterSeconds(error.retryAfterMs))
|
|
619
|
+
});
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
throw error;
|
|
623
|
+
}
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const requestMethod = request.method;
|
|
627
|
+
const routeMatch = requestMethod ? matchRoute(options.manifest.routeTable, requestMethod, url.pathname) : undefined;
|
|
628
|
+
if (routeMatch && requestMethod) {
|
|
629
|
+
channel = routeMatch.route.channel;
|
|
630
|
+
const authorization = await authorizeOrSend(request, response, runtime, options, "provider-channel", url.pathname, channel);
|
|
631
|
+
if (!authorization)
|
|
632
|
+
return;
|
|
633
|
+
if (!await rateLimitOrSend(request, response, rateLimit, "provider-channel", url.pathname, channel))
|
|
634
|
+
return;
|
|
635
|
+
const { rawBody, rawBodyBytes, body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
636
|
+
const result = await runtime.dispatchHttpChannel(routeMatch.route.channel, {
|
|
637
|
+
method: requestMethod,
|
|
638
|
+
url: request.url,
|
|
639
|
+
path: url.pathname,
|
|
640
|
+
params: routeMatch.params,
|
|
641
|
+
headers: normalizeHeaders(request.headers),
|
|
642
|
+
rawBody,
|
|
643
|
+
rawBodyBytes,
|
|
644
|
+
body,
|
|
645
|
+
trusted: authorization.trusted === true
|
|
646
|
+
});
|
|
647
|
+
send(response, result.status, result.body, result.headers);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
send(response, 404, { error: "Not found." });
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
if (error instanceof HttpRequestError) {
|
|
654
|
+
send(response, error.status, { error: error.message });
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
// Log the real cause server-side but return a generic message: raw
|
|
658
|
+
// exception text (DB errors, file paths, driver internals) must not leak
|
|
659
|
+
// to callers.
|
|
660
|
+
runtime.logger.error("unhandled runtime http error", {
|
|
661
|
+
error: error instanceof Error ? error.message : String(error),
|
|
662
|
+
...(error instanceof Error && error.stack ? { stack: error.stack } : {})
|
|
663
|
+
});
|
|
664
|
+
send(response, 500, { error: "Internal server error." });
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
export async function listenNodeRuntime(rawOptions) {
|
|
669
|
+
// Rename compatibility: make every branded env var readable under both its
|
|
670
|
+
// ASSEMBLY_LINE_* and OPENEVE_* spellings for all downstream readers.
|
|
671
|
+
const options = rawOptions.env
|
|
672
|
+
? { ...rawOptions, env: applyEnvAliases(rawOptions.env).env }
|
|
673
|
+
: rawOptions;
|
|
674
|
+
if (!rawOptions.env)
|
|
675
|
+
mirrorEnvAliasesInPlace(process.env);
|
|
676
|
+
const runtime = new OpenEveRuntime(withNodeModelHarnesses(options));
|
|
677
|
+
runtime.boot();
|
|
678
|
+
validateRuntimeHttpSecurity(runtime, options);
|
|
679
|
+
const drain = { draining: false };
|
|
680
|
+
const server = createNodeRuntimeServerForRuntime(runtime, options, drain);
|
|
681
|
+
const port = options.port ?? 0;
|
|
682
|
+
const host = options.host ?? "127.0.0.1";
|
|
683
|
+
await new Promise((resolve, reject) => {
|
|
684
|
+
server.once("error", reject);
|
|
685
|
+
server.listen(port, host, () => resolve());
|
|
686
|
+
});
|
|
687
|
+
const address = server.address();
|
|
688
|
+
const boundPort = typeof address === "object" && address ? address.port : port;
|
|
689
|
+
const scheduler = await runtime.startSchedulerLoop();
|
|
690
|
+
const stopScheduler = once(() => scheduler?.stop());
|
|
691
|
+
if (scheduler)
|
|
692
|
+
server.once("close", stopScheduler);
|
|
693
|
+
const env = options.env ?? runtime.env ?? process.env;
|
|
694
|
+
if (env.OPENEVE_RUN_RECOVERY !== "false" && env.OPENEVE_RUN_RECOVERY !== "0") {
|
|
695
|
+
// Repair runs orphaned by a previous process before accepting new traffic.
|
|
696
|
+
await runtime.recoverIncompleteRuns().catch((error) => {
|
|
697
|
+
runtime.logger.warn("boot run recovery failed", {
|
|
698
|
+
error: error instanceof Error ? error.message : String(error)
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
const ingress = await runtime.startChannelIngress();
|
|
703
|
+
const stopIngress = onceAsync(async () => {
|
|
704
|
+
await ingress?.stop();
|
|
705
|
+
});
|
|
706
|
+
if (ingress)
|
|
707
|
+
server.once("close", () => void stopIngress());
|
|
708
|
+
const workers = runtime.startBackgroundWorkers();
|
|
709
|
+
const stopWorkers = once(() => workers.stop());
|
|
710
|
+
server.once("close", stopWorkers);
|
|
711
|
+
const handleInput = {
|
|
712
|
+
server,
|
|
713
|
+
url: `http://${host}:${boundPort}`,
|
|
714
|
+
runtime,
|
|
715
|
+
drain,
|
|
716
|
+
env,
|
|
717
|
+
stopIngress,
|
|
718
|
+
stopScheduler,
|
|
719
|
+
stopWorkers
|
|
720
|
+
};
|
|
721
|
+
if (scheduler)
|
|
722
|
+
handleInput.scheduler = scheduler;
|
|
723
|
+
if (ingress)
|
|
724
|
+
handleInput.ingress = ingress;
|
|
725
|
+
const handle = createNodeRuntimeHandle(handleInput);
|
|
726
|
+
// Hosts like Railway send SIGTERM on every redeploy/restart. With no handler
|
|
727
|
+
// installed, Node terminates immediately and the graceful drain never runs, so
|
|
728
|
+
// in-flight runs are orphaned. Install the handlers by default (opt-out via
|
|
729
|
+
// option or OPENEVE_SIGNAL_HANDLERS) so redeploys drain instead of hard-kill.
|
|
730
|
+
if (options.installSignalHandlers !== false &&
|
|
731
|
+
env.OPENEVE_SIGNAL_HANDLERS !== "false" &&
|
|
732
|
+
env.OPENEVE_SIGNAL_HANDLERS !== "0") {
|
|
733
|
+
installSignalHandlers(handle);
|
|
734
|
+
}
|
|
735
|
+
return handle;
|
|
736
|
+
}
|
|
737
|
+
function once(fn) {
|
|
738
|
+
let done = false;
|
|
739
|
+
return () => {
|
|
740
|
+
if (done)
|
|
741
|
+
return;
|
|
742
|
+
done = true;
|
|
743
|
+
fn();
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function onceAsync(fn) {
|
|
747
|
+
let pending;
|
|
748
|
+
return () => (pending ??= fn());
|
|
749
|
+
}
|
|
750
|
+
//# sourceMappingURL=index.js.map
|