@nylorun/runtime 0.7.0-beta → 0.9.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 +50 -0
- package/README.md +7 -5
- package/dist/adapters/sandbox/microsandbox.d.ts +2 -0
- package/dist/adapters/sandbox/microsandbox.js +238 -0
- package/dist/adapters/sandbox/virtual.d.ts +4 -0
- package/dist/adapters/sandbox/virtual.js +125 -0
- package/dist/configuration.d.ts +1 -1
- package/dist/configuration.js +1 -1
- package/dist/core/executors.d.ts +32 -0
- package/dist/core/executors.js +60 -0
- package/dist/core/main.js +7 -12
- package/dist/core/runtime.d.ts +24 -1
- package/dist/core/runtime.js +364 -43
- package/dist/core/store.d.ts +14 -1
- package/dist/core/store.js +59 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/mcp/pool.d.ts +13 -1
- package/dist/mcp/pool.js +53 -19
- package/dist/mcp/snapshot.d.ts +4 -0
- package/dist/model/catalog.d.ts +12 -0
- package/dist/model/catalog.js +29 -0
- package/dist/model/models.d.ts +3 -1
- package/dist/model/models.js +4 -2
- package/dist/model/pi-model.d.ts +9 -3
- package/dist/model/pi-model.js +47 -11
- package/dist/model/settings.d.ts +0 -2
- package/dist/model/settings.js +0 -47
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/sandbox/manager.d.ts +61 -0
- package/dist/sandbox/manager.js +239 -0
- package/dist/sandbox/policy.d.ts +13 -0
- package/dist/sandbox/policy.js +67 -0
- package/dist/sandbox/select.d.ts +35 -0
- package/dist/sandbox/select.js +76 -0
- package/dist/sandbox/tools.d.ts +25 -0
- package/dist/sandbox/tools.js +165 -0
- package/dist/sandbox/types.d.ts +66 -0
- package/dist/vault/service.d.ts +41 -1
- package/dist/vault/service.js +291 -4
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/package.json +9 -4
- package/dist/config.d.ts +0 -20
- package/dist/model/defaults.d.ts +0 -17
- package/dist/model/defaults.js +0 -21
- package/dist/model/http-model.d.ts +0 -12
- package/dist/model/http-model.js +0 -299
- package/dist/node/local-sessions.d.ts +0 -5
- package/dist/node/local-sessions.js +0 -174
- package/dist/server/ag-ui.d.ts +0 -8
- package/dist/server/ag-ui.js +0 -76
- package/dist/server/delivery.d.ts +0 -24
- package/dist/server/delivery.js +0 -107
- package/dist/server/digests.d.ts +0 -13
- package/dist/server/digests.js +0 -39
- package/dist/server/host.d.ts +0 -70
- package/dist/server/host.js +0 -647
- package/dist/session/api.d.ts +0 -10
- package/dist/session/api.js +0 -15
- package/dist/session/default.d.ts +0 -5
- package/dist/session/default.js +0 -30
- package/dist/session/handle.d.ts +0 -27
- package/dist/session/handle.js +0 -199
- package/dist/session/index.d.ts +0 -2
- package/dist/session/index.js +0 -2
- package/dist/sessions/host.d.ts +0 -39
- package/dist/sessions/host.js +0 -359
- package/dist/sessions/store.d.ts +0 -41
- package/dist/sessions/store.js +0 -33
- /package/dist/{config.js → sandbox/types.js} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SandboxBackend, SandboxBackendName, SandboxIsolation, SandboxProbe } from "./types.js";
|
|
2
|
+
export type SandboxPreference = "auto" | SandboxBackendName;
|
|
3
|
+
export interface SandboxSelection {
|
|
4
|
+
readonly preference: SandboxPreference;
|
|
5
|
+
readonly backend?: SandboxBackend;
|
|
6
|
+
/** Why this backend was chosen, or why none could be. */
|
|
7
|
+
readonly reason: string;
|
|
8
|
+
readonly probes: readonly SandboxProbe[];
|
|
9
|
+
}
|
|
10
|
+
/** Serializable view of a selection for the CLI and `GET /v1/host/sandbox`. */
|
|
11
|
+
export interface SandboxSelectionReport {
|
|
12
|
+
readonly preference: SandboxPreference;
|
|
13
|
+
readonly backend: SandboxBackendName | null;
|
|
14
|
+
readonly isolation: SandboxIsolation | null;
|
|
15
|
+
readonly reason: string;
|
|
16
|
+
readonly probes: readonly SandboxProbe[];
|
|
17
|
+
}
|
|
18
|
+
/** The default order: strongest isolation first, then the backend that always works. */
|
|
19
|
+
export declare function defaultSandboxBackends(options: {
|
|
20
|
+
readonly root: string;
|
|
21
|
+
}): SandboxBackend[];
|
|
22
|
+
export declare function parseSandboxPreference(value: string | undefined): SandboxPreference | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Probe backends in order and pick one. An explicit preference never falls back, so a
|
|
25
|
+
* production Runtime cannot silently run with weaker isolation than it was configured for.
|
|
26
|
+
*/
|
|
27
|
+
export declare function selectSandboxBackend(backends: readonly SandboxBackend[], preference: SandboxPreference): Promise<SandboxSelection>;
|
|
28
|
+
export declare function reportSelection(selection: SandboxSelection): SandboxSelectionReport;
|
|
29
|
+
/**
|
|
30
|
+
* Probe this machine the way a Runtime would at startup. Used by `nylorun doctor sandbox`.
|
|
31
|
+
* Nothing is created or booted.
|
|
32
|
+
*/
|
|
33
|
+
export declare function probeSandboxBackends(options?: {
|
|
34
|
+
readonly preference?: string;
|
|
35
|
+
}): Promise<SandboxSelectionReport>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { microsandboxBackend } from "../adapters/sandbox/microsandbox.js";
|
|
4
|
+
import { virtualBackend } from "../adapters/sandbox/virtual.js";
|
|
5
|
+
const PREFERENCES = ["auto", "microsandbox", "virtual"];
|
|
6
|
+
/** The default order: strongest isolation first, then the backend that always works. */
|
|
7
|
+
export function defaultSandboxBackends(options) {
|
|
8
|
+
return [microsandboxBackend(), virtualBackend({ root: options.root })];
|
|
9
|
+
}
|
|
10
|
+
export function parseSandboxPreference(value) {
|
|
11
|
+
const normalized = (value ?? "auto").trim().toLowerCase() || "auto";
|
|
12
|
+
return PREFERENCES.includes(normalized)
|
|
13
|
+
? normalized
|
|
14
|
+
: undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Probe backends in order and pick one. An explicit preference never falls back, so a
|
|
18
|
+
* production Runtime cannot silently run with weaker isolation than it was configured for.
|
|
19
|
+
*/
|
|
20
|
+
export async function selectSandboxBackend(backends, preference) {
|
|
21
|
+
const probes = [];
|
|
22
|
+
for (const backend of backends)
|
|
23
|
+
probes.push(await backend.probe());
|
|
24
|
+
const probeOf = (name) => probes.find((probe) => probe.name === name);
|
|
25
|
+
if (preference !== "auto") {
|
|
26
|
+
const backend = backends.find((item) => item.name === preference);
|
|
27
|
+
const probe = probeOf(preference);
|
|
28
|
+
if (backend && probe?.available)
|
|
29
|
+
return { preference, backend, reason: `${preference} selected by NYLORUN_SANDBOX`, probes };
|
|
30
|
+
return {
|
|
31
|
+
preference,
|
|
32
|
+
reason: `NYLORUN_SANDBOX=${preference} but ${preference} is unavailable: ${probe?.reason ?? "unknown backend"}. Fix it, or unset NYLORUN_SANDBOX to use the best available backend`,
|
|
33
|
+
probes,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const index = backends.findIndex((_, i) => probes[i].available);
|
|
37
|
+
if (index < 0)
|
|
38
|
+
return {
|
|
39
|
+
preference,
|
|
40
|
+
reason: `no sandbox backend is available (${probes.map((probe) => `${probe.name}: ${probe.reason}`).join("; ")})`,
|
|
41
|
+
probes,
|
|
42
|
+
};
|
|
43
|
+
const skipped = probes.slice(0, index).map((probe) => `${probe.name} unavailable: ${probe.reason}`);
|
|
44
|
+
return {
|
|
45
|
+
preference,
|
|
46
|
+
backend: backends[index],
|
|
47
|
+
reason: skipped.length ? skipped.join("; ") : probes[index].reason ?? backends[index].name,
|
|
48
|
+
probes,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function reportSelection(selection) {
|
|
52
|
+
return {
|
|
53
|
+
preference: selection.preference,
|
|
54
|
+
backend: selection.backend?.name ?? null,
|
|
55
|
+
isolation: selection.backend?.isolation ?? null,
|
|
56
|
+
reason: selection.reason,
|
|
57
|
+
probes: selection.probes,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Probe this machine the way a Runtime would at startup. Used by `nylorun doctor sandbox`.
|
|
62
|
+
* Nothing is created or booted.
|
|
63
|
+
*/
|
|
64
|
+
export async function probeSandboxBackends(options = {}) {
|
|
65
|
+
const preference = parseSandboxPreference(options.preference ?? process.env.NYLORUN_SANDBOX);
|
|
66
|
+
const backends = defaultSandboxBackends({ root: join(tmpdir(), "nylorun-sandbox-probe") });
|
|
67
|
+
if (!preference)
|
|
68
|
+
return {
|
|
69
|
+
preference: "auto",
|
|
70
|
+
backend: null,
|
|
71
|
+
isolation: null,
|
|
72
|
+
reason: "NYLORUN_SANDBOX must be auto, microsandbox or virtual",
|
|
73
|
+
probes: await Promise.all(backends.map((backend) => backend.probe())),
|
|
74
|
+
};
|
|
75
|
+
return reportSelection(await selectSandboxBackend(backends, preference));
|
|
76
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type SandboxToolName } from "@nylorun/core/define";
|
|
2
|
+
import type { SandboxHandle } from "./types.js";
|
|
3
|
+
export type SandboxToolOutcome = {
|
|
4
|
+
readonly kind: "completed";
|
|
5
|
+
readonly output: unknown;
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: "failed";
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly message: string;
|
|
10
|
+
};
|
|
11
|
+
/** What the Runtime reports about a tool call on the session stream. */
|
|
12
|
+
export interface SandboxToolReport {
|
|
13
|
+
readonly command?: string;
|
|
14
|
+
readonly path?: string;
|
|
15
|
+
readonly exitCode?: number;
|
|
16
|
+
readonly killed?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function quote(value: string): string;
|
|
19
|
+
export declare function resolvePath(path: string): string;
|
|
20
|
+
/** Keep the head and tail of long output so the model sees both the start and the error. */
|
|
21
|
+
export declare function truncateMiddle(value: string, limit?: number): {
|
|
22
|
+
text: string;
|
|
23
|
+
truncated: boolean;
|
|
24
|
+
};
|
|
25
|
+
export declare function runSandboxTool(handle: SandboxHandle, name: SandboxToolName, input: Record<string, any>, signal: AbortSignal, report: (value: SandboxToolReport) => void): Promise<SandboxToolOutcome>;
|