@crewhaus/sandbox-image-rust 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 ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@crewhaus/sandbox-image-rust",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Rust stable 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-rust"
29
+ },
30
+ "homepage": "https://github.com/crewhaus/factory/tree/main/packages/sandbox-image-rust#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,157 @@
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
+ RUST_COLD_START_BUDGET_MS,
13
+ RUST_DEFAULT_ENTRYPOINT,
14
+ RUST_HEALTHCHECK_ARGV,
15
+ RUST_IMAGE_ID,
16
+ RUST_IMAGE_REF,
17
+ registerRustSandboxImage,
18
+ } from "./index";
19
+
20
+ describe("registerRustSandboxImage (T1 + T2)", () => {
21
+ beforeEach(() => _resetSandboxImageRegistry());
22
+ afterEach(() => _resetSandboxImageRegistry());
23
+
24
+ test("constants match the kickoff prompt's spec", () => {
25
+ expect(RUST_IMAGE_ID).toBe("rust");
26
+ expect(RUST_IMAGE_REF).toBe("rust:1-alpine");
27
+ expect(RUST_DEFAULT_ENTRYPOINT).toEqual(["rustc", "-"]);
28
+ expect(RUST_HEALTHCHECK_ARGV).toEqual(["rustc", "--version"]);
29
+ });
30
+
31
+ test("registerRustSandboxImage() registers an image with the right shape", () => {
32
+ const entry = registerRustSandboxImage();
33
+ expect(entry.id).toBe("rust");
34
+ expect(entry.image).toBe("rust:1-alpine");
35
+ expect(entry.defaultEntrypoint).toEqual(["rustc", "-"]);
36
+ expect(entry.healthcheck.command).toEqual(["rustc", "--version"]);
37
+ expect(entry.healthcheck.expectedExitCode).toBe(0);
38
+ expect(entry.healthcheck.timeoutMs).toBe(RUST_COLD_START_BUDGET_MS);
39
+ expect(entry.description).toMatch(/Rust/);
40
+ });
41
+
42
+ test("lookupSandboxImage('rust') returns the registered entry", () => {
43
+ registerRustSandboxImage();
44
+ expect(hasSandboxImage("rust")).toBe(true);
45
+ expect(lookupSandboxImage("rust").image).toBe("rust:1-alpine");
46
+ });
47
+
48
+ test("listAllowedImageRefs includes rust:1-alpine after registration", () => {
49
+ registerRustSandboxImage();
50
+ const refs = listAllowedImageRefs();
51
+ expect(refs).toContain("rust:1-alpine");
52
+ expect(refs).toContain("python:3.13-slim");
53
+ });
54
+ });
55
+
56
+ describe("Rust-shape T2 contract — round-trip via noop sandbox", () => {
57
+ beforeEach(() => _resetSandboxImageRegistry());
58
+ afterEach(() => _resetSandboxImageRegistry());
59
+
60
+ test("noop sandbox accepts rust:1-alpine when registered", async () => {
61
+ registerRustSandboxImage();
62
+ const sandbox = createSandbox({
63
+ backend: "noop",
64
+ allowedImages: listAllowedImageRefs(),
65
+ });
66
+ const result = await sandbox.exec({
67
+ image: RUST_IMAGE_REF,
68
+ argv: ["printf", "hello-from-rust"],
69
+ });
70
+ expect(result.exitCode).toBe(0);
71
+ expect(result.stdout).toBe("hello-from-rust");
72
+ await sandbox.close();
73
+ });
74
+
75
+ test("noop sandbox refuses rust:1-alpine when NOT registered", async () => {
76
+ const trioRefs = ["python:3.13-slim", "node:22-alpine", "alpine:3.19"];
77
+ const sandbox = createSandbox({ backend: "noop", allowedImages: trioRefs });
78
+ await expect(sandbox.exec({ image: RUST_IMAGE_REF, argv: ["printf", "x"] })).rejects.toThrow(
79
+ /not on the allowlist/,
80
+ );
81
+ await sandbox.close();
82
+ });
83
+ });
84
+
85
+ describe("Rust-shape T7 — cold-start budget", () => {
86
+ beforeEach(() => _resetSandboxImageRegistry());
87
+ afterEach(() => _resetSandboxImageRegistry());
88
+
89
+ test("healthcheck timeoutMs is within the compiled-language budget (≤2s)", () => {
90
+ const entry = registerRustSandboxImage();
91
+ expect(entry.healthcheck.timeoutMs).toBeLessThanOrEqual(2_000);
92
+ expect(entry.healthcheck.timeoutMs).toBeGreaterThan(0);
93
+ });
94
+ });
95
+
96
+ describe("Rust-shape T8 — escape-attempt suite (reuses §18 corpus shape)", () => {
97
+ beforeEach(() => _resetSandboxImageRegistry());
98
+ afterEach(() => _resetSandboxImageRegistry());
99
+
100
+ test("Rust image registration is idempotent-then-rejected", () => {
101
+ registerRustSandboxImage();
102
+ expect(() => registerRustSandboxImage()).toThrow(/already registered/);
103
+ });
104
+
105
+ test("Rust entry's image string passes registry validation", () => {
106
+ expect(RUST_IMAGE_REF.startsWith("-")).toBe(false);
107
+ expect(/\s/.test(RUST_IMAGE_REF)).toBe(false);
108
+ expect(RUST_IMAGE_REF.includes("\n")).toBe(false);
109
+ });
110
+
111
+ test("Rust defaultEntrypoint contains no shell-meta", () => {
112
+ for (const arg of RUST_DEFAULT_ENTRYPOINT) {
113
+ expect(/[;&|<>$`(){}]/.test(arg)).toBe(false);
114
+ expect(arg.includes("\n")).toBe(false);
115
+ }
116
+ });
117
+
118
+ test("Rust healthcheck argv contains no shell-meta", () => {
119
+ for (const arg of RUST_HEALTHCHECK_ARGV) {
120
+ expect(/[;&|<>$`(){}]/.test(arg)).toBe(false);
121
+ expect(arg.includes("\n")).toBe(false);
122
+ }
123
+ });
124
+
125
+ test("attempted CLI-flag-injection registration via Rust id is refused", () => {
126
+ expect(() =>
127
+ registerSandboxImage({
128
+ id: "rust",
129
+ image: "--privileged",
130
+ defaultEntrypoint: ["rustc", "-"],
131
+ healthcheck: { command: ["rustc", "--version"], expectedExitCode: 0 },
132
+ }),
133
+ ).toThrow(ImageRegistrationError);
134
+ });
135
+
136
+ test("attempted whitespace-tampered Rust image is refused", () => {
137
+ expect(() =>
138
+ registerSandboxImage({
139
+ id: "rust",
140
+ image: "rust:1-alpine --privileged",
141
+ defaultEntrypoint: ["rustc", "-"],
142
+ healthcheck: { command: ["rustc", "--version"], expectedExitCode: 0 },
143
+ }),
144
+ ).toThrow(/whitespace/);
145
+ });
146
+
147
+ test("attempted shell-meta-tagged Rust image is refused", () => {
148
+ expect(() =>
149
+ registerSandboxImage({
150
+ id: "rust",
151
+ image: "rust:$(id)",
152
+ defaultEntrypoint: ["rustc", "-"],
153
+ healthcheck: { command: ["rustc", "--version"], expectedExitCode: 0 },
154
+ }),
155
+ ).toThrow(/valid registry/);
156
+ });
157
+ });
package/src/index.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { type SandboxImageEntry, registerSandboxImage } from "@crewhaus/sandbox-image-registry";
2
+
3
+ /**
4
+ * Catalog R8 `sandbox-image-rust` — Section 36 polyglot Rust sandbox image.
5
+ *
6
+ * Registers a curated Rust stable image into `@crewhaus/sandbox-image-registry`
7
+ * so polyglot agents can `lookupSandboxImage("rust")` and route a snippet
8
+ * through `tool-code-execution`'s sandbox path.
9
+ *
10
+ * Snippet mode (default entrypoint): `rustc - -o /tmp/snippet && /tmp/snippet`
11
+ * is the cargo-script-style pattern. Advanced callers who mount a project
12
+ * exec their own compiled binary path. We keep the registered entrypoint
13
+ * short — `["rustc", "-"]` — and let `tool-code-execution` extend it with
14
+ * the snippet text + post-compile exec at call time.
15
+ *
16
+ * Cold-start budget: ≤2s for the compiled-language warm pool. Rust's
17
+ * `rustc --version` healthcheck is a single binary invocation and stays
18
+ * well under the budget. The actual compile-and-run round-trip can take
19
+ * longer; tool-code-execution callers may pass a higher timeoutMs.
20
+ *
21
+ * Layer R8.
22
+ */
23
+
24
+ export const RUST_IMAGE_ID = "rust";
25
+ export const RUST_IMAGE_REF = "rust:1-alpine";
26
+ export const RUST_DEFAULT_ENTRYPOINT: ReadonlyArray<string> = ["rustc", "-"];
27
+ export const RUST_HEALTHCHECK_ARGV: ReadonlyArray<string> = ["rustc", "--version"];
28
+
29
+ /** Cold-start budget for the warm pool (ms). T7 layer asserts this. */
30
+ export const RUST_COLD_START_BUDGET_MS = 2_000;
31
+
32
+ /** Idempotent — calling twice throws via the registry's duplicate-id refusal. */
33
+ export function registerRustSandboxImage(): SandboxImageEntry {
34
+ return registerSandboxImage({
35
+ id: RUST_IMAGE_ID,
36
+ image: RUST_IMAGE_REF,
37
+ defaultEntrypoint: RUST_DEFAULT_ENTRYPOINT,
38
+ healthcheck: {
39
+ command: RUST_HEALTHCHECK_ARGV,
40
+ expectedExitCode: 0,
41
+ timeoutMs: RUST_COLD_START_BUDGET_MS,
42
+ },
43
+ description:
44
+ "Rust stable on alpine — snippet mode via `rustc -`; compiled-binary mode for mounted crates.",
45
+ });
46
+ }