@guilz-dev/belay 0.8.0 → 0.9.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.
Files changed (111) hide show
  1. package/README.md +60 -10
  2. package/dist/adapters/shared/gate-runtime.d.ts +5 -0
  3. package/dist/adapters/shared/gate-runtime.js +237 -33
  4. package/dist/bundle/claude-runtime.mjs +4082 -1230
  5. package/dist/bundle/codex-runtime.mjs +4057 -1205
  6. package/dist/bundle/cursor-runtime.mjs +4057 -1205
  7. package/dist/commands/classify-for-report.js +5 -0
  8. package/dist/commands/doctor.js +90 -27
  9. package/dist/commands/explain.js +3 -2
  10. package/dist/commands/metrics.js +75 -4
  11. package/dist/commands/quality.js +2 -6
  12. package/dist/commands/recovery-checkpoints.d.ts +8 -1
  13. package/dist/commands/recovery-checkpoints.js +44 -11
  14. package/dist/commands/session.d.ts +5 -0
  15. package/dist/commands/session.js +8 -1
  16. package/dist/commands/status.js +1 -1
  17. package/dist/config-io.js +2 -2
  18. package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
  19. package/dist/conformance/contained-execution-guarantee.js +121 -0
  20. package/dist/conformance/guarantee-table.js +7 -0
  21. package/dist/core/audit-io.js +8 -0
  22. package/dist/core/audit-metrics.d.ts +34 -0
  23. package/dist/core/audit-metrics.js +87 -15
  24. package/dist/core/audit-recovery-metrics.d.ts +33 -0
  25. package/dist/core/audit-recovery-metrics.js +146 -0
  26. package/dist/core/audit-types.d.ts +7 -1
  27. package/dist/core/audit-types.js +1 -1
  28. package/dist/core/bounded-output.d.ts +11 -0
  29. package/dist/core/bounded-output.js +26 -0
  30. package/dist/core/capability/attestation.d.ts +56 -0
  31. package/dist/core/capability/attestation.js +110 -3
  32. package/dist/core/capability/boundary-session.d.ts +5 -0
  33. package/dist/core/capability/boundary-session.js +38 -1
  34. package/dist/core/capability/index.d.ts +1 -1
  35. package/dist/core/capability/index.js +1 -1
  36. package/dist/core/config.d.ts +18 -0
  37. package/dist/core/config.js +89 -10
  38. package/dist/core/contained-execution/docker-policy.d.ts +18 -0
  39. package/dist/core/contained-execution/docker-policy.js +97 -0
  40. package/dist/core/contained-execution/docker.d.ts +223 -0
  41. package/dist/core/contained-execution/docker.js +846 -0
  42. package/dist/core/contained-execution/eligibility.d.ts +9 -0
  43. package/dist/core/contained-execution/eligibility.js +103 -0
  44. package/dist/core/contained-execution/failure.d.ts +18 -0
  45. package/dist/core/contained-execution/failure.js +105 -0
  46. package/dist/core/contained-execution/mirror.d.ts +54 -0
  47. package/dist/core/contained-execution/mirror.js +587 -0
  48. package/dist/core/contained-execution/policy.d.ts +10 -0
  49. package/dist/core/contained-execution/policy.js +20 -0
  50. package/dist/core/effect-ir/index.d.ts +1 -1
  51. package/dist/core/effect-ir/index.js +1 -1
  52. package/dist/core/effect-ir/normalize.d.ts +5 -0
  53. package/dist/core/effect-ir/normalize.js +17 -7
  54. package/dist/core/effect-ir/shell-build.js +2 -14
  55. package/dist/core/effect-ir/shell-lower.js +100 -25
  56. package/dist/core/gate-contract.d.ts +4 -1
  57. package/dist/core/gate-contract.js +2 -0
  58. package/dist/core/process-runner.d.ts +10 -3
  59. package/dist/core/process-runner.js +66 -10
  60. package/dist/core/recovery/checkpoint.d.ts +3 -0
  61. package/dist/core/recovery/checkpoint.js +33 -4
  62. package/dist/core/recovery/types.d.ts +1 -0
  63. package/dist/core/scrub.d.ts +17 -0
  64. package/dist/core/scrub.js +499 -5
  65. package/dist/core/standing-allow.d.ts +0 -20
  66. package/dist/core/standing-allow.js +0 -71
  67. package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
  68. package/dist/core/transactional/apply-observed-changes.js +11 -0
  69. package/dist/core/transactional/backend-selector.d.ts +2 -1
  70. package/dist/core/transactional/backend-selector.js +32 -29
  71. package/dist/core/transactional/backend.d.ts +3 -0
  72. package/dist/core/transactional/file-checkpoint-backend.js +242 -54
  73. package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
  74. package/dist/core/transactional/file-checkpoint-git.js +53 -0
  75. package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
  76. package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
  77. package/dist/core/transactional/git-worktree-backend.js +3 -0
  78. package/dist/core/transactional/git-worktree.d.ts +1 -0
  79. package/dist/core/transactional/git-worktree.js +1 -0
  80. package/dist/core/transactional/runner.js +59 -13
  81. package/dist/core/types.d.ts +13 -0
  82. package/dist/core/verdict/parser.d.ts +10 -0
  83. package/dist/core/verdict/parser.js +271 -40
  84. package/dist/core/verdict/shell-semantics.js +21 -0
  85. package/dist/corpus/adversarial-probe.d.ts +1 -1
  86. package/dist/corpus/adversarial-probe.js +3 -3
  87. package/dist/corpus/benign-probe-cores.d.ts +6 -0
  88. package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
  89. package/dist/corpus/coverage-compare.d.ts +36 -0
  90. package/dist/corpus/coverage-compare.js +197 -0
  91. package/dist/corpus/coverage-contexts.d.ts +17 -0
  92. package/dist/corpus/coverage-contexts.js +88 -0
  93. package/dist/corpus/coverage-matrix.d.ts +37 -0
  94. package/dist/corpus/coverage-matrix.js +159 -0
  95. package/dist/corpus/coverage-probe.d.ts +91 -0
  96. package/dist/corpus/coverage-probe.js +341 -0
  97. package/dist/corpus/evaluate.d.ts +1 -1
  98. package/dist/corpus/evaluate.js +1 -1
  99. package/dist/corpus/structural-fixture-root.d.ts +2 -0
  100. package/dist/corpus/structural-fixture-root.js +5 -0
  101. package/dist/operational-insights.d.ts +3 -0
  102. package/dist/operational-insights.js +7 -3
  103. package/dist/runtime-provenance.d.ts +8 -0
  104. package/dist/runtime-provenance.js +34 -0
  105. package/dist/types.d.ts +3 -0
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/package.json +3 -1
  109. package/skills/belay/SKILL.md +19 -0
  110. package/skills/belay/belay-status.md +5 -0
  111. package/dist/corpus/must-allow-commands.d.ts +0 -6
@@ -0,0 +1,846 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import { access, constants, lstat, mkdtemp, realpath, rm } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { isContainedExecutionAttestationFresh } from '../capability/attestation.js';
7
+ import { verifySignedBoundaryAttestation } from '../capability/boundary-attestation-sign.js';
8
+ import { canonicalStringify, hashValue } from '../fingerprint.js';
9
+ import { canonicalPath, pathWithinRoot } from '../path-utils.js';
10
+ import { runProcessWithBoundedOutput, } from '../process-runner.js';
11
+ import { assertContainedExecutionResourceLimits, buildContainedDockerCreateArgs, IMAGE_ID_PATTERN, PROXY_ENV_NAMES, SHM_SIZE_MIB, TMPFS_OPTIONS, TMPFS_SIZE_BYTES, } from './docker-policy.js';
12
+ import { ContainedExecutionFailureError } from './failure.js';
13
+ import { validateContainedExecutionMirrorLease, } from './mirror.js';
14
+ import { CONTAINED_EXECUTION_OUTPUT_SCRUB_OPTIONS } from './policy.js';
15
+ const DOCKER_CONTROL_TIMEOUT_MS = 10_000;
16
+ const CONTAINED_ATTESTATION_TTL_MS = 15 * 60_000;
17
+ const MINIMAL_DOCKER_ENV = Object.freeze({
18
+ DOCKER_CONFIG: '/var/empty/belay-docker-config',
19
+ HOME: '/var/empty',
20
+ LC_ALL: 'C',
21
+ PATH: '/usr/bin:/bin',
22
+ });
23
+ const CONTAINER_ID = /^[a-f0-9]{64}$/;
24
+ const IMAGE_INSPECT_FORMAT = '[{{json .Id}},{{json .Config.Env}}]';
25
+ const CONTAINER_INSPECT_FIELDS = [
26
+ '.Id',
27
+ '.Image',
28
+ '.Config.User',
29
+ '.Config.Env',
30
+ '.Config.Entrypoint',
31
+ '.Config.Cmd',
32
+ '.Config.WorkingDir',
33
+ '.Config.Healthcheck',
34
+ '.HostConfig.AutoRemove',
35
+ '.HostConfig.Privileged',
36
+ '.HostConfig.CapAdd',
37
+ '.HostConfig.CapDrop',
38
+ '.HostConfig.SecurityOpt',
39
+ '.HostConfig.LogConfig',
40
+ '.HostConfig.Devices',
41
+ '.HostConfig.DeviceRequests',
42
+ '.HostConfig.DeviceCgroupRules',
43
+ '.HostConfig.GroupAdd',
44
+ '.HostConfig.VolumesFrom',
45
+ '.HostConfig.Binds',
46
+ '.HostConfig.PortBindings',
47
+ '.HostConfig.PublishAllPorts',
48
+ '.HostConfig.Links',
49
+ '.HostConfig.ExtraHosts',
50
+ '.HostConfig.Dns',
51
+ '.HostConfig.DnsOptions',
52
+ '.HostConfig.DnsSearch',
53
+ '.HostConfig.NetworkMode',
54
+ '.HostConfig.ReadonlyRootfs',
55
+ '.HostConfig.Tmpfs',
56
+ '.HostConfig.Memory',
57
+ '.HostConfig.MemorySwap',
58
+ '.HostConfig.ShmSize',
59
+ '.HostConfig.NanoCpus',
60
+ '.HostConfig.PidsLimit',
61
+ '.HostConfig.RestartPolicy',
62
+ '.HostConfig.IpcMode',
63
+ '.HostConfig.PidMode',
64
+ '.HostConfig.UTSMode',
65
+ '.HostConfig.UsernsMode',
66
+ '.HostConfig.CgroupnsMode',
67
+ '.NetworkSettings.Ports',
68
+ '.Mounts',
69
+ ];
70
+ const CONTAINER_INSPECT_FORMAT = `[${CONTAINER_INSPECT_FIELDS.map((field) => `{{json ${field}}}`).join(',')}]`;
71
+ const CONTAINED_EXECUTION_CONTAINER_CLEANUP_UNCONFIRMED = 'contained_execution_container_cleanup_unconfirmed';
72
+ export class ContainedDockerBoundaryUnavailableError extends ContainedExecutionFailureError {
73
+ reason;
74
+ receipt = undefined;
75
+ constructor(reason, options) {
76
+ super(reason, { ...options, executionStarted: false });
77
+ this.reason = reason;
78
+ this.name = 'ContainedDockerBoundaryUnavailableError';
79
+ }
80
+ }
81
+ export class ContainedDockerStartAttemptError extends ContainedExecutionFailureError {
82
+ constructor(options) {
83
+ super('contained_execution_start_attempt_failed', { ...options, executionStarted: true });
84
+ this.name = 'ContainedDockerStartAttemptError';
85
+ }
86
+ }
87
+ export class ContainedDockerCleanupUnconfirmedError extends ContainedExecutionFailureError {
88
+ containerName;
89
+ executionStarted;
90
+ cleanupConfirmed = false;
91
+ constructor(containerName, executionStarted, options) {
92
+ super(CONTAINED_EXECUTION_CONTAINER_CLEANUP_UNCONFIRMED, {
93
+ ...options,
94
+ executionStarted,
95
+ });
96
+ this.containerName = containerName;
97
+ this.executionStarted = executionStarted;
98
+ this.name = 'ContainedDockerCleanupUnconfirmedError';
99
+ }
100
+ }
101
+ function overlaps(candidate, root) {
102
+ return pathWithinRoot(root, candidate) || pathWithinRoot(candidate, root);
103
+ }
104
+ async function digestFile(file) {
105
+ const hash = createHash('sha256');
106
+ for await (const chunk of createReadStream(file))
107
+ hash.update(chunk);
108
+ return hash.digest('hex');
109
+ }
110
+ async function resolveConfiguredDockerSubstrate(params) {
111
+ if (!path.isAbsolute(params.executable) || /[\0\n\r]/.test(params.executable)) {
112
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_docker_binary_invalid');
113
+ }
114
+ if (!params.host.startsWith('unix:///') || /[\0\n\r]/.test(params.host)) {
115
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_docker_host_invalid');
116
+ }
117
+ const configuredSocket = params.host.slice('unix://'.length);
118
+ if (!path.isAbsolute(configuredSocket)) {
119
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_docker_host_invalid');
120
+ }
121
+ let binaryPath;
122
+ let socketPath;
123
+ try {
124
+ await access(params.executable, constants.X_OK);
125
+ [binaryPath, socketPath] = await Promise.all([
126
+ realpath(params.executable),
127
+ realpath(configuredSocket),
128
+ ]);
129
+ const [binaryInfo, socketInfo] = await Promise.all([lstat(binaryPath), lstat(socketPath)]);
130
+ if (!binaryInfo.isFile() || binaryInfo.isSymbolicLink() || !socketInfo.isSocket()) {
131
+ throw new Error('invalid Docker substrate type');
132
+ }
133
+ }
134
+ catch (error) {
135
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_docker_substrate_unavailable', { cause: error });
136
+ }
137
+ const excluded = [params.repoRoot, ...params.protectedRoots].map(canonicalPath);
138
+ if (excluded.some((root) => overlaps(binaryPath, root) || overlaps(socketPath, root))) {
139
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_docker_substrate_overlap');
140
+ }
141
+ const endpoint = `unix://${socketPath}`;
142
+ const info = await runProcessWithBoundedOutput(binaryPath, ['--host', endpoint, 'info', '--format', '{{json .ID}}'], { env: { ...MINIMAL_DOCKER_ENV }, argv0: 'docker' }, DOCKER_CONTROL_TIMEOUT_MS);
143
+ if (info.timedOut || info.exitCode !== 0) {
144
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_docker_daemon_unavailable');
145
+ }
146
+ let daemonId;
147
+ try {
148
+ daemonId = JSON.parse(info.stdout.trim());
149
+ }
150
+ catch (error) {
151
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_docker_daemon_invalid', {
152
+ cause: error,
153
+ });
154
+ }
155
+ if (typeof daemonId !== 'string' || !daemonId || /[\0\n\r]/.test(daemonId)) {
156
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_docker_daemon_invalid');
157
+ }
158
+ return { binaryPath, binarySha256: await digestFile(binaryPath), endpoint, daemonId };
159
+ }
160
+ const productionDependencies = {
161
+ resolveSubstrate: resolveConfiguredDockerSubstrate,
162
+ runProcess: (file, args, env, timeoutMs, outputPolicy) => runProcessWithBoundedOutput(file, args, { env, argv0: 'docker' }, timeoutMs, outputPolicy),
163
+ now: () => Date.now(),
164
+ randomUUID,
165
+ uid: () => {
166
+ if (!process.getuid) {
167
+ throw new ContainedExecutionFailureError('contained_execution_host_identity_unavailable');
168
+ }
169
+ return process.getuid();
170
+ },
171
+ gid: () => {
172
+ if (!process.getgid) {
173
+ throw new ContainedExecutionFailureError('contained_execution_host_identity_unavailable');
174
+ }
175
+ return process.getgid();
176
+ },
177
+ };
178
+ function dockerCall(dependencies, substrate, args, timeoutMs, outputPolicy) {
179
+ return dependencies.runProcess(substrate.binaryPath, ['--host', substrate.endpoint, ...args], { ...MINIMAL_DOCKER_ENV }, timeoutMs, outputPolicy);
180
+ }
181
+ export { buildContainedDockerCreateArgs };
182
+ function inspectFailureCode(prefix, suffix) {
183
+ return `${prefix}_${suffix}`;
184
+ }
185
+ function parseFormattedInspect(result, expectedLength, code) {
186
+ if (result.stdoutTruncated) {
187
+ throw new ContainedDockerBoundaryUnavailableError(inspectFailureCode(code, 'truncated'));
188
+ }
189
+ let parsed;
190
+ try {
191
+ parsed = JSON.parse(result.stdout);
192
+ }
193
+ catch {
194
+ throw new ContainedDockerBoundaryUnavailableError(inspectFailureCode(code, 'invalid'));
195
+ }
196
+ if (!Array.isArray(parsed) || parsed.length !== expectedLength) {
197
+ throw new ContainedDockerBoundaryUnavailableError(inspectFailureCode(code, 'invalid'));
198
+ }
199
+ return parsed;
200
+ }
201
+ function parseContainerInspect(result, code) {
202
+ const v = parseFormattedInspect(result, CONTAINER_INSPECT_FIELDS.length, code);
203
+ return {
204
+ Id: v[0],
205
+ Image: v[1],
206
+ Config: {
207
+ User: v[2],
208
+ Env: v[3],
209
+ Entrypoint: v[4],
210
+ Cmd: v[5],
211
+ WorkingDir: v[6],
212
+ Healthcheck: v[7],
213
+ },
214
+ HostConfig: {
215
+ AutoRemove: v[8],
216
+ Privileged: v[9],
217
+ CapAdd: v[10],
218
+ CapDrop: v[11],
219
+ SecurityOpt: v[12],
220
+ LogConfig: v[13],
221
+ Devices: v[14],
222
+ DeviceRequests: v[15],
223
+ DeviceCgroupRules: v[16],
224
+ GroupAdd: v[17],
225
+ VolumesFrom: v[18],
226
+ Binds: v[19],
227
+ PortBindings: v[20],
228
+ PublishAllPorts: v[21],
229
+ Links: v[22],
230
+ ExtraHosts: v[23],
231
+ Dns: v[24],
232
+ DnsOptions: v[25],
233
+ DnsSearch: v[26],
234
+ NetworkMode: v[27],
235
+ ReadonlyRootfs: v[28],
236
+ Tmpfs: v[29],
237
+ Memory: v[30],
238
+ MemorySwap: v[31],
239
+ ShmSize: v[32],
240
+ NanoCpus: v[33],
241
+ PidsLimit: v[34],
242
+ RestartPolicy: v[35],
243
+ IpcMode: v[36],
244
+ PidMode: v[37],
245
+ UTSMode: v[38],
246
+ UsernsMode: v[39],
247
+ CgroupnsMode: v[40],
248
+ },
249
+ NetworkSettings: { Ports: v[41] },
250
+ Mounts: v[42],
251
+ };
252
+ }
253
+ async function resolveLocalImage(reference, dependencies, substrate) {
254
+ if (!reference.trim() || /[\0\n\r]/.test(reference)) {
255
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_image_missing');
256
+ }
257
+ const inspected = await dockerCall(dependencies, substrate, ['image', 'inspect', '--format', IMAGE_INSPECT_FORMAT, reference], DOCKER_CONTROL_TIMEOUT_MS);
258
+ if (inspected.timedOut || inspected.exitCode !== 0) {
259
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_image_missing');
260
+ }
261
+ const [inspectedId, inspectedEnvironment] = parseFormattedInspect(inspected, 2, 'contained_execution_image_inspect');
262
+ if (typeof inspectedId !== 'string' ||
263
+ !IMAGE_ID_PATTERN.test(inspectedId) ||
264
+ (inspectedEnvironment != null &&
265
+ (!Array.isArray(inspectedEnvironment) ||
266
+ !inspectedEnvironment.every((entry) => typeof entry === 'string'))))
267
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_image_inspect_invalid');
268
+ return {
269
+ imageId: inspectedId,
270
+ environment: inspectedEnvironment ?? [],
271
+ };
272
+ }
273
+ function parseEnvironment(entries) {
274
+ const parsed = new Map();
275
+ for (const entry of entries) {
276
+ const separator = entry.indexOf('=');
277
+ const name = entry.slice(0, separator);
278
+ if (separator <= 0 || parsed.has(name))
279
+ return null;
280
+ parsed.set(name, entry.slice(separator + 1));
281
+ }
282
+ return parsed;
283
+ }
284
+ function environmentMatches(actualEntries, imageEntries) {
285
+ const actual = parseEnvironment(actualEntries);
286
+ const expected = new Map();
287
+ for (const entry of imageEntries) {
288
+ const separator = entry.indexOf('=');
289
+ if (separator <= 0)
290
+ return false;
291
+ expected.set(entry.slice(0, separator), entry.slice(separator + 1));
292
+ }
293
+ if (!actual)
294
+ return false;
295
+ for (const name of PROXY_ENV_NAMES)
296
+ expected.set(name, '');
297
+ return (actual.size === expected.size &&
298
+ [...expected].every(([name, value]) => actual.get(name) === value));
299
+ }
300
+ function empty(value) {
301
+ return value == null || value.length === 0;
302
+ }
303
+ function emptyRecord(value) {
304
+ return value == null || Object.keys(value).length === 0;
305
+ }
306
+ function sameSet(actual, expected) {
307
+ return Boolean(actual &&
308
+ actual.length === expected.length &&
309
+ [...actual].sort().join('\0') === [...expected].sort().join('\0'));
310
+ }
311
+ function assertContainedInspect(params) {
312
+ const { inspect: value, resourceLimits: limits } = params;
313
+ const mount = value.Mounts[0];
314
+ const healthcheck = value.Config.Healthcheck?.Test;
315
+ const valid = value.Id === params.containerId &&
316
+ value.Image === params.imageId &&
317
+ value.Config.User === params.user &&
318
+ Array.isArray(value.Config.Entrypoint) &&
319
+ value.Config.Entrypoint.length === 1 &&
320
+ value.Config.Entrypoint[0] === '/bin/sh' &&
321
+ Array.isArray(value.Config.Cmd) &&
322
+ value.Config.Cmd.length === 2 &&
323
+ value.Config.Cmd[0] === '-c' &&
324
+ value.Config.Cmd[1] === params.command &&
325
+ value.Config.WorkingDir === params.guestCwd &&
326
+ Array.isArray(healthcheck) &&
327
+ healthcheck.length === 1 &&
328
+ healthcheck[0] === 'NONE' &&
329
+ environmentMatches(value.Config.Env ?? [], params.imageEnvironment) &&
330
+ value.HostConfig.AutoRemove === false &&
331
+ value.HostConfig.Privileged === false &&
332
+ empty(value.HostConfig.CapAdd) &&
333
+ sameSet(value.HostConfig.CapDrop, ['ALL']) &&
334
+ sameSet(value.HostConfig.SecurityOpt, ['no-new-privileges']) &&
335
+ value.HostConfig.LogConfig != null &&
336
+ exactKeys(value.HostConfig.LogConfig, ['Type', 'Config']) &&
337
+ value.HostConfig.LogConfig.Type === 'none' &&
338
+ value.HostConfig.LogConfig.Config != null &&
339
+ exactKeys(value.HostConfig.LogConfig.Config, []) &&
340
+ empty(value.HostConfig.Devices) &&
341
+ empty(value.HostConfig.DeviceRequests) &&
342
+ empty(value.HostConfig.DeviceCgroupRules) &&
343
+ empty(value.HostConfig.GroupAdd) &&
344
+ empty(value.HostConfig.VolumesFrom) &&
345
+ empty(value.HostConfig.Binds) &&
346
+ emptyRecord(value.HostConfig.PortBindings) &&
347
+ value.HostConfig.PublishAllPorts === false &&
348
+ empty(value.HostConfig.Links) &&
349
+ empty(value.HostConfig.ExtraHosts) &&
350
+ empty(value.HostConfig.Dns) &&
351
+ empty(value.HostConfig.DnsOptions) &&
352
+ empty(value.HostConfig.DnsSearch) &&
353
+ value.HostConfig.NetworkMode === 'none' &&
354
+ value.HostConfig.ReadonlyRootfs === true &&
355
+ value.HostConfig.Tmpfs != null &&
356
+ Object.keys(value.HostConfig.Tmpfs).length === 1 &&
357
+ value.HostConfig.Tmpfs['/tmp'] === TMPFS_OPTIONS &&
358
+ value.HostConfig.Memory === limits.memoryMiB * 1024 * 1024 &&
359
+ value.HostConfig.MemorySwap === limits.memoryMiB * 1024 * 1024 &&
360
+ value.HostConfig.ShmSize === SHM_SIZE_MIB * 1024 * 1024 &&
361
+ value.HostConfig.NanoCpus === Math.round(limits.cpus * 1_000_000_000) &&
362
+ value.HostConfig.PidsLimit === limits.pids &&
363
+ value.HostConfig.RestartPolicy?.Name === 'no' &&
364
+ value.HostConfig.RestartPolicy?.MaximumRetryCount === 0 &&
365
+ value.HostConfig.IpcMode === 'none' &&
366
+ value.HostConfig.PidMode === '' &&
367
+ value.HostConfig.UTSMode === '' &&
368
+ value.HostConfig.UsernsMode === '' &&
369
+ value.HostConfig.CgroupnsMode === 'private' &&
370
+ emptyRecord(value.NetworkSettings.Ports) &&
371
+ value.Mounts.length === 1 &&
372
+ mount?.Type === 'bind' &&
373
+ canonicalPath(mount.Source) === canonicalPath(params.hostMirrorRoot) &&
374
+ mount.Destination === params.guestWorkspacePath &&
375
+ mount.RW === true;
376
+ if (!valid)
377
+ throw new ContainedExecutionFailureError('contained_execution_inspect_mismatch');
378
+ }
379
+ function containerMissing(result) {
380
+ return /no such (?:object|container)/i.test(`${result.stdout}\n${result.stderr}`);
381
+ }
382
+ async function cleanupContainer(identity, executionStarted, dependencies, substrate) {
383
+ try {
384
+ await dockerCall(dependencies, substrate, ['rm', '-f', identity], DOCKER_CONTROL_TIMEOUT_MS);
385
+ const inspected = await dockerCall(dependencies, substrate, ['inspect', '--type', 'container', '--format', CONTAINER_INSPECT_FORMAT, identity], DOCKER_CONTROL_TIMEOUT_MS);
386
+ if (inspected.timedOut || inspected.exitCode === 0 || !containerMissing(inspected)) {
387
+ throw new ContainedDockerCleanupUnconfirmedError(identity, executionStarted);
388
+ }
389
+ }
390
+ catch (error) {
391
+ if (error instanceof ContainedDockerCleanupUnconfirmedError)
392
+ throw error;
393
+ throw new ContainedDockerCleanupUnconfirmedError(identity, executionStarted, { cause: error });
394
+ }
395
+ }
396
+ function limitsFromConfig(config) {
397
+ return {
398
+ timeoutMs: config.timeoutMs,
399
+ memoryMiB: config.memoryMiB,
400
+ cpus: config.cpus,
401
+ pids: config.pids,
402
+ };
403
+ }
404
+ function substrateMatches(left, right) {
405
+ return (left.binaryPath === right.binaryPath &&
406
+ left.binarySha256 === right.binarySha256 &&
407
+ left.endpoint === right.endpoint &&
408
+ left.daemonId === right.daemonId);
409
+ }
410
+ async function substrateFor(params) {
411
+ if (!params.config.dockerExecutable || !params.config.dockerHost) {
412
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_docker_config_missing');
413
+ }
414
+ return params.dependencies.resolveSubstrate({
415
+ executable: params.config.dockerExecutable,
416
+ host: params.config.dockerHost,
417
+ repoRoot: params.repoRoot,
418
+ protectedRoots: params.protectedRoots,
419
+ });
420
+ }
421
+ export async function probeContainedDockerBoundary(params) {
422
+ const dependencies = params.dependencies ?? productionDependencies;
423
+ assertContainedExecutionResourceLimits(params.resourceLimits);
424
+ const substrate = await dependencies.resolveSubstrate({
425
+ executable: params.dockerExecutable,
426
+ host: params.dockerHost,
427
+ repoRoot: params.repoRoot,
428
+ protectedRoots: params.protectedRoots,
429
+ });
430
+ const image = await resolveLocalImage(params.imageReference, dependencies, substrate);
431
+ const containerName = `belay-contained-${dependencies.randomUUID()}`;
432
+ const user = `${dependencies.uid()}:${dependencies.gid()}`;
433
+ const args = buildContainedDockerCreateArgs({
434
+ containerName,
435
+ imageId: image.imageId,
436
+ command: ':',
437
+ hostMirrorRoot: params.hostProbeRoot,
438
+ guestWorkspacePath: params.guestWorkspacePath,
439
+ guestCwd: params.guestWorkspacePath,
440
+ resourceLimits: params.resourceLimits,
441
+ user,
442
+ });
443
+ let operationError;
444
+ let identity = containerName;
445
+ let executionStarted = false;
446
+ try {
447
+ const created = await dockerCall(dependencies, substrate, args, DOCKER_CONTROL_TIMEOUT_MS);
448
+ const captured = created.stdout.trim();
449
+ if (CONTAINER_ID.test(captured))
450
+ identity = captured;
451
+ if (created.timedOut || created.exitCode !== 0 || !CONTAINER_ID.test(captured)) {
452
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_probe_create_failed');
453
+ }
454
+ const inspected = await dockerCall(dependencies, substrate, ['inspect', '--type', 'container', '--format', CONTAINER_INSPECT_FORMAT, identity], DOCKER_CONTROL_TIMEOUT_MS);
455
+ if (inspected.timedOut || inspected.exitCode !== 0) {
456
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_probe_inspect_failed');
457
+ }
458
+ assertContainedInspect({
459
+ inspect: parseContainerInspect(inspected, 'contained_execution_probe_inspect'),
460
+ containerId: identity,
461
+ imageId: image.imageId,
462
+ hostMirrorRoot: params.hostProbeRoot,
463
+ guestWorkspacePath: params.guestWorkspacePath,
464
+ guestCwd: params.guestWorkspacePath,
465
+ command: ':',
466
+ imageEnvironment: image.environment,
467
+ resourceLimits: params.resourceLimits,
468
+ user,
469
+ });
470
+ executionStarted = true;
471
+ let started;
472
+ try {
473
+ started = await dockerCall(dependencies, substrate, ['start', '--attach', identity], params.resourceLimits.timeoutMs);
474
+ }
475
+ catch (error) {
476
+ throw new ContainedDockerStartAttemptError({ cause: error });
477
+ }
478
+ if (started.timedOut || started.exitCode !== 0) {
479
+ throw new ContainedDockerStartAttemptError();
480
+ }
481
+ }
482
+ catch (error) {
483
+ operationError = error;
484
+ }
485
+ await cleanupContainer(identity, executionStarted, dependencies, substrate);
486
+ if (operationError)
487
+ throw operationError;
488
+ const probedAtMs = dependencies.now();
489
+ return {
490
+ version: 1,
491
+ imageId: image.imageId,
492
+ imageReference: params.imageReference,
493
+ networkNone: true,
494
+ isolatesWorkspaceMirror: true,
495
+ readOnlyRoot: true,
496
+ sanitizedEnvironment: true,
497
+ dockerSubstrate: substrate,
498
+ dockerConfiguration: {
499
+ executable: params.dockerExecutable,
500
+ host: params.dockerHost,
501
+ },
502
+ user,
503
+ entrypoint: '/bin/sh',
504
+ capDropAll: true,
505
+ noNewPrivileges: true,
506
+ logDriver: 'none',
507
+ proxyEnvironment: 'neutralized-empty',
508
+ tmpfs: {
509
+ path: '/tmp',
510
+ sizeBytes: TMPFS_SIZE_BYTES,
511
+ mode: 0o1777,
512
+ exec: false,
513
+ nosuid: true,
514
+ nodev: true,
515
+ },
516
+ memorySwapMiB: params.resourceLimits.memoryMiB,
517
+ shmSizeMiB: SHM_SIZE_MIB,
518
+ healthcheckDisabled: true,
519
+ privateNamespaces: true,
520
+ privileged: false,
521
+ devicesNone: true,
522
+ resourceLimits: { ...params.resourceLimits },
523
+ probedAt: new Date(probedAtMs).toISOString(),
524
+ expiresAt: new Date(probedAtMs + CONTAINED_ATTESTATION_TTL_MS).toISOString(),
525
+ };
526
+ }
527
+ async function validatedGuestCwd(params) {
528
+ const requiredExclusions = [
529
+ ...new Set([params.controlPlaneDir, ...params.protectedRoots].map(canonicalPath)),
530
+ ];
531
+ if (params.mirror.backend !== 'file_copy' ||
532
+ params.mirror.guestWorkspacePath !== path.resolve(params.repoRoot) ||
533
+ !path.isAbsolute(params.guestCwd) ||
534
+ !pathWithinRoot(params.mirror.guestWorkspacePath, params.guestCwd))
535
+ throw new ContainedExecutionFailureError('contained_execution_invalid_cwd');
536
+ const resolvedRoot = await realpath(params.mirror.hostMirrorRoot);
537
+ const protectedRoots = [canonicalPath(params.repoRoot), ...requiredExclusions];
538
+ if (protectedRoots.some((root) => overlaps(resolvedRoot, root))) {
539
+ throw new ContainedExecutionFailureError('contained_execution_protected_root_overlap');
540
+ }
541
+ if (!validateContainedExecutionMirrorLease(params.mirror, {
542
+ sourceRoot: params.repoRoot,
543
+ protectedRoots: requiredExclusions,
544
+ }))
545
+ throw new ContainedExecutionFailureError('contained_execution_invalid_mirror_lease');
546
+ const relative = path.relative(params.mirror.guestWorkspacePath, path.resolve(params.guestCwd));
547
+ let resolvedCwd;
548
+ try {
549
+ resolvedCwd = await realpath(path.join(resolvedRoot, relative));
550
+ }
551
+ catch {
552
+ throw new ContainedExecutionFailureError('contained_execution_invalid_cwd');
553
+ }
554
+ const info = await lstat(resolvedCwd);
555
+ if (!info.isDirectory() || !pathWithinRoot(resolvedRoot, resolvedCwd)) {
556
+ throw new ContainedExecutionFailureError('contained_execution_invalid_cwd');
557
+ }
558
+ return path.join(params.mirror.guestWorkspacePath, path.relative(resolvedRoot, resolvedCwd));
559
+ }
560
+ function exactKeys(value, allowed) {
561
+ const names = new Set(allowed);
562
+ return Object.keys(value).every((name) => names.has(name));
563
+ }
564
+ function limitsEqual(left, right) {
565
+ return (left.timeoutMs === right.timeoutMs &&
566
+ left.memoryMiB === right.memoryMiB &&
567
+ left.cpus === right.cpus &&
568
+ left.pids === right.pids);
569
+ }
570
+ export async function executeContainedDocker(params) {
571
+ if (!exactKeys(params, [
572
+ 'repoRoot',
573
+ 'controlPlaneDir',
574
+ 'protectedRoots',
575
+ 'config',
576
+ 'mirror',
577
+ 'guestCwd',
578
+ 'command',
579
+ 'inputFingerprint',
580
+ 'signedAttestation',
581
+ 'dependencies',
582
+ ]) ||
583
+ !exactKeys(params.config, [
584
+ 'enabled',
585
+ 'image',
586
+ 'dockerExecutable',
587
+ 'dockerHost',
588
+ 'timeoutMs',
589
+ 'memoryMiB',
590
+ 'cpus',
591
+ 'pids',
592
+ ]) ||
593
+ !exactKeys(params.mirror, ['hostMirrorRoot', 'guestWorkspacePath', 'backend', 'cleanup'])) {
594
+ throw new ContainedExecutionFailureError('contained_execution_unknown_option');
595
+ }
596
+ if (!params.config.enabled || !params.config.image)
597
+ throw new ContainedExecutionFailureError('contained_execution_disabled');
598
+ if (!/^[a-f0-9]{64}$/.test(params.inputFingerprint))
599
+ throw new ContainedExecutionFailureError('contained_execution_input_fingerprint_invalid');
600
+ const dependencies = params.dependencies ?? productionDependencies;
601
+ const limits = limitsFromConfig(params.config);
602
+ assertContainedExecutionResourceLimits(limits);
603
+ const guestCwd = await validatedGuestCwd(params);
604
+ const verified = await verifySignedBoundaryAttestation({
605
+ file: params.signedAttestation,
606
+ expectedRepoRoot: params.repoRoot,
607
+ controlPlaneDir: params.controlPlaneDir,
608
+ });
609
+ const current = dependencies.now();
610
+ const capability = verified?.driver === 'container' ? verified.containedExecution : undefined;
611
+ if (!capability ||
612
+ !isContainedExecutionAttestationFresh(capability, current) ||
613
+ !verified ||
614
+ Date.parse(verified.probedAt) > current ||
615
+ Date.parse(verified.expiresAt) <= current)
616
+ throw new ContainedExecutionFailureError('contained_execution_capability_invalid');
617
+ if (!limitsEqual(capability.resourceLimits, limits) ||
618
+ capability.memorySwapMiB !== limits.memoryMiB ||
619
+ capability.shmSizeMiB !== SHM_SIZE_MIB ||
620
+ capability.healthcheckDisabled !== true ||
621
+ capability.privateNamespaces !== true ||
622
+ capability.privileged !== false ||
623
+ capability.devicesNone !== true ||
624
+ capability.tmpfs.sizeBytes !== TMPFS_SIZE_BYTES ||
625
+ capability.tmpfs.exec !== false ||
626
+ capability.proxyEnvironment !== 'neutralized-empty' ||
627
+ capability.entrypoint !== '/bin/sh' ||
628
+ capability.capDropAll !== true ||
629
+ capability.noNewPrivileges !== true ||
630
+ capability.logDriver !== 'none' ||
631
+ capability.imageReference !== params.config.image ||
632
+ capability.dockerConfiguration.executable !== params.config.dockerExecutable ||
633
+ capability.dockerConfiguration.host !== params.config.dockerHost)
634
+ throw new ContainedExecutionFailureError('contained_execution_capability_mismatch');
635
+ const substrate = await substrateFor({
636
+ config: params.config,
637
+ repoRoot: params.repoRoot,
638
+ protectedRoots: params.protectedRoots,
639
+ dependencies,
640
+ });
641
+ if (!substrateMatches(substrate, capability.dockerSubstrate)) {
642
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_docker_substrate_mismatch');
643
+ }
644
+ const image = await resolveLocalImage(params.config.image, dependencies, substrate);
645
+ if (image.imageId !== capability.imageId) {
646
+ throw new ContainedExecutionFailureError('contained_execution_image_mismatch');
647
+ }
648
+ const user = `${dependencies.uid()}:${dependencies.gid()}`;
649
+ if (user !== capability.user) {
650
+ throw new ContainedExecutionFailureError('contained_execution_capability_mismatch');
651
+ }
652
+ const containerName = `belay-contained-${dependencies.randomUUID()}`;
653
+ const createArgs = buildContainedDockerCreateArgs({
654
+ containerName,
655
+ imageId: image.imageId,
656
+ command: params.command,
657
+ hostMirrorRoot: params.mirror.hostMirrorRoot,
658
+ guestWorkspacePath: params.mirror.guestWorkspacePath,
659
+ guestCwd,
660
+ resourceLimits: limits,
661
+ user,
662
+ });
663
+ let identity = containerName;
664
+ let executionStarted = false;
665
+ let operationError;
666
+ let startResult;
667
+ try {
668
+ const created = await dockerCall(dependencies, substrate, createArgs, DOCKER_CONTROL_TIMEOUT_MS);
669
+ const captured = created.stdout.trim();
670
+ if (CONTAINER_ID.test(captured))
671
+ identity = captured;
672
+ if (created.timedOut || created.exitCode !== 0 || !CONTAINER_ID.test(captured)) {
673
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_create_failed');
674
+ }
675
+ const inspected = await dockerCall(dependencies, substrate, ['inspect', '--type', 'container', '--format', CONTAINER_INSPECT_FORMAT, identity], DOCKER_CONTROL_TIMEOUT_MS);
676
+ if (inspected.timedOut || inspected.exitCode !== 0) {
677
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_inspect_failed');
678
+ }
679
+ try {
680
+ assertContainedInspect({
681
+ inspect: parseContainerInspect(inspected, 'contained_execution_inspect'),
682
+ containerId: identity,
683
+ imageId: image.imageId,
684
+ hostMirrorRoot: params.mirror.hostMirrorRoot,
685
+ guestWorkspacePath: params.mirror.guestWorkspacePath,
686
+ guestCwd,
687
+ command: params.command,
688
+ imageEnvironment: image.environment,
689
+ resourceLimits: limits,
690
+ user,
691
+ });
692
+ }
693
+ catch (error) {
694
+ if (error instanceof ContainedDockerBoundaryUnavailableError)
695
+ throw error;
696
+ throw new ContainedDockerBoundaryUnavailableError('contained_execution_inspect_mismatch', {
697
+ cause: error,
698
+ });
699
+ }
700
+ executionStarted = true;
701
+ try {
702
+ startResult = await dockerCall(dependencies, substrate, ['start', '--attach', identity], limits.timeoutMs, { scrubOptions: CONTAINED_EXECUTION_OUTPUT_SCRUB_OPTIONS });
703
+ }
704
+ catch (error) {
705
+ throw new ContainedDockerStartAttemptError({ cause: error });
706
+ }
707
+ }
708
+ catch (error) {
709
+ operationError = error;
710
+ }
711
+ await cleanupContainer(identity, executionStarted, dependencies, substrate);
712
+ if (operationError)
713
+ throw operationError;
714
+ if (!startResult)
715
+ throw new ContainedDockerStartAttemptError();
716
+ const receipt = {
717
+ version: 1,
718
+ inputFingerprint: params.inputFingerprint,
719
+ actionFingerprint: hashValue(canonicalStringify({
720
+ version: 1,
721
+ inputFingerprint: params.inputFingerprint,
722
+ command: params.command,
723
+ guestCwd,
724
+ })),
725
+ attestationDigest: hashValue(canonicalStringify(params.signedAttestation)),
726
+ executionStarted: true,
727
+ timeoutMs: limits.timeoutMs,
728
+ dockerSubstrate: substrate,
729
+ imageId: image.imageId,
730
+ mirror: {
731
+ backend: params.mirror.backend,
732
+ type: 'bind',
733
+ cardinality: 1,
734
+ readWrite: true,
735
+ sourceFingerprint: hashValue(canonicalPath(params.mirror.hostMirrorRoot)),
736
+ guestWorkspacePathFingerprint: hashValue(canonicalPath(params.mirror.guestWorkspacePath)),
737
+ guestCwdFingerprint: hashValue(canonicalPath(guestCwd)),
738
+ },
739
+ logging: { driver: 'none', config: {} },
740
+ network: {
741
+ mode: 'none',
742
+ publishAllPorts: false,
743
+ portBindings: {},
744
+ links: [],
745
+ extraHosts: [],
746
+ dns: [],
747
+ dnsOptions: [],
748
+ dnsSearch: [],
749
+ },
750
+ readOnlyRoot: true,
751
+ tmpfs: {
752
+ path: '/tmp',
753
+ sizeBytes: TMPFS_SIZE_BYTES,
754
+ mode: 0o1777,
755
+ exec: false,
756
+ nosuid: true,
757
+ nodev: true,
758
+ },
759
+ environment: {
760
+ hostForwarded: false,
761
+ proxyEnvironment: 'neutralized-empty',
762
+ neutralizedVariables: [...PROXY_ENV_NAMES],
763
+ imageEnvironmentDigest: hashValue(canonicalStringify(image.environment)),
764
+ },
765
+ privilege: {
766
+ privileged: false,
767
+ noNewPrivileges: true,
768
+ capAdd: [],
769
+ capDrop: ['ALL'],
770
+ groupAdd: [],
771
+ },
772
+ devices: { devices: [], deviceRequests: [], deviceCgroupRules: [] },
773
+ storage: { volumesFrom: [], bindsOutsideMirror: [] },
774
+ namespaces: { ipc: 'none', pid: 'private', uts: 'private', cgroup: 'private', user: 'default' },
775
+ healthcheckDisabled: true,
776
+ lifecycle: { autoRemove: false, restart: 'no', maximumRetryCount: 0 },
777
+ resources: {
778
+ memoryMiB: limits.memoryMiB,
779
+ memorySwapMiB: limits.memoryMiB,
780
+ shmSizeMiB: SHM_SIZE_MIB,
781
+ cpus: limits.cpus,
782
+ nanoCpus: Math.round(limits.cpus * 1_000_000_000),
783
+ pids: limits.pids,
784
+ },
785
+ user,
786
+ entrypoint: '/bin/sh',
787
+ exitCode: startResult.exitCode,
788
+ timedOut: startResult.timedOut,
789
+ };
790
+ return {
791
+ ...startResult,
792
+ executionStarted: true,
793
+ receipt,
794
+ receiptHash: hashValue(canonicalStringify(receipt)),
795
+ };
796
+ }
797
+ export async function probeContainedDockerForSession(params) {
798
+ if (!params.config.enabled ||
799
+ !params.config.image ||
800
+ !params.config.dockerExecutable ||
801
+ !params.config.dockerHost)
802
+ throw new ContainedExecutionFailureError('contained_execution_disabled');
803
+ const probeRoot = await mkdtemp(path.join(os.tmpdir(), 'belay-contained-probe-'));
804
+ let probeError;
805
+ let capability;
806
+ try {
807
+ capability = await probeContainedDockerBoundary({
808
+ repoRoot: params.repoRoot,
809
+ protectedRoots: [params.controlPlaneDir],
810
+ imageReference: params.config.image,
811
+ dockerExecutable: params.config.dockerExecutable,
812
+ dockerHost: params.config.dockerHost,
813
+ hostProbeRoot: probeRoot,
814
+ guestWorkspacePath: path.resolve(params.repoRoot),
815
+ resourceLimits: limitsFromConfig(params.config),
816
+ dependencies: params.dependencies,
817
+ });
818
+ }
819
+ catch (error) {
820
+ probeError = error;
821
+ }
822
+ try {
823
+ await rm(probeRoot, { recursive: true, force: true });
824
+ await lstat(probeRoot);
825
+ throw new ContainedExecutionFailureError('contained_execution_probe_root_cleanup_unconfirmed');
826
+ }
827
+ catch (error) {
828
+ if (error.code !== 'ENOENT')
829
+ throw error;
830
+ }
831
+ if (probeError)
832
+ throw probeError;
833
+ if (!capability)
834
+ throw new ContainedExecutionFailureError('contained_execution_probe_failed');
835
+ return {
836
+ version: 1,
837
+ driver: 'container',
838
+ probedAt: capability.probedAt,
839
+ expiresAt: capability.expiresAt,
840
+ deniesUngrantedEffects: false,
841
+ materializesGrants: false,
842
+ isolatesWorkspaceMounts: false,
843
+ probeSignals: ['docker', 'contained-execution', 'network-none', 'immutable-image'],
844
+ containedExecution: capability,
845
+ };
846
+ }