@guilz-dev/belay 0.6.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 +20 -1
- package/dist/adapters/claude/runtime-entry.js +54 -25
- package/dist/adapters/shared/gate-runtime.d.ts +7 -0
- package/dist/adapters/shared/gate-runtime.js +420 -220
- package/dist/bundle/claude-runtime.mjs +13215 -8864
- package/dist/bundle/codex-runtime.mjs +10771 -6453
- package/dist/bundle/cursor-runtime.mjs +10797 -6484
- package/dist/cli.js +37 -1
- package/dist/commands/approve.js +34 -10
- package/dist/commands/doctor.js +51 -0
- 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/status.js +7 -0
- package/dist/config-io.js +49 -18
- package/dist/conformance/guarantee-table.js +6 -0
- 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 +86 -27
- package/dist/core/approval.d.ts +2 -0
- package/dist/core/approval.js +6 -3
- package/dist/core/audit-types.d.ts +13 -0
- package/dist/core/capability/approval-state-mutation.d.ts +19 -0
- package/dist/core/capability/approval-state-mutation.js +76 -3
- package/dist/core/capability/approval-v3.d.ts +18 -1
- package/dist/core/capability/approval-v3.js +63 -23
- package/dist/core/capability/attestation.d.ts +3 -0
- package/dist/core/capability/attestation.js +7 -0
- package/dist/core/capability/boundary-driver-container.d.ts +12 -0
- package/dist/core/capability/boundary-driver-container.js +101 -34
- package/dist/core/capability/boundary-driver.js +1 -0
- package/dist/core/capability/boundary-grant-materialize.d.ts +1 -0
- package/dist/core/capability/boundary-grant-materialize.js +15 -0
- package/dist/core/capability/boundary-run.d.ts +25 -0
- package/dist/core/capability/boundary-run.js +37 -9
- package/dist/core/capability/boundary-session.d.ts +4 -0
- package/dist/core/capability/boundary-session.js +9 -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/gate-policy-shadow.js +23 -1
- 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 +36 -1
- package/dist/core/capability/grant-lease.js +213 -16
- package/dist/core/capability/grant-match.js +3 -0
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/index.js +1 -1
- package/dist/core/capability/policy-engine.d.ts +7 -0
- package/dist/core/capability/policy-engine.js +202 -15
- package/dist/core/capability/request.d.ts +3 -0
- package/dist/core/config.d.ts +20 -0
- package/dist/core/config.js +62 -0
- 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 +2 -0
- package/dist/core/gate-contract.js +2 -0
- package/dist/core/gate-engine.js +26 -9
- 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/network-endpoint.d.ts +10 -0
- package/dist/core/network-endpoint.js +51 -0
- 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/checkpoint.d.ts +25 -0
- package/dist/core/recovery/checkpoint.js +260 -0
- package/dist/core/recovery/index.d.ts +3 -1
- package/dist/core/recovery/index.js +2 -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 +85 -1
- 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/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 +9 -11
- package/dist/core/transactional/git-worktree.js +24 -119
- package/dist/core/transactional/index.d.ts +11 -0
- package/dist/core/transactional/index.js +10 -0
- package/dist/core/transactional/reasons.js +1 -0
- package/dist/core/transactional/runner.js +157 -25
- 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 +17 -1
- package/dist/core/types.d.ts +10 -1
- package/dist/core/verdict/adapter.js +8 -0
- package/dist/core/verdict/launcher-resolve.js +2 -20
- package/dist/core/verdict/shell-policy.d.ts +1 -0
- package/dist/core/verdict/shell-policy.js +1 -1
- package/dist/core/verdict/types.d.ts +5 -0
- package/dist/core/verdict/verdict.js +37 -20
- package/dist/types.d.ts +11 -0
- 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
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { canonicalPath } from '../path-utils.js';
|
|
4
|
+
import { runProcessWithBoundedOutput } from '../process-runner.js';
|
|
3
5
|
import { dockerEnvArgs, dockerNetworkArgs, ensureBelayContainerNetwork } from './boundary-egress.js';
|
|
4
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';
|
|
5
9
|
const ATTESTATION_TTL_MS = 15 * 60_000;
|
|
6
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
|
+
}
|
|
7
31
|
export async function isDockerAvailable() {
|
|
8
32
|
return new Promise((resolve) => {
|
|
9
33
|
const child = spawn('docker', ['info'], { stdio: 'ignore' });
|
|
@@ -13,7 +37,12 @@ export async function isDockerAvailable() {
|
|
|
13
37
|
}
|
|
14
38
|
function containerAttestation(proxyEnv) {
|
|
15
39
|
const probedAt = new Date().toISOString();
|
|
16
|
-
const signals = [
|
|
40
|
+
const signals = [
|
|
41
|
+
'docker',
|
|
42
|
+
'container-driver',
|
|
43
|
+
'repo-mount-ro-default',
|
|
44
|
+
'workspace-mount-isolation',
|
|
45
|
+
];
|
|
17
46
|
if (Object.keys(proxyEnv).length > 0) {
|
|
18
47
|
signals.push('egress-proxy-chokepoint');
|
|
19
48
|
}
|
|
@@ -25,9 +54,12 @@ function containerAttestation(proxyEnv) {
|
|
|
25
54
|
driver: 'container',
|
|
26
55
|
probedAt,
|
|
27
56
|
expiresAt: new Date(Date.now() + ATTESTATION_TTL_MS).toISOString(),
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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'],
|
|
31
63
|
};
|
|
32
64
|
}
|
|
33
65
|
function runDockerProbe(image) {
|
|
@@ -39,45 +71,80 @@ function runDockerProbe(image) {
|
|
|
39
71
|
child.on('close', (code) => resolve(code === 0));
|
|
40
72
|
});
|
|
41
73
|
}
|
|
42
|
-
function
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
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 [
|
|
48
110
|
'run',
|
|
49
111
|
'--rm',
|
|
112
|
+
...containerNameArgs,
|
|
50
113
|
...networkArgs,
|
|
51
114
|
'-v',
|
|
52
115
|
mountSpec,
|
|
53
116
|
'-w',
|
|
54
117
|
mount,
|
|
55
|
-
...dockerEnvArgs(proxyEnv),
|
|
56
|
-
image,
|
|
118
|
+
...dockerEnvArgs(params.proxyEnv),
|
|
119
|
+
params.image,
|
|
57
120
|
'sh',
|
|
58
121
|
'-c',
|
|
59
|
-
command,
|
|
122
|
+
params.command,
|
|
60
123
|
];
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
clearTimeout(timer);
|
|
74
|
-
resolve({
|
|
75
|
-
exitCode,
|
|
76
|
-
signal: signal ? String(signal) : null,
|
|
77
|
-
timedOut,
|
|
78
|
-
});
|
|
79
|
-
});
|
|
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,
|
|
80
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;
|
|
81
148
|
}
|
|
82
149
|
export function createContainerBoundaryDriver(options = {}) {
|
|
83
150
|
const image = options.image ?? DEFAULT_IMAGE;
|
|
@@ -102,12 +169,12 @@ export function createContainerBoundaryDriver(options = {}) {
|
|
|
102
169
|
}
|
|
103
170
|
},
|
|
104
171
|
async run(command, cwd, timeoutMs, options) {
|
|
105
|
-
const mountReadOnly = options?.mountReadOnly !== false;
|
|
172
|
+
const mountReadOnly = options?.mountReadOnly !== false && !options?.workspaceMount;
|
|
106
173
|
const proxyActive = Object.keys(proxyEnv).length > 0;
|
|
107
174
|
if (proxyActive && repoRoot && !preparedNetwork) {
|
|
108
175
|
await ensureBelayContainerNetwork(repoRoot);
|
|
109
176
|
}
|
|
110
|
-
return runInContainer(image, command, cwd, timeoutMs, proxyEnv, mountReadOnly, repoRoot);
|
|
177
|
+
return runInContainer(image, command, cwd, timeoutMs, proxyEnv, mountReadOnly, repoRoot, options);
|
|
111
178
|
},
|
|
112
179
|
materializeGrant(request, context) {
|
|
113
180
|
return materializeContainerBoundaryGrant(request, {
|
|
@@ -11,6 +11,7 @@ function hostIntegrationAttestation(driver) {
|
|
|
11
11
|
expiresAt: new Date(Date.now() + ATTESTATION_TTL_MS).toISOString(),
|
|
12
12
|
deniesUngrantedEffects: false,
|
|
13
13
|
materializesGrants: false,
|
|
14
|
+
isolatesWorkspaceMounts: false,
|
|
14
15
|
probeSignals: ['host-integration', 'l3-policy-only'],
|
|
15
16
|
};
|
|
16
17
|
}
|
|
@@ -11,3 +11,4 @@ export interface MaterializeBoundaryGrantParams {
|
|
|
11
11
|
sensitivePaths?: string[];
|
|
12
12
|
}
|
|
13
13
|
export declare function materializeContainerBoundaryGrant(request: CapabilityRequestV1, params: MaterializeBoundaryGrantParams): CapabilityGrantV1 | null;
|
|
14
|
+
export declare function materializeContainerBoundaryGrants(requests: readonly CapabilityRequestV1[], params: MaterializeBoundaryGrantParams): CapabilityGrantV1[];
|
|
@@ -76,3 +76,18 @@ export function materializeContainerBoundaryGrant(request, params) {
|
|
|
76
76
|
}
|
|
77
77
|
return null;
|
|
78
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
|
+
}
|
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
import type { ShellRunResult } from '../transactional/git-worktree.js';
|
|
2
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
|
+
}
|
|
3
27
|
export interface BoundaryRunOptions {
|
|
4
28
|
/** When true, container driver mounts the working directory read-only. */
|
|
5
29
|
mountReadOnly?: boolean;
|
|
30
|
+
workspaceMount?: BoundaryWorkspaceMount;
|
|
6
31
|
}
|
|
7
32
|
export interface BoundaryPrepareContext {
|
|
8
33
|
repoRoot?: string;
|
|
@@ -1,17 +1,45 @@
|
|
|
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
|
+
}
|
|
1
32
|
export function boundaryMountReadOnlyFromPrediction(predicted) {
|
|
2
33
|
const effect = predicted.axes?.effect;
|
|
3
34
|
if (effect === 'local_mutation' || effect === 'remote_mutation') {
|
|
4
35
|
return false;
|
|
5
36
|
}
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
if (action === 'network.connect' || action === 'process.exec' || action === 'indeterminate') {
|
|
14
|
-
return true;
|
|
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
|
+
}
|
|
15
43
|
}
|
|
16
44
|
return true;
|
|
17
45
|
}
|
|
@@ -12,6 +12,9 @@ export interface ResolvedBoundaryDriverContext {
|
|
|
12
12
|
proxyActive: boolean;
|
|
13
13
|
proxyEnv: Record<string, string>;
|
|
14
14
|
prepareContext: BoundaryPrepareContext;
|
|
15
|
+
attestationPath: string;
|
|
16
|
+
attestation: BoundaryAttestation | null;
|
|
17
|
+
attestationFresh: boolean;
|
|
15
18
|
}
|
|
16
19
|
export declare function boundaryAttestationPath(repoRoot: string, config: BelayConfigV4): string;
|
|
17
20
|
export declare function loadBoundaryAttestation(filePath: string, expectedRepoRoot?: string, controlPlaneDir?: string): Promise<BoundaryAttestation | null>;
|
|
@@ -53,5 +56,6 @@ export declare function runBoundaryAgentCommand(params: {
|
|
|
53
56
|
command: string;
|
|
54
57
|
cwd?: string;
|
|
55
58
|
timeoutMs?: number;
|
|
59
|
+
runOptions?: BoundaryRunOptions;
|
|
56
60
|
}): Promise<Awaited<ReturnType<BoundaryDriver['run']>>>;
|
|
57
61
|
export { dockerNetworkArgs };
|
|
@@ -51,6 +51,9 @@ export function hostIntegrationBoundaryContext(repoRoot) {
|
|
|
51
51
|
egressProxyActive: false,
|
|
52
52
|
proxyEnv: {},
|
|
53
53
|
},
|
|
54
|
+
attestationPath: '',
|
|
55
|
+
attestation: null,
|
|
56
|
+
attestationFresh: false,
|
|
54
57
|
};
|
|
55
58
|
}
|
|
56
59
|
export async function resolveBoundaryDriverContext(params) {
|
|
@@ -74,6 +77,8 @@ export async function resolveBoundaryDriverContext(params) {
|
|
|
74
77
|
egressProxyEnv: proxyEnv,
|
|
75
78
|
repoRoot: params.repoRoot,
|
|
76
79
|
});
|
|
80
|
+
const attestationPath = boundaryAttestationPath(params.repoRoot, params.config);
|
|
81
|
+
const attestation = await loadBoundaryAttestation(attestationPath, params.repoRoot, configuredControlPlaneDir(params.config));
|
|
77
82
|
return {
|
|
78
83
|
driver,
|
|
79
84
|
driverId,
|
|
@@ -84,6 +89,9 @@ export async function resolveBoundaryDriverContext(params) {
|
|
|
84
89
|
egressProxyActive: proxyActive,
|
|
85
90
|
proxyEnv,
|
|
86
91
|
},
|
|
92
|
+
attestationPath,
|
|
93
|
+
attestation,
|
|
94
|
+
attestationFresh: attestation ? isAttestationFresh(attestation) : false,
|
|
87
95
|
};
|
|
88
96
|
}
|
|
89
97
|
export async function runWithBoundaryDriver(params) {
|
|
@@ -124,6 +132,7 @@ export async function runBoundaryAgentCommand(params) {
|
|
|
124
132
|
command: params.command,
|
|
125
133
|
cwd: params.cwd ?? params.repoRoot,
|
|
126
134
|
timeoutMs: params.timeoutMs ?? 30 * 60_000,
|
|
135
|
+
runOptions: params.runOptions,
|
|
127
136
|
});
|
|
128
137
|
}
|
|
129
138
|
export { dockerNetworkArgs };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BoundaryWorkspaceMount } from './boundary-run.js';
|
|
2
|
+
export declare function validateWorkspaceMount(mount: BoundaryWorkspaceMount): void;
|
|
3
|
+
export declare function resolveGuestWorkdir(mount: BoundaryWorkspaceMount): string;
|
|
4
|
+
export declare function buildWorkspaceMountSpec(mount: BoundaryWorkspaceMount): string;
|
|
5
|
+
export declare function workspaceMountEnvArgs(mount: BoundaryWorkspaceMount): string[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { canonicalPath, pathWithinRoot } from '../path-utils.js';
|
|
3
|
+
const HOST_PATH_ENV_VARS = [
|
|
4
|
+
'BELAY_EGRESS_REPO_ROOT',
|
|
5
|
+
'BELAY_JUDGE_BROKER_REPO_ROOT',
|
|
6
|
+
'BELAY_JUDGE_BROKER_STATE_DIR',
|
|
7
|
+
'BELAY_REPO_ROOT',
|
|
8
|
+
];
|
|
9
|
+
export function validateWorkspaceMount(mount) {
|
|
10
|
+
const hostSourceRoot = canonicalPath(mount.hostSourceRoot);
|
|
11
|
+
const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
|
|
12
|
+
if (!hostSourceRoot || !guestTargetRoot) {
|
|
13
|
+
throw new Error('boundary_workspace_mount_invalid_root');
|
|
14
|
+
}
|
|
15
|
+
if (hostSourceRoot.includes(',') || guestTargetRoot.includes(',')) {
|
|
16
|
+
throw new Error('boundary_workspace_mount_invalid_root');
|
|
17
|
+
}
|
|
18
|
+
if (hostSourceRoot === guestTargetRoot) {
|
|
19
|
+
throw new Error('boundary_workspace_mount_source_equals_target');
|
|
20
|
+
}
|
|
21
|
+
if (pathWithinRoot(hostSourceRoot, guestTargetRoot)) {
|
|
22
|
+
throw new Error('boundary_workspace_mount_source_contains_target');
|
|
23
|
+
}
|
|
24
|
+
if (mount.cwdRelative.includes('\0')) {
|
|
25
|
+
throw new Error('boundary_workspace_mount_invalid_cwd');
|
|
26
|
+
}
|
|
27
|
+
const normalizedRelative = path.posix.normalize(mount.cwdRelative.replace(/\\/g, '/'));
|
|
28
|
+
if (normalizedRelative === '..' || normalizedRelative.startsWith('../')) {
|
|
29
|
+
throw new Error('boundary_workspace_mount_invalid_cwd');
|
|
30
|
+
}
|
|
31
|
+
if (path.posix.isAbsolute(normalizedRelative)) {
|
|
32
|
+
throw new Error('boundary_workspace_mount_invalid_cwd');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function resolveGuestWorkdir(mount) {
|
|
36
|
+
validateWorkspaceMount(mount);
|
|
37
|
+
const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
|
|
38
|
+
const normalizedRelative = path.posix.normalize(mount.cwdRelative.replace(/\\/g, '/'));
|
|
39
|
+
if (normalizedRelative === '.' || normalizedRelative === '') {
|
|
40
|
+
return guestTargetRoot;
|
|
41
|
+
}
|
|
42
|
+
const segments = normalizedRelative.split('/').filter(Boolean);
|
|
43
|
+
return path.posix.join(guestTargetRoot, ...segments);
|
|
44
|
+
}
|
|
45
|
+
export function buildWorkspaceMountSpec(mount) {
|
|
46
|
+
validateWorkspaceMount(mount);
|
|
47
|
+
const hostSourceRoot = canonicalPath(mount.hostSourceRoot);
|
|
48
|
+
const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
|
|
49
|
+
const readonly = mount.writable ? '' : ',readonly';
|
|
50
|
+
return `type=bind,src=${hostSourceRoot},dst=${guestTargetRoot}${readonly}`;
|
|
51
|
+
}
|
|
52
|
+
export function workspaceMountEnvArgs(mount) {
|
|
53
|
+
const workdir = resolveGuestWorkdir(mount);
|
|
54
|
+
if (!mount.hideHostSourcePath) {
|
|
55
|
+
return ['-e', `PWD=${workdir}`];
|
|
56
|
+
}
|
|
57
|
+
const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
|
|
58
|
+
const hostSourceRoot = canonicalPath(mount.hostSourceRoot);
|
|
59
|
+
const args = [];
|
|
60
|
+
for (const key of HOST_PATH_ENV_VARS) {
|
|
61
|
+
args.push('-e', `${key}=`);
|
|
62
|
+
}
|
|
63
|
+
args.push('-e', `PWD=${workdir}`, '-e', `OLDPWD=${workdir}`, '-e', `BELAY_REPO_ROOT=${guestTargetRoot}`);
|
|
64
|
+
if (hostSourceRoot !== guestTargetRoot) {
|
|
65
|
+
args.push('-e', `BELAY_EGRESS_REPO_ROOT=${guestTargetRoot}`);
|
|
66
|
+
}
|
|
67
|
+
return args;
|
|
68
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { belayStateDir } from '../config.js';
|
|
3
|
+
import { effectPlanAuditFields } from '../effect-ir/audit.js';
|
|
3
4
|
import { buildTier1Prompt } from '../verdict/judge.js';
|
|
4
5
|
import { normalizeJudgeRuntimeConfig } from '../verdict/judge-runtime-config.js';
|
|
5
6
|
import { judgeShadowAuditFields, recordShadowComparison, shouldRunShadowComparison, triggerJudgeSessionKillSwitch, } from '../verdict/judge-shadow.js';
|
|
@@ -7,7 +8,16 @@ import { evaluateWithJudgeTransport } from '../verdict/judge-transport.js';
|
|
|
7
8
|
import { recordPolicyJudgeComparison } from './gate-shadow-ratchet.js';
|
|
8
9
|
export { recordGateApprovalAsk } from './gate-shadow-ratchet.js';
|
|
9
10
|
export function capabilityDecisionAuditFields(result) {
|
|
10
|
-
const fields = {
|
|
11
|
+
const fields = {
|
|
12
|
+
...effectPlanAuditFields(result.effectPlan, {
|
|
13
|
+
capabilityRequests: result.capabilityRequests,
|
|
14
|
+
decisions: result.effectPlanPolicyDecisions,
|
|
15
|
+
authorizationDecision: result.authorizationDecision,
|
|
16
|
+
}),
|
|
17
|
+
executionRoute: 'downstream_host',
|
|
18
|
+
enforcementStatus: 'prediction_only',
|
|
19
|
+
boundaryEnforcement: 'prediction_only',
|
|
20
|
+
};
|
|
11
21
|
if (result.capabilityRequests?.length) {
|
|
12
22
|
fields.capabilityRequestCount = result.capabilityRequests.length;
|
|
13
23
|
fields.capabilityActions = result.capabilityRequests.map((request) => request.action);
|
|
@@ -19,6 +29,18 @@ export function capabilityDecisionAuditFields(result) {
|
|
|
19
29
|
fields.policyMatchedRule = result.authorizationDecision.matchedRule;
|
|
20
30
|
}
|
|
21
31
|
}
|
|
32
|
+
const decisions = result.effectPlanPolicyDecisions ?? [];
|
|
33
|
+
if (decisions.length > 0 &&
|
|
34
|
+
decisions.every((decision) => decision.matchedRule === 'boundary.verified')) {
|
|
35
|
+
fields.authorizationMode = 'boundary_policy';
|
|
36
|
+
}
|
|
37
|
+
else if (decisions.length > 0 &&
|
|
38
|
+
decisions.every((decision) => decision.matchedRule === 'grant.exact')) {
|
|
39
|
+
fields.authorizationMode = 'exact_grant';
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
fields.authorizationMode = result.authorizationDecision ? 'policy_decision' : 'none';
|
|
43
|
+
}
|
|
22
44
|
if (result.boundaryProfile) {
|
|
23
45
|
fields.boundaryProfile = result.boundaryProfile;
|
|
24
46
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ClassifyResult } from '../types.js';
|
|
2
|
+
export declare function capabilityGrantLeaseRequired(result: Pick<ClassifyResult, 'reason' | 'authorizationDecision'>): boolean;
|
|
3
|
+
/** Fail closed when a grant lease is required but capability requests are missing. */
|
|
4
|
+
export declare function canConsumeCapabilityGrantLease(result: ClassifyResult): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function capabilityGrantLeaseRequired(result) {
|
|
2
|
+
return (result.reason === 'capability_grant' ||
|
|
3
|
+
result.authorizationDecision?.matchedRule === 'grant.exact');
|
|
4
|
+
}
|
|
5
|
+
/** Fail closed when a grant lease is required but capability requests are missing. */
|
|
6
|
+
export function canConsumeCapabilityGrantLease(result) {
|
|
7
|
+
if (!capabilityGrantLeaseRequired(result)) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
return (result.capabilityRequests?.length ?? 0) > 0;
|
|
11
|
+
}
|
|
@@ -1,9 +1,44 @@
|
|
|
1
|
-
import type { ApprovalStateFile } from '../types.js';
|
|
1
|
+
import type { ApprovalRecord, ApprovalStateFile } from '../types.js';
|
|
2
2
|
import type { CapabilityGrantV1 } from './grant.js';
|
|
3
3
|
import type { CapabilityRequestV1 } from './request.js';
|
|
4
|
+
export type GrantBundleValidationFailureReason = 'legacy_record' | 'empty_requests' | 'cardinality_mismatch' | 'grant_scope_too_broad' | 'grant_inactive' | 'grant_mismatch';
|
|
5
|
+
export type GrantBundleValidationResult = {
|
|
6
|
+
ok: true;
|
|
7
|
+
approval: ApprovalRecord;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
reason: GrantBundleValidationFailureReason;
|
|
11
|
+
};
|
|
12
|
+
export declare function grantsFromApproval(approval: {
|
|
13
|
+
grant?: CapabilityGrantV1;
|
|
14
|
+
grants?: CapabilityGrantV1[];
|
|
15
|
+
}): CapabilityGrantV1[];
|
|
16
|
+
export declare function approvalGrantBundleExhausted(approval: {
|
|
17
|
+
grant?: CapabilityGrantV1;
|
|
18
|
+
grants?: CapabilityGrantV1[];
|
|
19
|
+
}): boolean;
|
|
20
|
+
export declare function consumeApprovalGrantBundle(approval: ApprovalRecord, requests: readonly CapabilityRequestV1[]): {
|
|
21
|
+
approval: ApprovalRecord;
|
|
22
|
+
consumed: boolean;
|
|
23
|
+
};
|
|
24
|
+
/** Validate and consume a marked exact bundle without mutating on any failure. */
|
|
25
|
+
export declare function validateAndConsumeGrantBundle(approval: ApprovalRecord, requests: readonly CapabilityRequestV1[], now?: number): GrantBundleValidationResult;
|
|
26
|
+
/** Revalidate a marked bundle during an active replay lease without consuming it twice. */
|
|
27
|
+
export declare function validateGrantBundleForLeaseReuse(approval: ApprovalRecord, requests: readonly CapabilityRequestV1[], now?: number): GrantBundleValidationResult;
|
|
28
|
+
/** Consume every grant in an approved record after fingerprint/hash replay validation. */
|
|
29
|
+
export declare function consumeApprovedRecordGrantBundle(approval: ApprovalRecord): {
|
|
30
|
+
approval: ApprovalRecord;
|
|
31
|
+
consumed: boolean;
|
|
32
|
+
};
|
|
33
|
+
export declare function decrementApprovalLegacyGrant(approval: ApprovalRecord): ApprovalRecord;
|
|
4
34
|
export declare function grantsFromApprovedState(state: ApprovalStateFile, repoRoot: string): CapabilityGrantV1[];
|
|
5
35
|
export declare function consumeGrantLease(state: ApprovalStateFile, grantId: string): {
|
|
6
36
|
state: ApprovalStateFile;
|
|
7
37
|
consumed: boolean;
|
|
8
38
|
};
|
|
39
|
+
/** Consume one lease for each request; all must succeed (atomic bundle consumption). */
|
|
40
|
+
export declare function consumeGrantLeasesForRequests(state: ApprovalStateFile, requests: CapabilityRequestV1[]): {
|
|
41
|
+
state: ApprovalStateFile;
|
|
42
|
+
consumed: boolean;
|
|
43
|
+
};
|
|
9
44
|
export declare function findMatchingGrant(grants: CapabilityGrantV1[] | undefined, request: CapabilityRequestV1): CapabilityGrantV1 | undefined;
|