@nylorun/runtime 0.7.0-beta → 0.8.0-beta
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/CHANGELOG.md +33 -0
- package/README.md +7 -5
- package/dist/adapters/sandbox/microsandbox.d.ts +2 -0
- package/dist/adapters/sandbox/microsandbox.js +238 -0
- package/dist/adapters/sandbox/virtual.d.ts +4 -0
- package/dist/adapters/sandbox/virtual.js +125 -0
- package/dist/configuration.d.ts +1 -1
- package/dist/configuration.js +1 -1
- package/dist/core/executors.d.ts +32 -0
- package/dist/core/executors.js +60 -0
- package/dist/core/main.js +7 -12
- package/dist/core/runtime.d.ts +24 -1
- package/dist/core/runtime.js +322 -30
- package/dist/core/store.d.ts +12 -0
- package/dist/core/store.js +50 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/model/catalog.d.ts +12 -0
- package/dist/model/catalog.js +29 -0
- package/dist/model/models.d.ts +3 -1
- package/dist/model/models.js +4 -2
- package/dist/model/pi-model.d.ts +9 -3
- package/dist/model/pi-model.js +47 -11
- package/dist/model/settings.d.ts +0 -2
- package/dist/model/settings.js +0 -47
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/sandbox/manager.d.ts +61 -0
- package/dist/sandbox/manager.js +239 -0
- package/dist/sandbox/policy.d.ts +13 -0
- package/dist/sandbox/policy.js +67 -0
- package/dist/sandbox/select.d.ts +35 -0
- package/dist/sandbox/select.js +76 -0
- package/dist/sandbox/tools.d.ts +25 -0
- package/dist/sandbox/tools.js +165 -0
- package/dist/sandbox/types.d.ts +66 -0
- package/dist/vault/service.d.ts +41 -1
- package/dist/vault/service.js +291 -4
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/package.json +9 -4
- package/dist/config.d.ts +0 -20
- package/dist/model/defaults.d.ts +0 -17
- package/dist/model/defaults.js +0 -21
- package/dist/model/http-model.d.ts +0 -12
- package/dist/model/http-model.js +0 -299
- package/dist/node/local-sessions.d.ts +0 -5
- package/dist/node/local-sessions.js +0 -174
- package/dist/server/ag-ui.d.ts +0 -8
- package/dist/server/ag-ui.js +0 -76
- package/dist/server/delivery.d.ts +0 -24
- package/dist/server/delivery.js +0 -107
- package/dist/server/digests.d.ts +0 -13
- package/dist/server/digests.js +0 -39
- package/dist/server/host.d.ts +0 -70
- package/dist/server/host.js +0 -647
- package/dist/session/api.d.ts +0 -10
- package/dist/session/api.js +0 -15
- package/dist/session/default.d.ts +0 -5
- package/dist/session/default.js +0 -30
- package/dist/session/handle.d.ts +0 -27
- package/dist/session/handle.js +0 -199
- package/dist/session/index.d.ts +0 -2
- package/dist/session/index.js +0 -2
- package/dist/sessions/host.d.ts +0 -39
- package/dist/sessions/host.js +0 -359
- package/dist/sessions/store.d.ts +0 -41
- package/dist/sessions/store.js +0 -33
- /package/dist/{config.js → sandbox/types.js} +0 -0
package/dist/core/runtime.js
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import { createServer, } from "node:http";
|
|
2
2
|
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
3
|
-
import { mkdirSync, openSync, closeSync, unlinkSync, writeFileSync, readFileSync, } from "node:fs";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { mkdirSync, openSync, closeSync, unlinkSync, writeFileSync, readFileSync, realpathSync, mkdtempSync, } from "node:fs";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { AgentManifestSchema, CreateCredentialRequestSchema, CreateVaultRequestSchema, PutAgentRequestSchema, PutHostModelRequestSchema, SelectHostModelRequestSchema, PutSessionRequestSchema, RegisterExecutorsRequestSchema, RotateCredentialRequestSchema, SessionCommandSchema, ActionClaimRequestSchema, ActionHeartbeatRequestSchema, } from "@nylorun/core/contracts";
|
|
6
8
|
import { createDurableCheckpoint, runDurable, } from "@nylorun/harness/run";
|
|
7
9
|
import { hashManifest } from "@nylorun/core/compatibility";
|
|
8
10
|
import { schemaFromJSON, } from "@nylorun/core/define";
|
|
9
11
|
import { Store, canonical } from "./store.js";
|
|
12
|
+
import { ExecutorRegistry, assertExecutorCredential, hashToken, } from "./executors.js";
|
|
13
|
+
import { RUNTIME_VERSION } from "../version.js";
|
|
14
|
+
import { hostModelCatalog } from "../model/catalog.js";
|
|
15
|
+
import { piModel } from "../model/pi-model.js";
|
|
10
16
|
import { scriptedModel } from "./provider.js";
|
|
11
17
|
import { scrub } from "../redact.js";
|
|
12
18
|
import { createKekFile, defaultKekPath, readVaultKek } from "../vault/kek.js";
|
|
13
19
|
import { VaultError } from "../vault/error.js";
|
|
14
20
|
import { VaultService } from "../vault/service.js";
|
|
15
21
|
import { McpPool } from "../mcp/pool.js";
|
|
22
|
+
import { SandboxManager, sandboxCapabilityOf } from "../sandbox/manager.js";
|
|
23
|
+
import { defaultSandboxBackends } from "../sandbox/select.js";
|
|
16
24
|
class HttpError extends Error {
|
|
17
25
|
status;
|
|
18
26
|
constructor(status, message) {
|
|
@@ -112,10 +120,15 @@ export class CoreRuntime {
|
|
|
112
120
|
running = new Map();
|
|
113
121
|
pending = new Set();
|
|
114
122
|
observers = new Map();
|
|
115
|
-
|
|
123
|
+
// Keyed by token hash so a rotation can end exactly the streams that the replaced token owns.
|
|
124
|
+
executorStreams = new Map();
|
|
125
|
+
registry = new ExecutorRegistry();
|
|
126
|
+
scopeId;
|
|
116
127
|
mcp;
|
|
128
|
+
sandbox;
|
|
117
129
|
server;
|
|
118
130
|
closing = false;
|
|
131
|
+
closed = false;
|
|
119
132
|
lockPath;
|
|
120
133
|
timer;
|
|
121
134
|
constructor(options) {
|
|
@@ -128,11 +141,8 @@ export class CoreRuntime {
|
|
|
128
141
|
throw new Error("Executor tokens must be unique");
|
|
129
142
|
if (!options.serverToken || options.serverToken.length < 16)
|
|
130
143
|
throw new Error("A server token of at least 16 characters is required");
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
!e.agentId ||
|
|
134
|
-
!e.implementationVersion))
|
|
135
|
-
throw new Error("Executor tokens require independent credentials and an agent id");
|
|
144
|
+
for (const executor of options.executors)
|
|
145
|
+
assertExecutorCredential(executor, options.serverToken);
|
|
136
146
|
if (options.sqlitePath !== ":memory:") {
|
|
137
147
|
mkdirSync(dirname(options.sqlitePath), { recursive: true });
|
|
138
148
|
this.lockPath = options.sqlitePath + ".runtime-lock";
|
|
@@ -177,11 +187,75 @@ export class CoreRuntime {
|
|
|
177
187
|
unlinkSync(this.lockPath);
|
|
178
188
|
throw e;
|
|
179
189
|
}
|
|
190
|
+
// A short digest of the resolved database path identifies this scope to the CLI without
|
|
191
|
+
// disclosing the path itself over the unauthenticated health route.
|
|
192
|
+
this.scopeId =
|
|
193
|
+
options.sqlitePath === ":memory:"
|
|
194
|
+
? "memory"
|
|
195
|
+
: createHash("sha256")
|
|
196
|
+
.update(realpathSync(options.sqlitePath))
|
|
197
|
+
.digest("hex")
|
|
198
|
+
.slice(0, 16);
|
|
199
|
+
// Registrations persisted by PUT /v1/executors load first; scopes supplied through
|
|
200
|
+
// NYLORUN_EXECUTORS_JSON then overlay them for this process only and are never written back.
|
|
201
|
+
this.registry.seed(this.store.allExecutors().map((row) => ({
|
|
202
|
+
agentId: row.agentId,
|
|
203
|
+
implementationVersion: row.implementationVersion,
|
|
204
|
+
...(row.manifestHash === undefined
|
|
205
|
+
? {}
|
|
206
|
+
: { manifestHash: row.manifestHash }),
|
|
207
|
+
tokenHash: row.tokenHash,
|
|
208
|
+
persisted: true,
|
|
209
|
+
updatedAt: row.updatedAt,
|
|
210
|
+
})));
|
|
211
|
+
const now = new Date().toISOString();
|
|
212
|
+
for (const executor of options.executors) {
|
|
213
|
+
const tokenHash = hashToken(executor.token);
|
|
214
|
+
const collision = this.registry.find(tokenHash);
|
|
215
|
+
if (collision && collision.agentId !== executor.agentId) {
|
|
216
|
+
this.store.db.close();
|
|
217
|
+
if (this.lockPath)
|
|
218
|
+
unlinkSync(this.lockPath);
|
|
219
|
+
throw new Error("Executor tokens must be unique");
|
|
220
|
+
}
|
|
221
|
+
this.registry.upsert({
|
|
222
|
+
agentId: executor.agentId,
|
|
223
|
+
implementationVersion: executor.implementationVersion,
|
|
224
|
+
...(executor.manifestHash === undefined
|
|
225
|
+
? {}
|
|
226
|
+
: { manifestHash: executor.manifestHash }),
|
|
227
|
+
tokenHash,
|
|
228
|
+
persisted: false,
|
|
229
|
+
updatedAt: now,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
180
232
|
this.vault = new VaultService(this.store.db, (fn) => this.store.tx(fn), () => this.ensureKek(), options.vaultFetch ?? globalThis.fetch);
|
|
181
233
|
this.mcp = new McpPool({
|
|
182
234
|
dataDir: dirname(options.sqlitePath),
|
|
183
235
|
authorize: (sessionId, request) => this.authorize(sessionId, request),
|
|
184
236
|
});
|
|
237
|
+
const ephemeral = options.sqlitePath === ":memory:";
|
|
238
|
+
const sandboxRoot = options.sandbox?.root ??
|
|
239
|
+
(ephemeral
|
|
240
|
+
? mkdtempSync(join(tmpdir(), "nylorun-sandboxes-"))
|
|
241
|
+
: join(dirname(options.sqlitePath), "sandboxes"));
|
|
242
|
+
this.sandbox = new SandboxManager({
|
|
243
|
+
scope: ephemeral ? `m${randomUUID().slice(0, 8)}` : this.scopeId,
|
|
244
|
+
store: this.store,
|
|
245
|
+
backends: options.sandbox?.backends ?? defaultSandboxBackends({ root: sandboxRoot }),
|
|
246
|
+
preference: options.sandbox?.backend ?? process.env.NYLORUN_SANDBOX,
|
|
247
|
+
ephemeral,
|
|
248
|
+
emit: (sessionId, turnId, type, payload) => {
|
|
249
|
+
if (this.closed)
|
|
250
|
+
return;
|
|
251
|
+
const event = this.store.tx(() => this.store.event(sessionId, turnId, type, payload));
|
|
252
|
+
this.publish(event);
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
if (!ephemeral && this.sandbox.hasRecords())
|
|
256
|
+
void this.sandbox
|
|
257
|
+
.reconcile((id) => !this.closed && !!this.store.get("sessions", id))
|
|
258
|
+
.catch(() => undefined);
|
|
185
259
|
this.store.tx(() => {
|
|
186
260
|
for (const effect of this.store.all("effects"))
|
|
187
261
|
if (effect.status === "invoking") {
|
|
@@ -199,6 +273,7 @@ export class CoreRuntime {
|
|
|
199
273
|
}
|
|
200
274
|
}
|
|
201
275
|
});
|
|
276
|
+
this.retireLegacyHooks();
|
|
202
277
|
this.expireClaims();
|
|
203
278
|
this.timer = setInterval(() => this.expireClaims(), Math.min(options.leaseMs ?? 30000, 5000));
|
|
204
279
|
this.timer.unref();
|
|
@@ -218,15 +293,53 @@ export class CoreRuntime {
|
|
|
218
293
|
response.destroy();
|
|
219
294
|
}
|
|
220
295
|
notify() {
|
|
221
|
-
for (const
|
|
222
|
-
|
|
296
|
+
for (const streams of this.executorStreams.values())
|
|
297
|
+
for (const response of streams)
|
|
298
|
+
this.send(response, 'event: work_available\ndata: {"type":"work_available"}\n\n');
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Manifest v4 replaced beforeModelCall/afterModelCall with scoped hooks. Turns started under
|
|
302
|
+
* an older manifest cannot continue, and their hook actions no longer parse, so end them.
|
|
303
|
+
*/
|
|
304
|
+
retireLegacyHooks() {
|
|
305
|
+
this.store.tx(() => {
|
|
306
|
+
for (const action of this.store.all("actions"))
|
|
307
|
+
if ((action.kind === "beforeModelCall" || action.kind === "afterModelCall") &&
|
|
308
|
+
(action.status === "pending" || action.status === "claimed")) {
|
|
309
|
+
action.status = "cancelled";
|
|
310
|
+
this.store.put("actions", action.actionId, action);
|
|
311
|
+
}
|
|
312
|
+
for (const s of this.store.all("sessions"))
|
|
313
|
+
if (s.activeTurnId && s.manifest?.manifestSchemaVersion !== 4) {
|
|
314
|
+
this.store.event(s.id, s.activeTurnId, "turn.failed", {
|
|
315
|
+
error: {
|
|
316
|
+
code: "execution.incompatible",
|
|
317
|
+
message: "This turn started under manifest schema 3; see MIGRATION.md for before/after hooks",
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
s.status = "failed";
|
|
321
|
+
s.activeTurnId = null;
|
|
322
|
+
this.store.put("sessions", s.id, s);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
223
325
|
}
|
|
224
326
|
expireClaims() {
|
|
225
327
|
const events = [];
|
|
328
|
+
let redeliver = false;
|
|
226
329
|
this.store.tx(() => {
|
|
227
330
|
for (const action of this.store.all("actions"))
|
|
228
331
|
if (action.status === "claimed" &&
|
|
229
332
|
Date.parse(action.leaseExpiresAt) <= Date.now()) {
|
|
333
|
+
if (action.kind === "hook") {
|
|
334
|
+
// Hooks are pure by contract, so a lost claim is simply delivered again.
|
|
335
|
+
// The next claim bumps the generation, which fences any late result.
|
|
336
|
+
action.status = "pending";
|
|
337
|
+
action.claimId = null;
|
|
338
|
+
action.leaseExpiresAt = null;
|
|
339
|
+
this.store.put("actions", action.actionId, action);
|
|
340
|
+
redeliver = true;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
230
343
|
action.status = "uncertain";
|
|
231
344
|
this.store.put("actions", action.actionId, action);
|
|
232
345
|
const effect = this.store.get("effects", action.actionId);
|
|
@@ -243,6 +356,8 @@ export class CoreRuntime {
|
|
|
243
356
|
}
|
|
244
357
|
});
|
|
245
358
|
events.forEach((e) => this.publish(e));
|
|
359
|
+
if (redeliver)
|
|
360
|
+
this.notify();
|
|
246
361
|
}
|
|
247
362
|
schedule(id) {
|
|
248
363
|
if (this.closing)
|
|
@@ -357,6 +472,20 @@ export class CoreRuntime {
|
|
|
357
472
|
this.schedule(id);
|
|
358
473
|
}
|
|
359
474
|
}
|
|
475
|
+
invokeModel(request, signal) {
|
|
476
|
+
if (!this.options.useHostModel)
|
|
477
|
+
return (this.options.model ?? scriptedModel())(request, signal);
|
|
478
|
+
const adapter = piModel({
|
|
479
|
+
readHostModel: () => this.vault.readHostModel(),
|
|
480
|
+
writeHostCredential: (credential) => this.vault.updateHostCredential(credential),
|
|
481
|
+
});
|
|
482
|
+
return adapter(request.input, {
|
|
483
|
+
request: request.context.request,
|
|
484
|
+
invocationId: String(request.context.invocationId),
|
|
485
|
+
signal,
|
|
486
|
+
reportPreparedCall() { },
|
|
487
|
+
});
|
|
488
|
+
}
|
|
360
489
|
async resolveEffect(request, signal) {
|
|
361
490
|
let invoke;
|
|
362
491
|
let notify = false;
|
|
@@ -383,29 +512,29 @@ export class CoreRuntime {
|
|
|
383
512
|
const mcpTool = request.kind === "tool"
|
|
384
513
|
? mcpToolOf(s, request.capabilityId, request.toolName)
|
|
385
514
|
: undefined;
|
|
515
|
+
const sandboxTool = request.kind === "tool" && !mcpTool
|
|
516
|
+
? sandboxCapabilityOf(s.manifest, request.capabilityId, request.toolName)
|
|
517
|
+
: undefined;
|
|
386
518
|
this.store.put("effects", request.effectId, {
|
|
387
519
|
request,
|
|
388
|
-
status: request.kind === "model" || mcpTool
|
|
520
|
+
status: request.kind === "model" || mcpTool || sandboxTool
|
|
521
|
+
? "invoking"
|
|
522
|
+
: "pending",
|
|
389
523
|
});
|
|
390
|
-
if (request.kind === "model" || mcpTool) {
|
|
391
|
-
invoke = mcpTool ? "mcp" : "model";
|
|
524
|
+
if (request.kind === "model" || mcpTool || sandboxTool) {
|
|
525
|
+
invoke = mcpTool ? "mcp" : sandboxTool ? "sandbox" : "model";
|
|
392
526
|
return undefined;
|
|
393
527
|
}
|
|
394
528
|
const tool = request.kind === "tool"
|
|
395
529
|
? pinnedTool(s.manifest, request.capabilityId, request.toolName)
|
|
396
530
|
: undefined;
|
|
397
|
-
const
|
|
531
|
+
const base = {
|
|
398
532
|
actionId: request.effectId,
|
|
399
533
|
sessionId: request.sessionId,
|
|
400
534
|
turnId: request.turnId,
|
|
401
535
|
agentId: request.agentId,
|
|
402
536
|
manifestHash: request.manifestHash,
|
|
403
537
|
implementationVersion: s.implementationVersion,
|
|
404
|
-
kind: request.kind,
|
|
405
|
-
capabilityId: request.capabilityId,
|
|
406
|
-
...(request.toolName ? { toolName: request.toolName } : {}),
|
|
407
|
-
...(tool?.inputSchema ? { inputSchema: tool.inputSchema } : {}),
|
|
408
|
-
...(tool?.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
409
538
|
input: request.input,
|
|
410
539
|
context: request.context,
|
|
411
540
|
status: "pending",
|
|
@@ -413,11 +542,29 @@ export class CoreRuntime {
|
|
|
413
542
|
claimId: null,
|
|
414
543
|
leaseExpiresAt: null,
|
|
415
544
|
};
|
|
545
|
+
const action = request.kind === "hook"
|
|
546
|
+
? {
|
|
547
|
+
...base,
|
|
548
|
+
kind: "hook",
|
|
549
|
+
hook: {
|
|
550
|
+
at: request.hook.at,
|
|
551
|
+
scope: request.hook.scope,
|
|
552
|
+
capabilityIds: [...request.hook.capabilityIds],
|
|
553
|
+
},
|
|
554
|
+
}
|
|
555
|
+
: {
|
|
556
|
+
...base,
|
|
557
|
+
kind: "tool",
|
|
558
|
+
capabilityId: request.capabilityId,
|
|
559
|
+
toolName: request.toolName,
|
|
560
|
+
...(tool?.inputSchema ? { inputSchema: tool.inputSchema } : {}),
|
|
561
|
+
...(tool?.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
562
|
+
};
|
|
416
563
|
this.store.put("actions", action.actionId, action);
|
|
417
564
|
event = this.store.event(s.id, s.activeTurnId, "action.pending", {
|
|
418
565
|
actionId: action.actionId,
|
|
419
566
|
kind: action.kind,
|
|
420
|
-
|
|
567
|
+
...actionTarget(action),
|
|
421
568
|
input: action.input,
|
|
422
569
|
});
|
|
423
570
|
notify = true;
|
|
@@ -432,7 +579,9 @@ export class CoreRuntime {
|
|
|
432
579
|
try {
|
|
433
580
|
const value = invoke === "mcp"
|
|
434
581
|
? await this.callMcpTool(request)
|
|
435
|
-
:
|
|
582
|
+
: invoke === "sandbox"
|
|
583
|
+
? await this.callSandboxTool(request, signal)
|
|
584
|
+
: await this.invokeModel(request, signal);
|
|
436
585
|
return this.store.tx(() => {
|
|
437
586
|
const s = this.session(request.sessionId);
|
|
438
587
|
if (s.status === "cancelled" ||
|
|
@@ -468,9 +617,13 @@ export class CoreRuntime {
|
|
|
468
617
|
scope(request) {
|
|
469
618
|
const header = request.headers.authorization;
|
|
470
619
|
const token = header?.startsWith("Bearer ") ? header.slice(7) : "";
|
|
471
|
-
if (token
|
|
620
|
+
if (!token)
|
|
621
|
+
return fail(401, "Invalid credentials");
|
|
622
|
+
if (equals(token, this.options.serverToken))
|
|
472
623
|
return "server";
|
|
473
|
-
|
|
624
|
+
// The map is keyed by a digest of the presented token, so the lookup reveals nothing the
|
|
625
|
+
// caller does not already hold; the server token keeps its constant-time comparison.
|
|
626
|
+
const executor = this.registry.find(hashToken(token));
|
|
474
627
|
if (!executor)
|
|
475
628
|
return fail(401, "Invalid credentials");
|
|
476
629
|
return executor;
|
|
@@ -542,7 +695,7 @@ export class CoreRuntime {
|
|
|
542
695
|
schedule = true;
|
|
543
696
|
event = this.store.event(id, s.activeTurnId, "action.completed", {
|
|
544
697
|
actionId: action.actionId,
|
|
545
|
-
|
|
698
|
+
...actionTarget(action),
|
|
546
699
|
kind: action.kind,
|
|
547
700
|
result: command.outcome.value,
|
|
548
701
|
});
|
|
@@ -658,7 +811,7 @@ export class CoreRuntime {
|
|
|
658
811
|
this.schedule(id);
|
|
659
812
|
return response;
|
|
660
813
|
}
|
|
661
|
-
sse(request, response, set) {
|
|
814
|
+
sse(request, response, set, whenEmpty) {
|
|
662
815
|
response.writeHead(200, {
|
|
663
816
|
"content-type": "text/event-stream",
|
|
664
817
|
"cache-control": "no-cache",
|
|
@@ -672,6 +825,8 @@ export class CoreRuntime {
|
|
|
672
825
|
request.on("close", () => {
|
|
673
826
|
clearInterval(timer);
|
|
674
827
|
set.delete(response);
|
|
828
|
+
if (!set.size)
|
|
829
|
+
whenEmpty?.();
|
|
675
830
|
});
|
|
676
831
|
}
|
|
677
832
|
async handle(request, response) {
|
|
@@ -687,7 +842,13 @@ export class CoreRuntime {
|
|
|
687
842
|
.map(decodeURIComponent);
|
|
688
843
|
const method = request.method;
|
|
689
844
|
if (url.pathname === "/health")
|
|
690
|
-
return json({
|
|
845
|
+
return json({
|
|
846
|
+
status: "ok",
|
|
847
|
+
service: "oss-runtime",
|
|
848
|
+
version: RUNTIME_VERSION,
|
|
849
|
+
scopeId: this.scopeId,
|
|
850
|
+
pid: process.pid,
|
|
851
|
+
});
|
|
691
852
|
if (url.pathname === "/ready") {
|
|
692
853
|
this.store.db.prepare("SELECT 1").get();
|
|
693
854
|
return json({
|
|
@@ -708,7 +869,11 @@ export class CoreRuntime {
|
|
|
708
869
|
method === "GET") {
|
|
709
870
|
if (scope === "server")
|
|
710
871
|
fail(403, "Executor credential required");
|
|
711
|
-
|
|
872
|
+
const tokenHash = scope.tokenHash;
|
|
873
|
+
let streams = this.executorStreams.get(tokenHash);
|
|
874
|
+
if (!streams)
|
|
875
|
+
this.executorStreams.set(tokenHash, (streams = new Set()));
|
|
876
|
+
this.sse(request, response, streams, () => this.executorStreams.delete(tokenHash));
|
|
712
877
|
this.send(response, 'event: work_available\ndata: {"type":"work_available"}\n\n');
|
|
713
878
|
return;
|
|
714
879
|
}
|
|
@@ -775,8 +940,99 @@ export class CoreRuntime {
|
|
|
775
940
|
return json(this.command(path[2], SessionCommandSchema.parse(await this.body(request)), scope));
|
|
776
941
|
if (path[1] === "vaults")
|
|
777
942
|
return json(await this.dispatchVault(scope, method, path, url, request));
|
|
943
|
+
if (path[1] === "host")
|
|
944
|
+
return json(await this.dispatchHost(scope, method, path, request));
|
|
778
945
|
if (scope !== "server")
|
|
779
946
|
fail(403, "Application credential required");
|
|
947
|
+
if (path[1] === "executors" && path.length === 2 && method === "GET")
|
|
948
|
+
return json({
|
|
949
|
+
executors: this.registry.list().map((e) => ({
|
|
950
|
+
agentId: e.agentId,
|
|
951
|
+
implementationVersion: e.implementationVersion,
|
|
952
|
+
...(e.manifestHash === undefined
|
|
953
|
+
? {}
|
|
954
|
+
: { manifestHash: e.manifestHash }),
|
|
955
|
+
connected: (this.executorStreams.get(e.tokenHash)?.size ?? 0) > 0,
|
|
956
|
+
updatedAt: e.updatedAt,
|
|
957
|
+
})),
|
|
958
|
+
});
|
|
959
|
+
// The length guard matters: the connect branch above only matches GET, so without it a
|
|
960
|
+
// PUT to /v1/executors/connect would register an agent literally named "connect".
|
|
961
|
+
if (path[1] === "executors" && path.length === 2 && method === "PUT") {
|
|
962
|
+
const body = RegisterExecutorsRequestSchema.parse(await this.body(request));
|
|
963
|
+
// Credential rules are shared with startup validation, which throws plainly; on the
|
|
964
|
+
// wire a rejected registration is a bad request, not a server fault.
|
|
965
|
+
try {
|
|
966
|
+
for (const executor of body.executors)
|
|
967
|
+
assertExecutorCredential(executor, this.options.serverToken);
|
|
968
|
+
}
|
|
969
|
+
catch (error) {
|
|
970
|
+
fail(400, error instanceof Error ? error.message : String(error));
|
|
971
|
+
}
|
|
972
|
+
if (new Set(body.executors.map((e) => e.agentId)).size !==
|
|
973
|
+
body.executors.length ||
|
|
974
|
+
new Set(body.executors.map((e) => e.token)).size !==
|
|
975
|
+
body.executors.length)
|
|
976
|
+
fail(400, "Executor registrations must be unique per agent and token");
|
|
977
|
+
const updatedAt = new Date().toISOString();
|
|
978
|
+
const records = body.executors.map((executor) => ({
|
|
979
|
+
agentId: executor.agentId,
|
|
980
|
+
implementationVersion: executor.implementationVersion,
|
|
981
|
+
...(executor.manifestHash === undefined
|
|
982
|
+
? {}
|
|
983
|
+
: { manifestHash: executor.manifestHash }),
|
|
984
|
+
tokenHash: hashToken(executor.token),
|
|
985
|
+
persisted: true,
|
|
986
|
+
updatedAt,
|
|
987
|
+
}));
|
|
988
|
+
for (const record of records) {
|
|
989
|
+
const collision = this.registry.find(record.tokenHash);
|
|
990
|
+
if (collision && collision.agentId !== record.agentId)
|
|
991
|
+
fail(409, "Executor tokens must be unique");
|
|
992
|
+
}
|
|
993
|
+
// Persist the whole batch first; the in-memory registry must never run ahead of SQLite.
|
|
994
|
+
this.store.tx(() => {
|
|
995
|
+
for (const record of records)
|
|
996
|
+
this.store.putExecutor({
|
|
997
|
+
agentId: record.agentId,
|
|
998
|
+
tokenHash: record.tokenHash,
|
|
999
|
+
implementationVersion: record.implementationVersion,
|
|
1000
|
+
...(record.manifestHash === undefined
|
|
1001
|
+
? {}
|
|
1002
|
+
: { manifestHash: record.manifestHash }),
|
|
1003
|
+
updatedAt,
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
1006
|
+
const results = records.map((record) => {
|
|
1007
|
+
const { rotated, previousHash } = this.registry.upsert(record);
|
|
1008
|
+
if (rotated && previousHash) {
|
|
1009
|
+
for (const r of this.executorStreams.get(previousHash) ?? [])
|
|
1010
|
+
r.end();
|
|
1011
|
+
this.executorStreams.delete(previousHash);
|
|
1012
|
+
}
|
|
1013
|
+
return {
|
|
1014
|
+
agentId: record.agentId,
|
|
1015
|
+
implementationVersion: record.implementationVersion,
|
|
1016
|
+
rotated,
|
|
1017
|
+
};
|
|
1018
|
+
});
|
|
1019
|
+
return json({ executors: results });
|
|
1020
|
+
}
|
|
1021
|
+
if (path[1] === "executors" &&
|
|
1022
|
+
path.length === 3 &&
|
|
1023
|
+
path[2] &&
|
|
1024
|
+
method === "DELETE") {
|
|
1025
|
+
const agentId = path[2];
|
|
1026
|
+
const existing = this.registry.get(agentId) ?? fail(404, "Executor not found");
|
|
1027
|
+
if (!existing.persisted)
|
|
1028
|
+
fail(409, "Executor is provided by NYLORUN_EXECUTORS_JSON and cannot be deleted");
|
|
1029
|
+
this.store.tx(() => this.store.deleteExecutor(agentId));
|
|
1030
|
+
this.registry.remove(agentId);
|
|
1031
|
+
for (const r of this.executorStreams.get(existing.tokenHash) ?? [])
|
|
1032
|
+
r.end();
|
|
1033
|
+
this.executorStreams.delete(existing.tokenHash);
|
|
1034
|
+
return json({ agentId, deleted: true });
|
|
1035
|
+
}
|
|
780
1036
|
if (path[1] === "agents" && path.length === 2 && method === "GET")
|
|
781
1037
|
return json({
|
|
782
1038
|
agents: this.store.all("definitions").map((d) => ({
|
|
@@ -1004,6 +1260,13 @@ export class CoreRuntime {
|
|
|
1004
1260
|
pluginRoots: s.pluginRoots ?? {},
|
|
1005
1261
|
});
|
|
1006
1262
|
}
|
|
1263
|
+
async callSandboxTool(request, signal) {
|
|
1264
|
+
const s = this.session(request.sessionId);
|
|
1265
|
+
const capability = sandboxCapabilityOf(s.manifest, request.capabilityId, request.toolName);
|
|
1266
|
+
if (!capability)
|
|
1267
|
+
throw new Error(`'${request.toolName ?? ""}' is not a sandbox tool`);
|
|
1268
|
+
return this.sandbox.run({ id: s.id, activeTurnId: s.activeTurnId, manifest: s.manifest }, capability, request.toolName, request.input, signal);
|
|
1269
|
+
}
|
|
1007
1270
|
async authorize(sessionId, request) {
|
|
1008
1271
|
const s = this.session(sessionId);
|
|
1009
1272
|
const result = await this.vault.authorize({
|
|
@@ -1029,6 +1292,28 @@ export class CoreRuntime {
|
|
|
1029
1292
|
this.kek = createKekFile(this.kekPath);
|
|
1030
1293
|
return this.kek;
|
|
1031
1294
|
}
|
|
1295
|
+
async dispatchHost(scope, method, path, request) {
|
|
1296
|
+
if (scope !== "server") {
|
|
1297
|
+
this.vault.reject(path.join("/"));
|
|
1298
|
+
fail(403, "Application credential required");
|
|
1299
|
+
}
|
|
1300
|
+
if (path[2] === "models" && path.length === 3 && method === "GET")
|
|
1301
|
+
return hostModelCatalog();
|
|
1302
|
+
if (path[2] === "sandbox" && path.length === 3 && method === "GET")
|
|
1303
|
+
return this.sandbox.report();
|
|
1304
|
+
if (path[2] === "providers" && path.length === 3 && method === "GET")
|
|
1305
|
+
return this.vault.listHostProviders();
|
|
1306
|
+
if (path[2] === "model" && path.length === 3 && method === "GET")
|
|
1307
|
+
return this.vault.getHostModel();
|
|
1308
|
+
if (path[2] === "model" && path.length === 3 && method === "PUT")
|
|
1309
|
+
return this.vault.putHostModel(PutHostModelRequestSchema.parse(await this.body(request)));
|
|
1310
|
+
if (path[2] === "model" &&
|
|
1311
|
+
path[3] === "selection" &&
|
|
1312
|
+
path.length === 4 &&
|
|
1313
|
+
method === "PUT")
|
|
1314
|
+
return this.vault.selectHostModel(SelectHostModelRequestSchema.parse(await this.body(request)));
|
|
1315
|
+
fail(404, "Route not found");
|
|
1316
|
+
}
|
|
1032
1317
|
async dispatchVault(scope, method, path, url, request) {
|
|
1033
1318
|
if (scope !== "server") {
|
|
1034
1319
|
this.vault.reject(path.join("/"));
|
|
@@ -1092,8 +1377,9 @@ export class CoreRuntime {
|
|
|
1092
1377
|
for (const c of this.running.values())
|
|
1093
1378
|
c.abort();
|
|
1094
1379
|
await this.mcp.close();
|
|
1095
|
-
for (const
|
|
1096
|
-
r
|
|
1380
|
+
for (const streams of this.executorStreams.values())
|
|
1381
|
+
for (const r of streams)
|
|
1382
|
+
r.end();
|
|
1097
1383
|
for (const set of this.observers.values())
|
|
1098
1384
|
for (const r of set)
|
|
1099
1385
|
r.end();
|
|
@@ -1105,6 +1391,8 @@ export class CoreRuntime {
|
|
|
1105
1391
|
// Providers must honor AbortSignal. Keep storage open until any in-flight journal writes have drained.
|
|
1106
1392
|
while (this.running.size)
|
|
1107
1393
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1394
|
+
await this.sandbox.close();
|
|
1395
|
+
this.closed = true;
|
|
1108
1396
|
this.store.db.close();
|
|
1109
1397
|
if (this.lockPath)
|
|
1110
1398
|
unlinkSync(this.lockPath);
|
|
@@ -1124,3 +1412,7 @@ export async function startRuntime(options) {
|
|
|
1124
1412
|
throw error;
|
|
1125
1413
|
}
|
|
1126
1414
|
}
|
|
1415
|
+
/** What an action runs, for events: a tool name, or a hook point and its capabilities. */
|
|
1416
|
+
function actionTarget(action) {
|
|
1417
|
+
return action.kind === "hook" ? { hook: action.hook } : { toolName: action.toolName };
|
|
1418
|
+
}
|
package/dist/core/store.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { DatabaseSync } from "node:sqlite";
|
|
2
2
|
import type { LiveEvent } from "@nylorun/core/contracts";
|
|
3
3
|
export declare function canonical(value: unknown): string;
|
|
4
|
+
export interface ExecutorRow {
|
|
5
|
+
agentId: string;
|
|
6
|
+
tokenHash: string;
|
|
7
|
+
implementationVersion: string;
|
|
8
|
+
manifestHash?: string;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
}
|
|
4
12
|
export declare class Store {
|
|
5
13
|
readonly db: DatabaseSync;
|
|
6
14
|
constructor(path: string);
|
|
@@ -8,6 +16,10 @@ export declare class Store {
|
|
|
8
16
|
get<T = any>(table: string, id: string): T | undefined;
|
|
9
17
|
all<T = any>(table: string): T[];
|
|
10
18
|
put(table: string, id: string, body: unknown): void;
|
|
19
|
+
delete(table: string, id: string): void;
|
|
20
|
+
allExecutors(): ExecutorRow[];
|
|
21
|
+
putExecutor(row: Omit<ExecutorRow, "createdAt">): void;
|
|
22
|
+
deleteExecutor(agentId: string): void;
|
|
11
23
|
event(sessionId: string, turnId: string | null, type: string, payload: unknown): LiveEvent;
|
|
12
24
|
credentialCount(): number;
|
|
13
25
|
history(sessionId: string, cursor?: string): {
|
package/dist/core/store.js
CHANGED
|
@@ -25,6 +25,7 @@ export class Store {
|
|
|
25
25
|
CREATE TABLE IF NOT EXISTS checkpoints(id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
|
26
26
|
CREATE TABLE IF NOT EXISTS effects(id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
|
27
27
|
CREATE TABLE IF NOT EXISTS actions(id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
|
28
|
+
CREATE TABLE IF NOT EXISTS sandboxes(id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
|
28
29
|
CREATE TABLE IF NOT EXISTS events(sequence INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, body TEXT NOT NULL);
|
|
29
30
|
CREATE INDEX IF NOT EXISTS events_session ON events(session_id, sequence);
|
|
30
31
|
CREATE TABLE IF NOT EXISTS vaults(
|
|
@@ -32,7 +33,8 @@ export class Store {
|
|
|
32
33
|
name TEXT NOT NULL,
|
|
33
34
|
owner_user_id TEXT NOT NULL,
|
|
34
35
|
metadata_json TEXT,
|
|
35
|
-
created_at TEXT NOT NULL
|
|
36
|
+
created_at TEXT NOT NULL,
|
|
37
|
+
scope TEXT NOT NULL DEFAULT 'user'
|
|
36
38
|
);
|
|
37
39
|
CREATE INDEX IF NOT EXISTS vaults_owner ON vaults(owner_user_id);
|
|
38
40
|
CREATE TABLE IF NOT EXISTS vault_credentials(
|
|
@@ -65,7 +67,16 @@ export class Store {
|
|
|
65
67
|
id TEXT PRIMARY KEY,
|
|
66
68
|
body_hash TEXT NOT NULL,
|
|
67
69
|
response TEXT NOT NULL
|
|
70
|
+
);
|
|
71
|
+
CREATE TABLE IF NOT EXISTS executors(
|
|
72
|
+
agent_id TEXT PRIMARY KEY,
|
|
73
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
74
|
+
implementation_version TEXT NOT NULL,
|
|
75
|
+
manifest_hash TEXT,
|
|
76
|
+
created_at TEXT NOT NULL,
|
|
77
|
+
updated_at TEXT NOT NULL
|
|
68
78
|
);`);
|
|
79
|
+
migrateVaultScope(this.db);
|
|
69
80
|
}
|
|
70
81
|
tx(fn) {
|
|
71
82
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -94,6 +105,38 @@ export class Store {
|
|
|
94
105
|
.prepare(`INSERT INTO ${table}(id,body) VALUES(?,?) ON CONFLICT(id) DO UPDATE SET body=excluded.body`)
|
|
95
106
|
.run(id, JSON.stringify(body));
|
|
96
107
|
}
|
|
108
|
+
delete(table, id) {
|
|
109
|
+
this.db.prepare(`DELETE FROM ${table} WHERE id=?`).run(id);
|
|
110
|
+
}
|
|
111
|
+
// Executors carry a bearer secret, so they use typed columns with a database-enforced unique
|
|
112
|
+
// token hash instead of flowing through the generic JSON document helpers above.
|
|
113
|
+
allExecutors() {
|
|
114
|
+
return this.db
|
|
115
|
+
.prepare("SELECT agent_id,token_hash,implementation_version,manifest_hash,created_at,updated_at FROM executors")
|
|
116
|
+
.all()
|
|
117
|
+
.map((r) => ({
|
|
118
|
+
agentId: String(r.agent_id),
|
|
119
|
+
tokenHash: String(r.token_hash),
|
|
120
|
+
implementationVersion: String(r.implementation_version),
|
|
121
|
+
manifestHash: r.manifest_hash == null ? undefined : String(r.manifest_hash),
|
|
122
|
+
createdAt: String(r.created_at),
|
|
123
|
+
updatedAt: String(r.updated_at),
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
putExecutor(row) {
|
|
127
|
+
this.db
|
|
128
|
+
.prepare(`INSERT INTO executors(agent_id,token_hash,implementation_version,manifest_hash,created_at,updated_at)
|
|
129
|
+
VALUES(?,?,?,?,?,?)
|
|
130
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
131
|
+
token_hash=excluded.token_hash,
|
|
132
|
+
implementation_version=excluded.implementation_version,
|
|
133
|
+
manifest_hash=excluded.manifest_hash,
|
|
134
|
+
updated_at=excluded.updated_at`)
|
|
135
|
+
.run(row.agentId, row.tokenHash, row.implementationVersion, row.manifestHash ?? null, row.updatedAt, row.updatedAt);
|
|
136
|
+
}
|
|
137
|
+
deleteExecutor(agentId) {
|
|
138
|
+
this.db.prepare("DELETE FROM executors WHERE agent_id=?").run(agentId);
|
|
139
|
+
}
|
|
97
140
|
event(sessionId, turnId, type, payload) {
|
|
98
141
|
const base = {
|
|
99
142
|
eventId: randomUUID(),
|
|
@@ -144,3 +187,9 @@ export class Store {
|
|
|
144
187
|
};
|
|
145
188
|
}
|
|
146
189
|
}
|
|
190
|
+
function migrateVaultScope(db) {
|
|
191
|
+
const columns = db.prepare("PRAGMA table_info(vaults)").all();
|
|
192
|
+
if (!columns.some((column) => column.name === "scope"))
|
|
193
|
+
db.exec("ALTER TABLE vaults ADD COLUMN scope TEXT NOT NULL DEFAULT 'user'");
|
|
194
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS vaults_one_host ON vaults(scope) WHERE scope = 'host'");
|
|
195
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { CoreRuntime, createRuntime, startRuntime, type RuntimeOptions } from "./core/runtime.js";
|
|
2
2
|
export { scriptedModel, gatewayModel, type ModelProvider } from "./core/provider.js";
|
|
3
|
-
export { httpModel, type HttpModelOptions, type ModelEnvironment } from "./model/http-model.js";
|
|
4
3
|
export type { RuntimeAgent } from "./contracts.js";
|
|
5
4
|
export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes } from "./media.js";
|
|
6
5
|
export type { RuntimeModelAdapter } from "./contracts.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export { CoreRuntime, createRuntime, startRuntime } from "./core/runtime.js";
|
|
2
2
|
export { scriptedModel, gatewayModel } from "./core/provider.js";
|
|
3
|
-
export { httpModel } from "./model/http-model.js";
|
|
4
3
|
export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes } from "./media.js";
|