@guilz-dev/belay 0.8.1 → 0.9.1

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 (127) hide show
  1. package/README.md +67 -11
  2. package/dist/adapters/claude/runtime-entry.js +2 -0
  3. package/dist/adapters/codex/runtime-entry.js +2 -0
  4. package/dist/adapters/cursor/hooks.d.ts +5 -1
  5. package/dist/adapters/cursor/hooks.js +37 -2
  6. package/dist/adapters/cursor/runtime-entry.js +5 -0
  7. package/dist/adapters/shared/gate-runtime.d.ts +6 -0
  8. package/dist/adapters/shared/gate-runtime.js +260 -59
  9. package/dist/bundle/claude-runtime.mjs +7068 -3966
  10. package/dist/bundle/codex-runtime.mjs +7001 -3899
  11. package/dist/bundle/cursor-runtime.mjs +7004 -3899
  12. package/dist/commands/classify-for-report.js +5 -0
  13. package/dist/commands/doctor.js +88 -9
  14. package/dist/commands/explain.js +3 -2
  15. package/dist/commands/metrics.js +42 -0
  16. package/dist/commands/recovery-checkpoints.d.ts +8 -1
  17. package/dist/commands/recovery-checkpoints.js +44 -11
  18. package/dist/commands/session.d.ts +5 -0
  19. package/dist/commands/session.js +8 -1
  20. package/dist/config-io.js +2 -2
  21. package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
  22. package/dist/conformance/contained-execution-guarantee.js +121 -0
  23. package/dist/conformance/guarantee-table.js +7 -0
  24. package/dist/core/audit-analysis.js +7 -4
  25. package/dist/core/audit-io.d.ts +1 -0
  26. package/dist/core/audit-io.js +16 -6
  27. package/dist/core/audit-legacy-archive.d.ts +6 -0
  28. package/dist/core/audit-legacy-archive.js +51 -0
  29. package/dist/core/audit-metrics.d.ts +15 -0
  30. package/dist/core/audit-metrics.js +16 -2
  31. package/dist/core/audit-query.d.ts +2 -0
  32. package/dist/core/audit-query.js +33 -9
  33. package/dist/core/audit-recovery-metrics.d.ts +33 -0
  34. package/dist/core/audit-recovery-metrics.js +146 -0
  35. package/dist/core/audit-serialize.d.ts +9 -0
  36. package/dist/core/audit-serialize.js +179 -0
  37. package/dist/core/audit-types.d.ts +8 -1
  38. package/dist/core/audit-types.js +1 -1
  39. package/dist/core/bounded-output.d.ts +11 -0
  40. package/dist/core/bounded-output.js +26 -0
  41. package/dist/core/capability/attestation.d.ts +56 -0
  42. package/dist/core/capability/attestation.js +110 -3
  43. package/dist/core/capability/boundary-session.d.ts +5 -0
  44. package/dist/core/capability/boundary-session.js +38 -1
  45. package/dist/core/capability/index.d.ts +1 -1
  46. package/dist/core/capability/index.js +1 -1
  47. package/dist/core/capability/policy-engine.js +32 -13
  48. package/dist/core/config.d.ts +18 -0
  49. package/dist/core/config.js +89 -10
  50. package/dist/core/contained-execution/docker-policy.d.ts +18 -0
  51. package/dist/core/contained-execution/docker-policy.js +97 -0
  52. package/dist/core/contained-execution/docker.d.ts +223 -0
  53. package/dist/core/contained-execution/docker.js +846 -0
  54. package/dist/core/contained-execution/eligibility.d.ts +9 -0
  55. package/dist/core/contained-execution/eligibility.js +103 -0
  56. package/dist/core/contained-execution/failure.d.ts +18 -0
  57. package/dist/core/contained-execution/failure.js +105 -0
  58. package/dist/core/contained-execution/mirror.d.ts +54 -0
  59. package/dist/core/contained-execution/mirror.js +587 -0
  60. package/dist/core/contained-execution/policy.d.ts +10 -0
  61. package/dist/core/contained-execution/policy.js +20 -0
  62. package/dist/core/decision-config-fingerprint.d.ts +3 -0
  63. package/dist/core/decision-config-fingerprint.js +6 -0
  64. package/dist/core/effect-ir/index.d.ts +1 -1
  65. package/dist/core/effect-ir/index.js +1 -1
  66. package/dist/core/effect-ir/normalize.d.ts +5 -0
  67. package/dist/core/effect-ir/normalize.js +17 -7
  68. package/dist/core/effect-ir/shell-build.js +2 -14
  69. package/dist/core/effect-ir/shell-lower.js +103 -25
  70. package/dist/core/gate-contract.d.ts +4 -1
  71. package/dist/core/gate-contract.js +2 -0
  72. package/dist/core/process-runner.d.ts +10 -3
  73. package/dist/core/process-runner.js +66 -10
  74. package/dist/core/recovery/checkpoint.d.ts +3 -0
  75. package/dist/core/recovery/checkpoint.js +33 -4
  76. package/dist/core/recovery/types.d.ts +1 -0
  77. package/dist/core/scrub.d.ts +17 -0
  78. package/dist/core/scrub.js +499 -5
  79. package/dist/core/standing-allow.d.ts +0 -20
  80. package/dist/core/standing-allow.js +0 -71
  81. package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
  82. package/dist/core/transactional/apply-observed-changes.js +11 -0
  83. package/dist/core/transactional/backend-selector.d.ts +2 -1
  84. package/dist/core/transactional/backend-selector.js +32 -29
  85. package/dist/core/transactional/backend.d.ts +3 -0
  86. package/dist/core/transactional/file-checkpoint-backend.js +242 -54
  87. package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
  88. package/dist/core/transactional/file-checkpoint-git.js +53 -0
  89. package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
  90. package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
  91. package/dist/core/transactional/git-worktree-backend.js +3 -0
  92. package/dist/core/transactional/git-worktree.d.ts +1 -0
  93. package/dist/core/transactional/git-worktree.js +1 -0
  94. package/dist/core/transactional/runner.js +59 -13
  95. package/dist/core/types.d.ts +13 -0
  96. package/dist/core/verdict/egress-classify.js +1 -0
  97. package/dist/core/verdict/parser.d.ts +10 -0
  98. package/dist/core/verdict/parser.js +271 -40
  99. package/dist/core/verdict/shell-semantics.js +21 -0
  100. package/dist/corpus/adversarial-probe.d.ts +1 -1
  101. package/dist/corpus/adversarial-probe.js +3 -3
  102. package/dist/corpus/benign-probe-cores.d.ts +6 -0
  103. package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
  104. package/dist/corpus/coverage-compare.d.ts +36 -0
  105. package/dist/corpus/coverage-compare.js +197 -0
  106. package/dist/corpus/coverage-contexts.d.ts +17 -0
  107. package/dist/corpus/coverage-contexts.js +88 -0
  108. package/dist/corpus/coverage-matrix.d.ts +37 -0
  109. package/dist/corpus/coverage-matrix.js +159 -0
  110. package/dist/corpus/coverage-probe.d.ts +91 -0
  111. package/dist/corpus/coverage-probe.js +341 -0
  112. package/dist/corpus/evaluate.d.ts +1 -1
  113. package/dist/corpus/evaluate.js +1 -1
  114. package/dist/corpus/structural-fixture-root.d.ts +2 -0
  115. package/dist/corpus/structural-fixture-root.js +5 -0
  116. package/dist/defaults.js +0 -8
  117. package/dist/egress-daemon.js +20 -4
  118. package/dist/installer.js +3 -0
  119. package/dist/runtime-provenance.d.ts +10 -0
  120. package/dist/runtime-provenance.js +63 -0
  121. package/dist/templates.js +12 -7
  122. package/dist/version.d.ts +1 -1
  123. package/dist/version.js +1 -1
  124. package/package.json +4 -1
  125. package/skills/belay/SKILL.md +19 -0
  126. package/skills/belay/belay-status.md +5 -0
  127. package/dist/corpus/must-allow-commands.d.ts +0 -6
@@ -0,0 +1,179 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { appendFile, mkdir } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { scrubString, scrubValue } from './scrub.js';
5
+ export const AUDIT_SCHEMA_VERSION = 3;
6
+ const ISO8601_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
7
+ const HEX64_PATTERN = /^[a-f0-9]{64}$/;
8
+ const SCRUB_PLACEHOLDERS = new Set(['<timestamp>', '<high-entropy>', '<approval-id>', '<uuid>']);
9
+ const PRESERVED_HASH_FIELDS = new Set([
10
+ 'fingerprint',
11
+ 'commandFingerprint',
12
+ 'effectIRHash',
13
+ 'payloadHash',
14
+ 'configFingerprint',
15
+ 'runtimeArtifactHash',
16
+ 'decisionConfigFingerprint',
17
+ 'receiptHash',
18
+ ]);
19
+ const PRESERVED_LITERAL_FIELDS = new Set([
20
+ 'timestamp',
21
+ 'approvalCorrelationId',
22
+ 'runtimeVersion',
23
+ 'runtimeBuildStamp',
24
+ 'boundaryProfile',
25
+ 'schemaVersion',
26
+ 'imageId',
27
+ 'mirrorBackend',
28
+ 'wouldMediate',
29
+ 'exitCode',
30
+ 'timedOut',
31
+ ]);
32
+ const SCRUBBED_CONTAINER_FIELDS = new Set([
33
+ 'summary',
34
+ 'command',
35
+ 'payload',
36
+ 'replayContext',
37
+ 'actionSnapshot',
38
+ 'assessment',
39
+ 'predictedAssessment',
40
+ 'observedAssessment',
41
+ ]);
42
+ export function approvalCorrelationId(approvalId) {
43
+ return createHash('sha256').update(approvalId).digest('hex').slice(0, 16);
44
+ }
45
+ export function isValidApprovalCorrelationId(value) {
46
+ return /^[a-f0-9]{16}$/.test(value);
47
+ }
48
+ export function isValidAuditTimestamp(value) {
49
+ if (SCRUB_PLACEHOLDERS.has(value) || !ISO8601_PATTERN.test(value)) {
50
+ return false;
51
+ }
52
+ return !Number.isNaN(Date.parse(value));
53
+ }
54
+ export function isValidAuditFingerprint(value) {
55
+ return HEX64_PATTERN.test(value) && !SCRUB_PLACEHOLDERS.has(value);
56
+ }
57
+ function isValidPreservedHashField(field, value) {
58
+ if (field === 'receiptHash') {
59
+ return HEX64_PATTERN.test(value);
60
+ }
61
+ if (field === 'imageId') {
62
+ return /^sha256:[a-f0-9]{64}$/.test(value);
63
+ }
64
+ return isValidAuditFingerprint(value);
65
+ }
66
+ function scrubAuditContainer(value, options) {
67
+ return scrubValue(value, {
68
+ ...options,
69
+ maskHighEntropyStrings: true,
70
+ });
71
+ }
72
+ function serializeAuditField(key, value, options) {
73
+ if (value === undefined) {
74
+ return undefined;
75
+ }
76
+ if (key === 'approvalId') {
77
+ return undefined;
78
+ }
79
+ if (key === 'ts' && typeof value === 'string' && isValidAuditTimestamp(value)) {
80
+ return undefined;
81
+ }
82
+ if (PRESERVED_LITERAL_FIELDS.has(key)) {
83
+ if (key === 'timestamp' && typeof value === 'string' && isValidAuditTimestamp(value)) {
84
+ return value;
85
+ }
86
+ if (key === 'approvalCorrelationId' &&
87
+ typeof value === 'string' &&
88
+ isValidApprovalCorrelationId(value)) {
89
+ return value;
90
+ }
91
+ if ((key === 'runtimeVersion' || key === 'runtimeBuildStamp' || key === 'boundaryProfile') &&
92
+ typeof value === 'string' &&
93
+ value.length > 0) {
94
+ return value;
95
+ }
96
+ if (key === 'schemaVersion' && typeof value === 'number') {
97
+ return value;
98
+ }
99
+ if (key === 'mirrorBackend' && value === 'file_copy') {
100
+ return value;
101
+ }
102
+ if (key === 'wouldMediate' && value === true) {
103
+ return true;
104
+ }
105
+ if (key === 'exitCode' && (value === null || Number.isSafeInteger(value))) {
106
+ return value;
107
+ }
108
+ if (key === 'timedOut' && typeof value === 'boolean') {
109
+ return value;
110
+ }
111
+ if (key === 'imageId' && typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value)) {
112
+ return value;
113
+ }
114
+ return undefined;
115
+ }
116
+ if (PRESERVED_HASH_FIELDS.has(key) && typeof value === 'string') {
117
+ return isValidPreservedHashField(key, value) ? value : undefined;
118
+ }
119
+ if (SCRUBBED_CONTAINER_FIELDS.has(key)) {
120
+ return scrubAuditContainer(value, options);
121
+ }
122
+ if (typeof value === 'string') {
123
+ return scrubString(value, { ...options, maskHighEntropyStrings: true });
124
+ }
125
+ if (value !== null && typeof value === 'object') {
126
+ return scrubAuditContainer(value, options);
127
+ }
128
+ return value;
129
+ }
130
+ export function serializeAuditRecordV3(record, options) {
131
+ const timestamp = typeof record.timestamp === 'string' && isValidAuditTimestamp(record.timestamp)
132
+ ? record.timestamp
133
+ : typeof record.ts === 'string' && isValidAuditTimestamp(record.ts)
134
+ ? record.ts
135
+ : new Date().toISOString();
136
+ const serialized = {
137
+ schemaVersion: AUDIT_SCHEMA_VERSION,
138
+ timestamp,
139
+ };
140
+ if (typeof record.approvalId === 'string' && record.approvalId.length > 0) {
141
+ serialized.approvalCorrelationId = approvalCorrelationId(record.approvalId);
142
+ }
143
+ else if (typeof record.approvalCorrelationId === 'string' &&
144
+ isValidApprovalCorrelationId(record.approvalCorrelationId)) {
145
+ serialized.approvalCorrelationId = record.approvalCorrelationId;
146
+ }
147
+ for (const [key, value] of Object.entries(record)) {
148
+ if (key === 'timestamp' || key === 'ts' || key === 'approvalId' || key === 'schemaVersion') {
149
+ continue;
150
+ }
151
+ const next = serializeAuditField(key, value, options);
152
+ if (next !== undefined) {
153
+ serialized[key] = next;
154
+ }
155
+ }
156
+ return serialized;
157
+ }
158
+ export function parseAuditNdjsonLine(line) {
159
+ const trimmed = line.trim();
160
+ if (!trimmed) {
161
+ return null;
162
+ }
163
+ try {
164
+ const parsed = JSON.parse(trimmed);
165
+ if (typeof parsed.ts === 'string' && parsed.timestamp === undefined) {
166
+ parsed.timestamp = parsed.ts;
167
+ delete parsed.ts;
168
+ }
169
+ return parsed;
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ }
175
+ export async function appendAuditRecord(auditPath, record, options) {
176
+ await mkdir(path.dirname(auditPath), { recursive: true });
177
+ const line = JSON.stringify(serializeAuditRecordV3(record, options));
178
+ await appendFile(auditPath, `${line}\n`, 'utf8');
179
+ }
@@ -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 {
@@ -63,6 +69,7 @@ export interface ApprovalRoundTrip {
63
69
  approvalTimestamp?: string;
64
70
  executeTimestamp?: string;
65
71
  approvalId?: string;
72
+ approvalCorrelationId?: string;
66
73
  fingerprint: string;
67
74
  reason: string;
68
75
  summary: string;
@@ -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';
@@ -40,17 +40,11 @@ function effectPathIsLocal(target, context) {
40
40
  const resolved = resolveCapabilityPath(target, context.cwd);
41
41
  return (resolveWorkspaceRootMatch(context.repoRoot, [...(context.trustedWorkspaceRoots ?? [])], resolved) !== null);
42
42
  }
43
- function effectPathIsHighStakes(target, context) {
43
+ function effectPathIsSecretOrCredentialPath(target, context) {
44
44
  const resolved = resolveCapabilityPath(target, context.cwd);
45
45
  if (HIGH_STAKES_EFFECT_PATHS.some((pattern) => pattern.test(resolved))) {
46
46
  return true;
47
47
  }
48
- if (context.protectedArtifactRoots?.some((root) => pathWithinRoot(canonicalPath(root), resolved))) {
49
- return true;
50
- }
51
- if (isGitMetadataPath(resolved, context.repoRoot)) {
52
- return true;
53
- }
54
48
  const workspaceMatch = resolveWorkspaceRootMatch(context.repoRoot, [...(context.trustedWorkspaceRoots ?? [])], resolved);
55
49
  if (!workspaceMatch) {
56
50
  return false;
@@ -59,6 +53,24 @@ function effectPathIsHighStakes(target, context) {
59
53
  ...(context.sensitivePaths ?? []),
60
54
  ]);
61
55
  }
56
+ function effectPathIsControlPlaneArtifactPath(target, context) {
57
+ const resolved = resolveCapabilityPath(target, context.cwd);
58
+ return (context.protectedArtifactRoots?.some((root) => pathWithinRoot(canonicalPath(root), resolved)) ??
59
+ false);
60
+ }
61
+ function effectPathIsHighStakes(target, context) {
62
+ if (effectPathIsSecretOrCredentialPath(target, context)) {
63
+ return true;
64
+ }
65
+ if (effectPathIsControlPlaneArtifactPath(target, context)) {
66
+ return true;
67
+ }
68
+ const resolved = resolveCapabilityPath(target, context.cwd);
69
+ if (isGitMetadataPath(resolved, context.repoRoot)) {
70
+ return true;
71
+ }
72
+ return false;
73
+ }
62
74
  /**
63
75
  * Policy for a lowered shell effect. It intentionally ignores executable
64
76
  * command names, segment heads, command text, and config allowlists.
@@ -115,12 +127,13 @@ export function evaluateEffectRequirementPolicy(requirement, context) {
115
127
  }
116
128
  switch (requirement.action) {
117
129
  case 'fs.read':
118
- if (requirement.resource.kind === 'path' &&
119
- effectPathIsHighStakes(requirement.resource.path, context)) {
120
- return effectDecision('ask', 'high_stakes_path', 'effect.fs_read_high_stakes', [
121
- ...signals,
122
- 'sensitive_path_read',
123
- ]);
130
+ if (requirement.resource.kind === 'path') {
131
+ if (effectPathIsSecretOrCredentialPath(requirement.resource.path, context)) {
132
+ return effectDecision('ask', 'high_stakes_path', 'effect.fs_read_high_stakes', [
133
+ ...signals,
134
+ 'sensitive_path_read',
135
+ ]);
136
+ }
124
137
  }
125
138
  return effectDecision('allow', 'read_only', 'effect.fs_read', signals);
126
139
  case 'fs.write':
@@ -130,6 +143,12 @@ export function evaluateEffectRequirementPolicy(requirement, context) {
130
143
  if (requirement.resource.kind !== 'path') {
131
144
  return effectDecision('ask', 'indeterminate_effect', 'effect.fs_write_unknown', signals);
132
145
  }
146
+ if (effectPathIsControlPlaneArtifactPath(requirement.resource.path, context)) {
147
+ return effectDecision('ask', 'control_plane_mutation', 'effect.control_plane_write', [
148
+ ...signals,
149
+ 'control_plane_path',
150
+ ]);
151
+ }
133
152
  if (effectPathIsHighStakes(requirement.resource.path, context)) {
134
153
  return effectDecision('ask', 'high_stakes_path', 'effect.fs_write_high_stakes', [
135
154
  ...signals,