@sandblocks/sdk 0.1.0

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 (39) hide show
  1. package/README.md +21 -0
  2. package/dist/agents.js +209 -0
  3. package/dist/bin.js +59 -0
  4. package/dist/index.js +1018 -0
  5. package/dist/output.js +54 -0
  6. package/dist/providers/custom.js +49 -0
  7. package/dist/providers/docker.js +257 -0
  8. package/dist/providers/podman.js +257 -0
  9. package/dist/providers/remote.js +341 -0
  10. package/dist/providers/unsafe-host.js +126 -0
  11. package/dist/types/agents.d.ts +63 -0
  12. package/dist/types/agents.d.ts.map +1 -0
  13. package/dist/types/bin.d.ts +3 -0
  14. package/dist/types/bin.d.ts.map +1 -0
  15. package/dist/types/index.d.ts +262 -0
  16. package/dist/types/index.d.ts.map +1 -0
  17. package/dist/types/init.d.ts +6 -0
  18. package/dist/types/init.d.ts.map +1 -0
  19. package/dist/types/orchestration.d.ts +94 -0
  20. package/dist/types/orchestration.d.ts.map +1 -0
  21. package/dist/types/output.d.ts +12 -0
  22. package/dist/types/output.d.ts.map +1 -0
  23. package/dist/types/process.d.ts +6 -0
  24. package/dist/types/process.d.ts.map +1 -0
  25. package/dist/types/providers/custom.d.ts +15 -0
  26. package/dist/types/providers/custom.d.ts.map +1 -0
  27. package/dist/types/providers/docker.d.ts +7 -0
  28. package/dist/types/providers/docker.d.ts.map +1 -0
  29. package/dist/types/providers/oci.d.ts +37 -0
  30. package/dist/types/providers/oci.d.ts.map +1 -0
  31. package/dist/types/providers/podman.d.ts +7 -0
  32. package/dist/types/providers/podman.d.ts.map +1 -0
  33. package/dist/types/providers/remote.d.ts +31 -0
  34. package/dist/types/providers/remote.d.ts.map +1 -0
  35. package/dist/types/providers/unsafe-host.d.ts +14 -0
  36. package/dist/types/providers/unsafe-host.d.ts.map +1 -0
  37. package/dist/types/types.d.ts +196 -0
  38. package/dist/types/types.d.ts.map +1 -0
  39. package/package.json +48 -0
package/dist/output.js ADDED
@@ -0,0 +1,54 @@
1
+ // src/output.ts
2
+ var Output = {
3
+ string(input) {
4
+ return { kind: "string", tag: validTag(input.tag) };
5
+ },
6
+ object(input) {
7
+ return { kind: "object", tag: validTag(input.tag), schema: input.schema };
8
+ }
9
+ };
10
+ async function extractOutput(spec, text) {
11
+ const pattern = new RegExp(`<${escapeRegex(spec.tag)}>([\\s\\S]*?)<\\/${escapeRegex(spec.tag)}>`);
12
+ const match = text.match(pattern);
13
+ if (!match)
14
+ throw new Error(`structured output tag <${spec.tag}> was not found`);
15
+ const content = match[1]?.trim();
16
+ if (content === undefined)
17
+ throw new Error(`structured output tag <${spec.tag}> is empty`);
18
+ if (spec.kind === "string")
19
+ return content;
20
+ let value;
21
+ try {
22
+ value = JSON.parse(content);
23
+ } catch {
24
+ throw new Error(`structured output <${spec.tag}> is not valid JSON`);
25
+ }
26
+ const schema = spec.schema;
27
+ if (schema["~standard"]) {
28
+ const result = await schema["~standard"].validate(value);
29
+ if (result.issues?.length)
30
+ throw new Error(`structured output validation failed: ${JSON.stringify(result.issues)}`);
31
+ return result.value;
32
+ }
33
+ if (schema.safeParse) {
34
+ const result = schema.safeParse(value);
35
+ if (!result.success)
36
+ throw new Error(`structured output validation failed: ${String(result.error)}`);
37
+ return result.data;
38
+ }
39
+ if (schema.parse)
40
+ return schema.parse(value);
41
+ throw new Error("structured output schema is unsupported");
42
+ }
43
+ function validTag(tag) {
44
+ if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(tag))
45
+ throw new Error("output tag is invalid");
46
+ return tag;
47
+ }
48
+ function escapeRegex(value) {
49
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
50
+ }
51
+ export {
52
+ extractOutput,
53
+ Output
54
+ };
@@ -0,0 +1,49 @@
1
+ // src/providers/custom.ts
2
+ function createSandboxProvider(options) {
3
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(options.kind))
4
+ throw new Error("custom provider kind is invalid");
5
+ const reconnect = options.reconnect;
6
+ return {
7
+ kind: options.kind,
8
+ capabilities: Object.freeze({ ...options.capabilities }),
9
+ create: (input) => options.create(input),
10
+ ...reconnect ? { reconnect: (input) => reconnect(input) } : {}
11
+ };
12
+ }
13
+ function createBindMountSandboxProvider(options) {
14
+ return createSandboxProvider({
15
+ ...options,
16
+ capabilities: {
17
+ bindMounts: true,
18
+ isolatedFilesystem: true,
19
+ persistent: true,
20
+ networks: false,
21
+ devices: false,
22
+ snapshots: false,
23
+ reconnect: Boolean(options.reconnect),
24
+ remote: false,
25
+ ...options.capabilities
26
+ }
27
+ });
28
+ }
29
+ function createIsolatedSandboxProvider(options) {
30
+ return createSandboxProvider({
31
+ ...options,
32
+ capabilities: {
33
+ bindMounts: false,
34
+ isolatedFilesystem: true,
35
+ persistent: true,
36
+ networks: true,
37
+ devices: false,
38
+ snapshots: false,
39
+ reconnect: Boolean(options.reconnect),
40
+ remote: true,
41
+ ...options.capabilities
42
+ }
43
+ });
44
+ }
45
+ export {
46
+ createSandboxProvider,
47
+ createIsolatedSandboxProvider,
48
+ createBindMountSandboxProvider
49
+ };
@@ -0,0 +1,257 @@
1
+ // src/providers/oci.ts
2
+ import { realpath } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+
5
+ // src/process.ts
6
+ import { spawn } from "node:child_process";
7
+ async function executeProcess(command, options = {}) {
8
+ if (!command.length || command.some((part) => !part || part.includes("\x00"))) {
9
+ throw new Error("command must contain safe non-empty arguments");
10
+ }
11
+ const startedAt = new Date().toISOString();
12
+ const [executable, ...args] = command;
13
+ if (!executable)
14
+ throw new Error("command executable is required");
15
+ const child = spawn(executable, args, {
16
+ cwd: options.cwd ?? options.hostCwd,
17
+ env: options.env ? { ...process.env, ...options.env } : process.env,
18
+ stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"]
19
+ });
20
+ if (options.stdin !== undefined)
21
+ child.stdin?.end(options.stdin);
22
+ let timedOut = false;
23
+ let stdout = "";
24
+ let stderr = "";
25
+ let callbackQueue = Promise.resolve();
26
+ child.stdout?.on("data", (chunk) => {
27
+ const text = chunk.toString();
28
+ stdout = `${stdout}${text}`.slice(-4 * 1024 * 1024);
29
+ callbackQueue = callbackQueue.then(() => options.onStdout?.(text)).then(() => {
30
+ return;
31
+ });
32
+ });
33
+ child.stderr?.on("data", (chunk) => {
34
+ const text = chunk.toString();
35
+ stderr = `${stderr}${text}`.slice(-4 * 1024 * 1024);
36
+ callbackQueue = callbackQueue.then(() => options.onStderr?.(text)).then(() => {
37
+ return;
38
+ });
39
+ });
40
+ const stop = () => child.kill("SIGTERM");
41
+ options.signal?.addEventListener("abort", stop, { once: true });
42
+ const timer = options.timeoutMs ? setTimeout(() => {
43
+ timedOut = true;
44
+ child.kill("SIGKILL");
45
+ }, options.timeoutMs) : undefined;
46
+ try {
47
+ const exitCode = await new Promise((resolve, reject) => {
48
+ child.once("error", reject);
49
+ child.once("close", (code, signal) => resolve(code ?? (signal ? 128 : 1)));
50
+ });
51
+ await callbackQueue;
52
+ if (options.signal?.aborted)
53
+ throw options.signal.reason ?? new Error("operation aborted");
54
+ return {
55
+ exitCode: timedOut ? 124 : exitCode,
56
+ stdout,
57
+ stderr,
58
+ timedOut,
59
+ startedAt,
60
+ finishedAt: new Date().toISOString()
61
+ };
62
+ } finally {
63
+ if (timer)
64
+ clearTimeout(timer);
65
+ options.signal?.removeEventListener("abort", stop);
66
+ }
67
+ }
68
+ function expandHome(path) {
69
+ if (path === "~")
70
+ return process.env.HOME ?? path;
71
+ if (path.startsWith("~/"))
72
+ return `${process.env.HOME ?? "~"}/${path.slice(2)}`;
73
+ return path;
74
+ }
75
+
76
+ // src/providers/oci.ts
77
+ var capabilities = {
78
+ bindMounts: true,
79
+ isolatedFilesystem: true,
80
+ persistent: true,
81
+ networks: true,
82
+ devices: true,
83
+ snapshots: false,
84
+ reconnect: true,
85
+ remote: false
86
+ };
87
+
88
+ class OciProvider {
89
+ capabilities = capabilities;
90
+ kind;
91
+ options;
92
+ binary;
93
+ constructor(kind, options = {}) {
94
+ this.kind = kind;
95
+ this.options = options;
96
+ this.binary = options.binary ?? kind;
97
+ }
98
+ async create(input) {
99
+ await this.preflight();
100
+ const cwd = await realpath(input.cwd);
101
+ const image = input.image ?? this.options.image;
102
+ if (!image)
103
+ throw new Error(`${this.kind} provider requires an image`);
104
+ if (!(this.options.allowUnpinnedImages ?? true) && !image.includes("@sha256:")) {
105
+ throw new Error("OCI image must be digest-pinned unless allowUnpinnedImages is enabled");
106
+ }
107
+ const id = safeName(input.id);
108
+ const existing = await this.inspect(id);
109
+ if (existing) {
110
+ await this.command(["start", id], { signal: input.signal, timeoutMs: 30000 }, true);
111
+ return this.runtime(id, cwd);
112
+ }
113
+ const uid = this.options.containerUid ?? (typeof process.getuid === "function" ? process.getuid() : 1000);
114
+ const gid = this.options.containerGid ?? (typeof process.getgid === "function" ? process.getgid() : 1000);
115
+ const networks = array(input.network ?? this.options.network ?? "none");
116
+ const args = [
117
+ "create",
118
+ "--name",
119
+ id,
120
+ "--label",
121
+ "sandblocks.sdk.managed=true",
122
+ "--label",
123
+ `sandblocks.sdk.id=${id}`,
124
+ "--user",
125
+ `${uid}:${gid}`,
126
+ "--cap-drop",
127
+ "ALL",
128
+ "--security-opt",
129
+ "no-new-privileges",
130
+ "--pids-limit",
131
+ String(input.pids ?? this.options.pids ?? 256),
132
+ "--memory",
133
+ `${input.memoryMb ?? this.options.memoryMb ?? 4096}m`,
134
+ "--cpus",
135
+ String(input.cpus ?? this.options.cpus ?? 2),
136
+ "--network",
137
+ networks[0] ?? "none",
138
+ "--tmpfs",
139
+ `/tmp:rw,nosuid,nodev,exec,size=${input.tmpfsMb ?? this.options.tmpfsMb ?? 512}m`,
140
+ "--workdir",
141
+ "/workspace"
142
+ ];
143
+ if (this.options.readOnlyRoot !== false)
144
+ args.push("--read-only");
145
+ const rootMount = { source: cwd, target: "/workspace" };
146
+ for (const mount of [rootMount, ...this.options.mounts ?? [], ...input.mounts ?? []]) {
147
+ if (!mount.source)
148
+ throw new Error("local OCI mounts require source");
149
+ const source = resolve(cwd, expandHome(mount.source));
150
+ const target = mount.target.startsWith("/") ? mount.target : resolve("/workspace", mount.target);
151
+ const label = this.options.selinuxLabel === false ? "" : `,${this.options.selinuxLabel ?? "z"}`;
152
+ args.push("--volume", `${source}:${target}:${mount.readonly ? "ro" : "rw"}${label}`);
153
+ }
154
+ for (const [key, value] of Object.entries({ ...this.options.env, ...input.env })) {
155
+ assertEnv(key, value);
156
+ args.push("--env", `${key}=${value}`);
157
+ }
158
+ for (const group of [...this.options.groups ?? [], ...input.groups ?? []])
159
+ args.push("--group-add", String(group));
160
+ for (const device of [...this.options.devices ?? [], ...input.devices ?? []])
161
+ args.push("--device", device);
162
+ for (const [key, value] of Object.entries(input.metadata ?? {}))
163
+ args.push("--label", `sandblocks.sdk.${safeLabel(key)}=${safeLabel(value)}`);
164
+ args.push(image, ...this.options.idleCommand ?? ["sh", "-lc", "trap : TERM INT; sleep infinity & wait"]);
165
+ await this.command(args, { signal: input.signal, timeoutMs: 60000 });
166
+ await this.command(["start", id], { signal: input.signal, timeoutMs: 30000 });
167
+ for (const network of networks.slice(1))
168
+ await this.command(["network", "connect", network, id], { signal: input.signal });
169
+ return this.runtime(id, cwd);
170
+ }
171
+ async reconnect(input) {
172
+ const id = safeName(input.id);
173
+ if (!await this.inspect(id))
174
+ throw new Error(`sandbox '${id}' was not found`);
175
+ await this.command(["start", id], { signal: input.signal, timeoutMs: 30000 }, true);
176
+ return this.runtime(id, resolve(input.cwd ?? process.cwd()));
177
+ }
178
+ runtime(id, cwd) {
179
+ return {
180
+ id,
181
+ provider: this.kind,
182
+ cwd,
183
+ exec: async (command, options = {}) => {
184
+ const args = ["exec"];
185
+ if (options.stdin !== undefined)
186
+ args.push("-i");
187
+ args.push("--workdir", options.cwd ?? "/workspace");
188
+ for (const [key, value] of Object.entries(options.env ?? {})) {
189
+ assertEnv(key, value);
190
+ args.push("--env", `${key}=${value}`);
191
+ }
192
+ args.push(id, ...command);
193
+ return this.command(args, options);
194
+ },
195
+ upload: async (source, destination) => {
196
+ await this.command(["cp", resolve(cwd, source), `${id}:${destination}`]);
197
+ },
198
+ download: async (source, destination) => {
199
+ await this.command(["cp", `${id}:${source}`, resolve(cwd, destination)]);
200
+ },
201
+ stop: async () => {
202
+ await this.command(["kill", id], {}, true);
203
+ },
204
+ destroy: async () => {
205
+ await this.command(["rm", "-f", "-v", id], {}, true);
206
+ }
207
+ };
208
+ }
209
+ async command(args, options = {}, allowFailure = false) {
210
+ const result = this.options.commandRunner ? await this.options.commandRunner([this.binary, ...args], options) : await executeProcess([this.binary, ...args], options);
211
+ if (result.exitCode !== 0 && !allowFailure) {
212
+ throw new Error(`${this.kind} ${args[0]} failed (${result.exitCode}): ${(result.stderr || result.stdout).slice(-2000)}`);
213
+ }
214
+ return result;
215
+ }
216
+ async inspect(id) {
217
+ return (await this.command(["inspect", id], { timeoutMs: 15000 }, true)).exitCode === 0;
218
+ }
219
+ async preflight() {
220
+ const result = this.kind === "docker" ? await this.command(["info", "--format", "{{json .SecurityOptions}}"], { timeoutMs: 15000 }, true) : await this.command(["info", "--format", "{{.Host.Security.Rootless}}"], { timeoutMs: 15000 }, true);
221
+ if (result.exitCode !== 0)
222
+ throw new Error(`${this.kind} runtime is unavailable`);
223
+ if (this.options.rootless !== false && !/rootless|true/i.test(result.stdout)) {
224
+ throw new Error(`${this.kind} provider requires a rootless runtime`);
225
+ }
226
+ }
227
+ }
228
+ function array(value) {
229
+ return Array.isArray(value) ? value : [value];
230
+ }
231
+ function safeName(value) {
232
+ const name = `sandblocks-sdk-${value}`.toLowerCase().replace(/[^a-z0-9_.-]/g, "-").slice(0, 120);
233
+ if (name.length < 3)
234
+ throw new Error("sandbox id is invalid");
235
+ return name;
236
+ }
237
+ function safeLabel(value) {
238
+ return value.replace(/[^A-Za-z0-9_.:/@+-]/g, "_").slice(0, 200);
239
+ }
240
+ function assertEnv(key, value) {
241
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || value.includes("\x00"))
242
+ throw new Error("sandbox environment is invalid");
243
+ }
244
+
245
+ // src/providers/docker.ts
246
+ class DockerProvider extends OciProvider {
247
+ constructor(options = {}) {
248
+ super("docker", options);
249
+ }
250
+ }
251
+ function docker(options = {}) {
252
+ return new DockerProvider(options);
253
+ }
254
+ export {
255
+ docker,
256
+ DockerProvider
257
+ };
@@ -0,0 +1,257 @@
1
+ // src/providers/oci.ts
2
+ import { realpath } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+
5
+ // src/process.ts
6
+ import { spawn } from "node:child_process";
7
+ async function executeProcess(command, options = {}) {
8
+ if (!command.length || command.some((part) => !part || part.includes("\x00"))) {
9
+ throw new Error("command must contain safe non-empty arguments");
10
+ }
11
+ const startedAt = new Date().toISOString();
12
+ const [executable, ...args] = command;
13
+ if (!executable)
14
+ throw new Error("command executable is required");
15
+ const child = spawn(executable, args, {
16
+ cwd: options.cwd ?? options.hostCwd,
17
+ env: options.env ? { ...process.env, ...options.env } : process.env,
18
+ stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"]
19
+ });
20
+ if (options.stdin !== undefined)
21
+ child.stdin?.end(options.stdin);
22
+ let timedOut = false;
23
+ let stdout = "";
24
+ let stderr = "";
25
+ let callbackQueue = Promise.resolve();
26
+ child.stdout?.on("data", (chunk) => {
27
+ const text = chunk.toString();
28
+ stdout = `${stdout}${text}`.slice(-4 * 1024 * 1024);
29
+ callbackQueue = callbackQueue.then(() => options.onStdout?.(text)).then(() => {
30
+ return;
31
+ });
32
+ });
33
+ child.stderr?.on("data", (chunk) => {
34
+ const text = chunk.toString();
35
+ stderr = `${stderr}${text}`.slice(-4 * 1024 * 1024);
36
+ callbackQueue = callbackQueue.then(() => options.onStderr?.(text)).then(() => {
37
+ return;
38
+ });
39
+ });
40
+ const stop = () => child.kill("SIGTERM");
41
+ options.signal?.addEventListener("abort", stop, { once: true });
42
+ const timer = options.timeoutMs ? setTimeout(() => {
43
+ timedOut = true;
44
+ child.kill("SIGKILL");
45
+ }, options.timeoutMs) : undefined;
46
+ try {
47
+ const exitCode = await new Promise((resolve, reject) => {
48
+ child.once("error", reject);
49
+ child.once("close", (code, signal) => resolve(code ?? (signal ? 128 : 1)));
50
+ });
51
+ await callbackQueue;
52
+ if (options.signal?.aborted)
53
+ throw options.signal.reason ?? new Error("operation aborted");
54
+ return {
55
+ exitCode: timedOut ? 124 : exitCode,
56
+ stdout,
57
+ stderr,
58
+ timedOut,
59
+ startedAt,
60
+ finishedAt: new Date().toISOString()
61
+ };
62
+ } finally {
63
+ if (timer)
64
+ clearTimeout(timer);
65
+ options.signal?.removeEventListener("abort", stop);
66
+ }
67
+ }
68
+ function expandHome(path) {
69
+ if (path === "~")
70
+ return process.env.HOME ?? path;
71
+ if (path.startsWith("~/"))
72
+ return `${process.env.HOME ?? "~"}/${path.slice(2)}`;
73
+ return path;
74
+ }
75
+
76
+ // src/providers/oci.ts
77
+ var capabilities = {
78
+ bindMounts: true,
79
+ isolatedFilesystem: true,
80
+ persistent: true,
81
+ networks: true,
82
+ devices: true,
83
+ snapshots: false,
84
+ reconnect: true,
85
+ remote: false
86
+ };
87
+
88
+ class OciProvider {
89
+ capabilities = capabilities;
90
+ kind;
91
+ options;
92
+ binary;
93
+ constructor(kind, options = {}) {
94
+ this.kind = kind;
95
+ this.options = options;
96
+ this.binary = options.binary ?? kind;
97
+ }
98
+ async create(input) {
99
+ await this.preflight();
100
+ const cwd = await realpath(input.cwd);
101
+ const image = input.image ?? this.options.image;
102
+ if (!image)
103
+ throw new Error(`${this.kind} provider requires an image`);
104
+ if (!(this.options.allowUnpinnedImages ?? true) && !image.includes("@sha256:")) {
105
+ throw new Error("OCI image must be digest-pinned unless allowUnpinnedImages is enabled");
106
+ }
107
+ const id = safeName(input.id);
108
+ const existing = await this.inspect(id);
109
+ if (existing) {
110
+ await this.command(["start", id], { signal: input.signal, timeoutMs: 30000 }, true);
111
+ return this.runtime(id, cwd);
112
+ }
113
+ const uid = this.options.containerUid ?? (typeof process.getuid === "function" ? process.getuid() : 1000);
114
+ const gid = this.options.containerGid ?? (typeof process.getgid === "function" ? process.getgid() : 1000);
115
+ const networks = array(input.network ?? this.options.network ?? "none");
116
+ const args = [
117
+ "create",
118
+ "--name",
119
+ id,
120
+ "--label",
121
+ "sandblocks.sdk.managed=true",
122
+ "--label",
123
+ `sandblocks.sdk.id=${id}`,
124
+ "--user",
125
+ `${uid}:${gid}`,
126
+ "--cap-drop",
127
+ "ALL",
128
+ "--security-opt",
129
+ "no-new-privileges",
130
+ "--pids-limit",
131
+ String(input.pids ?? this.options.pids ?? 256),
132
+ "--memory",
133
+ `${input.memoryMb ?? this.options.memoryMb ?? 4096}m`,
134
+ "--cpus",
135
+ String(input.cpus ?? this.options.cpus ?? 2),
136
+ "--network",
137
+ networks[0] ?? "none",
138
+ "--tmpfs",
139
+ `/tmp:rw,nosuid,nodev,exec,size=${input.tmpfsMb ?? this.options.tmpfsMb ?? 512}m`,
140
+ "--workdir",
141
+ "/workspace"
142
+ ];
143
+ if (this.options.readOnlyRoot !== false)
144
+ args.push("--read-only");
145
+ const rootMount = { source: cwd, target: "/workspace" };
146
+ for (const mount of [rootMount, ...this.options.mounts ?? [], ...input.mounts ?? []]) {
147
+ if (!mount.source)
148
+ throw new Error("local OCI mounts require source");
149
+ const source = resolve(cwd, expandHome(mount.source));
150
+ const target = mount.target.startsWith("/") ? mount.target : resolve("/workspace", mount.target);
151
+ const label = this.options.selinuxLabel === false ? "" : `,${this.options.selinuxLabel ?? "z"}`;
152
+ args.push("--volume", `${source}:${target}:${mount.readonly ? "ro" : "rw"}${label}`);
153
+ }
154
+ for (const [key, value] of Object.entries({ ...this.options.env, ...input.env })) {
155
+ assertEnv(key, value);
156
+ args.push("--env", `${key}=${value}`);
157
+ }
158
+ for (const group of [...this.options.groups ?? [], ...input.groups ?? []])
159
+ args.push("--group-add", String(group));
160
+ for (const device of [...this.options.devices ?? [], ...input.devices ?? []])
161
+ args.push("--device", device);
162
+ for (const [key, value] of Object.entries(input.metadata ?? {}))
163
+ args.push("--label", `sandblocks.sdk.${safeLabel(key)}=${safeLabel(value)}`);
164
+ args.push(image, ...this.options.idleCommand ?? ["sh", "-lc", "trap : TERM INT; sleep infinity & wait"]);
165
+ await this.command(args, { signal: input.signal, timeoutMs: 60000 });
166
+ await this.command(["start", id], { signal: input.signal, timeoutMs: 30000 });
167
+ for (const network of networks.slice(1))
168
+ await this.command(["network", "connect", network, id], { signal: input.signal });
169
+ return this.runtime(id, cwd);
170
+ }
171
+ async reconnect(input) {
172
+ const id = safeName(input.id);
173
+ if (!await this.inspect(id))
174
+ throw new Error(`sandbox '${id}' was not found`);
175
+ await this.command(["start", id], { signal: input.signal, timeoutMs: 30000 }, true);
176
+ return this.runtime(id, resolve(input.cwd ?? process.cwd()));
177
+ }
178
+ runtime(id, cwd) {
179
+ return {
180
+ id,
181
+ provider: this.kind,
182
+ cwd,
183
+ exec: async (command, options = {}) => {
184
+ const args = ["exec"];
185
+ if (options.stdin !== undefined)
186
+ args.push("-i");
187
+ args.push("--workdir", options.cwd ?? "/workspace");
188
+ for (const [key, value] of Object.entries(options.env ?? {})) {
189
+ assertEnv(key, value);
190
+ args.push("--env", `${key}=${value}`);
191
+ }
192
+ args.push(id, ...command);
193
+ return this.command(args, options);
194
+ },
195
+ upload: async (source, destination) => {
196
+ await this.command(["cp", resolve(cwd, source), `${id}:${destination}`]);
197
+ },
198
+ download: async (source, destination) => {
199
+ await this.command(["cp", `${id}:${source}`, resolve(cwd, destination)]);
200
+ },
201
+ stop: async () => {
202
+ await this.command(["kill", id], {}, true);
203
+ },
204
+ destroy: async () => {
205
+ await this.command(["rm", "-f", "-v", id], {}, true);
206
+ }
207
+ };
208
+ }
209
+ async command(args, options = {}, allowFailure = false) {
210
+ const result = this.options.commandRunner ? await this.options.commandRunner([this.binary, ...args], options) : await executeProcess([this.binary, ...args], options);
211
+ if (result.exitCode !== 0 && !allowFailure) {
212
+ throw new Error(`${this.kind} ${args[0]} failed (${result.exitCode}): ${(result.stderr || result.stdout).slice(-2000)}`);
213
+ }
214
+ return result;
215
+ }
216
+ async inspect(id) {
217
+ return (await this.command(["inspect", id], { timeoutMs: 15000 }, true)).exitCode === 0;
218
+ }
219
+ async preflight() {
220
+ const result = this.kind === "docker" ? await this.command(["info", "--format", "{{json .SecurityOptions}}"], { timeoutMs: 15000 }, true) : await this.command(["info", "--format", "{{.Host.Security.Rootless}}"], { timeoutMs: 15000 }, true);
221
+ if (result.exitCode !== 0)
222
+ throw new Error(`${this.kind} runtime is unavailable`);
223
+ if (this.options.rootless !== false && !/rootless|true/i.test(result.stdout)) {
224
+ throw new Error(`${this.kind} provider requires a rootless runtime`);
225
+ }
226
+ }
227
+ }
228
+ function array(value) {
229
+ return Array.isArray(value) ? value : [value];
230
+ }
231
+ function safeName(value) {
232
+ const name = `sandblocks-sdk-${value}`.toLowerCase().replace(/[^a-z0-9_.-]/g, "-").slice(0, 120);
233
+ if (name.length < 3)
234
+ throw new Error("sandbox id is invalid");
235
+ return name;
236
+ }
237
+ function safeLabel(value) {
238
+ return value.replace(/[^A-Za-z0-9_.:/@+-]/g, "_").slice(0, 200);
239
+ }
240
+ function assertEnv(key, value) {
241
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || value.includes("\x00"))
242
+ throw new Error("sandbox environment is invalid");
243
+ }
244
+
245
+ // src/providers/podman.ts
246
+ class PodmanProvider extends OciProvider {
247
+ constructor(options = {}) {
248
+ super("podman", options);
249
+ }
250
+ }
251
+ function podman(options = {}) {
252
+ return new PodmanProvider(options);
253
+ }
254
+ export {
255
+ podman,
256
+ PodmanProvider
257
+ };