@nylorun/runtime 0.10.0-beta → 0.11.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 (190) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +53 -51
  3. package/dist/adapters/execution/restate.d.ts +130 -0
  4. package/dist/adapters/execution/restate.js +442 -0
  5. package/dist/adapters/sandbox/virtual.js +1 -1
  6. package/dist/adapters/streams/s2.d.ts +18 -0
  7. package/dist/adapters/streams/s2.js +361 -0
  8. package/dist/core/executors.d.ts +4 -1
  9. package/dist/core/executors.js +20 -1
  10. package/dist/core/flow-host.d.ts +93 -132
  11. package/dist/core/flow-host.js +282 -547
  12. package/dist/core/sandbox-routes.d.ts +8 -3
  13. package/dist/core/sandbox-routes.js +10 -10
  14. package/dist/execution/memory.d.ts +54 -0
  15. package/dist/execution/memory.js +217 -0
  16. package/dist/execution/types.d.ts +125 -0
  17. package/dist/execution/types.js +52 -0
  18. package/dist/host/config-for.d.ts +2 -1
  19. package/dist/host/config-for.js +3 -21
  20. package/dist/host/config.d.ts +0 -8
  21. package/dist/host/create-host.d.ts +36 -1
  22. package/dist/host/create-host.js +48 -24
  23. package/dist/host/execution.d.ts +61 -0
  24. package/dist/host/execution.js +69 -0
  25. package/dist/host/http.d.ts +4 -0
  26. package/dist/host/http.js +6 -0
  27. package/dist/host/index.d.ts +3 -1
  28. package/dist/host/index.js +2 -0
  29. package/dist/host/main.js +106 -47
  30. package/dist/host/stack-config.d.ts +72 -0
  31. package/dist/host/stack-config.js +193 -0
  32. package/dist/index.d.ts +1 -1
  33. package/dist/infra/database.d.ts +16 -0
  34. package/dist/infra/database.js +31 -0
  35. package/dist/infra/execution.d.ts +39 -0
  36. package/dist/infra/execution.js +68 -0
  37. package/dist/infra/index.d.ts +42 -0
  38. package/dist/infra/index.js +56 -0
  39. package/dist/infra/readiness.d.ts +33 -0
  40. package/dist/infra/readiness.js +53 -0
  41. package/dist/infra/streams.d.ts +17 -0
  42. package/dist/infra/streams.js +33 -0
  43. package/dist/infra/workers.d.ts +39 -0
  44. package/dist/infra/workers.js +31 -0
  45. package/dist/model/pi-model.d.ts +2 -2
  46. package/dist/model/pi-model.js +5 -5
  47. package/dist/sandbox/manager.d.ts +26 -8
  48. package/dist/sandbox/manager.js +95 -47
  49. package/dist/sandbox/select.d.ts +1 -1
  50. package/dist/sandbox/select.js +4 -5
  51. package/dist/sandbox/types.d.ts +2 -2
  52. package/dist/store/canonical.d.ts +5 -0
  53. package/dist/store/canonical.js +17 -0
  54. package/dist/store/cursor.d.ts +9 -0
  55. package/dist/store/cursor.js +18 -0
  56. package/dist/store/memory.d.ts +57 -0
  57. package/dist/store/memory.js +643 -0
  58. package/dist/store/ownership.d.ts +19 -0
  59. package/dist/store/ownership.js +33 -0
  60. package/dist/store/postgres/connect.d.ts +25 -0
  61. package/dist/store/postgres/connect.js +25 -0
  62. package/dist/store/postgres/locking.d.ts +16 -0
  63. package/dist/store/postgres/locking.js +22 -0
  64. package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
  65. package/dist/store/postgres/migrations/001_initial.js +196 -0
  66. package/dist/store/postgres/migrations/index.d.ts +49 -0
  67. package/dist/store/postgres/migrations/index.js +75 -0
  68. package/dist/store/postgres/names.d.ts +11 -0
  69. package/dist/store/postgres/names.js +42 -0
  70. package/dist/store/postgres/store.d.ts +11 -0
  71. package/dist/store/postgres/store.js +790 -0
  72. package/dist/store/postgres/tenants.d.ts +90 -0
  73. package/dist/store/postgres/tenants.js +207 -0
  74. package/dist/store/types.d.ts +440 -0
  75. package/dist/store/types.js +10 -0
  76. package/dist/streams/basin.d.ts +23 -0
  77. package/dist/streams/basin.js +51 -0
  78. package/dist/streams/memory.d.ts +25 -0
  79. package/dist/streams/memory.js +124 -0
  80. package/dist/streams/relay.d.ts +37 -0
  81. package/dist/streams/relay.js +254 -0
  82. package/dist/streams/types.d.ts +115 -0
  83. package/dist/streams/types.js +86 -0
  84. package/dist/tenant/actions.d.ts +153 -0
  85. package/dist/tenant/actions.js +191 -0
  86. package/dist/tenant/advance.d.ts +15 -0
  87. package/dist/tenant/advance.js +439 -0
  88. package/dist/tenant/auth.d.ts +11 -0
  89. package/dist/tenant/auth.js +34 -0
  90. package/dist/tenant/commands.d.ts +3 -0
  91. package/dist/tenant/commands.js +384 -0
  92. package/dist/tenant/context.d.ts +140 -0
  93. package/dist/tenant/context.js +40 -0
  94. package/dist/tenant/effects.d.ts +20 -0
  95. package/dist/tenant/effects.js +539 -0
  96. package/dist/tenant/envelope.d.ts +0 -3
  97. package/dist/tenant/envelope.js +2 -34
  98. package/dist/tenant/ephemeral.d.ts +15 -4
  99. package/dist/tenant/ephemeral.js +106 -38
  100. package/dist/tenant/http.d.ts +32 -0
  101. package/dist/tenant/http.js +51 -0
  102. package/dist/tenant/live.d.ts +92 -0
  103. package/dist/tenant/live.js +290 -0
  104. package/dist/tenant/model-setting.d.ts +16 -0
  105. package/dist/tenant/model-setting.js +12 -0
  106. package/dist/tenant/module.d.ts +29 -8
  107. package/dist/tenant/module.js +162 -158
  108. package/dist/tenant/paths.d.ts +12 -8
  109. package/dist/tenant/paths.js +38 -12
  110. package/dist/tenant/principals.d.ts +9 -13
  111. package/dist/tenant/principals.js +31 -38
  112. package/dist/tenant/quarantine-error.d.ts +1 -1
  113. package/dist/tenant/quarantine-error.js +2 -8
  114. package/dist/tenant/quarantine.d.ts +0 -8
  115. package/dist/tenant/quarantine.js +8 -30
  116. package/dist/tenant/reset.d.ts +7 -6
  117. package/dist/tenant/reset.js +7 -51
  118. package/dist/tenant/routes-tenant.d.ts +12 -0
  119. package/dist/tenant/routes-tenant.js +126 -0
  120. package/dist/tenant/routes.d.ts +9 -0
  121. package/dist/tenant/routes.js +142 -0
  122. package/dist/tenant/runtime.d.ts +59 -58
  123. package/dist/tenant/runtime.js +243 -2201
  124. package/dist/tenant/scheduler.d.ts +25 -0
  125. package/dist/tenant/scheduler.js +84 -0
  126. package/dist/tenant/session.d.ts +60 -0
  127. package/dist/tenant/session.js +94 -0
  128. package/dist/tenant/sessions.d.ts +34 -0
  129. package/dist/tenant/sessions.js +149 -0
  130. package/dist/tenant/status.d.ts +13 -4
  131. package/dist/tenant/status.js +76 -41
  132. package/dist/tenant/store-memory.d.ts +9 -12
  133. package/dist/tenant/store-memory.js +79 -68
  134. package/dist/tenant/store-pg.d.ts +15 -0
  135. package/dist/tenant/store-pg.js +140 -0
  136. package/dist/tenant/streams.d.ts +126 -0
  137. package/dist/tenant/streams.js +419 -0
  138. package/dist/tenant/sweep.d.ts +15 -0
  139. package/dist/tenant/sweep.js +122 -0
  140. package/dist/tenant/types.d.ts +95 -15
  141. package/dist/tenant/types.js +23 -1
  142. package/dist/tenant/worker.d.ts +84 -0
  143. package/dist/tenant/worker.js +189 -0
  144. package/dist/vault/service.d.ts +37 -31
  145. package/dist/vault/service.js +439 -359
  146. package/dist/version.d.ts +1 -1
  147. package/dist/version.js +1 -1
  148. package/package.json +13 -13
  149. package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
  150. package/dist/adapters/sandbox/microsandbox.js +0 -238
  151. package/dist/core/store.d.ts +0 -32
  152. package/dist/core/store.js +0 -143
  153. package/dist/launcher/commands.d.ts +0 -24
  154. package/dist/launcher/commands.js +0 -260
  155. package/dist/launcher/ensure-config.d.ts +0 -16
  156. package/dist/launcher/ensure-config.js +0 -79
  157. package/dist/launcher/errors.d.ts +0 -12
  158. package/dist/launcher/errors.js +0 -22
  159. package/dist/launcher/host-config.d.ts +0 -17
  160. package/dist/launcher/host-config.js +0 -64
  161. package/dist/launcher/host-state.d.ts +0 -17
  162. package/dist/launcher/host-state.js +0 -61
  163. package/dist/launcher/lifecycle.d.ts +0 -66
  164. package/dist/launcher/lifecycle.js +0 -407
  165. package/dist/launcher/locks.d.ts +0 -26
  166. package/dist/launcher/locks.js +0 -117
  167. package/dist/launcher/logs.d.ts +0 -18
  168. package/dist/launcher/logs.js +0 -117
  169. package/dist/launcher/main.d.ts +0 -2
  170. package/dist/launcher/main.js +0 -66
  171. package/dist/launcher/output.d.ts +0 -11
  172. package/dist/launcher/output.js +0 -68
  173. package/dist/launcher/paths.d.ts +0 -20
  174. package/dist/launcher/paths.js +0 -36
  175. package/dist/launcher/probe.d.ts +0 -23
  176. package/dist/launcher/probe.js +0 -72
  177. package/dist/launcher/protocol.d.ts +0 -62
  178. package/dist/launcher/protocol.js +0 -1
  179. package/dist/launcher/schema-guard.d.ts +0 -6
  180. package/dist/launcher/schema-guard.js +0 -36
  181. package/dist/launcher/spawn.d.ts +0 -22
  182. package/dist/launcher/spawn.js +0 -85
  183. package/dist/launcher/status.d.ts +0 -6
  184. package/dist/launcher/status.js +0 -65
  185. package/dist/tenant/migration.d.ts +0 -16
  186. package/dist/tenant/migration.js +0 -116
  187. package/dist/tenant/schema.d.ts +0 -23
  188. package/dist/tenant/schema.js +0 -151
  189. package/dist/tenant/store-fs.d.ts +0 -16
  190. package/dist/tenant/store-fs.js +0 -205
@@ -10,8 +10,8 @@ export interface PiModelOptions {
10
10
  readonly root?: string;
11
11
  readonly onPreview?: (preview: ModelPreview) => void;
12
12
  readonly media?: Pick<RuntimeMedia, "dataUrl">;
13
- readonly readHostModel?: () => HostModelSecret | undefined;
14
- readonly writeHostCredential?: (credential: Credential) => void;
13
+ readonly readHostModel?: () => HostModelSecret | undefined | Promise<HostModelSecret | undefined>;
14
+ readonly writeHostCredential?: (credential: Credential) => void | Promise<void>;
15
15
  }
16
16
  /** Node model adapter. Local provider configuration is read only when invoked. */
17
17
  export declare function piModel(options?: PiModelOptions): RuntimeModelAdapter;
@@ -14,9 +14,9 @@ export function piModel(options = {}) {
14
14
  return async (call, context) => {
15
15
  context.signal.throwIfAborted();
16
16
  const root = options.root ?? "";
17
- const stored = options.readHostModel?.();
17
+ const stored = await options.readHostModel?.();
18
18
  if (!stored)
19
- throw new Error("Model provider is not configured. Start nylorun dev in a terminal, or set it in Studio.");
19
+ throw new Error("Model provider is not configured. Set it in Studio, or run nylo configure (npx @nylorun/cli configure).");
20
20
  const requested = call.model?.id;
21
21
  const selection = {
22
22
  provider: stored.provider,
@@ -28,7 +28,7 @@ export function piModel(options = {}) {
28
28
  const registry = modelsFor(selection, hostCredentialStore(stored, options.writeHostCredential), { environment: false });
29
29
  const selected = registry.getModel(selection.provider, selection.model);
30
30
  if (!selected)
31
- throw new Error("Unknown model. Run nylorun configure.");
31
+ throw new Error("Unknown model. Run nylo configure (npx @nylorun/cli configure).");
32
32
  const signatureFor = (part) => {
33
33
  const metadata = "providerMetadata" in part ? part.providerMetadata?.pi : undefined;
34
34
  if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
@@ -290,12 +290,12 @@ function hostCredentialStore(stored, write) {
290
290
  const next = await fn(current);
291
291
  if (next === undefined)
292
292
  return current;
293
- write?.(next);
293
+ await write?.(next);
294
294
  current = next;
295
295
  return next;
296
296
  },
297
297
  async delete() {
298
- throw new Error("Replace the model provider through nylorun dev or Studio.");
298
+ throw new Error("Replace the model provider in Studio, or with nylo configure.");
299
299
  },
300
300
  };
301
301
  }
@@ -1,5 +1,5 @@
1
1
  import { type AgentManifest, type CapabilityManifest, type SandboxToolName } from "@nylorun/core/define";
2
- import type { Store } from "../core/store.js";
2
+ import type { SessionStore } from "../store/types.js";
3
3
  import { type SandboxSelection, type SandboxSelectionReport } from "./select.js";
4
4
  import { type SandboxToolOutcome } from "./tools.js";
5
5
  import type { SandboxBackend } from "./types.js";
@@ -18,16 +18,23 @@ interface SandboxRecord {
18
18
  createdAt: string;
19
19
  updatedAt: string;
20
20
  }
21
+ export interface SandboxSweepOptions {
22
+ /** The sweep's clock, in ms since the epoch. Defaults to `Date.now()`. */
23
+ readonly now?: number;
24
+ /** Whether a session still exists; sandboxes of deleted sessions are removed. */
25
+ readonly sessionExists: (sessionId: string) => boolean | Promise<boolean>;
26
+ }
21
27
  export interface SandboxManagerOptions {
22
28
  /** Distinguishes this Runtime's sandboxes from other Runtimes on the same machine. */
23
29
  readonly scope: string;
24
- readonly store: Store;
30
+ readonly store: SessionStore;
25
31
  readonly backends: readonly SandboxBackend[];
26
- /** `auto`, `microsandbox` or `virtual`. Undefined means an invalid NYLORUN_SANDBOX value. */
32
+ /** `auto` or `virtual`. Undefined means an invalid NYLORUN_SANDBOX value. */
27
33
  readonly preference: string | undefined;
28
34
  /** Delete sandboxes on close (the Runtime's store does not outlive the process). */
29
35
  readonly ephemeral: boolean;
30
- readonly emit: (sessionId: string, turnId: string | null, type: string, payload: unknown) => void;
36
+ /** Writes a session event in its own transaction; never called inside one. */
37
+ readonly emit: (sessionId: string, turnId: string | null, type: string, payload: unknown) => void | Promise<void>;
31
38
  }
32
39
  /** The sandbox capability that owns this tool call, when the call is a built-in sandbox tool. */
33
40
  export declare function sandboxCapabilityOf(manifest: AgentManifest | undefined, capabilityId: string | undefined, toolName: string | undefined): CapabilityManifest | undefined;
@@ -37,25 +44,36 @@ export declare class SandboxManager {
37
44
  private readonly live;
38
45
  private readonly prefix;
39
46
  private closing;
47
+ private reconciled;
40
48
  constructor(options: SandboxManagerOptions);
49
+ /**
50
+ * One Tenant sweep pass:
51
+ * - stops sandboxes idle for at least their idle timeout (their files remain; the next
52
+ * call reattaches);
53
+ * - marks records whose compute this process does not hold as stopped (compute is gone
54
+ * after a Runtime restart);
55
+ * - on the first pass of this process, removes sandboxes whose session no longer exists
56
+ * (skipped for ephemeral Runtimes, which delete theirs on close).
57
+ */
58
+ sweep(options: SandboxSweepOptions): Promise<void>;
41
59
  /** Backends are probed once, on first use, and the choice holds for the life of the Runtime. */
42
60
  get ready(): Promise<SandboxSelection>;
43
61
  /** Whether any sandbox was ever recorded, so startup can skip probing for agents without one. */
44
- hasRecords(): boolean;
62
+ hasRecords(): Promise<boolean>;
45
63
  keyOf(sessionId: string): string;
46
64
  report(): Promise<SandboxSelectionReport & {
47
65
  readonly defaultImage: string;
48
66
  }>;
49
67
  run(session: SandboxSessionRef, capability: CapabilityManifest, toolName: SandboxToolName, input: unknown, signal: AbortSignal): Promise<SandboxToolOutcome>;
50
68
  private execute;
69
+ private executeOpen;
51
70
  private open;
52
- private armIdle;
53
71
  private stop;
54
72
  private record;
55
73
  /** The sandbox record for a session, if one was ever created. */
56
- status(sessionId: string): SandboxRecord | undefined;
74
+ status(sessionId: string): Promise<SandboxRecord | undefined>;
57
75
  /** Delete sandboxes this Runtime owns whose session no longer exists. */
58
- reconcile(sessionExists: (sessionId: string) => boolean): Promise<void>;
76
+ reconcile(sessionExists: (sessionId: string) => boolean | Promise<boolean>): Promise<void>;
59
77
  close(): Promise<void>;
60
78
  }
61
79
  export {};
@@ -2,6 +2,10 @@
2
2
  * Sandbox Manager: owns sandbox lifecycle for the Runtime. One sandbox per session, created on
3
3
  * the first sandbox tool call, stopped after its idle timeout, reattached on the next call.
4
4
  * Commands against one sandbox run one at a time.
5
+ *
6
+ * The manager keeps no timers. The Tenant sweep calls `sweep`, which stops sandboxes idle
7
+ * past their timeout, marks records of compute this process no longer holds as stopped
8
+ * (after a restart), and, once per process, removes sandboxes whose session is gone.
5
9
  */
6
10
  import { createHash } from "node:crypto";
7
11
  import { isSandboxToolName, } from "@nylorun/core/define";
@@ -24,15 +28,53 @@ export class SandboxManager {
24
28
  live = new Map();
25
29
  prefix;
26
30
  closing = false;
31
+ reconciled = false;
27
32
  constructor(options) {
28
33
  this.options = options;
29
34
  this.prefix = `nylorun-${options.scope}-`;
30
- // Compute is gone after a Runtime restart; files remain and the next call reattaches.
31
- options.store.tx(() => {
32
- for (const record of options.store.all("sandboxes"))
33
- if (record.state !== "stopped")
34
- options.store.put("sandboxes", record.key, { ...record, state: "stopped" });
35
+ }
36
+ /**
37
+ * One Tenant sweep pass:
38
+ * - stops sandboxes idle for at least their idle timeout (their files remain; the next
39
+ * call reattaches);
40
+ * - marks records whose compute this process does not hold as stopped (compute is gone
41
+ * after a Runtime restart);
42
+ * - on the first pass of this process, removes sandboxes whose session no longer exists
43
+ * (skipped for ephemeral Runtimes, which delete theirs on close).
44
+ */
45
+ async sweep(options) {
46
+ if (this.closing)
47
+ return;
48
+ const now = options.now ?? Date.now();
49
+ const idle = (live) => !!live.handle && live.active === 0 && now - live.lastUsedAt >= live.idleMs;
50
+ const stops = [];
51
+ for (const [key, live] of this.live) {
52
+ if (!idle(live))
53
+ continue;
54
+ // Queued behind any call already waiting; checked again when its turn comes.
55
+ live.tail = live.tail
56
+ .then(() => (idle(live) ? this.stop(key, live) : undefined))
57
+ .catch(() => undefined);
58
+ stops.push(live.tail);
59
+ }
60
+ await Promise.all(stops);
61
+ await this.options.store.tx(async (t) => {
62
+ for (const record of await t.listSandboxes()) {
63
+ if (record.state === "stopped")
64
+ continue;
65
+ const live = this.live.get(record.key);
66
+ if (live && (live.handle || live.active > 0))
67
+ continue;
68
+ await t.put("sandboxes", record.key, { ...record, state: "stopped" });
69
+ }
35
70
  });
71
+ if (this.reconciled || this.options.ephemeral)
72
+ return;
73
+ this.reconciled = true;
74
+ if (!(await this.hasRecords()))
75
+ return;
76
+ await this.reconcile(async (sessionId) => [...this.live.values()].some((live) => live.sessionId === sessionId) ||
77
+ (await options.sessionExists(sessionId)));
36
78
  }
37
79
  /** Backends are probed once, on first use, and the choice holds for the life of the Runtime. */
38
80
  get ready() {
@@ -42,15 +84,15 @@ export class SandboxManager {
42
84
  ? selectSandboxBackend(this.options.backends, preference)
43
85
  : Promise.resolve({
44
86
  preference: "auto",
45
- reason: `NYLORUN_SANDBOX='${this.options.preference}' is not valid; use auto, microsandbox or virtual`,
87
+ reason: `NYLORUN_SANDBOX='${this.options.preference}' is not valid; use auto or virtual`,
46
88
  probes: [],
47
89
  });
48
90
  }
49
91
  return this.selection;
50
92
  }
51
93
  /** Whether any sandbox was ever recorded, so startup can skip probing for agents without one. */
52
- hasRecords() {
53
- return this.options.store.all("sandboxes").length > 0;
94
+ async hasRecords() {
95
+ return (await this.options.store.tx((t) => t.counts())).sandboxes > 0;
54
96
  }
55
97
  keyOf(sessionId) {
56
98
  return this.prefix + createHash("sha256").update(sessionId).digest("hex").slice(0, 16);
@@ -84,7 +126,14 @@ export class SandboxManager {
84
126
  };
85
127
  let live = this.live.get(key);
86
128
  if (!live) {
87
- live = { sessionId: session.id, backend, idleMs: idleMsOf(capability.sandbox), tail: Promise.resolve() };
129
+ live = {
130
+ sessionId: session.id,
131
+ backend,
132
+ idleMs: idleMsOf(capability.sandbox),
133
+ active: 0,
134
+ lastUsedAt: Date.now(),
135
+ tail: Promise.resolve(),
136
+ };
88
137
  this.live.set(key, live);
89
138
  }
90
139
  const entry = live;
@@ -97,15 +146,24 @@ export class SandboxManager {
97
146
  throw new Error("Turn cancelled");
98
147
  if (this.closing)
99
148
  throw new Error("Runtime is shutting down");
100
- clearTimeout(live.timer);
149
+ live.active += 1;
150
+ try {
151
+ return await this.executeOpen(live, session, spec, toolName, input, signal);
152
+ }
153
+ finally {
154
+ live.active -= 1;
155
+ live.lastUsedAt = Date.now();
156
+ }
157
+ }
158
+ async executeOpen(live, session, spec, toolName, input, signal) {
101
159
  if (!live.handle) {
102
160
  try {
103
161
  live.handle = await this.open(live, session, spec);
104
162
  }
105
163
  catch (error) {
106
164
  const message = error instanceof Error ? error.message : String(error);
107
- this.record(spec.key, session.id, live.backend.name, spec.image, "stopped");
108
- this.options.emit(session.id, session.activeTurnId, "sandbox.state", {
165
+ await this.record(spec.key, session.id, live.backend.name, spec.image, "stopped");
166
+ await this.options.emit(session.id, session.activeTurnId, "sandbox.state", {
109
167
  state: "stopped",
110
168
  backend: live.backend.name,
111
169
  error: message,
@@ -124,7 +182,7 @@ export class SandboxManager {
124
182
  const outcome = await runSandboxTool(live.handle, toolName, (input ?? {}), signal, (value) => {
125
183
  report = value;
126
184
  });
127
- this.options.emit(session.id, session.activeTurnId, "sandbox.exec", {
185
+ await this.options.emit(session.id, session.activeTurnId, "sandbox.exec", {
128
186
  tool: toolName,
129
187
  ...report,
130
188
  ...(report.command ? { command: report.command.slice(0, 500) } : {}),
@@ -145,38 +203,26 @@ export class SandboxManager {
145
203
  message: error instanceof Error ? error.message : String(error),
146
204
  };
147
205
  }
148
- finally {
149
- this.armIdle(spec.key, live);
150
- }
151
206
  }
152
207
  async open(live, session, spec) {
153
- const existing = this.options.store.get("sandboxes", spec.key);
208
+ const existing = await this.options.store.tx((t) => t.get("sandboxes", spec.key));
154
209
  const payload = {
155
210
  backend: live.backend.name,
156
211
  isolation: live.backend.isolation,
157
212
  image: spec.image,
158
213
  network: describeNetwork(spec.network),
159
214
  };
160
- this.record(spec.key, session.id, live.backend.name, spec.image, "creating", existing);
161
- this.options.emit(session.id, session.activeTurnId, "sandbox.state", {
215
+ await this.record(spec.key, session.id, live.backend.name, spec.image, "creating", existing);
216
+ await this.options.emit(session.id, session.activeTurnId, "sandbox.state", {
162
217
  state: "creating",
163
218
  ...payload,
164
219
  ...(existing ? { reattach: true } : {}),
165
220
  });
166
221
  const handle = await live.backend.open(spec);
167
- this.record(spec.key, session.id, live.backend.name, spec.image, "running", existing);
168
- this.options.emit(session.id, session.activeTurnId, "sandbox.state", { state: "running", ...payload });
222
+ await this.record(spec.key, session.id, live.backend.name, spec.image, "running", existing);
223
+ await this.options.emit(session.id, session.activeTurnId, "sandbox.state", { state: "running", ...payload });
169
224
  return handle;
170
225
  }
171
- armIdle(key, live) {
172
- clearTimeout(live.timer);
173
- if (this.closing)
174
- return;
175
- live.timer = setTimeout(() => {
176
- live.tail = live.tail.then(() => this.stop(key, live)).catch(() => undefined);
177
- }, live.idleMs);
178
- live.timer.unref();
179
- }
180
226
  async stop(key, live) {
181
227
  const handle = live.handle;
182
228
  if (!handle)
@@ -186,25 +232,28 @@ export class SandboxManager {
186
232
  await handle.stop();
187
233
  }
188
234
  finally {
189
- this.record(key, live.sessionId, live.backend.name, undefined, "stopped");
190
- this.options.emit(live.sessionId, null, "sandbox.state", { state: "stopped", backend: live.backend.name });
235
+ await this.record(key, live.sessionId, live.backend.name, undefined, "stopped");
236
+ await this.options.emit(live.sessionId, null, "sandbox.state", { state: "stopped", backend: live.backend.name });
191
237
  }
192
238
  }
193
- record(key, sessionId, backend, image, state, existing = this.options.store.get("sandboxes", key)) {
239
+ async record(key, sessionId, backend, image, state, known) {
194
240
  const now = new Date().toISOString();
195
- this.options.store.tx(() => this.options.store.put("sandboxes", key, {
196
- key,
197
- sessionId,
198
- backend,
199
- image: image ?? existing?.image ?? "",
200
- state,
201
- createdAt: existing?.createdAt ?? now,
202
- updatedAt: now,
203
- }));
241
+ await this.options.store.tx(async (t) => {
242
+ const existing = known ?? (await t.get("sandboxes", key));
243
+ await t.put("sandboxes", key, {
244
+ key,
245
+ sessionId,
246
+ backend,
247
+ image: image ?? existing?.image ?? "",
248
+ state,
249
+ createdAt: existing?.createdAt ?? now,
250
+ updatedAt: now,
251
+ });
252
+ });
204
253
  }
205
254
  /** The sandbox record for a session, if one was ever created. */
206
255
  status(sessionId) {
207
- return this.options.store.get("sandboxes", this.keyOf(sessionId));
256
+ return this.options.store.tx((t) => t.get("sandboxes", this.keyOf(sessionId)));
208
257
  }
209
258
  /** Delete sandboxes this Runtime owns whose session no longer exists. */
210
259
  async reconcile(sessionExists) {
@@ -212,11 +261,11 @@ export class SandboxManager {
212
261
  if (!backend)
213
262
  return;
214
263
  for (const key of await backend.list(this.prefix)) {
215
- const record = this.options.store.get("sandboxes", key);
216
- if (record && sessionExists(record.sessionId))
264
+ const record = await this.options.store.tx((t) => t.get("sandboxes", key));
265
+ if (record && (await sessionExists(record.sessionId)))
217
266
  continue;
218
267
  await backend.remove(key);
219
- this.options.store.tx(() => this.options.store.delete("sandboxes", key));
268
+ await this.options.store.tx((t) => t.delete("sandboxes", key));
220
269
  }
221
270
  }
222
271
  async close() {
@@ -224,7 +273,6 @@ export class SandboxManager {
224
273
  if (!this.selection)
225
274
  return;
226
275
  const tasks = [...this.live.entries()].map(async ([key, live]) => {
227
- clearTimeout(live.timer);
228
276
  await live.tail;
229
277
  if (this.options.ephemeral) {
230
278
  live.handle = undefined;
@@ -15,7 +15,7 @@ export interface SandboxSelectionReport {
15
15
  readonly reason: string;
16
16
  readonly probes: readonly SandboxProbe[];
17
17
  }
18
- /** The default order: strongest isolation first, then the backend that always works. */
18
+ /** The backends a Runtime probes, in order. `auto` picks the first available one. */
19
19
  export declare function defaultSandboxBackends(options: {
20
20
  readonly root: string;
21
21
  }): SandboxBackend[];
@@ -1,9 +1,8 @@
1
- import { microsandboxBackend } from "../adapters/sandbox/microsandbox.js";
2
1
  import { virtualBackend } from "../adapters/sandbox/virtual.js";
3
- const PREFERENCES = ["auto", "microsandbox", "virtual"];
4
- /** The default order: strongest isolation first, then the backend that always works. */
2
+ const PREFERENCES = ["auto", "virtual"];
3
+ /** The backends a Runtime probes, in order. `auto` picks the first available one. */
5
4
  export function defaultSandboxBackends(options) {
6
- return [microsandboxBackend(), virtualBackend({ root: options.root })];
5
+ return [virtualBackend({ root: options.root })];
7
6
  }
8
7
  export function parseSandboxPreference(value) {
9
8
  const normalized = (value ?? "auto").trim().toLowerCase() || "auto";
@@ -75,7 +74,7 @@ export async function probeSandboxBackends(options = {}) {
75
74
  preference: "auto",
76
75
  backend: null,
77
76
  isolation: null,
78
- reason: "sandbox.backend must be auto, microsandbox or virtual",
77
+ reason: "sandbox.backend must be auto or virtual",
79
78
  probes: await Promise.all(backends.map((backend) => backend.probe())),
80
79
  };
81
80
  return reportSelection(await selectSandboxBackend(backends, preference));
@@ -3,8 +3,8 @@
3
3
  * and import a substrate SDK; everything else in the Runtime talks to this interface.
4
4
  */
5
5
  import type { SandboxNetworkPreset } from "@nylorun/core/define";
6
- export type SandboxBackendName = "microsandbox" | "virtual";
7
- export type SandboxIsolation = "vm" | "process";
6
+ export type SandboxBackendName = "virtual";
7
+ export type SandboxIsolation = "process";
8
8
  export interface SandboxProbe {
9
9
  readonly name: SandboxBackendName;
10
10
  readonly available: boolean;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Canonical JSON: object keys sorted, `undefined` members dropped. Idempotency
3
+ * comparisons (commands, effects, vault requests) compare canonical forms.
4
+ */
5
+ export declare function canonical(value: unknown): string;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Canonical JSON: object keys sorted, `undefined` members dropped. Idempotency
3
+ * comparisons (commands, effects, vault requests) compare canonical forms.
4
+ */
5
+ export function canonical(value) {
6
+ if (Array.isArray(value))
7
+ return "[" + value.map(canonical).join(",") + "]";
8
+ if (value && typeof value === "object")
9
+ return ("{" +
10
+ Object.keys(value)
11
+ .sort()
12
+ .filter((k) => value[k] !== undefined)
13
+ .map((k) => JSON.stringify(k) + ":" + canonical(value[k]))
14
+ .join(",") +
15
+ "}");
16
+ return JSON.stringify(value) ?? "null";
17
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Event cursors. A cursor names the last event a client has seen:
3
+ * `base64url("<sessionId>:<seq>")`, where `seq` is the per-session event
4
+ * sequence and also the S2 sequence number in `sessions/<sessionId>`.
5
+ * The encoding matches the old global-sequence cursors, so clients see no format change.
6
+ */
7
+ export declare function encodeCursor(sessionId: string, seq: number): string;
8
+ /** Decodes a cursor for `sessionId`, or throws `Invalid cursor`. */
9
+ export declare function decodeCursor(sessionId: string, cursor: string): number;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Event cursors. A cursor names the last event a client has seen:
3
+ * `base64url("<sessionId>:<seq>")`, where `seq` is the per-session event
4
+ * sequence and also the S2 sequence number in `sessions/<sessionId>`.
5
+ * The encoding matches the old global-sequence cursors, so clients see no format change.
6
+ */
7
+ export function encodeCursor(sessionId, seq) {
8
+ return Buffer.from(`${sessionId}:${seq}`).toString("base64url");
9
+ }
10
+ /** Decodes a cursor for `sessionId`, or throws `Invalid cursor`. */
11
+ export function decodeCursor(sessionId, cursor) {
12
+ const decoded = Buffer.from(cursor, "base64url").toString();
13
+ const prefix = `${sessionId}:`;
14
+ const rest = decoded.slice(prefix.length);
15
+ if (!decoded.startsWith(prefix) || !/^\d+$/.test(rest))
16
+ throw new Error("Invalid cursor");
17
+ return Number(rest);
18
+ }
@@ -0,0 +1,57 @@
1
+ import { type CommitListener, type DocTable, type ExecutorRow, type PrincipalRow, type SessionOwnership, type SessionStore, type SessionStoreOptions, type StoreHealth, type Tx, type VaultAuditRow, type VaultCredentialRow, type VaultIdempotencyRow, type VaultRow } from "./types.js";
2
+ /** Schema version the fake reports; it has no migrations. */
3
+ export declare const MEMORY_SCHEMA_VERSION = 1;
4
+ interface SessionMeta extends SessionOwnership {
5
+ nextSeq: number;
6
+ }
7
+ interface State {
8
+ /** JSON text per id, like a `body` column. */
9
+ docs: Record<DocTable, Map<string, string>>;
10
+ sessionMeta: Map<string, SessionMeta>;
11
+ /** sessionId → seq → event JSON. */
12
+ outbox: Map<string, Map<number, string>>;
13
+ executors: Map<string, ExecutorRow>;
14
+ principals: Map<string, PrincipalRow>;
15
+ vaults: Map<string, VaultRow>;
16
+ credentials: Map<string, VaultCredentialRow>;
17
+ audit: VaultAuditRow[];
18
+ idempotency: Map<string, VaultIdempotencyRow>;
19
+ settings: Map<string, string>;
20
+ }
21
+ /**
22
+ * The data behind in-memory Session Stores: what a database is to its connections. Several
23
+ * `MemorySessionStore`s on one `MemoryStoreData` see each other's commits and serialize their
24
+ * transactions together, as several processes on one Tenant schema would; it outlives them,
25
+ * so a test can close a Tenant and open it again.
26
+ */
27
+ export declare class MemoryStoreData {
28
+ /** @internal */ state: State;
29
+ /** @internal */ queue: Promise<void>;
30
+ }
31
+ /**
32
+ * In-memory `SessionStore` for unit tests. Not a supported profile.
33
+ *
34
+ * Transactions are serialized (one at a time), each works on a copy of the
35
+ * state and replaces it on commit, so a throw rolls back everything: documents,
36
+ * sequences, outbox rows, `afterCommit` callbacks and `signalWork`. Commit listeners see
37
+ * only this store's commits, as with a Postgres connection.
38
+ */
39
+ export declare class MemorySessionStore implements SessionStore {
40
+ /** Shared with other stores on the same data; a fresh, private one by default. */
41
+ private readonly data;
42
+ readonly tenantId: string;
43
+ private readonly active;
44
+ private readonly listeners;
45
+ private readonly now;
46
+ private readonly onError;
47
+ private closed;
48
+ constructor(options: SessionStoreOptions,
49
+ /** Shared with other stores on the same data; a fresh, private one by default. */
50
+ data?: MemoryStoreData);
51
+ tx<T>(fn: (t: Tx) => Promise<T>): Promise<T>;
52
+ onCommit(listener: CommitListener): () => void;
53
+ health(): Promise<StoreHealth>;
54
+ close(): Promise<void>;
55
+ private acquire;
56
+ }
57
+ export {};