@crewhaus/sandbox-image-go 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.
- package/package.json +43 -0
- package/src/index.test.ts +185 -0
- package/src/index.ts +50 -0
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/sandbox-image-go",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Go 1.23 polyglot sandbox image: registry registration + multi-stage Dockerfile + T2/T7/T8 contract tests (Section 36)",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "bun test src"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@crewhaus/errors": "0.0.0",
|
|
16
|
+
"@crewhaus/sandbox": "0.0.0",
|
|
17
|
+
"@crewhaus/sandbox-image-registry": "0.0.0"
|
|
18
|
+
},
|
|
19
|
+
"license": "Apache-2.0",
|
|
20
|
+
"author": {
|
|
21
|
+
"name": "Max Meier",
|
|
22
|
+
"email": "max@studiomax.io",
|
|
23
|
+
"url": "https://studiomax.io"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/crewhaus/factory.git",
|
|
28
|
+
"directory": "packages/sandbox-image-go"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/sandbox-image-go#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/crewhaus/factory/issues"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "restricted"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"src",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE",
|
|
41
|
+
"NOTICE"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createSandbox } from "@crewhaus/sandbox";
|
|
3
|
+
import {
|
|
4
|
+
ImageRegistrationError,
|
|
5
|
+
_resetSandboxImageRegistry,
|
|
6
|
+
hasSandboxImage,
|
|
7
|
+
listAllowedImageRefs,
|
|
8
|
+
lookupSandboxImage,
|
|
9
|
+
registerSandboxImage,
|
|
10
|
+
} from "@crewhaus/sandbox-image-registry";
|
|
11
|
+
import {
|
|
12
|
+
GO_COLD_START_BUDGET_MS,
|
|
13
|
+
GO_DEFAULT_ENTRYPOINT,
|
|
14
|
+
GO_HEALTHCHECK_ARGV,
|
|
15
|
+
GO_IMAGE_ID,
|
|
16
|
+
GO_IMAGE_REF,
|
|
17
|
+
registerGoSandboxImage,
|
|
18
|
+
} from "./index";
|
|
19
|
+
|
|
20
|
+
describe("registerGoSandboxImage (T1 + T2)", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
_resetSandboxImageRegistry();
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
_resetSandboxImageRegistry();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("constants match the kickoff prompt's spec", () => {
|
|
29
|
+
expect(GO_IMAGE_ID).toBe("go");
|
|
30
|
+
expect(GO_IMAGE_REF).toBe("golang:1.23-alpine");
|
|
31
|
+
expect(GO_DEFAULT_ENTRYPOINT).toEqual(["go", "run", "-"]);
|
|
32
|
+
expect(GO_HEALTHCHECK_ARGV).toEqual(["go", "version"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("registerGoSandboxImage() registers an image with the right shape", () => {
|
|
36
|
+
const entry = registerGoSandboxImage();
|
|
37
|
+
expect(entry.id).toBe("go");
|
|
38
|
+
expect(entry.image).toBe("golang:1.23-alpine");
|
|
39
|
+
expect(entry.defaultEntrypoint).toEqual(["go", "run", "-"]);
|
|
40
|
+
expect(entry.healthcheck.command).toEqual(["go", "version"]);
|
|
41
|
+
expect(entry.healthcheck.expectedExitCode).toBe(0);
|
|
42
|
+
expect(entry.healthcheck.timeoutMs).toBe(GO_COLD_START_BUDGET_MS);
|
|
43
|
+
expect(entry.description).toMatch(/Go 1\.23/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("lookupSandboxImage('go') returns the registered entry", () => {
|
|
47
|
+
registerGoSandboxImage();
|
|
48
|
+
expect(hasSandboxImage("go")).toBe(true);
|
|
49
|
+
const entry = lookupSandboxImage("go");
|
|
50
|
+
expect(entry.image).toBe("golang:1.23-alpine");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("listAllowedImageRefs includes golang:1.23-alpine after registration", () => {
|
|
54
|
+
registerGoSandboxImage();
|
|
55
|
+
const refs = listAllowedImageRefs();
|
|
56
|
+
expect(refs).toContain("golang:1.23-alpine");
|
|
57
|
+
// Bootstrap trio still present
|
|
58
|
+
expect(refs).toContain("python:3.13-slim");
|
|
59
|
+
expect(refs).toContain("node:22-alpine");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("Go-shape T2 contract — round-trip via noop sandbox", () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
_resetSandboxImageRegistry();
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
_resetSandboxImageRegistry();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("noop sandbox accepts golang:1.23-alpine when registered", async () => {
|
|
72
|
+
registerGoSandboxImage();
|
|
73
|
+
const sandbox = createSandbox({
|
|
74
|
+
backend: "noop",
|
|
75
|
+
allowedImages: listAllowedImageRefs(),
|
|
76
|
+
});
|
|
77
|
+
// The noop backend spawns the argv directly — `printf` is universal,
|
|
78
|
+
// so we use it instead of `go run -` to assert the wiring without
|
|
79
|
+
// needing a Go toolchain on the test host.
|
|
80
|
+
const result = await sandbox.exec({
|
|
81
|
+
image: GO_IMAGE_REF,
|
|
82
|
+
argv: ["printf", "hello-from-go"],
|
|
83
|
+
});
|
|
84
|
+
expect(result.exitCode).toBe(0);
|
|
85
|
+
expect(result.stdout).toBe("hello-from-go");
|
|
86
|
+
await sandbox.close();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("noop sandbox refuses golang:1.23-alpine when NOT registered", async () => {
|
|
90
|
+
// Registry not touched here — Go image is not in the allowlist.
|
|
91
|
+
// The bootstrap trio is still present, so we explicitly use a
|
|
92
|
+
// sandbox configured with only the trio.
|
|
93
|
+
const trioRefs = ["python:3.13-slim", "node:22-alpine", "alpine:3.19"];
|
|
94
|
+
const sandbox = createSandbox({ backend: "noop", allowedImages: trioRefs });
|
|
95
|
+
await expect(sandbox.exec({ image: GO_IMAGE_REF, argv: ["printf", "x"] })).rejects.toThrow(
|
|
96
|
+
/not on the allowlist/,
|
|
97
|
+
);
|
|
98
|
+
await sandbox.close();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("Go-shape T7 — cold-start budget", () => {
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
_resetSandboxImageRegistry();
|
|
105
|
+
});
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
_resetSandboxImageRegistry();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("healthcheck timeoutMs is within the compiled-language budget (≤2s)", () => {
|
|
111
|
+
const entry = registerGoSandboxImage();
|
|
112
|
+
expect(entry.healthcheck.timeoutMs).toBeLessThanOrEqual(2_000);
|
|
113
|
+
expect(entry.healthcheck.timeoutMs).toBeGreaterThan(0);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("Go-shape T8 — escape-attempt suite (reuses §18 corpus shape)", () => {
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
_resetSandboxImageRegistry();
|
|
120
|
+
});
|
|
121
|
+
afterEach(() => {
|
|
122
|
+
_resetSandboxImageRegistry();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("Go image registration is idempotent-then-rejected (no silent override)", () => {
|
|
126
|
+
registerGoSandboxImage();
|
|
127
|
+
expect(() => registerGoSandboxImage()).toThrow(/already registered/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("Go entry's image string passes registry validation (no leading dash)", () => {
|
|
131
|
+
expect(GO_IMAGE_REF.startsWith("-")).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("Go entry's image string contains no whitespace / newline", () => {
|
|
135
|
+
expect(/\s/.test(GO_IMAGE_REF)).toBe(false);
|
|
136
|
+
expect(GO_IMAGE_REF.includes("\n")).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("Go entry's defaultEntrypoint contains no shell-meta sequences", () => {
|
|
140
|
+
for (const arg of GO_DEFAULT_ENTRYPOINT) {
|
|
141
|
+
expect(/[;&|<>$`(){}]/.test(arg)).toBe(false);
|
|
142
|
+
expect(arg.includes("\n")).toBe(false);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("Go entry's healthcheck argv contains no shell-meta sequences", () => {
|
|
147
|
+
for (const arg of GO_HEALTHCHECK_ARGV) {
|
|
148
|
+
expect(/[;&|<>$`(){}]/.test(arg)).toBe(false);
|
|
149
|
+
expect(arg.includes("\n")).toBe(false);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("attempted CLI-flag-injection registration via Go id is refused", () => {
|
|
154
|
+
expect(() =>
|
|
155
|
+
registerSandboxImage({
|
|
156
|
+
id: "go",
|
|
157
|
+
image: "--privileged",
|
|
158
|
+
defaultEntrypoint: ["go", "run", "-"],
|
|
159
|
+
healthcheck: { command: ["go", "version"], expectedExitCode: 0 },
|
|
160
|
+
}),
|
|
161
|
+
).toThrow(ImageRegistrationError);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("attempted whitespace-tampered Go image is refused", () => {
|
|
165
|
+
expect(() =>
|
|
166
|
+
registerSandboxImage({
|
|
167
|
+
id: "go",
|
|
168
|
+
image: "golang:1.23-alpine --privileged",
|
|
169
|
+
defaultEntrypoint: ["go", "run", "-"],
|
|
170
|
+
healthcheck: { command: ["go", "version"], expectedExitCode: 0 },
|
|
171
|
+
}),
|
|
172
|
+
).toThrow(/whitespace/);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("attempted shell-meta-tagged Go image is refused", () => {
|
|
176
|
+
expect(() =>
|
|
177
|
+
registerSandboxImage({
|
|
178
|
+
id: "go",
|
|
179
|
+
image: "golang:$(id)",
|
|
180
|
+
defaultEntrypoint: ["go", "run", "-"],
|
|
181
|
+
healthcheck: { command: ["go", "version"], expectedExitCode: 0 },
|
|
182
|
+
}),
|
|
183
|
+
).toThrow(/valid registry/);
|
|
184
|
+
});
|
|
185
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type SandboxImageEntry, registerSandboxImage } from "@crewhaus/sandbox-image-registry";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Catalog R8 `sandbox-image-go` — Section 36 polyglot Go sandbox image.
|
|
5
|
+
*
|
|
6
|
+
* Registers a curated Go 1.23 image into `@crewhaus/sandbox-image-registry`
|
|
7
|
+
* so polyglot agents can `lookupSandboxImage("go")` and route a snippet
|
|
8
|
+
* through `tool-code-execution`'s sandbox path.
|
|
9
|
+
*
|
|
10
|
+
* Snippet mode (default entrypoint): `go run -` reads stdin as a single
|
|
11
|
+
* `package main` source file. Advanced callers who mount a project can
|
|
12
|
+
* exec `./<binary>` after a build step — the entrypoint is overridable
|
|
13
|
+
* at `sandbox.exec({ argv })` time.
|
|
14
|
+
*
|
|
15
|
+
* Cold-start budget: ≤2s for the compiled-language warm pool; the
|
|
16
|
+
* `healthcheck.timeoutMs = 2_000` enforces the registry's view of
|
|
17
|
+
* "healthy". Live-image probe runs `go version` against the actual
|
|
18
|
+
* docker image when `CREWHAUS_SECTION36_LIVE_DOCKER=1` is set.
|
|
19
|
+
*
|
|
20
|
+
* Layer R8. Pairs with `sandbox-image-registry` (R8 — registration
|
|
21
|
+
* surface) and `sandbox` (R8 — exec backend).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const GO_IMAGE_ID = "go";
|
|
25
|
+
export const GO_IMAGE_REF = "golang:1.23-alpine";
|
|
26
|
+
export const GO_DEFAULT_ENTRYPOINT: ReadonlyArray<string> = ["go", "run", "-"];
|
|
27
|
+
export const GO_HEALTHCHECK_ARGV: ReadonlyArray<string> = ["go", "version"];
|
|
28
|
+
|
|
29
|
+
/** Cold-start budget for the warm pool (ms). T7 layer asserts this. */
|
|
30
|
+
export const GO_COLD_START_BUDGET_MS = 2_000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register the Go 1.23 image. Idempotent — calling twice throws via
|
|
34
|
+
* the registry's duplicate-id refusal. Callers in app boot paths
|
|
35
|
+
* should call `registerGoSandboxImage()` once.
|
|
36
|
+
*/
|
|
37
|
+
export function registerGoSandboxImage(): SandboxImageEntry {
|
|
38
|
+
return registerSandboxImage({
|
|
39
|
+
id: GO_IMAGE_ID,
|
|
40
|
+
image: GO_IMAGE_REF,
|
|
41
|
+
defaultEntrypoint: GO_DEFAULT_ENTRYPOINT,
|
|
42
|
+
healthcheck: {
|
|
43
|
+
command: GO_HEALTHCHECK_ARGV,
|
|
44
|
+
expectedExitCode: 0,
|
|
45
|
+
timeoutMs: GO_COLD_START_BUDGET_MS,
|
|
46
|
+
},
|
|
47
|
+
description:
|
|
48
|
+
"Go 1.23 alpine — snippet mode via `go run -`; compiled-binary mode for mounted projects.",
|
|
49
|
+
});
|
|
50
|
+
}
|