@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.
Files changed (111) hide show
  1. package/README.md +60 -10
  2. package/dist/adapters/shared/gate-runtime.d.ts +5 -0
  3. package/dist/adapters/shared/gate-runtime.js +237 -33
  4. package/dist/bundle/claude-runtime.mjs +4082 -1230
  5. package/dist/bundle/codex-runtime.mjs +4057 -1205
  6. package/dist/bundle/cursor-runtime.mjs +4057 -1205
  7. package/dist/commands/classify-for-report.js +5 -0
  8. package/dist/commands/doctor.js +90 -27
  9. package/dist/commands/explain.js +3 -2
  10. package/dist/commands/metrics.js +75 -4
  11. package/dist/commands/quality.js +2 -6
  12. package/dist/commands/recovery-checkpoints.d.ts +8 -1
  13. package/dist/commands/recovery-checkpoints.js +44 -11
  14. package/dist/commands/session.d.ts +5 -0
  15. package/dist/commands/session.js +8 -1
  16. package/dist/commands/status.js +1 -1
  17. package/dist/config-io.js +2 -2
  18. package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
  19. package/dist/conformance/contained-execution-guarantee.js +121 -0
  20. package/dist/conformance/guarantee-table.js +7 -0
  21. package/dist/core/audit-io.js +8 -0
  22. package/dist/core/audit-metrics.d.ts +34 -0
  23. package/dist/core/audit-metrics.js +87 -15
  24. package/dist/core/audit-recovery-metrics.d.ts +33 -0
  25. package/dist/core/audit-recovery-metrics.js +146 -0
  26. package/dist/core/audit-types.d.ts +7 -1
  27. package/dist/core/audit-types.js +1 -1
  28. package/dist/core/bounded-output.d.ts +11 -0
  29. package/dist/core/bounded-output.js +26 -0
  30. package/dist/core/capability/attestation.d.ts +56 -0
  31. package/dist/core/capability/attestation.js +110 -3
  32. package/dist/core/capability/boundary-session.d.ts +5 -0
  33. package/dist/core/capability/boundary-session.js +38 -1
  34. package/dist/core/capability/index.d.ts +1 -1
  35. package/dist/core/capability/index.js +1 -1
  36. package/dist/core/config.d.ts +18 -0
  37. package/dist/core/config.js +89 -10
  38. package/dist/core/contained-execution/docker-policy.d.ts +18 -0
  39. package/dist/core/contained-execution/docker-policy.js +97 -0
  40. package/dist/core/contained-execution/docker.d.ts +223 -0
  41. package/dist/core/contained-execution/docker.js +846 -0
  42. package/dist/core/contained-execution/eligibility.d.ts +9 -0
  43. package/dist/core/contained-execution/eligibility.js +103 -0
  44. package/dist/core/contained-execution/failure.d.ts +18 -0
  45. package/dist/core/contained-execution/failure.js +105 -0
  46. package/dist/core/contained-execution/mirror.d.ts +54 -0
  47. package/dist/core/contained-execution/mirror.js +587 -0
  48. package/dist/core/contained-execution/policy.d.ts +10 -0
  49. package/dist/core/contained-execution/policy.js +20 -0
  50. package/dist/core/effect-ir/index.d.ts +1 -1
  51. package/dist/core/effect-ir/index.js +1 -1
  52. package/dist/core/effect-ir/normalize.d.ts +5 -0
  53. package/dist/core/effect-ir/normalize.js +17 -7
  54. package/dist/core/effect-ir/shell-build.js +2 -14
  55. package/dist/core/effect-ir/shell-lower.js +100 -25
  56. package/dist/core/gate-contract.d.ts +4 -1
  57. package/dist/core/gate-contract.js +2 -0
  58. package/dist/core/process-runner.d.ts +10 -3
  59. package/dist/core/process-runner.js +66 -10
  60. package/dist/core/recovery/checkpoint.d.ts +3 -0
  61. package/dist/core/recovery/checkpoint.js +33 -4
  62. package/dist/core/recovery/types.d.ts +1 -0
  63. package/dist/core/scrub.d.ts +17 -0
  64. package/dist/core/scrub.js +499 -5
  65. package/dist/core/standing-allow.d.ts +0 -20
  66. package/dist/core/standing-allow.js +0 -71
  67. package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
  68. package/dist/core/transactional/apply-observed-changes.js +11 -0
  69. package/dist/core/transactional/backend-selector.d.ts +2 -1
  70. package/dist/core/transactional/backend-selector.js +32 -29
  71. package/dist/core/transactional/backend.d.ts +3 -0
  72. package/dist/core/transactional/file-checkpoint-backend.js +242 -54
  73. package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
  74. package/dist/core/transactional/file-checkpoint-git.js +53 -0
  75. package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
  76. package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
  77. package/dist/core/transactional/git-worktree-backend.js +3 -0
  78. package/dist/core/transactional/git-worktree.d.ts +1 -0
  79. package/dist/core/transactional/git-worktree.js +1 -0
  80. package/dist/core/transactional/runner.js +59 -13
  81. package/dist/core/types.d.ts +13 -0
  82. package/dist/core/verdict/parser.d.ts +10 -0
  83. package/dist/core/verdict/parser.js +271 -40
  84. package/dist/core/verdict/shell-semantics.js +21 -0
  85. package/dist/corpus/adversarial-probe.d.ts +1 -1
  86. package/dist/corpus/adversarial-probe.js +3 -3
  87. package/dist/corpus/benign-probe-cores.d.ts +6 -0
  88. package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
  89. package/dist/corpus/coverage-compare.d.ts +36 -0
  90. package/dist/corpus/coverage-compare.js +197 -0
  91. package/dist/corpus/coverage-contexts.d.ts +17 -0
  92. package/dist/corpus/coverage-contexts.js +88 -0
  93. package/dist/corpus/coverage-matrix.d.ts +37 -0
  94. package/dist/corpus/coverage-matrix.js +159 -0
  95. package/dist/corpus/coverage-probe.d.ts +91 -0
  96. package/dist/corpus/coverage-probe.js +341 -0
  97. package/dist/corpus/evaluate.d.ts +1 -1
  98. package/dist/corpus/evaluate.js +1 -1
  99. package/dist/corpus/structural-fixture-root.d.ts +2 -0
  100. package/dist/corpus/structural-fixture-root.js +5 -0
  101. package/dist/operational-insights.d.ts +3 -0
  102. package/dist/operational-insights.js +7 -3
  103. package/dist/runtime-provenance.d.ts +8 -0
  104. package/dist/runtime-provenance.js +34 -0
  105. package/dist/types.d.ts +3 -0
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/package.json +3 -1
  109. package/skills/belay/SKILL.md +19 -0
  110. package/skills/belay/belay-status.md +5 -0
  111. 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 = 3;
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 {
@@ -1,2 +1,2 @@
1
- export const AUDIT_METRICS_SCHEMA_VERSION = 3;
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 || typeof value !== 'object') {
18
+ if (!isRecord(value)) {
11
19
  return false;
12
20
  }
13
21
  const record = value;
14
22
  if (record.version !== BOUNDARY_ATTESTATION_VERSION) {
15
23
  return false;
16
24
  }
17
- if (!KNOWN_DRIVERS.has(record.driver)) {
25
+ if (!isBoundaryDriver(record.driver)) {
18
26
  return false;
19
27
  }
20
28
  if (typeof record.probedAt !== 'string' || typeof record.expiresAt !== 'string') {
@@ -34,6 +42,12 @@ export function validateBoundaryAttestation(value) {
34
42
  typeof record.isolatesWorkspaceMounts !== 'boolean') {
35
43
  return false;
36
44
  }
45
+ if (record.containedExecution !== undefined) {
46
+ if (record.driver !== 'container' ||
47
+ !validateContainedExecutionAttestation(record.containedExecution)) {
48
+ return false;
49
+ }
50
+ }
37
51
  if (record.driver === 'host-integration' && record.materializesGrants) {
38
52
  return false;
39
53
  }
@@ -44,7 +58,94 @@ export function validateBoundaryAttestation(value) {
44
58
  }
45
59
  return true;
46
60
  }
47
- export function isAttestationFresh(attestation, now = Date.now()) {
61
+ function hasPositiveLimit(value) {
62
+ return typeof value === 'number' && Number.isFinite(value) && value > 0;
63
+ }
64
+ function hasPositiveIntegerLimit(value) {
65
+ return hasPositiveLimit(value) && Number.isSafeInteger(value);
66
+ }
67
+ export function validateContainedExecutionAttestation(value) {
68
+ if (!isRecord(value)) {
69
+ return false;
70
+ }
71
+ const record = value;
72
+ const dockerSubstrate = record.dockerSubstrate;
73
+ const dockerConfiguration = record.dockerConfiguration;
74
+ if (record.version !== CONTAINED_EXECUTION_ATTESTATION_VERSION ||
75
+ typeof record.imageId !== 'string' ||
76
+ !/^sha256:[a-f0-9]{64}$/i.test(record.imageId) ||
77
+ typeof record.imageReference !== 'string' ||
78
+ !record.imageReference ||
79
+ /[\0\n\r]/.test(record.imageReference) ||
80
+ record.networkNone !== true ||
81
+ record.isolatesWorkspaceMirror !== true ||
82
+ record.readOnlyRoot !== true ||
83
+ record.sanitizedEnvironment !== true ||
84
+ !isRecord(dockerSubstrate) ||
85
+ typeof dockerSubstrate.binaryPath !== 'string' ||
86
+ !path.isAbsolute(dockerSubstrate.binaryPath) ||
87
+ /[\0\n\r]/.test(dockerSubstrate.binaryPath) ||
88
+ typeof dockerSubstrate.binarySha256 !== 'string' ||
89
+ !/^[a-f0-9]{64}$/.test(dockerSubstrate.binarySha256) ||
90
+ typeof dockerSubstrate.endpoint !== 'string' ||
91
+ !dockerSubstrate.endpoint.startsWith('unix:///') ||
92
+ !path.isAbsolute(dockerSubstrate.endpoint.slice('unix://'.length)) ||
93
+ /[\0\n\r]/.test(dockerSubstrate.endpoint) ||
94
+ typeof dockerSubstrate.daemonId !== 'string' ||
95
+ !dockerSubstrate.daemonId ||
96
+ /[\0\n\r]/.test(dockerSubstrate.daemonId) ||
97
+ !isRecord(dockerConfiguration) ||
98
+ typeof dockerConfiguration.executable !== 'string' ||
99
+ !path.isAbsolute(dockerConfiguration.executable) ||
100
+ /[\0\n\r]/.test(dockerConfiguration.executable) ||
101
+ typeof dockerConfiguration.host !== 'string' ||
102
+ !dockerConfiguration.host.startsWith('unix:///') ||
103
+ !path.isAbsolute(dockerConfiguration.host.slice('unix://'.length)) ||
104
+ /[\0\n\r]/.test(dockerConfiguration.host) ||
105
+ typeof record.user !== 'string' ||
106
+ !/^\d+:\d+$/.test(record.user) ||
107
+ record.entrypoint !== '/bin/sh' ||
108
+ record.capDropAll !== true ||
109
+ record.noNewPrivileges !== true ||
110
+ record.logDriver !== 'none' ||
111
+ record.proxyEnvironment !== 'neutralized-empty' ||
112
+ typeof record.probedAt !== 'string' ||
113
+ typeof record.expiresAt !== 'string') {
114
+ return false;
115
+ }
116
+ const probedAt = Date.parse(record.probedAt);
117
+ const expiresAt = Date.parse(record.expiresAt);
118
+ if (!Number.isFinite(probedAt) || !Number.isFinite(expiresAt) || expiresAt <= probedAt) {
119
+ return false;
120
+ }
121
+ const limits = record.resourceLimits;
122
+ const tmpfs = record.tmpfs;
123
+ return (isRecord(tmpfs) &&
124
+ tmpfs.path === '/tmp' &&
125
+ hasPositiveIntegerLimit(tmpfs.sizeBytes) &&
126
+ tmpfs.mode === 0o1777 &&
127
+ tmpfs.exec === false &&
128
+ tmpfs.nosuid === true &&
129
+ tmpfs.nodev === true &&
130
+ hasPositiveIntegerLimit(record.memorySwapMiB) &&
131
+ hasPositiveIntegerLimit(record.shmSizeMiB) &&
132
+ record.healthcheckDisabled === true &&
133
+ record.privateNamespaces === true &&
134
+ record.privileged === false &&
135
+ record.devicesNone === true &&
136
+ isRecord(limits) &&
137
+ hasPositiveIntegerLimit(limits.timeoutMs) &&
138
+ hasPositiveIntegerLimit(limits.memoryMiB) &&
139
+ hasPositiveLimit(limits.cpus) &&
140
+ hasPositiveIntegerLimit(limits.pids));
141
+ }
142
+ export function isContainedExecutionAttestationFresh(attestation, now = Date.now()) {
143
+ // A contained proof must have been observed already; no clock skew is accepted.
144
+ return (validateContainedExecutionAttestation(attestation) &&
145
+ Date.parse(attestation.probedAt) <= now &&
146
+ Date.parse(attestation.expiresAt) > now);
147
+ }
148
+ export function isAttestationCurrent(attestation, now = Date.now()) {
48
149
  const expires = Date.parse(attestation.expiresAt);
49
150
  if (!Number.isFinite(expires)) {
50
151
  return false;
@@ -52,6 +153,12 @@ export function isAttestationFresh(attestation, now = Date.now()) {
52
153
  if (expires <= now) {
53
154
  return false;
54
155
  }
156
+ return true;
157
+ }
158
+ export function isAttestationFresh(attestation, now = Date.now()) {
159
+ if (!isAttestationCurrent(attestation, now)) {
160
+ return false;
161
+ }
55
162
  if (attestation.driver === 'host-integration') {
56
163
  return true;
57
164
  }
@@ -1,4 +1,5 @@
1
1
  import type { BelayConfigV4 } from '../config.js';
2
+ import { type ContainedDockerDependencies } from '../contained-execution/docker.js';
2
3
  import type { BoundaryAttestation, BoundaryDriverId } from './attestation.js';
3
4
  import { type BoundaryDriver } from './boundary-driver.js';
4
5
  import { dockerNetworkArgs } from './boundary-egress.js';
@@ -15,6 +16,7 @@ export interface ResolvedBoundaryDriverContext {
15
16
  attestationPath: string;
16
17
  attestation: BoundaryAttestation | null;
17
18
  attestationFresh: boolean;
19
+ containedExecutionFresh?: boolean;
18
20
  }
19
21
  export declare function boundaryAttestationPath(repoRoot: string, config: BelayConfigV4): string;
20
22
  export declare function loadBoundaryAttestation(filePath: string, expectedRepoRoot?: string, controlPlaneDir?: string): Promise<BoundaryAttestation | null>;
@@ -38,6 +40,7 @@ export declare function startBoundarySession(params: {
38
40
  config: BelayConfigV4;
39
41
  driverId?: BoundaryDriverId;
40
42
  egressProxyRunning?: boolean;
43
+ containedDockerDependencies?: ContainedDockerDependencies;
41
44
  }): Promise<{
42
45
  attestation: BoundaryAttestation;
43
46
  attestationPath: string;
@@ -45,10 +48,12 @@ export declare function startBoundarySession(params: {
45
48
  export declare function boundarySessionStatus(params: {
46
49
  repoRoot: string;
47
50
  config: BelayConfigV4;
51
+ now?: number;
48
52
  }): Promise<{
49
53
  attestationPath: string;
50
54
  attestation: BoundaryAttestation | null;
51
55
  fresh: boolean;
56
+ containedExecutionFresh?: boolean;
52
57
  }>;
53
58
  export declare function runBoundaryAgentCommand(params: {
54
59
  repoRoot: string;
@@ -2,11 +2,34 @@ import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { egressStatus } from '../../services/egress-service.js';
4
4
  import { configuredControlPlaneDir } from '../config.js';
5
- import { isAttestationFresh } from './attestation.js';
5
+ import { probeContainedDockerForSession, } from '../contained-execution/docker.js';
6
+ import { isAttestationFresh, isContainedExecutionAttestationFresh } from './attestation.js';
6
7
  import { readSignedAttestationFile, signBoundaryAttestation, verifySignedBoundaryAttestation, } from './boundary-attestation-sign.js';
7
8
  import { getDefaultBoundaryDriver } from './boundary-driver.js';
8
9
  import { dockerNetworkArgs, isEgressProxyActive, resolveBoundaryEgressProxyEnv, } from './boundary-egress.js';
9
10
  import { runWithBoundaryRunnable, } from './boundary-run.js';
11
+ function containedExecutionFresh(attestation, config, now = Date.now()) {
12
+ if (attestation?.driver !== 'container' || !attestation.containedExecution)
13
+ return undefined;
14
+ const current = config.sandbox.containedExecution;
15
+ const capability = attestation.containedExecution;
16
+ const configCompatible = Boolean(current?.enabled &&
17
+ current.image === capability.imageReference &&
18
+ current.dockerExecutable === capability.dockerConfiguration.executable &&
19
+ current.dockerHost === capability.dockerConfiguration.host &&
20
+ current.timeoutMs === capability.resourceLimits.timeoutMs &&
21
+ current.memoryMiB === capability.resourceLimits.memoryMiB &&
22
+ current.cpus === capability.resourceLimits.cpus &&
23
+ current.pids === capability.resourceLimits.pids);
24
+ const probedAt = Date.parse(attestation.probedAt);
25
+ const expiresAt = Date.parse(attestation.expiresAt);
26
+ return (Number.isFinite(probedAt) &&
27
+ Number.isFinite(expiresAt) &&
28
+ probedAt <= now &&
29
+ expiresAt > now &&
30
+ configCompatible &&
31
+ isContainedExecutionAttestationFresh(capability, now));
32
+ }
10
33
  export function boundaryAttestationPath(repoRoot, config) {
11
34
  const rel = config.capability?.attestationRelPath ?? '.belay/attestation.json';
12
35
  return path.isAbsolute(rel) ? rel : path.join(repoRoot, rel);
@@ -92,6 +115,7 @@ export async function resolveBoundaryDriverContext(params) {
92
115
  attestationPath,
93
116
  attestation,
94
117
  attestationFresh: attestation ? isAttestationFresh(attestation) : false,
118
+ containedExecutionFresh: containedExecutionFresh(attestation, params.config),
95
119
  };
96
120
  }
97
121
  export async function runWithBoundaryDriver(params) {
@@ -104,6 +128,18 @@ export async function runWithBoundaryDriver(params) {
104
128
  });
105
129
  }
106
130
  export async function startBoundarySession(params) {
131
+ const contained = params.config.sandbox.containedExecution;
132
+ if (contained?.enabled === true) {
133
+ const attestation = await probeContainedDockerForSession({
134
+ repoRoot: params.repoRoot,
135
+ controlPlaneDir: configuredControlPlaneDir(params.config),
136
+ config: contained,
137
+ dependencies: params.containedDockerDependencies,
138
+ });
139
+ const attestationPath = boundaryAttestationPath(params.repoRoot, params.config);
140
+ await saveBoundaryAttestation(attestationPath, attestation, params.repoRoot, configuredControlPlaneDir(params.config));
141
+ return { attestation, attestationPath };
142
+ }
107
143
  const resolved = await resolveBoundaryDriverContext(params);
108
144
  const attestation = await resolved.driver.probe();
109
145
  if (resolved.driver.prepare) {
@@ -120,6 +156,7 @@ export async function boundarySessionStatus(params) {
120
156
  attestationPath,
121
157
  attestation,
122
158
  fresh: attestation ? isAttestationFresh(attestation) : false,
159
+ containedExecutionFresh: containedExecutionFresh(attestation, params.config, params.now),
123
160
  };
124
161
  }
125
162
  export async function runBoundaryAgentCommand(params) {
@@ -1,5 +1,5 @@
1
1
  export { addPathToAllowlist, allPathsAllowlisted, fsScopeAllowlistPath, isPathAllowlisted, loadFsScopeAllowlist, loadFsScopeAllowlistSync, saveFsScopeAllowlist, } from './allowlist.js';
2
- export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, type BoundaryAttestation, type BoundaryDriverId, isAttestationFresh, } from './attestation.js';
2
+ export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, type BoundaryAttestation, type BoundaryDriverId, CONTAINED_EXECUTION_ATTESTATION_VERSION, type ContainedExecutionAttestation, type ContainedExecutionResourceLimits, isAttestationFresh, isContainedExecutionAttestationFresh, validateContainedExecutionAttestation, } from './attestation.js';
3
3
  export { BOUNDARY_PROFILE_L1_ATTESTED, BOUNDARY_PROFILE_L3_L4_ONLY, BOUNDARY_PROFILE_L3_POLICY, boundaryVerifiedAllowEnabled, isAttestedBoundary, resolveBoundaryProfile, } from './boundary-profile.js';
4
4
  export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
5
5
  export { CAPABILITY_GRANT_VERSION, type CapabilityGrantV1, isGrantScopeTooBroad, } from './grant.js';
@@ -1,5 +1,5 @@
1
1
  export { addPathToAllowlist, allPathsAllowlisted, fsScopeAllowlistPath, isPathAllowlisted, loadFsScopeAllowlist, loadFsScopeAllowlistSync, saveFsScopeAllowlist, } from './allowlist.js';
2
- export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, isAttestationFresh, } from './attestation.js';
2
+ export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, CONTAINED_EXECUTION_ATTESTATION_VERSION, isAttestationFresh, isContainedExecutionAttestationFresh, validateContainedExecutionAttestation, } from './attestation.js';
3
3
  export { BOUNDARY_PROFILE_L1_ATTESTED, BOUNDARY_PROFILE_L3_L4_ONLY, BOUNDARY_PROFILE_L3_POLICY, boundaryVerifiedAllowEnabled, isAttestedBoundary, resolveBoundaryProfile, } from './boundary-profile.js';
4
4
  export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
5
5
  export { CAPABILITY_GRANT_VERSION, isGrantScopeTooBroad, } from './grant.js';
@@ -146,6 +146,20 @@ export interface BelaySandboxConfig {
146
146
  enabled: boolean;
147
147
  runtime: SandboxRuntime;
148
148
  denyNetworkByDefault: boolean;
149
+ /** Optional in source config for backwards compatibility; normalization always supplies it. */
150
+ containedExecution?: BelayContainedExecutionConfig;
151
+ }
152
+ export interface BelayContainedExecutionConfig {
153
+ enabled: boolean;
154
+ image: string | null;
155
+ /** Required when enabled; optional in source objects for disabled backwards compatibility. */
156
+ dockerExecutable?: string | null;
157
+ /** Required when enabled; only local absolute unix:// endpoints are accepted. */
158
+ dockerHost?: string | null;
159
+ timeoutMs: number;
160
+ memoryMiB: number;
161
+ cpus: number;
162
+ pids: number;
149
163
  }
150
164
  export interface BelayClassifierConfig {
151
165
  strictChains: boolean;
@@ -225,7 +239,9 @@ export declare const DEFAULT_REDACTION_V3: BelayRedactionConfig;
225
239
  export declare const DEFAULT_CONTROL_PLANE_ISOLATION_V3: BelayControlPlaneIsolationConfig;
226
240
  export declare const LEGACY_CONTROL_PLANE_V3: BelayControlPlaneConfig;
227
241
  export declare const DEFAULT_CONTROL_PLANE_V3: BelayControlPlaneConfig;
242
+ export declare const DEFAULT_CONTAINED_EXECUTION: BelayContainedExecutionConfig;
228
243
  export declare const DEFAULT_SANDBOX_V3: BelaySandboxConfig;
244
+ export declare function normalizeContainedExecutionConfig(raw: Partial<BelayContainedExecutionConfig> | undefined): BelayContainedExecutionConfig;
229
245
  export declare const DEFAULT_NOTIFICATIONS_V3: BelayNotificationsConfig;
230
246
  export declare const DEFAULT_APPROVAL_SIGNING_V3: BelayApprovalSigningConfig;
231
247
  export declare const DEFAULT_APPROVAL_CONFIG: BelayApprovalConfig;
@@ -276,6 +292,8 @@ export declare function normalizeConfigV2(config: BelayConfigV2): BelayConfigV2;
276
292
  export declare function normalizeConfig(config: BelayConfigV4): BelayConfigV4;
277
293
  export declare function normalizeConfig(config: BelayConfigV2): BelayConfigV2;
278
294
  export declare function isFreshConfigInput(loaded: unknown): boolean;
295
+ export declare function hasForbiddenShellOverrideLists(config: BelayConfigV4): boolean;
296
+ export declare function stripForbiddenShellOverrideLists(config: BelayConfigV4): BelayConfigV4;
279
297
  export declare function mergeConfig(existing: unknown, defaults?: BelayConfigV4): BelayConfigV4;
280
298
  export declare function scrubOptionsFromConfig(config: BelayConfigV4): ScrubOptions;
281
299
  export declare function classifierOptionsFromConfig(config: BelayConfigV4): ClassifierOptions;