@nylorun/runtime 0.6.0-beta → 0.7.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 +15 -0
- package/README.md +3 -1
- package/dist/core/runtime.d.ts +17 -0
- package/dist/core/runtime.js +280 -31
- package/dist/core/store.d.ts +1 -0
- package/dist/core/store.js +46 -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/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 +59 -0
- package/dist/vault/service.js +538 -0
- package/dist/vault/url.d.ts +3 -0
- package/dist/vault/url.js +27 -0
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0-beta
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3a88f51: Ship Agent-Plugins (`plugin()` / `loadPlugin`), Skills (`load_skill` / skill resources), Runtime MCP pool + vault credentials, and manifest v3 capability fields. Validate completed tool `output` against the tool output schema so ordinary tools with `outputSchema` no longer false-fail as `tool.invalid-output`.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Pin core to the tested release.
|
|
12
|
+
- Pin harness to the tested release.
|
|
13
|
+
- Updated dependencies [3a88f51]
|
|
14
|
+
- Updated dependencies
|
|
15
|
+
- @nylorun/core@0.2.0-beta
|
|
16
|
+
- @nylorun/harness@0.16.0-beta
|
|
17
|
+
|
|
3
18
|
## 0.6.0-beta
|
|
4
19
|
|
|
5
20
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ NYLORUN_SERVER_KEY='<separate-secret-at-least-16-characters>' npm start --worksp
|
|
|
14
14
|
|
|
15
15
|
Default address: `http://127.0.0.1:8787`. `HOST`, `PORT`, and `NYLORUN_SQLITE_PATH` configure binding and storage. Missing server credentials fail startup. The default model is a credential-free scripted development response. It does not simulate conversations, tools or provider compatibility.
|
|
16
16
|
|
|
17
|
-
`NYLORUN_EXECUTORS_JSON` is an array of `{token, agentId,
|
|
17
|
+
`NYLORUN_EXECUTORS_JSON` is an array of `{token, agentId, implementationVersion}`. Each secret must differ from the application server key and every other executor token. An optional `manifestHash` on an existing record is ignored and does not hide or reject work. SDK clients use `NYLORUN_RUNTIME_URL`, `NYLORUN_SERVER_KEY`, and `NYLORUN_EXECUTOR_KEY`; runtime executor scope is the agent id, never broadened by client input.
|
|
18
18
|
|
|
19
19
|
For a model gateway, set `NYLORUN_MODEL_GATEWAY_URL`, `NYLORUN_MODEL_GATEWAY_KEY`, and `NYLORUN_MODEL`. The gateway receives `{model, call, context}` and returns a harness ModelCandidate or string. Provider credentials remain host configuration. The generic gateway requires an implementation of this envelope; it does not claim arbitrary provider wire compatibility. Alternatively supply a `ModelProvider` to `startRuntime`.
|
|
20
20
|
|
|
@@ -36,6 +36,8 @@ SQLite transactions persist session checkpoints, command receipts, individual ef
|
|
|
36
36
|
|
|
37
37
|
One process owns each SQLite database. A local PID lock rejects concurrent owners and recovers only when the recorded process no longer exists. Shared/network filesystems and multiple replicas are unsupported. Keep the database and its WAL/journal together; do not delete persisted effects separately from checkpoints. Invalid lock contents or PID reuse fail closed and require operator inspection. TLS and external ingress belong to deployment infrastructure.
|
|
38
38
|
|
|
39
|
+
Vault credentials are a separate store from server tokens, executor tokens, and model provider keys. Each vault belongs to one `ownerUserId`. Secret values are encrypted with AES-256-GCM under a key-encryption key. Set `NYLORUN_VAULT_KEK` to 32 bytes encoded as base64, or allow the host to create `.nylorun/vault-kek` (mode 0600) on the first write. A database that already holds vault ciphertext will not open without that key. Reads of a vault or credential return metadata only.
|
|
40
|
+
|
|
39
41
|
## Local project workflow
|
|
40
42
|
|
|
41
43
|
Install `@nylorun/cli` (generated projects already include it). Use `npm run configure`, then `nylorun dev` in a generated project. It loads the exported `agents` registry from `agents/index.ts`, starts this Runtime in a separate process, registers manifests, connects scoped SDK executors, and opens Studio. Use `--no-studio` or `--no-open` as needed. Runtime binds loopback on port 8787 (`PORT` override). The CLI creates separate server and executor credentials with mode 0600 in `.nylorun/local-credentials.json`; SQLite defaults to `.nylorun/runtime.sqlite`.
|
package/dist/core/runtime.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ExecutorScope } from "@nylorun/core/contracts";
|
|
2
2
|
import { type ModelProvider } from "./provider.js";
|
|
3
|
+
import { type AuthorizeResult } from "../vault/service.js";
|
|
3
4
|
export interface RuntimeOptions {
|
|
4
5
|
sqlitePath: string;
|
|
5
6
|
serverToken: string;
|
|
@@ -8,14 +9,22 @@ export interface RuntimeOptions {
|
|
|
8
9
|
})[];
|
|
9
10
|
model?: ModelProvider;
|
|
10
11
|
leaseMs?: number;
|
|
12
|
+
/** 32-byte key, or base64 of that key. `null` disables env and file lookup. */
|
|
13
|
+
vaultKek?: Buffer | string | null;
|
|
14
|
+
vaultKekPath?: string;
|
|
15
|
+
vaultFetch?: typeof fetch;
|
|
11
16
|
}
|
|
12
17
|
export declare class CoreRuntime {
|
|
13
18
|
private readonly options;
|
|
14
19
|
private readonly store;
|
|
20
|
+
private readonly vault;
|
|
21
|
+
private kek;
|
|
22
|
+
private readonly kekPath;
|
|
15
23
|
private readonly running;
|
|
16
24
|
private readonly pending;
|
|
17
25
|
private readonly observers;
|
|
18
26
|
private readonly executors;
|
|
27
|
+
private readonly mcp;
|
|
19
28
|
private server?;
|
|
20
29
|
private closing;
|
|
21
30
|
private readonly lockPath?;
|
|
@@ -36,6 +45,14 @@ export declare class CoreRuntime {
|
|
|
36
45
|
private sse;
|
|
37
46
|
private handle;
|
|
38
47
|
private view;
|
|
48
|
+
private prepareMcp;
|
|
49
|
+
private callMcpTool;
|
|
50
|
+
authorize(sessionId: string, request: {
|
|
51
|
+
url: string;
|
|
52
|
+
serverName?: string;
|
|
53
|
+
}): Promise<AuthorizeResult>;
|
|
54
|
+
private ensureKek;
|
|
55
|
+
private dispatchVault;
|
|
39
56
|
listen(port?: number, hostname?: string): Promise<{
|
|
40
57
|
url: string;
|
|
41
58
|
}>;
|
package/dist/core/runtime.js
CHANGED
|
@@ -2,11 +2,17 @@ import { createServer, } from "node:http";
|
|
|
2
2
|
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import { mkdirSync, openSync, closeSync, unlinkSync, writeFileSync, readFileSync, } from "node:fs";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
|
-
import { AgentManifestSchema, PutAgentRequestSchema, PutSessionRequestSchema, SessionCommandSchema, ActionClaimRequestSchema, ActionHeartbeatRequestSchema, } from "@nylorun/core/contracts";
|
|
5
|
+
import { AgentManifestSchema, CreateCredentialRequestSchema, CreateVaultRequestSchema, PutAgentRequestSchema, PutSessionRequestSchema, RotateCredentialRequestSchema, SessionCommandSchema, ActionClaimRequestSchema, ActionHeartbeatRequestSchema, } from "@nylorun/core/contracts";
|
|
6
6
|
import { createDurableCheckpoint, runDurable, } from "@nylorun/harness/run";
|
|
7
7
|
import { hashManifest } from "@nylorun/core/compatibility";
|
|
8
|
+
import { schemaFromJSON, } from "@nylorun/core/define";
|
|
8
9
|
import { Store, canonical } from "./store.js";
|
|
9
10
|
import { scriptedModel } from "./provider.js";
|
|
11
|
+
import { scrub } from "../redact.js";
|
|
12
|
+
import { createKekFile, defaultKekPath, readVaultKek } from "../vault/kek.js";
|
|
13
|
+
import { VaultError } from "../vault/error.js";
|
|
14
|
+
import { VaultService } from "../vault/service.js";
|
|
15
|
+
import { McpPool } from "../mcp/pool.js";
|
|
10
16
|
class HttpError extends Error {
|
|
11
17
|
status;
|
|
12
18
|
constructor(status, message) {
|
|
@@ -17,10 +23,82 @@ class HttpError extends Error {
|
|
|
17
23
|
const fail = (status, message) => {
|
|
18
24
|
throw new HttpError(status, message);
|
|
19
25
|
};
|
|
26
|
+
function sessionToolsOf(snapshot) {
|
|
27
|
+
if (!snapshot?.mcpTools.length)
|
|
28
|
+
return undefined;
|
|
29
|
+
return snapshot.mcpTools.map((tool) => ({
|
|
30
|
+
capabilityId: tool.capabilityId,
|
|
31
|
+
name: tool.name,
|
|
32
|
+
...(tool.description === undefined
|
|
33
|
+
? {}
|
|
34
|
+
: { description: tool.description }),
|
|
35
|
+
inputSchema: tool.inputSchema,
|
|
36
|
+
...(tool.outputSchema === undefined
|
|
37
|
+
? {}
|
|
38
|
+
: { outputSchema: tool.outputSchema }),
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
function mcpToolOf(session, capabilityId, toolName) {
|
|
42
|
+
return session.mcpSnapshot?.mcpTools.find((tool) => tool.capabilityId === capabilityId && tool.name === toolName);
|
|
43
|
+
}
|
|
44
|
+
function pinnedTool(manifest, capabilityId, toolName) {
|
|
45
|
+
const capability = manifest.capabilities.find((item) => item.id === capabilityId);
|
|
46
|
+
return capability?.tools?.find((tool) => tool.name === toolName);
|
|
47
|
+
}
|
|
48
|
+
function acceptedToolResult(action, command) {
|
|
49
|
+
if (action.kind !== "tool" || !action.outputSchema)
|
|
50
|
+
return command;
|
|
51
|
+
const value = command.outcome.value;
|
|
52
|
+
if (isFailedToolValue(value))
|
|
53
|
+
return command;
|
|
54
|
+
// Executors wrap successful tool output as `{ kind: "completed", output }`.
|
|
55
|
+
// Validate the tool payload, not the outcome envelope.
|
|
56
|
+
const candidate = completedToolOutput(value);
|
|
57
|
+
let matches = false;
|
|
58
|
+
try {
|
|
59
|
+
matches = schemaFromJSON(action.outputSchema).validate(candidate).ok;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
matches = false;
|
|
63
|
+
}
|
|
64
|
+
if (matches)
|
|
65
|
+
return command;
|
|
66
|
+
return {
|
|
67
|
+
...command,
|
|
68
|
+
outcome: {
|
|
69
|
+
...command.outcome,
|
|
70
|
+
value: {
|
|
71
|
+
kind: "failed",
|
|
72
|
+
code: "tool.invalid-output",
|
|
73
|
+
message: "Tool result does not match the output schema stored on the action",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function completedToolOutput(value) {
|
|
79
|
+
if (value &&
|
|
80
|
+
typeof value === "object" &&
|
|
81
|
+
!Array.isArray(value) &&
|
|
82
|
+
value.kind === "completed" &&
|
|
83
|
+
"output" in value) {
|
|
84
|
+
return value.output;
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
function isFailedToolValue(value) {
|
|
89
|
+
return (!!value &&
|
|
90
|
+
typeof value === "object" &&
|
|
91
|
+
!Array.isArray(value) &&
|
|
92
|
+
value.kind === "failed");
|
|
93
|
+
}
|
|
20
94
|
const semantic = (value) => {
|
|
21
95
|
const { requestId: _, ...body } = value;
|
|
22
96
|
return canonical(body);
|
|
23
97
|
};
|
|
98
|
+
const sessionIdentity = (value) => {
|
|
99
|
+
const { requestId: _requestId, vaultIds: _vaultIds, credentialSelections: _selections, ...body } = value ?? {};
|
|
100
|
+
return canonical(body);
|
|
101
|
+
};
|
|
24
102
|
const equals = (a, b) => {
|
|
25
103
|
const aa = Buffer.from(a), bb = Buffer.from(b);
|
|
26
104
|
return aa.length === bb.length && timingSafeEqual(aa, bb);
|
|
@@ -28,10 +106,14 @@ const equals = (a, b) => {
|
|
|
28
106
|
export class CoreRuntime {
|
|
29
107
|
options;
|
|
30
108
|
store;
|
|
109
|
+
vault;
|
|
110
|
+
kek;
|
|
111
|
+
kekPath;
|
|
31
112
|
running = new Map();
|
|
32
113
|
pending = new Set();
|
|
33
114
|
observers = new Map();
|
|
34
115
|
executors = new Set();
|
|
116
|
+
mcp;
|
|
35
117
|
server;
|
|
36
118
|
closing = false;
|
|
37
119
|
lockPath;
|
|
@@ -49,9 +131,8 @@ export class CoreRuntime {
|
|
|
49
131
|
if (options.executors.some((e) => e.token.length < 16 ||
|
|
50
132
|
equals(e.token, options.serverToken) ||
|
|
51
133
|
!e.agentId ||
|
|
52
|
-
!e.manifestHash ||
|
|
53
134
|
!e.implementationVersion))
|
|
54
|
-
throw new Error("Executor tokens require independent credentials and
|
|
135
|
+
throw new Error("Executor tokens require independent credentials and an agent id");
|
|
55
136
|
if (options.sqlitePath !== ":memory:") {
|
|
56
137
|
mkdirSync(dirname(options.sqlitePath), { recursive: true });
|
|
57
138
|
this.lockPath = options.sqlitePath + ".runtime-lock";
|
|
@@ -78,14 +159,29 @@ export class CoreRuntime {
|
|
|
78
159
|
writeFileSync(fd, String(process.pid));
|
|
79
160
|
closeSync(fd);
|
|
80
161
|
}
|
|
162
|
+
this.kekPath = options.vaultKekPath ?? defaultKekPath();
|
|
163
|
+
let store;
|
|
81
164
|
try {
|
|
82
|
-
|
|
165
|
+
store = new Store(options.sqlitePath);
|
|
166
|
+
this.kek = readVaultKek({
|
|
167
|
+
vaultKek: options.vaultKek,
|
|
168
|
+
vaultKekPath: this.kekPath,
|
|
169
|
+
});
|
|
170
|
+
if (store.credentialCount() > 0 && !this.kek)
|
|
171
|
+
throw new Error("Vault key-encryption key is required to open this database");
|
|
172
|
+
this.store = store;
|
|
83
173
|
}
|
|
84
174
|
catch (e) {
|
|
175
|
+
store?.db.close();
|
|
85
176
|
if (this.lockPath)
|
|
86
177
|
unlinkSync(this.lockPath);
|
|
87
178
|
throw e;
|
|
88
179
|
}
|
|
180
|
+
this.vault = new VaultService(this.store.db, (fn) => this.store.tx(fn), () => this.ensureKek(), options.vaultFetch ?? globalThis.fetch);
|
|
181
|
+
this.mcp = new McpPool({
|
|
182
|
+
dataDir: dirname(options.sqlitePath),
|
|
183
|
+
authorize: (sessionId, request) => this.authorize(sessionId, request),
|
|
184
|
+
});
|
|
89
185
|
this.store.tx(() => {
|
|
90
186
|
for (const effect of this.store.all("effects"))
|
|
91
187
|
if (effect.status === "invoking") {
|
|
@@ -166,10 +262,13 @@ export class CoreRuntime {
|
|
|
166
262
|
const controller = new AbortController();
|
|
167
263
|
this.running.set(id, controller);
|
|
168
264
|
try {
|
|
265
|
+
await this.prepareMcp(id, controller.signal);
|
|
266
|
+
const current = this.session(id);
|
|
169
267
|
const result = await runDurable({
|
|
170
|
-
manifest:
|
|
171
|
-
checkpoint:
|
|
268
|
+
manifest: current.manifest,
|
|
269
|
+
checkpoint: current.checkpoint,
|
|
172
270
|
signal: controller.signal,
|
|
271
|
+
sessionTools: sessionToolsOf(current.mcpSnapshot),
|
|
173
272
|
host: {
|
|
174
273
|
resolveEffect: (e) => this.resolveEffect(e, controller.signal),
|
|
175
274
|
},
|
|
@@ -259,7 +358,7 @@ export class CoreRuntime {
|
|
|
259
358
|
}
|
|
260
359
|
}
|
|
261
360
|
async resolveEffect(request, signal) {
|
|
262
|
-
let invoke
|
|
361
|
+
let invoke;
|
|
263
362
|
let notify = false;
|
|
264
363
|
let event;
|
|
265
364
|
const resolution = this.store.tx(() => {
|
|
@@ -281,14 +380,20 @@ export class CoreRuntime {
|
|
|
281
380
|
: "pending",
|
|
282
381
|
};
|
|
283
382
|
}
|
|
383
|
+
const mcpTool = request.kind === "tool"
|
|
384
|
+
? mcpToolOf(s, request.capabilityId, request.toolName)
|
|
385
|
+
: undefined;
|
|
284
386
|
this.store.put("effects", request.effectId, {
|
|
285
387
|
request,
|
|
286
|
-
status: request.kind === "model" ? "invoking" : "pending",
|
|
388
|
+
status: request.kind === "model" || mcpTool ? "invoking" : "pending",
|
|
287
389
|
});
|
|
288
|
-
if (request.kind === "model") {
|
|
289
|
-
invoke =
|
|
390
|
+
if (request.kind === "model" || mcpTool) {
|
|
391
|
+
invoke = mcpTool ? "mcp" : "model";
|
|
290
392
|
return undefined;
|
|
291
393
|
}
|
|
394
|
+
const tool = request.kind === "tool"
|
|
395
|
+
? pinnedTool(s.manifest, request.capabilityId, request.toolName)
|
|
396
|
+
: undefined;
|
|
292
397
|
const action = {
|
|
293
398
|
actionId: request.effectId,
|
|
294
399
|
sessionId: request.sessionId,
|
|
@@ -299,6 +404,8 @@ export class CoreRuntime {
|
|
|
299
404
|
kind: request.kind,
|
|
300
405
|
capabilityId: request.capabilityId,
|
|
301
406
|
...(request.toolName ? { toolName: request.toolName } : {}),
|
|
407
|
+
...(tool?.inputSchema ? { inputSchema: tool.inputSchema } : {}),
|
|
408
|
+
...(tool?.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
302
409
|
input: request.input,
|
|
303
410
|
context: request.context,
|
|
304
411
|
status: "pending",
|
|
@@ -323,7 +430,9 @@ export class CoreRuntime {
|
|
|
323
430
|
if (!invoke)
|
|
324
431
|
return resolution;
|
|
325
432
|
try {
|
|
326
|
-
const value =
|
|
433
|
+
const value = invoke === "mcp"
|
|
434
|
+
? await this.callMcpTool(request)
|
|
435
|
+
: await (this.options.model ?? scriptedModel())(request, signal);
|
|
327
436
|
return this.store.tx(() => {
|
|
328
437
|
const s = this.session(request.sessionId);
|
|
329
438
|
if (s.status === "cancelled" ||
|
|
@@ -367,10 +476,7 @@ export class CoreRuntime {
|
|
|
367
476
|
return executor;
|
|
368
477
|
}
|
|
369
478
|
scoped(scope, action) {
|
|
370
|
-
if (scope === "server" ||
|
|
371
|
-
scope.agentId !== action.agentId ||
|
|
372
|
-
scope.manifestHash !== action.manifestHash ||
|
|
373
|
-
scope.implementationVersion !== action.implementationVersion)
|
|
479
|
+
if (scope === "server" || scope.agentId !== action.agentId)
|
|
374
480
|
fail(403, "Executor scope does not authorize this action");
|
|
375
481
|
}
|
|
376
482
|
async body(request) {
|
|
@@ -387,21 +493,23 @@ export class CoreRuntime {
|
|
|
387
493
|
return fail(400, "Invalid JSON");
|
|
388
494
|
}
|
|
389
495
|
}
|
|
390
|
-
command(id,
|
|
496
|
+
command(id, input, scope) {
|
|
391
497
|
let event;
|
|
392
498
|
let schedule = false;
|
|
393
499
|
const response = this.store.tx(() => {
|
|
394
500
|
const s = this.session(id);
|
|
395
|
-
|
|
501
|
+
let command = input;
|
|
396
502
|
if (command.type === "action_result") {
|
|
397
503
|
const a = this.store.get("actions", command.actionId) ??
|
|
398
504
|
fail(404, "Action not found");
|
|
399
505
|
if (a.sessionId !== id)
|
|
400
506
|
fail(403, "Action belongs to another session");
|
|
401
507
|
this.scoped(scope, a);
|
|
508
|
+
command = acceptedToolResult(a, command);
|
|
402
509
|
}
|
|
403
510
|
else if (scope !== "server")
|
|
404
511
|
fail(403, "Application credential required");
|
|
512
|
+
const key = JSON.stringify([id, command.idempotencyKey]);
|
|
405
513
|
const existing = this.store.get("commands", key);
|
|
406
514
|
if (existing) {
|
|
407
515
|
if (semantic(existing.command) !== semantic(command))
|
|
@@ -544,7 +652,7 @@ export class CoreRuntime {
|
|
|
544
652
|
});
|
|
545
653
|
if (event)
|
|
546
654
|
this.publish(event);
|
|
547
|
-
if (
|
|
655
|
+
if (input.type === "cancel")
|
|
548
656
|
this.running.get(id)?.abort();
|
|
549
657
|
if (schedule)
|
|
550
658
|
this.schedule(id);
|
|
@@ -612,10 +720,7 @@ export class CoreRuntime {
|
|
|
612
720
|
return json({
|
|
613
721
|
actions: this.store
|
|
614
722
|
.all("actions")
|
|
615
|
-
.filter((a) => a.status === "pending" &&
|
|
616
|
-
a.agentId === scope.agentId &&
|
|
617
|
-
a.manifestHash === scope.manifestHash &&
|
|
618
|
-
a.implementationVersion === scope.implementationVersion),
|
|
723
|
+
.filter((a) => a.status === "pending" && a.agentId === scope.agentId),
|
|
619
724
|
});
|
|
620
725
|
const actionId = path[2];
|
|
621
726
|
if (!actionId)
|
|
@@ -627,10 +732,7 @@ export class CoreRuntime {
|
|
|
627
732
|
fail(404, "Action not found");
|
|
628
733
|
this.scoped(scope, action);
|
|
629
734
|
if (method === "POST" && path[3] === "claim") {
|
|
630
|
-
|
|
631
|
-
if (claim.manifestHash !== action.manifestHash ||
|
|
632
|
-
claim.implementationVersion !== action.implementationVersion)
|
|
633
|
-
fail(409, "Definition mismatch");
|
|
735
|
+
ActionClaimRequestSchema.parse(body);
|
|
634
736
|
if (action.status !== "pending" ||
|
|
635
737
|
this.session(action.sessionId).status === "cancelled" ||
|
|
636
738
|
this.session(action.sessionId).activeTurnId !== action.turnId)
|
|
@@ -671,13 +773,13 @@ export class CoreRuntime {
|
|
|
671
773
|
path[3] === "commands" &&
|
|
672
774
|
method === "POST")
|
|
673
775
|
return json(this.command(path[2], SessionCommandSchema.parse(await this.body(request)), scope));
|
|
776
|
+
if (path[1] === "vaults")
|
|
777
|
+
return json(await this.dispatchVault(scope, method, path, url, request));
|
|
674
778
|
if (scope !== "server")
|
|
675
779
|
fail(403, "Application credential required");
|
|
676
780
|
if (path[1] === "agents" && path.length === 2 && method === "GET")
|
|
677
781
|
return json({
|
|
678
|
-
agents: this.store
|
|
679
|
-
.all("definitions")
|
|
680
|
-
.map((d) => ({
|
|
782
|
+
agents: this.store.all("definitions").map((d) => ({
|
|
681
783
|
agentId: d.manifest.id,
|
|
682
784
|
manifest: d.manifest,
|
|
683
785
|
manifestHash: d.manifestHash,
|
|
@@ -723,11 +825,19 @@ export class CoreRuntime {
|
|
|
723
825
|
const id = path[2];
|
|
724
826
|
if (method === "PUT" && path.length === 3) {
|
|
725
827
|
const body = PutSessionRequestSchema.parse(await this.body(request));
|
|
828
|
+
const vaultIds = body.vaultIds ?? [];
|
|
829
|
+
const credentialSelections = body.credentialSelections ?? [];
|
|
726
830
|
const result = this.store.tx(() => {
|
|
831
|
+
this.vault.assertAttachment(body.ownerUserId, vaultIds, credentialSelections);
|
|
727
832
|
const prior = this.store.get("sessions", id);
|
|
728
833
|
if (prior) {
|
|
729
|
-
if (
|
|
834
|
+
if (sessionIdentity(prior.creation) !== sessionIdentity(body))
|
|
730
835
|
fail(409, "Session already exists with different creation parameters");
|
|
836
|
+
prior.vaultIds = vaultIds;
|
|
837
|
+
prior.credentialSelections = credentialSelections;
|
|
838
|
+
prior.creation = body;
|
|
839
|
+
this.store.put("sessions", id, prior);
|
|
840
|
+
this.vault.recordAttachment(id, vaultIds);
|
|
731
841
|
return prior;
|
|
732
842
|
}
|
|
733
843
|
const definition = this.store.get("definitions", body.agentId) ??
|
|
@@ -743,8 +853,12 @@ export class CoreRuntime {
|
|
|
743
853
|
status: "idle",
|
|
744
854
|
activeTurnId: null,
|
|
745
855
|
creation: body,
|
|
856
|
+
vaultIds,
|
|
857
|
+
credentialSelections,
|
|
858
|
+
pluginRoots: definition.pluginRoots ?? {},
|
|
746
859
|
};
|
|
747
860
|
this.store.put("sessions", id, s);
|
|
861
|
+
this.vault.recordAttachment(id, vaultIds);
|
|
748
862
|
return s;
|
|
749
863
|
});
|
|
750
864
|
return json(this.view(result));
|
|
@@ -775,7 +889,7 @@ export class CoreRuntime {
|
|
|
775
889
|
response.end();
|
|
776
890
|
return;
|
|
777
891
|
}
|
|
778
|
-
const status = error instanceof HttpError
|
|
892
|
+
const status = error instanceof HttpError || error instanceof VaultError
|
|
779
893
|
? error.status
|
|
780
894
|
: error?.name === "ZodError" ||
|
|
781
895
|
error?.message === "Invalid cursor"
|
|
@@ -799,6 +913,10 @@ export class CoreRuntime {
|
|
|
799
913
|
implementationVersion: s.implementationVersion,
|
|
800
914
|
status: s.status,
|
|
801
915
|
activeTurnId: s.activeTurnId,
|
|
916
|
+
vaultIds: s.vaultIds ?? [],
|
|
917
|
+
credentialSelections: s.credentialSelections ?? [],
|
|
918
|
+
mcpSnapshot: s.mcpSnapshot ?? null,
|
|
919
|
+
mcpDiagnostics: s.mcpDiagnostics ?? [],
|
|
802
920
|
waits: Array.isArray(s.waits)
|
|
803
921
|
? s.waits.map((call) => ({
|
|
804
922
|
invocationId: call.invocationId,
|
|
@@ -823,6 +941,136 @@ export class CoreRuntime {
|
|
|
823
941
|
})),
|
|
824
942
|
};
|
|
825
943
|
}
|
|
944
|
+
async prepareMcp(id, signal) {
|
|
945
|
+
const s = this.session(id);
|
|
946
|
+
const declared = s.manifest.capabilities.some((capability) => capability.mcpServers !== undefined &&
|
|
947
|
+
Object.keys(capability.mcpServers).length > 0);
|
|
948
|
+
if (!declared)
|
|
949
|
+
return;
|
|
950
|
+
if (!s.mcpSnapshot) {
|
|
951
|
+
const found = await this.mcp.discover({
|
|
952
|
+
sessionId: id,
|
|
953
|
+
manifest: s.manifest,
|
|
954
|
+
manifestHash: s.manifestHash,
|
|
955
|
+
pluginRoots: s.pluginRoots ?? {},
|
|
956
|
+
signal,
|
|
957
|
+
});
|
|
958
|
+
this.store.tx(() => {
|
|
959
|
+
const current = this.session(id);
|
|
960
|
+
if (current.mcpSnapshot)
|
|
961
|
+
return;
|
|
962
|
+
current.mcpSnapshot = found.snapshot;
|
|
963
|
+
current.mcpDiagnostics = found.diagnostics;
|
|
964
|
+
this.store.put("sessions", id, current);
|
|
965
|
+
});
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
const diagnostics = await this.mcp.reconnect({
|
|
969
|
+
sessionId: id,
|
|
970
|
+
manifest: s.manifest,
|
|
971
|
+
pluginRoots: s.pluginRoots ?? {},
|
|
972
|
+
tools: s.mcpSnapshot.mcpTools,
|
|
973
|
+
signal,
|
|
974
|
+
});
|
|
975
|
+
if (diagnostics.length === 0)
|
|
976
|
+
return;
|
|
977
|
+
this.store.tx(() => {
|
|
978
|
+
const current = this.session(id);
|
|
979
|
+
const prior = [...(current.mcpDiagnostics ?? [])];
|
|
980
|
+
for (const item of diagnostics) {
|
|
981
|
+
const index = prior.findIndex((existing) => existing.capabilityId === item.capabilityId &&
|
|
982
|
+
existing.serverName === item.serverName);
|
|
983
|
+
if (index >= 0)
|
|
984
|
+
prior[index] = item;
|
|
985
|
+
else
|
|
986
|
+
prior.push(item);
|
|
987
|
+
}
|
|
988
|
+
current.mcpDiagnostics = prior;
|
|
989
|
+
this.store.put("sessions", id, current);
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
async callMcpTool(request) {
|
|
993
|
+
const s = this.session(request.sessionId);
|
|
994
|
+
const tool = mcpToolOf(s, request.capabilityId, request.toolName);
|
|
995
|
+
if (!tool)
|
|
996
|
+
throw new Error(`MCP tool '${request.toolName ?? ""}' is not in the session snapshot`);
|
|
997
|
+
return this.mcp.call({
|
|
998
|
+
sessionId: s.id,
|
|
999
|
+
capabilityId: tool.capabilityId,
|
|
1000
|
+
serverName: tool.serverName,
|
|
1001
|
+
serverToolName: tool.serverToolName,
|
|
1002
|
+
args: request.input,
|
|
1003
|
+
manifest: s.manifest,
|
|
1004
|
+
pluginRoots: s.pluginRoots ?? {},
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
async authorize(sessionId, request) {
|
|
1008
|
+
const s = this.session(sessionId);
|
|
1009
|
+
const result = await this.vault.authorize({
|
|
1010
|
+
sessionId,
|
|
1011
|
+
vaultIds: s.vaultIds ?? [],
|
|
1012
|
+
credentialSelections: s.credentialSelections ?? [],
|
|
1013
|
+
url: request.url,
|
|
1014
|
+
serverName: request.serverName,
|
|
1015
|
+
});
|
|
1016
|
+
if (result.status === "authorized") {
|
|
1017
|
+
const token = result.headers.authorization.slice("Bearer ".length);
|
|
1018
|
+
scrub({ authorization: result.headers.authorization, url: result.url }, [
|
|
1019
|
+
token,
|
|
1020
|
+
]);
|
|
1021
|
+
}
|
|
1022
|
+
return result;
|
|
1023
|
+
}
|
|
1024
|
+
ensureKek() {
|
|
1025
|
+
if (this.kek)
|
|
1026
|
+
return this.kek;
|
|
1027
|
+
if (this.options.vaultKek === null)
|
|
1028
|
+
throw new Error("Vault key-encryption key is required");
|
|
1029
|
+
this.kek = createKekFile(this.kekPath);
|
|
1030
|
+
return this.kek;
|
|
1031
|
+
}
|
|
1032
|
+
async dispatchVault(scope, method, path, url, request) {
|
|
1033
|
+
if (scope !== "server") {
|
|
1034
|
+
this.vault.reject(path.join("/"));
|
|
1035
|
+
fail(403, "Application credential required");
|
|
1036
|
+
}
|
|
1037
|
+
if (path.length === 2 && method === "POST") {
|
|
1038
|
+
const body = CreateVaultRequestSchema.parse(await this.body(request));
|
|
1039
|
+
return this.vault.createVault(body);
|
|
1040
|
+
}
|
|
1041
|
+
if (path.length === 2 && method === "GET") {
|
|
1042
|
+
const ownerUserId = url.searchParams.get("ownerUserId") ??
|
|
1043
|
+
fail(400, "ownerUserId is required");
|
|
1044
|
+
return { vaults: this.vault.listVaults(ownerUserId) };
|
|
1045
|
+
}
|
|
1046
|
+
const vaultId = path[2];
|
|
1047
|
+
if (!vaultId)
|
|
1048
|
+
fail(404, "Vault not found");
|
|
1049
|
+
if (path.length === 3 && method === "GET")
|
|
1050
|
+
return this.vault.getVault(vaultId);
|
|
1051
|
+
if (path.length === 3 && method === "DELETE")
|
|
1052
|
+
return this.vault.deleteVault(vaultId);
|
|
1053
|
+
if (path[3] !== "credentials")
|
|
1054
|
+
fail(404, "Route not found");
|
|
1055
|
+
if (path.length === 4 && method === "POST") {
|
|
1056
|
+
const body = CreateCredentialRequestSchema.parse(await this.body(request));
|
|
1057
|
+
return this.vault.createCredential(vaultId, body);
|
|
1058
|
+
}
|
|
1059
|
+
if (path.length === 4 && method === "GET")
|
|
1060
|
+
return { credentials: this.vault.listCredentials(vaultId) };
|
|
1061
|
+
const credentialId = path[4];
|
|
1062
|
+
if (!credentialId)
|
|
1063
|
+
fail(404, "Credential not found");
|
|
1064
|
+
if (path.length === 5 && method === "GET")
|
|
1065
|
+
return this.vault.getCredential(vaultId, credentialId);
|
|
1066
|
+
if (path.length === 5 && method === "POST") {
|
|
1067
|
+
const body = RotateCredentialRequestSchema.parse(await this.body(request));
|
|
1068
|
+
return this.vault.rotateCredential(vaultId, credentialId, body);
|
|
1069
|
+
}
|
|
1070
|
+
if (path.length === 5 && method === "DELETE")
|
|
1071
|
+
return this.vault.deleteCredential(vaultId, credentialId);
|
|
1072
|
+
fail(404, "Route not found");
|
|
1073
|
+
}
|
|
826
1074
|
async listen(port = 8787, hostname = "127.0.0.1") {
|
|
827
1075
|
if (this.server)
|
|
828
1076
|
throw new Error("Already listening");
|
|
@@ -843,6 +1091,7 @@ export class CoreRuntime {
|
|
|
843
1091
|
clearInterval(this.timer);
|
|
844
1092
|
for (const c of this.running.values())
|
|
845
1093
|
c.abort();
|
|
1094
|
+
await this.mcp.close();
|
|
846
1095
|
for (const r of this.executors)
|
|
847
1096
|
r.end();
|
|
848
1097
|
for (const set of this.observers.values())
|
package/dist/core/store.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare class Store {
|
|
|
9
9
|
all<T = any>(table: string): T[];
|
|
10
10
|
put(table: string, id: string, body: unknown): void;
|
|
11
11
|
event(sessionId: string, turnId: string | null, type: string, payload: unknown): LiveEvent;
|
|
12
|
+
credentialCount(): number;
|
|
12
13
|
history(sessionId: string, cursor?: string): {
|
|
13
14
|
items: LiveEvent[];
|
|
14
15
|
cursor: string | null;
|
package/dist/core/store.js
CHANGED
|
@@ -26,7 +26,46 @@ export class Store {
|
|
|
26
26
|
CREATE TABLE IF NOT EXISTS effects(id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
|
27
27
|
CREATE TABLE IF NOT EXISTS actions(id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
|
28
28
|
CREATE TABLE IF NOT EXISTS events(sequence INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, body TEXT NOT NULL);
|
|
29
|
-
CREATE INDEX IF NOT EXISTS events_session ON events(session_id, sequence)
|
|
29
|
+
CREATE INDEX IF NOT EXISTS events_session ON events(session_id, sequence);
|
|
30
|
+
CREATE TABLE IF NOT EXISTS vaults(
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
name TEXT NOT NULL,
|
|
33
|
+
owner_user_id TEXT NOT NULL,
|
|
34
|
+
metadata_json TEXT,
|
|
35
|
+
created_at TEXT NOT NULL
|
|
36
|
+
);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS vaults_owner ON vaults(owner_user_id);
|
|
38
|
+
CREATE TABLE IF NOT EXISTS vault_credentials(
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
vault_id TEXT NOT NULL REFERENCES vaults(id) ON DELETE CASCADE,
|
|
41
|
+
name TEXT NOT NULL,
|
|
42
|
+
type TEXT NOT NULL,
|
|
43
|
+
binding_json TEXT NOT NULL,
|
|
44
|
+
expires_at TEXT,
|
|
45
|
+
created_at TEXT NOT NULL,
|
|
46
|
+
rotated_at TEXT,
|
|
47
|
+
kek_id TEXT NOT NULL,
|
|
48
|
+
nonce BLOB NOT NULL,
|
|
49
|
+
ciphertext BLOB NOT NULL,
|
|
50
|
+
wrapped_dek BLOB NOT NULL
|
|
51
|
+
);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS vault_credentials_vault ON vault_credentials(vault_id);
|
|
53
|
+
CREATE TABLE IF NOT EXISTS vault_audit(
|
|
54
|
+
id TEXT PRIMARY KEY,
|
|
55
|
+
at TEXT NOT NULL,
|
|
56
|
+
actor TEXT NOT NULL,
|
|
57
|
+
action TEXT NOT NULL,
|
|
58
|
+
vault_id TEXT,
|
|
59
|
+
credential_id TEXT,
|
|
60
|
+
session_id TEXT,
|
|
61
|
+
target TEXT,
|
|
62
|
+
outcome TEXT NOT NULL
|
|
63
|
+
);
|
|
64
|
+
CREATE TABLE IF NOT EXISTS vault_idempotency(
|
|
65
|
+
id TEXT PRIMARY KEY,
|
|
66
|
+
body_hash TEXT NOT NULL,
|
|
67
|
+
response TEXT NOT NULL
|
|
68
|
+
);`);
|
|
30
69
|
}
|
|
31
70
|
tx(fn) {
|
|
32
71
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -76,6 +115,12 @@ export class Store {
|
|
|
76
115
|
.run(JSON.stringify(event), result.lastInsertRowid);
|
|
77
116
|
return event;
|
|
78
117
|
}
|
|
118
|
+
credentialCount() {
|
|
119
|
+
const row = this.db
|
|
120
|
+
.prepare("SELECT COUNT(*) AS n FROM vault_credentials")
|
|
121
|
+
.get();
|
|
122
|
+
return Number(row.n);
|
|
123
|
+
}
|
|
79
124
|
history(sessionId, cursor) {
|
|
80
125
|
let sequence = 0;
|
|
81
126
|
if (cursor) {
|