@nylorun/runtime 0.6.0-beta → 0.8.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +9 -5
- package/dist/adapters/sandbox/microsandbox.d.ts +2 -0
- package/dist/adapters/sandbox/microsandbox.js +238 -0
- package/dist/adapters/sandbox/virtual.d.ts +4 -0
- package/dist/adapters/sandbox/virtual.js +125 -0
- package/dist/configuration.d.ts +1 -1
- package/dist/configuration.js +1 -1
- package/dist/core/executors.d.ts +32 -0
- package/dist/core/executors.js +60 -0
- package/dist/core/main.js +7 -12
- package/dist/core/runtime.d.ts +41 -1
- package/dist/core/runtime.js +594 -53
- package/dist/core/store.d.ts +13 -0
- package/dist/core/store.js +95 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/mcp/connect.d.ts +31 -0
- package/dist/mcp/connect.js +190 -0
- package/dist/mcp/pool.d.ts +45 -0
- package/dist/mcp/pool.js +143 -0
- package/dist/mcp/snapshot.d.ts +25 -0
- package/dist/mcp/snapshot.js +10 -0
- package/dist/mcp/stdio.d.ts +15 -0
- package/dist/mcp/stdio.js +80 -0
- package/dist/model/catalog.d.ts +12 -0
- package/dist/model/catalog.js +29 -0
- package/dist/model/models.d.ts +3 -1
- package/dist/model/models.js +4 -2
- package/dist/model/pi-model.d.ts +9 -3
- package/dist/model/pi-model.js +47 -11
- package/dist/model/settings.d.ts +0 -2
- package/dist/model/settings.js +0 -47
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/sandbox/manager.d.ts +61 -0
- package/dist/sandbox/manager.js +239 -0
- package/dist/sandbox/policy.d.ts +13 -0
- package/dist/sandbox/policy.js +67 -0
- package/dist/sandbox/select.d.ts +35 -0
- package/dist/sandbox/select.js +76 -0
- package/dist/sandbox/tools.d.ts +25 -0
- package/dist/sandbox/tools.js +165 -0
- package/dist/sandbox/types.d.ts +66 -0
- package/dist/vault/crypto.d.ts +12 -0
- package/dist/vault/crypto.js +80 -0
- package/dist/vault/error.d.ts +4 -0
- package/dist/vault/error.js +7 -0
- package/dist/vault/kek.d.ts +6 -0
- package/dist/vault/kek.js +26 -0
- package/dist/vault/service.d.ts +99 -0
- package/dist/vault/service.js +825 -0
- package/dist/vault/url.d.ts +3 -0
- package/dist/vault/url.js +27 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/package.json +12 -6
- package/dist/config.d.ts +0 -20
- package/dist/model/defaults.d.ts +0 -17
- package/dist/model/defaults.js +0 -21
- package/dist/model/http-model.d.ts +0 -12
- package/dist/model/http-model.js +0 -299
- package/dist/node/local-sessions.d.ts +0 -5
- package/dist/node/local-sessions.js +0 -174
- package/dist/server/ag-ui.d.ts +0 -8
- package/dist/server/ag-ui.js +0 -76
- package/dist/server/delivery.d.ts +0 -24
- package/dist/server/delivery.js +0 -107
- package/dist/server/digests.d.ts +0 -13
- package/dist/server/digests.js +0 -39
- package/dist/server/host.d.ts +0 -70
- package/dist/server/host.js +0 -647
- package/dist/session/api.d.ts +0 -10
- package/dist/session/api.js +0 -15
- package/dist/session/default.d.ts +0 -5
- package/dist/session/default.js +0 -30
- package/dist/session/handle.d.ts +0 -27
- package/dist/session/handle.js +0 -199
- package/dist/session/index.d.ts +0 -2
- package/dist/session/index.js +0 -2
- package/dist/sessions/host.d.ts +0 -39
- package/dist/sessions/host.js +0 -359
- package/dist/sessions/store.d.ts +0 -41
- package/dist/sessions/store.js +0 -33
- /package/dist/{config.js → sandbox/types.js} +0 -0
package/dist/core/runtime.js
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { createServer, } from "node:http";
|
|
2
2
|
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
3
|
-
import { mkdirSync, openSync, closeSync, unlinkSync, writeFileSync, readFileSync, } from "node:fs";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { mkdirSync, openSync, closeSync, unlinkSync, writeFileSync, readFileSync, realpathSync, mkdtempSync, } from "node:fs";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { AgentManifestSchema, CreateCredentialRequestSchema, CreateVaultRequestSchema, PutAgentRequestSchema, PutHostModelRequestSchema, SelectHostModelRequestSchema, PutSessionRequestSchema, RegisterExecutorsRequestSchema, RotateCredentialRequestSchema, SessionCommandSchema, ActionClaimRequestSchema, ActionHeartbeatRequestSchema, } from "@nylorun/core/contracts";
|
|
6
8
|
import { createDurableCheckpoint, runDurable, } from "@nylorun/harness/run";
|
|
7
9
|
import { hashManifest } from "@nylorun/core/compatibility";
|
|
10
|
+
import { schemaFromJSON, } from "@nylorun/core/define";
|
|
8
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";
|
|
9
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 } from "../mcp/pool.js";
|
|
22
|
+
import { SandboxManager, sandboxCapabilityOf } from "../sandbox/manager.js";
|
|
23
|
+
import { defaultSandboxBackends } from "../sandbox/select.js";
|
|
10
24
|
class HttpError extends Error {
|
|
11
25
|
status;
|
|
12
26
|
constructor(status, message) {
|
|
@@ -17,10 +31,82 @@ class HttpError extends Error {
|
|
|
17
31
|
const fail = (status, message) => {
|
|
18
32
|
throw new HttpError(status, message);
|
|
19
33
|
};
|
|
34
|
+
function sessionToolsOf(snapshot) {
|
|
35
|
+
if (!snapshot?.mcpTools.length)
|
|
36
|
+
return undefined;
|
|
37
|
+
return snapshot.mcpTools.map((tool) => ({
|
|
38
|
+
capabilityId: tool.capabilityId,
|
|
39
|
+
name: tool.name,
|
|
40
|
+
...(tool.description === undefined
|
|
41
|
+
? {}
|
|
42
|
+
: { description: tool.description }),
|
|
43
|
+
inputSchema: tool.inputSchema,
|
|
44
|
+
...(tool.outputSchema === undefined
|
|
45
|
+
? {}
|
|
46
|
+
: { outputSchema: tool.outputSchema }),
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
function mcpToolOf(session, capabilityId, toolName) {
|
|
50
|
+
return session.mcpSnapshot?.mcpTools.find((tool) => tool.capabilityId === capabilityId && tool.name === toolName);
|
|
51
|
+
}
|
|
52
|
+
function pinnedTool(manifest, capabilityId, toolName) {
|
|
53
|
+
const capability = manifest.capabilities.find((item) => item.id === capabilityId);
|
|
54
|
+
return capability?.tools?.find((tool) => tool.name === toolName);
|
|
55
|
+
}
|
|
56
|
+
function acceptedToolResult(action, command) {
|
|
57
|
+
if (action.kind !== "tool" || !action.outputSchema)
|
|
58
|
+
return command;
|
|
59
|
+
const value = command.outcome.value;
|
|
60
|
+
if (isFailedToolValue(value))
|
|
61
|
+
return command;
|
|
62
|
+
// Executors wrap successful tool output as `{ kind: "completed", output }`.
|
|
63
|
+
// Validate the tool payload, not the outcome envelope.
|
|
64
|
+
const candidate = completedToolOutput(value);
|
|
65
|
+
let matches = false;
|
|
66
|
+
try {
|
|
67
|
+
matches = schemaFromJSON(action.outputSchema).validate(candidate).ok;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
matches = false;
|
|
71
|
+
}
|
|
72
|
+
if (matches)
|
|
73
|
+
return command;
|
|
74
|
+
return {
|
|
75
|
+
...command,
|
|
76
|
+
outcome: {
|
|
77
|
+
...command.outcome,
|
|
78
|
+
value: {
|
|
79
|
+
kind: "failed",
|
|
80
|
+
code: "tool.invalid-output",
|
|
81
|
+
message: "Tool result does not match the output schema stored on the action",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function completedToolOutput(value) {
|
|
87
|
+
if (value &&
|
|
88
|
+
typeof value === "object" &&
|
|
89
|
+
!Array.isArray(value) &&
|
|
90
|
+
value.kind === "completed" &&
|
|
91
|
+
"output" in value) {
|
|
92
|
+
return value.output;
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
function isFailedToolValue(value) {
|
|
97
|
+
return (!!value &&
|
|
98
|
+
typeof value === "object" &&
|
|
99
|
+
!Array.isArray(value) &&
|
|
100
|
+
value.kind === "failed");
|
|
101
|
+
}
|
|
20
102
|
const semantic = (value) => {
|
|
21
103
|
const { requestId: _, ...body } = value;
|
|
22
104
|
return canonical(body);
|
|
23
105
|
};
|
|
106
|
+
const sessionIdentity = (value) => {
|
|
107
|
+
const { requestId: _requestId, vaultIds: _vaultIds, credentialSelections: _selections, ...body } = value ?? {};
|
|
108
|
+
return canonical(body);
|
|
109
|
+
};
|
|
24
110
|
const equals = (a, b) => {
|
|
25
111
|
const aa = Buffer.from(a), bb = Buffer.from(b);
|
|
26
112
|
return aa.length === bb.length && timingSafeEqual(aa, bb);
|
|
@@ -28,12 +114,21 @@ const equals = (a, b) => {
|
|
|
28
114
|
export class CoreRuntime {
|
|
29
115
|
options;
|
|
30
116
|
store;
|
|
117
|
+
vault;
|
|
118
|
+
kek;
|
|
119
|
+
kekPath;
|
|
31
120
|
running = new Map();
|
|
32
121
|
pending = new Set();
|
|
33
122
|
observers = new Map();
|
|
34
|
-
|
|
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;
|
|
127
|
+
mcp;
|
|
128
|
+
sandbox;
|
|
35
129
|
server;
|
|
36
130
|
closing = false;
|
|
131
|
+
closed = false;
|
|
37
132
|
lockPath;
|
|
38
133
|
timer;
|
|
39
134
|
constructor(options) {
|
|
@@ -46,12 +141,8 @@ export class CoreRuntime {
|
|
|
46
141
|
throw new Error("Executor tokens must be unique");
|
|
47
142
|
if (!options.serverToken || options.serverToken.length < 16)
|
|
48
143
|
throw new Error("A server token of at least 16 characters is required");
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
!e.agentId ||
|
|
52
|
-
!e.manifestHash ||
|
|
53
|
-
!e.implementationVersion))
|
|
54
|
-
throw new Error("Executor tokens require independent credentials and exact definition scope");
|
|
144
|
+
for (const executor of options.executors)
|
|
145
|
+
assertExecutorCredential(executor, options.serverToken);
|
|
55
146
|
if (options.sqlitePath !== ":memory:") {
|
|
56
147
|
mkdirSync(dirname(options.sqlitePath), { recursive: true });
|
|
57
148
|
this.lockPath = options.sqlitePath + ".runtime-lock";
|
|
@@ -78,14 +169,93 @@ export class CoreRuntime {
|
|
|
78
169
|
writeFileSync(fd, String(process.pid));
|
|
79
170
|
closeSync(fd);
|
|
80
171
|
}
|
|
172
|
+
this.kekPath = options.vaultKekPath ?? defaultKekPath();
|
|
173
|
+
let store;
|
|
81
174
|
try {
|
|
82
|
-
|
|
175
|
+
store = new Store(options.sqlitePath);
|
|
176
|
+
this.kek = readVaultKek({
|
|
177
|
+
vaultKek: options.vaultKek,
|
|
178
|
+
vaultKekPath: this.kekPath,
|
|
179
|
+
});
|
|
180
|
+
if (store.credentialCount() > 0 && !this.kek)
|
|
181
|
+
throw new Error("Vault key-encryption key is required to open this database");
|
|
182
|
+
this.store = store;
|
|
83
183
|
}
|
|
84
184
|
catch (e) {
|
|
185
|
+
store?.db.close();
|
|
85
186
|
if (this.lockPath)
|
|
86
187
|
unlinkSync(this.lockPath);
|
|
87
188
|
throw e;
|
|
88
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
|
+
}
|
|
232
|
+
this.vault = new VaultService(this.store.db, (fn) => this.store.tx(fn), () => this.ensureKek(), options.vaultFetch ?? globalThis.fetch);
|
|
233
|
+
this.mcp = new McpPool({
|
|
234
|
+
dataDir: dirname(options.sqlitePath),
|
|
235
|
+
authorize: (sessionId, request) => this.authorize(sessionId, request),
|
|
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);
|
|
89
259
|
this.store.tx(() => {
|
|
90
260
|
for (const effect of this.store.all("effects"))
|
|
91
261
|
if (effect.status === "invoking") {
|
|
@@ -103,6 +273,7 @@ export class CoreRuntime {
|
|
|
103
273
|
}
|
|
104
274
|
}
|
|
105
275
|
});
|
|
276
|
+
this.retireLegacyHooks();
|
|
106
277
|
this.expireClaims();
|
|
107
278
|
this.timer = setInterval(() => this.expireClaims(), Math.min(options.leaseMs ?? 30000, 5000));
|
|
108
279
|
this.timer.unref();
|
|
@@ -122,15 +293,53 @@ export class CoreRuntime {
|
|
|
122
293
|
response.destroy();
|
|
123
294
|
}
|
|
124
295
|
notify() {
|
|
125
|
-
for (const
|
|
126
|
-
|
|
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
|
+
});
|
|
127
325
|
}
|
|
128
326
|
expireClaims() {
|
|
129
327
|
const events = [];
|
|
328
|
+
let redeliver = false;
|
|
130
329
|
this.store.tx(() => {
|
|
131
330
|
for (const action of this.store.all("actions"))
|
|
132
331
|
if (action.status === "claimed" &&
|
|
133
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
|
+
}
|
|
134
343
|
action.status = "uncertain";
|
|
135
344
|
this.store.put("actions", action.actionId, action);
|
|
136
345
|
const effect = this.store.get("effects", action.actionId);
|
|
@@ -147,6 +356,8 @@ export class CoreRuntime {
|
|
|
147
356
|
}
|
|
148
357
|
});
|
|
149
358
|
events.forEach((e) => this.publish(e));
|
|
359
|
+
if (redeliver)
|
|
360
|
+
this.notify();
|
|
150
361
|
}
|
|
151
362
|
schedule(id) {
|
|
152
363
|
if (this.closing)
|
|
@@ -166,10 +377,13 @@ export class CoreRuntime {
|
|
|
166
377
|
const controller = new AbortController();
|
|
167
378
|
this.running.set(id, controller);
|
|
168
379
|
try {
|
|
380
|
+
await this.prepareMcp(id, controller.signal);
|
|
381
|
+
const current = this.session(id);
|
|
169
382
|
const result = await runDurable({
|
|
170
|
-
manifest:
|
|
171
|
-
checkpoint:
|
|
383
|
+
manifest: current.manifest,
|
|
384
|
+
checkpoint: current.checkpoint,
|
|
172
385
|
signal: controller.signal,
|
|
386
|
+
sessionTools: sessionToolsOf(current.mcpSnapshot),
|
|
173
387
|
host: {
|
|
174
388
|
resolveEffect: (e) => this.resolveEffect(e, controller.signal),
|
|
175
389
|
},
|
|
@@ -258,8 +472,22 @@ export class CoreRuntime {
|
|
|
258
472
|
this.schedule(id);
|
|
259
473
|
}
|
|
260
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
|
+
}
|
|
261
489
|
async resolveEffect(request, signal) {
|
|
262
|
-
let invoke
|
|
490
|
+
let invoke;
|
|
263
491
|
let notify = false;
|
|
264
492
|
let event;
|
|
265
493
|
const resolution = this.store.tx(() => {
|
|
@@ -281,24 +509,32 @@ export class CoreRuntime {
|
|
|
281
509
|
: "pending",
|
|
282
510
|
};
|
|
283
511
|
}
|
|
512
|
+
const mcpTool = request.kind === "tool"
|
|
513
|
+
? mcpToolOf(s, request.capabilityId, request.toolName)
|
|
514
|
+
: undefined;
|
|
515
|
+
const sandboxTool = request.kind === "tool" && !mcpTool
|
|
516
|
+
? sandboxCapabilityOf(s.manifest, request.capabilityId, request.toolName)
|
|
517
|
+
: undefined;
|
|
284
518
|
this.store.put("effects", request.effectId, {
|
|
285
519
|
request,
|
|
286
|
-
status: request.kind === "model"
|
|
520
|
+
status: request.kind === "model" || mcpTool || sandboxTool
|
|
521
|
+
? "invoking"
|
|
522
|
+
: "pending",
|
|
287
523
|
});
|
|
288
|
-
if (request.kind === "model") {
|
|
289
|
-
invoke =
|
|
524
|
+
if (request.kind === "model" || mcpTool || sandboxTool) {
|
|
525
|
+
invoke = mcpTool ? "mcp" : sandboxTool ? "sandbox" : "model";
|
|
290
526
|
return undefined;
|
|
291
527
|
}
|
|
292
|
-
const
|
|
528
|
+
const tool = request.kind === "tool"
|
|
529
|
+
? pinnedTool(s.manifest, request.capabilityId, request.toolName)
|
|
530
|
+
: undefined;
|
|
531
|
+
const base = {
|
|
293
532
|
actionId: request.effectId,
|
|
294
533
|
sessionId: request.sessionId,
|
|
295
534
|
turnId: request.turnId,
|
|
296
535
|
agentId: request.agentId,
|
|
297
536
|
manifestHash: request.manifestHash,
|
|
298
537
|
implementationVersion: s.implementationVersion,
|
|
299
|
-
kind: request.kind,
|
|
300
|
-
capabilityId: request.capabilityId,
|
|
301
|
-
...(request.toolName ? { toolName: request.toolName } : {}),
|
|
302
538
|
input: request.input,
|
|
303
539
|
context: request.context,
|
|
304
540
|
status: "pending",
|
|
@@ -306,11 +542,29 @@ export class CoreRuntime {
|
|
|
306
542
|
claimId: null,
|
|
307
543
|
leaseExpiresAt: null,
|
|
308
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
|
+
};
|
|
309
563
|
this.store.put("actions", action.actionId, action);
|
|
310
564
|
event = this.store.event(s.id, s.activeTurnId, "action.pending", {
|
|
311
565
|
actionId: action.actionId,
|
|
312
566
|
kind: action.kind,
|
|
313
|
-
|
|
567
|
+
...actionTarget(action),
|
|
314
568
|
input: action.input,
|
|
315
569
|
});
|
|
316
570
|
notify = true;
|
|
@@ -323,7 +577,11 @@ export class CoreRuntime {
|
|
|
323
577
|
if (!invoke)
|
|
324
578
|
return resolution;
|
|
325
579
|
try {
|
|
326
|
-
const value =
|
|
580
|
+
const value = invoke === "mcp"
|
|
581
|
+
? await this.callMcpTool(request)
|
|
582
|
+
: invoke === "sandbox"
|
|
583
|
+
? await this.callSandboxTool(request, signal)
|
|
584
|
+
: await this.invokeModel(request, signal);
|
|
327
585
|
return this.store.tx(() => {
|
|
328
586
|
const s = this.session(request.sessionId);
|
|
329
587
|
if (s.status === "cancelled" ||
|
|
@@ -359,18 +617,19 @@ export class CoreRuntime {
|
|
|
359
617
|
scope(request) {
|
|
360
618
|
const header = request.headers.authorization;
|
|
361
619
|
const token = header?.startsWith("Bearer ") ? header.slice(7) : "";
|
|
362
|
-
if (token
|
|
620
|
+
if (!token)
|
|
621
|
+
return fail(401, "Invalid credentials");
|
|
622
|
+
if (equals(token, this.options.serverToken))
|
|
363
623
|
return "server";
|
|
364
|
-
|
|
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));
|
|
365
627
|
if (!executor)
|
|
366
628
|
return fail(401, "Invalid credentials");
|
|
367
629
|
return executor;
|
|
368
630
|
}
|
|
369
631
|
scoped(scope, action) {
|
|
370
|
-
if (scope === "server" ||
|
|
371
|
-
scope.agentId !== action.agentId ||
|
|
372
|
-
scope.manifestHash !== action.manifestHash ||
|
|
373
|
-
scope.implementationVersion !== action.implementationVersion)
|
|
632
|
+
if (scope === "server" || scope.agentId !== action.agentId)
|
|
374
633
|
fail(403, "Executor scope does not authorize this action");
|
|
375
634
|
}
|
|
376
635
|
async body(request) {
|
|
@@ -387,21 +646,23 @@ export class CoreRuntime {
|
|
|
387
646
|
return fail(400, "Invalid JSON");
|
|
388
647
|
}
|
|
389
648
|
}
|
|
390
|
-
command(id,
|
|
649
|
+
command(id, input, scope) {
|
|
391
650
|
let event;
|
|
392
651
|
let schedule = false;
|
|
393
652
|
const response = this.store.tx(() => {
|
|
394
653
|
const s = this.session(id);
|
|
395
|
-
|
|
654
|
+
let command = input;
|
|
396
655
|
if (command.type === "action_result") {
|
|
397
656
|
const a = this.store.get("actions", command.actionId) ??
|
|
398
657
|
fail(404, "Action not found");
|
|
399
658
|
if (a.sessionId !== id)
|
|
400
659
|
fail(403, "Action belongs to another session");
|
|
401
660
|
this.scoped(scope, a);
|
|
661
|
+
command = acceptedToolResult(a, command);
|
|
402
662
|
}
|
|
403
663
|
else if (scope !== "server")
|
|
404
664
|
fail(403, "Application credential required");
|
|
665
|
+
const key = JSON.stringify([id, command.idempotencyKey]);
|
|
405
666
|
const existing = this.store.get("commands", key);
|
|
406
667
|
if (existing) {
|
|
407
668
|
if (semantic(existing.command) !== semantic(command))
|
|
@@ -434,7 +695,7 @@ export class CoreRuntime {
|
|
|
434
695
|
schedule = true;
|
|
435
696
|
event = this.store.event(id, s.activeTurnId, "action.completed", {
|
|
436
697
|
actionId: action.actionId,
|
|
437
|
-
|
|
698
|
+
...actionTarget(action),
|
|
438
699
|
kind: action.kind,
|
|
439
700
|
result: command.outcome.value,
|
|
440
701
|
});
|
|
@@ -544,13 +805,13 @@ export class CoreRuntime {
|
|
|
544
805
|
});
|
|
545
806
|
if (event)
|
|
546
807
|
this.publish(event);
|
|
547
|
-
if (
|
|
808
|
+
if (input.type === "cancel")
|
|
548
809
|
this.running.get(id)?.abort();
|
|
549
810
|
if (schedule)
|
|
550
811
|
this.schedule(id);
|
|
551
812
|
return response;
|
|
552
813
|
}
|
|
553
|
-
sse(request, response, set) {
|
|
814
|
+
sse(request, response, set, whenEmpty) {
|
|
554
815
|
response.writeHead(200, {
|
|
555
816
|
"content-type": "text/event-stream",
|
|
556
817
|
"cache-control": "no-cache",
|
|
@@ -564,6 +825,8 @@ export class CoreRuntime {
|
|
|
564
825
|
request.on("close", () => {
|
|
565
826
|
clearInterval(timer);
|
|
566
827
|
set.delete(response);
|
|
828
|
+
if (!set.size)
|
|
829
|
+
whenEmpty?.();
|
|
567
830
|
});
|
|
568
831
|
}
|
|
569
832
|
async handle(request, response) {
|
|
@@ -579,7 +842,13 @@ export class CoreRuntime {
|
|
|
579
842
|
.map(decodeURIComponent);
|
|
580
843
|
const method = request.method;
|
|
581
844
|
if (url.pathname === "/health")
|
|
582
|
-
return json({
|
|
845
|
+
return json({
|
|
846
|
+
status: "ok",
|
|
847
|
+
service: "oss-runtime",
|
|
848
|
+
version: RUNTIME_VERSION,
|
|
849
|
+
scopeId: this.scopeId,
|
|
850
|
+
pid: process.pid,
|
|
851
|
+
});
|
|
583
852
|
if (url.pathname === "/ready") {
|
|
584
853
|
this.store.db.prepare("SELECT 1").get();
|
|
585
854
|
return json({
|
|
@@ -600,7 +869,11 @@ export class CoreRuntime {
|
|
|
600
869
|
method === "GET") {
|
|
601
870
|
if (scope === "server")
|
|
602
871
|
fail(403, "Executor credential required");
|
|
603
|
-
|
|
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));
|
|
604
877
|
this.send(response, 'event: work_available\ndata: {"type":"work_available"}\n\n');
|
|
605
878
|
return;
|
|
606
879
|
}
|
|
@@ -612,10 +885,7 @@ export class CoreRuntime {
|
|
|
612
885
|
return json({
|
|
613
886
|
actions: this.store
|
|
614
887
|
.all("actions")
|
|
615
|
-
.filter((a) => a.status === "pending" &&
|
|
616
|
-
a.agentId === scope.agentId &&
|
|
617
|
-
a.manifestHash === scope.manifestHash &&
|
|
618
|
-
a.implementationVersion === scope.implementationVersion),
|
|
888
|
+
.filter((a) => a.status === "pending" && a.agentId === scope.agentId),
|
|
619
889
|
});
|
|
620
890
|
const actionId = path[2];
|
|
621
891
|
if (!actionId)
|
|
@@ -627,10 +897,7 @@ export class CoreRuntime {
|
|
|
627
897
|
fail(404, "Action not found");
|
|
628
898
|
this.scoped(scope, action);
|
|
629
899
|
if (method === "POST" && path[3] === "claim") {
|
|
630
|
-
|
|
631
|
-
if (claim.manifestHash !== action.manifestHash ||
|
|
632
|
-
claim.implementationVersion !== action.implementationVersion)
|
|
633
|
-
fail(409, "Definition mismatch");
|
|
900
|
+
ActionClaimRequestSchema.parse(body);
|
|
634
901
|
if (action.status !== "pending" ||
|
|
635
902
|
this.session(action.sessionId).status === "cancelled" ||
|
|
636
903
|
this.session(action.sessionId).activeTurnId !== action.turnId)
|
|
@@ -671,13 +938,104 @@ export class CoreRuntime {
|
|
|
671
938
|
path[3] === "commands" &&
|
|
672
939
|
method === "POST")
|
|
673
940
|
return json(this.command(path[2], SessionCommandSchema.parse(await this.body(request)), scope));
|
|
941
|
+
if (path[1] === "vaults")
|
|
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));
|
|
674
945
|
if (scope !== "server")
|
|
675
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
|
+
}
|
|
676
1036
|
if (path[1] === "agents" && path.length === 2 && method === "GET")
|
|
677
1037
|
return json({
|
|
678
|
-
agents: this.store
|
|
679
|
-
.all("definitions")
|
|
680
|
-
.map((d) => ({
|
|
1038
|
+
agents: this.store.all("definitions").map((d) => ({
|
|
681
1039
|
agentId: d.manifest.id,
|
|
682
1040
|
manifest: d.manifest,
|
|
683
1041
|
manifestHash: d.manifestHash,
|
|
@@ -723,11 +1081,19 @@ export class CoreRuntime {
|
|
|
723
1081
|
const id = path[2];
|
|
724
1082
|
if (method === "PUT" && path.length === 3) {
|
|
725
1083
|
const body = PutSessionRequestSchema.parse(await this.body(request));
|
|
1084
|
+
const vaultIds = body.vaultIds ?? [];
|
|
1085
|
+
const credentialSelections = body.credentialSelections ?? [];
|
|
726
1086
|
const result = this.store.tx(() => {
|
|
1087
|
+
this.vault.assertAttachment(body.ownerUserId, vaultIds, credentialSelections);
|
|
727
1088
|
const prior = this.store.get("sessions", id);
|
|
728
1089
|
if (prior) {
|
|
729
|
-
if (
|
|
1090
|
+
if (sessionIdentity(prior.creation) !== sessionIdentity(body))
|
|
730
1091
|
fail(409, "Session already exists with different creation parameters");
|
|
1092
|
+
prior.vaultIds = vaultIds;
|
|
1093
|
+
prior.credentialSelections = credentialSelections;
|
|
1094
|
+
prior.creation = body;
|
|
1095
|
+
this.store.put("sessions", id, prior);
|
|
1096
|
+
this.vault.recordAttachment(id, vaultIds);
|
|
731
1097
|
return prior;
|
|
732
1098
|
}
|
|
733
1099
|
const definition = this.store.get("definitions", body.agentId) ??
|
|
@@ -743,8 +1109,12 @@ export class CoreRuntime {
|
|
|
743
1109
|
status: "idle",
|
|
744
1110
|
activeTurnId: null,
|
|
745
1111
|
creation: body,
|
|
1112
|
+
vaultIds,
|
|
1113
|
+
credentialSelections,
|
|
1114
|
+
pluginRoots: definition.pluginRoots ?? {},
|
|
746
1115
|
};
|
|
747
1116
|
this.store.put("sessions", id, s);
|
|
1117
|
+
this.vault.recordAttachment(id, vaultIds);
|
|
748
1118
|
return s;
|
|
749
1119
|
});
|
|
750
1120
|
return json(this.view(result));
|
|
@@ -775,7 +1145,7 @@ export class CoreRuntime {
|
|
|
775
1145
|
response.end();
|
|
776
1146
|
return;
|
|
777
1147
|
}
|
|
778
|
-
const status = error instanceof HttpError
|
|
1148
|
+
const status = error instanceof HttpError || error instanceof VaultError
|
|
779
1149
|
? error.status
|
|
780
1150
|
: error?.name === "ZodError" ||
|
|
781
1151
|
error?.message === "Invalid cursor"
|
|
@@ -799,6 +1169,10 @@ export class CoreRuntime {
|
|
|
799
1169
|
implementationVersion: s.implementationVersion,
|
|
800
1170
|
status: s.status,
|
|
801
1171
|
activeTurnId: s.activeTurnId,
|
|
1172
|
+
vaultIds: s.vaultIds ?? [],
|
|
1173
|
+
credentialSelections: s.credentialSelections ?? [],
|
|
1174
|
+
mcpSnapshot: s.mcpSnapshot ?? null,
|
|
1175
|
+
mcpDiagnostics: s.mcpDiagnostics ?? [],
|
|
802
1176
|
waits: Array.isArray(s.waits)
|
|
803
1177
|
? s.waits.map((call) => ({
|
|
804
1178
|
invocationId: call.invocationId,
|
|
@@ -823,6 +1197,165 @@ export class CoreRuntime {
|
|
|
823
1197
|
})),
|
|
824
1198
|
};
|
|
825
1199
|
}
|
|
1200
|
+
async prepareMcp(id, signal) {
|
|
1201
|
+
const s = this.session(id);
|
|
1202
|
+
const declared = s.manifest.capabilities.some((capability) => capability.mcpServers !== undefined &&
|
|
1203
|
+
Object.keys(capability.mcpServers).length > 0);
|
|
1204
|
+
if (!declared)
|
|
1205
|
+
return;
|
|
1206
|
+
if (!s.mcpSnapshot) {
|
|
1207
|
+
const found = await this.mcp.discover({
|
|
1208
|
+
sessionId: id,
|
|
1209
|
+
manifest: s.manifest,
|
|
1210
|
+
manifestHash: s.manifestHash,
|
|
1211
|
+
pluginRoots: s.pluginRoots ?? {},
|
|
1212
|
+
signal,
|
|
1213
|
+
});
|
|
1214
|
+
this.store.tx(() => {
|
|
1215
|
+
const current = this.session(id);
|
|
1216
|
+
if (current.mcpSnapshot)
|
|
1217
|
+
return;
|
|
1218
|
+
current.mcpSnapshot = found.snapshot;
|
|
1219
|
+
current.mcpDiagnostics = found.diagnostics;
|
|
1220
|
+
this.store.put("sessions", id, current);
|
|
1221
|
+
});
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const diagnostics = await this.mcp.reconnect({
|
|
1225
|
+
sessionId: id,
|
|
1226
|
+
manifest: s.manifest,
|
|
1227
|
+
pluginRoots: s.pluginRoots ?? {},
|
|
1228
|
+
tools: s.mcpSnapshot.mcpTools,
|
|
1229
|
+
signal,
|
|
1230
|
+
});
|
|
1231
|
+
if (diagnostics.length === 0)
|
|
1232
|
+
return;
|
|
1233
|
+
this.store.tx(() => {
|
|
1234
|
+
const current = this.session(id);
|
|
1235
|
+
const prior = [...(current.mcpDiagnostics ?? [])];
|
|
1236
|
+
for (const item of diagnostics) {
|
|
1237
|
+
const index = prior.findIndex((existing) => existing.capabilityId === item.capabilityId &&
|
|
1238
|
+
existing.serverName === item.serverName);
|
|
1239
|
+
if (index >= 0)
|
|
1240
|
+
prior[index] = item;
|
|
1241
|
+
else
|
|
1242
|
+
prior.push(item);
|
|
1243
|
+
}
|
|
1244
|
+
current.mcpDiagnostics = prior;
|
|
1245
|
+
this.store.put("sessions", id, current);
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
async callMcpTool(request) {
|
|
1249
|
+
const s = this.session(request.sessionId);
|
|
1250
|
+
const tool = mcpToolOf(s, request.capabilityId, request.toolName);
|
|
1251
|
+
if (!tool)
|
|
1252
|
+
throw new Error(`MCP tool '${request.toolName ?? ""}' is not in the session snapshot`);
|
|
1253
|
+
return this.mcp.call({
|
|
1254
|
+
sessionId: s.id,
|
|
1255
|
+
capabilityId: tool.capabilityId,
|
|
1256
|
+
serverName: tool.serverName,
|
|
1257
|
+
serverToolName: tool.serverToolName,
|
|
1258
|
+
args: request.input,
|
|
1259
|
+
manifest: s.manifest,
|
|
1260
|
+
pluginRoots: s.pluginRoots ?? {},
|
|
1261
|
+
});
|
|
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
|
+
}
|
|
1270
|
+
async authorize(sessionId, request) {
|
|
1271
|
+
const s = this.session(sessionId);
|
|
1272
|
+
const result = await this.vault.authorize({
|
|
1273
|
+
sessionId,
|
|
1274
|
+
vaultIds: s.vaultIds ?? [],
|
|
1275
|
+
credentialSelections: s.credentialSelections ?? [],
|
|
1276
|
+
url: request.url,
|
|
1277
|
+
serverName: request.serverName,
|
|
1278
|
+
});
|
|
1279
|
+
if (result.status === "authorized") {
|
|
1280
|
+
const token = result.headers.authorization.slice("Bearer ".length);
|
|
1281
|
+
scrub({ authorization: result.headers.authorization, url: result.url }, [
|
|
1282
|
+
token,
|
|
1283
|
+
]);
|
|
1284
|
+
}
|
|
1285
|
+
return result;
|
|
1286
|
+
}
|
|
1287
|
+
ensureKek() {
|
|
1288
|
+
if (this.kek)
|
|
1289
|
+
return this.kek;
|
|
1290
|
+
if (this.options.vaultKek === null)
|
|
1291
|
+
throw new Error("Vault key-encryption key is required");
|
|
1292
|
+
this.kek = createKekFile(this.kekPath);
|
|
1293
|
+
return this.kek;
|
|
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
|
+
}
|
|
1317
|
+
async dispatchVault(scope, method, path, url, request) {
|
|
1318
|
+
if (scope !== "server") {
|
|
1319
|
+
this.vault.reject(path.join("/"));
|
|
1320
|
+
fail(403, "Application credential required");
|
|
1321
|
+
}
|
|
1322
|
+
if (path.length === 2 && method === "POST") {
|
|
1323
|
+
const body = CreateVaultRequestSchema.parse(await this.body(request));
|
|
1324
|
+
return this.vault.createVault(body);
|
|
1325
|
+
}
|
|
1326
|
+
if (path.length === 2 && method === "GET") {
|
|
1327
|
+
const ownerUserId = url.searchParams.get("ownerUserId") ??
|
|
1328
|
+
fail(400, "ownerUserId is required");
|
|
1329
|
+
return { vaults: this.vault.listVaults(ownerUserId) };
|
|
1330
|
+
}
|
|
1331
|
+
const vaultId = path[2];
|
|
1332
|
+
if (!vaultId)
|
|
1333
|
+
fail(404, "Vault not found");
|
|
1334
|
+
if (path.length === 3 && method === "GET")
|
|
1335
|
+
return this.vault.getVault(vaultId);
|
|
1336
|
+
if (path.length === 3 && method === "DELETE")
|
|
1337
|
+
return this.vault.deleteVault(vaultId);
|
|
1338
|
+
if (path[3] !== "credentials")
|
|
1339
|
+
fail(404, "Route not found");
|
|
1340
|
+
if (path.length === 4 && method === "POST") {
|
|
1341
|
+
const body = CreateCredentialRequestSchema.parse(await this.body(request));
|
|
1342
|
+
return this.vault.createCredential(vaultId, body);
|
|
1343
|
+
}
|
|
1344
|
+
if (path.length === 4 && method === "GET")
|
|
1345
|
+
return { credentials: this.vault.listCredentials(vaultId) };
|
|
1346
|
+
const credentialId = path[4];
|
|
1347
|
+
if (!credentialId)
|
|
1348
|
+
fail(404, "Credential not found");
|
|
1349
|
+
if (path.length === 5 && method === "GET")
|
|
1350
|
+
return this.vault.getCredential(vaultId, credentialId);
|
|
1351
|
+
if (path.length === 5 && method === "POST") {
|
|
1352
|
+
const body = RotateCredentialRequestSchema.parse(await this.body(request));
|
|
1353
|
+
return this.vault.rotateCredential(vaultId, credentialId, body);
|
|
1354
|
+
}
|
|
1355
|
+
if (path.length === 5 && method === "DELETE")
|
|
1356
|
+
return this.vault.deleteCredential(vaultId, credentialId);
|
|
1357
|
+
fail(404, "Route not found");
|
|
1358
|
+
}
|
|
826
1359
|
async listen(port = 8787, hostname = "127.0.0.1") {
|
|
827
1360
|
if (this.server)
|
|
828
1361
|
throw new Error("Already listening");
|
|
@@ -843,8 +1376,10 @@ export class CoreRuntime {
|
|
|
843
1376
|
clearInterval(this.timer);
|
|
844
1377
|
for (const c of this.running.values())
|
|
845
1378
|
c.abort();
|
|
846
|
-
|
|
847
|
-
|
|
1379
|
+
await this.mcp.close();
|
|
1380
|
+
for (const streams of this.executorStreams.values())
|
|
1381
|
+
for (const r of streams)
|
|
1382
|
+
r.end();
|
|
848
1383
|
for (const set of this.observers.values())
|
|
849
1384
|
for (const r of set)
|
|
850
1385
|
r.end();
|
|
@@ -856,6 +1391,8 @@ export class CoreRuntime {
|
|
|
856
1391
|
// Providers must honor AbortSignal. Keep storage open until any in-flight journal writes have drained.
|
|
857
1392
|
while (this.running.size)
|
|
858
1393
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1394
|
+
await this.sandbox.close();
|
|
1395
|
+
this.closed = true;
|
|
859
1396
|
this.store.db.close();
|
|
860
1397
|
if (this.lockPath)
|
|
861
1398
|
unlinkSync(this.lockPath);
|
|
@@ -875,3 +1412,7 @@ export async function startRuntime(options) {
|
|
|
875
1412
|
throw error;
|
|
876
1413
|
}
|
|
877
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
|
+
}
|