@omg-dev/sandbox 0.4.27 → 0.4.29

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/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as SandboxClient, a as compileStartScript, c as download, d as templateContentVersionRef, f as templateDefinitionHash, g as SandboxApiError, h as waitForSnapshotUpload, i as check, l as files, m as templateVersionRef, n as apt, o as compileStep, p as templateRuntimeContract, r as bakeTemplate, s as defineTemplate, t as applyTemplate, u as run, v as assertTemplateId } from "./templates-D2llpqYs.mjs";
1
+ import { _ as SandboxClient, a as compileStartScript, c as download, d as templateContentVersionRef, f as templateDefinitionHash, g as SandboxApiError, h as waitForSnapshotUpload, i as check, l as files, m as templateVersionRef, n as apt, o as compileStep, p as templateRuntimeContract, r as bakeTemplate, s as defineTemplate, t as applyTemplate, u as run, v as assertTemplateId } from "./templates-C3mBSGdT.mjs";
2
2
  export { SandboxApiError, SandboxClient, applyTemplate, apt, assertTemplateId, bakeTemplate, check, compileStartScript, compileStep, defineTemplate, download, files, run, templateContentVersionRef, templateDefinitionHash, templateRuntimeContract, templateVersionRef, waitForSnapshotUpload };
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { readFile, readdir } from "node:fs/promises";
3
3
  import { join, relative } from "node:path";
4
4
  //#region src/client.ts
@@ -228,6 +228,67 @@ function compileStartScript(start) {
228
228
  async function assertExec(label, result) {
229
229
  if (result.exitCode !== 0) throw new Error(`${label} failed (${result.exitCode}): ${(result.stderr || result.stdout).trim().slice(-1200)}`);
230
230
  }
231
+ async function runTemplateStep(sandbox, label, script, timeoutMs) {
232
+ const workDir = `/tmp/omg-template-step-${randomUUID()}`;
233
+ const pidPath = `${workDir}/pid`;
234
+ const statusPath = `${workDir}/status`;
235
+ const stdoutPath = `${workDir}/stdout`;
236
+ const stderrPath = `${workDir}/stderr`;
237
+ const wrapper = [
238
+ "set -u",
239
+ `mkdir -p ${quote(workDir)}`,
240
+ `printf '%s\\n' \"$$\" > ${quote(pidPath)}`,
241
+ `set +e; /bin/bash -lc ${quote(script)} > ${quote(stdoutPath)} 2> ${quote(stderrPath)}`,
242
+ "code=$?",
243
+ `printf '%s\\n' \"$code\" > ${quote(`${statusPath}.tmp`)}`,
244
+ `mv ${quote(`${statusPath}.tmp`)} ${quote(statusPath)}`,
245
+ "exit \"$code\""
246
+ ].join("\n");
247
+ const prepared = await sandbox.shell(`mkdir -p ${quote(workDir)}`, { timeoutMs: 1e4 });
248
+ await assertExec(`${label} workspace preparation`, prepared);
249
+ const deadline = Date.now() + timeoutMs;
250
+ const launchDeadline = Math.min(deadline, Date.now() + 1e4);
251
+ try {
252
+ if (!(await sandbox.shell(wrapper, { detached: true })).commandId) throw new Error(`${label} did not start as a detached guest command`);
253
+ while (Date.now() < deadline) {
254
+ const probe = await sandbox.shell([
255
+ `if test -f ${quote(statusPath)}; then`,
256
+ " printf 'done\\n'",
257
+ ` cat ${quote(statusPath)}`,
258
+ `elif test -f ${quote(pidPath)} && kill -0 \"$(cat ${quote(pidPath)})\" 2>/dev/null; then`,
259
+ " printf 'running\\n'",
260
+ `elif test ! -f ${quote(pidPath)}; then`,
261
+ " printf 'starting\\n'",
262
+ "else",
263
+ " printf 'lost\\n'",
264
+ "fi"
265
+ ].join("\n"), { timeoutMs: 1e4 });
266
+ await assertExec(`${label} status probe`, probe);
267
+ const [state, rawExitCode] = probe.stdout.trim().split(/\s+/, 2);
268
+ if (state === "done") {
269
+ const exitCode = Number(rawExitCode);
270
+ if (!Number.isInteger(exitCode)) throw new Error(`${label} wrote an invalid exit status: ${JSON.stringify(rawExitCode)}`);
271
+ const stdout = await sandbox.shell(`cat ${quote(stdoutPath)} 2>/dev/null || true`, { timeoutMs: 1e4 });
272
+ const stderr = await sandbox.shell(`cat ${quote(stderrPath)} 2>/dev/null || true`, { timeoutMs: 1e4 });
273
+ await assertExec(`${label} stdout read`, stdout);
274
+ await assertExec(`${label} stderr read`, stderr);
275
+ return {
276
+ exitCode,
277
+ stdout: stdout.stdout,
278
+ stderr: stderr.stdout
279
+ };
280
+ }
281
+ if (state === "lost") throw new Error(`${label} guest process exited without a completion record`);
282
+ if (state === "starting" && Date.now() >= launchDeadline) throw new Error(`${label} guest process never initialized`);
283
+ if (state !== "running" && state !== "starting") throw new Error(`${label} returned an invalid guest state: ${JSON.stringify(state)}`);
284
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
285
+ }
286
+ await sandbox.shell(`test ! -f ${quote(pidPath)} || kill \"$(cat ${quote(pidPath)})\" 2>/dev/null || true`, { timeoutMs: 1e4 }).catch(() => {});
287
+ throw new Error(`${label} exceeded its ${timeoutMs}ms guest execution deadline`);
288
+ } finally {
289
+ await sandbox.shell(`rm -rf ${quote(workDir)}`, { timeoutMs: 1e4 }).catch(() => {});
290
+ }
291
+ }
231
292
  const FILES_STEP_SKIP_NAMES = new Set([".gitkeep", ".DS_Store"]);
232
293
  async function walkSourceDir(dir) {
233
294
  const out = [];
@@ -272,7 +333,8 @@ async function applyTemplate(sandbox, definition, onLog = () => {}) {
272
333
  continue;
273
334
  }
274
335
  const compiled = compileStep(step);
275
- await assertExec(`template step ${index + 1}`, await sandbox.shell(compiled.script, { timeoutMs: compiled.timeoutMs ?? 10 * 6e4 }));
336
+ const label = `template step ${index + 1}`;
337
+ await assertExec(label, await runTemplateStep(sandbox, label, compiled.script, compiled.timeoutMs ?? 10 * 6e4));
276
338
  }
277
339
  if (definition.start) await sandbox.writeFiles([{
278
340
  path: "/home/user/.omg/template/bootstrap.sh",
@@ -1,2 +1,2 @@
1
- import { a as compileStartScript, c as download, d as templateContentVersionRef, f as templateDefinitionHash, h as waitForSnapshotUpload, i as check, l as files, m as templateVersionRef, n as apt, o as compileStep, p as templateRuntimeContract, r as bakeTemplate, s as defineTemplate, t as applyTemplate, u as run } from "./templates-D2llpqYs.mjs";
1
+ import { a as compileStartScript, c as download, d as templateContentVersionRef, f as templateDefinitionHash, h as waitForSnapshotUpload, i as check, l as files, m as templateVersionRef, n as apt, o as compileStep, p as templateRuntimeContract, r as bakeTemplate, s as defineTemplate, t as applyTemplate, u as run } from "./templates-C3mBSGdT.mjs";
2
2
  export { applyTemplate, apt, bakeTemplate, check, compileStartScript, compileStep, defineTemplate, download, files, run, templateContentVersionRef, templateDefinitionHash, templateRuntimeContract, templateVersionRef, waitForSnapshotUpload };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omg-dev/sandbox",
3
- "version": "0.4.27",
3
+ "version": "0.4.29",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -87,28 +87,80 @@ describe("sandbox templates", () => {
87
87
  await expect(applyTemplate(sandbox, definition)).rejects.toThrow();
88
88
  });
89
89
 
90
+ test("applyTemplate runs install commands out of band from the CDN request window", async () => {
91
+ const calls: Array<{ script: string; detached?: boolean }> = [];
92
+ const writes: Array<Array<{ path: string; content: Uint8Array | string; mode?: number }>> = [];
93
+ const sandbox: Sandbox = {
94
+ id: "fake",
95
+ exec: async () => ({ stdout: "", stderr: "", exitCode: 0 }),
96
+ shell: async (script, options = {}) => {
97
+ calls.push({ script, detached: options.detached });
98
+ if (options.detached) return { stdout: "", stderr: "", exitCode: 0, commandId: "cmd-1" };
99
+ if (script.includes("printf 'done")) return { stdout: "done\n0\n", stderr: "", exitCode: 0 };
100
+ if (script.includes("/stdout")) return { stdout: "installed\n", stderr: "", exitCode: 0 };
101
+ if (script.includes("/stderr")) return { stdout: "", stderr: "", exitCode: 0 };
102
+ return { stdout: "", stderr: "", exitCode: 0 };
103
+ },
104
+ writeFiles: async (batch) => { writes.push(batch); },
105
+ snapshot: async () => ({ id: "snap", sizeBytes: 0, createdAt: new Date(0).toISOString() }),
106
+ stop: async () => {},
107
+ };
108
+
109
+ await applyTemplate(sandbox, defineTemplate({
110
+ id: "async-install",
111
+ version: "1",
112
+ title: "async install",
113
+ install: [run({ command: "sleep 120 && echo installed" })],
114
+ }));
115
+
116
+ const detached = calls.find((call) => call.detached);
117
+ expect(detached).toBeDefined();
118
+ expect(detached?.script).toContain("sleep 120 && echo installed");
119
+ expect(calls.some((call) => call.script.includes("printf 'done"))).toBe(true);
120
+ expect(calls.some((call) => call.script.includes("printf 'starting"))).toBe(true);
121
+ expect(writes.some((batch) => batch.some((file) => file.path.endsWith("definition.json")))).toBe(true);
122
+ });
123
+
124
+ test("applyTemplate preserves a detached install's failing exit status and stderr", async () => {
125
+ const sandbox = fakeSandbox();
126
+ sandbox.shell = async (script, options = {}) => {
127
+ if (options.detached) return { stdout: "", stderr: "", exitCode: 0, commandId: "cmd-2" };
128
+ if (script.includes("printf 'done")) return { stdout: "done\n42\n", stderr: "", exitCode: 0 };
129
+ if (script.includes("/stdout")) return { stdout: "", stderr: "", exitCode: 0 };
130
+ if (script.includes("/stderr")) return { stdout: "package exploded\n", stderr: "", exitCode: 0 };
131
+ return { stdout: "", stderr: "", exitCode: 0 };
132
+ };
133
+
134
+ await expect(applyTemplate(sandbox, defineTemplate({
135
+ id: "async-failure",
136
+ version: "1",
137
+ title: "async failure",
138
+ install: [run({ command: "exit 42" })],
139
+ }))).rejects.toThrow("package exploded");
140
+ });
141
+
90
142
  test("LFG owns every runtime dependency and zero-config transport default", () => {
91
143
  const lfg = agentTemplates.lfg;
92
- expect(templateVersionRef(lfg)).toBe("agent-lfg-v14");
144
+ expect(templateVersionRef(lfg)).toBe("agent-lfg-v17");
93
145
  expect(lfg.install).toContainEqual(apt.packages(["tmux"]));
94
146
  expect(lfg.checks).toContainEqual(check.command("tmux"));
95
147
  expect(lfg.start?.env?.LIVE_TRANSPORT).toBe("ws");
96
148
  expect(lfg.start?.readiness?.port).toBe(8766);
97
149
  expect(templateRuntimeContract(lfg, templateVersionRef(lfg))).toEqual({
98
- immutableRef: "agent-lfg-v14",
150
+ immutableRef: "agent-lfg-v17",
99
151
  startCommand: "exec /home/user/.omg/template/bootstrap.sh",
100
152
  readinessPort: 8766,
101
153
  readinessPath: "/",
102
- ports: [8766],
154
+ ports: [8766, 5173],
103
155
  });
104
156
  });
105
157
 
106
- test("LFG bakes the react-ts scaffold into /home/user/app at bake time", () => {
158
+ test("LFG bakes the react-ts scaffold into /home/user/project at bake time", () => {
107
159
  const lfg = agentTemplates.lfg;
108
160
  const filesSteps = lfg.install.filter((step) => step.kind === "files");
109
161
  expect(filesSteps).toHaveLength(1);
110
162
  const scaffold = filesSteps[0] as Extract<(typeof filesSteps)[number], { kind: "files" }>;
111
- expect(scaffold.destination).toBe("/home/user/app");
163
+ expect(scaffold.destination).toBe("/home/user/project");
112
164
  expect(scaffold.sourceDir.endsWith(join("templates", "react-ts"))).toBe(true);
113
165
  // recipe.sh (shadcn init) must run against the baked scaffold, not just
114
166
  // land on disk unused — otherwise Computers still improvise shadcn init
@@ -117,6 +169,6 @@ describe("sandbox templates", () => {
117
169
  .filter((step): step is Extract<typeof step, { kind: "run" }> => step.kind === "run")
118
170
  .map((step) => step.command);
119
171
  expect(commands.some((c) => c.includes("recipe.sh"))).toBe(true);
120
- expect(lfg.metadata?.lfgRelease).toBe("v0.1.39");
172
+ expect(lfg.metadata?.lfgRelease).toBe("v0.1.40");
121
173
  });
122
174
  });
package/src/templates.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { readdir, readFile } from "node:fs/promises";
3
3
  import { join, relative } from "node:path";
4
4
  import type { Sandbox, SandboxClient, Snapshot } from "./client.js";
@@ -203,6 +203,77 @@ async function assertExec(label: string, result: { exitCode: number; stdout: str
203
203
  if (result.exitCode !== 0) throw new Error(`${label} failed (${result.exitCode}): ${(result.stderr || result.stdout).trim().slice(-1200)}`);
204
204
  }
205
205
 
206
+ // Template installs routinely outlive Cloudflare's request window (apt and
207
+ // package-manager steps can take several minutes). A blocking /exec request
208
+ // therefore makes a healthy guest look failed with a 524. Launch the command
209
+ // inside the guest and make its on-disk completion record the authority; every
210
+ // control-plane request below is short and bounded, while exit status and logs
211
+ // still fail loud exactly once.
212
+ async function runTemplateStep(
213
+ sandbox: Sandbox,
214
+ label: string,
215
+ script: string,
216
+ timeoutMs: number,
217
+ ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
218
+ const workDir = `/tmp/omg-template-step-${randomUUID()}`;
219
+ const pidPath = `${workDir}/pid`;
220
+ const statusPath = `${workDir}/status`;
221
+ const stdoutPath = `${workDir}/stdout`;
222
+ const stderrPath = `${workDir}/stderr`;
223
+ const wrapper = [
224
+ "set -u",
225
+ `mkdir -p ${quote(workDir)}`,
226
+ `printf '%s\\n' \"$$\" > ${quote(pidPath)}`,
227
+ `set +e; /bin/bash -lc ${quote(script)} > ${quote(stdoutPath)} 2> ${quote(stderrPath)}`,
228
+ "code=$?",
229
+ `printf '%s\\n' \"$code\" > ${quote(`${statusPath}.tmp`)}`,
230
+ `mv ${quote(`${statusPath}.tmp`)} ${quote(statusPath)}`,
231
+ "exit \"$code\"",
232
+ ].join("\n");
233
+
234
+ const prepared = await sandbox.shell(`mkdir -p ${quote(workDir)}`, { timeoutMs: 10_000 });
235
+ await assertExec(`${label} workspace preparation`, prepared);
236
+ const deadline = Date.now() + timeoutMs;
237
+ const launchDeadline = Math.min(deadline, Date.now() + 10_000);
238
+ try {
239
+ const started = await sandbox.shell(wrapper, { detached: true });
240
+ if (!started.commandId) throw new Error(`${label} did not start as a detached guest command`);
241
+ while (Date.now() < deadline) {
242
+ const probe = await sandbox.shell([
243
+ `if test -f ${quote(statusPath)}; then`,
244
+ " printf 'done\\n'",
245
+ ` cat ${quote(statusPath)}`,
246
+ `elif test -f ${quote(pidPath)} && kill -0 \"$(cat ${quote(pidPath)})\" 2>/dev/null; then`,
247
+ " printf 'running\\n'",
248
+ `elif test ! -f ${quote(pidPath)}; then`,
249
+ " printf 'starting\\n'",
250
+ "else",
251
+ " printf 'lost\\n'",
252
+ "fi",
253
+ ].join("\n"), { timeoutMs: 10_000 });
254
+ await assertExec(`${label} status probe`, probe);
255
+ const [state, rawExitCode] = probe.stdout.trim().split(/\s+/, 2);
256
+ if (state === "done") {
257
+ const exitCode = Number(rawExitCode);
258
+ if (!Number.isInteger(exitCode)) throw new Error(`${label} wrote an invalid exit status: ${JSON.stringify(rawExitCode)}`);
259
+ const stdout = await sandbox.shell(`cat ${quote(stdoutPath)} 2>/dev/null || true`, { timeoutMs: 10_000 });
260
+ const stderr = await sandbox.shell(`cat ${quote(stderrPath)} 2>/dev/null || true`, { timeoutMs: 10_000 });
261
+ await assertExec(`${label} stdout read`, stdout);
262
+ await assertExec(`${label} stderr read`, stderr);
263
+ return { exitCode, stdout: stdout.stdout, stderr: stderr.stdout };
264
+ }
265
+ if (state === "lost") throw new Error(`${label} guest process exited without a completion record`);
266
+ if (state === "starting" && Date.now() >= launchDeadline) throw new Error(`${label} guest process never initialized`);
267
+ if (state !== "running" && state !== "starting") throw new Error(`${label} returned an invalid guest state: ${JSON.stringify(state)}`);
268
+ await new Promise((resolve) => setTimeout(resolve, 1_000));
269
+ }
270
+ await sandbox.shell(`test ! -f ${quote(pidPath)} || kill \"$(cat ${quote(pidPath)})\" 2>/dev/null || true`, { timeoutMs: 10_000 }).catch(() => {});
271
+ throw new Error(`${label} exceeded its ${timeoutMs}ms guest execution deadline`);
272
+ } finally {
273
+ await sandbox.shell(`rm -rf ${quote(workDir)}`, { timeoutMs: 10_000 }).catch(() => {});
274
+ }
275
+ }
276
+
206
277
  const FILES_STEP_SKIP_NAMES = new Set([".gitkeep", ".DS_Store"]);
207
278
 
208
279
  async function walkSourceDir(dir: string): Promise<Array<{ relPath: string; buf: Buffer }>> {
@@ -251,7 +322,8 @@ export async function applyTemplate(sandbox: Sandbox, definition: SandboxTemplat
251
322
  continue;
252
323
  }
253
324
  const compiled = compileStep(step);
254
- await assertExec(`template step ${index + 1}`, await sandbox.shell(compiled.script, { timeoutMs: compiled.timeoutMs ?? 10 * 60_000 }));
325
+ const label = `template step ${index + 1}`;
326
+ await assertExec(label, await runTemplateStep(sandbox, label, compiled.script, compiled.timeoutMs ?? 10 * 60_000));
255
327
  }
256
328
  if (definition.start) {
257
329
  await sandbox.writeFiles([{ path: "/home/user/.omg/template/bootstrap.sh", content: compileStartScript(definition.start), mode: 0o755 }]);