@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 SandboxApiError, a as check, c as defineTemplate, d as run, f as templateContentVersionRef, g as waitForSnapshotUpload, h as templateVersionRef, i as bakeTemplate, l as download, m as templateRuntimeContract, n as applyTemplate, o as compileStartScript, p as templateDefinitionHash, r as apt, s as compileStep, t as APT_FORCE_IPV4_SNIPPET, u as files, v as SandboxClient, y as assertTemplateId } from "./templates-D-Ap1Yxk.mjs";
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
- return createHash("sha256").update(JSON.stringify(definition)).digest("hex");
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 { SandboxApiError as _, check as a, defineTemplate as c, run as d, templateContentVersionRef as f, waitForSnapshotUpload as g, templateVersionRef as h, bakeTemplate as i, download as l, templateRuntimeContract as m, applyTemplate as n, compileStartScript as o, templateDefinitionHash as p, apt as r, compileStep as s, APT_FORCE_IPV4_SNIPPET as t, files as u, SandboxClient as v, assertTemplateId as y };
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 };
@@ -1,2 +1,2 @@
1
- import { a as check, c as defineTemplate, d as run, f as templateContentVersionRef, g as waitForSnapshotUpload, h as templateVersionRef, i as bakeTemplate, l as download, m as templateRuntimeContract, n as applyTemplate, o as compileStartScript, p as templateDefinitionHash, r as apt, s as compileStep, t as APT_FORCE_IPV4_SNIPPET, u as files } from "./templates-D-Ap1Yxk.mjs";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omg-dev/sandbox",
3
- "version": "0.4.40",
3
+ "version": "0.4.42",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -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/);
@@ -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 { agentTemplates, CLOUD_COMPUTER_DEFAULT_FOLDER } from "../../../templates/agent-catalog";
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
- expect(templateVersionRef(lfg)).toBe("agent-lfg-v81");
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: "agent-lfg-v81",
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
- return createHash("sha256").update(JSON.stringify(definition)).digest("hex");
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
- const PROXY_ENV_VARS = ["OMG_AI_URL", "ANTHROPIC_BASE_URL", "ANTHROPIC_API_KEY", "OPENAI_BASE_URL", "OPENAI_API_KEY", "OMG_MEDIA_URL"];
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)}`;