@crewhaus/sandbox-image-php 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 +157 -0
- package/src/index.ts +40 -0
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/sandbox-image-php",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "PHP 8.3 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-php"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/sandbox-image-php#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
|
+
PHP_COLD_START_BUDGET_MS,
|
|
13
|
+
PHP_DEFAULT_ENTRYPOINT,
|
|
14
|
+
PHP_HEALTHCHECK_ARGV,
|
|
15
|
+
PHP_IMAGE_ID,
|
|
16
|
+
PHP_IMAGE_REF,
|
|
17
|
+
registerPhpSandboxImage,
|
|
18
|
+
} from "./index";
|
|
19
|
+
|
|
20
|
+
describe("registerPhpSandboxImage (T1 + T2)", () => {
|
|
21
|
+
beforeEach(() => _resetSandboxImageRegistry());
|
|
22
|
+
afterEach(() => _resetSandboxImageRegistry());
|
|
23
|
+
|
|
24
|
+
test("constants match the kickoff prompt's spec", () => {
|
|
25
|
+
expect(PHP_IMAGE_ID).toBe("php");
|
|
26
|
+
expect(PHP_IMAGE_REF).toBe("php:8.3-alpine");
|
|
27
|
+
expect(PHP_DEFAULT_ENTRYPOINT).toEqual(["php", "-r"]);
|
|
28
|
+
expect(PHP_HEALTHCHECK_ARGV).toEqual(["php", "--version"]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("registerPhpSandboxImage() registers an image with the right shape", () => {
|
|
32
|
+
const entry = registerPhpSandboxImage();
|
|
33
|
+
expect(entry.id).toBe("php");
|
|
34
|
+
expect(entry.image).toBe("php:8.3-alpine");
|
|
35
|
+
expect(entry.defaultEntrypoint).toEqual(["php", "-r"]);
|
|
36
|
+
expect(entry.healthcheck.command).toEqual(["php", "--version"]);
|
|
37
|
+
expect(entry.healthcheck.expectedExitCode).toBe(0);
|
|
38
|
+
expect(entry.healthcheck.timeoutMs).toBe(PHP_COLD_START_BUDGET_MS);
|
|
39
|
+
expect(entry.description).toMatch(/PHP 8\.3/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("lookupSandboxImage('php') returns the registered entry", () => {
|
|
43
|
+
registerPhpSandboxImage();
|
|
44
|
+
expect(hasSandboxImage("php")).toBe(true);
|
|
45
|
+
expect(lookupSandboxImage("php").image).toBe("php:8.3-alpine");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("listAllowedImageRefs includes php:8.3-alpine", () => {
|
|
49
|
+
registerPhpSandboxImage();
|
|
50
|
+
const refs = listAllowedImageRefs();
|
|
51
|
+
expect(refs).toContain("php:8.3-alpine");
|
|
52
|
+
expect(refs).toContain("python:3.13-slim");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("Php-shape T2 contract — round-trip via noop sandbox", () => {
|
|
57
|
+
beforeEach(() => _resetSandboxImageRegistry());
|
|
58
|
+
afterEach(() => _resetSandboxImageRegistry());
|
|
59
|
+
|
|
60
|
+
test("noop sandbox accepts php:8.3-alpine when registered", async () => {
|
|
61
|
+
registerPhpSandboxImage();
|
|
62
|
+
const sandbox = createSandbox({
|
|
63
|
+
backend: "noop",
|
|
64
|
+
allowedImages: listAllowedImageRefs(),
|
|
65
|
+
});
|
|
66
|
+
const result = await sandbox.exec({
|
|
67
|
+
image: PHP_IMAGE_REF,
|
|
68
|
+
argv: ["printf", "hello-from-php"],
|
|
69
|
+
});
|
|
70
|
+
expect(result.exitCode).toBe(0);
|
|
71
|
+
expect(result.stdout).toBe("hello-from-php");
|
|
72
|
+
await sandbox.close();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("noop sandbox refuses php:8.3-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: PHP_IMAGE_REF, argv: ["printf", "x"] })).rejects.toThrow(
|
|
79
|
+
/not on the allowlist/,
|
|
80
|
+
);
|
|
81
|
+
await sandbox.close();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("Php-shape T7 — cold-start budget (≤500ms, the shell-shape bucket)", () => {
|
|
86
|
+
beforeEach(() => _resetSandboxImageRegistry());
|
|
87
|
+
afterEach(() => _resetSandboxImageRegistry());
|
|
88
|
+
|
|
89
|
+
test("healthcheck timeoutMs is within the shell-shape interpreter budget (≤500ms)", () => {
|
|
90
|
+
const entry = registerPhpSandboxImage();
|
|
91
|
+
expect(entry.healthcheck.timeoutMs).toBeLessThanOrEqual(500);
|
|
92
|
+
expect(entry.healthcheck.timeoutMs).toBeGreaterThan(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("Php-shape T8 — escape-attempt suite (reuses §18 corpus shape)", () => {
|
|
97
|
+
beforeEach(() => _resetSandboxImageRegistry());
|
|
98
|
+
afterEach(() => _resetSandboxImageRegistry());
|
|
99
|
+
|
|
100
|
+
test("PHP image registration is idempotent-then-rejected", () => {
|
|
101
|
+
registerPhpSandboxImage();
|
|
102
|
+
expect(() => registerPhpSandboxImage()).toThrow(/already registered/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("PHP entry's image string passes registry validation", () => {
|
|
106
|
+
expect(PHP_IMAGE_REF.startsWith("-")).toBe(false);
|
|
107
|
+
expect(/\s/.test(PHP_IMAGE_REF)).toBe(false);
|
|
108
|
+
expect(PHP_IMAGE_REF.includes("\n")).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("PHP defaultEntrypoint contains no shell-meta", () => {
|
|
112
|
+
for (const arg of PHP_DEFAULT_ENTRYPOINT) {
|
|
113
|
+
expect(/[;&|<>$`(){}]/.test(arg)).toBe(false);
|
|
114
|
+
expect(arg.includes("\n")).toBe(false);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("PHP healthcheck argv contains no shell-meta", () => {
|
|
119
|
+
for (const arg of PHP_HEALTHCHECK_ARGV) {
|
|
120
|
+
expect(/[;&|<>$`(){}]/.test(arg)).toBe(false);
|
|
121
|
+
expect(arg.includes("\n")).toBe(false);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("CLI-flag-injection registration via PHP id is refused", () => {
|
|
126
|
+
expect(() =>
|
|
127
|
+
registerSandboxImage({
|
|
128
|
+
id: "php",
|
|
129
|
+
image: "--privileged",
|
|
130
|
+
defaultEntrypoint: ["php", "-r"],
|
|
131
|
+
healthcheck: { command: ["php", "--version"], expectedExitCode: 0 },
|
|
132
|
+
}),
|
|
133
|
+
).toThrow(ImageRegistrationError);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("whitespace-tampered PHP image is refused", () => {
|
|
137
|
+
expect(() =>
|
|
138
|
+
registerSandboxImage({
|
|
139
|
+
id: "php",
|
|
140
|
+
image: "php:8.3-alpine --privileged",
|
|
141
|
+
defaultEntrypoint: ["php", "-r"],
|
|
142
|
+
healthcheck: { command: ["php", "--version"], expectedExitCode: 0 },
|
|
143
|
+
}),
|
|
144
|
+
).toThrow(/whitespace/);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("shell-meta-tagged PHP image is refused", () => {
|
|
148
|
+
expect(() =>
|
|
149
|
+
registerSandboxImage({
|
|
150
|
+
id: "php",
|
|
151
|
+
image: "php:$(id)",
|
|
152
|
+
defaultEntrypoint: ["php", "-r"],
|
|
153
|
+
healthcheck: { command: ["php", "--version"], expectedExitCode: 0 },
|
|
154
|
+
}),
|
|
155
|
+
).toThrow(/valid registry/);
|
|
156
|
+
});
|
|
157
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type SandboxImageEntry, registerSandboxImage } from "@crewhaus/sandbox-image-registry";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Catalog R8 `sandbox-image-php` — Section 36 polyglot PHP sandbox image.
|
|
5
|
+
*
|
|
6
|
+
* Snippet mode (default entrypoint): `php -r <code>` reads the snippet
|
|
7
|
+
* from the next argv slot. Advanced callers who mount a project can
|
|
8
|
+
* exec `php <file>.php` after a composer install. Composer 2 is
|
|
9
|
+
* preinstalled into /usr/bin/composer for projects with a
|
|
10
|
+
* composer.json.
|
|
11
|
+
*
|
|
12
|
+
* Cold-start budget: ≤500ms — PHP is a shell-shape interpreter and
|
|
13
|
+
* `php --version` returns in ~50ms warm. The kickoff prompt's
|
|
14
|
+
* tighter shell-shape ≤500ms budget applies.
|
|
15
|
+
*
|
|
16
|
+
* Layer R8.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const PHP_IMAGE_ID = "php";
|
|
20
|
+
export const PHP_IMAGE_REF = "php:8.3-alpine";
|
|
21
|
+
export const PHP_DEFAULT_ENTRYPOINT: ReadonlyArray<string> = ["php", "-r"];
|
|
22
|
+
export const PHP_HEALTHCHECK_ARGV: ReadonlyArray<string> = ["php", "--version"];
|
|
23
|
+
|
|
24
|
+
/** Cold-start budget for the warm pool (ms). T7 layer asserts this. */
|
|
25
|
+
export const PHP_COLD_START_BUDGET_MS = 500;
|
|
26
|
+
|
|
27
|
+
export function registerPhpSandboxImage(): SandboxImageEntry {
|
|
28
|
+
return registerSandboxImage({
|
|
29
|
+
id: PHP_IMAGE_ID,
|
|
30
|
+
image: PHP_IMAGE_REF,
|
|
31
|
+
defaultEntrypoint: PHP_DEFAULT_ENTRYPOINT,
|
|
32
|
+
healthcheck: {
|
|
33
|
+
command: PHP_HEALTHCHECK_ARGV,
|
|
34
|
+
expectedExitCode: 0,
|
|
35
|
+
timeoutMs: PHP_COLD_START_BUDGET_MS,
|
|
36
|
+
},
|
|
37
|
+
description:
|
|
38
|
+
"PHP 8.3 alpine — snippet mode via `php -r`; composer 2 preinstalled for mounted projects.",
|
|
39
|
+
});
|
|
40
|
+
}
|