@guilz-dev/belay 0.4.0 → 0.7.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 +27 -2
- package/dist/adapters/claude/runtime-entry.js +54 -25
- package/dist/adapters/layouts/scope.js +3 -2
- package/dist/adapters/shared/gate-runtime.d.ts +8 -2
- package/dist/adapters/shared/gate-runtime.js +579 -181
- package/dist/bundle/claude-runtime.mjs +13574 -6334
- package/dist/bundle/codex-runtime.mjs +13108 -5903
- package/dist/bundle/cursor-runtime.mjs +12676 -5476
- package/dist/cli.js +80 -2
- package/dist/commands/approve.js +34 -10
- package/dist/commands/classify-for-report.js +1 -1
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +134 -11
- package/dist/commands/doctor.js +87 -1
- package/dist/commands/explain.js +11 -1
- package/dist/commands/judge.js +3 -2
- package/dist/commands/recovery-checkpoints.d.ts +164 -0
- package/dist/commands/recovery-checkpoints.js +347 -0
- package/dist/commands/revoke.js +19 -7
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.js +51 -0
- package/dist/commands/status.js +7 -0
- package/dist/config-io.js +91 -23
- package/dist/conformance/guarantee-posture.d.ts +21 -0
- package/dist/conformance/guarantee-posture.js +46 -0
- package/dist/conformance/guarantee-table.d.ts +1 -0
- package/dist/conformance/guarantee-table.js +20 -9
- package/dist/conformance/types.d.ts +1 -0
- package/dist/core/approval-replay.d.ts +1 -1
- package/dist/core/approval-replay.js +4 -1
- package/dist/core/approval-service.d.ts +6 -1
- package/dist/core/approval-service.js +87 -24
- package/dist/core/approval.d.ts +4 -0
- package/dist/core/approval.js +19 -5
- package/dist/core/audit-types.d.ts +13 -0
- package/dist/core/capability/approval-state-mutation.d.ts +32 -0
- package/dist/core/capability/approval-state-mutation.js +165 -0
- package/dist/core/capability/approval-v3.d.ts +32 -0
- package/dist/core/capability/approval-v3.js +114 -0
- package/dist/core/capability/attestation.d.ts +16 -0
- package/dist/core/capability/attestation.js +62 -0
- package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
- package/dist/core/capability/boundary-attestation-sign.js +84 -0
- package/dist/core/capability/boundary-driver-container.d.ts +19 -0
- package/dist/core/capability/boundary-driver-container.js +189 -0
- package/dist/core/capability/boundary-driver.d.ts +29 -0
- package/dist/core/capability/boundary-driver.js +44 -0
- package/dist/core/capability/boundary-egress.d.ts +21 -0
- package/dist/core/capability/boundary-egress.js +109 -0
- package/dist/core/capability/boundary-grant-materialize.d.ts +14 -0
- package/dist/core/capability/boundary-grant-materialize.js +93 -0
- package/dist/core/capability/boundary-profile.d.ts +15 -0
- package/dist/core/capability/boundary-profile.js +25 -0
- package/dist/core/capability/boundary-run.d.ts +48 -0
- package/dist/core/capability/boundary-run.js +51 -0
- package/dist/core/capability/boundary-session.d.ts +61 -0
- package/dist/core/capability/boundary-session.js +138 -0
- package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
- package/dist/core/capability/boundary-workspace-mount.js +68 -0
- package/dist/core/capability/capability-request-hash.d.ts +2 -0
- package/dist/core/capability/capability-request-hash.js +8 -0
- package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
- package/dist/core/capability/gate-latency-ratchet.js +18 -0
- package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
- package/dist/core/capability/gate-policy-shadow.js +130 -0
- package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
- package/dist/core/capability/gate-shadow-audit.js +1 -0
- package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
- package/dist/core/capability/gate-shadow-ratchet.js +81 -0
- package/dist/core/capability/grant-consumption.d.ts +4 -0
- package/dist/core/capability/grant-consumption.js +11 -0
- package/dist/core/capability/grant-lease.d.ts +44 -0
- package/dist/core/capability/grant-lease.js +243 -0
- package/dist/core/capability/grant-loader.d.ts +12 -0
- package/dist/core/capability/grant-loader.js +23 -0
- package/dist/core/capability/grant-match.d.ts +6 -0
- package/dist/core/capability/grant-match.js +96 -0
- package/dist/core/capability/grant.d.ts +16 -0
- package/dist/core/capability/grant.js +16 -0
- package/dist/core/capability/index.d.ts +8 -0
- package/dist/core/capability/index.js +7 -0
- package/dist/core/capability/limits.d.ts +7 -0
- package/dist/core/capability/limits.js +55 -0
- package/dist/core/capability/policy-bridge.d.ts +18 -0
- package/dist/core/capability/policy-bridge.js +51 -0
- package/dist/core/capability/policy-engine.d.ts +77 -0
- package/dist/core/capability/policy-engine.js +663 -0
- package/dist/core/capability/policy-types.d.ts +20 -0
- package/dist/core/capability/policy-types.js +1 -0
- package/dist/core/capability/request.d.ts +48 -0
- package/dist/core/capability/request.js +1 -0
- package/dist/core/capability/resolver.d.ts +4 -0
- package/dist/core/capability/resolver.js +4 -0
- package/dist/core/classify-subagent.d.ts +2 -1
- package/dist/core/classify-subagent.js +38 -2
- package/dist/core/classify-tool.js +126 -62
- package/dist/core/config.d.ts +34 -3
- package/dist/core/config.js +118 -3
- package/dist/core/effect-ir/audit.d.ts +9 -0
- package/dist/core/effect-ir/audit.js +86 -0
- package/dist/core/effect-ir/build.d.ts +31 -0
- package/dist/core/effect-ir/build.js +277 -0
- package/dist/core/effect-ir/index.d.ts +8 -0
- package/dist/core/effect-ir/index.js +7 -0
- package/dist/core/effect-ir/normalize.d.ts +4 -0
- package/dist/core/effect-ir/normalize.js +104 -0
- package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
- package/dist/core/effect-ir/package-exec-eval.js +152 -0
- package/dist/core/effect-ir/package-exec.d.ts +29 -0
- package/dist/core/effect-ir/package-exec.js +292 -0
- package/dist/core/effect-ir/policy.d.ts +13 -0
- package/dist/core/effect-ir/policy.js +77 -0
- package/dist/core/effect-ir/types.d.ts +55 -0
- package/dist/core/effect-ir/types.js +1 -0
- package/dist/core/egress/allowlist.d.ts +1 -0
- package/dist/core/egress/allowlist.js +8 -0
- package/dist/core/egress-approval.js +45 -51
- package/dist/core/gate-contract.d.ts +5 -0
- package/dist/core/gate-contract.js +5 -0
- package/dist/core/gate-engine.js +30 -8
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +2 -1
- package/dist/core/judge-cloud-consent.js +29 -15
- package/dist/core/judge-doctor.js +39 -14
- package/dist/core/judge-fallback-hints.d.ts +13 -0
- package/dist/core/judge-fallback-hints.js +108 -0
- package/dist/core/network-endpoint.d.ts +10 -0
- package/dist/core/network-endpoint.js +51 -0
- package/dist/core/path-utils.d.ts +1 -0
- package/dist/core/path-utils.js +5 -2
- package/dist/core/process-runner.d.ts +10 -0
- package/dist/core/process-runner.js +124 -0
- package/dist/core/recover-advice.js +2 -1
- package/dist/core/recovery/artifact-store.d.ts +29 -0
- package/dist/core/recovery/artifact-store.js +336 -0
- package/dist/core/recovery/capability.d.ts +3 -0
- package/dist/core/recovery/capability.js +22 -0
- package/dist/core/recovery/checkpoint.d.ts +25 -0
- package/dist/core/recovery/checkpoint.js +260 -0
- package/dist/core/recovery/fail-closed.d.ts +6 -0
- package/dist/core/recovery/fail-closed.js +25 -0
- package/dist/core/recovery/index.d.ts +5 -0
- package/dist/core/recovery/index.js +4 -0
- package/dist/core/recovery/operator-guidance.d.ts +7 -0
- package/dist/core/recovery/operator-guidance.js +39 -0
- package/dist/core/recovery/reconcile.d.ts +3 -0
- package/dist/core/recovery/reconcile.js +48 -0
- package/dist/core/recovery/resource-identity.d.ts +4 -0
- package/dist/core/recovery/resource-identity.js +34 -0
- package/dist/core/recovery/restore.d.ts +13 -0
- package/dist/core/recovery/restore.js +114 -0
- package/dist/core/recovery/snapshot-node.d.ts +27 -0
- package/dist/core/recovery/snapshot-node.js +261 -0
- package/dist/core/recovery/types.d.ts +104 -0
- package/dist/core/recovery/types.js +1 -0
- package/dist/core/standing-allow.js +3 -0
- package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
- package/dist/core/transactional/apply-observed-changes.js +275 -0
- package/dist/core/transactional/backend-selector.d.ts +8 -0
- package/dist/core/transactional/backend-selector.js +118 -0
- package/dist/core/transactional/backend.d.ts +55 -0
- package/dist/core/transactional/backend.js +1 -0
- package/dist/core/transactional/diff-evaluator.js +4 -1
- package/dist/core/transactional/eligibility.js +4 -0
- package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-backend.js +278 -0
- package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
- package/dist/core/transactional/file-checkpoint-git.js +217 -0
- package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
- package/dist/core/transactional/file-checkpoint-staging.js +54 -0
- package/dist/core/transactional/file-clone.d.ts +15 -0
- package/dist/core/transactional/file-clone.js +139 -0
- package/dist/core/transactional/file-tree-path.d.ts +7 -0
- package/dist/core/transactional/file-tree-path.js +50 -0
- package/dist/core/transactional/file-tree.d.ts +41 -0
- package/dist/core/transactional/file-tree.js +186 -0
- package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
- package/dist/core/transactional/git-worktree-backend.js +50 -0
- package/dist/core/transactional/git-worktree.d.ts +14 -9
- package/dist/core/transactional/git-worktree.js +62 -87
- package/dist/core/transactional/index.d.ts +11 -0
- package/dist/core/transactional/index.js +10 -0
- package/dist/core/transactional/reasons.d.ts +1 -0
- package/dist/core/transactional/reasons.js +5 -0
- package/dist/core/transactional/runner.js +171 -23
- package/dist/core/transactional/snapshot-node.d.ts +26 -0
- package/dist/core/transactional/snapshot-node.js +87 -0
- package/dist/core/transactional/types.d.ts +21 -1
- package/dist/core/types.d.ts +30 -3
- package/dist/core/verdict/adapter.d.ts +3 -4
- package/dist/core/verdict/adapter.js +24 -12
- package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
- package/dist/core/verdict/cursor-acp-client.js +345 -0
- package/dist/core/verdict/egress-classify.d.ts +2 -2
- package/dist/core/verdict/egress-classify.js +2 -2
- package/dist/core/verdict/git-classifier.d.ts +26 -0
- package/dist/core/verdict/git-classifier.js +533 -0
- package/dist/core/verdict/judge-audit.js +1 -0
- package/dist/core/verdict/judge-baseline.d.ts +1 -1
- package/dist/core/verdict/judge-baseline.js +1 -0
- package/dist/core/verdict/judge-broker-service.js +12 -0
- package/dist/core/verdict/judge-catalog.js +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +6 -0
- package/dist/core/verdict/judge-cli.js +5 -3
- package/dist/core/verdict/judge-provider-matrix.js +1 -1
- package/dist/core/verdict/judge-runtime-config.d.ts +2 -0
- package/dist/core/verdict/judge-runtime-config.js +10 -6
- package/dist/core/verdict/judge-session-broker.d.ts +1 -1
- package/dist/core/verdict/judge-session-broker.js +10 -5
- package/dist/core/verdict/judge-transport.d.ts +5 -1
- package/dist/core/verdict/judge-transport.js +24 -14
- package/dist/core/verdict/judge.d.ts +2 -15
- package/dist/core/verdict/judge.js +5 -68
- package/dist/core/verdict/launcher-resolve.d.ts +2 -0
- package/dist/core/verdict/launcher-resolve.js +14 -16
- package/dist/core/verdict/prescan.d.ts +15 -0
- package/dist/core/verdict/prescan.js +68 -0
- package/dist/core/verdict/shell-policy.d.ts +26 -0
- package/dist/core/verdict/shell-policy.js +40 -0
- package/dist/core/verdict/shell-semantics.d.ts +24 -0
- package/dist/core/verdict/shell-semantics.js +225 -0
- package/dist/core/verdict/types.d.ts +18 -1
- package/dist/core/verdict/verdict.js +313 -178
- package/dist/corpus/evaluate.js +3 -7
- package/dist/corpus/gate-latency-budget.d.ts +39 -0
- package/dist/corpus/gate-latency-budget.js +33 -0
- package/dist/corpus/mutators.d.ts +1 -1
- package/dist/corpus/mutators.js +11 -0
- package/dist/corpus/runtime-match.js +1 -3
- package/dist/corpus/standing-allow-catalog.generated.js +29 -9
- package/dist/judge-broker-daemon.js +15 -1
- package/dist/services/sandbox-service.d.ts +4 -0
- package/dist/services/sandbox-service.js +28 -2
- package/dist/types.d.ts +13 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/belay/SKILL.md +8 -3
- package/skills/belay/belay-approve.md +10 -7
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { canonicalPath } from '../path-utils.js';
|
|
4
|
+
import { runProcessWithBoundedOutput } from '../process-runner.js';
|
|
5
|
+
import { dockerEnvArgs, dockerNetworkArgs, ensureBelayContainerNetwork } from './boundary-egress.js';
|
|
6
|
+
import { materializeContainerBoundaryGrant } from './boundary-grant-materialize.js';
|
|
7
|
+
import { BoundaryCleanupError, } from './boundary-run.js';
|
|
8
|
+
import { buildWorkspaceMountSpec, resolveGuestWorkdir, workspaceMountEnvArgs, } from './boundary-workspace-mount.js';
|
|
9
|
+
const ATTESTATION_TTL_MS = 15 * 60_000;
|
|
10
|
+
const DEFAULT_IMAGE = 'alpine:3.20';
|
|
11
|
+
const CONTAINER_CLEANUP_TIMEOUT_MS = 10_000;
|
|
12
|
+
const CONTAINER_INSPECT_TIMEOUT_MS = 2_000;
|
|
13
|
+
const CONTAINER_ABSENCE_ATTEMPTS = 10;
|
|
14
|
+
const CONTAINER_ABSENCE_RETRY_MS = 100;
|
|
15
|
+
function containerDoesNotExist(result) {
|
|
16
|
+
return /no such container/i.test(`${result.stderr ?? ''}\n${result.stdout ?? ''}`);
|
|
17
|
+
}
|
|
18
|
+
async function confirmContainerAbsent(containerName) {
|
|
19
|
+
for (let attempt = 0; attempt < CONTAINER_ABSENCE_ATTEMPTS; attempt += 1) {
|
|
20
|
+
const inspection = await runProcessWithBoundedOutput('docker', ['inspect', '--type', 'container', containerName], {}, CONTAINER_INSPECT_TIMEOUT_MS);
|
|
21
|
+
if (!inspection.timedOut && inspection.exitCode !== 0 && containerDoesNotExist(inspection)) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (inspection.timedOut || (inspection.exitCode !== 0 && !containerDoesNotExist(inspection))) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
await new Promise((resolve) => setTimeout(resolve, CONTAINER_ABSENCE_RETRY_MS));
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
export async function isDockerAvailable() {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const child = spawn('docker', ['info'], { stdio: 'ignore' });
|
|
34
|
+
child.on('error', () => resolve(false));
|
|
35
|
+
child.on('close', (code) => resolve(code === 0));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function containerAttestation(proxyEnv) {
|
|
39
|
+
const probedAt = new Date().toISOString();
|
|
40
|
+
const signals = [
|
|
41
|
+
'docker',
|
|
42
|
+
'container-driver',
|
|
43
|
+
'repo-mount-ro-default',
|
|
44
|
+
'workspace-mount-isolation',
|
|
45
|
+
];
|
|
46
|
+
if (Object.keys(proxyEnv).length > 0) {
|
|
47
|
+
signals.push('egress-proxy-chokepoint');
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
signals.push('network-none');
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
version: 1,
|
|
54
|
+
driver: 'container',
|
|
55
|
+
probedAt,
|
|
56
|
+
expiresAt: new Date(Date.now() + ATTESTATION_TTL_MS).toISOString(),
|
|
57
|
+
// Mount and network classes are isolated, but exact grants are not yet
|
|
58
|
+
// passed to and enforced by the process runner.
|
|
59
|
+
deniesUngrantedEffects: false,
|
|
60
|
+
materializesGrants: false,
|
|
61
|
+
isolatesWorkspaceMounts: true,
|
|
62
|
+
probeSignals: [...signals, 'exact-grant-enforcement-unavailable'],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function runDockerProbe(image) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const child = spawn('docker', ['run', '--rm', '--network', 'none', image, 'echo', 'ok'], {
|
|
68
|
+
stdio: 'ignore',
|
|
69
|
+
});
|
|
70
|
+
child.on('error', () => resolve(false));
|
|
71
|
+
child.on('close', (code) => resolve(code === 0));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function buildContainerRunArgs(params) {
|
|
75
|
+
const proxyActive = Object.keys(params.proxyEnv).length > 0;
|
|
76
|
+
const networkArgs = dockerNetworkArgs(proxyActive, params.repoRoot);
|
|
77
|
+
const containerNameArgs = params.containerName ? ['--name', params.containerName] : [];
|
|
78
|
+
const workspaceMount = params.runOptions?.workspaceMount;
|
|
79
|
+
if (workspaceMount) {
|
|
80
|
+
if (!params.repoRoot) {
|
|
81
|
+
throw new Error('boundary_workspace_mount_missing_resource_root');
|
|
82
|
+
}
|
|
83
|
+
if (canonicalPath(workspaceMount.guestTargetRoot) !== canonicalPath(params.repoRoot)) {
|
|
84
|
+
throw new Error('boundary_workspace_mount_target_mismatch');
|
|
85
|
+
}
|
|
86
|
+
const workdir = resolveGuestWorkdir(workspaceMount);
|
|
87
|
+
if (canonicalPath(params.cwd) !== workdir) {
|
|
88
|
+
throw new Error('boundary_workspace_mount_cwd_mismatch');
|
|
89
|
+
}
|
|
90
|
+
return [
|
|
91
|
+
'run',
|
|
92
|
+
'--rm',
|
|
93
|
+
...containerNameArgs,
|
|
94
|
+
...networkArgs,
|
|
95
|
+
'--mount',
|
|
96
|
+
buildWorkspaceMountSpec(workspaceMount),
|
|
97
|
+
'-w',
|
|
98
|
+
workdir,
|
|
99
|
+
...workspaceMountEnvArgs(workspaceMount),
|
|
100
|
+
...dockerEnvArgs(params.proxyEnv),
|
|
101
|
+
params.image,
|
|
102
|
+
'sh',
|
|
103
|
+
'-c',
|
|
104
|
+
params.command,
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
const mount = canonicalPath(params.cwd);
|
|
108
|
+
const mountSpec = params.mountReadOnly ? `${mount}:${mount}:ro` : `${mount}:${mount}`;
|
|
109
|
+
return [
|
|
110
|
+
'run',
|
|
111
|
+
'--rm',
|
|
112
|
+
...containerNameArgs,
|
|
113
|
+
...networkArgs,
|
|
114
|
+
'-v',
|
|
115
|
+
mountSpec,
|
|
116
|
+
'-w',
|
|
117
|
+
mount,
|
|
118
|
+
...dockerEnvArgs(params.proxyEnv),
|
|
119
|
+
params.image,
|
|
120
|
+
'sh',
|
|
121
|
+
'-c',
|
|
122
|
+
params.command,
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
async function runInContainer(image, command, cwd, timeoutMs, proxyEnv, mountReadOnly, repoRoot, runOptions) {
|
|
126
|
+
const containerName = `belay-run-${randomUUID()}`;
|
|
127
|
+
const args = buildContainerRunArgs({
|
|
128
|
+
image,
|
|
129
|
+
command,
|
|
130
|
+
cwd,
|
|
131
|
+
proxyEnv,
|
|
132
|
+
mountReadOnly,
|
|
133
|
+
repoRoot,
|
|
134
|
+
runOptions,
|
|
135
|
+
containerName,
|
|
136
|
+
});
|
|
137
|
+
const result = await runProcessWithBoundedOutput('docker', args, {}, timeoutMs);
|
|
138
|
+
if (result.timedOut) {
|
|
139
|
+
const cleanup = await runProcessWithBoundedOutput('docker', ['rm', '-f', containerName], {}, CONTAINER_CLEANUP_TIMEOUT_MS);
|
|
140
|
+
const cleanupFailed = cleanup.timedOut || cleanup.exitCode !== 0;
|
|
141
|
+
const absent = containerDoesNotExist(cleanup) ||
|
|
142
|
+
(cleanupFailed && (await confirmContainerAbsent(containerName)));
|
|
143
|
+
if (cleanupFailed && !absent) {
|
|
144
|
+
throw new BoundaryCleanupError('container', containerName);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
export function createContainerBoundaryDriver(options = {}) {
|
|
150
|
+
const image = options.image ?? DEFAULT_IMAGE;
|
|
151
|
+
const proxyEnv = options.egressProxyEnv ?? {};
|
|
152
|
+
const repoRoot = options.repoRoot;
|
|
153
|
+
let preparedNetwork = false;
|
|
154
|
+
return {
|
|
155
|
+
id: 'container',
|
|
156
|
+
async probe() {
|
|
157
|
+
if (!(await isDockerAvailable())) {
|
|
158
|
+
throw new Error('Docker is not available for container boundary driver');
|
|
159
|
+
}
|
|
160
|
+
if (!(await runDockerProbe(image))) {
|
|
161
|
+
throw new Error(`Docker probe failed for image ${image}`);
|
|
162
|
+
}
|
|
163
|
+
return containerAttestation(proxyEnv);
|
|
164
|
+
},
|
|
165
|
+
async prepare(context) {
|
|
166
|
+
if (context.egressProxyActive && context.repoRoot) {
|
|
167
|
+
await ensureBelayContainerNetwork(context.repoRoot);
|
|
168
|
+
preparedNetwork = true;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
async run(command, cwd, timeoutMs, options) {
|
|
172
|
+
const mountReadOnly = options?.mountReadOnly !== false && !options?.workspaceMount;
|
|
173
|
+
const proxyActive = Object.keys(proxyEnv).length > 0;
|
|
174
|
+
if (proxyActive && repoRoot && !preparedNetwork) {
|
|
175
|
+
await ensureBelayContainerNetwork(repoRoot);
|
|
176
|
+
}
|
|
177
|
+
return runInContainer(image, command, cwd, timeoutMs, proxyEnv, mountReadOnly, repoRoot, options);
|
|
178
|
+
},
|
|
179
|
+
materializeGrant(request, context) {
|
|
180
|
+
return materializeContainerBoundaryGrant(request, {
|
|
181
|
+
attestation: context.attestation,
|
|
182
|
+
mountRoot: context.mountRoot,
|
|
183
|
+
egressProxyActive: context.egressProxyActive,
|
|
184
|
+
existingGrants: context.existingGrants,
|
|
185
|
+
sensitivePaths: context.sensitivePaths,
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ShellRunResult } from '../transactional/git-worktree.js';
|
|
2
|
+
import type { BoundaryAttestation, BoundaryDriverId } from './attestation.js';
|
|
3
|
+
import type { BoundaryPrepareContext, BoundaryRunOptions } from './boundary-run.js';
|
|
4
|
+
import type { CapabilityGrantV1 } from './grant.js';
|
|
5
|
+
import type { CapabilityRequestV1 } from './request.js';
|
|
6
|
+
export type { ShellRunResult as BoundaryRunResult };
|
|
7
|
+
export interface BoundaryMaterializeContext {
|
|
8
|
+
attestation: BoundaryAttestation;
|
|
9
|
+
mountRoot: string;
|
|
10
|
+
egressProxyActive: boolean;
|
|
11
|
+
existingGrants?: CapabilityGrantV1[];
|
|
12
|
+
sensitivePaths?: string[];
|
|
13
|
+
}
|
|
14
|
+
export type { BoundaryRunOptions } from './boundary-run.js';
|
|
15
|
+
export { boundaryMountReadOnlyFromPrediction } from './boundary-run.js';
|
|
16
|
+
export interface BoundaryDriver {
|
|
17
|
+
id: BoundaryDriverId;
|
|
18
|
+
probe(): Promise<BoundaryAttestation>;
|
|
19
|
+
prepare?(context: BoundaryPrepareContext): Promise<void>;
|
|
20
|
+
run(command: string, cwd: string, timeoutMs: number, options?: BoundaryRunOptions): Promise<ShellRunResult>;
|
|
21
|
+
materializeGrant(request: CapabilityRequestV1, context: BoundaryMaterializeContext): CapabilityGrantV1 | null;
|
|
22
|
+
}
|
|
23
|
+
export interface BoundaryDriverOptions {
|
|
24
|
+
egressProxyEnv?: Record<string, string>;
|
|
25
|
+
image?: string;
|
|
26
|
+
repoRoot?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function createHostIntegrationDriver(): BoundaryDriver;
|
|
29
|
+
export declare function getDefaultBoundaryDriver(driverId?: BoundaryDriverId, options?: BoundaryDriverOptions): BoundaryDriver;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { runShellCommand } from '../transactional/git-worktree.js';
|
|
2
|
+
import { createContainerBoundaryDriver } from './boundary-driver-container.js';
|
|
3
|
+
export { boundaryMountReadOnlyFromPrediction } from './boundary-run.js';
|
|
4
|
+
const ATTESTATION_TTL_MS = 15 * 60_000;
|
|
5
|
+
function hostIntegrationAttestation(driver) {
|
|
6
|
+
const probedAt = new Date().toISOString();
|
|
7
|
+
return {
|
|
8
|
+
version: 1,
|
|
9
|
+
driver,
|
|
10
|
+
probedAt,
|
|
11
|
+
expiresAt: new Date(Date.now() + ATTESTATION_TTL_MS).toISOString(),
|
|
12
|
+
deniesUngrantedEffects: false,
|
|
13
|
+
materializesGrants: false,
|
|
14
|
+
isolatesWorkspaceMounts: false,
|
|
15
|
+
probeSignals: ['host-integration', 'l3-policy-only'],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function createHostIntegrationDriver() {
|
|
19
|
+
return {
|
|
20
|
+
id: 'host-integration',
|
|
21
|
+
async probe() {
|
|
22
|
+
return hostIntegrationAttestation('host-integration');
|
|
23
|
+
},
|
|
24
|
+
async prepare() {
|
|
25
|
+
// L3 host path has no runtime preparation.
|
|
26
|
+
},
|
|
27
|
+
run(command, cwd, timeoutMs) {
|
|
28
|
+
return runShellCommand(command, cwd, timeoutMs);
|
|
29
|
+
},
|
|
30
|
+
materializeGrant() {
|
|
31
|
+
return null;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function getDefaultBoundaryDriver(driverId = 'host-integration', options = {}) {
|
|
36
|
+
if (driverId === 'container') {
|
|
37
|
+
return createContainerBoundaryDriver({
|
|
38
|
+
image: options.image,
|
|
39
|
+
egressProxyEnv: options.egressProxyEnv,
|
|
40
|
+
repoRoot: options.repoRoot,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return createHostIntegrationDriver();
|
|
44
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BelayConfigV4 } from '../config.js';
|
|
2
|
+
import type { BoundaryDriverId } from './attestation.js';
|
|
3
|
+
export declare function isEgressProxyActive(params: {
|
|
4
|
+
config: BelayConfigV4;
|
|
5
|
+
running: boolean;
|
|
6
|
+
foreignProxy?: boolean;
|
|
7
|
+
repoRootMismatch?: boolean;
|
|
8
|
+
}): boolean;
|
|
9
|
+
export declare function egressProxyEnvFromConfig(config: BelayConfigV4, proxyActive: boolean): Record<string, string>;
|
|
10
|
+
/** Container workloads cannot reach 127.0.0.1 on the host; route via host.docker.internal. */
|
|
11
|
+
export declare function egressProxyEnvForContainer(config: BelayConfigV4, proxyActive: boolean): Record<string, string>;
|
|
12
|
+
export declare function resolveBoundaryEgressProxyEnv(params: {
|
|
13
|
+
driverId: BoundaryDriverId;
|
|
14
|
+
config: BelayConfigV4;
|
|
15
|
+
proxyActive: boolean;
|
|
16
|
+
}): Record<string, string>;
|
|
17
|
+
export declare function belayContainerNetworkName(repoRoot: string): string;
|
|
18
|
+
export declare function isBelayContainerNetworkReady(repoRoot: string): Promise<boolean>;
|
|
19
|
+
export declare function ensureBelayContainerNetwork(repoRoot: string): Promise<string>;
|
|
20
|
+
export declare function dockerNetworkArgs(proxyActive: boolean, repoRoot?: string): string[];
|
|
21
|
+
export declare function dockerEnvArgs(proxyEnv: Record<string, string>): string[];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { recommendedProxyEnv } from '../egress/env.js';
|
|
4
|
+
import { canonicalPath } from '../path-utils.js';
|
|
5
|
+
export function isEgressProxyActive(params) {
|
|
6
|
+
if (!params.config.egress.enabled) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
return params.running && !params.foreignProxy && !params.repoRootMismatch;
|
|
10
|
+
}
|
|
11
|
+
function proxyHostForContainer(listenHost) {
|
|
12
|
+
if (listenHost === '127.0.0.1' || listenHost === 'localhost' || listenHost === '::1') {
|
|
13
|
+
return 'host.docker.internal';
|
|
14
|
+
}
|
|
15
|
+
return listenHost;
|
|
16
|
+
}
|
|
17
|
+
export function egressProxyEnvFromConfig(config, proxyActive) {
|
|
18
|
+
if (!proxyActive || !config.egress.enabled) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
return recommendedProxyEnv(config.egress);
|
|
22
|
+
}
|
|
23
|
+
/** Container workloads cannot reach 127.0.0.1 on the host; route via host.docker.internal. */
|
|
24
|
+
export function egressProxyEnvForContainer(config, proxyActive) {
|
|
25
|
+
if (!proxyActive || !config.egress.enabled) {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
const host = proxyHostForContainer(config.egress.listenHost);
|
|
29
|
+
const proxyUrl = `http://${host}:${config.egress.listenPort}`;
|
|
30
|
+
return {
|
|
31
|
+
HTTP_PROXY: proxyUrl,
|
|
32
|
+
HTTPS_PROXY: proxyUrl,
|
|
33
|
+
http_proxy: proxyUrl,
|
|
34
|
+
https_proxy: proxyUrl,
|
|
35
|
+
NO_PROXY: '127.0.0.1,localhost',
|
|
36
|
+
no_proxy: '127.0.0.1,localhost',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function resolveBoundaryEgressProxyEnv(params) {
|
|
40
|
+
if (params.driverId === 'container') {
|
|
41
|
+
return egressProxyEnvForContainer(params.config, params.proxyActive);
|
|
42
|
+
}
|
|
43
|
+
return egressProxyEnvFromConfig(params.config, params.proxyActive);
|
|
44
|
+
}
|
|
45
|
+
export function belayContainerNetworkName(repoRoot) {
|
|
46
|
+
const hash = createHash('sha256').update(canonicalPath(repoRoot)).digest('hex').slice(0, 12);
|
|
47
|
+
return `belay-int-${hash}`;
|
|
48
|
+
}
|
|
49
|
+
function dockerNetworkInspect(name) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
const child = spawn('docker', ['network', 'inspect', name], { stdio: 'ignore' });
|
|
52
|
+
child.on('error', () => resolve(false));
|
|
53
|
+
child.on('close', (code) => resolve(code === 0));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export async function isBelayContainerNetworkReady(repoRoot) {
|
|
57
|
+
return dockerNetworkInspect(belayContainerNetworkName(repoRoot));
|
|
58
|
+
}
|
|
59
|
+
function dockerNetworkCreateInternal(name) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const child = spawn('docker', ['network', 'create', '--internal', '--label', 'belay=1', name], {
|
|
62
|
+
stdio: 'ignore',
|
|
63
|
+
});
|
|
64
|
+
child.on('error', reject);
|
|
65
|
+
child.on('close', (code) => {
|
|
66
|
+
if (code === 0) {
|
|
67
|
+
resolve();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
reject(new Error(`Failed to create internal docker network ${name}`));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export async function ensureBelayContainerNetwork(repoRoot) {
|
|
75
|
+
const name = belayContainerNetworkName(repoRoot);
|
|
76
|
+
if (await dockerNetworkInspect(name)) {
|
|
77
|
+
return name;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await dockerNetworkCreateInternal(name);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (await dockerNetworkInspect(name)) {
|
|
84
|
+
return name;
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
return name;
|
|
89
|
+
}
|
|
90
|
+
export function dockerNetworkArgs(proxyActive, repoRoot) {
|
|
91
|
+
if (!proxyActive) {
|
|
92
|
+
return ['--network', 'none'];
|
|
93
|
+
}
|
|
94
|
+
if (!repoRoot) {
|
|
95
|
+
return ['--network', 'none'];
|
|
96
|
+
}
|
|
97
|
+
return [
|
|
98
|
+
'--add-host=host.docker.internal:host-gateway',
|
|
99
|
+
'--network',
|
|
100
|
+
belayContainerNetworkName(repoRoot),
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
export function dockerEnvArgs(proxyEnv) {
|
|
104
|
+
const args = [];
|
|
105
|
+
for (const [key, value] of Object.entries(proxyEnv)) {
|
|
106
|
+
args.push('-e', `${key}=${value}`);
|
|
107
|
+
}
|
|
108
|
+
return args;
|
|
109
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BoundaryAttestation } from './attestation.js';
|
|
2
|
+
import { type CapabilityGrantV1 } from './grant.js';
|
|
3
|
+
import type { CapabilityRequestV1 } from './request.js';
|
|
4
|
+
export declare const BOUNDARY_GRANT_ISSUER_CONTAINER: "boundary:container";
|
|
5
|
+
export declare function isPathWithinBoundaryMount(request: CapabilityRequestV1): boolean;
|
|
6
|
+
export interface MaterializeBoundaryGrantParams {
|
|
7
|
+
attestation: BoundaryAttestation;
|
|
8
|
+
mountRoot: string;
|
|
9
|
+
egressProxyActive: boolean;
|
|
10
|
+
existingGrants?: CapabilityGrantV1[];
|
|
11
|
+
sensitivePaths?: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function materializeContainerBoundaryGrant(request: CapabilityRequestV1, params: MaterializeBoundaryGrantParams): CapabilityGrantV1 | null;
|
|
14
|
+
export declare function materializeContainerBoundaryGrants(requests: readonly CapabilityRequestV1[], params: MaterializeBoundaryGrantParams): CapabilityGrantV1[];
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { matchesSensitivePath } from '../glob.js';
|
|
3
|
+
import { canonicalPath, pathWithinRoot } from '../path-utils.js';
|
|
4
|
+
import { newGrantId } from './approval-v3.js';
|
|
5
|
+
import { isAttestationFresh } from './attestation.js';
|
|
6
|
+
import { CAPABILITY_GRANT_VERSION } from './grant.js';
|
|
7
|
+
import { grantMatchesRequest } from './grant-match.js';
|
|
8
|
+
const BOUNDARY_GRANT_TTL_MS = 15 * 60_000;
|
|
9
|
+
export const BOUNDARY_GRANT_ISSUER_CONTAINER = 'boundary:container';
|
|
10
|
+
function resolveCapabilityPath(targetPath, cwd) {
|
|
11
|
+
const joined = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
|
|
12
|
+
return canonicalPath(joined);
|
|
13
|
+
}
|
|
14
|
+
export function isPathWithinBoundaryMount(request) {
|
|
15
|
+
if (request.resource.kind !== 'path') {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
const mountRoot = canonicalPath(request.context.cwd);
|
|
19
|
+
const resolved = resolveCapabilityPath(request.resource.path, request.context.cwd);
|
|
20
|
+
return pathWithinRoot(mountRoot, resolved);
|
|
21
|
+
}
|
|
22
|
+
function mintBoundaryGrant(request, attestation, issuer) {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const attestationExpiry = Date.parse(attestation.expiresAt);
|
|
25
|
+
const grantExpiry = now + BOUNDARY_GRANT_TTL_MS;
|
|
26
|
+
const expiresAt = new Date(Number.isFinite(attestationExpiry) ? Math.min(attestationExpiry, grantExpiry) : grantExpiry).toISOString();
|
|
27
|
+
return {
|
|
28
|
+
version: CAPABILITY_GRANT_VERSION,
|
|
29
|
+
grantId: newGrantId(),
|
|
30
|
+
principal: request.principal,
|
|
31
|
+
action: request.action,
|
|
32
|
+
resource: request.resource,
|
|
33
|
+
inputFingerprint: request.context.inputFingerprint,
|
|
34
|
+
issuedAt: new Date(now).toISOString(),
|
|
35
|
+
expiresAt,
|
|
36
|
+
maxUses: 1,
|
|
37
|
+
usesRemaining: 1,
|
|
38
|
+
issuer,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function materializeContainerBoundaryGrant(request, params) {
|
|
42
|
+
if (!isAttestationFresh(params.attestation)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (!params.attestation.materializesGrants || params.attestation.driver !== 'container') {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (request.evidence.level !== 'certain') {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const repoRoot = canonicalPath(request.principal.repoRoot);
|
|
52
|
+
const mountRoot = canonicalPath(params.mountRoot);
|
|
53
|
+
if (request.action === 'fs.read' || request.action === 'fs.write') {
|
|
54
|
+
if (request.resource.kind !== 'path') {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const resolved = resolveCapabilityPath(request.resource.path, request.context.cwd);
|
|
58
|
+
if (!pathWithinRoot(repoRoot, resolved) || !pathWithinRoot(mountRoot, resolved)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const relative = resolved.slice(repoRoot.length).replace(/^[/\\]/, '');
|
|
62
|
+
if (params.sensitivePaths?.length && matchesSensitivePath(relative, params.sensitivePaths)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return mintBoundaryGrant(request, params.attestation, BOUNDARY_GRANT_ISSUER_CONTAINER);
|
|
66
|
+
}
|
|
67
|
+
if (request.action === 'network.connect' && request.resource.kind === 'network') {
|
|
68
|
+
if (!params.egressProxyActive) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const approved = params.existingGrants?.some((grant) => grant.issuer !== BOUNDARY_GRANT_ISSUER_CONTAINER && grantMatchesRequest(grant, request));
|
|
72
|
+
if (!approved) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return mintBoundaryGrant(request, params.attestation, BOUNDARY_GRANT_ISSUER_CONTAINER);
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
export function materializeContainerBoundaryGrants(requests, params) {
|
|
80
|
+
const grants = [];
|
|
81
|
+
let existing = [...(params.existingGrants ?? [])];
|
|
82
|
+
for (const request of requests) {
|
|
83
|
+
const materialized = materializeContainerBoundaryGrant(request, {
|
|
84
|
+
...params,
|
|
85
|
+
existingGrants: existing,
|
|
86
|
+
});
|
|
87
|
+
if (materialized) {
|
|
88
|
+
grants.push(materialized);
|
|
89
|
+
existing = [...existing, materialized];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return grants;
|
|
93
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BelayConfigV4 } from '../config.js';
|
|
2
|
+
import type { BoundaryAttestation } from './attestation.js';
|
|
3
|
+
/** Policy-only boundary (L3/L4). No attested L1 enforcement. */
|
|
4
|
+
export declare const BOUNDARY_PROFILE_L3_POLICY: "l3-policy-engine";
|
|
5
|
+
/** Editor-facing label when only prediction + approval layers are active. */
|
|
6
|
+
export declare const BOUNDARY_PROFILE_L3_L4_ONLY: "l3-l4-only";
|
|
7
|
+
/** Attested container / sandbox boundary with grant materialization. */
|
|
8
|
+
export declare const BOUNDARY_PROFILE_L1_ATTESTED: "l1-attested-boundary";
|
|
9
|
+
export type BoundaryProfileId = typeof BOUNDARY_PROFILE_L3_POLICY | typeof BOUNDARY_PROFILE_L3_L4_ONLY | typeof BOUNDARY_PROFILE_L1_ATTESTED;
|
|
10
|
+
export declare function isAttestedBoundary(attestation: BoundaryAttestation | null | undefined): boolean;
|
|
11
|
+
export declare function resolveBoundaryProfile(params: {
|
|
12
|
+
config: BelayConfigV4;
|
|
13
|
+
attestation?: BoundaryAttestation | null;
|
|
14
|
+
}): BoundaryProfileId;
|
|
15
|
+
export declare function boundaryVerifiedAllowEnabled(attestation: BoundaryAttestation | null | undefined): boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isAttestationFresh } from './attestation.js';
|
|
2
|
+
/** Policy-only boundary (L3/L4). No attested L1 enforcement. */
|
|
3
|
+
export const BOUNDARY_PROFILE_L3_POLICY = 'l3-policy-engine';
|
|
4
|
+
/** Editor-facing label when only prediction + approval layers are active. */
|
|
5
|
+
export const BOUNDARY_PROFILE_L3_L4_ONLY = 'l3-l4-only';
|
|
6
|
+
/** Attested container / sandbox boundary with grant materialization. */
|
|
7
|
+
export const BOUNDARY_PROFILE_L1_ATTESTED = 'l1-attested-boundary';
|
|
8
|
+
export function isAttestedBoundary(attestation) {
|
|
9
|
+
if (!attestation || !isAttestationFresh(attestation)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
return attestation.deniesUngrantedEffects && attestation.materializesGrants;
|
|
13
|
+
}
|
|
14
|
+
export function resolveBoundaryProfile(params) {
|
|
15
|
+
if (!isAttestedBoundary(params.attestation)) {
|
|
16
|
+
return BOUNDARY_PROFILE_L3_L4_ONLY;
|
|
17
|
+
}
|
|
18
|
+
if (params.attestation?.driver === 'container') {
|
|
19
|
+
return BOUNDARY_PROFILE_L1_ATTESTED;
|
|
20
|
+
}
|
|
21
|
+
return BOUNDARY_PROFILE_L3_POLICY;
|
|
22
|
+
}
|
|
23
|
+
export function boundaryVerifiedAllowEnabled(attestation) {
|
|
24
|
+
return isAttestedBoundary(attestation);
|
|
25
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ShellRunResult } from '../transactional/git-worktree.js';
|
|
2
|
+
import type { ClassifyResult } from '../types.js';
|
|
3
|
+
export interface BoundaryCleanupFailure {
|
|
4
|
+
code: 'BOUNDARY_CLEANUP_UNCONFIRMED';
|
|
5
|
+
resourceKind: 'container';
|
|
6
|
+
resourceId: string;
|
|
7
|
+
executionStarted: true;
|
|
8
|
+
cleanupConfirmed: false;
|
|
9
|
+
}
|
|
10
|
+
export declare class BoundaryCleanupError extends Error implements BoundaryCleanupFailure {
|
|
11
|
+
readonly resourceKind: 'container';
|
|
12
|
+
readonly resourceId: string;
|
|
13
|
+
readonly code: "BOUNDARY_CLEANUP_UNCONFIRMED";
|
|
14
|
+
readonly executionStarted: true;
|
|
15
|
+
readonly cleanupConfirmed: false;
|
|
16
|
+
constructor(resourceKind: 'container', resourceId: string);
|
|
17
|
+
}
|
|
18
|
+
export declare function isBoundaryCleanupError(value: unknown): value is BoundaryCleanupFailure;
|
|
19
|
+
export declare function safeBoundaryCleanupResourceId(value: unknown): string | undefined;
|
|
20
|
+
export interface BoundaryWorkspaceMount {
|
|
21
|
+
hostSourceRoot: string;
|
|
22
|
+
guestTargetRoot: string;
|
|
23
|
+
cwdRelative: string;
|
|
24
|
+
writable: boolean;
|
|
25
|
+
hideHostSourcePath: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface BoundaryRunOptions {
|
|
28
|
+
/** When true, container driver mounts the working directory read-only. */
|
|
29
|
+
mountReadOnly?: boolean;
|
|
30
|
+
workspaceMount?: BoundaryWorkspaceMount;
|
|
31
|
+
}
|
|
32
|
+
export interface BoundaryPrepareContext {
|
|
33
|
+
repoRoot?: string;
|
|
34
|
+
egressProxyActive: boolean;
|
|
35
|
+
proxyEnv: Record<string, string>;
|
|
36
|
+
}
|
|
37
|
+
export interface BoundaryRunnable {
|
|
38
|
+
prepare?(context: BoundaryPrepareContext): Promise<void>;
|
|
39
|
+
run(command: string, cwd: string, timeoutMs: number, options?: BoundaryRunOptions): Promise<ShellRunResult>;
|
|
40
|
+
}
|
|
41
|
+
export declare function boundaryMountReadOnlyFromPrediction(predicted: ClassifyResult): boolean;
|
|
42
|
+
export declare function runWithBoundaryRunnable(target: BoundaryRunnable, params: {
|
|
43
|
+
prepareContext: BoundaryPrepareContext;
|
|
44
|
+
command: string;
|
|
45
|
+
cwd: string;
|
|
46
|
+
timeoutMs: number;
|
|
47
|
+
runOptions?: BoundaryRunOptions;
|
|
48
|
+
}): Promise<ShellRunResult>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const SAFE_CONTAINER_RESOURCE_ID = /^belay-run-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
2
|
+
export class BoundaryCleanupError extends Error {
|
|
3
|
+
resourceKind;
|
|
4
|
+
resourceId;
|
|
5
|
+
code = 'BOUNDARY_CLEANUP_UNCONFIRMED';
|
|
6
|
+
executionStarted = true;
|
|
7
|
+
cleanupConfirmed = false;
|
|
8
|
+
constructor(resourceKind, resourceId) {
|
|
9
|
+
super('Boundary cleanup could not be confirmed');
|
|
10
|
+
this.resourceKind = resourceKind;
|
|
11
|
+
this.resourceId = resourceId;
|
|
12
|
+
this.name = 'BoundaryCleanupError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function isBoundaryCleanupError(value) {
|
|
16
|
+
if (!value || typeof value !== 'object') {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const candidate = value;
|
|
20
|
+
return (candidate.code === 'BOUNDARY_CLEANUP_UNCONFIRMED' &&
|
|
21
|
+
candidate.resourceKind === 'container' &&
|
|
22
|
+
typeof candidate.resourceId === 'string' &&
|
|
23
|
+
candidate.executionStarted === true &&
|
|
24
|
+
candidate.cleanupConfirmed === false);
|
|
25
|
+
}
|
|
26
|
+
export function safeBoundaryCleanupResourceId(value) {
|
|
27
|
+
if (!isBoundaryCleanupError(value) || !SAFE_CONTAINER_RESOURCE_ID.test(value.resourceId)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return value.resourceId;
|
|
31
|
+
}
|
|
32
|
+
export function boundaryMountReadOnlyFromPrediction(predicted) {
|
|
33
|
+
const effect = predicted.axes?.effect;
|
|
34
|
+
if (effect === 'local_mutation' || effect === 'remote_mutation') {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
for (const request of predicted.capabilityRequests ?? []) {
|
|
38
|
+
if (request.action === 'fs.write' ||
|
|
39
|
+
request.action === 'git.ref.write' ||
|
|
40
|
+
request.action === 'control_plane.write') {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
export async function runWithBoundaryRunnable(target, params) {
|
|
47
|
+
if (target.prepare) {
|
|
48
|
+
await target.prepare(params.prepareContext);
|
|
49
|
+
}
|
|
50
|
+
return target.run(params.command, params.cwd, params.timeoutMs, params.runOptions);
|
|
51
|
+
}
|