@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +7 -5
  3. package/dist/adapters/sandbox/microsandbox.d.ts +2 -0
  4. package/dist/adapters/sandbox/microsandbox.js +238 -0
  5. package/dist/adapters/sandbox/virtual.d.ts +4 -0
  6. package/dist/adapters/sandbox/virtual.js +125 -0
  7. package/dist/configuration.d.ts +1 -1
  8. package/dist/configuration.js +1 -1
  9. package/dist/core/executors.d.ts +32 -0
  10. package/dist/core/executors.js +60 -0
  11. package/dist/core/main.js +7 -12
  12. package/dist/core/runtime.d.ts +24 -1
  13. package/dist/core/runtime.js +322 -30
  14. package/dist/core/store.d.ts +12 -0
  15. package/dist/core/store.js +50 -1
  16. package/dist/index.d.ts +0 -1
  17. package/dist/index.js +0 -1
  18. package/dist/model/catalog.d.ts +12 -0
  19. package/dist/model/catalog.js +29 -0
  20. package/dist/model/models.d.ts +3 -1
  21. package/dist/model/models.js +4 -2
  22. package/dist/model/pi-model.d.ts +9 -3
  23. package/dist/model/pi-model.js +47 -11
  24. package/dist/model/settings.d.ts +0 -2
  25. package/dist/model/settings.js +0 -47
  26. package/dist/node/index.d.ts +1 -1
  27. package/dist/node/index.js +1 -1
  28. package/dist/sandbox/manager.d.ts +61 -0
  29. package/dist/sandbox/manager.js +239 -0
  30. package/dist/sandbox/policy.d.ts +13 -0
  31. package/dist/sandbox/policy.js +67 -0
  32. package/dist/sandbox/select.d.ts +35 -0
  33. package/dist/sandbox/select.js +76 -0
  34. package/dist/sandbox/tools.d.ts +25 -0
  35. package/dist/sandbox/tools.js +165 -0
  36. package/dist/sandbox/types.d.ts +66 -0
  37. package/dist/vault/service.d.ts +41 -1
  38. package/dist/vault/service.js +291 -4
  39. package/dist/version.d.ts +1 -0
  40. package/dist/version.js +3 -0
  41. package/package.json +9 -4
  42. package/dist/config.d.ts +0 -20
  43. package/dist/model/defaults.d.ts +0 -17
  44. package/dist/model/defaults.js +0 -21
  45. package/dist/model/http-model.d.ts +0 -12
  46. package/dist/model/http-model.js +0 -299
  47. package/dist/node/local-sessions.d.ts +0 -5
  48. package/dist/node/local-sessions.js +0 -174
  49. package/dist/server/ag-ui.d.ts +0 -8
  50. package/dist/server/ag-ui.js +0 -76
  51. package/dist/server/delivery.d.ts +0 -24
  52. package/dist/server/delivery.js +0 -107
  53. package/dist/server/digests.d.ts +0 -13
  54. package/dist/server/digests.js +0 -39
  55. package/dist/server/host.d.ts +0 -70
  56. package/dist/server/host.js +0 -647
  57. package/dist/session/api.d.ts +0 -10
  58. package/dist/session/api.js +0 -15
  59. package/dist/session/default.d.ts +0 -5
  60. package/dist/session/default.js +0 -30
  61. package/dist/session/handle.d.ts +0 -27
  62. package/dist/session/handle.js +0 -199
  63. package/dist/session/index.d.ts +0 -2
  64. package/dist/session/index.js +0 -2
  65. package/dist/sessions/host.d.ts +0 -39
  66. package/dist/sessions/host.js +0 -359
  67. package/dist/sessions/store.d.ts +0 -41
  68. package/dist/sessions/store.js +0 -33
  69. /package/dist/{config.js → sandbox/types.js} +0 -0
@@ -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 { dirname } from "node:path";
5
- import { AgentManifestSchema, CreateCredentialRequestSchema, CreateVaultRequestSchema, PutAgentRequestSchema, PutSessionRequestSchema, RotateCredentialRequestSchema, SessionCommandSchema, ActionClaimRequestSchema, ActionHeartbeatRequestSchema, } from "@nylorun/core/contracts";
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
- executors = new Set();
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
- if (options.executors.some((e) => e.token.length < 16 ||
132
- equals(e.token, options.serverToken) ||
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 response of this.executors)
222
- this.send(response, 'event: work_available\ndata: {"type":"work_available"}\n\n');
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 ? "invoking" : "pending",
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 action = {
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
- toolName: action.toolName,
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
- : await (this.options.model ?? scriptedModel())(request, signal);
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 && equals(token, this.options.serverToken))
620
+ if (!token)
621
+ return fail(401, "Invalid credentials");
622
+ if (equals(token, this.options.serverToken))
472
623
  return "server";
473
- const executor = this.options.executors.find((e) => token && equals(token, e.token));
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
- toolName: action.toolName,
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({ status: "ok", service: "oss-runtime" });
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
- this.sse(request, response, this.executors);
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 r of this.executors)
1096
- r.end();
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
+ }
@@ -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): {
@@ -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";