@omg-dev/sandbox 0.4.30 → 0.4.32
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-
|
|
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-BLd13wPT.mjs";
|
|
2
2
|
export { SandboxApiError, SandboxClient, applyTemplate, apt, assertTemplateId, bakeTemplate, check, compileStartScript, compileStep, defineTemplate, download, files, run, templateContentVersionRef, templateDefinitionHash, templateRuntimeContract, templateVersionRef, waitForSnapshotUpload };
|
|
@@ -96,7 +96,7 @@ var SandboxClient = class {
|
|
|
96
96
|
mode: file.mode
|
|
97
97
|
})));
|
|
98
98
|
},
|
|
99
|
-
snapshot: () => this.request("POST", `/v1/sandboxes/${id}/snapshot
|
|
99
|
+
snapshot: (options = {}) => this.request("POST", `/v1/sandboxes/${id}/snapshot`, options),
|
|
100
100
|
stop: () => this.request("DELETE", `/v1/sandboxes/${id}`)
|
|
101
101
|
};
|
|
102
102
|
}
|
|
@@ -150,6 +150,7 @@ function defineTemplate(definition) {
|
|
|
150
150
|
assertTemplateId(definition.id);
|
|
151
151
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,19}$/.test(definition.version)) throw new Error("invalid template version");
|
|
152
152
|
if (!definition.title.trim()) throw new Error("template title is required");
|
|
153
|
+
if (definition.restoreKind !== "project_only" && definition.restoreKind !== "full_rootfs") throw new Error("template restoreKind must be project_only or full_rootfs");
|
|
153
154
|
for (const port of definition.ports ?? []) if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`invalid template port ${port}`);
|
|
154
155
|
return Object.freeze(definition);
|
|
155
156
|
}
|
|
@@ -172,6 +173,7 @@ function templateDefinitionHash(definition) {
|
|
|
172
173
|
function templateRuntimeContract(definition, immutableRef) {
|
|
173
174
|
return {
|
|
174
175
|
immutableRef,
|
|
176
|
+
restoreKind: definition.restoreKind,
|
|
175
177
|
startCommand: definition.start ? "exec /home/user/.omg/template/bootstrap.sh" : void 0,
|
|
176
178
|
readinessPort: definition.start?.readiness?.port,
|
|
177
179
|
readinessPath: definition.start?.readiness?.path,
|
|
@@ -218,12 +220,14 @@ function compileStep(step) {
|
|
|
218
220
|
function compileStartScript(start) {
|
|
219
221
|
const port = start.readiness?.port;
|
|
220
222
|
const timeout = start.readiness?.timeoutSeconds ?? 60;
|
|
223
|
+
const supervisorMarker = "omg-template-supervisor";
|
|
221
224
|
const foreground = commandForUser(`${shellEnvAssignments({
|
|
222
225
|
HOME: start.user ? `/home/${start.user}` : "/root",
|
|
223
226
|
...start.env ?? {}
|
|
224
227
|
}).map((assignment) => `export ${assignment}`).join("\n")}\n${`${start.cwd ? `cd ${quote(start.cwd)} && ` : ""}${start.command}`}`, start.user);
|
|
225
228
|
const probe = port ? `for _ in $(seq 1 ${timeout}); do curl -fsS -m1 -o /dev/null ${quote(`http://127.0.0.1:${port}${start.readiness?.path ?? "/"}`)} && exit 0; sleep 1; done\necho 'template service did not become ready on :${port}' >&2\nexit 1` : "exit 0";
|
|
226
|
-
|
|
229
|
+
const supervisor = `while true; do ${foreground} >>/home/user/.omg/template/start.log 2>&1 || true; sleep 2; done`;
|
|
230
|
+
return `#!/usr/bin/env bash\nset -euo pipefail\nmkdir -p /home/user/.omg/template\npidfile=/home/user/.omg/template/start.pid\npid=\"\"\nif [ -s \"$pidfile\" ]; then pid=\"$(cat \"$pidfile\" 2>/dev/null || true)\"; fi\nsupervisor_alive=false\nif [[ \"$pid\" =~ ^[0-9]+$ ]] && kill -0 \"$pid\" 2>/dev/null && tr '\\0' '\\n' < \"/proc/$pid/cmdline\" 2>/dev/null | grep -Fxq ${quote(supervisorMarker)}; then supervisor_alive=true; fi\nif [ \"$supervisor_alive\" != true ]; then\n rm -f \"$pidfile\"\n nohup /bin/bash -lc ${quote(supervisor)} ${quote(supervisorMarker)} >/dev/null 2>&1 &\n echo $! > \"$pidfile\"\nfi\n${probe}\n`;
|
|
227
231
|
}
|
|
228
232
|
async function assertExec(label, result) {
|
|
229
233
|
if (result.exitCode !== 0) throw new Error(`${label} failed (${result.exitCode}): ${(result.stderr || result.stdout).trim().slice(-1200)}`);
|
|
@@ -289,7 +293,7 @@ async function runTemplateStep(sandbox, label, script, timeoutMs) {
|
|
|
289
293
|
await sandbox.shell(`rm -rf ${quote(workDir)}`, { timeoutMs: 1e4 }).catch(() => {});
|
|
290
294
|
}
|
|
291
295
|
}
|
|
292
|
-
const FILES_STEP_SKIP_NAMES = new Set([".gitkeep", ".DS_Store"]);
|
|
296
|
+
const FILES_STEP_SKIP_NAMES = /* @__PURE__ */ new Set([".gitkeep", ".DS_Store"]);
|
|
293
297
|
async function walkSourceDir(dir) {
|
|
294
298
|
const out = [];
|
|
295
299
|
async function visit(d) {
|
|
@@ -374,7 +378,7 @@ async function bakeTemplate(client, definition, options = {}) {
|
|
|
374
378
|
let snapshotted = false;
|
|
375
379
|
try {
|
|
376
380
|
await applyTemplate(sandbox, definition, log);
|
|
377
|
-
const snapshot = await sandbox.snapshot();
|
|
381
|
+
const snapshot = await sandbox.snapshot({ restoreKind: definition.restoreKind });
|
|
378
382
|
snapshotted = true;
|
|
379
383
|
const rootfsSha = (await waitForSnapshotUpload(client, snapshot)).rootfsSha?.trim();
|
|
380
384
|
if (!rootfsSha) throw new Error(`snapshot ${snapshot.id} has no rootfsSha — cannot derive an immutable template version identity`);
|
package/dist/templates.mjs
CHANGED
|
@@ -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-
|
|
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-BLd13wPT.mjs";
|
|
2
2
|
export { applyTemplate, apt, bakeTemplate, check, compileStartScript, compileStep, defineTemplate, download, files, run, templateContentVersionRef, templateDefinitionHash, templateRuntimeContract, templateVersionRef, waitForSnapshotUpload };
|
package/package.json
CHANGED
package/src/client.test.ts
CHANGED
|
@@ -44,6 +44,7 @@ describe("template version identity", () => {
|
|
|
44
44
|
id: "my-agent",
|
|
45
45
|
version: "3",
|
|
46
46
|
title: "Mine",
|
|
47
|
+
restoreKind: "full_rootfs",
|
|
47
48
|
install: [],
|
|
48
49
|
});
|
|
49
50
|
|
|
@@ -68,7 +69,7 @@ describe("template version identity", () => {
|
|
|
68
69
|
});
|
|
69
70
|
|
|
70
71
|
test("long ids truncate to fit the 40-char registry limit", () => {
|
|
71
|
-
const long = defineTemplate({ id: "a".repeat(40), version: "12", title: "Long", install: [] });
|
|
72
|
+
const long = defineTemplate({ id: "a".repeat(40), version: "12", title: "Long", restoreKind: "full_rootfs", install: [] });
|
|
72
73
|
const ref = templateContentVersionRef(long, "rootfs-sha-1");
|
|
73
74
|
expect(ref.length).toBeLessThanOrEqual(40);
|
|
74
75
|
expect(ref).toMatch(/-v12-[0-9a-f]{8}$/);
|
|
@@ -84,6 +85,7 @@ describe("SandboxClient template bake", () => {
|
|
|
84
85
|
id: "my-agent",
|
|
85
86
|
version: "3",
|
|
86
87
|
title: "Mine",
|
|
88
|
+
restoreKind: "full_rootfs",
|
|
87
89
|
ports: [8766],
|
|
88
90
|
install: [],
|
|
89
91
|
start: { command: "bun start", readiness: { port: 8766, path: "/health" } },
|
|
@@ -101,6 +103,7 @@ describe("SandboxClient template bake", () => {
|
|
|
101
103
|
expect(calls.filter((call) => call.path.includes("/v1/templates/") && call.method === "POST").map((call) => call.body)).toEqual([
|
|
102
104
|
{
|
|
103
105
|
snapshotId: "snap-1",
|
|
106
|
+
restoreKind: "full_rootfs",
|
|
104
107
|
startCommand: "exec /home/user/.omg/template/bootstrap.sh",
|
|
105
108
|
readinessPort: 8766,
|
|
106
109
|
readinessPath: "/health",
|
|
@@ -108,6 +111,7 @@ describe("SandboxClient template bake", () => {
|
|
|
108
111
|
},
|
|
109
112
|
{
|
|
110
113
|
snapshotId: "snap-1",
|
|
114
|
+
restoreKind: "full_rootfs",
|
|
111
115
|
startCommand: "exec /home/user/.omg/template/bootstrap.sh",
|
|
112
116
|
readinessPort: 8766,
|
|
113
117
|
readinessPath: "/health",
|
|
@@ -124,6 +128,7 @@ describe("SandboxClient template bake", () => {
|
|
|
124
128
|
id: "my-agent",
|
|
125
129
|
version: "3",
|
|
126
130
|
title: "Mine",
|
|
131
|
+
restoreKind: "full_rootfs",
|
|
127
132
|
install: [],
|
|
128
133
|
}), { systemClient });
|
|
129
134
|
const sandboxCalls = calls.filter((call) => !call.path.includes("/v1/templates/"));
|
|
@@ -133,7 +138,7 @@ describe("SandboxClient template bake", () => {
|
|
|
133
138
|
});
|
|
134
139
|
|
|
135
140
|
test("an already-published content identity is reused: latest repoints, the immutable version is never republished", async () => {
|
|
136
|
-
const definition = defineTemplate({ id: "my-agent", version: "3", title: "Mine", install: [] });
|
|
141
|
+
const definition = defineTemplate({ id: "my-agent", version: "3", title: "Mine", restoreKind: "full_rootfs", install: [] });
|
|
137
142
|
const ref = templateContentVersionRef(definition, "rootfs-sha-1");
|
|
138
143
|
const { calls, fakeFetch } = fakeInfra({ existingVersions: { [ref]: "snap-old" } });
|
|
139
144
|
const client = new SandboxClient({ baseUrl: "https://infra.example", token: "secret", ownerId: "user-123", fetch: fakeFetch });
|
|
@@ -154,7 +159,7 @@ describe("SandboxClient template bake", () => {
|
|
|
154
159
|
test("missing rootfsSha fails loud instead of publishing a collision-prone static ref", async () => {
|
|
155
160
|
const { fakeFetch } = fakeInfra({ snapshotRootfsSha: null });
|
|
156
161
|
const client = new SandboxClient({ baseUrl: "https://infra.example", token: "secret", ownerId: "user-123", fetch: fakeFetch });
|
|
157
|
-
await expect(bakeTemplate(client, defineTemplate({ id: "my-agent", version: "3", title: "Mine", install: [] })))
|
|
162
|
+
await expect(bakeTemplate(client, defineTemplate({ id: "my-agent", version: "3", title: "Mine", restoreKind: "full_rootfs", install: [] })))
|
|
158
163
|
.rejects.toThrow(/rootfsSha/);
|
|
159
164
|
});
|
|
160
165
|
});
|
package/src/client.ts
CHANGED
|
@@ -29,6 +29,10 @@ export interface Snapshot {
|
|
|
29
29
|
rootfsSha?: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export interface SnapshotOptions {
|
|
33
|
+
restoreKind?: "project_only" | "full_rootfs";
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
/** Error thrown by SandboxClient.request carrying the HTTP status, so callers
|
|
33
37
|
* can branch on well-defined statuses (e.g. 404 template probe) instead of
|
|
34
38
|
* string-matching error messages. */
|
|
@@ -53,6 +57,7 @@ export interface CreateSandboxOptions {
|
|
|
53
57
|
export interface TemplateRuntimeContract {
|
|
54
58
|
/** Reserved for service-owned system template versions. */
|
|
55
59
|
immutableRef?: string;
|
|
60
|
+
restoreKind?: "project_only" | "full_rootfs";
|
|
56
61
|
startCommand?: string;
|
|
57
62
|
readinessPort?: number;
|
|
58
63
|
readinessPath?: string;
|
|
@@ -64,7 +69,7 @@ export interface Sandbox {
|
|
|
64
69
|
exec(command: string, args?: string[], options?: ExecOptions): Promise<ExecResult>;
|
|
65
70
|
shell(script: string, options?: ExecOptions): Promise<ExecResult>;
|
|
66
71
|
writeFiles(files: Array<{ path: string; content: Uint8Array | string; mode?: number }>): Promise<void>;
|
|
67
|
-
snapshot(): Promise<Snapshot>;
|
|
72
|
+
snapshot(options?: SnapshotOptions): Promise<Snapshot>;
|
|
68
73
|
stop(): Promise<void>;
|
|
69
74
|
}
|
|
70
75
|
|
|
@@ -156,7 +161,7 @@ export class SandboxClient {
|
|
|
156
161
|
mode: file.mode,
|
|
157
162
|
})));
|
|
158
163
|
},
|
|
159
|
-
snapshot: () => this.request("POST", `/v1/sandboxes/${id}/snapshot
|
|
164
|
+
snapshot: (options = {}) => this.request("POST", `/v1/sandboxes/${id}/snapshot`, options),
|
|
160
165
|
stop: () => this.request("DELETE", `/v1/sandboxes/${id}`),
|
|
161
166
|
};
|
|
162
167
|
}
|
package/src/templates.test.ts
CHANGED
|
@@ -28,7 +28,7 @@ function fakeSandbox(): Sandbox & { writeBatches: Array<Array<{ path: string; co
|
|
|
28
28
|
|
|
29
29
|
describe("sandbox templates", () => {
|
|
30
30
|
test("defines a typed, immutable versioned template", () => {
|
|
31
|
-
const template = defineTemplate({ id: "angel-lfg", version: "12", title: "Angel", install: [apt.packages(["tmux"])], checks: [check.command("tmux")] });
|
|
31
|
+
const template = defineTemplate({ id: "angel-lfg", version: "12", title: "Angel", restoreKind: "full_rootfs", install: [apt.packages(["tmux"])], checks: [check.command("tmux")] });
|
|
32
32
|
expect(templateVersionRef(template)).toBe("angel-lfg-v12");
|
|
33
33
|
expect(Object.isFrozen(template)).toBe(true);
|
|
34
34
|
});
|
|
@@ -44,6 +44,9 @@ describe("sandbox templates", () => {
|
|
|
44
44
|
const script = compileStartScript({ command: "bun start", user: "user", readiness: { port: 8766 } });
|
|
45
45
|
expect(script).toContain("/home/user/.omg/template/start.pid");
|
|
46
46
|
expect(script).toContain("http://127.0.0.1:8766/");
|
|
47
|
+
expect(script).toContain("grep -Fxq 'omg-template-supervisor'");
|
|
48
|
+
expect(script).toContain("nohup /bin/bash -lc");
|
|
49
|
+
expect(script).not.toContain(`kill -0 "$(cat "$pidfile")" 2>/dev/null; then exit 0`);
|
|
47
50
|
expect(() => compileStep(run({ command: "true", env: { "BAD-NAME": "x" } }))).toThrow("invalid environment variable");
|
|
48
51
|
});
|
|
49
52
|
|
|
@@ -62,6 +65,7 @@ describe("sandbox templates", () => {
|
|
|
62
65
|
id: "files-fixture",
|
|
63
66
|
version: "1",
|
|
64
67
|
title: "files fixture",
|
|
68
|
+
restoreKind: "full_rootfs",
|
|
65
69
|
install: [files.copy({ sourceDir: FIXTURE_DIR, destination: "/home/user/app/" })],
|
|
66
70
|
});
|
|
67
71
|
await applyTemplate(sandbox, definition);
|
|
@@ -82,6 +86,7 @@ describe("sandbox templates", () => {
|
|
|
82
86
|
id: "empty-fixture",
|
|
83
87
|
version: "1",
|
|
84
88
|
title: "empty fixture",
|
|
89
|
+
restoreKind: "full_rootfs",
|
|
85
90
|
install: [files.copy({ sourceDir: join(FIXTURE_DIR, "nested", "does-not-exist"), destination: "/home/user/app" })],
|
|
86
91
|
});
|
|
87
92
|
await expect(applyTemplate(sandbox, definition)).rejects.toThrow();
|
|
@@ -110,6 +115,7 @@ describe("sandbox templates", () => {
|
|
|
110
115
|
id: "async-install",
|
|
111
116
|
version: "1",
|
|
112
117
|
title: "async install",
|
|
118
|
+
restoreKind: "full_rootfs",
|
|
113
119
|
install: [run({ command: "sleep 120 && echo installed" })],
|
|
114
120
|
}));
|
|
115
121
|
|
|
@@ -135,24 +141,37 @@ describe("sandbox templates", () => {
|
|
|
135
141
|
id: "async-failure",
|
|
136
142
|
version: "1",
|
|
137
143
|
title: "async failure",
|
|
144
|
+
restoreKind: "full_rootfs",
|
|
138
145
|
install: [run({ command: "exit 42" })],
|
|
139
146
|
}))).rejects.toThrow("package exploded");
|
|
140
147
|
});
|
|
141
148
|
|
|
142
149
|
test("LFG owns every runtime dependency and zero-config transport default", () => {
|
|
143
150
|
const lfg = agentTemplates.lfg;
|
|
144
|
-
expect(templateVersionRef(lfg)).toBe("agent-lfg-
|
|
151
|
+
expect(templateVersionRef(lfg)).toBe("agent-lfg-v33");
|
|
145
152
|
expect(lfg.install).toContainEqual(apt.packages(["tmux"]));
|
|
146
153
|
expect(lfg.checks).toContainEqual(check.command("tmux"));
|
|
154
|
+
expect(lfg.checks).toContainEqual(check.file("/home/user/lfg/omg-start.sh"));
|
|
155
|
+
expect(lfg.start?.command).toBe("exec /home/user/lfg/omg-start.sh");
|
|
147
156
|
expect(lfg.start?.env?.LIVE_TRANSPORT).toBe("ws");
|
|
148
157
|
expect(lfg.start?.readiness?.port).toBe(8766);
|
|
149
158
|
expect(templateRuntimeContract(lfg, templateVersionRef(lfg))).toEqual({
|
|
150
|
-
immutableRef: "agent-lfg-
|
|
159
|
+
immutableRef: "agent-lfg-v33",
|
|
160
|
+
restoreKind: "full_rootfs",
|
|
151
161
|
startCommand: "exec /home/user/.omg/template/bootstrap.sh",
|
|
152
162
|
readinessPort: 8766,
|
|
153
163
|
readinessPath: "/",
|
|
154
164
|
ports: [8766, 5173],
|
|
155
165
|
});
|
|
166
|
+
|
|
167
|
+
const commands = lfg.install
|
|
168
|
+
.filter((step): step is Extract<typeof step, { kind: "run" }> => step.kind === "run")
|
|
169
|
+
.map((step) => step.command);
|
|
170
|
+
const startScript = commands.find((command) => command.includes("OMG_START_EOF"));
|
|
171
|
+
expect(startScript).toBeDefined();
|
|
172
|
+
expect(startScript!).toContain("LFG_CONNECT_EVENTS=1");
|
|
173
|
+
expect(startScript!).toContain("relay-credentials.json");
|
|
174
|
+
expect(startScript!).toContain("trap cleanup EXIT INT TERM");
|
|
156
175
|
});
|
|
157
176
|
|
|
158
177
|
test("LFG bakes the react-ts scaffold into /home/user/project at bake time", () => {
|
|
@@ -169,6 +188,10 @@ describe("sandbox templates", () => {
|
|
|
169
188
|
.filter((step): step is Extract<typeof step, { kind: "run" }> => step.kind === "run")
|
|
170
189
|
.map((step) => step.command);
|
|
171
190
|
expect(commands.some((c) => c.includes("recipe.sh"))).toBe(true);
|
|
172
|
-
expect(lfg.metadata?.lfgRelease).toBe("v0.1.
|
|
191
|
+
expect(lfg.metadata?.lfgRelease).toBe("v0.1.167");
|
|
192
|
+
const lfgInstall = commands.find((c) => c.includes("bun install --production"));
|
|
193
|
+
expect(lfgInstall).toBeDefined();
|
|
194
|
+
expect(lfgInstall!).not.toContain("delete manifest.workspaces");
|
|
195
|
+
expect(lfgInstall!).not.toContain("rm -f bun.lock bun.lockb");
|
|
173
196
|
});
|
|
174
197
|
});
|
package/src/templates.ts
CHANGED
|
@@ -34,6 +34,12 @@ export interface SandboxTemplate {
|
|
|
34
34
|
version: string;
|
|
35
35
|
title: string;
|
|
36
36
|
description?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Declares whether the recipe's complete mutable payload is confined to
|
|
39
|
+
* /home/user/project. Typed recipes must choose explicitly: the baker cannot
|
|
40
|
+
* safely infer arbitrary shell-command side effects.
|
|
41
|
+
*/
|
|
42
|
+
restoreKind: "project_only" | "full_rootfs";
|
|
37
43
|
ports?: readonly number[];
|
|
38
44
|
install: readonly TemplateStep[];
|
|
39
45
|
checks?: readonly TemplateCheck[];
|
|
@@ -89,6 +95,9 @@ export function defineTemplate<const T extends SandboxTemplate>(definition: T):
|
|
|
89
95
|
assertTemplateId(definition.id);
|
|
90
96
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,19}$/.test(definition.version)) throw new Error("invalid template version");
|
|
91
97
|
if (!definition.title.trim()) throw new Error("template title is required");
|
|
98
|
+
if (definition.restoreKind !== "project_only" && definition.restoreKind !== "full_rootfs") {
|
|
99
|
+
throw new Error("template restoreKind must be project_only or full_rootfs");
|
|
100
|
+
}
|
|
92
101
|
for (const port of definition.ports ?? []) {
|
|
93
102
|
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`invalid template port ${port}`);
|
|
94
103
|
}
|
|
@@ -136,6 +145,7 @@ export function templateDefinitionHash(definition: SandboxTemplate): string {
|
|
|
136
145
|
export function templateRuntimeContract(definition: SandboxTemplate, immutableRef?: string) {
|
|
137
146
|
return {
|
|
138
147
|
immutableRef,
|
|
148
|
+
restoreKind: definition.restoreKind,
|
|
139
149
|
startCommand: definition.start ? "exec /home/user/.omg/template/bootstrap.sh" : undefined,
|
|
140
150
|
readinessPort: definition.start?.readiness?.port,
|
|
141
151
|
readinessPath: definition.start?.readiness?.path,
|
|
@@ -189,6 +199,7 @@ export function compileStep(step: TemplateStep): { script: string; timeoutMs?: n
|
|
|
189
199
|
export function compileStartScript(start: TemplateStart): string {
|
|
190
200
|
const port = start.readiness?.port;
|
|
191
201
|
const timeout = start.readiness?.timeoutSeconds ?? 60;
|
|
202
|
+
const supervisorMarker = "omg-template-supervisor";
|
|
192
203
|
const env = { HOME: start.user ? `/home/${start.user}` : "/root", ...(start.env ?? {}) };
|
|
193
204
|
const exports = shellEnvAssignments(env).map((assignment) => `export ${assignment}`).join("\n");
|
|
194
205
|
const command = `${start.cwd ? `cd ${quote(start.cwd)} && ` : ""}${start.command}`;
|
|
@@ -196,7 +207,8 @@ export function compileStartScript(start: TemplateStart): string {
|
|
|
196
207
|
const probe = port
|
|
197
208
|
? `for _ in $(seq 1 ${timeout}); do curl -fsS -m1 -o /dev/null ${quote(`http://127.0.0.1:${port}${start.readiness?.path ?? "/"}`)} && exit 0; sleep 1; done\necho 'template service did not become ready on :${port}' >&2\nexit 1`
|
|
198
209
|
: "exit 0";
|
|
199
|
-
|
|
210
|
+
const supervisor = `while true; do ${foreground} >>/home/user/.omg/template/start.log 2>&1 || true; sleep 2; done`;
|
|
211
|
+
return `#!/usr/bin/env bash\nset -euo pipefail\nmkdir -p /home/user/.omg/template\npidfile=/home/user/.omg/template/start.pid\npid=\"\"\nif [ -s \"$pidfile\" ]; then pid=\"$(cat \"$pidfile\" 2>/dev/null || true)\"; fi\nsupervisor_alive=false\nif [[ \"$pid\" =~ ^[0-9]+$ ]] && kill -0 \"$pid\" 2>/dev/null && tr '\\0' '\\n' < \"/proc/$pid/cmdline\" 2>/dev/null | grep -Fxq ${quote(supervisorMarker)}; then supervisor_alive=true; fi\nif [ \"$supervisor_alive\" != true ]; then\n rm -f \"$pidfile\"\n nohup /bin/bash -lc ${quote(supervisor)} ${quote(supervisorMarker)} >/dev/null 2>&1 &\n echo $! > \"$pidfile\"\nfi\n${probe}\n`;
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
async function assertExec(label: string, result: { exitCode: number; stdout: string; stderr: string }): Promise<void> {
|
|
@@ -356,7 +368,7 @@ export async function bakeTemplate(
|
|
|
356
368
|
let snapshotted = false;
|
|
357
369
|
try {
|
|
358
370
|
await applyTemplate(sandbox, definition, log);
|
|
359
|
-
const snapshot = await sandbox.snapshot();
|
|
371
|
+
const snapshot = await sandbox.snapshot({ restoreKind: definition.restoreKind });
|
|
360
372
|
snapshotted = true;
|
|
361
373
|
const uploaded = await waitForSnapshotUpload(client, snapshot);
|
|
362
374
|
// The immutable version ref is content-addressed: definition hash + the
|