@omg-dev/sandbox 0.4.40 → 0.4.42
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
|
|
2
|
-
export { APT_FORCE_IPV4_SNIPPET, SandboxApiError, SandboxClient, applyTemplate, apt, assertTemplateId, bakeTemplate, check, compileStartScript, compileStep, defineTemplate, download, files, run, templateContentVersionRef, templateDefinitionHash, templateRuntimeContract, templateVersionRef, waitForSnapshotUpload };
|
|
1
|
+
import { _ as waitForSnapshotUpload, a as canonicalTemplateSourceDir, b as assertTemplateId, c as compileStep, d as files, f as run, g as templateVersionRef, h as templateRuntimeContract, i as bakeTemplate, l as defineTemplate, m as templateDefinitionHash, n as applyTemplate, o as check, p as templateContentVersionRef, r as apt, s as compileStartScript, t as APT_FORCE_IPV4_SNIPPET, u as download, v as SandboxApiError, y as SandboxClient } from "./templates-DRLyKqoS.mjs";
|
|
2
|
+
export { APT_FORCE_IPV4_SNIPPET, SandboxApiError, SandboxClient, applyTemplate, apt, assertTemplateId, bakeTemplate, canonicalTemplateSourceDir, check, compileStartScript, compileStep, defineTemplate, download, files, run, templateContentVersionRef, templateDefinitionHash, templateRuntimeContract, templateVersionRef, waitForSnapshotUpload };
|
|
@@ -167,8 +167,24 @@ function templateContentVersionRef(definition, contentKey) {
|
|
|
167
167
|
const hash = createHash("sha256").update(`${templateDefinitionHash(definition)}\n${contentKey.trim()}`).digest("hex").slice(0, 8);
|
|
168
168
|
return refWithSuffix(definition.id, `-v${definition.version.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}-${hash}`);
|
|
169
169
|
}
|
|
170
|
+
function canonicalTemplateSourceDir(sourceDir) {
|
|
171
|
+
const normalized = sourceDir.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
172
|
+
const marker = normalized.lastIndexOf("/templates/");
|
|
173
|
+
return marker < 0 ? normalized : normalized.slice(marker + 1);
|
|
174
|
+
}
|
|
170
175
|
function templateDefinitionHash(definition) {
|
|
171
|
-
|
|
176
|
+
const canonical = {
|
|
177
|
+
...definition,
|
|
178
|
+
install: definition.install.map((step) => step.kind === "files" ? {
|
|
179
|
+
...step,
|
|
180
|
+
sourceDir: canonicalTemplateSourceDir(step.sourceDir)
|
|
181
|
+
} : step),
|
|
182
|
+
__rendered: {
|
|
183
|
+
start: definition.start ? compileStartScript(definition.start) : null,
|
|
184
|
+
install: definition.install.map((step) => step.kind === "files" ? null : compileStep(step).script)
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
172
188
|
}
|
|
173
189
|
function templateRuntimeContract(definition, immutableRef) {
|
|
174
190
|
return {
|
|
@@ -189,7 +205,8 @@ const PROXY_ENV_VARS = [
|
|
|
189
205
|
"ANTHROPIC_API_KEY",
|
|
190
206
|
"OPENAI_BASE_URL",
|
|
191
207
|
"OPENAI_API_KEY",
|
|
192
|
-
"OMG_MEDIA_URL"
|
|
208
|
+
"OMG_MEDIA_URL",
|
|
209
|
+
"OMG_SCHEDULE_URL"
|
|
193
210
|
];
|
|
194
211
|
function commandForUser(command, user) {
|
|
195
212
|
if (!user) return `/bin/bash -lc ${quote(command)}`;
|
|
@@ -421,4 +438,4 @@ async function resolveTemplateVersion(client, versionRef) {
|
|
|
421
438
|
}
|
|
422
439
|
}
|
|
423
440
|
//#endregion
|
|
424
|
-
export {
|
|
441
|
+
export { waitForSnapshotUpload as _, canonicalTemplateSourceDir as a, assertTemplateId as b, compileStep as c, files as d, run as f, templateVersionRef as g, templateRuntimeContract as h, bakeTemplate as i, defineTemplate as l, templateDefinitionHash as m, applyTemplate as n, check as o, templateContentVersionRef as p, apt as r, compileStartScript as s, APT_FORCE_IPV4_SNIPPET as t, download as u, SandboxApiError as v, SandboxClient as y };
|
package/dist/templates.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { APT_FORCE_IPV4_SNIPPET, applyTemplate, apt, bakeTemplate, check, compileStartScript, compileStep, defineTemplate, download, files, run, templateContentVersionRef, templateDefinitionHash, templateRuntimeContract, templateVersionRef, waitForSnapshotUpload };
|
|
1
|
+
import { _ as waitForSnapshotUpload, a as canonicalTemplateSourceDir, c as compileStep, d as files, f as run, g as templateVersionRef, h as templateRuntimeContract, i as bakeTemplate, l as defineTemplate, m as templateDefinitionHash, n as applyTemplate, o as check, p as templateContentVersionRef, r as apt, s as compileStartScript, t as APT_FORCE_IPV4_SNIPPET, u as download } from "./templates-DRLyKqoS.mjs";
|
|
2
|
+
export { APT_FORCE_IPV4_SNIPPET, applyTemplate, apt, bakeTemplate, canonicalTemplateSourceDir, check, compileStartScript, compileStep, defineTemplate, download, files, run, templateContentVersionRef, templateDefinitionHash, templateRuntimeContract, templateVersionRef, waitForSnapshotUpload };
|
package/package.json
CHANGED
package/src/client.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { SandboxClient } from "./client";
|
|
3
|
-
import { bakeTemplate, defineTemplate, templateContentVersionRef, templateVersionRef } from "./templates";
|
|
3
|
+
import { bakeTemplate, defineTemplate, files, templateContentVersionRef, templateVersionRef } from "./templates";
|
|
4
4
|
|
|
5
5
|
interface RecordedCall { method: string; path: string; body?: unknown; owner?: string | null }
|
|
6
6
|
|
|
@@ -63,6 +63,33 @@ describe("template version identity", () => {
|
|
|
63
63
|
expect(templateContentVersionRef(bumped, "rootfs-sha-1")).not.toBe(ref);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
test("content ref does not depend on where the repo is checked out", () => {
|
|
67
|
+
// A files step carries an absolute sourceDir resolved from the authoring
|
|
68
|
+
// checkout, so hashing it raw gave CI (/home/runner/actions-runner/_work/…)
|
|
69
|
+
// and any other checkout different identities for byte-identical content.
|
|
70
|
+
// The updater's bake gate recomputes the published ref to decide whether
|
|
71
|
+
// the pinned version is live — a path-dependent ref would report a perfectly
|
|
72
|
+
// baked template as unbaked and rebake it every hour.
|
|
73
|
+
const at = (root: string) =>
|
|
74
|
+
defineTemplate({
|
|
75
|
+
...definition,
|
|
76
|
+
install: [files.copy({ sourceDir: `${root}/templates/react-ts`, destination: "/home/user/project" })],
|
|
77
|
+
});
|
|
78
|
+
expect(templateContentVersionRef(at("/home/runner/actions-runner/_work/vibes/vibes"), "rootfs-sha-1")).toBe(
|
|
79
|
+
templateContentVersionRef(at("/home/dev/lfg-worktrees/lfg-a0f114"), "rootfs-sha-1"),
|
|
80
|
+
);
|
|
81
|
+
// …but which directory is copied still changes the identity.
|
|
82
|
+
expect(templateContentVersionRef(at("/repo"), "rootfs-sha-1")).not.toBe(
|
|
83
|
+
templateContentVersionRef(
|
|
84
|
+
defineTemplate({
|
|
85
|
+
...definition,
|
|
86
|
+
install: [files.copy({ sourceDir: "/repo/templates/react-ts-start", destination: "/home/user/project" })],
|
|
87
|
+
}),
|
|
88
|
+
"rootfs-sha-1",
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
66
93
|
test("empty content key fails loud instead of falling back to the static ref", () => {
|
|
67
94
|
expect(() => templateContentVersionRef(definition, "")).toThrow(/content key/);
|
|
68
95
|
expect(() => templateContentVersionRef(definition, " ")).toThrow(/content key/);
|
package/src/templates.test.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { applyTemplate, apt, check, compileStartScript, compileStep, defineTemplate, download, files, run, templateRuntimeContract, templateVersionRef } from "./templates";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
agentTemplates,
|
|
6
|
+
CLOUD_COMPUTER_DEFAULT_FOLDER,
|
|
7
|
+
FREE_OPENCODE_MODEL,
|
|
8
|
+
OPENCODE_GLOBAL_CONFIG,
|
|
9
|
+
} from "../../../templates/agent-catalog";
|
|
5
10
|
import type { Sandbox } from "./client";
|
|
6
11
|
|
|
7
12
|
const FIXTURE_DIR = join(import.meta.dirname, "__fixtures__", "mini-template");
|
|
@@ -158,7 +163,12 @@ describe("sandbox templates", () => {
|
|
|
158
163
|
|
|
159
164
|
test("LFG owns every runtime dependency and zero-config transport default", () => {
|
|
160
165
|
const lfg = agentTemplates.lfg;
|
|
161
|
-
|
|
166
|
+
// Derived, not a literal: update-lfg-app.yml bumps this template's version
|
|
167
|
+
// on every LFG release, so a hardcoded ref rots the moment a release lands
|
|
168
|
+
// (this asserted v81 while the catalog was already at v102, leaving
|
|
169
|
+
// ci-packages red-in-waiting for the next packages/** change).
|
|
170
|
+
const lfgRef = `agent-lfg-v${lfg.version}`;
|
|
171
|
+
expect(templateVersionRef(lfg)).toBe(lfgRef);
|
|
162
172
|
expect(lfg.install).toContainEqual(apt.packages(["tmux"]));
|
|
163
173
|
expect(lfg.checks).toContainEqual(check.command("tmux"));
|
|
164
174
|
expect(lfg.checks).toContainEqual(check.file("/home/user/lfg/omg-start.sh"));
|
|
@@ -166,7 +176,7 @@ describe("sandbox templates", () => {
|
|
|
166
176
|
expect(lfg.start?.env?.LIVE_TRANSPORT).toBe("ws");
|
|
167
177
|
expect(lfg.start?.readiness?.port).toBe(8766);
|
|
168
178
|
expect(templateRuntimeContract(lfg, templateVersionRef(lfg))).toEqual({
|
|
169
|
-
immutableRef:
|
|
179
|
+
immutableRef: lfgRef,
|
|
170
180
|
restoreKind: "full_rootfs",
|
|
171
181
|
startCommand: "exec /home/user/.omg/template/bootstrap.sh",
|
|
172
182
|
readinessPort: 8766,
|
|
@@ -177,6 +187,11 @@ describe("sandbox templates", () => {
|
|
|
177
187
|
const commands = lfg.install
|
|
178
188
|
.filter((step): step is Extract<typeof step, { kind: "run" }> => step.kind === "run")
|
|
179
189
|
.map((step) => step.command);
|
|
190
|
+
const packageInstalls = commands.filter((command) => command.includes("bun install"));
|
|
191
|
+
expect(packageInstalls).toHaveLength(6);
|
|
192
|
+
for (const command of packageInstalls) {
|
|
193
|
+
expect(command).toContain("trap 'rm -rf /home/user/.bun/install/cache' EXIT");
|
|
194
|
+
}
|
|
180
195
|
const startScript = commands.find((command) => command.includes("OMG_START_EOF"));
|
|
181
196
|
expect(startScript).toBeDefined();
|
|
182
197
|
expect(startScript!).toContain("LFG_CONNECT_EVENTS=1");
|
|
@@ -192,6 +207,31 @@ describe("sandbox templates", () => {
|
|
|
192
207
|
expect(startScript!).toContain('cd "$LFG_ROOT"');
|
|
193
208
|
});
|
|
194
209
|
|
|
210
|
+
test("OpenCode utility agents stay on the credential-free model", () => {
|
|
211
|
+
expect(OPENCODE_GLOBAL_CONFIG).toEqual({
|
|
212
|
+
$schema: "https://opencode.ai/config.json",
|
|
213
|
+
model: FREE_OPENCODE_MODEL,
|
|
214
|
+
small_model: FREE_OPENCODE_MODEL,
|
|
215
|
+
agent: {
|
|
216
|
+
title: { model: FREE_OPENCODE_MODEL },
|
|
217
|
+
summary: { model: FREE_OPENCODE_MODEL },
|
|
218
|
+
compaction: { model: FREE_OPENCODE_MODEL },
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(Object.keys(agentTemplates)).toEqual(["lfg"]);
|
|
223
|
+
const commands = agentTemplates.lfg.install
|
|
224
|
+
.filter((step): step is Extract<typeof step, { kind: "run" }> => step.kind === "run")
|
|
225
|
+
.map((step) => step.command);
|
|
226
|
+
const config = commands.find((command) => command.includes("opencode.jsonc"));
|
|
227
|
+
expect(config).toBeDefined();
|
|
228
|
+
expect(config).toContain(`\"small_model\":\"${FREE_OPENCODE_MODEL}\"`);
|
|
229
|
+
expect(config).toContain(`\"title\":{\"model\":\"${FREE_OPENCODE_MODEL}\"}`);
|
|
230
|
+
expect(config).toContain(`\"summary\":{\"model\":\"${FREE_OPENCODE_MODEL}\"}`);
|
|
231
|
+
expect(config).toContain(`\"compaction\":{\"model\":\"${FREE_OPENCODE_MODEL}\"}`);
|
|
232
|
+
expect(agentTemplates.lfg.checks).toContainEqual(check.file("/home/user/.config/opencode/opencode.jsonc"));
|
|
233
|
+
});
|
|
234
|
+
|
|
195
235
|
test("LFG bakes the react-ts scaffold into /home/user/project at bake time", () => {
|
|
196
236
|
const lfg = agentTemplates.lfg;
|
|
197
237
|
const filesSteps = lfg.install.filter((step) => step.kind === "files");
|
package/src/templates.ts
CHANGED
|
@@ -138,8 +138,48 @@ export function templateContentVersionRef(definition: SandboxTemplate, contentKe
|
|
|
138
138
|
return refWithSuffix(definition.id, `-v${definition.version.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}-${hash}`);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// A files step's sourceDir is an absolute path the definition resolved from
|
|
142
|
+
// whatever checkout authored it (templates/agent-catalog.ts builds it from
|
|
143
|
+
// import.meta.dirname), so hashing it raw makes the bake identity depend on
|
|
144
|
+
// WHERE the repo happens to live: byte-identical content hashes differently
|
|
145
|
+
// under /home/runner/actions-runner/_work/... than under a local worktree.
|
|
146
|
+
// Strip the checkout prefix so the ref a bake published can be recomputed
|
|
147
|
+
// anywhere — that reproducibility is what lets template-bake-status.ts ask the
|
|
148
|
+
// registry "is the definition on main the one that is live?" instead of
|
|
149
|
+
// inferring it from whether this run happened to change the pin.
|
|
150
|
+
export function canonicalTemplateSourceDir(sourceDir: string): string {
|
|
151
|
+
const normalized = sourceDir.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
152
|
+
const marker = normalized.lastIndexOf("/templates/");
|
|
153
|
+
return marker < 0 ? normalized : normalized.slice(marker + 1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// NOTE: this still hashes the definition, not the bytes under a files step's
|
|
157
|
+
// sourceDir — a templates/react-ts-only change does not invalidate a snapshot
|
|
158
|
+
// that copies it, and still needs a hand-bumped `version` (see the comment on
|
|
159
|
+
// files.copy in templates/agent-catalog.ts).
|
|
141
160
|
export function templateDefinitionHash(definition: SandboxTemplate): string {
|
|
142
|
-
|
|
161
|
+
const canonical = {
|
|
162
|
+
...definition,
|
|
163
|
+
install: definition.install.map((step) =>
|
|
164
|
+
step.kind === "files"
|
|
165
|
+
? { ...step, sourceDir: canonicalTemplateSourceDir(step.sourceDir) }
|
|
166
|
+
: step,
|
|
167
|
+
),
|
|
168
|
+
// The rendered codegen, not just the definition. The generators
|
|
169
|
+
// (compileStartScript / commandForUser / compileStep) are part of what a
|
|
170
|
+
// snapshot bakes — the definition alone missed the OMG_SCHEDULE_URL
|
|
171
|
+
// supervisor-wrapper fix on 2026-08-13: every template answered "reused
|
|
172
|
+
// existing immutable version" and the fleet kept forking snapshots whose
|
|
173
|
+
// baked serve env predated the change. Hashing the rendered scripts makes
|
|
174
|
+
// any generator change invalidate exactly the templates it affects.
|
|
175
|
+
__rendered: {
|
|
176
|
+
start: definition.start ? compileStartScript(definition.start) : null,
|
|
177
|
+
install: definition.install.map((step) =>
|
|
178
|
+
step.kind === "files" ? null : compileStep(step).script,
|
|
179
|
+
),
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
143
183
|
}
|
|
144
184
|
|
|
145
185
|
export function templateRuntimeContract(definition: SandboxTemplate, immutableRef?: string) {
|
|
@@ -163,7 +203,16 @@ function quote(value: string): string { return `'${value.replaceAll("'", `'\"'\"
|
|
|
163
203
|
// literal VAR=value args to env(1) is not "preserving" the invoking shell's
|
|
164
204
|
// env (which sudoers' env_reset would still block) — it is env(1) itself
|
|
165
205
|
// setting them in the de-privileged child, which needs no sudoers policy.
|
|
166
|
-
|
|
206
|
+
//
|
|
207
|
+
// THIS LIST HAS THREE COPIES that must stay in sync — a var missing here is
|
|
208
|
+
// invisible to the template's long-lived `start` process (the baked serve)
|
|
209
|
+
// while still present on ad-hoc execs, which is exactly the split that made
|
|
210
|
+
// OMG_SCHEDULE_URL silently absent from every fresh Computer's lfg serve
|
|
211
|
+
// (wake-hooks e2e, 2026-08-13) while an in-VM probe of the same var worked:
|
|
212
|
+
// 1. apps/infra/internal/orchestrator/manager.go (ExecContext — the source)
|
|
213
|
+
// 2. apps/infra/internal/agenttemplates/catalog.go (legacy agent-serve.sh)
|
|
214
|
+
// 3. here (current bootstrap.sh supervisor wrapper)
|
|
215
|
+
const PROXY_ENV_VARS = ["OMG_AI_URL", "ANTHROPIC_BASE_URL", "ANTHROPIC_API_KEY", "OPENAI_BASE_URL", "OPENAI_API_KEY", "OMG_MEDIA_URL", "OMG_SCHEDULE_URL"];
|
|
167
216
|
|
|
168
217
|
function commandForUser(command: string, user?: string): string {
|
|
169
218
|
if (!user) return `/bin/bash -lc ${quote(command)}`;
|