@nylorun/runtime 0.9.0-beta → 0.10.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 (129) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +96 -25
  3. package/dist/adapters/media.d.ts +2 -2
  4. package/dist/adapters/media.js +2 -2
  5. package/dist/adapters/observe.d.ts +2 -1
  6. package/dist/adapters/observe.js +2 -3
  7. package/dist/assets.d.ts +2 -2
  8. package/dist/assets.js +5 -3
  9. package/dist/core/executors.d.ts +11 -2
  10. package/dist/core/executors.js +20 -4
  11. package/dist/core/flow-host.d.ts +264 -0
  12. package/dist/core/flow-host.js +783 -0
  13. package/dist/core/limits.d.ts +40 -0
  14. package/dist/core/limits.js +76 -0
  15. package/dist/core/runtime.d.ts +5 -90
  16. package/dist/core/runtime.js +3 -1447
  17. package/dist/core/sandbox-routes.d.ts +35 -0
  18. package/dist/core/sandbox-routes.js +106 -0
  19. package/dist/core/store.d.ts +3 -1
  20. package/dist/core/store.js +14 -73
  21. package/dist/core/turn-manifest.d.ts +50 -0
  22. package/dist/core/turn-manifest.js +66 -0
  23. package/dist/host/config-for.d.ts +16 -0
  24. package/dist/host/config-for.js +45 -0
  25. package/dist/host/config.d.ts +24 -0
  26. package/dist/host/create-host.d.ts +21 -0
  27. package/dist/host/create-host.js +396 -0
  28. package/dist/host/environment.d.ts +13 -0
  29. package/dist/host/environment.js +53 -0
  30. package/dist/host/http.d.ts +35 -0
  31. package/dist/host/http.js +136 -0
  32. package/dist/host/index.d.ts +6 -0
  33. package/dist/host/index.js +5 -0
  34. package/dist/host/logger.d.ts +3 -0
  35. package/dist/host/logger.js +18 -0
  36. package/dist/host/main.d.ts +3 -0
  37. package/dist/host/main.js +142 -0
  38. package/dist/index.d.ts +7 -2
  39. package/dist/index.js +4 -2
  40. package/dist/launcher/commands.d.ts +24 -0
  41. package/dist/launcher/commands.js +260 -0
  42. package/dist/launcher/ensure-config.d.ts +16 -0
  43. package/dist/launcher/ensure-config.js +79 -0
  44. package/dist/launcher/errors.d.ts +12 -0
  45. package/dist/launcher/errors.js +22 -0
  46. package/dist/launcher/host-config.d.ts +17 -0
  47. package/dist/launcher/host-config.js +64 -0
  48. package/dist/launcher/host-state.d.ts +17 -0
  49. package/dist/launcher/host-state.js +61 -0
  50. package/dist/launcher/lifecycle.d.ts +66 -0
  51. package/dist/launcher/lifecycle.js +407 -0
  52. package/dist/launcher/locks.d.ts +26 -0
  53. package/dist/launcher/locks.js +117 -0
  54. package/dist/launcher/logs.d.ts +18 -0
  55. package/dist/launcher/logs.js +117 -0
  56. package/dist/launcher/main.d.ts +2 -0
  57. package/dist/launcher/main.js +66 -0
  58. package/dist/launcher/output.d.ts +11 -0
  59. package/dist/launcher/output.js +68 -0
  60. package/dist/launcher/paths.d.ts +20 -0
  61. package/dist/launcher/paths.js +36 -0
  62. package/dist/launcher/probe.d.ts +23 -0
  63. package/dist/launcher/probe.js +72 -0
  64. package/dist/launcher/protocol.d.ts +62 -0
  65. package/dist/launcher/protocol.js +1 -0
  66. package/dist/launcher/schema-guard.d.ts +6 -0
  67. package/dist/launcher/schema-guard.js +36 -0
  68. package/dist/launcher/spawn.d.ts +22 -0
  69. package/dist/launcher/spawn.js +85 -0
  70. package/dist/launcher/status.d.ts +6 -0
  71. package/dist/launcher/status.js +65 -0
  72. package/dist/mcp/connect.d.ts +1 -0
  73. package/dist/mcp/connect.js +2 -0
  74. package/dist/mcp/pool.d.ts +3 -1
  75. package/dist/mcp/pool.js +2 -1
  76. package/dist/mcp/stdio.d.ts +2 -0
  77. package/dist/mcp/stdio.js +3 -1
  78. package/dist/model/auth-store.d.ts +1 -1
  79. package/dist/model/auth-store.js +2 -2
  80. package/dist/model/models.d.ts +4 -0
  81. package/dist/model/models.js +22 -26
  82. package/dist/model/pi-model.js +1 -1
  83. package/dist/model/settings.d.ts +5 -1
  84. package/dist/model/settings.js +6 -2
  85. package/dist/sandbox/select.d.ts +4 -3
  86. package/dist/sandbox/select.js +15 -9
  87. package/dist/sandbox/share.d.ts +23 -0
  88. package/dist/sandbox/share.js +58 -0
  89. package/dist/tenant/envelope.d.ts +10 -0
  90. package/dist/tenant/envelope.js +54 -0
  91. package/dist/tenant/ephemeral.d.ts +31 -0
  92. package/dist/tenant/ephemeral.js +156 -0
  93. package/dist/tenant/logger.d.ts +9 -0
  94. package/dist/tenant/logger.js +25 -0
  95. package/dist/tenant/migration.d.ts +16 -0
  96. package/dist/tenant/migration.js +116 -0
  97. package/dist/tenant/module.d.ts +17 -0
  98. package/dist/tenant/module.js +315 -0
  99. package/dist/tenant/paths.d.ts +24 -0
  100. package/dist/tenant/paths.js +65 -0
  101. package/dist/tenant/pool.d.ts +10 -0
  102. package/dist/tenant/pool.js +54 -0
  103. package/dist/tenant/principals.d.ts +17 -0
  104. package/dist/tenant/principals.js +44 -0
  105. package/dist/tenant/quarantine-error.d.ts +6 -0
  106. package/dist/tenant/quarantine-error.js +15 -0
  107. package/dist/tenant/quarantine.d.ts +35 -0
  108. package/dist/tenant/quarantine.js +94 -0
  109. package/dist/tenant/reset.d.ts +21 -0
  110. package/dist/tenant/reset.js +125 -0
  111. package/dist/tenant/runtime.d.ts +80 -0
  112. package/dist/tenant/runtime.js +2256 -0
  113. package/dist/tenant/schema.d.ts +23 -0
  114. package/dist/tenant/schema.js +151 -0
  115. package/dist/tenant/status.d.ts +28 -0
  116. package/dist/tenant/status.js +95 -0
  117. package/dist/tenant/store-fs.d.ts +16 -0
  118. package/dist/tenant/store-fs.js +205 -0
  119. package/dist/tenant/store-memory.d.ts +19 -0
  120. package/dist/tenant/store-memory.js +100 -0
  121. package/dist/tenant/types.d.ts +125 -0
  122. package/dist/tenant/types.js +1 -0
  123. package/dist/vault/kek.d.ts +5 -2
  124. package/dist/vault/kek.js +7 -10
  125. package/dist/version.d.ts +1 -1
  126. package/dist/version.js +1 -1
  127. package/package.json +9 -6
  128. package/dist/core/main.js +0 -35
  129. /package/dist/{core/main.d.ts → host/config.js} +0 -0
@@ -1,1447 +1,3 @@
1
- import { createServer, } from "node:http";
2
- import { randomUUID, timingSafeEqual } from "node:crypto";
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";
8
- import { createDurableCheckpoint, runDurable, } from "@nylorun/harness/run";
9
- import { hashManifest } from "@nylorun/core/compatibility";
10
- import { delegateManifest, schemaFromJSON, } from "@nylorun/core/define";
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";
16
- import { scriptedModel } from "./provider.js";
17
- import { scrub } from "../redact.js";
18
- import { createKekFile, defaultKekPath, readVaultKek } from "../vault/kek.js";
19
- import { VaultError } from "../vault/error.js";
20
- import { VaultService } from "../vault/service.js";
21
- import { McpPool, serversOf } from "../mcp/pool.js";
22
- import { SandboxManager, sandboxCapabilityOf } from "../sandbox/manager.js";
23
- import { defaultSandboxBackends } from "../sandbox/select.js";
24
- class HttpError extends Error {
25
- status;
26
- constructor(status, message) {
27
- super(message);
28
- this.status = status;
29
- }
30
- }
31
- const fail = (status, message) => {
32
- throw new HttpError(status, message);
33
- };
34
- function sessionToolsOf(snapshot) {
35
- if (!snapshot?.mcpTools.length)
36
- return undefined;
37
- return snapshot.mcpTools.map((tool) => ({
38
- ...(tool.agentId === undefined ? {} : { agentId: tool.agentId }),
39
- capabilityId: tool.capabilityId,
40
- name: tool.name,
41
- ...(tool.description === undefined
42
- ? {}
43
- : { description: tool.description }),
44
- inputSchema: tool.inputSchema,
45
- ...(tool.outputSchema === undefined
46
- ? {}
47
- : { outputSchema: tool.outputSchema }),
48
- }));
49
- }
50
- function mcpToolOf(session, request) {
51
- return session.mcpSnapshot?.mcpTools.find((tool) => tool.agentId === request.agent?.id &&
52
- tool.capabilityId === request.capabilityId &&
53
- tool.name === request.toolName);
54
- }
55
- /** The manifest of the agent an effect belongs to: the root, or an agent it uses as a tool. */
56
- function manifestFor(manifest, agent) {
57
- return agent ? delegateManifest(manifest, agent.id) : manifest;
58
- }
59
- function pinnedTool(manifest, capabilityId, toolName) {
60
- const capability = manifest.capabilities.find((item) => item.id === capabilityId);
61
- return capability?.tools?.find((tool) => tool.name === toolName);
62
- }
63
- function acceptedToolResult(action, command) {
64
- if (action.kind !== "tool" || !action.outputSchema)
65
- return command;
66
- const value = command.outcome.value;
67
- if (isFailedToolValue(value))
68
- return command;
69
- // Executors wrap successful tool output as `{ kind: "completed", output }`.
70
- // Validate the tool payload, not the outcome envelope.
71
- const candidate = completedToolOutput(value);
72
- let matches = false;
73
- try {
74
- matches = schemaFromJSON(action.outputSchema).validate(candidate).ok;
75
- }
76
- catch {
77
- matches = false;
78
- }
79
- if (matches)
80
- return command;
81
- return {
82
- ...command,
83
- outcome: {
84
- ...command.outcome,
85
- value: {
86
- kind: "failed",
87
- code: "tool.invalid-output",
88
- message: "Tool result does not match the output schema stored on the action",
89
- },
90
- },
91
- };
92
- }
93
- function completedToolOutput(value) {
94
- if (value &&
95
- typeof value === "object" &&
96
- !Array.isArray(value) &&
97
- value.kind === "completed" &&
98
- "output" in value) {
99
- return value.output;
100
- }
101
- return value;
102
- }
103
- function isFailedToolValue(value) {
104
- return (!!value &&
105
- typeof value === "object" &&
106
- !Array.isArray(value) &&
107
- value.kind === "failed");
108
- }
109
- const semantic = (value) => {
110
- const { requestId: _, ...body } = value;
111
- return canonical(body);
112
- };
113
- const sessionIdentity = (value) => {
114
- const { requestId: _requestId, vaultIds: _vaultIds, credentialSelections: _selections, ...body } = value ?? {};
115
- return canonical(body);
116
- };
117
- const equals = (a, b) => {
118
- const aa = Buffer.from(a), bb = Buffer.from(b);
119
- return aa.length === bb.length && timingSafeEqual(aa, bb);
120
- };
121
- export class CoreRuntime {
122
- options;
123
- store;
124
- vault;
125
- kek;
126
- kekPath;
127
- running = new Map();
128
- pending = new Set();
129
- observers = new Map();
130
- // Keyed by token hash so a rotation can end exactly the streams that the replaced token owns.
131
- executorStreams = new Map();
132
- registry = new ExecutorRegistry();
133
- scopeId;
134
- mcp;
135
- sandbox;
136
- server;
137
- closing = false;
138
- closed = false;
139
- lockPath;
140
- timer;
141
- constructor(options) {
142
- this.options = options;
143
- if (options.leaseMs !== undefined &&
144
- (!Number.isFinite(options.leaseMs) || options.leaseMs <= 0))
145
- throw new Error("leaseMs must be finite and positive");
146
- if (new Set(options.executors.map((e) => e.token)).size !==
147
- options.executors.length)
148
- throw new Error("Executor tokens must be unique");
149
- if (!options.serverToken || options.serverToken.length < 16)
150
- throw new Error("A server token of at least 16 characters is required");
151
- for (const executor of options.executors)
152
- assertExecutorCredential(executor, options.serverToken);
153
- if (options.sqlitePath !== ":memory:") {
154
- mkdirSync(dirname(options.sqlitePath), { recursive: true });
155
- this.lockPath = options.sqlitePath + ".runtime-lock";
156
- // One local scheduling owner; recover a stale process lock only after checking that its PID is absent.
157
- try {
158
- const oldPid = Number(readFileSync(this.lockPath, "utf8"));
159
- if (!Number.isSafeInteger(oldPid) || oldPid < 1)
160
- throw new Error("Invalid runtime lock; inspect before removing");
161
- try {
162
- process.kill(oldPid, 0);
163
- throw new Error("Another Runtime owns this SQLite database");
164
- }
165
- catch (error) {
166
- if (error.code !== "ESRCH")
167
- throw error;
168
- unlinkSync(this.lockPath);
169
- }
170
- }
171
- catch (error) {
172
- if (error.code !== "ENOENT")
173
- throw error;
174
- }
175
- const fd = openSync(this.lockPath, "wx");
176
- writeFileSync(fd, String(process.pid));
177
- closeSync(fd);
178
- }
179
- this.kekPath = options.vaultKekPath ?? defaultKekPath();
180
- let store;
181
- try {
182
- store = new Store(options.sqlitePath);
183
- this.kek = readVaultKek({
184
- vaultKek: options.vaultKek,
185
- vaultKekPath: this.kekPath,
186
- });
187
- if (store.credentialCount() > 0 && !this.kek)
188
- throw new Error("Vault key-encryption key is required to open this database");
189
- this.store = store;
190
- }
191
- catch (e) {
192
- store?.db.close();
193
- if (this.lockPath)
194
- unlinkSync(this.lockPath);
195
- throw e;
196
- }
197
- // A short digest of the resolved database path identifies this scope to the CLI without
198
- // disclosing the path itself over the unauthenticated health route.
199
- this.scopeId =
200
- options.sqlitePath === ":memory:"
201
- ? "memory"
202
- : createHash("sha256")
203
- .update(realpathSync(options.sqlitePath))
204
- .digest("hex")
205
- .slice(0, 16);
206
- // Registrations persisted by PUT /v1/executors load first; scopes supplied through
207
- // NYLORUN_EXECUTORS_JSON then overlay them for this process only and are never written back.
208
- this.registry.seed(this.store.allExecutors().map((row) => ({
209
- agentId: row.agentId,
210
- implementationVersion: row.implementationVersion,
211
- ...(row.manifestHash === undefined
212
- ? {}
213
- : { manifestHash: row.manifestHash }),
214
- tokenHash: row.tokenHash,
215
- persisted: true,
216
- updatedAt: row.updatedAt,
217
- })));
218
- const now = new Date().toISOString();
219
- for (const executor of options.executors) {
220
- const tokenHash = hashToken(executor.token);
221
- const collision = this.registry.find(tokenHash);
222
- if (collision && collision.agentId !== executor.agentId) {
223
- this.store.db.close();
224
- if (this.lockPath)
225
- unlinkSync(this.lockPath);
226
- throw new Error("Executor tokens must be unique");
227
- }
228
- this.registry.upsert({
229
- agentId: executor.agentId,
230
- implementationVersion: executor.implementationVersion,
231
- ...(executor.manifestHash === undefined
232
- ? {}
233
- : { manifestHash: executor.manifestHash }),
234
- tokenHash,
235
- persisted: false,
236
- updatedAt: now,
237
- });
238
- }
239
- this.vault = new VaultService(this.store.db, (fn) => this.store.tx(fn), () => this.ensureKek(), options.vaultFetch ?? globalThis.fetch);
240
- this.mcp = new McpPool({
241
- dataDir: dirname(options.sqlitePath),
242
- authorize: (sessionId, request) => this.authorize(sessionId, request),
243
- });
244
- const ephemeral = options.sqlitePath === ":memory:";
245
- const sandboxRoot = options.sandbox?.root ??
246
- (ephemeral
247
- ? mkdtempSync(join(tmpdir(), "nylorun-sandboxes-"))
248
- : join(dirname(options.sqlitePath), "sandboxes"));
249
- this.sandbox = new SandboxManager({
250
- scope: ephemeral ? `m${randomUUID().slice(0, 8)}` : this.scopeId,
251
- store: this.store,
252
- backends: options.sandbox?.backends ?? defaultSandboxBackends({ root: sandboxRoot }),
253
- preference: options.sandbox?.backend ?? process.env.NYLORUN_SANDBOX,
254
- ephemeral,
255
- emit: (sessionId, turnId, type, payload) => {
256
- if (this.closed)
257
- return;
258
- const event = this.store.tx(() => this.store.event(sessionId, turnId, type, payload));
259
- this.publish(event);
260
- },
261
- });
262
- if (!ephemeral && this.sandbox.hasRecords())
263
- void this.sandbox
264
- .reconcile((id) => !this.closed && !!this.store.get("sessions", id))
265
- .catch(() => undefined);
266
- this.store.tx(() => {
267
- for (const effect of this.store.all("effects"))
268
- if (effect.status === "invoking") {
269
- effect.status = "uncertain";
270
- this.store.put("effects", effect.request.effectId, effect);
271
- const s = this.store.get("sessions", effect.request.sessionId);
272
- if (s &&
273
- s.status !== "cancelled" &&
274
- s.activeTurnId === effect.request.turnId) {
275
- s.status = "uncertain";
276
- this.store.put("sessions", s.id, s);
277
- this.store.event(s.id, s.activeTurnId, "effect.uncertain", {
278
- effectId: effect.request.effectId,
279
- });
280
- }
281
- }
282
- });
283
- this.retireLegacyHooks();
284
- this.expireClaims();
285
- this.timer = setInterval(() => this.expireClaims(), Math.min(options.leaseMs ?? 30000, 5000));
286
- this.timer.unref();
287
- for (const s of this.store.all("sessions"))
288
- if (s.status === "running" || s.status === "runnable")
289
- this.schedule(s.id);
290
- }
291
- session(id) {
292
- return (this.store.get("sessions", id) ?? fail(404, "Session not found"));
293
- }
294
- publish(event) {
295
- for (const response of this.observers.get(event.sessionId) ?? [])
296
- this.send(response, `id: ${event.cursor}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
297
- }
298
- send(response, data) {
299
- if (!response.write(data))
300
- response.destroy();
301
- }
302
- notify() {
303
- for (const streams of this.executorStreams.values())
304
- for (const response of streams)
305
- this.send(response, 'event: work_available\ndata: {"type":"work_available"}\n\n');
306
- }
307
- /**
308
- * Manifest v4 replaced beforeModelCall/afterModelCall with scoped hooks. Turns started under
309
- * an older manifest cannot continue, and their hook actions no longer parse, so end them.
310
- */
311
- retireLegacyHooks() {
312
- this.store.tx(() => {
313
- for (const action of this.store.all("actions"))
314
- if ((action.kind === "beforeModelCall" || action.kind === "afterModelCall") &&
315
- (action.status === "pending" || action.status === "claimed")) {
316
- action.status = "cancelled";
317
- this.store.put("actions", action.actionId, action);
318
- }
319
- for (const s of this.store.all("sessions"))
320
- if (s.activeTurnId && s.manifest?.manifestSchemaVersion !== 4) {
321
- this.store.event(s.id, s.activeTurnId, "turn.failed", {
322
- error: {
323
- code: "execution.incompatible",
324
- message: "This turn started under manifest schema 3; see MIGRATION.md for before/after hooks",
325
- },
326
- });
327
- s.status = "failed";
328
- s.activeTurnId = null;
329
- this.store.put("sessions", s.id, s);
330
- }
331
- });
332
- }
333
- expireClaims() {
334
- const events = [];
335
- let redeliver = false;
336
- this.store.tx(() => {
337
- for (const action of this.store.all("actions"))
338
- if (action.status === "claimed" &&
339
- Date.parse(action.leaseExpiresAt) <= Date.now()) {
340
- if (action.kind === "hook") {
341
- // Hooks are pure by contract, so a lost claim is simply delivered again.
342
- // The next claim bumps the generation, which fences any late result.
343
- action.status = "pending";
344
- action.claimId = null;
345
- action.leaseExpiresAt = null;
346
- this.store.put("actions", action.actionId, action);
347
- redeliver = true;
348
- continue;
349
- }
350
- action.status = "uncertain";
351
- this.store.put("actions", action.actionId, action);
352
- const effect = this.store.get("effects", action.actionId);
353
- effect.status = "uncertain";
354
- this.store.put("effects", action.actionId, effect);
355
- const s = this.session(action.sessionId);
356
- if (s.status !== "cancelled" && s.activeTurnId === action.turnId) {
357
- s.status = "uncertain";
358
- this.store.put("sessions", s.id, s);
359
- events.push(this.store.event(s.id, s.activeTurnId, "action.uncertain", {
360
- actionId: action.actionId,
361
- ...(action.agent ? { agent: action.agent } : {}),
362
- }));
363
- }
364
- }
365
- });
366
- events.forEach((e) => this.publish(e));
367
- if (redeliver)
368
- this.notify();
369
- }
370
- schedule(id) {
371
- if (this.closing)
372
- return;
373
- this.pending.add(id);
374
- setImmediate(() => {
375
- if (!this.running.has(id) && this.pending.delete(id) && !this.closing)
376
- void this.execute(id);
377
- });
378
- }
379
- async execute(id) {
380
- const s = this.session(id);
381
- if (!s.checkpoint || !["running", "runnable"].includes(s.status))
382
- return;
383
- s.status = "running";
384
- this.store.tx(() => this.store.put("sessions", id, s));
385
- const controller = new AbortController();
386
- this.running.set(id, controller);
387
- try {
388
- await this.prepareMcp(id, controller.signal);
389
- const current = this.session(id);
390
- const result = await runDurable({
391
- manifest: current.manifest,
392
- checkpoint: current.checkpoint,
393
- signal: controller.signal,
394
- sessionTools: sessionToolsOf(current.mcpSnapshot),
395
- host: {
396
- resolveEffect: (e) => this.resolveEffect(e, controller.signal),
397
- },
398
- });
399
- let event;
400
- this.store.tx(() => {
401
- const current = this.session(id);
402
- if (current.status === "cancelled" ||
403
- current.activeTurnId !== s.activeTurnId)
404
- return;
405
- // A result can arrive during concurrent action persistence. Preserve the runnable marker.
406
- const resumeRequested = current.status === "runnable";
407
- if (result.status === "waiting" || result.status === "uncertain") {
408
- current.status = resumeRequested
409
- ? "runnable"
410
- : current.status === "uncertain"
411
- ? "uncertain"
412
- : result.status;
413
- current.waits = { effectIds: result.effectIds };
414
- }
415
- else if ("result" in result) {
416
- current.state =
417
- result.status === "failed"
418
- ? current.turnStartState
419
- : result.result.state;
420
- current.checkpoint = result.checkpoint;
421
- this.store.put("checkpoints", JSON.stringify([id, s.activeTurnId, result.checkpoint.segment]), { checkpoint: result.checkpoint, status: result.status });
422
- current.status = result.status;
423
- current.waits =
424
- result.status === "paused"
425
- ? result.result.pending
426
- : undefined;
427
- if (result.status !== "paused")
428
- current.activeTurnId = null;
429
- event = this.store.event(id, s.activeTurnId, `turn.${result.status}`, result.status === "completed"
430
- ? { output: result.result.output }
431
- : result.status === "paused"
432
- ? {
433
- interactions: (result.result.pending ?? []).map((call) => ({
434
- invocationId: call.invocationId,
435
- interaction: call.interaction,
436
- wait: call.wait,
437
- status: call.status,
438
- })),
439
- }
440
- : result.status === "failed"
441
- ? {
442
- error: {
443
- code: result.result.error?.code,
444
- message: result.result.error?.message,
445
- },
446
- }
447
- : {});
448
- }
449
- this.store.put("sessions", id, current);
450
- });
451
- if (event)
452
- this.publish(event);
453
- }
454
- catch (error) {
455
- let event;
456
- this.store.tx(() => {
457
- const current = this.session(id);
458
- if (current.status === "cancelled" ||
459
- current.activeTurnId !== s.activeTurnId)
460
- return;
461
- current.status = "failed";
462
- current.state = current.turnStartState;
463
- if (current.checkpoint)
464
- this.store.put("checkpoints", JSON.stringify([id, s.activeTurnId, current.checkpoint.segment]), { checkpoint: current.checkpoint, status: "failed" });
465
- current.error = error instanceof Error ? error.message : String(error);
466
- this.store.put("sessions", id, current);
467
- event = this.store.event(id, current.activeTurnId, "turn.failed", {
468
- message: current.error,
469
- });
470
- current.activeTurnId = null;
471
- this.store.put("sessions", id, current);
472
- });
473
- if (event)
474
- this.publish(event);
475
- }
476
- finally {
477
- this.running.delete(id);
478
- if (!this.closing &&
479
- (this.pending.has(id) || this.session(id).status === "runnable"))
480
- this.schedule(id);
481
- }
482
- }
483
- invokeModel(request, signal) {
484
- if (!this.options.useHostModel)
485
- return (this.options.model ?? scriptedModel())(request, signal);
486
- const adapter = piModel({
487
- readHostModel: () => this.vault.readHostModel(),
488
- writeHostCredential: (credential) => this.vault.updateHostCredential(credential),
489
- });
490
- return adapter(request.input, {
491
- request: request.context.request,
492
- invocationId: String(request.context.invocationId),
493
- signal,
494
- reportPreparedCall() { },
495
- });
496
- }
497
- async resolveEffect(request, signal) {
498
- let invoke;
499
- let notify = false;
500
- let event;
501
- const resolution = this.store.tx(() => {
502
- const s = this.session(request.sessionId);
503
- if (s.status === "cancelled" ||
504
- s.activeTurnId !== request.turnId ||
505
- signal.aborted)
506
- throw new Error("Turn cancelled");
507
- const existing = this.store.get("effects", request.effectId);
508
- if (existing) {
509
- if (canonical(existing.request) !== canonical(request))
510
- throw new Error("Effect identity request drift");
511
- return existing.status === "completed"
512
- ? { status: "completed", outcome: existing.outcome }
513
- : {
514
- status: existing.status === "uncertain" ||
515
- existing.status === "invoking"
516
- ? "uncertain"
517
- : "pending",
518
- };
519
- }
520
- if (request.kind === "delegation") {
521
- // Lifecycle points of an agent used as a tool: journaled once, so replays never re-emit.
522
- const outcome = { value: null };
523
- this.store.put("effects", request.effectId, {
524
- request,
525
- status: "completed",
526
- outcome,
527
- });
528
- const settled = request.effectId.endsWith(":settled");
529
- event = this.store.event(s.id, s.activeTurnId, settled ? "delegation.completed" : "delegation.started", { agent: request.agent, ...request.input });
530
- return { status: "completed", outcome };
531
- }
532
- const agentManifest = manifestFor(s.manifest, request.agent);
533
- if (!agentManifest)
534
- throw new Error(`Agent '${request.agent?.id ?? ""}' is not used as a tool`);
535
- const mcpTool = request.kind === "tool" ? mcpToolOf(s, request) : undefined;
536
- const sandboxTool = request.kind === "tool" && !mcpTool
537
- ? sandboxCapabilityOf(agentManifest, request.capabilityId, request.toolName)
538
- : undefined;
539
- this.store.put("effects", request.effectId, {
540
- request,
541
- status: request.kind === "model" || mcpTool || sandboxTool
542
- ? "invoking"
543
- : "pending",
544
- });
545
- if (request.kind === "model" || mcpTool || sandboxTool) {
546
- invoke = mcpTool ? "mcp" : sandboxTool ? "sandbox" : "model";
547
- return undefined;
548
- }
549
- const tool = request.kind === "tool"
550
- ? pinnedTool(agentManifest, request.capabilityId, request.toolName)
551
- : undefined;
552
- const base = {
553
- actionId: request.effectId,
554
- sessionId: request.sessionId,
555
- turnId: request.turnId,
556
- agentId: request.agentId,
557
- manifestHash: request.manifestHash,
558
- implementationVersion: s.implementationVersion,
559
- input: request.input,
560
- context: request.context,
561
- status: "pending",
562
- generation: 0,
563
- claimId: null,
564
- leaseExpiresAt: null,
565
- ...(request.agent ? { agent: request.agent } : {}),
566
- };
567
- const action = request.kind === "hook"
568
- ? {
569
- ...base,
570
- kind: "hook",
571
- hook: {
572
- at: request.hook.at,
573
- scope: request.hook.scope,
574
- capabilityIds: [...request.hook.capabilityIds],
575
- },
576
- }
577
- : {
578
- ...base,
579
- kind: "tool",
580
- capabilityId: request.capabilityId,
581
- toolName: request.toolName,
582
- ...(tool?.inputSchema ? { inputSchema: tool.inputSchema } : {}),
583
- ...(tool?.outputSchema ? { outputSchema: tool.outputSchema } : {}),
584
- };
585
- this.store.put("actions", action.actionId, action);
586
- event = this.store.event(s.id, s.activeTurnId, "action.pending", {
587
- actionId: action.actionId,
588
- kind: action.kind,
589
- ...actionTarget(action),
590
- input: action.input,
591
- });
592
- notify = true;
593
- return { status: "pending" };
594
- });
595
- if (event)
596
- this.publish(event);
597
- if (notify)
598
- this.notify();
599
- if (!invoke)
600
- return resolution;
601
- try {
602
- const value = invoke === "mcp"
603
- ? await this.callMcpTool(request)
604
- : invoke === "sandbox"
605
- ? await this.callSandboxTool(request, signal)
606
- : await this.invokeModel(request, signal);
607
- return this.store.tx(() => {
608
- const s = this.session(request.sessionId);
609
- if (s.status === "cancelled" ||
610
- s.activeTurnId !== request.turnId ||
611
- signal.aborted)
612
- throw new Error("Turn cancelled");
613
- const effect = this.store.get("effects", request.effectId);
614
- effect.status = "completed";
615
- effect.outcome = { value };
616
- this.store.put("effects", request.effectId, effect);
617
- return { status: "completed", outcome: effect.outcome };
618
- });
619
- }
620
- catch (error) {
621
- let event;
622
- this.store.tx(() => {
623
- const effect = this.store.get("effects", request.effectId);
624
- effect.status = "uncertain";
625
- effect.error = error instanceof Error ? error.message : String(error);
626
- this.store.put("effects", request.effectId, effect);
627
- const s = this.session(request.sessionId);
628
- if (s.status !== "cancelled" && s.activeTurnId === request.turnId)
629
- event = this.store.event(s.id, request.turnId, "effect.uncertain", {
630
- effectId: request.effectId,
631
- message: effect.error,
632
- });
633
- });
634
- if (event)
635
- this.publish(event);
636
- return { status: "uncertain" };
637
- }
638
- }
639
- scope(request) {
640
- const header = request.headers.authorization;
641
- const token = header?.startsWith("Bearer ") ? header.slice(7) : "";
642
- if (!token)
643
- return fail(401, "Invalid credentials");
644
- if (equals(token, this.options.serverToken))
645
- return "server";
646
- // The map is keyed by a digest of the presented token, so the lookup reveals nothing the
647
- // caller does not already hold; the server token keeps its constant-time comparison.
648
- const executor = this.registry.find(hashToken(token));
649
- if (!executor)
650
- return fail(401, "Invalid credentials");
651
- return executor;
652
- }
653
- scoped(scope, action) {
654
- if (scope === "server" || scope.agentId !== action.agentId)
655
- fail(403, "Executor scope does not authorize this action");
656
- }
657
- async body(request) {
658
- let data = "";
659
- for await (const chunk of request) {
660
- data += chunk;
661
- if (Buffer.byteLength(data) > 1024 * 1024)
662
- fail(413, "Request too large");
663
- }
664
- try {
665
- return JSON.parse(data);
666
- }
667
- catch {
668
- return fail(400, "Invalid JSON");
669
- }
670
- }
671
- command(id, input, scope) {
672
- let event;
673
- let schedule = false;
674
- const response = this.store.tx(() => {
675
- const s = this.session(id);
676
- let command = input;
677
- if (command.type === "action_result") {
678
- const a = this.store.get("actions", command.actionId) ??
679
- fail(404, "Action not found");
680
- if (a.sessionId !== id)
681
- fail(403, "Action belongs to another session");
682
- this.scoped(scope, a);
683
- command = acceptedToolResult(a, command);
684
- }
685
- else if (scope !== "server")
686
- fail(403, "Application credential required");
687
- const key = JSON.stringify([id, command.idempotencyKey]);
688
- const existing = this.store.get("commands", key);
689
- if (existing) {
690
- if (semantic(existing.command) !== semantic(command))
691
- fail(409, "Idempotency key already binds another command");
692
- return existing.response;
693
- }
694
- if (command.type === "action_result") {
695
- const action = this.store.get("actions", command.actionId);
696
- const prior = this.store.get("effects", command.actionId);
697
- if (action.status === "completed") {
698
- if (action.claimId !== command.claimId ||
699
- action.generation !== command.generation ||
700
- canonical(prior.outcome) !== canonical(command.outcome))
701
- fail(409, "Conflicting action result");
702
- this.store.put("commands", key, { command, response: prior.receipt });
703
- return prior.receipt;
704
- }
705
- if (s.status === "cancelled" ||
706
- s.activeTurnId !== action.turnId ||
707
- action.status !== "claimed" ||
708
- action.claimId !== command.claimId ||
709
- action.generation !== command.generation ||
710
- Date.parse(action.leaseExpiresAt) <= Date.now())
711
- fail(409, "Stale, expired, or cancelled claim");
712
- action.status = "completed";
713
- this.store.put("actions", action.actionId, action);
714
- prior.status = "completed";
715
- prior.outcome = command.outcome;
716
- s.status = "runnable";
717
- schedule = true;
718
- event = this.store.event(id, s.activeTurnId, "action.completed", {
719
- actionId: action.actionId,
720
- ...actionTarget(action),
721
- kind: action.kind,
722
- result: command.outcome.value,
723
- });
724
- const receipt = {
725
- status: "accepted",
726
- turnId: s.activeTurnId,
727
- cursor: event.cursor,
728
- requestId: command.requestId,
729
- };
730
- prior.receipt = receipt;
731
- this.store.put("effects", action.actionId, prior);
732
- }
733
- else if (command.type === "cancel") {
734
- const cancelledTurnId = s.activeTurnId;
735
- s.status = "cancelled";
736
- for (const a of this.store.all("actions"))
737
- if (a.sessionId === id &&
738
- a.turnId === cancelledTurnId &&
739
- ["pending", "claimed"].includes(a.status)) {
740
- // Claimed work may already have an external effect. Preserve it for reconciliation.
741
- a.status = a.status === "claimed" ? "uncertain" : "cancelled";
742
- this.store.put("actions", a.actionId, a);
743
- const effect = this.store.get("effects", a.actionId);
744
- effect.status = a.status;
745
- this.store.put("effects", a.actionId, effect);
746
- }
747
- for (const effect of this.store.all("effects"))
748
- if (effect.request.sessionId === id &&
749
- effect.request.turnId === cancelledTurnId &&
750
- effect.status === "invoking") {
751
- effect.status = "uncertain";
752
- effect.error =
753
- "Turn cancelled before model outcome was durably recorded";
754
- this.store.put("effects", effect.request.effectId, effect);
755
- }
756
- event = this.store.event(id, cancelledTurnId, "turn.cancelled", {
757
- reason: command.reason,
758
- });
759
- s.activeTurnId = null;
760
- // The next turn starts from the state preceding the cancelled turn, never its paused plan.
761
- if (cancelledTurnId !== null)
762
- s.state = s.turnStartState;
763
- s.checkpoint = undefined;
764
- s.waits = undefined;
765
- s.error = undefined;
766
- }
767
- else {
768
- if (command.type === "message") {
769
- if (!["idle", "completed", "failed", "cancelled"].includes(s.status))
770
- fail(409, "Session has active or unresolved work");
771
- s.turnStartState = s.state;
772
- s.activeTurnId = randomUUID();
773
- s.checkpoint = createDurableCheckpoint({
774
- manifest: s.manifest,
775
- sessionId: id,
776
- turnId: s.activeTurnId,
777
- input: command.content,
778
- state: s.state,
779
- info: s.info,
780
- });
781
- }
782
- else {
783
- if (s.status !== "paused" || !s.state || !s.activeTurnId)
784
- fail(409, "Session is not awaiting a response");
785
- const pending = s.state.plan?.calls ?? [];
786
- if (!pending.some((c) => c.status === "interaction" &&
787
- c.interaction?.id === command.interactionId &&
788
- (command.type === "approve") ===
789
- (c.interaction?.kind === "approval")))
790
- fail(409, "Unknown interaction");
791
- const input = command.type === "approve"
792
- ? {
793
- kind: "approve",
794
- interactionId: command.interactionId,
795
- approved: command.approved,
796
- }
797
- : {
798
- kind: "respond",
799
- interactionId: command.interactionId,
800
- value: command.value,
801
- };
802
- s.checkpoint = createDurableCheckpoint({
803
- manifest: s.manifest,
804
- sessionId: id,
805
- turnId: s.activeTurnId,
806
- input: input,
807
- state: s.state,
808
- info: s.info,
809
- segment: (s.checkpoint?.segment ?? 0) + 1,
810
- });
811
- }
812
- this.store.put("checkpoints", JSON.stringify([id, s.activeTurnId, s.checkpoint.segment]), { checkpoint: s.checkpoint, status: "runnable" });
813
- s.status = "runnable";
814
- s.waits = undefined;
815
- schedule = true;
816
- event = this.store.event(id, s.activeTurnId, `command.${command.type}`, command);
817
- }
818
- this.store.put("sessions", id, s);
819
- const response = {
820
- status: "accepted",
821
- turnId: s.activeTurnId,
822
- cursor: event?.cursor ?? this.store.history(id).cursor,
823
- requestId: command.requestId,
824
- };
825
- this.store.put("commands", key, { command, response });
826
- return response;
827
- });
828
- if (event)
829
- this.publish(event);
830
- if (input.type === "cancel")
831
- this.running.get(id)?.abort();
832
- if (schedule)
833
- this.schedule(id);
834
- return response;
835
- }
836
- sse(request, response, set, whenEmpty) {
837
- response.writeHead(200, {
838
- "content-type": "text/event-stream",
839
- "cache-control": "no-cache",
840
- connection: "keep-alive",
841
- "x-accel-buffering": "no",
842
- });
843
- response.flushHeaders();
844
- set.add(response);
845
- const timer = setInterval(() => this.send(response, ": keepalive\n\n"), 15000);
846
- timer.unref();
847
- request.on("close", () => {
848
- clearInterval(timer);
849
- set.delete(response);
850
- if (!set.size)
851
- whenEmpty?.();
852
- });
853
- }
854
- async handle(request, response) {
855
- const json = (value, status = 200) => {
856
- response.writeHead(status, { "content-type": "application/json" });
857
- response.end(JSON.stringify(value));
858
- };
859
- try {
860
- const url = new URL(request.url ?? "/", "http://runtime");
861
- const path = url.pathname
862
- .split("/")
863
- .filter(Boolean)
864
- .map(decodeURIComponent);
865
- const method = request.method;
866
- if (url.pathname === "/health")
867
- return json({
868
- status: "ok",
869
- service: "oss-runtime",
870
- version: RUNTIME_VERSION,
871
- scopeId: this.scopeId,
872
- pid: process.pid,
873
- });
874
- if (url.pathname === "/ready") {
875
- this.store.db.prepare("SELECT 1").get();
876
- return json({
877
- status: this.closing ? "not_ready" : "ready",
878
- service: "oss-runtime",
879
- checks: {
880
- sqlite: true,
881
- scheduler: !this.closing,
882
- modelConfigured: true,
883
- },
884
- }, this.closing ? 503 : 200);
885
- }
886
- const scope = this.scope(request);
887
- if (path[0] !== "v1")
888
- fail(404, "Route not found");
889
- if (path[1] === "executors" &&
890
- path[2] === "connect" &&
891
- method === "GET") {
892
- if (scope === "server")
893
- fail(403, "Executor credential required");
894
- const tokenHash = scope.tokenHash;
895
- let streams = this.executorStreams.get(tokenHash);
896
- if (!streams)
897
- this.executorStreams.set(tokenHash, (streams = new Set()));
898
- this.sse(request, response, streams, () => this.executorStreams.delete(tokenHash));
899
- this.send(response, 'event: work_available\ndata: {"type":"work_available"}\n\n');
900
- return;
901
- }
902
- if (path[1] === "actions") {
903
- if (scope === "server")
904
- return fail(403, "Executor credential required");
905
- this.expireClaims();
906
- if (path.length === 2 && method === "GET")
907
- return json({
908
- actions: this.store
909
- .all("actions")
910
- .filter((a) => a.status === "pending" && a.agentId === scope.agentId),
911
- });
912
- const actionId = path[2];
913
- if (!actionId)
914
- fail(404, "Action not found");
915
- const body = await this.body(request);
916
- let event;
917
- const result = this.store.tx(() => {
918
- const action = this.store.get("actions", actionId) ??
919
- fail(404, "Action not found");
920
- this.scoped(scope, action);
921
- if (method === "POST" && path[3] === "claim") {
922
- ActionClaimRequestSchema.parse(body);
923
- if (action.status !== "pending" ||
924
- this.session(action.sessionId).status === "cancelled" ||
925
- this.session(action.sessionId).activeTurnId !== action.turnId)
926
- fail(409, "Action unavailable");
927
- action.status = "claimed";
928
- action.generation++;
929
- action.claimId = randomUUID();
930
- action.leaseExpiresAt = new Date(Date.now() + (this.options.leaseMs ?? 30000)).toISOString();
931
- this.store.put("actions", actionId, action);
932
- event = this.store.event(action.sessionId, action.turnId, "action.claimed", {
933
- actionId,
934
- generation: action.generation,
935
- ...(action.agent ? { agent: action.agent } : {}),
936
- });
937
- return {
938
- action,
939
- claimId: action.claimId,
940
- generation: action.generation,
941
- leaseExpiresAt: action.leaseExpiresAt,
942
- };
943
- }
944
- if (method === "POST" && path[3] === "heartbeat") {
945
- const beat = ActionHeartbeatRequestSchema.parse(body);
946
- if (this.session(action.sessionId).activeTurnId !== action.turnId ||
947
- action.status !== "claimed" ||
948
- action.claimId !== beat.claimId ||
949
- action.generation !== beat.generation ||
950
- Date.parse(action.leaseExpiresAt) <= Date.now())
951
- fail(409, "Stale or expired claim");
952
- action.leaseExpiresAt = new Date(Date.now() + (this.options.leaseMs ?? 30000)).toISOString();
953
- this.store.put("actions", actionId, action);
954
- return { leaseExpiresAt: action.leaseExpiresAt };
955
- }
956
- return fail(404, "Route not found");
957
- });
958
- if (event)
959
- this.publish(event);
960
- return json(result);
961
- }
962
- if (path[1] === "sessions" &&
963
- path[2] &&
964
- path[3] === "commands" &&
965
- method === "POST")
966
- return json(this.command(path[2], SessionCommandSchema.parse(await this.body(request)), scope));
967
- if (path[1] === "vaults")
968
- return json(await this.dispatchVault(scope, method, path, url, request));
969
- if (path[1] === "host")
970
- return json(await this.dispatchHost(scope, method, path, request));
971
- if (scope !== "server")
972
- fail(403, "Application credential required");
973
- if (path[1] === "executors" && path.length === 2 && method === "GET")
974
- return json({
975
- executors: this.registry.list().map((e) => ({
976
- agentId: e.agentId,
977
- implementationVersion: e.implementationVersion,
978
- ...(e.manifestHash === undefined
979
- ? {}
980
- : { manifestHash: e.manifestHash }),
981
- connected: (this.executorStreams.get(e.tokenHash)?.size ?? 0) > 0,
982
- updatedAt: e.updatedAt,
983
- })),
984
- });
985
- // The length guard matters: the connect branch above only matches GET, so without it a
986
- // PUT to /v1/executors/connect would register an agent literally named "connect".
987
- if (path[1] === "executors" && path.length === 2 && method === "PUT") {
988
- const body = RegisterExecutorsRequestSchema.parse(await this.body(request));
989
- // Credential rules are shared with startup validation, which throws plainly; on the
990
- // wire a rejected registration is a bad request, not a server fault.
991
- try {
992
- for (const executor of body.executors)
993
- assertExecutorCredential(executor, this.options.serverToken);
994
- }
995
- catch (error) {
996
- fail(400, error instanceof Error ? error.message : String(error));
997
- }
998
- if (new Set(body.executors.map((e) => e.agentId)).size !==
999
- body.executors.length ||
1000
- new Set(body.executors.map((e) => e.token)).size !==
1001
- body.executors.length)
1002
- fail(400, "Executor registrations must be unique per agent and token");
1003
- const updatedAt = new Date().toISOString();
1004
- const records = body.executors.map((executor) => ({
1005
- agentId: executor.agentId,
1006
- implementationVersion: executor.implementationVersion,
1007
- ...(executor.manifestHash === undefined
1008
- ? {}
1009
- : { manifestHash: executor.manifestHash }),
1010
- tokenHash: hashToken(executor.token),
1011
- persisted: true,
1012
- updatedAt,
1013
- }));
1014
- for (const record of records) {
1015
- const collision = this.registry.find(record.tokenHash);
1016
- if (collision && collision.agentId !== record.agentId)
1017
- fail(409, "Executor tokens must be unique");
1018
- }
1019
- // Persist the whole batch first; the in-memory registry must never run ahead of SQLite.
1020
- this.store.tx(() => {
1021
- for (const record of records)
1022
- this.store.putExecutor({
1023
- agentId: record.agentId,
1024
- tokenHash: record.tokenHash,
1025
- implementationVersion: record.implementationVersion,
1026
- ...(record.manifestHash === undefined
1027
- ? {}
1028
- : { manifestHash: record.manifestHash }),
1029
- updatedAt,
1030
- });
1031
- });
1032
- const results = records.map((record) => {
1033
- const { rotated, previousHash } = this.registry.upsert(record);
1034
- if (rotated && previousHash) {
1035
- for (const r of this.executorStreams.get(previousHash) ?? [])
1036
- r.end();
1037
- this.executorStreams.delete(previousHash);
1038
- }
1039
- return {
1040
- agentId: record.agentId,
1041
- implementationVersion: record.implementationVersion,
1042
- rotated,
1043
- };
1044
- });
1045
- return json({ executors: results });
1046
- }
1047
- if (path[1] === "executors" &&
1048
- path.length === 3 &&
1049
- path[2] &&
1050
- method === "DELETE") {
1051
- const agentId = path[2];
1052
- const existing = this.registry.get(agentId) ?? fail(404, "Executor not found");
1053
- if (!existing.persisted)
1054
- fail(409, "Executor is provided by NYLORUN_EXECUTORS_JSON and cannot be deleted");
1055
- this.store.tx(() => this.store.deleteExecutor(agentId));
1056
- this.registry.remove(agentId);
1057
- for (const r of this.executorStreams.get(existing.tokenHash) ?? [])
1058
- r.end();
1059
- this.executorStreams.delete(existing.tokenHash);
1060
- return json({ agentId, deleted: true });
1061
- }
1062
- if (path[1] === "agents" && path.length === 2 && method === "GET")
1063
- return json({
1064
- agents: this.store.all("definitions").map((d) => ({
1065
- agentId: d.manifest.id,
1066
- manifest: d.manifest,
1067
- manifestHash: d.manifestHash,
1068
- implementationVersion: d.implementationVersion,
1069
- })),
1070
- });
1071
- if (path[1] === "sessions" && path.length === 2 && method === "GET")
1072
- return json({
1073
- sessions: this.store
1074
- .all("sessions")
1075
- .filter((s) => !url.searchParams.has("agentId") ||
1076
- s.agentId === url.searchParams.get("agentId"))
1077
- .map((s) => ({
1078
- id: s.id,
1079
- agentId: s.agentId,
1080
- ownerUserId: s.ownerUserId,
1081
- status: s.status,
1082
- activeTurnId: s.activeTurnId,
1083
- })),
1084
- });
1085
- if (path[1] === "agents" &&
1086
- path[2] &&
1087
- path.length === 3 &&
1088
- method === "PUT") {
1089
- const body = PutAgentRequestSchema.parse(await this.body(request));
1090
- if (body.manifest.id !== path[2])
1091
- fail(400, "Agent id mismatch");
1092
- AgentManifestSchema.parse(body.manifest);
1093
- const definition = {
1094
- ...body,
1095
- manifestHash: hashManifest(body.manifest),
1096
- };
1097
- this.store.tx(() => {
1098
- this.store.put("definitions", path[2], definition);
1099
- });
1100
- return json({
1101
- agentId: path[2],
1102
- manifestHash: definition.manifestHash,
1103
- implementationVersion: body.implementationVersion,
1104
- });
1105
- }
1106
- if (path[1] === "sessions" && path[2]) {
1107
- const id = path[2];
1108
- if (method === "PUT" && path.length === 3) {
1109
- const body = PutSessionRequestSchema.parse(await this.body(request));
1110
- const vaultIds = body.vaultIds ?? [];
1111
- const credentialSelections = body.credentialSelections ?? [];
1112
- const result = this.store.tx(() => {
1113
- this.vault.assertAttachment(body.ownerUserId, vaultIds, credentialSelections);
1114
- const prior = this.store.get("sessions", id);
1115
- if (prior) {
1116
- if (sessionIdentity(prior.creation) !== sessionIdentity(body))
1117
- fail(409, "Session already exists with different creation parameters");
1118
- prior.vaultIds = vaultIds;
1119
- prior.credentialSelections = credentialSelections;
1120
- prior.creation = body;
1121
- this.store.put("sessions", id, prior);
1122
- this.vault.recordAttachment(id, vaultIds);
1123
- return prior;
1124
- }
1125
- const definition = this.store.get("definitions", body.agentId) ??
1126
- fail(404, "Definition not found");
1127
- const s = {
1128
- id,
1129
- agentId: body.agentId,
1130
- ownerUserId: body.ownerUserId,
1131
- manifest: definition.manifest,
1132
- manifestHash: definition.manifestHash,
1133
- implementationVersion: definition.implementationVersion,
1134
- info: body.info,
1135
- status: "idle",
1136
- activeTurnId: null,
1137
- creation: body,
1138
- vaultIds,
1139
- credentialSelections,
1140
- pluginRoots: definition.pluginRoots ?? {},
1141
- };
1142
- this.store.put("sessions", id, s);
1143
- this.vault.recordAttachment(id, vaultIds);
1144
- return s;
1145
- });
1146
- return json(this.view(result));
1147
- }
1148
- const s = this.session(id);
1149
- if (method === "GET" && path.length === 3)
1150
- return json(this.view(s));
1151
- const cursor = url.searchParams.get("cursor") ??
1152
- (typeof request.headers["last-event-id"] === "string"
1153
- ? request.headers["last-event-id"]
1154
- : undefined);
1155
- if (method === "GET" && path[3] === "items")
1156
- return json(this.store.history(id, cursor, url.searchParams.get("agent") ?? undefined));
1157
- if (method === "GET" && path[3] === "events") {
1158
- const history = this.store.history(id, cursor);
1159
- const set = this.observers.get(id) ?? new Set();
1160
- this.observers.set(id, set);
1161
- this.sse(request, response, set);
1162
- for (const event of history.items)
1163
- this.send(response, `id: ${event.cursor}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
1164
- return;
1165
- }
1166
- }
1167
- fail(404, "Route not found");
1168
- }
1169
- catch (error) {
1170
- if (response.headersSent) {
1171
- response.end();
1172
- return;
1173
- }
1174
- const status = error instanceof HttpError || error instanceof VaultError
1175
- ? error.status
1176
- : error?.name === "ZodError" ||
1177
- error?.message === "Invalid cursor"
1178
- ? 400
1179
- : 500;
1180
- json({
1181
- status: "rejected",
1182
- code: status === 500 ? "internal_error" : "request_rejected",
1183
- message: status === 500
1184
- ? "Runtime request failed"
1185
- : error.message,
1186
- }, status);
1187
- }
1188
- }
1189
- view(s) {
1190
- return {
1191
- id: s.id,
1192
- agentId: s.agentId,
1193
- ownerUserId: s.ownerUserId,
1194
- manifestHash: s.manifestHash,
1195
- implementationVersion: s.implementationVersion,
1196
- status: s.status,
1197
- activeTurnId: s.activeTurnId,
1198
- vaultIds: s.vaultIds ?? [],
1199
- credentialSelections: s.credentialSelections ?? [],
1200
- mcpSnapshot: s.mcpSnapshot ?? null,
1201
- mcpDiagnostics: s.mcpDiagnostics ?? [],
1202
- waits: Array.isArray(s.waits)
1203
- ? s.waits.map((call) => ({
1204
- invocationId: call.invocationId,
1205
- interaction: call.interaction,
1206
- wait: call.wait,
1207
- status: call.status,
1208
- }))
1209
- : s.waits,
1210
- error: s.error,
1211
- actions: this.store
1212
- .all("actions")
1213
- .filter((a) => a.sessionId === s.id &&
1214
- !["completed", "cancelled"].includes(a.status)),
1215
- uncertainEffects: this.store
1216
- .all("effects")
1217
- .filter((e) => e.request.sessionId === s.id && e.status === "uncertain")
1218
- .map((e) => ({
1219
- effectId: e.request.effectId,
1220
- turnId: e.request.turnId,
1221
- kind: e.request.kind,
1222
- error: e.error,
1223
- })),
1224
- };
1225
- }
1226
- async prepareMcp(id, signal) {
1227
- const s = this.session(id);
1228
- if (serversOf(s.manifest).length === 0)
1229
- return;
1230
- if (!s.mcpSnapshot) {
1231
- const found = await this.mcp.discover({
1232
- sessionId: id,
1233
- manifest: s.manifest,
1234
- manifestHash: s.manifestHash,
1235
- pluginRoots: s.pluginRoots ?? {},
1236
- signal,
1237
- });
1238
- this.store.tx(() => {
1239
- const current = this.session(id);
1240
- if (current.mcpSnapshot)
1241
- return;
1242
- current.mcpSnapshot = found.snapshot;
1243
- current.mcpDiagnostics = found.diagnostics;
1244
- this.store.put("sessions", id, current);
1245
- });
1246
- return;
1247
- }
1248
- const diagnostics = await this.mcp.reconnect({
1249
- sessionId: id,
1250
- manifest: s.manifest,
1251
- pluginRoots: s.pluginRoots ?? {},
1252
- tools: s.mcpSnapshot.mcpTools,
1253
- signal,
1254
- });
1255
- if (diagnostics.length === 0)
1256
- return;
1257
- this.store.tx(() => {
1258
- const current = this.session(id);
1259
- const prior = [...(current.mcpDiagnostics ?? [])];
1260
- for (const item of diagnostics) {
1261
- const index = prior.findIndex((existing) => existing.capabilityId === item.capabilityId &&
1262
- existing.serverName === item.serverName);
1263
- if (index >= 0)
1264
- prior[index] = item;
1265
- else
1266
- prior.push(item);
1267
- }
1268
- current.mcpDiagnostics = prior;
1269
- this.store.put("sessions", id, current);
1270
- });
1271
- }
1272
- async callMcpTool(request) {
1273
- const s = this.session(request.sessionId);
1274
- const tool = mcpToolOf(s, request);
1275
- if (!tool)
1276
- throw new Error(`MCP tool '${request.toolName ?? ""}' is not in the session snapshot`);
1277
- return this.mcp.call({
1278
- sessionId: s.id,
1279
- ...(tool.agentId === undefined ? {} : { agentId: tool.agentId }),
1280
- capabilityId: tool.capabilityId,
1281
- serverName: tool.serverName,
1282
- serverToolName: tool.serverToolName,
1283
- args: request.input,
1284
- manifest: s.manifest,
1285
- pluginRoots: s.pluginRoots ?? {},
1286
- });
1287
- }
1288
- async callSandboxTool(request, signal) {
1289
- const s = this.session(request.sessionId);
1290
- // Agents used as tools share the session's sandbox; the tree declares one sandbox spec.
1291
- const capability = sandboxCapabilityOf(manifestFor(s.manifest, request.agent), request.capabilityId, request.toolName);
1292
- if (!capability)
1293
- throw new Error(`'${request.toolName ?? ""}' is not a sandbox tool`);
1294
- return this.sandbox.run({ id: s.id, activeTurnId: s.activeTurnId, manifest: s.manifest }, capability, request.toolName, request.input, signal);
1295
- }
1296
- async authorize(sessionId, request) {
1297
- const s = this.session(sessionId);
1298
- const result = await this.vault.authorize({
1299
- sessionId,
1300
- vaultIds: s.vaultIds ?? [],
1301
- credentialSelections: s.credentialSelections ?? [],
1302
- url: request.url,
1303
- serverName: request.serverName,
1304
- });
1305
- if (result.status === "authorized") {
1306
- const token = result.headers.authorization.slice("Bearer ".length);
1307
- scrub({ authorization: result.headers.authorization, url: result.url }, [
1308
- token,
1309
- ]);
1310
- }
1311
- return result;
1312
- }
1313
- ensureKek() {
1314
- if (this.kek)
1315
- return this.kek;
1316
- if (this.options.vaultKek === null)
1317
- throw new Error("Vault key-encryption key is required");
1318
- this.kek = createKekFile(this.kekPath);
1319
- return this.kek;
1320
- }
1321
- async dispatchHost(scope, method, path, request) {
1322
- if (scope !== "server") {
1323
- this.vault.reject(path.join("/"));
1324
- fail(403, "Application credential required");
1325
- }
1326
- if (path[2] === "models" && path.length === 3 && method === "GET")
1327
- return hostModelCatalog();
1328
- if (path[2] === "sandbox" && path.length === 3 && method === "GET")
1329
- return this.sandbox.report();
1330
- if (path[2] === "providers" && path.length === 3 && method === "GET")
1331
- return this.vault.listHostProviders();
1332
- if (path[2] === "model" && path.length === 3 && method === "GET")
1333
- return this.vault.getHostModel();
1334
- if (path[2] === "model" && path.length === 3 && method === "PUT")
1335
- return this.vault.putHostModel(PutHostModelRequestSchema.parse(await this.body(request)));
1336
- if (path[2] === "model" &&
1337
- path[3] === "selection" &&
1338
- path.length === 4 &&
1339
- method === "PUT")
1340
- return this.vault.selectHostModel(SelectHostModelRequestSchema.parse(await this.body(request)));
1341
- fail(404, "Route not found");
1342
- }
1343
- async dispatchVault(scope, method, path, url, request) {
1344
- if (scope !== "server") {
1345
- this.vault.reject(path.join("/"));
1346
- fail(403, "Application credential required");
1347
- }
1348
- if (path.length === 2 && method === "POST") {
1349
- const body = CreateVaultRequestSchema.parse(await this.body(request));
1350
- return this.vault.createVault(body);
1351
- }
1352
- if (path.length === 2 && method === "GET") {
1353
- const ownerUserId = url.searchParams.get("ownerUserId") ??
1354
- fail(400, "ownerUserId is required");
1355
- return { vaults: this.vault.listVaults(ownerUserId) };
1356
- }
1357
- const vaultId = path[2];
1358
- if (!vaultId)
1359
- fail(404, "Vault not found");
1360
- if (path.length === 3 && method === "GET")
1361
- return this.vault.getVault(vaultId);
1362
- if (path.length === 3 && method === "DELETE")
1363
- return this.vault.deleteVault(vaultId);
1364
- if (path[3] !== "credentials")
1365
- fail(404, "Route not found");
1366
- if (path.length === 4 && method === "POST") {
1367
- const body = CreateCredentialRequestSchema.parse(await this.body(request));
1368
- return this.vault.createCredential(vaultId, body);
1369
- }
1370
- if (path.length === 4 && method === "GET")
1371
- return { credentials: this.vault.listCredentials(vaultId) };
1372
- const credentialId = path[4];
1373
- if (!credentialId)
1374
- fail(404, "Credential not found");
1375
- if (path.length === 5 && method === "GET")
1376
- return this.vault.getCredential(vaultId, credentialId);
1377
- if (path.length === 5 && method === "POST") {
1378
- const body = RotateCredentialRequestSchema.parse(await this.body(request));
1379
- return this.vault.rotateCredential(vaultId, credentialId, body);
1380
- }
1381
- if (path.length === 5 && method === "DELETE")
1382
- return this.vault.deleteCredential(vaultId, credentialId);
1383
- fail(404, "Route not found");
1384
- }
1385
- async listen(port = 8787, hostname = "127.0.0.1") {
1386
- if (this.server)
1387
- throw new Error("Already listening");
1388
- this.server = createServer((q, s) => {
1389
- void this.handle(q, s);
1390
- });
1391
- await new Promise((resolve, reject) => {
1392
- this.server.once("error", reject);
1393
- this.server.listen(port, hostname, resolve);
1394
- });
1395
- const address = this.server.address();
1396
- return {
1397
- url: `http://${hostname}:${typeof address === "object" && address ? address.port : port}`,
1398
- };
1399
- }
1400
- async close() {
1401
- this.closing = true;
1402
- clearInterval(this.timer);
1403
- for (const c of this.running.values())
1404
- c.abort();
1405
- await this.mcp.close();
1406
- for (const streams of this.executorStreams.values())
1407
- for (const r of streams)
1408
- r.end();
1409
- for (const set of this.observers.values())
1410
- for (const r of set)
1411
- r.end();
1412
- if (this.server)
1413
- await new Promise((resolve) => {
1414
- this.server.close(() => resolve());
1415
- this.server.closeIdleConnections();
1416
- });
1417
- // Providers must honor AbortSignal. Keep storage open until any in-flight journal writes have drained.
1418
- while (this.running.size)
1419
- await new Promise((resolve) => setTimeout(resolve, 10));
1420
- await this.sandbox.close();
1421
- this.closed = true;
1422
- this.store.db.close();
1423
- if (this.lockPath)
1424
- unlinkSync(this.lockPath);
1425
- }
1426
- }
1427
- export function createRuntime(options) {
1428
- return new CoreRuntime(options);
1429
- }
1430
- export async function startRuntime(options) {
1431
- const runtime = createRuntime(options);
1432
- try {
1433
- const { url } = await runtime.listen(options.port, options.hostname);
1434
- return Object.assign(runtime, { url });
1435
- }
1436
- catch (error) {
1437
- await runtime.close();
1438
- throw error;
1439
- }
1440
- }
1441
- /** What an action runs, for events: a tool name, or a hook point and its capabilities. */
1442
- function actionTarget(action) {
1443
- return {
1444
- ...(action.kind === "hook" ? { hook: action.hook } : { toolName: action.toolName }),
1445
- ...(action.agent ? { agent: action.agent } : {}),
1446
- };
1447
- }
1
+ export { openTenantRuntime, TenantRuntime, } from "../tenant/runtime.js";
2
+ export { startEphemeralRuntime, } from "../tenant/ephemeral.js";
3
+ export { scriptedModel, gatewayModel } from "./provider.js";