@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.
- package/README.md +60 -10
- package/dist/adapters/shared/gate-runtime.d.ts +5 -0
- package/dist/adapters/shared/gate-runtime.js +237 -33
- package/dist/bundle/claude-runtime.mjs +4082 -1230
- package/dist/bundle/codex-runtime.mjs +4057 -1205
- package/dist/bundle/cursor-runtime.mjs +4057 -1205
- package/dist/commands/classify-for-report.js +5 -0
- package/dist/commands/doctor.js +73 -6
- package/dist/commands/explain.js +3 -2
- package/dist/commands/metrics.js +42 -0
- package/dist/commands/recovery-checkpoints.d.ts +8 -1
- package/dist/commands/recovery-checkpoints.js +44 -11
- package/dist/commands/session.d.ts +5 -0
- package/dist/commands/session.js +8 -1
- package/dist/config-io.js +2 -2
- package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
- package/dist/conformance/contained-execution-guarantee.js +121 -0
- package/dist/conformance/guarantee-table.js +7 -0
- package/dist/core/audit-io.js +8 -0
- package/dist/core/audit-metrics.d.ts +11 -0
- package/dist/core/audit-metrics.js +14 -0
- package/dist/core/audit-recovery-metrics.d.ts +33 -0
- package/dist/core/audit-recovery-metrics.js +146 -0
- package/dist/core/audit-types.d.ts +7 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/bounded-output.d.ts +11 -0
- package/dist/core/bounded-output.js +26 -0
- package/dist/core/capability/attestation.d.ts +56 -0
- package/dist/core/capability/attestation.js +110 -3
- package/dist/core/capability/boundary-session.d.ts +5 -0
- package/dist/core/capability/boundary-session.js +38 -1
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/index.js +1 -1
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +89 -10
- package/dist/core/contained-execution/docker-policy.d.ts +18 -0
- package/dist/core/contained-execution/docker-policy.js +97 -0
- package/dist/core/contained-execution/docker.d.ts +223 -0
- package/dist/core/contained-execution/docker.js +846 -0
- package/dist/core/contained-execution/eligibility.d.ts +9 -0
- package/dist/core/contained-execution/eligibility.js +103 -0
- package/dist/core/contained-execution/failure.d.ts +18 -0
- package/dist/core/contained-execution/failure.js +105 -0
- package/dist/core/contained-execution/mirror.d.ts +54 -0
- package/dist/core/contained-execution/mirror.js +587 -0
- package/dist/core/contained-execution/policy.d.ts +10 -0
- package/dist/core/contained-execution/policy.js +20 -0
- package/dist/core/effect-ir/index.d.ts +1 -1
- package/dist/core/effect-ir/index.js +1 -1
- package/dist/core/effect-ir/normalize.d.ts +5 -0
- package/dist/core/effect-ir/normalize.js +17 -7
- package/dist/core/effect-ir/shell-build.js +2 -14
- package/dist/core/effect-ir/shell-lower.js +100 -25
- package/dist/core/gate-contract.d.ts +4 -1
- package/dist/core/gate-contract.js +2 -0
- package/dist/core/process-runner.d.ts +10 -3
- package/dist/core/process-runner.js +66 -10
- package/dist/core/recovery/checkpoint.d.ts +3 -0
- package/dist/core/recovery/checkpoint.js +33 -4
- package/dist/core/recovery/types.d.ts +1 -0
- package/dist/core/scrub.d.ts +17 -0
- package/dist/core/scrub.js +499 -5
- package/dist/core/standing-allow.d.ts +0 -20
- package/dist/core/standing-allow.js +0 -71
- package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
- package/dist/core/transactional/apply-observed-changes.js +11 -0
- package/dist/core/transactional/backend-selector.d.ts +2 -1
- package/dist/core/transactional/backend-selector.js +32 -29
- package/dist/core/transactional/backend.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-backend.js +242 -54
- package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-git.js +53 -0
- package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
- package/dist/core/transactional/git-worktree-backend.js +3 -0
- package/dist/core/transactional/git-worktree.d.ts +1 -0
- package/dist/core/transactional/git-worktree.js +1 -0
- package/dist/core/transactional/runner.js +59 -13
- package/dist/core/types.d.ts +13 -0
- package/dist/core/verdict/parser.d.ts +10 -0
- package/dist/core/verdict/parser.js +271 -40
- package/dist/core/verdict/shell-semantics.js +21 -0
- package/dist/corpus/adversarial-probe.d.ts +1 -1
- package/dist/corpus/adversarial-probe.js +3 -3
- package/dist/corpus/benign-probe-cores.d.ts +6 -0
- package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
- package/dist/corpus/coverage-compare.d.ts +36 -0
- package/dist/corpus/coverage-compare.js +197 -0
- package/dist/corpus/coverage-contexts.d.ts +17 -0
- package/dist/corpus/coverage-contexts.js +88 -0
- package/dist/corpus/coverage-matrix.d.ts +37 -0
- package/dist/corpus/coverage-matrix.js +159 -0
- package/dist/corpus/coverage-probe.d.ts +91 -0
- package/dist/corpus/coverage-probe.js +341 -0
- package/dist/corpus/evaluate.d.ts +1 -1
- package/dist/corpus/evaluate.js +1 -1
- package/dist/corpus/structural-fixture-root.d.ts +2 -0
- package/dist/corpus/structural-fixture-root.js +5 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -1
- package/skills/belay/SKILL.md +19 -0
- package/skills/belay/belay-status.md +5 -0
- 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
|
|
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 (!
|
|
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
|
-
|
|
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 {
|
|
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';
|
package/dist/core/config.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/config.js
CHANGED
|
@@ -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
|
+
}
|