@guilz-dev/belay 0.8.1 → 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 (104) 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 +73 -6
  9. package/dist/commands/explain.js +3 -2
  10. package/dist/commands/metrics.js +42 -0
  11. package/dist/commands/recovery-checkpoints.d.ts +8 -1
  12. package/dist/commands/recovery-checkpoints.js +44 -11
  13. package/dist/commands/session.d.ts +5 -0
  14. package/dist/commands/session.js +8 -1
  15. package/dist/config-io.js +2 -2
  16. package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
  17. package/dist/conformance/contained-execution-guarantee.js +121 -0
  18. package/dist/conformance/guarantee-table.js +7 -0
  19. package/dist/core/audit-io.js +8 -0
  20. package/dist/core/audit-metrics.d.ts +11 -0
  21. package/dist/core/audit-metrics.js +14 -0
  22. package/dist/core/audit-recovery-metrics.d.ts +33 -0
  23. package/dist/core/audit-recovery-metrics.js +146 -0
  24. package/dist/core/audit-types.d.ts +7 -1
  25. package/dist/core/audit-types.js +1 -1
  26. package/dist/core/bounded-output.d.ts +11 -0
  27. package/dist/core/bounded-output.js +26 -0
  28. package/dist/core/capability/attestation.d.ts +56 -0
  29. package/dist/core/capability/attestation.js +110 -3
  30. package/dist/core/capability/boundary-session.d.ts +5 -0
  31. package/dist/core/capability/boundary-session.js +38 -1
  32. package/dist/core/capability/index.d.ts +1 -1
  33. package/dist/core/capability/index.js +1 -1
  34. package/dist/core/config.d.ts +18 -0
  35. package/dist/core/config.js +89 -10
  36. package/dist/core/contained-execution/docker-policy.d.ts +18 -0
  37. package/dist/core/contained-execution/docker-policy.js +97 -0
  38. package/dist/core/contained-execution/docker.d.ts +223 -0
  39. package/dist/core/contained-execution/docker.js +846 -0
  40. package/dist/core/contained-execution/eligibility.d.ts +9 -0
  41. package/dist/core/contained-execution/eligibility.js +103 -0
  42. package/dist/core/contained-execution/failure.d.ts +18 -0
  43. package/dist/core/contained-execution/failure.js +105 -0
  44. package/dist/core/contained-execution/mirror.d.ts +54 -0
  45. package/dist/core/contained-execution/mirror.js +587 -0
  46. package/dist/core/contained-execution/policy.d.ts +10 -0
  47. package/dist/core/contained-execution/policy.js +20 -0
  48. package/dist/core/effect-ir/index.d.ts +1 -1
  49. package/dist/core/effect-ir/index.js +1 -1
  50. package/dist/core/effect-ir/normalize.d.ts +5 -0
  51. package/dist/core/effect-ir/normalize.js +17 -7
  52. package/dist/core/effect-ir/shell-build.js +2 -14
  53. package/dist/core/effect-ir/shell-lower.js +100 -25
  54. package/dist/core/gate-contract.d.ts +4 -1
  55. package/dist/core/gate-contract.js +2 -0
  56. package/dist/core/process-runner.d.ts +10 -3
  57. package/dist/core/process-runner.js +66 -10
  58. package/dist/core/recovery/checkpoint.d.ts +3 -0
  59. package/dist/core/recovery/checkpoint.js +33 -4
  60. package/dist/core/recovery/types.d.ts +1 -0
  61. package/dist/core/scrub.d.ts +17 -0
  62. package/dist/core/scrub.js +499 -5
  63. package/dist/core/standing-allow.d.ts +0 -20
  64. package/dist/core/standing-allow.js +0 -71
  65. package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
  66. package/dist/core/transactional/apply-observed-changes.js +11 -0
  67. package/dist/core/transactional/backend-selector.d.ts +2 -1
  68. package/dist/core/transactional/backend-selector.js +32 -29
  69. package/dist/core/transactional/backend.d.ts +3 -0
  70. package/dist/core/transactional/file-checkpoint-backend.js +242 -54
  71. package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
  72. package/dist/core/transactional/file-checkpoint-git.js +53 -0
  73. package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
  74. package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
  75. package/dist/core/transactional/git-worktree-backend.js +3 -0
  76. package/dist/core/transactional/git-worktree.d.ts +1 -0
  77. package/dist/core/transactional/git-worktree.js +1 -0
  78. package/dist/core/transactional/runner.js +59 -13
  79. package/dist/core/types.d.ts +13 -0
  80. package/dist/core/verdict/parser.d.ts +10 -0
  81. package/dist/core/verdict/parser.js +271 -40
  82. package/dist/core/verdict/shell-semantics.js +21 -0
  83. package/dist/corpus/adversarial-probe.d.ts +1 -1
  84. package/dist/corpus/adversarial-probe.js +3 -3
  85. package/dist/corpus/benign-probe-cores.d.ts +6 -0
  86. package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
  87. package/dist/corpus/coverage-compare.d.ts +36 -0
  88. package/dist/corpus/coverage-compare.js +197 -0
  89. package/dist/corpus/coverage-contexts.d.ts +17 -0
  90. package/dist/corpus/coverage-contexts.js +88 -0
  91. package/dist/corpus/coverage-matrix.d.ts +37 -0
  92. package/dist/corpus/coverage-matrix.js +159 -0
  93. package/dist/corpus/coverage-probe.d.ts +91 -0
  94. package/dist/corpus/coverage-probe.js +341 -0
  95. package/dist/corpus/evaluate.d.ts +1 -1
  96. package/dist/corpus/evaluate.js +1 -1
  97. package/dist/corpus/structural-fixture-root.d.ts +2 -0
  98. package/dist/corpus/structural-fixture-root.js +5 -0
  99. package/dist/version.d.ts +1 -1
  100. package/dist/version.js +1 -1
  101. package/package.json +3 -1
  102. package/skills/belay/SKILL.md +19 -0
  103. package/skills/belay/belay-status.md +5 -0
  104. package/dist/corpus/must-allow-commands.d.ts +0 -6
@@ -1,4 +1,6 @@
1
+ import path from 'node:path';
1
2
  export const BOUNDARY_ATTESTATION_VERSION = 1;
3
+ export const CONTAINED_EXECUTION_ATTESTATION_VERSION = 1;
2
4
  const KNOWN_DRIVERS = new Set([
3
5
  'container',
4
6
  'cursor-sandbox',
@@ -6,15 +8,21 @@ const KNOWN_DRIVERS = new Set([
6
8
  'landlock',
7
9
  'host-integration',
8
10
  ]);
11
+ function isRecord(value) {
12
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
13
+ }
14
+ function isBoundaryDriver(value) {
15
+ return typeof value === 'string' && KNOWN_DRIVERS.has(value);
16
+ }
9
17
  export function validateBoundaryAttestation(value) {
10
- if (!value || typeof value !== 'object') {
18
+ if (!isRecord(value)) {
11
19
  return false;
12
20
  }
13
21
  const record = value;
14
22
  if (record.version !== BOUNDARY_ATTESTATION_VERSION) {
15
23
  return false;
16
24
  }
17
- if (!KNOWN_DRIVERS.has(record.driver)) {
25
+ if (!isBoundaryDriver(record.driver)) {
18
26
  return false;
19
27
  }
20
28
  if (typeof record.probedAt !== 'string' || typeof record.expiresAt !== 'string') {
@@ -34,6 +42,12 @@ export function validateBoundaryAttestation(value) {
34
42
  typeof record.isolatesWorkspaceMounts !== 'boolean') {
35
43
  return false;
36
44
  }
45
+ if (record.containedExecution !== undefined) {
46
+ if (record.driver !== 'container' ||
47
+ !validateContainedExecutionAttestation(record.containedExecution)) {
48
+ return false;
49
+ }
50
+ }
37
51
  if (record.driver === 'host-integration' && record.materializesGrants) {
38
52
  return false;
39
53
  }
@@ -44,7 +58,94 @@ export function validateBoundaryAttestation(value) {
44
58
  }
45
59
  return true;
46
60
  }
47
- export function isAttestationFresh(attestation, now = Date.now()) {
61
+ function hasPositiveLimit(value) {
62
+ return typeof value === 'number' && Number.isFinite(value) && value > 0;
63
+ }
64
+ function hasPositiveIntegerLimit(value) {
65
+ return hasPositiveLimit(value) && Number.isSafeInteger(value);
66
+ }
67
+ export function validateContainedExecutionAttestation(value) {
68
+ if (!isRecord(value)) {
69
+ return false;
70
+ }
71
+ const record = value;
72
+ const dockerSubstrate = record.dockerSubstrate;
73
+ const dockerConfiguration = record.dockerConfiguration;
74
+ if (record.version !== CONTAINED_EXECUTION_ATTESTATION_VERSION ||
75
+ typeof record.imageId !== 'string' ||
76
+ !/^sha256:[a-f0-9]{64}$/i.test(record.imageId) ||
77
+ typeof record.imageReference !== 'string' ||
78
+ !record.imageReference ||
79
+ /[\0\n\r]/.test(record.imageReference) ||
80
+ record.networkNone !== true ||
81
+ record.isolatesWorkspaceMirror !== true ||
82
+ record.readOnlyRoot !== true ||
83
+ record.sanitizedEnvironment !== true ||
84
+ !isRecord(dockerSubstrate) ||
85
+ typeof dockerSubstrate.binaryPath !== 'string' ||
86
+ !path.isAbsolute(dockerSubstrate.binaryPath) ||
87
+ /[\0\n\r]/.test(dockerSubstrate.binaryPath) ||
88
+ typeof dockerSubstrate.binarySha256 !== 'string' ||
89
+ !/^[a-f0-9]{64}$/.test(dockerSubstrate.binarySha256) ||
90
+ typeof dockerSubstrate.endpoint !== 'string' ||
91
+ !dockerSubstrate.endpoint.startsWith('unix:///') ||
92
+ !path.isAbsolute(dockerSubstrate.endpoint.slice('unix://'.length)) ||
93
+ /[\0\n\r]/.test(dockerSubstrate.endpoint) ||
94
+ typeof dockerSubstrate.daemonId !== 'string' ||
95
+ !dockerSubstrate.daemonId ||
96
+ /[\0\n\r]/.test(dockerSubstrate.daemonId) ||
97
+ !isRecord(dockerConfiguration) ||
98
+ typeof dockerConfiguration.executable !== 'string' ||
99
+ !path.isAbsolute(dockerConfiguration.executable) ||
100
+ /[\0\n\r]/.test(dockerConfiguration.executable) ||
101
+ typeof dockerConfiguration.host !== 'string' ||
102
+ !dockerConfiguration.host.startsWith('unix:///') ||
103
+ !path.isAbsolute(dockerConfiguration.host.slice('unix://'.length)) ||
104
+ /[\0\n\r]/.test(dockerConfiguration.host) ||
105
+ typeof record.user !== 'string' ||
106
+ !/^\d+:\d+$/.test(record.user) ||
107
+ record.entrypoint !== '/bin/sh' ||
108
+ record.capDropAll !== true ||
109
+ record.noNewPrivileges !== true ||
110
+ record.logDriver !== 'none' ||
111
+ record.proxyEnvironment !== 'neutralized-empty' ||
112
+ typeof record.probedAt !== 'string' ||
113
+ typeof record.expiresAt !== 'string') {
114
+ return false;
115
+ }
116
+ const probedAt = Date.parse(record.probedAt);
117
+ const expiresAt = Date.parse(record.expiresAt);
118
+ if (!Number.isFinite(probedAt) || !Number.isFinite(expiresAt) || expiresAt <= probedAt) {
119
+ return false;
120
+ }
121
+ const limits = record.resourceLimits;
122
+ const tmpfs = record.tmpfs;
123
+ return (isRecord(tmpfs) &&
124
+ tmpfs.path === '/tmp' &&
125
+ hasPositiveIntegerLimit(tmpfs.sizeBytes) &&
126
+ tmpfs.mode === 0o1777 &&
127
+ tmpfs.exec === false &&
128
+ tmpfs.nosuid === true &&
129
+ tmpfs.nodev === true &&
130
+ hasPositiveIntegerLimit(record.memorySwapMiB) &&
131
+ hasPositiveIntegerLimit(record.shmSizeMiB) &&
132
+ record.healthcheckDisabled === true &&
133
+ record.privateNamespaces === true &&
134
+ record.privileged === false &&
135
+ record.devicesNone === true &&
136
+ isRecord(limits) &&
137
+ hasPositiveIntegerLimit(limits.timeoutMs) &&
138
+ hasPositiveIntegerLimit(limits.memoryMiB) &&
139
+ hasPositiveLimit(limits.cpus) &&
140
+ hasPositiveIntegerLimit(limits.pids));
141
+ }
142
+ export function isContainedExecutionAttestationFresh(attestation, now = Date.now()) {
143
+ // A contained proof must have been observed already; no clock skew is accepted.
144
+ return (validateContainedExecutionAttestation(attestation) &&
145
+ Date.parse(attestation.probedAt) <= now &&
146
+ Date.parse(attestation.expiresAt) > now);
147
+ }
148
+ export function isAttestationCurrent(attestation, now = Date.now()) {
48
149
  const expires = Date.parse(attestation.expiresAt);
49
150
  if (!Number.isFinite(expires)) {
50
151
  return false;
@@ -52,6 +153,12 @@ export function isAttestationFresh(attestation, now = Date.now()) {
52
153
  if (expires <= now) {
53
154
  return false;
54
155
  }
156
+ return true;
157
+ }
158
+ export function isAttestationFresh(attestation, now = Date.now()) {
159
+ if (!isAttestationCurrent(attestation, now)) {
160
+ return false;
161
+ }
55
162
  if (attestation.driver === 'host-integration') {
56
163
  return true;
57
164
  }
@@ -1,4 +1,5 @@
1
1
  import type { BelayConfigV4 } from '../config.js';
2
+ import { type ContainedDockerDependencies } from '../contained-execution/docker.js';
2
3
  import type { BoundaryAttestation, BoundaryDriverId } from './attestation.js';
3
4
  import { type BoundaryDriver } from './boundary-driver.js';
4
5
  import { dockerNetworkArgs } from './boundary-egress.js';
@@ -15,6 +16,7 @@ export interface ResolvedBoundaryDriverContext {
15
16
  attestationPath: string;
16
17
  attestation: BoundaryAttestation | null;
17
18
  attestationFresh: boolean;
19
+ containedExecutionFresh?: boolean;
18
20
  }
19
21
  export declare function boundaryAttestationPath(repoRoot: string, config: BelayConfigV4): string;
20
22
  export declare function loadBoundaryAttestation(filePath: string, expectedRepoRoot?: string, controlPlaneDir?: string): Promise<BoundaryAttestation | null>;
@@ -38,6 +40,7 @@ export declare function startBoundarySession(params: {
38
40
  config: BelayConfigV4;
39
41
  driverId?: BoundaryDriverId;
40
42
  egressProxyRunning?: boolean;
43
+ containedDockerDependencies?: ContainedDockerDependencies;
41
44
  }): Promise<{
42
45
  attestation: BoundaryAttestation;
43
46
  attestationPath: string;
@@ -45,10 +48,12 @@ export declare function startBoundarySession(params: {
45
48
  export declare function boundarySessionStatus(params: {
46
49
  repoRoot: string;
47
50
  config: BelayConfigV4;
51
+ now?: number;
48
52
  }): Promise<{
49
53
  attestationPath: string;
50
54
  attestation: BoundaryAttestation | null;
51
55
  fresh: boolean;
56
+ containedExecutionFresh?: boolean;
52
57
  }>;
53
58
  export declare function runBoundaryAgentCommand(params: {
54
59
  repoRoot: string;
@@ -2,11 +2,34 @@ import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { egressStatus } from '../../services/egress-service.js';
4
4
  import { configuredControlPlaneDir } from '../config.js';
5
- import { isAttestationFresh } from './attestation.js';
5
+ import { probeContainedDockerForSession, } from '../contained-execution/docker.js';
6
+ import { isAttestationFresh, isContainedExecutionAttestationFresh } from './attestation.js';
6
7
  import { readSignedAttestationFile, signBoundaryAttestation, verifySignedBoundaryAttestation, } from './boundary-attestation-sign.js';
7
8
  import { getDefaultBoundaryDriver } from './boundary-driver.js';
8
9
  import { dockerNetworkArgs, isEgressProxyActive, resolveBoundaryEgressProxyEnv, } from './boundary-egress.js';
9
10
  import { runWithBoundaryRunnable, } from './boundary-run.js';
11
+ function containedExecutionFresh(attestation, config, now = Date.now()) {
12
+ if (attestation?.driver !== 'container' || !attestation.containedExecution)
13
+ return undefined;
14
+ const current = config.sandbox.containedExecution;
15
+ const capability = attestation.containedExecution;
16
+ const configCompatible = Boolean(current?.enabled &&
17
+ current.image === capability.imageReference &&
18
+ current.dockerExecutable === capability.dockerConfiguration.executable &&
19
+ current.dockerHost === capability.dockerConfiguration.host &&
20
+ current.timeoutMs === capability.resourceLimits.timeoutMs &&
21
+ current.memoryMiB === capability.resourceLimits.memoryMiB &&
22
+ current.cpus === capability.resourceLimits.cpus &&
23
+ current.pids === capability.resourceLimits.pids);
24
+ const probedAt = Date.parse(attestation.probedAt);
25
+ const expiresAt = Date.parse(attestation.expiresAt);
26
+ return (Number.isFinite(probedAt) &&
27
+ Number.isFinite(expiresAt) &&
28
+ probedAt <= now &&
29
+ expiresAt > now &&
30
+ configCompatible &&
31
+ isContainedExecutionAttestationFresh(capability, now));
32
+ }
10
33
  export function boundaryAttestationPath(repoRoot, config) {
11
34
  const rel = config.capability?.attestationRelPath ?? '.belay/attestation.json';
12
35
  return path.isAbsolute(rel) ? rel : path.join(repoRoot, rel);
@@ -92,6 +115,7 @@ export async function resolveBoundaryDriverContext(params) {
92
115
  attestationPath,
93
116
  attestation,
94
117
  attestationFresh: attestation ? isAttestationFresh(attestation) : false,
118
+ containedExecutionFresh: containedExecutionFresh(attestation, params.config),
95
119
  };
96
120
  }
97
121
  export async function runWithBoundaryDriver(params) {
@@ -104,6 +128,18 @@ export async function runWithBoundaryDriver(params) {
104
128
  });
105
129
  }
106
130
  export async function startBoundarySession(params) {
131
+ const contained = params.config.sandbox.containedExecution;
132
+ if (contained?.enabled === true) {
133
+ const attestation = await probeContainedDockerForSession({
134
+ repoRoot: params.repoRoot,
135
+ controlPlaneDir: configuredControlPlaneDir(params.config),
136
+ config: contained,
137
+ dependencies: params.containedDockerDependencies,
138
+ });
139
+ const attestationPath = boundaryAttestationPath(params.repoRoot, params.config);
140
+ await saveBoundaryAttestation(attestationPath, attestation, params.repoRoot, configuredControlPlaneDir(params.config));
141
+ return { attestation, attestationPath };
142
+ }
107
143
  const resolved = await resolveBoundaryDriverContext(params);
108
144
  const attestation = await resolved.driver.probe();
109
145
  if (resolved.driver.prepare) {
@@ -120,6 +156,7 @@ export async function boundarySessionStatus(params) {
120
156
  attestationPath,
121
157
  attestation,
122
158
  fresh: attestation ? isAttestationFresh(attestation) : false,
159
+ containedExecutionFresh: containedExecutionFresh(attestation, params.config, params.now),
123
160
  };
124
161
  }
125
162
  export async function runBoundaryAgentCommand(params) {
@@ -1,5 +1,5 @@
1
1
  export { addPathToAllowlist, allPathsAllowlisted, fsScopeAllowlistPath, isPathAllowlisted, loadFsScopeAllowlist, loadFsScopeAllowlistSync, saveFsScopeAllowlist, } from './allowlist.js';
2
- export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, type BoundaryAttestation, type BoundaryDriverId, isAttestationFresh, } from './attestation.js';
2
+ export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, type BoundaryAttestation, type BoundaryDriverId, CONTAINED_EXECUTION_ATTESTATION_VERSION, type ContainedExecutionAttestation, type ContainedExecutionResourceLimits, isAttestationFresh, isContainedExecutionAttestationFresh, validateContainedExecutionAttestation, } from './attestation.js';
3
3
  export { BOUNDARY_PROFILE_L1_ATTESTED, BOUNDARY_PROFILE_L3_L4_ONLY, BOUNDARY_PROFILE_L3_POLICY, boundaryVerifiedAllowEnabled, isAttestedBoundary, resolveBoundaryProfile, } from './boundary-profile.js';
4
4
  export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
5
5
  export { CAPABILITY_GRANT_VERSION, type CapabilityGrantV1, isGrantScopeTooBroad, } from './grant.js';
@@ -1,5 +1,5 @@
1
1
  export { addPathToAllowlist, allPathsAllowlisted, fsScopeAllowlistPath, isPathAllowlisted, loadFsScopeAllowlist, loadFsScopeAllowlistSync, saveFsScopeAllowlist, } from './allowlist.js';
2
- export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, isAttestationFresh, } from './attestation.js';
2
+ export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, CONTAINED_EXECUTION_ATTESTATION_VERSION, isAttestationFresh, isContainedExecutionAttestationFresh, validateContainedExecutionAttestation, } from './attestation.js';
3
3
  export { BOUNDARY_PROFILE_L1_ATTESTED, BOUNDARY_PROFILE_L3_L4_ONLY, BOUNDARY_PROFILE_L3_POLICY, boundaryVerifiedAllowEnabled, isAttestedBoundary, resolveBoundaryProfile, } from './boundary-profile.js';
4
4
  export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
5
5
  export { CAPABILITY_GRANT_VERSION, isGrantScopeTooBroad, } from './grant.js';
@@ -146,6 +146,20 @@ export interface BelaySandboxConfig {
146
146
  enabled: boolean;
147
147
  runtime: SandboxRuntime;
148
148
  denyNetworkByDefault: boolean;
149
+ /** Optional in source config for backwards compatibility; normalization always supplies it. */
150
+ containedExecution?: BelayContainedExecutionConfig;
151
+ }
152
+ export interface BelayContainedExecutionConfig {
153
+ enabled: boolean;
154
+ image: string | null;
155
+ /** Required when enabled; optional in source objects for disabled backwards compatibility. */
156
+ dockerExecutable?: string | null;
157
+ /** Required when enabled; only local absolute unix:// endpoints are accepted. */
158
+ dockerHost?: string | null;
159
+ timeoutMs: number;
160
+ memoryMiB: number;
161
+ cpus: number;
162
+ pids: number;
149
163
  }
150
164
  export interface BelayClassifierConfig {
151
165
  strictChains: boolean;
@@ -225,7 +239,9 @@ export declare const DEFAULT_REDACTION_V3: BelayRedactionConfig;
225
239
  export declare const DEFAULT_CONTROL_PLANE_ISOLATION_V3: BelayControlPlaneIsolationConfig;
226
240
  export declare const LEGACY_CONTROL_PLANE_V3: BelayControlPlaneConfig;
227
241
  export declare const DEFAULT_CONTROL_PLANE_V3: BelayControlPlaneConfig;
242
+ export declare const DEFAULT_CONTAINED_EXECUTION: BelayContainedExecutionConfig;
228
243
  export declare const DEFAULT_SANDBOX_V3: BelaySandboxConfig;
244
+ export declare function normalizeContainedExecutionConfig(raw: Partial<BelayContainedExecutionConfig> | undefined): BelayContainedExecutionConfig;
229
245
  export declare const DEFAULT_NOTIFICATIONS_V3: BelayNotificationsConfig;
230
246
  export declare const DEFAULT_APPROVAL_SIGNING_V3: BelayApprovalSigningConfig;
231
247
  export declare const DEFAULT_APPROVAL_CONFIG: BelayApprovalConfig;
@@ -276,6 +292,8 @@ export declare function normalizeConfigV2(config: BelayConfigV2): BelayConfigV2;
276
292
  export declare function normalizeConfig(config: BelayConfigV4): BelayConfigV4;
277
293
  export declare function normalizeConfig(config: BelayConfigV2): BelayConfigV2;
278
294
  export declare function isFreshConfigInput(loaded: unknown): boolean;
295
+ export declare function hasForbiddenShellOverrideLists(config: BelayConfigV4): boolean;
296
+ export declare function stripForbiddenShellOverrideLists(config: BelayConfigV4): BelayConfigV4;
279
297
  export declare function mergeConfig(existing: unknown, defaults?: BelayConfigV4): BelayConfigV4;
280
298
  export declare function scrubOptionsFromConfig(config: BelayConfigV4): ScrubOptions;
281
299
  export declare function classifierOptionsFromConfig(config: BelayConfigV4): ClassifierOptions;
@@ -148,11 +148,80 @@ export const DEFAULT_CONTROL_PLANE_V3 = {
148
148
  integrity: 'hash-pinned',
149
149
  isolation: { ...DEFAULT_CONTROL_PLANE_ISOLATION_V3 },
150
150
  };
151
+ export const DEFAULT_CONTAINED_EXECUTION = {
152
+ enabled: false,
153
+ image: null,
154
+ dockerExecutable: null,
155
+ dockerHost: null,
156
+ timeoutMs: 30_000,
157
+ memoryMiB: 2048,
158
+ cpus: 2,
159
+ pids: 256,
160
+ };
151
161
  export const DEFAULT_SANDBOX_V3 = {
152
162
  enabled: false,
153
163
  runtime: 'none',
154
164
  denyNetworkByDefault: true,
165
+ containedExecution: { ...DEFAULT_CONTAINED_EXECUTION },
155
166
  };
167
+ function positiveInteger(value, fallback) {
168
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : fallback;
169
+ }
170
+ function positiveNumber(value, fallback) {
171
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
172
+ }
173
+ export function normalizeContainedExecutionConfig(raw) {
174
+ return {
175
+ enabled: raw?.enabled === true,
176
+ image: typeof raw?.image === 'string' && raw.image.trim() ? raw.image.trim() : null,
177
+ dockerExecutable: typeof raw?.dockerExecutable === 'string' && raw.dockerExecutable.trim()
178
+ ? raw.dockerExecutable.trim()
179
+ : null,
180
+ dockerHost: typeof raw?.dockerHost === 'string' && raw.dockerHost.trim() ? raw.dockerHost.trim() : null,
181
+ timeoutMs: positiveInteger(raw?.timeoutMs, DEFAULT_CONTAINED_EXECUTION.timeoutMs),
182
+ memoryMiB: positiveInteger(raw?.memoryMiB, DEFAULT_CONTAINED_EXECUTION.memoryMiB),
183
+ cpus: positiveNumber(raw?.cpus, DEFAULT_CONTAINED_EXECUTION.cpus),
184
+ pids: positiveInteger(raw?.pids, DEFAULT_CONTAINED_EXECUTION.pids),
185
+ };
186
+ }
187
+ function normalizeSandboxConfig(raw) {
188
+ const runtime = raw?.runtime === 'cursor-sandbox' ||
189
+ raw?.runtime === 'container' ||
190
+ raw?.runtime === 'seatbelt' ||
191
+ raw?.runtime === 'landlock'
192
+ ? raw.runtime
193
+ : DEFAULT_SANDBOX_V3.runtime;
194
+ const containedExecution = normalizeContainedExecutionConfig(raw?.containedExecution);
195
+ if (containedExecution.enabled && (raw?.enabled !== true || runtime !== 'container')) {
196
+ throw new Error('contained execution requires sandbox.runtime=container with sandbox.enabled=true');
197
+ }
198
+ if (containedExecution.enabled && !containedExecution.image) {
199
+ throw new Error('contained execution requires an explicit image');
200
+ }
201
+ if (containedExecution.enabled && !containedExecution.dockerExecutable) {
202
+ throw new Error('contained execution requires an explicit Docker executable');
203
+ }
204
+ if (containedExecution.enabled &&
205
+ (!path.isAbsolute(containedExecution.dockerExecutable ?? '') ||
206
+ /[\0\n\r]/.test(containedExecution.dockerExecutable ?? ''))) {
207
+ throw new Error('contained execution requires an absolute executable path');
208
+ }
209
+ if (containedExecution.enabled && !containedExecution.dockerHost) {
210
+ throw new Error('contained execution requires an explicit Docker host');
211
+ }
212
+ if (containedExecution.enabled &&
213
+ (!containedExecution.dockerHost?.startsWith('unix:///') ||
214
+ !path.isAbsolute(containedExecution.dockerHost.slice('unix://'.length)) ||
215
+ /[\0\n\r]/.test(containedExecution.dockerHost))) {
216
+ throw new Error('contained execution requires a local unix Docker host');
217
+ }
218
+ return {
219
+ enabled: raw?.enabled === true,
220
+ runtime,
221
+ denyNetworkByDefault: raw?.denyNetworkByDefault !== false,
222
+ containedExecution,
223
+ };
224
+ }
156
225
  export const DEFAULT_NOTIFICATIONS_V3 = {};
157
226
  export const DEFAULT_APPROVAL_SIGNING_V3 = {
158
227
  required: false,
@@ -829,16 +898,7 @@ export function normalizeConfig(config) {
829
898
  : DEFAULT_EGRESS_V3.listenPort,
830
899
  demoteL3External: v4.egress?.demoteL3External !== false,
831
900
  },
832
- sandbox: {
833
- enabled: v4.sandbox?.enabled === true,
834
- runtime: v4.sandbox?.runtime === 'cursor-sandbox' ||
835
- v4.sandbox?.runtime === 'container' ||
836
- v4.sandbox?.runtime === 'seatbelt' ||
837
- v4.sandbox?.runtime === 'landlock'
838
- ? v4.sandbox.runtime
839
- : DEFAULT_SANDBOX_V3.runtime,
840
- denyNetworkByDefault: v4.sandbox?.denyNetworkByDefault !== false,
841
- },
901
+ sandbox: normalizeSandboxConfig(v4.sandbox),
842
902
  audit: {
843
903
  logPath: v4.audit?.logPath || DEFAULT_CONFIG_V4.audit.logPath,
844
904
  includeAssessment: v4.audit?.includeAssessment !== false,
@@ -859,6 +919,21 @@ export function isFreshConfigInput(loaded) {
859
919
  }
860
920
  return Object.keys(loaded).length === 0;
861
921
  }
922
+ export function hasForbiddenShellOverrideLists(config) {
923
+ return config.overrides.allow.length > 0 || config.overrides.external.length > 0;
924
+ }
925
+ export function stripForbiddenShellOverrideLists(config) {
926
+ if (!hasForbiddenShellOverrideLists(config)) {
927
+ return config;
928
+ }
929
+ return {
930
+ ...config,
931
+ overrides: {
932
+ allow: [],
933
+ external: [],
934
+ },
935
+ };
936
+ }
862
937
  export function mergeConfig(existing, defaults = DEFAULT_CONFIG_V4) {
863
938
  const migrated = isFreshConfigInput(existing)
864
939
  ? normalizeConfig({ ...defaults, version: 4 })
@@ -914,6 +989,10 @@ export function mergeConfig(existing, defaults = DEFAULT_CONFIG_V4) {
914
989
  sandbox: {
915
990
  ...defaults.sandbox,
916
991
  ...migrated.sandbox,
992
+ containedExecution: {
993
+ ...(defaults.sandbox.containedExecution ?? DEFAULT_CONTAINED_EXECUTION),
994
+ ...(migrated.sandbox.containedExecution ?? DEFAULT_CONTAINED_EXECUTION),
995
+ },
917
996
  },
918
997
  audit: {
919
998
  ...defaults.audit,
@@ -0,0 +1,18 @@
1
+ import type { ContainedExecutionResourceLimits } from '../capability/attestation.js';
2
+ export declare const TMPFS_SIZE_BYTES: number;
3
+ export declare const SHM_SIZE_MIB = 64;
4
+ export declare const TMPFS_OPTIONS = "rw,nosuid,nodev,noexec,size=67108864,mode=1777";
5
+ export declare const PROXY_ENV_NAMES: readonly ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "all_proxy", "no_proxy"];
6
+ export declare const IMAGE_ID_PATTERN: RegExp;
7
+ export declare function assertContainedExecutionResourceLimits(limits: ContainedExecutionResourceLimits): void;
8
+ export interface BuildContainedDockerCreateArgsParams {
9
+ containerName: string;
10
+ imageId: string;
11
+ command: string;
12
+ hostMirrorRoot: string;
13
+ guestWorkspacePath: string;
14
+ guestCwd: string;
15
+ resourceLimits: ContainedExecutionResourceLimits;
16
+ user: string;
17
+ }
18
+ export declare function buildContainedDockerCreateArgs(params: BuildContainedDockerCreateArgsParams): string[];
@@ -0,0 +1,97 @@
1
+ import path from 'node:path';
2
+ import { ContainedExecutionFailureError } from './failure.js';
3
+ export const TMPFS_SIZE_BYTES = 64 * 1024 * 1024;
4
+ export const SHM_SIZE_MIB = 64;
5
+ export const TMPFS_OPTIONS = `rw,nosuid,nodev,noexec,size=${TMPFS_SIZE_BYTES},mode=1777`;
6
+ export const PROXY_ENV_NAMES = [
7
+ 'HTTP_PROXY',
8
+ 'HTTPS_PROXY',
9
+ 'ALL_PROXY',
10
+ 'NO_PROXY',
11
+ 'http_proxy',
12
+ 'https_proxy',
13
+ 'all_proxy',
14
+ 'no_proxy',
15
+ ];
16
+ export const IMAGE_ID_PATTERN = /^sha256:[a-f0-9]{64}$/;
17
+ const SAFE_CONTAINER_NAME = /^belay-contained-[0-9a-f-]{36}$/;
18
+ function assertSafeDockerPath(value, code) {
19
+ if (!path.isAbsolute(value) || /[\0\n\r,]/.test(value)) {
20
+ throw new ContainedExecutionFailureError(code);
21
+ }
22
+ }
23
+ export function assertContainedExecutionResourceLimits(limits) {
24
+ if (!Number.isSafeInteger(limits.timeoutMs) ||
25
+ limits.timeoutMs <= 0 ||
26
+ !Number.isSafeInteger(limits.memoryMiB) ||
27
+ limits.memoryMiB <= 0 ||
28
+ !Number.isFinite(limits.cpus) ||
29
+ limits.cpus <= 0 ||
30
+ !Number.isSafeInteger(limits.pids) ||
31
+ limits.pids <= 0) {
32
+ throw new ContainedExecutionFailureError('contained_execution_resource_limits_invalid');
33
+ }
34
+ }
35
+ export function buildContainedDockerCreateArgs(params) {
36
+ if (!IMAGE_ID_PATTERN.test(params.imageId)) {
37
+ throw new ContainedExecutionFailureError('contained_execution_invalid_image_id');
38
+ }
39
+ if (!SAFE_CONTAINER_NAME.test(params.containerName)) {
40
+ throw new ContainedExecutionFailureError('contained_execution_invalid_container_name');
41
+ }
42
+ assertSafeDockerPath(params.hostMirrorRoot, 'contained_execution_invalid_mount');
43
+ assertSafeDockerPath(params.guestWorkspacePath, 'contained_execution_invalid_mount');
44
+ assertSafeDockerPath(params.guestCwd, 'contained_execution_invalid_cwd');
45
+ assertContainedExecutionResourceLimits(params.resourceLimits);
46
+ if (!/^\d+:\d+$/.test(params.user)) {
47
+ throw new ContainedExecutionFailureError('contained_execution_invalid_user');
48
+ }
49
+ const memory = `${params.resourceLimits.memoryMiB}m`;
50
+ return [
51
+ 'create',
52
+ '--name',
53
+ params.containerName,
54
+ '--network',
55
+ 'none',
56
+ '--read-only',
57
+ '--tmpfs',
58
+ `/tmp:${TMPFS_OPTIONS}`,
59
+ '--cap-drop',
60
+ 'ALL',
61
+ '--security-opt',
62
+ 'no-new-privileges',
63
+ '--log-driver',
64
+ 'none',
65
+ '--privileged=false',
66
+ '--publish-all=false',
67
+ '--restart',
68
+ 'no',
69
+ '--memory',
70
+ memory,
71
+ '--memory-swap',
72
+ memory,
73
+ '--shm-size',
74
+ `${SHM_SIZE_MIB}m`,
75
+ '--cpus',
76
+ String(params.resourceLimits.cpus),
77
+ '--pids-limit',
78
+ String(params.resourceLimits.pids),
79
+ '--user',
80
+ params.user,
81
+ '--no-healthcheck',
82
+ '--ipc',
83
+ 'none',
84
+ '--cgroupns',
85
+ 'private',
86
+ ...PROXY_ENV_NAMES.flatMap((name) => ['--env', `${name}=`]),
87
+ '--mount',
88
+ `type=bind,src=${params.hostMirrorRoot},dst=${params.guestWorkspacePath}`,
89
+ '--workdir',
90
+ params.guestCwd,
91
+ '--entrypoint',
92
+ '/bin/sh',
93
+ params.imageId,
94
+ '-c',
95
+ params.command,
96
+ ];
97
+ }