@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 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, manifestHash, implementationVersion}`. Each secret must differ from the application server key and every other executor token. The manifest hash is available from `@nylorun/core/compatibility` or saved definition registration. SDK clients use `NYLORUN_RUNTIME_URL`, `NYLORUN_SERVER_KEY`, and `NYLORUN_EXECUTOR_KEY`; runtime executor scope is configured explicitly, never broadened by client input.
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`.
@@ -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
  }>;
@@ -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 exact definition scope");
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
- this.store = new Store(options.sqlitePath);
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: s.manifest,
171
- checkpoint: s.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 = false;
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 = true;
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 = await (this.options.model ?? scriptedModel())(request, signal);
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, command, scope) {
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
- const key = JSON.stringify([id, command.idempotencyKey]);
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 (command.type === "cancel")
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
- const claim = ActionClaimRequestSchema.parse(body);
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 (semantic(prior.creation) !== semantic(body))
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())
@@ -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;
@@ -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) {