@guilz-dev/belay 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +90 -27
- package/dist/commands/explain.js +3 -2
- package/dist/commands/metrics.js +75 -4
- package/dist/commands/quality.js +2 -6
- 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/commands/status.js +1 -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 +34 -0
- package/dist/core/audit-metrics.js +87 -15
- 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/operational-insights.d.ts +3 -0
- package/dist/operational-insights.js +7 -3
- package/dist/runtime-provenance.d.ts +8 -0
- package/dist/runtime-provenance.js +34 -0
- package/dist/types.d.ts +3 -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
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { GATE_EVENTS } from './audit-types.js';
|
|
2
|
+
const RESTORE_EVENTS = new Set(['recoveryApplied', 'recoveryConflict', 'recoveryRejected']);
|
|
3
|
+
const STABLE_FAILURE_REASON = /^[a-z][a-z0-9_]*$/;
|
|
4
|
+
function increment(bucket, key) {
|
|
5
|
+
bucket[key] = (bucket[key] ?? 0) + 1;
|
|
6
|
+
}
|
|
7
|
+
function percentile(sorted, p) {
|
|
8
|
+
if (sorted.length === 0) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const index = Math.ceil((p / 100) * sorted.length) - 1;
|
|
12
|
+
return sorted[Math.max(0, Math.min(sorted.length - 1, index))] ?? null;
|
|
13
|
+
}
|
|
14
|
+
export function sanitizeRecoveryFailureReason(record) {
|
|
15
|
+
const reason = record.transactionalSkipReason ?? record.reason;
|
|
16
|
+
if (typeof reason === 'string' && STABLE_FAILURE_REASON.test(reason)) {
|
|
17
|
+
return reason;
|
|
18
|
+
}
|
|
19
|
+
return 'transactional_execution_failed';
|
|
20
|
+
}
|
|
21
|
+
function isRecoverySnapshotRecord(record) {
|
|
22
|
+
if (typeof record.transactional === 'boolean') {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (typeof record.transactionalSkipReason === 'string') {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (record.recoveryFailClosed === true) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
function isRecoveryRestoreRecord(record) {
|
|
34
|
+
const event = typeof record.event === 'string' ? record.event : '';
|
|
35
|
+
return RESTORE_EVENTS.has(event);
|
|
36
|
+
}
|
|
37
|
+
function matchesCohort(record, cohort) {
|
|
38
|
+
return (record.runtimeBuildStamp === cohort.runtimeBuildStamp &&
|
|
39
|
+
record.configFingerprint === cohort.configFingerprint);
|
|
40
|
+
}
|
|
41
|
+
function emptySnapshotMetrics() {
|
|
42
|
+
return {
|
|
43
|
+
attempts: 0,
|
|
44
|
+
applied: 0,
|
|
45
|
+
skipped: 0,
|
|
46
|
+
byBackend: {},
|
|
47
|
+
byResourceKind: {},
|
|
48
|
+
prepareSampleCount: 0,
|
|
49
|
+
prepareMsP50: null,
|
|
50
|
+
prepareMsP95: null,
|
|
51
|
+
failuresByReason: {},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function emptyRestoreMetrics() {
|
|
55
|
+
return {
|
|
56
|
+
applied: 0,
|
|
57
|
+
conflict: 0,
|
|
58
|
+
rejected: 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function isRecoverySnapshotApplied(record) {
|
|
62
|
+
return (record.transactional === true &&
|
|
63
|
+
(record.reason === 'transactional_already_applied' ||
|
|
64
|
+
typeof record.recoveryCheckpointId === 'string' ||
|
|
65
|
+
record.recoveryState === 'applied'));
|
|
66
|
+
}
|
|
67
|
+
function computeSnapshotMetrics(records) {
|
|
68
|
+
const metrics = emptySnapshotMetrics();
|
|
69
|
+
const prepareSamples = [];
|
|
70
|
+
for (const record of records) {
|
|
71
|
+
const event = typeof record.event === 'string' ? record.event : '';
|
|
72
|
+
if (!GATE_EVENTS.has(event)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!isRecoverySnapshotRecord(record)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
metrics.attempts += 1;
|
|
79
|
+
if (isRecoverySnapshotApplied(record)) {
|
|
80
|
+
metrics.applied += 1;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
metrics.skipped += 1;
|
|
84
|
+
if (record.transactionalReason === 'transactional_observed_risk') {
|
|
85
|
+
increment(metrics.failuresByReason, 'transactional_observed_risk');
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
increment(metrics.failuresByReason, sanitizeRecoveryFailureReason(record));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const backend = typeof record.transactionalBackend === 'string' ? record.transactionalBackend : 'unknown';
|
|
92
|
+
increment(metrics.byBackend, backend);
|
|
93
|
+
const resourceKind = typeof record.resourceKind === 'string' ? record.resourceKind : 'unknown';
|
|
94
|
+
increment(metrics.byResourceKind, resourceKind);
|
|
95
|
+
if (typeof record.snapshotPrepareMs === 'number' && Number.isFinite(record.snapshotPrepareMs)) {
|
|
96
|
+
prepareSamples.push(record.snapshotPrepareMs);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
prepareSamples.sort((left, right) => left - right);
|
|
100
|
+
metrics.prepareSampleCount = prepareSamples.length;
|
|
101
|
+
metrics.prepareMsP50 = percentile(prepareSamples, 50);
|
|
102
|
+
metrics.prepareMsP95 = percentile(prepareSamples, 95);
|
|
103
|
+
return metrics;
|
|
104
|
+
}
|
|
105
|
+
function computeRestoreMetrics(records) {
|
|
106
|
+
const metrics = emptyRestoreMetrics();
|
|
107
|
+
for (const record of records) {
|
|
108
|
+
const event = typeof record.event === 'string' ? record.event : '';
|
|
109
|
+
if (event === 'recoveryApplied') {
|
|
110
|
+
metrics.applied += 1;
|
|
111
|
+
}
|
|
112
|
+
else if (event === 'recoveryConflict') {
|
|
113
|
+
metrics.conflict += 1;
|
|
114
|
+
}
|
|
115
|
+
else if (event === 'recoveryRejected') {
|
|
116
|
+
metrics.rejected += 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return metrics;
|
|
120
|
+
}
|
|
121
|
+
export function computeRecoveryMetrics(records, options = {}) {
|
|
122
|
+
const gateSnapshotRecords = records.filter((record) => {
|
|
123
|
+
const event = typeof record.event === 'string' ? record.event : '';
|
|
124
|
+
return GATE_EVENTS.has(event) && isRecoverySnapshotRecord(record);
|
|
125
|
+
});
|
|
126
|
+
const restoreRecords = records.filter(isRecoveryRestoreRecord);
|
|
127
|
+
const activeCohort = options.activeCohort ?? null;
|
|
128
|
+
const cohortGateSnapshotRecords = activeCohort
|
|
129
|
+
? gateSnapshotRecords.filter((record) => matchesCohort(record, activeCohort))
|
|
130
|
+
: [];
|
|
131
|
+
const cohortRestoreRecords = activeCohort
|
|
132
|
+
? restoreRecords.filter((record) => matchesCohort(record, activeCohort))
|
|
133
|
+
: [];
|
|
134
|
+
return {
|
|
135
|
+
allTime: {
|
|
136
|
+
snapshot: computeSnapshotMetrics(gateSnapshotRecords),
|
|
137
|
+
restore: computeRestoreMetrics(restoreRecords),
|
|
138
|
+
},
|
|
139
|
+
currentCohort: {
|
|
140
|
+
snapshot: computeSnapshotMetrics(cohortGateSnapshotRecords),
|
|
141
|
+
restore: computeRestoreMetrics(cohortRestoreRecords),
|
|
142
|
+
excludedSnapshotAttempts: gateSnapshotRecords.length - cohortGateSnapshotRecords.length,
|
|
143
|
+
excludedRestoreEvents: restoreRecords.length - cohortRestoreRecords.length,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AuditActionSnapshot, AuditReplayContext } from './audit-replay-context.js';
|
|
2
2
|
import type { Assessment } from './types.js';
|
|
3
3
|
export type { AuditActionSnapshot, AuditReplayContext } from './audit-replay-context.js';
|
|
4
|
-
export declare const AUDIT_METRICS_SCHEMA_VERSION =
|
|
4
|
+
export declare const AUDIT_METRICS_SCHEMA_VERSION = 4;
|
|
5
5
|
export declare const GATE_EVENTS: Set<string>;
|
|
6
6
|
export interface AuditRecord {
|
|
7
7
|
timestamp?: string;
|
|
@@ -42,6 +42,12 @@ export interface AuditRecord {
|
|
|
42
42
|
recoveryState?: string;
|
|
43
43
|
replayContext?: AuditReplayContext;
|
|
44
44
|
actionSnapshot?: AuditActionSnapshot;
|
|
45
|
+
wouldMediate?: boolean;
|
|
46
|
+
receiptHash?: string;
|
|
47
|
+
imageId?: string;
|
|
48
|
+
mirrorBackend?: 'file_copy';
|
|
49
|
+
exitCode?: number | null;
|
|
50
|
+
timedOut?: boolean;
|
|
45
51
|
[key: string]: unknown;
|
|
46
52
|
}
|
|
47
53
|
export interface AuditFilter {
|
package/dist/core/audit-types.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const AUDIT_METRICS_SCHEMA_VERSION =
|
|
1
|
+
export const AUDIT_METRICS_SCHEMA_VERSION = 4;
|
|
2
2
|
export const GATE_EVENTS = new Set(['beforeShellExecution', 'preToolUse', 'subagentGate']);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const OUTPUT_TAIL_LIMIT_BYTES = 16384;
|
|
2
|
+
export interface BoundedOutputTail {
|
|
3
|
+
tail: Buffer;
|
|
4
|
+
truncated: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function appendBoundedOutputTail(current: Buffer, chunk: Buffer | string, limitBytes?: number): BoundedOutputTail;
|
|
7
|
+
export declare function decodeBoundedOutputTail(tail: Buffer): string;
|
|
8
|
+
export declare function boundedUtf8Tail(value: string, limitBytes?: number): {
|
|
9
|
+
value: string;
|
|
10
|
+
truncated: boolean;
|
|
11
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const OUTPUT_TAIL_LIMIT_BYTES = 16_384;
|
|
2
|
+
export function appendBoundedOutputTail(current, chunk, limitBytes = OUTPUT_TAIL_LIMIT_BYTES) {
|
|
3
|
+
const next = Buffer.concat([current, Buffer.from(chunk)]);
|
|
4
|
+
return {
|
|
5
|
+
tail: next.length > limitBytes ? next.subarray(-limitBytes) : next,
|
|
6
|
+
truncated: next.length > limitBytes,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function decodeBoundedOutputTail(tail) {
|
|
10
|
+
for (let offset = 0; offset <= Math.min(3, tail.length); offset += 1) {
|
|
11
|
+
try {
|
|
12
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(tail.subarray(offset));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// A byte cap may have cut a leading multi-byte character; try its next boundary.
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return tail.toString('utf8');
|
|
19
|
+
}
|
|
20
|
+
export function boundedUtf8Tail(value, limitBytes = OUTPUT_TAIL_LIMIT_BYTES) {
|
|
21
|
+
const encoded = Buffer.from(value);
|
|
22
|
+
if (encoded.length <= limitBytes)
|
|
23
|
+
return { value, truncated: false };
|
|
24
|
+
const tail = encoded.subarray(-limitBytes);
|
|
25
|
+
return { value: decodeBoundedOutputTail(tail), truncated: true };
|
|
26
|
+
}
|
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
export declare const BOUNDARY_ATTESTATION_VERSION: 1;
|
|
2
|
+
export declare const CONTAINED_EXECUTION_ATTESTATION_VERSION: 1;
|
|
2
3
|
export type BoundaryDriverId = 'container' | 'cursor-sandbox' | 'seatbelt' | 'landlock' | 'host-integration';
|
|
4
|
+
export interface ContainedExecutionResourceLimits {
|
|
5
|
+
timeoutMs: number;
|
|
6
|
+
memoryMiB: number;
|
|
7
|
+
cpus: number;
|
|
8
|
+
pids: number;
|
|
9
|
+
}
|
|
10
|
+
export interface DockerSubstrateIdentity {
|
|
11
|
+
binaryPath: string;
|
|
12
|
+
binarySha256: string;
|
|
13
|
+
endpoint: string;
|
|
14
|
+
daemonId: string;
|
|
15
|
+
}
|
|
16
|
+
/** A separate execution-only capability; it does not attest L1 grant enforcement. */
|
|
17
|
+
export interface ContainedExecutionAttestation {
|
|
18
|
+
version: typeof CONTAINED_EXECUTION_ATTESTATION_VERSION;
|
|
19
|
+
imageId: string;
|
|
20
|
+
imageReference: string;
|
|
21
|
+
networkNone: true;
|
|
22
|
+
isolatesWorkspaceMirror: true;
|
|
23
|
+
readOnlyRoot: true;
|
|
24
|
+
sanitizedEnvironment: true;
|
|
25
|
+
dockerSubstrate: DockerSubstrateIdentity;
|
|
26
|
+
dockerConfiguration: {
|
|
27
|
+
executable: string;
|
|
28
|
+
host: string;
|
|
29
|
+
};
|
|
30
|
+
user: string;
|
|
31
|
+
entrypoint: '/bin/sh';
|
|
32
|
+
capDropAll: true;
|
|
33
|
+
noNewPrivileges: true;
|
|
34
|
+
logDriver: 'none';
|
|
35
|
+
proxyEnvironment: 'neutralized-empty';
|
|
36
|
+
tmpfs: {
|
|
37
|
+
path: '/tmp';
|
|
38
|
+
sizeBytes: number;
|
|
39
|
+
mode: 0o1777;
|
|
40
|
+
exec: false;
|
|
41
|
+
nosuid: true;
|
|
42
|
+
nodev: true;
|
|
43
|
+
};
|
|
44
|
+
memorySwapMiB: number;
|
|
45
|
+
shmSizeMiB: number;
|
|
46
|
+
healthcheckDisabled: true;
|
|
47
|
+
privateNamespaces: true;
|
|
48
|
+
privileged: false;
|
|
49
|
+
devicesNone: true;
|
|
50
|
+
resourceLimits: ContainedExecutionResourceLimits;
|
|
51
|
+
probedAt: string;
|
|
52
|
+
expiresAt: string;
|
|
53
|
+
}
|
|
3
54
|
export interface BoundaryAttestation {
|
|
4
55
|
version: typeof BOUNDARY_ATTESTATION_VERSION;
|
|
5
56
|
driver: BoundaryDriverId;
|
|
@@ -10,7 +61,12 @@ export interface BoundaryAttestation {
|
|
|
10
61
|
probeSignals: string[];
|
|
11
62
|
/** When true, the driver can mount an execution mirror at the original workspace path. */
|
|
12
63
|
isolatesWorkspaceMounts?: boolean;
|
|
64
|
+
/** Optional, execution-only proof. This must never be treated as an L1-full attestation. */
|
|
65
|
+
containedExecution?: ContainedExecutionAttestation;
|
|
13
66
|
}
|
|
14
67
|
export declare function validateBoundaryAttestation(value: unknown): value is BoundaryAttestation;
|
|
68
|
+
export declare function validateContainedExecutionAttestation(value: unknown): value is ContainedExecutionAttestation;
|
|
69
|
+
export declare function isContainedExecutionAttestationFresh(attestation: ContainedExecutionAttestation | null | undefined, now?: number): boolean;
|
|
70
|
+
export declare function isAttestationCurrent(attestation: BoundaryAttestation, now?: number): boolean;
|
|
15
71
|
export declare function isAttestationFresh(attestation: BoundaryAttestation, now?: number): boolean;
|
|
16
72
|
export declare function attestsWorkspaceMountIsolation(attestation: BoundaryAttestation | null | undefined): boolean;
|
|
@@ -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;
|