@crewhaus/sandbox-image-dotnet 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 +162 -0
- package/src/index.ts +41 -0
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/sandbox-image-dotnet",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": ".NET 8 polyglot sandbox image: registry registration + 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-dotnet"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/sandbox-image-dotnet#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,162 @@
|
|
|
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
|
+
DOTNET_COLD_START_BUDGET_MS,
|
|
13
|
+
DOTNET_DEFAULT_ENTRYPOINT,
|
|
14
|
+
DOTNET_HEALTHCHECK_ARGV,
|
|
15
|
+
DOTNET_IMAGE_ID,
|
|
16
|
+
DOTNET_IMAGE_REF,
|
|
17
|
+
registerDotnetSandboxImage,
|
|
18
|
+
} from "./index";
|
|
19
|
+
|
|
20
|
+
describe("registerDotnetSandboxImage (T1 + T2)", () => {
|
|
21
|
+
beforeEach(() => _resetSandboxImageRegistry());
|
|
22
|
+
afterEach(() => _resetSandboxImageRegistry());
|
|
23
|
+
|
|
24
|
+
test("constants match the kickoff prompt's spec", () => {
|
|
25
|
+
expect(DOTNET_IMAGE_ID).toBe("dotnet");
|
|
26
|
+
expect(DOTNET_IMAGE_REF).toBe("mcr.microsoft.com/dotnet/sdk:8.0-alpine");
|
|
27
|
+
expect(DOTNET_DEFAULT_ENTRYPOINT).toEqual(["dotnet", "script"]);
|
|
28
|
+
expect(DOTNET_HEALTHCHECK_ARGV).toEqual(["dotnet", "--version"]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("registerDotnetSandboxImage() registers an image with the right shape", () => {
|
|
32
|
+
const entry = registerDotnetSandboxImage();
|
|
33
|
+
expect(entry.id).toBe("dotnet");
|
|
34
|
+
expect(entry.image).toBe("mcr.microsoft.com/dotnet/sdk:8.0-alpine");
|
|
35
|
+
expect(entry.defaultEntrypoint).toEqual(["dotnet", "script"]);
|
|
36
|
+
expect(entry.healthcheck.command).toEqual(["dotnet", "--version"]);
|
|
37
|
+
expect(entry.healthcheck.expectedExitCode).toBe(0);
|
|
38
|
+
expect(entry.healthcheck.timeoutMs).toBe(DOTNET_COLD_START_BUDGET_MS);
|
|
39
|
+
expect(entry.description).toMatch(/\.NET 8/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("lookupSandboxImage('dotnet') returns the registered entry", () => {
|
|
43
|
+
registerDotnetSandboxImage();
|
|
44
|
+
expect(hasSandboxImage("dotnet")).toBe(true);
|
|
45
|
+
expect(lookupSandboxImage("dotnet").image).toBe("mcr.microsoft.com/dotnet/sdk:8.0-alpine");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("listAllowedImageRefs includes the .NET SDK ref", () => {
|
|
49
|
+
registerDotnetSandboxImage();
|
|
50
|
+
const refs = listAllowedImageRefs();
|
|
51
|
+
expect(refs).toContain("mcr.microsoft.com/dotnet/sdk:8.0-alpine");
|
|
52
|
+
expect(refs).toContain("python:3.13-slim");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("Dotnet-shape T2 contract — round-trip via noop sandbox", () => {
|
|
57
|
+
beforeEach(() => _resetSandboxImageRegistry());
|
|
58
|
+
afterEach(() => _resetSandboxImageRegistry());
|
|
59
|
+
|
|
60
|
+
test("noop sandbox accepts the .NET SDK ref when registered", async () => {
|
|
61
|
+
registerDotnetSandboxImage();
|
|
62
|
+
const sandbox = createSandbox({
|
|
63
|
+
backend: "noop",
|
|
64
|
+
allowedImages: listAllowedImageRefs(),
|
|
65
|
+
});
|
|
66
|
+
const result = await sandbox.exec({
|
|
67
|
+
image: DOTNET_IMAGE_REF,
|
|
68
|
+
argv: ["printf", "hello-from-dotnet"],
|
|
69
|
+
});
|
|
70
|
+
expect(result.exitCode).toBe(0);
|
|
71
|
+
expect(result.stdout).toBe("hello-from-dotnet");
|
|
72
|
+
await sandbox.close();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("noop sandbox refuses the .NET SDK ref 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: DOTNET_IMAGE_REF, argv: ["printf", "x"] })).rejects.toThrow(
|
|
79
|
+
/not on the allowlist/,
|
|
80
|
+
);
|
|
81
|
+
await sandbox.close();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("Dotnet-shape T7 — cold-start budget (≤4s, the looser bucket)", () => {
|
|
86
|
+
beforeEach(() => _resetSandboxImageRegistry());
|
|
87
|
+
afterEach(() => _resetSandboxImageRegistry());
|
|
88
|
+
|
|
89
|
+
test("healthcheck timeoutMs is within the .NET-specific budget (≤4s)", () => {
|
|
90
|
+
const entry = registerDotnetSandboxImage();
|
|
91
|
+
expect(entry.healthcheck.timeoutMs).toBeLessThanOrEqual(4_000);
|
|
92
|
+
expect(entry.healthcheck.timeoutMs).toBeGreaterThan(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test(".NET budget is the looser one (the kickoff prompt grants 4s for .NET)", () => {
|
|
96
|
+
expect(DOTNET_COLD_START_BUDGET_MS).toBe(4_000);
|
|
97
|
+
expect(DOTNET_COLD_START_BUDGET_MS).toBeGreaterThan(2_000);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("Dotnet-shape T8 — escape-attempt suite (reuses §18 corpus shape)", () => {
|
|
102
|
+
beforeEach(() => _resetSandboxImageRegistry());
|
|
103
|
+
afterEach(() => _resetSandboxImageRegistry());
|
|
104
|
+
|
|
105
|
+
test("Dotnet image registration is idempotent-then-rejected", () => {
|
|
106
|
+
registerDotnetSandboxImage();
|
|
107
|
+
expect(() => registerDotnetSandboxImage()).toThrow(/already registered/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("Dotnet entry's image string passes registry validation", () => {
|
|
111
|
+
expect(DOTNET_IMAGE_REF.startsWith("-")).toBe(false);
|
|
112
|
+
expect(/\s/.test(DOTNET_IMAGE_REF)).toBe(false);
|
|
113
|
+
expect(DOTNET_IMAGE_REF.includes("\n")).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("Dotnet defaultEntrypoint contains no shell-meta", () => {
|
|
117
|
+
for (const arg of DOTNET_DEFAULT_ENTRYPOINT) {
|
|
118
|
+
expect(/[;&|<>$`(){}]/.test(arg)).toBe(false);
|
|
119
|
+
expect(arg.includes("\n")).toBe(false);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("Dotnet healthcheck argv contains no shell-meta", () => {
|
|
124
|
+
for (const arg of DOTNET_HEALTHCHECK_ARGV) {
|
|
125
|
+
expect(/[;&|<>$`(){}]/.test(arg)).toBe(false);
|
|
126
|
+
expect(arg.includes("\n")).toBe(false);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("CLI-flag-injection registration via dotnet id is refused", () => {
|
|
131
|
+
expect(() =>
|
|
132
|
+
registerSandboxImage({
|
|
133
|
+
id: "dotnet",
|
|
134
|
+
image: "--privileged",
|
|
135
|
+
defaultEntrypoint: ["dotnet", "script"],
|
|
136
|
+
healthcheck: { command: ["dotnet", "--version"], expectedExitCode: 0 },
|
|
137
|
+
}),
|
|
138
|
+
).toThrow(ImageRegistrationError);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("whitespace-tampered Dotnet image is refused", () => {
|
|
142
|
+
expect(() =>
|
|
143
|
+
registerSandboxImage({
|
|
144
|
+
id: "dotnet",
|
|
145
|
+
image: "mcr.microsoft.com/dotnet/sdk:8.0-alpine --privileged",
|
|
146
|
+
defaultEntrypoint: ["dotnet", "script"],
|
|
147
|
+
healthcheck: { command: ["dotnet", "--version"], expectedExitCode: 0 },
|
|
148
|
+
}),
|
|
149
|
+
).toThrow(/whitespace/);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("shell-meta-tagged Dotnet image is refused", () => {
|
|
153
|
+
expect(() =>
|
|
154
|
+
registerSandboxImage({
|
|
155
|
+
id: "dotnet",
|
|
156
|
+
image: "mcr.microsoft.com/dotnet/sdk:$(id)",
|
|
157
|
+
defaultEntrypoint: ["dotnet", "script"],
|
|
158
|
+
healthcheck: { command: ["dotnet", "--version"], expectedExitCode: 0 },
|
|
159
|
+
}),
|
|
160
|
+
).toThrow(/valid registry/);
|
|
161
|
+
});
|
|
162
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type SandboxImageEntry, registerSandboxImage } from "@crewhaus/sandbox-image-registry";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Catalog R8 `sandbox-image-dotnet` — Section 36 polyglot .NET sandbox image.
|
|
5
|
+
*
|
|
6
|
+
* Snippet mode (default entrypoint): `dotnet script` consumes a `.csx`
|
|
7
|
+
* file or stdin. The image bakes in the globally-installed
|
|
8
|
+
* `dotnet-script` tool so callers don't need a per-invocation install.
|
|
9
|
+
*
|
|
10
|
+
* Cold-start budget: ≤4s — more generous than the compiled-language
|
|
11
|
+
* 2s budget. The kickoff prompt assigns .NET this looser bucket
|
|
12
|
+
* because the .NET runtime + dotnet-script first-run JIT can take
|
|
13
|
+
* 2-3s on alpine. `dotnet --version` itself stays well under budget;
|
|
14
|
+
* the 4s value covers the realistic worst case for the first
|
|
15
|
+
* snippet exec in a fresh warm-pool slot.
|
|
16
|
+
*
|
|
17
|
+
* Layer R8.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const DOTNET_IMAGE_ID = "dotnet";
|
|
21
|
+
export const DOTNET_IMAGE_REF = "mcr.microsoft.com/dotnet/sdk:8.0-alpine";
|
|
22
|
+
export const DOTNET_DEFAULT_ENTRYPOINT: ReadonlyArray<string> = ["dotnet", "script"];
|
|
23
|
+
export const DOTNET_HEALTHCHECK_ARGV: ReadonlyArray<string> = ["dotnet", "--version"];
|
|
24
|
+
|
|
25
|
+
/** Cold-start budget for the warm pool (ms). T7 layer asserts this. */
|
|
26
|
+
export const DOTNET_COLD_START_BUDGET_MS = 4_000;
|
|
27
|
+
|
|
28
|
+
export function registerDotnetSandboxImage(): SandboxImageEntry {
|
|
29
|
+
return registerSandboxImage({
|
|
30
|
+
id: DOTNET_IMAGE_ID,
|
|
31
|
+
image: DOTNET_IMAGE_REF,
|
|
32
|
+
defaultEntrypoint: DOTNET_DEFAULT_ENTRYPOINT,
|
|
33
|
+
healthcheck: {
|
|
34
|
+
command: DOTNET_HEALTHCHECK_ARGV,
|
|
35
|
+
expectedExitCode: 0,
|
|
36
|
+
timeoutMs: DOTNET_COLD_START_BUDGET_MS,
|
|
37
|
+
},
|
|
38
|
+
description:
|
|
39
|
+
".NET 8 SDK alpine — snippet mode via `dotnet script` (preinstalled global tool); compiled-binary mode for mounted projects.",
|
|
40
|
+
});
|
|
41
|
+
}
|