@nylorun/runtime 0.5.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +30 -60
  3. package/dist/config.d.ts +1 -1
  4. package/dist/configuration.d.ts +3 -0
  5. package/dist/configuration.js +3 -0
  6. package/dist/contracts.d.ts +4 -2
  7. package/dist/core/main.js +40 -0
  8. package/dist/core/provider.d.ts +12 -0
  9. package/dist/core/provider.js +60 -0
  10. package/dist/core/runtime.d.ts +67 -0
  11. package/dist/core/runtime.js +1126 -0
  12. package/dist/core/store.d.ts +17 -0
  13. package/dist/core/store.js +146 -0
  14. package/dist/index.d.ts +6 -11
  15. package/dist/index.js +4 -6
  16. package/dist/mcp/connect.d.ts +31 -0
  17. package/dist/mcp/connect.js +190 -0
  18. package/dist/mcp/pool.d.ts +45 -0
  19. package/dist/mcp/pool.js +143 -0
  20. package/dist/mcp/snapshot.d.ts +25 -0
  21. package/dist/mcp/snapshot.js +10 -0
  22. package/dist/mcp/stdio.d.ts +15 -0
  23. package/dist/mcp/stdio.js +80 -0
  24. package/dist/model/defaults.d.ts +1 -1
  25. package/dist/model/http-model.d.ts +1 -1
  26. package/dist/node/local-sessions.js +17 -0
  27. package/dist/server/host.d.ts +42 -4
  28. package/dist/server/host.js +69 -3
  29. package/dist/session/api.d.ts +10 -0
  30. package/dist/session/api.js +15 -0
  31. package/dist/session/default.d.ts +5 -0
  32. package/dist/session/default.js +30 -0
  33. package/dist/session/handle.d.ts +27 -0
  34. package/dist/session/handle.js +199 -0
  35. package/dist/session/index.d.ts +2 -0
  36. package/dist/session/index.js +2 -0
  37. package/dist/sessions/host.d.ts +11 -1
  38. package/dist/sessions/host.js +75 -7
  39. package/dist/sessions/store.d.ts +1 -0
  40. package/dist/sessions/store.js +3 -0
  41. package/dist/vault/crypto.d.ts +12 -0
  42. package/dist/vault/crypto.js +80 -0
  43. package/dist/vault/error.d.ts +4 -0
  44. package/dist/vault/error.js +7 -0
  45. package/dist/vault/kek.d.ts +6 -0
  46. package/dist/vault/kek.js +26 -0
  47. package/dist/vault/service.d.ts +59 -0
  48. package/dist/vault/service.js +538 -0
  49. package/dist/vault/url.d.ts +3 -0
  50. package/dist/vault/url.js +27 -0
  51. package/package.json +19 -13
  52. package/dist/cli.d.ts +0 -2
  53. package/dist/cli.js +0 -100
  54. package/dist/dev-entry.js +0 -2
  55. package/dist/dev.d.ts +0 -2
  56. package/dist/dev.js +0 -126
  57. package/dist/environment.d.ts +0 -2
  58. package/dist/environment.js +0 -64
  59. package/dist/launcher.d.ts +0 -1
  60. package/dist/launcher.js +0 -33
  61. package/dist/model/configure.d.ts +0 -12
  62. package/dist/model/configure.js +0 -155
  63. /package/dist/{dev-entry.d.ts → core/main.d.ts} +0 -0
@@ -1,4 +1,5 @@
1
- import { isHarnessError } from "@nylorun/harness";
1
+ import { HarnessError, isHarnessError } from "@nylorun/core/define";
2
+ import { bindingFromAgent, createRunState, run, } from "@nylorun/harness/run";
2
3
  import { scrub } from "../redact.js";
3
4
  /** The built-in single-process coordinator. Storage adapters do not implement execution. */
4
5
  export class SessionHost {
@@ -26,6 +27,19 @@ export class SessionHost {
26
27
  ? { ...session, status: "interrupted" }
27
28
  : session);
28
29
  }
30
+ async delete(agentId, sessionId) {
31
+ const key = this.key(agentId, sessionId);
32
+ if (this.active.has(key))
33
+ throw new Error("Cannot delete a session while a run is active");
34
+ if ("delete" in this.store && typeof this.store.delete === "function")
35
+ return this.store.delete(agentId, sessionId);
36
+ const existing = await this.store.get(agentId, sessionId);
37
+ if (!existing)
38
+ return false;
39
+ // Stores without delete: archive by removing via put tombstone is not supported;
40
+ // require ManagedSessionStore.delete or memorySessions.delete.
41
+ throw new Error("Session store does not support delete");
42
+ }
29
43
  subscribe(agentId, sessionId, listener) {
30
44
  const key = this.key(agentId, sessionId);
31
45
  let listeners = this.listeners.get(key);
@@ -144,17 +158,21 @@ export class SessionHost {
144
158
  ...(typeof input === "string" ? { input } : {}),
145
159
  });
146
160
  await save();
147
- const result = await agent.run({
148
- state: document.state,
149
- input,
161
+ const binding = bindingFromAgent(agent);
162
+ const engineInput = resolveEngineInput(document.state, input);
163
+ const state = ensureEngineState(binding, document.state, options.sessionState);
164
+ const result = await run({
165
+ binding,
166
+ state,
167
+ input: engineInput,
150
168
  info: options.info,
151
169
  signal: controller.signal,
152
170
  onModelCall: options.onModelCall,
153
171
  onEvent: (event) => {
154
172
  add(event.type, event);
155
173
  },
156
- record: async (state) => {
157
- document = { ...document, state };
174
+ record: async (next) => {
175
+ document = { ...document, state: next };
158
176
  await save();
159
177
  },
160
178
  });
@@ -235,7 +253,9 @@ export class SessionHost {
235
253
  throw new Error("This session was interrupted; reconcile its external effects before cancellation.");
236
254
  if (stored?.state?.status !== "paused")
237
255
  return;
238
- const result = await agent.run({
256
+ const binding = bindingFromAgent(agent);
257
+ const result = await run({
258
+ binding,
239
259
  state: stored.state,
240
260
  input: { kind: "continue" },
241
261
  signal: AbortSignal.abort(new Error("Paused execution cancelled")),
@@ -289,3 +309,51 @@ export class SessionHost {
289
309
  await this.store.close();
290
310
  }
291
311
  }
312
+ function ensureEngineState(binding, existing, sessionState) {
313
+ if (existing) {
314
+ if (!sessionState || Object.keys(existing.state ?? {}).length > 0)
315
+ return existing;
316
+ return {
317
+ ...existing,
318
+ state: { ...(existing.state ?? {}), ...sessionState },
319
+ };
320
+ }
321
+ return createRunState(binding, {
322
+ ...(sessionState ? { state: sessionState } : {}),
323
+ });
324
+ }
325
+ /**
326
+ * Map DX `wait-resolve` onto harness approve/respond/settle using wait metadata.
327
+ * Engine `ExecutionInput` already includes `wait-resolve`; resume is wired via
328
+ * existing interaction/deferred kinds until full wait-id resume lands.
329
+ */
330
+ export function resolveEngineInput(state, input) {
331
+ if (typeof input !== "object" || !("kind" in input) || input.kind !== "wait-resolve")
332
+ return input;
333
+ const call = state?.plan?.calls.find((item) => item.wait?.waitId === input.waitId);
334
+ if (!call)
335
+ throw new HarnessError("execution.invalid-input", "No pending wait matches waitId");
336
+ if (call.status === "interaction" && call.interaction) {
337
+ if (call.interaction.kind === "approval")
338
+ return {
339
+ kind: "approve",
340
+ interactionId: call.interaction.id,
341
+ approved: input.value !== false && input.value !== "false",
342
+ };
343
+ return {
344
+ kind: "respond",
345
+ interactionId: call.interaction.id,
346
+ value: input.value ?? null,
347
+ };
348
+ }
349
+ if (call.status === "deferred")
350
+ return {
351
+ kind: "settle",
352
+ invocationId: call.invocationId,
353
+ outcome: {
354
+ kind: "completed",
355
+ output: input.value ?? null,
356
+ },
357
+ };
358
+ throw new HarnessError("execution.invalid-input", "Wait is no longer pending");
359
+ }
@@ -32,6 +32,7 @@ export interface SessionStore {
32
32
  get(agentId: string, sessionId: string): Promise<StoredSession | undefined>;
33
33
  put(agentId: string, sessionId: string, session: StoredSession): Promise<void>;
34
34
  list(agentId: string): Promise<readonly SessionSummary[]>;
35
+ delete?(agentId: string, sessionId: string): Promise<boolean>;
35
36
  }
36
37
  export interface ManagedSessionStore extends SessionStore {
37
38
  close(): Promise<void>;
@@ -26,5 +26,8 @@ export function memorySessions() {
26
26
  .map(sessionSummary)
27
27
  .sort((a, b) => b.startedAt - a.startedAt);
28
28
  },
29
+ async delete(agentId, sessionId) {
30
+ return documents.delete(key(agentId, sessionId));
31
+ },
29
32
  };
30
33
  }
@@ -0,0 +1,12 @@
1
+ export declare class VaultCryptoError extends Error {
2
+ constructor();
3
+ }
4
+ export declare function kekId(kek: Buffer): string;
5
+ export declare function parseKek(value: Buffer | string): Buffer;
6
+ export declare function encryptSecret(kek: Buffer, aad: Buffer, plaintext: Buffer): {
7
+ nonce: Buffer;
8
+ ciphertext: Buffer;
9
+ wrappedDek: Buffer;
10
+ kekId: string;
11
+ };
12
+ export declare function decryptSecret(kek: Buffer, aad: Buffer, nonce: Buffer, ciphertext: Buffer, wrappedDek: Buffer, expectedKekId: string): Buffer;
@@ -0,0 +1,80 @@
1
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, } from "node:crypto";
2
+ const IV_BYTES = 12;
3
+ const TAG_BYTES = 16;
4
+ export class VaultCryptoError extends Error {
5
+ constructor() {
6
+ super("Vault credential could not be read");
7
+ }
8
+ }
9
+ export function kekId(kek) {
10
+ return createHash("sha256").update(kek).digest("hex");
11
+ }
12
+ export function parseKek(value) {
13
+ const bytes = typeof value === "string" ? Buffer.from(value.trim(), "base64") : value;
14
+ if (bytes.length !== 32)
15
+ throw new Error("Vault key-encryption key must be 32 bytes");
16
+ return Buffer.from(bytes);
17
+ }
18
+ export function encryptSecret(kek, aad, plaintext) {
19
+ const dek = randomBytes(32);
20
+ try {
21
+ const nonce = randomBytes(IV_BYTES);
22
+ const cipher = createCipheriv("aes-256-gcm", dek, nonce);
23
+ cipher.setAAD(aad);
24
+ const body = Buffer.concat([cipher.update(plaintext), cipher.final()]);
25
+ const tag = cipher.getAuthTag();
26
+ return {
27
+ nonce,
28
+ ciphertext: Buffer.concat([body, tag]),
29
+ wrappedDek: wrapKey(kek, dek),
30
+ kekId: kekId(kek),
31
+ };
32
+ }
33
+ finally {
34
+ dek.fill(0);
35
+ }
36
+ }
37
+ export function decryptSecret(kek, aad, nonce, ciphertext, wrappedDek, expectedKekId) {
38
+ if (expectedKekId !== kekId(kek))
39
+ throw new VaultCryptoError();
40
+ const dek = unwrapKey(kek, wrappedDek);
41
+ try {
42
+ if (ciphertext.length < TAG_BYTES)
43
+ throw new VaultCryptoError();
44
+ const tag = ciphertext.subarray(ciphertext.length - TAG_BYTES);
45
+ const body = ciphertext.subarray(0, ciphertext.length - TAG_BYTES);
46
+ const decipher = createDecipheriv("aes-256-gcm", dek, nonce);
47
+ decipher.setAAD(aad);
48
+ decipher.setAuthTag(tag);
49
+ return Buffer.concat([decipher.update(body), decipher.final()]);
50
+ }
51
+ catch (error) {
52
+ if (error instanceof VaultCryptoError)
53
+ throw error;
54
+ throw new VaultCryptoError();
55
+ }
56
+ finally {
57
+ dek.fill(0);
58
+ }
59
+ }
60
+ function wrapKey(kek, dek) {
61
+ const nonce = randomBytes(IV_BYTES);
62
+ const cipher = createCipheriv("aes-256-gcm", kek, nonce);
63
+ const body = Buffer.concat([cipher.update(dek), cipher.final()]);
64
+ return Buffer.concat([nonce, body, cipher.getAuthTag()]);
65
+ }
66
+ function unwrapKey(kek, wrapped) {
67
+ try {
68
+ if (wrapped.length < IV_BYTES + TAG_BYTES)
69
+ throw new VaultCryptoError();
70
+ const nonce = wrapped.subarray(0, IV_BYTES);
71
+ const tag = wrapped.subarray(wrapped.length - TAG_BYTES);
72
+ const body = wrapped.subarray(IV_BYTES, wrapped.length - TAG_BYTES);
73
+ const decipher = createDecipheriv("aes-256-gcm", kek, nonce);
74
+ decipher.setAuthTag(tag);
75
+ return Buffer.concat([decipher.update(body), decipher.final()]);
76
+ }
77
+ catch {
78
+ throw new VaultCryptoError();
79
+ }
80
+ }
@@ -0,0 +1,4 @@
1
+ export declare class VaultError extends Error {
2
+ readonly status: number;
3
+ constructor(status: number, message: string);
4
+ }
@@ -0,0 +1,7 @@
1
+ export class VaultError extends Error {
2
+ status;
3
+ constructor(status, message) {
4
+ super(message);
5
+ this.status = status;
6
+ }
7
+ }
@@ -0,0 +1,6 @@
1
+ export declare function defaultKekPath(): string;
2
+ export declare function readVaultKek(options: {
3
+ vaultKek?: Buffer | string | null;
4
+ vaultKekPath?: string;
5
+ }): Buffer | undefined;
6
+ export declare function createKekFile(path: string): Buffer;
@@ -0,0 +1,26 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { parseKek } from "./crypto.js";
5
+ export function defaultKekPath() {
6
+ return join(process.cwd(), ".nylorun", "vault-kek");
7
+ }
8
+ export function readVaultKek(options) {
9
+ if (options.vaultKek === null)
10
+ return undefined;
11
+ if (options.vaultKek !== undefined)
12
+ return parseKek(options.vaultKek);
13
+ const fromEnv = process.env.NYLORUN_VAULT_KEK;
14
+ if (fromEnv)
15
+ return parseKek(fromEnv);
16
+ const path = options.vaultKekPath ?? defaultKekPath();
17
+ if (!existsSync(path))
18
+ return undefined;
19
+ return parseKek(readFileSync(path, "utf8"));
20
+ }
21
+ export function createKekFile(path) {
22
+ const bytes = randomBytes(32);
23
+ mkdirSync(dirname(path), { recursive: true });
24
+ writeFileSync(path, `${bytes.toString("base64")}\n`, { mode: 0o600 });
25
+ return bytes;
26
+ }
@@ -0,0 +1,59 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ import type { CreateCredentialRequest, CreateVaultRequest, CredentialInfo, CredentialSelection, RotateCredentialRequest, VaultInfo } from "@nylorun/core/contracts";
3
+ export type AuthorizeResult = {
4
+ status: "unauthenticated";
5
+ url: string;
6
+ headers: Record<string, string>;
7
+ } | {
8
+ status: "authorized";
9
+ url: string;
10
+ headers: {
11
+ authorization: string;
12
+ };
13
+ } | {
14
+ status: "refused";
15
+ url: string;
16
+ credentialIds: string[];
17
+ reason: string;
18
+ };
19
+ export declare class VaultService {
20
+ private readonly db;
21
+ private readonly tx;
22
+ private readonly kek;
23
+ private readonly fetchImpl;
24
+ constructor(db: DatabaseSync, tx: <T>(fn: () => T) => T, kek: () => Buffer, fetchImpl: typeof fetch);
25
+ createVault(body: CreateVaultRequest): VaultInfo;
26
+ listVaults(ownerUserId: string): VaultInfo[];
27
+ getVault(id: string): VaultInfo;
28
+ deleteVault(id: string): {
29
+ id: string;
30
+ };
31
+ createCredential(vaultId: string, body: CreateCredentialRequest): CredentialInfo;
32
+ listCredentials(vaultId: string): CredentialInfo[];
33
+ getCredential(vaultId: string, id: string): CredentialInfo;
34
+ rotateCredential(vaultId: string, id: string, body: RotateCredentialRequest): CredentialInfo;
35
+ deleteCredential(vaultId: string, id: string): {
36
+ id: string;
37
+ };
38
+ assertAttachment(ownerUserId: string, vaultIds: readonly string[], selections: readonly CredentialSelection[]): void;
39
+ recordAttachment(sessionId: string, vaultIds: readonly string[]): void;
40
+ authorize(input: {
41
+ sessionId: string;
42
+ vaultIds: readonly string[];
43
+ credentialSelections: readonly CredentialSelection[];
44
+ url: string;
45
+ serverName?: string;
46
+ }): Promise<AuthorizeResult>;
47
+ reject(route: string): void;
48
+ private refresh;
49
+ private replay;
50
+ private insertCredential;
51
+ private writePayload;
52
+ private readPayload;
53
+ private vaultInfo;
54
+ private vaultRow;
55
+ private credentialInfo;
56
+ private credentialRow;
57
+ private rowsForVault;
58
+ private audit;
59
+ }