@hachej/boring-sandbox 0.1.78 → 0.1.79

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.
@@ -0,0 +1,216 @@
1
+ import { z } from 'zod';
2
+
3
+ declare const RunscPreflightConfigSchema: z.ZodEffects<z.ZodObject<{
4
+ stateRoot: z.ZodEffects<z.ZodString, string, string>;
5
+ digestMarkerPath: z.ZodEffects<z.ZodString, string, string>;
6
+ networkNamespace: z.ZodString;
7
+ nftTable: z.ZodString;
8
+ requiredBlockedCidrs: z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">;
9
+ cgroupRoot: z.ZodEffects<z.ZodString, string, string>;
10
+ workspaceCgroupRoot: z.ZodEffects<z.ZodString, string, string>;
11
+ binaries: z.ZodObject<{
12
+ runsc: z.ZodEffects<z.ZodString, string, string>;
13
+ ip: z.ZodEffects<z.ZodString, string, string>;
14
+ nft: z.ZodEffects<z.ZodString, string, string>;
15
+ cat: z.ZodEffects<z.ZodString, string, string>;
16
+ true: z.ZodEffects<z.ZodString, string, string>;
17
+ }, "strict", z.ZodTypeAny, {
18
+ runsc: string;
19
+ ip: string;
20
+ nft: string;
21
+ cat: string;
22
+ true: string;
23
+ }, {
24
+ runsc: string;
25
+ ip: string;
26
+ nft: string;
27
+ cat: string;
28
+ true: string;
29
+ }>;
30
+ expected: z.ZodObject<{
31
+ imageDigest: z.ZodString;
32
+ cpuPeriodMicros: z.ZodNumber;
33
+ cpuQuotaMicros: z.ZodNumber;
34
+ memoryBytes: z.ZodNumber;
35
+ pidsMax: z.ZodNumber;
36
+ }, "strict", z.ZodTypeAny, {
37
+ imageDigest: string;
38
+ cpuPeriodMicros: number;
39
+ cpuQuotaMicros: number;
40
+ memoryBytes: number;
41
+ pidsMax: number;
42
+ }, {
43
+ imageDigest: string;
44
+ cpuPeriodMicros: number;
45
+ cpuQuotaMicros: number;
46
+ memoryBytes: number;
47
+ pidsMax: number;
48
+ }>;
49
+ }, "strict", z.ZodTypeAny, {
50
+ stateRoot: string;
51
+ digestMarkerPath: string;
52
+ networkNamespace: string;
53
+ nftTable: string;
54
+ expected: {
55
+ imageDigest: string;
56
+ cpuPeriodMicros: number;
57
+ cpuQuotaMicros: number;
58
+ memoryBytes: number;
59
+ pidsMax: number;
60
+ };
61
+ requiredBlockedCidrs: string[];
62
+ cgroupRoot: string;
63
+ workspaceCgroupRoot: string;
64
+ binaries: {
65
+ runsc: string;
66
+ ip: string;
67
+ nft: string;
68
+ cat: string;
69
+ true: string;
70
+ };
71
+ }, {
72
+ stateRoot: string;
73
+ digestMarkerPath: string;
74
+ networkNamespace: string;
75
+ nftTable: string;
76
+ expected: {
77
+ imageDigest: string;
78
+ cpuPeriodMicros: number;
79
+ cpuQuotaMicros: number;
80
+ memoryBytes: number;
81
+ pidsMax: number;
82
+ };
83
+ requiredBlockedCidrs: string[];
84
+ cgroupRoot: string;
85
+ workspaceCgroupRoot: string;
86
+ binaries: {
87
+ runsc: string;
88
+ ip: string;
89
+ nft: string;
90
+ cat: string;
91
+ true: string;
92
+ };
93
+ }>, {
94
+ stateRoot: string;
95
+ digestMarkerPath: string;
96
+ networkNamespace: string;
97
+ nftTable: string;
98
+ expected: {
99
+ imageDigest: string;
100
+ cpuPeriodMicros: number;
101
+ cpuQuotaMicros: number;
102
+ memoryBytes: number;
103
+ pidsMax: number;
104
+ };
105
+ requiredBlockedCidrs: string[];
106
+ cgroupRoot: string;
107
+ workspaceCgroupRoot: string;
108
+ binaries: {
109
+ runsc: string;
110
+ ip: string;
111
+ nft: string;
112
+ cat: string;
113
+ true: string;
114
+ };
115
+ }, {
116
+ stateRoot: string;
117
+ digestMarkerPath: string;
118
+ networkNamespace: string;
119
+ nftTable: string;
120
+ expected: {
121
+ imageDigest: string;
122
+ cpuPeriodMicros: number;
123
+ cpuQuotaMicros: number;
124
+ memoryBytes: number;
125
+ pidsMax: number;
126
+ };
127
+ requiredBlockedCidrs: string[];
128
+ cgroupRoot: string;
129
+ workspaceCgroupRoot: string;
130
+ binaries: {
131
+ runsc: string;
132
+ ip: string;
133
+ nft: string;
134
+ cat: string;
135
+ true: string;
136
+ };
137
+ }>;
138
+ type RunscPreflightConfig = z.infer<typeof RunscPreflightConfigSchema>;
139
+ declare function validateRunscPreflightConfig(value: unknown): RunscPreflightConfig;
140
+
141
+ declare const RUNSC_PREFLIGHT_ERROR_CODES: {
142
+ readonly invalidConfig: "RUNSC_PREFLIGHT_INVALID_CONFIG";
143
+ readonly commandFailed: "RUNSC_PREFLIGHT_COMMAND_FAILED";
144
+ readonly invalidOutput: "RUNSC_PREFLIGHT_INVALID_OUTPUT";
145
+ readonly structuralMismatch: "RUNSC_PREFLIGHT_STRUCTURAL_MISMATCH";
146
+ };
147
+ declare const RUNSC_REQUIRED_BLOCKED_CIDRS: readonly ["10.0.0.0/8", "100.64.0.0/10", "169.254.0.0/16", "172.16.0.0/12", "192.168.0.0/16", "fc00::/7", "fe80::/10"];
148
+ type RunscPreflightErrorCode = (typeof RUNSC_PREFLIGHT_ERROR_CODES)[keyof typeof RUNSC_PREFLIGHT_ERROR_CODES];
149
+ interface RunscStructuralObservations {
150
+ runscVersionOutputValid: true;
151
+ digestMarkerMatchesExpected: true;
152
+ namespaceCommandSucceeded: true;
153
+ nftTableReadable: true;
154
+ configuredCidrTextPresent: true;
155
+ cgroupControllersPresent: readonly ["cpu", "memory", "pids"];
156
+ configuredLimitFilesMatchExpected: true;
157
+ }
158
+ /** Security properties that this structural preflight deliberately does not attest. */
159
+ declare const RUNSC_UNPROVEN_SECURITY_FACTS: {
160
+ readonly systrapWorkload: "unknown";
161
+ readonly imageDigestBinding: "unknown";
162
+ readonly effectiveUid: "unknown";
163
+ readonly effectiveGid: "unknown";
164
+ readonly cgroupMembership: "unknown";
165
+ readonly resourceEnforcement: "unknown";
166
+ readonly networkIsolation: "unknown";
167
+ readonly metadataEgressDenied: "unknown";
168
+ readonly privateNetworkEgressDenied: "unknown";
169
+ readonly hostNetworkEgressDenied: "unknown";
170
+ readonly crossWorkspaceEgressDenied: "unknown";
171
+ readonly nftDropRulesEffective: "unknown";
172
+ readonly ociBundleUsed: "unknown";
173
+ readonly containerConfigurationUsed: "unknown";
174
+ readonly rootPathSafety: "unknown";
175
+ readonly hostRunnerEnforcement: "unknown";
176
+ };
177
+ type RunscUnprovenSecurityFacts = typeof RUNSC_UNPROVEN_SECURITY_FACTS;
178
+ interface RunscPreflightErrorRecord {
179
+ code: RunscPreflightErrorCode;
180
+ message: string;
181
+ }
182
+ type RunscPreflightResult = {
183
+ status: "observed";
184
+ provider: "runsc";
185
+ productionReady: false;
186
+ observations: RunscStructuralObservations;
187
+ unproven: RunscUnprovenSecurityFacts;
188
+ } | {
189
+ status: "failed";
190
+ provider: "runsc";
191
+ productionReady: false;
192
+ error: RunscPreflightErrorRecord;
193
+ };
194
+
195
+ declare class RunscPreflightError extends Error {
196
+ readonly code: RunscPreflightErrorCode;
197
+ constructor(code: RunscPreflightErrorCode, message: string);
198
+ }
199
+
200
+ interface RunscHostCommand {
201
+ file: string;
202
+ args: readonly string[];
203
+ timeoutMs: number;
204
+ maxOutputBytes: number;
205
+ }
206
+ interface RunscHostCommandResult {
207
+ exitCode: number;
208
+ stdout: string;
209
+ stderr: string;
210
+ }
211
+ interface RunscHostCommandRunner {
212
+ run(command: RunscHostCommand): Promise<unknown>;
213
+ }
214
+ declare function preflightRunsc(input: unknown, runner: RunscHostCommandRunner): Promise<RunscPreflightResult>;
215
+
216
+ export { RUNSC_PREFLIGHT_ERROR_CODES, RUNSC_REQUIRED_BLOCKED_CIDRS, RUNSC_UNPROVEN_SECURITY_FACTS, type RunscHostCommand, type RunscHostCommandResult, type RunscHostCommandRunner, type RunscPreflightConfig, RunscPreflightError, type RunscPreflightErrorCode, type RunscPreflightErrorRecord, type RunscPreflightResult, type RunscStructuralObservations, type RunscUnprovenSecurityFacts, preflightRunsc, validateRunscPreflightConfig };
@@ -0,0 +1,280 @@
1
+ // src/providers/runsc/config.ts
2
+ import { isIP } from "net";
3
+ import { isAbsolute, relative, resolve } from "path";
4
+ import { z } from "zod";
5
+
6
+ // src/shared/runsc.ts
7
+ var RUNSC_PREFLIGHT_ERROR_CODES = {
8
+ invalidConfig: "RUNSC_PREFLIGHT_INVALID_CONFIG",
9
+ commandFailed: "RUNSC_PREFLIGHT_COMMAND_FAILED",
10
+ invalidOutput: "RUNSC_PREFLIGHT_INVALID_OUTPUT",
11
+ structuralMismatch: "RUNSC_PREFLIGHT_STRUCTURAL_MISMATCH"
12
+ };
13
+ var RUNSC_REQUIRED_BLOCKED_CIDRS = [
14
+ "10.0.0.0/8",
15
+ "100.64.0.0/10",
16
+ "169.254.0.0/16",
17
+ "172.16.0.0/12",
18
+ "192.168.0.0/16",
19
+ "fc00::/7",
20
+ "fe80::/10"
21
+ ];
22
+ var RUNSC_UNPROVEN_SECURITY_FACTS = {
23
+ systrapWorkload: "unknown",
24
+ imageDigestBinding: "unknown",
25
+ effectiveUid: "unknown",
26
+ effectiveGid: "unknown",
27
+ cgroupMembership: "unknown",
28
+ resourceEnforcement: "unknown",
29
+ networkIsolation: "unknown",
30
+ metadataEgressDenied: "unknown",
31
+ privateNetworkEgressDenied: "unknown",
32
+ hostNetworkEgressDenied: "unknown",
33
+ crossWorkspaceEgressDenied: "unknown",
34
+ nftDropRulesEffective: "unknown",
35
+ ociBundleUsed: "unknown",
36
+ containerConfigurationUsed: "unknown",
37
+ rootPathSafety: "unknown",
38
+ hostRunnerEnforcement: "unknown"
39
+ };
40
+
41
+ // src/providers/runsc/errors.ts
42
+ var RunscPreflightError = class extends Error {
43
+ constructor(code, message) {
44
+ super(message);
45
+ this.code = code;
46
+ this.name = "RunscPreflightError";
47
+ }
48
+ code;
49
+ };
50
+
51
+ // src/providers/runsc/config.ts
52
+ var SAFE_ID = /^[a-z0-9][a-z0-9_.-]{0,62}$/;
53
+ var SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/;
54
+ var MIN_MEMORY_BYTES = 64 * 1024 * 1024;
55
+ var AbsolutePath = z.string().min(1).max(4096).refine((value) => isAbsolute(value) && !value.includes("\0"));
56
+ var Cidr = z.string().max(64).refine(isCidr);
57
+ var PositiveInteger = z.number().int().positive().safe();
58
+ var NonEmptyString = z.string().min(1);
59
+ var RunscPreflightConfigSchema = z.object({
60
+ stateRoot: AbsolutePath,
61
+ digestMarkerPath: AbsolutePath,
62
+ networkNamespace: NonEmptyString.regex(SAFE_ID),
63
+ nftTable: NonEmptyString.regex(SAFE_ID),
64
+ requiredBlockedCidrs: z.array(Cidr).max(64),
65
+ cgroupRoot: AbsolutePath,
66
+ workspaceCgroupRoot: AbsolutePath,
67
+ binaries: z.object({
68
+ runsc: AbsolutePath,
69
+ ip: AbsolutePath,
70
+ nft: AbsolutePath,
71
+ cat: AbsolutePath,
72
+ true: AbsolutePath
73
+ }).strict(),
74
+ expected: z.object({
75
+ imageDigest: NonEmptyString.regex(SHA256_DIGEST),
76
+ cpuPeriodMicros: PositiveInteger,
77
+ cpuQuotaMicros: PositiveInteger,
78
+ memoryBytes: z.number().int().safe().min(MIN_MEMORY_BYTES),
79
+ pidsMax: z.number().int().min(16).max(65536)
80
+ }).strict()
81
+ }).strict().superRefine((config, ctx) => {
82
+ if (!isChildPath(config.stateRoot, config.digestMarkerPath)) reject(ctx);
83
+ if (!isChildPath(config.cgroupRoot, config.workspaceCgroupRoot)) reject(ctx);
84
+ if (RUNSC_REQUIRED_BLOCKED_CIDRS.some((cidr) => !config.requiredBlockedCidrs.includes(cidr))) {
85
+ reject(ctx);
86
+ }
87
+ if (config.expected.cpuQuotaMicros > config.expected.cpuPeriodMicros * 64) reject(ctx);
88
+ });
89
+ function validateRunscPreflightConfig(value) {
90
+ const parsed = RunscPreflightConfigSchema.safeParse(value);
91
+ if (!parsed.success) invalid("config does not match the runsc preflight schema");
92
+ return parsed.data;
93
+ }
94
+ function reject(ctx) {
95
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "cross-field constraint failed" });
96
+ }
97
+ function isChildPath(root, value) {
98
+ const child = resolve(value);
99
+ const rel = relative(resolve(root), child);
100
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
101
+ }
102
+ function isCidr(value) {
103
+ const [address, prefix, extra] = value.split("/");
104
+ const family = address ? isIP(address) : 0;
105
+ if (!prefix || !/^\d+$/.test(prefix)) return false;
106
+ const prefixNumber = Number(prefix);
107
+ const maxPrefix = family === 4 ? 32 : family === 6 ? 128 : -1;
108
+ return !(extra !== void 0 || !Number.isInteger(prefixNumber) || prefixNumber < 0 || prefixNumber > maxPrefix);
109
+ }
110
+ function invalid(message) {
111
+ throw new RunscPreflightError(RUNSC_PREFLIGHT_ERROR_CODES.invalidConfig, message);
112
+ }
113
+
114
+ // src/providers/runsc/preflight.ts
115
+ import { join } from "path";
116
+ var PREFLIGHT_TIMEOUT_MS = 1e4;
117
+ var PREFLIGHT_MAX_OUTPUT_BYTES = 256 * 1024;
118
+ async function preflightRunsc(input, runner) {
119
+ try {
120
+ const config = validateRunscPreflightConfig(input);
121
+ const version = await runProbe(runner, "runsc version", config.binaries.runsc, ["--version"]);
122
+ assertVersionOutput(version.stdout);
123
+ const bundleDigest = await runProbe(runner, "digest marker", config.binaries.cat, [
124
+ config.digestMarkerPath
125
+ ]);
126
+ if (bundleDigest.stdout.trim() !== config.expected.imageDigest) {
127
+ throw new RunscPreflightError(
128
+ RUNSC_PREFLIGHT_ERROR_CODES.structuralMismatch,
129
+ "digest marker does not match the configured image digest"
130
+ );
131
+ }
132
+ await runProbe(runner, "network namespace", config.binaries.ip, [
133
+ "netns",
134
+ "exec",
135
+ config.networkNamespace,
136
+ config.binaries.true
137
+ ]);
138
+ const nft = await runProbe(runner, "nftables table", config.binaries.nft, [
139
+ "list",
140
+ "table",
141
+ "inet",
142
+ config.nftTable
143
+ ]);
144
+ for (const cidr of config.requiredBlockedCidrs) {
145
+ if (!nft.stdout.includes(cidr)) {
146
+ throw new RunscPreflightError(
147
+ RUNSC_PREFLIGHT_ERROR_CODES.structuralMismatch,
148
+ "nftables table output is missing configured CIDR text"
149
+ );
150
+ }
151
+ }
152
+ const controllers = await runProbe(runner, "cgroup v2 controllers", config.binaries.cat, [
153
+ join(config.cgroupRoot, "cgroup.controllers")
154
+ ]);
155
+ const controllerSet = new Set(controllers.stdout.trim().split(/\s+/));
156
+ for (const controller of ["cpu", "memory", "pids"]) {
157
+ if (!controllerSet.has(controller)) {
158
+ throw new RunscPreflightError(
159
+ RUNSC_PREFLIGHT_ERROR_CODES.structuralMismatch,
160
+ `cgroup v2 controller ${controller} is unavailable`
161
+ );
162
+ }
163
+ }
164
+ await verifyResourceLimits(config, runner);
165
+ return {
166
+ status: "observed",
167
+ provider: "runsc",
168
+ productionReady: false,
169
+ observations: {
170
+ runscVersionOutputValid: true,
171
+ digestMarkerMatchesExpected: true,
172
+ namespaceCommandSucceeded: true,
173
+ nftTableReadable: true,
174
+ configuredCidrTextPresent: true,
175
+ cgroupControllersPresent: ["cpu", "memory", "pids"],
176
+ configuredLimitFilesMatchExpected: true
177
+ },
178
+ unproven: RUNSC_UNPROVEN_SECURITY_FACTS
179
+ };
180
+ } catch (error) {
181
+ const normalized = normalizeError(error);
182
+ return {
183
+ status: "failed",
184
+ provider: "runsc",
185
+ productionReady: false,
186
+ error: { code: normalized.code, message: normalized.message }
187
+ };
188
+ }
189
+ }
190
+ async function runProbe(runner, label, file, args) {
191
+ let raw;
192
+ try {
193
+ raw = await runner.run({
194
+ file,
195
+ args,
196
+ timeoutMs: PREFLIGHT_TIMEOUT_MS,
197
+ maxOutputBytes: PREFLIGHT_MAX_OUTPUT_BYTES
198
+ });
199
+ } catch {
200
+ throw new RunscPreflightError(RUNSC_PREFLIGHT_ERROR_CODES.commandFailed, `${label} probe failed`);
201
+ }
202
+ const result = parseCommandResult(raw, label);
203
+ if (result.exitCode !== 0) {
204
+ throw new RunscPreflightError(RUNSC_PREFLIGHT_ERROR_CODES.commandFailed, `${label} probe failed`);
205
+ }
206
+ return result;
207
+ }
208
+ function assertVersionOutput(output) {
209
+ const line = output.split(/\r?\n/, 1)[0]?.trim() ?? "";
210
+ const match = /^runsc version\s+([a-z0-9][a-z0-9._+-]{0,127})$/i.exec(line);
211
+ if (!match?.[1]) {
212
+ throw new RunscPreflightError(
213
+ RUNSC_PREFLIGHT_ERROR_CODES.invalidOutput,
214
+ "runsc version output is invalid"
215
+ );
216
+ }
217
+ }
218
+ async function verifyResourceLimits(config, runner) {
219
+ const expected = config.expected;
220
+ const cpu = await runProbe(runner, "cgroup CPU limit", config.binaries.cat, [
221
+ join(config.workspaceCgroupRoot, "cpu.max")
222
+ ]);
223
+ const memory = await runProbe(runner, "cgroup memory limit", config.binaries.cat, [
224
+ join(config.workspaceCgroupRoot, "memory.max")
225
+ ]);
226
+ const pids = await runProbe(runner, "cgroup pid limit", config.binaries.cat, [
227
+ join(config.workspaceCgroupRoot, "pids.max")
228
+ ]);
229
+ const [quota, period, extra] = cpu.stdout.trim().split(/\s+/);
230
+ if (extra !== void 0 || quota !== String(expected.cpuQuotaMicros) || period !== String(expected.cpuPeriodMicros)) {
231
+ mismatch("CPU");
232
+ }
233
+ if (memory.stdout.trim() !== String(expected.memoryBytes)) mismatch("memory");
234
+ if (pids.stdout.trim() !== String(expected.pidsMax)) mismatch("pid");
235
+ }
236
+ function mismatch(resource) {
237
+ throw new RunscPreflightError(
238
+ RUNSC_PREFLIGHT_ERROR_CODES.structuralMismatch,
239
+ `cgroup ${resource} limit does not match the approved policy`
240
+ );
241
+ }
242
+ function parseCommandResult(value, label) {
243
+ if (typeof value !== "object" || value === null || Array.isArray(value)) invalidOutput(label);
244
+ const result = value;
245
+ if (!Number.isInteger(result.exitCode) || typeof result.stdout !== "string" || typeof result.stderr !== "string") {
246
+ invalidOutput(label);
247
+ }
248
+ const stdoutBytes = outputBytes(result.stdout);
249
+ const stderrBudget = PREFLIGHT_MAX_OUTPUT_BYTES - stdoutBytes;
250
+ if (stdoutBytes > PREFLIGHT_MAX_OUTPUT_BYTES || outputBytes(result.stderr) > stderrBudget) {
251
+ invalidOutput(label);
252
+ }
253
+ return {
254
+ exitCode: result.exitCode,
255
+ stdout: result.stdout,
256
+ stderr: result.stderr
257
+ };
258
+ }
259
+ function outputBytes(value) {
260
+ if (value.length > PREFLIGHT_MAX_OUTPUT_BYTES) return value.length;
261
+ return new TextEncoder().encode(value).byteLength;
262
+ }
263
+ function invalidOutput(label) {
264
+ throw new RunscPreflightError(
265
+ RUNSC_PREFLIGHT_ERROR_CODES.invalidOutput,
266
+ `${label} probe returned invalid output`
267
+ );
268
+ }
269
+ function normalizeError(error) {
270
+ if (error instanceof RunscPreflightError) return error;
271
+ return new RunscPreflightError(RUNSC_PREFLIGHT_ERROR_CODES.commandFailed, "runsc preflight failed");
272
+ }
273
+ export {
274
+ RUNSC_PREFLIGHT_ERROR_CODES,
275
+ RUNSC_REQUIRED_BLOCKED_CIDRS,
276
+ RUNSC_UNPROVEN_SECURITY_FACTS,
277
+ RunscPreflightError,
278
+ preflightRunsc,
279
+ validateRunscPreflightConfig
280
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-sandbox",
3
- "version": "0.1.78",
3
+ "version": "0.1.79",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Sandbox provider contracts and implementations for Boring runtimes.",
@@ -24,9 +24,17 @@
24
24
  "./providers": {
25
25
  "types": "./dist/providers/index.d.ts",
26
26
  "import": "./dist/providers/index.js"
27
+ },
28
+ "./providers/runsc": {
29
+ "types": "./dist/providers/runsc/index.d.ts",
30
+ "import": "./dist/providers/runsc/index.js"
27
31
  }
28
32
  },
33
+ "dependencies": {
34
+ "zod": "^3.25.76"
35
+ },
29
36
  "devDependencies": {
37
+ "@types/node": "^22.20.0",
30
38
  "tsup": "^8.4.0",
31
39
  "typescript": "^6.0.3",
32
40
  "vitest": "^4.1.9"