@nylorun/runtime 0.7.0-beta → 0.8.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +7 -5
  3. package/dist/adapters/sandbox/microsandbox.d.ts +2 -0
  4. package/dist/adapters/sandbox/microsandbox.js +238 -0
  5. package/dist/adapters/sandbox/virtual.d.ts +4 -0
  6. package/dist/adapters/sandbox/virtual.js +125 -0
  7. package/dist/configuration.d.ts +1 -1
  8. package/dist/configuration.js +1 -1
  9. package/dist/core/executors.d.ts +32 -0
  10. package/dist/core/executors.js +60 -0
  11. package/dist/core/main.js +7 -12
  12. package/dist/core/runtime.d.ts +24 -1
  13. package/dist/core/runtime.js +322 -30
  14. package/dist/core/store.d.ts +12 -0
  15. package/dist/core/store.js +50 -1
  16. package/dist/index.d.ts +0 -1
  17. package/dist/index.js +0 -1
  18. package/dist/model/catalog.d.ts +12 -0
  19. package/dist/model/catalog.js +29 -0
  20. package/dist/model/models.d.ts +3 -1
  21. package/dist/model/models.js +4 -2
  22. package/dist/model/pi-model.d.ts +9 -3
  23. package/dist/model/pi-model.js +47 -11
  24. package/dist/model/settings.d.ts +0 -2
  25. package/dist/model/settings.js +0 -47
  26. package/dist/node/index.d.ts +1 -1
  27. package/dist/node/index.js +1 -1
  28. package/dist/sandbox/manager.d.ts +61 -0
  29. package/dist/sandbox/manager.js +239 -0
  30. package/dist/sandbox/policy.d.ts +13 -0
  31. package/dist/sandbox/policy.js +67 -0
  32. package/dist/sandbox/select.d.ts +35 -0
  33. package/dist/sandbox/select.js +76 -0
  34. package/dist/sandbox/tools.d.ts +25 -0
  35. package/dist/sandbox/tools.js +165 -0
  36. package/dist/sandbox/types.d.ts +66 -0
  37. package/dist/vault/service.d.ts +41 -1
  38. package/dist/vault/service.js +291 -4
  39. package/dist/version.d.ts +1 -0
  40. package/dist/version.js +3 -0
  41. package/package.json +9 -4
  42. package/dist/config.d.ts +0 -20
  43. package/dist/model/defaults.d.ts +0 -17
  44. package/dist/model/defaults.js +0 -21
  45. package/dist/model/http-model.d.ts +0 -12
  46. package/dist/model/http-model.js +0 -299
  47. package/dist/node/local-sessions.d.ts +0 -5
  48. package/dist/node/local-sessions.js +0 -174
  49. package/dist/server/ag-ui.d.ts +0 -8
  50. package/dist/server/ag-ui.js +0 -76
  51. package/dist/server/delivery.d.ts +0 -24
  52. package/dist/server/delivery.js +0 -107
  53. package/dist/server/digests.d.ts +0 -13
  54. package/dist/server/digests.js +0 -39
  55. package/dist/server/host.d.ts +0 -70
  56. package/dist/server/host.js +0 -647
  57. package/dist/session/api.d.ts +0 -10
  58. package/dist/session/api.js +0 -15
  59. package/dist/session/default.d.ts +0 -5
  60. package/dist/session/default.js +0 -30
  61. package/dist/session/handle.d.ts +0 -27
  62. package/dist/session/handle.js +0 -199
  63. package/dist/session/index.d.ts +0 -2
  64. package/dist/session/index.js +0 -2
  65. package/dist/sessions/host.d.ts +0 -39
  66. package/dist/sessions/host.js +0 -359
  67. package/dist/sessions/store.d.ts +0 -41
  68. package/dist/sessions/store.js +0 -33
  69. /package/dist/{config.js → sandbox/types.js} +0 -0
@@ -0,0 +1,12 @@
1
+ export type HostModelCatalog = {
2
+ providers: {
3
+ id: string;
4
+ name: string;
5
+ models: {
6
+ id: string;
7
+ name: string;
8
+ }[];
9
+ }[];
10
+ };
11
+ /** Public provider and model names. No credentials. */
12
+ export declare function hostModelCatalog(): HostModelCatalog;
@@ -0,0 +1,29 @@
1
+ import { modelsFor } from "./models.js";
2
+ const emptyStore = {
3
+ async read() {
4
+ return undefined;
5
+ },
6
+ async list() {
7
+ return [];
8
+ },
9
+ async modify(_providerId, fn) {
10
+ return fn(undefined);
11
+ },
12
+ async delete() { },
13
+ };
14
+ /** Public provider and model names. No credentials. */
15
+ export function hostModelCatalog() {
16
+ const models = modelsFor({ provider: "", model: "" }, emptyStore, {
17
+ environment: false,
18
+ });
19
+ return {
20
+ providers: models.getProviders().map((provider) => ({
21
+ id: provider.id,
22
+ name: provider.name,
23
+ models: models.getModels(provider.id).map((model) => ({
24
+ id: model.id,
25
+ name: model.name,
26
+ })),
27
+ })),
28
+ };
29
+ }
@@ -6,4 +6,6 @@ export type Selection = Readonly<{
6
6
  baseUrl: string;
7
7
  }>;
8
8
  }>;
9
- export declare function modelsFor(selection: Selection, credentials: CredentialStore): import("@earendil-works/pi-ai").MutableModels;
9
+ export declare function modelsFor(selection: Selection, credentials: CredentialStore, options?: {
10
+ environment?: boolean;
11
+ }): import("@earendil-works/pi-ai").MutableModels;
@@ -1,7 +1,7 @@
1
1
  import { createProvider, defaultProviderAuthContext, envApiKeyAuth, } from "@earendil-works/pi-ai";
2
2
  import { stream, streamSimple, } from "@earendil-works/pi-ai/api/openai-completions";
3
3
  import { builtinModels } from "@earendil-works/pi-ai/providers/all";
4
- export function modelsFor(selection, credentials) {
4
+ export function modelsFor(selection, credentials, options = {}) {
5
5
  const environmentFirst = {
6
6
  async read(providerId, options) {
7
7
  const explicit = process.env.MODEL_PROVIDER_API_KEY;
@@ -23,7 +23,9 @@ export function modelsFor(selection, credentials) {
23
23
  modify: (id, fn, options) => credentials.modify(id, fn, options),
24
24
  delete: (id, options) => credentials.delete(id, options),
25
25
  };
26
- const models = builtinModels({ credentials: environmentFirst });
26
+ const models = builtinModels({
27
+ credentials: options.environment === false ? credentials : environmentFirst,
28
+ });
27
29
  if (!selection.custom)
28
30
  return models;
29
31
  const model = {
@@ -1,11 +1,17 @@
1
+ import type { Credential } from "@earendil-works/pi-ai";
1
2
  import type { RuntimeModelAdapter } from "../contracts.js";
2
3
  import type { RuntimeMedia } from "../adapters/media.js";
3
- import { type Selection } from "./models.js";
4
+ import type { HostModelSecret } from "../vault/service.js";
5
+ export interface ModelPreview {
6
+ readonly invocationId: string;
7
+ readonly text: string;
8
+ }
4
9
  export interface PiModelOptions {
5
10
  readonly root?: string;
6
- readonly onPreview?: (preview: import("./defaults.js").ModelPreview) => void;
7
- readonly selection?: Selection;
11
+ readonly onPreview?: (preview: ModelPreview) => void;
8
12
  readonly media?: Pick<RuntimeMedia, "dataUrl">;
13
+ readonly readHostModel?: () => HostModelSecret | undefined;
14
+ readonly writeHostCredential?: (credential: Credential) => void;
9
15
  }
10
16
  /** Node model adapter. Local provider configuration is read only when invoked. */
11
17
  export declare function piModel(options?: PiModelOptions): RuntimeModelAdapter;
@@ -1,8 +1,6 @@
1
- import { join } from "node:path";
2
1
  import { scrub } from "../redact.js";
3
- import { ProjectCredentialStore } from "./auth-store.js";
4
2
  import { modelsFor } from "./models.js";
5
- import { modelSelection, projectSecrets } from "./settings.js";
3
+ import { projectSecrets } from "./settings.js";
6
4
  const emptyUsage = () => ({
7
5
  input: 0,
8
6
  output: 0,
@@ -16,15 +14,18 @@ export function piModel(options = {}) {
16
14
  return async (call, context) => {
17
15
  context.signal.throwIfAborted();
18
16
  const root = options.root ?? process.cwd();
19
- const configured = options.selection ?? modelSelection(root);
17
+ const stored = options.readHostModel?.();
18
+ if (!stored)
19
+ throw new Error("Model provider is not configured. Start nylorun dev in a terminal, or set it in Studio.");
20
20
  const requested = call.model?.id;
21
21
  const selection = {
22
- ...configured,
23
- model: requested?.startsWith(`${configured.provider}/`)
24
- ? requested.slice(configured.provider.length + 1)
25
- : (requested ?? configured.model),
22
+ provider: stored.provider,
23
+ model: requested?.startsWith(`${stored.provider}/`)
24
+ ? requested.slice(stored.provider.length + 1)
25
+ : (requested ?? stored.model),
26
+ ...(stored.baseUrl ? { custom: { baseUrl: stored.baseUrl } } : {}),
26
27
  };
27
- const registry = modelsFor(selection, new ProjectCredentialStore(join(root, ".nylorun", "auth.json"), join(root, ".env", "auth.json")));
28
+ const registry = modelsFor(selection, hostCredentialStore(stored, options.writeHostCredential), { environment: false });
28
29
  const selected = registry.getModel(selection.provider, selection.model);
29
30
  if (!selected)
30
31
  throw new Error("Unknown model. Run nylorun configure.");
@@ -176,7 +177,10 @@ export function piModel(options = {}) {
176
177
  messages,
177
178
  tools,
178
179
  };
179
- const secrets = projectSecrets(root);
180
+ const secrets = [
181
+ ...projectSecrets(root),
182
+ ...credentialSecrets(stored.credential),
183
+ ];
180
184
  // Publish only portable references, never the materialized provider image bytes.
181
185
  context.reportPreparedCall?.({
182
186
  adapter: "runtime.pi-ai",
@@ -265,7 +269,39 @@ export function piModel(options = {}) {
265
269
  };
266
270
  }
267
271
  catch (error) {
268
- throw new Error(String(scrub(error instanceof Error ? error.message : String(error), projectSecrets(root))));
272
+ throw new Error(String(scrub(error instanceof Error ? error.message : String(error), secrets)));
269
273
  }
270
274
  };
271
275
  }
276
+ function hostCredentialStore(stored, write) {
277
+ let current = stored.credential;
278
+ return {
279
+ async read(providerId) {
280
+ if (providerId !== stored.provider)
281
+ return undefined;
282
+ return current;
283
+ },
284
+ async list() {
285
+ return [{ providerId: stored.provider, type: current.type }];
286
+ },
287
+ async modify(providerId, fn) {
288
+ if (providerId !== stored.provider)
289
+ return fn(undefined);
290
+ const next = await fn(current);
291
+ if (next === undefined)
292
+ return current;
293
+ write?.(next);
294
+ current = next;
295
+ return next;
296
+ },
297
+ async delete() {
298
+ throw new Error("Replace the model provider through nylorun dev or Studio.");
299
+ },
300
+ };
301
+ }
302
+ function credentialSecrets(credential) {
303
+ const values = [credential.key, credential.access, credential.refresh];
304
+ if (credential.env)
305
+ values.push(...Object.values(credential.env));
306
+ return values.filter((value) => Boolean(value));
307
+ }
@@ -1,3 +1 @@
1
- import type { Selection } from "./models.js";
2
- export declare function modelSelection(root?: string): Selection;
3
1
  export declare function projectSecrets(root?: string): readonly string[];
@@ -1,52 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- export function modelSelection(root = process.cwd()) {
4
- const { MODEL_PROVIDER: provider, MODEL: model, MODEL_PROVIDER_BASE_URL: baseUrl, } = process.env;
5
- if (provider !== undefined || model !== undefined || baseUrl !== undefined) {
6
- if (!provider?.trim() || !model?.trim())
7
- throw new Error("Set both MODEL_PROVIDER and MODEL, or run nylorun configure.");
8
- if (provider === "custom" && !baseUrl?.trim())
9
- throw new Error("Set MODEL_PROVIDER_BASE_URL for MODEL_PROVIDER=custom.");
10
- if (baseUrl && provider !== "custom")
11
- throw new Error("MODEL_PROVIDER_BASE_URL requires MODEL_PROVIDER=custom.");
12
- if (baseUrl) {
13
- let url;
14
- try {
15
- url = new URL(baseUrl);
16
- }
17
- catch {
18
- throw new Error("MODEL_PROVIDER_BASE_URL must be an HTTP(S) URL.");
19
- }
20
- if (!["http:", "https:"].includes(url.protocol))
21
- throw new Error("MODEL_PROVIDER_BASE_URL must be an HTTP(S) URL.");
22
- }
23
- return { provider, model, ...(baseUrl ? { custom: { baseUrl } } : {}) };
24
- }
25
- try {
26
- let contents;
27
- try {
28
- contents = readFileSync(join(root, ".env", "model.json"), "utf8");
29
- }
30
- catch (error) {
31
- if (!(error instanceof Error) ||
32
- !("code" in error) ||
33
- !["ENOENT", "ENOTDIR"].includes(String(error.code)))
34
- throw error;
35
- contents = readFileSync(join(root, "config", "model.json"), "utf8");
36
- }
37
- const value = JSON.parse(contents);
38
- if (typeof value.provider === "string" &&
39
- value.provider &&
40
- typeof value.model === "string" &&
41
- value.model &&
42
- (value.custom === undefined || typeof value.custom.baseUrl === "string"))
43
- return value;
44
- }
45
- catch {
46
- /* Report one actionable setup error without file or credential contents. */
47
- }
48
- throw new Error("Run nylorun configure to connect a model provider.");
49
- }
50
3
  export function projectSecrets(root = process.cwd()) {
51
4
  const values = Object.entries(process.env)
52
5
  .filter(([key]) => /key|token|secret|password|credential/i.test(key))
@@ -1,5 +1,5 @@
1
- export { localSessions } from "./local-sessions.js";
2
1
  export { localMedia, MediaStore } from "../adapters/media.js";
3
2
  export { jsonlObserver } from "../adapters/observe.js";
4
3
  export { piModel, type PiModelOptions } from "../model/pi-model.js";
5
4
  export { projectAsset } from "../assets.js";
5
+ export { probeSandboxBackends, type SandboxSelectionReport, } from "../sandbox/select.js";
@@ -1,5 +1,5 @@
1
- export { localSessions } from "./local-sessions.js";
2
1
  export { localMedia, MediaStore } from "../adapters/media.js";
3
2
  export { jsonlObserver } from "../adapters/observe.js";
4
3
  export { piModel } from "../model/pi-model.js";
5
4
  export { projectAsset } from "../assets.js";
5
+ export { probeSandboxBackends, } from "../sandbox/select.js";
@@ -0,0 +1,61 @@
1
+ import { type AgentManifest, type CapabilityManifest, type SandboxToolName } from "@nylorun/core/define";
2
+ import type { Store } from "../core/store.js";
3
+ import { type SandboxSelection, type SandboxSelectionReport } from "./select.js";
4
+ import { type SandboxToolOutcome } from "./tools.js";
5
+ import type { SandboxBackend } from "./types.js";
6
+ export interface SandboxSessionRef {
7
+ readonly id: string;
8
+ readonly activeTurnId: string | null;
9
+ readonly manifest: AgentManifest;
10
+ }
11
+ export type SandboxState = "creating" | "running" | "stopped";
12
+ interface SandboxRecord {
13
+ key: string;
14
+ sessionId: string;
15
+ backend: string;
16
+ image: string;
17
+ state: SandboxState;
18
+ createdAt: string;
19
+ updatedAt: string;
20
+ }
21
+ export interface SandboxManagerOptions {
22
+ /** Distinguishes this Runtime's sandboxes from other Runtimes on the same machine. */
23
+ readonly scope: string;
24
+ readonly store: Store;
25
+ readonly backends: readonly SandboxBackend[];
26
+ /** `auto`, `microsandbox` or `virtual`. Undefined means an invalid NYLORUN_SANDBOX value. */
27
+ readonly preference: string | undefined;
28
+ /** Delete sandboxes on close (the Runtime's store does not outlive the process). */
29
+ readonly ephemeral: boolean;
30
+ readonly emit: (sessionId: string, turnId: string | null, type: string, payload: unknown) => void;
31
+ }
32
+ /** The sandbox capability that owns this tool call, when the call is a built-in sandbox tool. */
33
+ export declare function sandboxCapabilityOf(manifest: AgentManifest | undefined, capabilityId: string | undefined, toolName: string | undefined): CapabilityManifest | undefined;
34
+ export declare class SandboxManager {
35
+ private readonly options;
36
+ private selection?;
37
+ private readonly live;
38
+ private readonly prefix;
39
+ private closing;
40
+ constructor(options: SandboxManagerOptions);
41
+ /** Backends are probed once, on first use, and the choice holds for the life of the Runtime. */
42
+ get ready(): Promise<SandboxSelection>;
43
+ /** Whether any sandbox was ever recorded, so startup can skip probing for agents without one. */
44
+ hasRecords(): boolean;
45
+ keyOf(sessionId: string): string;
46
+ report(): Promise<SandboxSelectionReport & {
47
+ readonly defaultImage: string;
48
+ }>;
49
+ run(session: SandboxSessionRef, capability: CapabilityManifest, toolName: SandboxToolName, input: unknown, signal: AbortSignal): Promise<SandboxToolOutcome>;
50
+ private execute;
51
+ private open;
52
+ private armIdle;
53
+ private stop;
54
+ private record;
55
+ /** The sandbox record for a session, if one was ever created. */
56
+ status(sessionId: string): SandboxRecord | undefined;
57
+ /** Delete sandboxes this Runtime owns whose session no longer exists. */
58
+ reconcile(sessionExists: (sessionId: string) => boolean): Promise<void>;
59
+ close(): Promise<void>;
60
+ }
61
+ export {};
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Sandbox Manager: owns sandbox lifecycle for the Runtime. One sandbox per session, created on
3
+ * the first sandbox tool call, stopped after its idle timeout, reattached on the next call.
4
+ * Commands against one sandbox run one at a time.
5
+ */
6
+ import { createHash } from "node:crypto";
7
+ import { isSandboxToolName, } from "@nylorun/core/define";
8
+ import { DEFAULT_SANDBOX_CPUS, DEFAULT_SANDBOX_IMAGE, describeNetwork, idleMsOf, memoryMiBOf, resolveNetwork, } from "./policy.js";
9
+ import { parseSandboxPreference, reportSelection, selectSandboxBackend, } from "./select.js";
10
+ import { runSandboxTool } from "./tools.js";
11
+ const READ_TOOLS = new Set(["read", "grep", "glob"]);
12
+ /** The sandbox capability that owns this tool call, when the call is a built-in sandbox tool. */
13
+ export function sandboxCapabilityOf(manifest, capabilityId, toolName) {
14
+ if (!manifest || !capabilityId || !toolName || !isSandboxToolName(toolName))
15
+ return undefined;
16
+ const capability = manifest.capabilities.find((item) => item.id === capabilityId);
17
+ return capability?.sandbox && capability.tools?.some((tool) => tool.name === toolName)
18
+ ? capability
19
+ : undefined;
20
+ }
21
+ export class SandboxManager {
22
+ options;
23
+ selection;
24
+ live = new Map();
25
+ prefix;
26
+ closing = false;
27
+ constructor(options) {
28
+ this.options = options;
29
+ 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
+ /** Backends are probed once, on first use, and the choice holds for the life of the Runtime. */
38
+ get ready() {
39
+ if (!this.selection) {
40
+ const preference = parseSandboxPreference(this.options.preference);
41
+ this.selection = preference
42
+ ? selectSandboxBackend(this.options.backends, preference)
43
+ : Promise.resolve({
44
+ preference: "auto",
45
+ reason: `NYLORUN_SANDBOX='${this.options.preference}' is not valid; use auto, microsandbox or virtual`,
46
+ probes: [],
47
+ });
48
+ }
49
+ return this.selection;
50
+ }
51
+ /** 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;
54
+ }
55
+ keyOf(sessionId) {
56
+ return this.prefix + createHash("sha256").update(sessionId).digest("hex").slice(0, 16);
57
+ }
58
+ async report() {
59
+ return { ...reportSelection(await this.ready), defaultImage: DEFAULT_SANDBOX_IMAGE };
60
+ }
61
+ async run(session, capability, toolName, input, signal) {
62
+ const selection = await this.ready;
63
+ const backend = selection.backend;
64
+ if (!backend)
65
+ return {
66
+ kind: "failed",
67
+ code: "sandbox.unavailable",
68
+ message: `No sandbox is available: ${selection.reason}. Run \`npx nylorun doctor sandbox\` for options.`,
69
+ };
70
+ const key = this.keyOf(session.id);
71
+ const spec = {
72
+ key,
73
+ image: capability.sandbox?.image ?? DEFAULT_SANDBOX_IMAGE,
74
+ cpus: capability.sandbox?.resources?.cpus ?? DEFAULT_SANDBOX_CPUS,
75
+ memoryMiB: memoryMiBOf(capability.sandbox),
76
+ network: resolveNetwork(capability.sandbox),
77
+ };
78
+ const unmet = backend.unmet(spec);
79
+ if (unmet)
80
+ return {
81
+ kind: "failed",
82
+ code: "sandbox.unavailable",
83
+ message: `This agent's sandbox cannot run on the ${backend.name} backend: ${unmet}.`,
84
+ };
85
+ let live = this.live.get(key);
86
+ if (!live) {
87
+ live = { sessionId: session.id, backend, idleMs: idleMsOf(capability.sandbox), tail: Promise.resolve() };
88
+ this.live.set(key, live);
89
+ }
90
+ const entry = live;
91
+ const task = entry.tail.then(() => this.execute(entry, session, spec, toolName, input, signal));
92
+ entry.tail = task.catch(() => undefined);
93
+ return task;
94
+ }
95
+ async execute(live, session, spec, toolName, input, signal) {
96
+ if (signal.aborted)
97
+ throw new Error("Turn cancelled");
98
+ if (this.closing)
99
+ throw new Error("Runtime is shutting down");
100
+ clearTimeout(live.timer);
101
+ if (!live.handle) {
102
+ try {
103
+ live.handle = await this.open(live, session, spec);
104
+ }
105
+ catch (error) {
106
+ 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", {
109
+ state: "stopped",
110
+ backend: live.backend.name,
111
+ error: message,
112
+ });
113
+ // Nothing ran yet, so a failed start is a definite outcome, not an uncertain one.
114
+ return {
115
+ kind: "failed",
116
+ code: "sandbox.start_failed",
117
+ message: `The sandbox could not start on ${live.backend.name} (image ${spec.image}): ${message}`,
118
+ };
119
+ }
120
+ }
121
+ const started = Date.now();
122
+ let report = {};
123
+ try {
124
+ const outcome = await runSandboxTool(live.handle, toolName, (input ?? {}), signal, (value) => {
125
+ report = value;
126
+ });
127
+ this.options.emit(session.id, session.activeTurnId, "sandbox.exec", {
128
+ tool: toolName,
129
+ ...report,
130
+ ...(report.command ? { command: report.command.slice(0, 500) } : {}),
131
+ outcome: outcome.kind,
132
+ ...(outcome.kind === "failed" ? { code: outcome.code } : {}),
133
+ durationMs: Date.now() - started,
134
+ });
135
+ return outcome;
136
+ }
137
+ catch (error) {
138
+ // The backend itself failed; reopen on the next call.
139
+ live.handle = undefined;
140
+ if (signal.aborted || !READ_TOOLS.has(toolName))
141
+ throw error;
142
+ return {
143
+ kind: "failed",
144
+ code: "sandbox.error",
145
+ message: error instanceof Error ? error.message : String(error),
146
+ };
147
+ }
148
+ finally {
149
+ this.armIdle(spec.key, live);
150
+ }
151
+ }
152
+ async open(live, session, spec) {
153
+ const existing = this.options.store.get("sandboxes", spec.key);
154
+ const payload = {
155
+ backend: live.backend.name,
156
+ isolation: live.backend.isolation,
157
+ image: spec.image,
158
+ network: describeNetwork(spec.network),
159
+ };
160
+ this.record(spec.key, session.id, live.backend.name, spec.image, "creating", existing);
161
+ this.options.emit(session.id, session.activeTurnId, "sandbox.state", {
162
+ state: "creating",
163
+ ...payload,
164
+ ...(existing ? { reattach: true } : {}),
165
+ });
166
+ 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 });
169
+ return handle;
170
+ }
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
+ async stop(key, live) {
181
+ const handle = live.handle;
182
+ if (!handle)
183
+ return;
184
+ live.handle = undefined;
185
+ try {
186
+ await handle.stop();
187
+ }
188
+ 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 });
191
+ }
192
+ }
193
+ record(key, sessionId, backend, image, state, existing = this.options.store.get("sandboxes", key)) {
194
+ 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
+ }));
204
+ }
205
+ /** The sandbox record for a session, if one was ever created. */
206
+ status(sessionId) {
207
+ return this.options.store.get("sandboxes", this.keyOf(sessionId));
208
+ }
209
+ /** Delete sandboxes this Runtime owns whose session no longer exists. */
210
+ async reconcile(sessionExists) {
211
+ const { backend } = await this.ready;
212
+ if (!backend)
213
+ return;
214
+ for (const key of await backend.list(this.prefix)) {
215
+ const record = this.options.store.get("sandboxes", key);
216
+ if (record && sessionExists(record.sessionId))
217
+ continue;
218
+ await backend.remove(key);
219
+ this.options.store.tx(() => this.options.store.delete("sandboxes", key));
220
+ }
221
+ }
222
+ async close() {
223
+ this.closing = true;
224
+ if (!this.selection)
225
+ return;
226
+ const tasks = [...this.live.entries()].map(async ([key, live]) => {
227
+ clearTimeout(live.timer);
228
+ await live.tail;
229
+ if (this.options.ephemeral) {
230
+ live.handle = undefined;
231
+ await live.backend.remove(key);
232
+ }
233
+ else
234
+ await this.stop(key, live);
235
+ });
236
+ await Promise.allSettled(tasks);
237
+ this.live.clear();
238
+ }
239
+ }
@@ -0,0 +1,13 @@
1
+ import { type SandboxManifest } from "@nylorun/core/define";
2
+ import type { ResolvedNetwork } from "./types.js";
3
+ /** Docker Official Image: Debian with bash, coreutils and Python 3; small first pull. */
4
+ export declare const DEFAULT_SANDBOX_IMAGE = "python:3.13-slim";
5
+ export declare const DEFAULT_SANDBOX_CPUS = 2;
6
+ export declare const DEFAULT_SANDBOX_MEMORY_MIB = 1024;
7
+ export declare const DEFAULT_SANDBOX_IDLE_MS: number;
8
+ /** Package registries and code hosts. Concrete names so every backend can express them. */
9
+ export declare const DEV_PRESET_HOSTS: readonly string[];
10
+ export declare function resolveNetwork(manifest: SandboxManifest | undefined): ResolvedNetwork;
11
+ export declare function idleMsOf(manifest: SandboxManifest | undefined): number;
12
+ export declare function memoryMiBOf(manifest: SandboxManifest | undefined): number;
13
+ export declare function describeNetwork(network: ResolvedNetwork): string;
@@ -0,0 +1,67 @@
1
+ import { parseSandboxDuration, parseSandboxSize, } from "@nylorun/core/define";
2
+ /** Docker Official Image: Debian with bash, coreutils and Python 3; small first pull. */
3
+ export const DEFAULT_SANDBOX_IMAGE = "python:3.13-slim";
4
+ export const DEFAULT_SANDBOX_CPUS = 2;
5
+ export const DEFAULT_SANDBOX_MEMORY_MIB = 1024;
6
+ export const DEFAULT_SANDBOX_IDLE_MS = 15 * 60_000;
7
+ /** Package registries and code hosts. Concrete names so every backend can express them. */
8
+ export const DEV_PRESET_HOSTS = Object.freeze([
9
+ // npm and yarn
10
+ "registry.npmjs.org",
11
+ "registry.yarnpkg.com",
12
+ // Python
13
+ "pypi.org",
14
+ "files.pythonhosted.org",
15
+ // Rust
16
+ "crates.io",
17
+ "index.crates.io",
18
+ "static.crates.io",
19
+ // Go
20
+ "proxy.golang.org",
21
+ "sum.golang.org",
22
+ // Debian and Ubuntu package mirrors
23
+ "deb.debian.org",
24
+ "security.debian.org",
25
+ "archive.ubuntu.com",
26
+ "security.ubuntu.com",
27
+ "ports.ubuntu.com",
28
+ // Code hosts
29
+ "github.com",
30
+ "api.github.com",
31
+ "codeload.github.com",
32
+ "objects.githubusercontent.com",
33
+ "raw.githubusercontent.com",
34
+ "gitlab.com",
35
+ ]);
36
+ export function resolveNetwork(manifest) {
37
+ const preset = manifest?.network?.preset ?? "dev";
38
+ const hosts = new Set(preset === "dev" ? DEV_PRESET_HOSTS : []);
39
+ const suffixes = new Set();
40
+ for (const entry of manifest?.network?.allow ?? []) {
41
+ const host = entry.toLowerCase();
42
+ if (host.startsWith("*."))
43
+ suffixes.add(host.slice(1));
44
+ else
45
+ hosts.add(host);
46
+ }
47
+ return Object.freeze({
48
+ preset,
49
+ hosts: Object.freeze([...hosts].sort()),
50
+ suffixes: Object.freeze([...suffixes].sort()),
51
+ });
52
+ }
53
+ export function idleMsOf(manifest) {
54
+ return (manifest?.idle && parseSandboxDuration(manifest.idle)) || DEFAULT_SANDBOX_IDLE_MS;
55
+ }
56
+ export function memoryMiBOf(manifest) {
57
+ const bytes = manifest?.resources?.memory
58
+ ? parseSandboxSize(manifest.resources.memory)
59
+ : undefined;
60
+ return bytes ? Math.max(128, Math.ceil(bytes / 1024 ** 2)) : DEFAULT_SANDBOX_MEMORY_MIB;
61
+ }
62
+ export function describeNetwork(network) {
63
+ const extra = network.preset === "dev"
64
+ ? network.hosts.length - DEV_PRESET_HOSTS.length + network.suffixes.length
65
+ : network.hosts.length + network.suffixes.length;
66
+ return extra > 0 ? `${network.preset} +${extra} host(s)` : network.preset;
67
+ }