@guilz-dev/belay 0.3.0 → 0.6.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 (242) hide show
  1. package/README.md +7 -1
  2. package/dist/adapters/cursor/runtime-entry.js +1 -0
  3. package/dist/adapters/layouts/scope.js +3 -2
  4. package/dist/adapters/shared/gate-runtime.d.ts +12 -16
  5. package/dist/adapters/shared/gate-runtime.js +499 -43
  6. package/dist/bundle/claude-runtime.mjs +8004 -2676
  7. package/dist/bundle/codex-runtime.mjs +8021 -2683
  8. package/dist/bundle/cursor-runtime.mjs +7993 -2667
  9. package/dist/cli.js +180 -6
  10. package/dist/commands/approve.d.ts +1 -0
  11. package/dist/commands/approve.js +45 -26
  12. package/dist/commands/classify-for-report.js +1 -1
  13. package/dist/commands/config.d.ts +8 -0
  14. package/dist/commands/config.js +264 -58
  15. package/dist/commands/doctor.js +47 -2
  16. package/dist/commands/explain.js +17 -2
  17. package/dist/commands/harvest.d.ts +29 -0
  18. package/dist/commands/harvest.js +72 -0
  19. package/dist/commands/judge.d.ts +11 -1
  20. package/dist/commands/judge.js +24 -4
  21. package/dist/commands/metrics.js +34 -2
  22. package/dist/commands/quality.d.ts +38 -0
  23. package/dist/commands/quality.js +105 -0
  24. package/dist/commands/session.d.ts +20 -0
  25. package/dist/commands/session.js +51 -0
  26. package/dist/commands/simulate.d.ts +1 -0
  27. package/dist/commands/simulate.js +7 -2
  28. package/dist/commands/standing-allow.d.ts +10 -0
  29. package/dist/commands/standing-allow.js +26 -0
  30. package/dist/commands/tui.d.ts +48 -0
  31. package/dist/commands/tui.js +150 -0
  32. package/dist/config-io.js +59 -17
  33. package/dist/conformance/guarantee-posture.d.ts +21 -0
  34. package/dist/conformance/guarantee-posture.js +46 -0
  35. package/dist/conformance/guarantee-table.d.ts +1 -0
  36. package/dist/conformance/guarantee-table.js +14 -9
  37. package/dist/core/approval-replay-cli.d.ts +7 -0
  38. package/dist/core/approval-replay-cli.js +36 -0
  39. package/dist/core/approval-replay.d.ts +45 -0
  40. package/dist/core/approval-replay.js +141 -0
  41. package/dist/core/approval-service.d.ts +15 -0
  42. package/dist/core/approval-service.js +53 -4
  43. package/dist/core/approval.d.ts +26 -3
  44. package/dist/core/approval.js +61 -7
  45. package/dist/core/audit-analysis.d.ts +7 -1
  46. package/dist/core/audit-analysis.js +111 -0
  47. package/dist/core/audit-metrics.d.ts +7 -0
  48. package/dist/core/audit-metrics.js +24 -4
  49. package/dist/core/audit-query.d.ts +1 -0
  50. package/dist/core/audit-query.js +3 -0
  51. package/dist/core/audit-replay-context.d.ts +35 -0
  52. package/dist/core/audit-replay-context.js +88 -0
  53. package/dist/core/audit-types.d.ts +24 -1
  54. package/dist/core/audit-types.js +1 -1
  55. package/dist/core/capability/approval-state-mutation.d.ts +13 -0
  56. package/dist/core/capability/approval-state-mutation.js +92 -0
  57. package/dist/core/capability/approval-v3.d.ts +15 -0
  58. package/dist/core/capability/approval-v3.js +74 -0
  59. package/dist/core/capability/attestation.d.ts +13 -0
  60. package/dist/core/capability/attestation.js +55 -0
  61. package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
  62. package/dist/core/capability/boundary-attestation-sign.js +84 -0
  63. package/dist/core/capability/boundary-driver-container.d.ts +7 -0
  64. package/dist/core/capability/boundary-driver-container.js +122 -0
  65. package/dist/core/capability/boundary-driver.d.ts +29 -0
  66. package/dist/core/capability/boundary-driver.js +43 -0
  67. package/dist/core/capability/boundary-egress.d.ts +21 -0
  68. package/dist/core/capability/boundary-egress.js +109 -0
  69. package/dist/core/capability/boundary-grant-materialize.d.ts +13 -0
  70. package/dist/core/capability/boundary-grant-materialize.js +78 -0
  71. package/dist/core/capability/boundary-profile.d.ts +15 -0
  72. package/dist/core/capability/boundary-profile.js +25 -0
  73. package/dist/core/capability/boundary-run.d.ts +23 -0
  74. package/dist/core/capability/boundary-run.js +23 -0
  75. package/dist/core/capability/boundary-session.d.ts +57 -0
  76. package/dist/core/capability/boundary-session.js +129 -0
  77. package/dist/core/capability/capability-request-hash.d.ts +2 -0
  78. package/dist/core/capability/capability-request-hash.js +8 -0
  79. package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
  80. package/dist/core/capability/gate-latency-ratchet.js +18 -0
  81. package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
  82. package/dist/core/capability/gate-policy-shadow.js +108 -0
  83. package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
  84. package/dist/core/capability/gate-shadow-audit.js +1 -0
  85. package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
  86. package/dist/core/capability/gate-shadow-ratchet.js +81 -0
  87. package/dist/core/capability/grant-lease.d.ts +9 -0
  88. package/dist/core/capability/grant-lease.js +46 -0
  89. package/dist/core/capability/grant-loader.d.ts +12 -0
  90. package/dist/core/capability/grant-loader.js +23 -0
  91. package/dist/core/capability/grant-match.d.ts +6 -0
  92. package/dist/core/capability/grant-match.js +93 -0
  93. package/dist/core/capability/grant.d.ts +16 -0
  94. package/dist/core/capability/grant.js +16 -0
  95. package/dist/core/capability/index.d.ts +10 -1
  96. package/dist/core/capability/index.js +8 -0
  97. package/dist/core/capability/limits.d.ts +7 -0
  98. package/dist/core/capability/limits.js +55 -0
  99. package/dist/core/capability/paths.d.ts +2 -2
  100. package/dist/core/capability/paths.js +9 -9
  101. package/dist/core/capability/policy-bridge.d.ts +18 -0
  102. package/dist/core/capability/policy-bridge.js +51 -0
  103. package/dist/core/capability/policy-engine.d.ts +70 -0
  104. package/dist/core/capability/policy-engine.js +476 -0
  105. package/dist/core/capability/policy-types.d.ts +20 -0
  106. package/dist/core/capability/policy-types.js +1 -0
  107. package/dist/core/capability/request.d.ts +45 -0
  108. package/dist/core/capability/request.js +1 -0
  109. package/dist/core/capability/resolver.d.ts +4 -0
  110. package/dist/core/capability/resolver.js +4 -0
  111. package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
  112. package/dist/core/capability/trusted-workspace-roots.js +149 -0
  113. package/dist/core/capability/types.d.ts +11 -1
  114. package/dist/core/capability-approval.d.ts +2 -1
  115. package/dist/core/capability-approval.js +100 -2
  116. package/dist/core/classify-subagent.d.ts +2 -1
  117. package/dist/core/classify-subagent.js +40 -22
  118. package/dist/core/classify-tool.js +157 -64
  119. package/dist/core/config.d.ts +32 -3
  120. package/dist/core/config.js +105 -3
  121. package/dist/core/gate-contract.d.ts +3 -0
  122. package/dist/core/gate-contract.js +3 -0
  123. package/dist/core/gate-engine.js +12 -7
  124. package/dist/core/harvest.d.ts +53 -0
  125. package/dist/core/harvest.js +276 -0
  126. package/dist/core/index.d.ts +9 -3
  127. package/dist/core/index.js +6 -2
  128. package/dist/core/judge-doctor.d.ts +3 -0
  129. package/dist/core/judge-doctor.js +90 -11
  130. package/dist/core/judge-fallback-hints.d.ts +13 -0
  131. package/dist/core/judge-fallback-hints.js +108 -0
  132. package/dist/core/path-utils.d.ts +12 -0
  133. package/dist/core/path-utils.js +60 -7
  134. package/dist/core/reclassify.d.ts +3 -0
  135. package/dist/core/reclassify.js +44 -14
  136. package/dist/core/recovery/capability.d.ts +3 -0
  137. package/dist/core/recovery/capability.js +22 -0
  138. package/dist/core/recovery/fail-closed.d.ts +6 -0
  139. package/dist/core/recovery/fail-closed.js +25 -0
  140. package/dist/core/recovery/index.d.ts +3 -0
  141. package/dist/core/recovery/index.js +2 -0
  142. package/dist/core/recovery/types.d.ts +20 -0
  143. package/dist/core/recovery/types.js +1 -0
  144. package/dist/core/replay-scrub.d.ts +9 -0
  145. package/dist/core/replay-scrub.js +43 -0
  146. package/dist/core/shell-tokenizer.d.ts +2 -0
  147. package/dist/core/shell-tokenizer.js +82 -23
  148. package/dist/core/standing-allow.d.ts +50 -0
  149. package/dist/core/standing-allow.js +178 -0
  150. package/dist/core/transactional/diff-evaluator.js +4 -1
  151. package/dist/core/transactional/eligibility.js +4 -0
  152. package/dist/core/transactional/git-worktree.d.ts +10 -3
  153. package/dist/core/transactional/git-worktree.js +81 -11
  154. package/dist/core/transactional/reasons.d.ts +1 -0
  155. package/dist/core/transactional/reasons.js +4 -0
  156. package/dist/core/transactional/runner.js +22 -6
  157. package/dist/core/transactional/types.d.ts +4 -0
  158. package/dist/core/types.d.ts +36 -3
  159. package/dist/core/verdict/adapter.d.ts +3 -4
  160. package/dist/core/verdict/adapter.js +19 -6
  161. package/dist/core/verdict/containment.d.ts +4 -3
  162. package/dist/core/verdict/containment.js +17 -12
  163. package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
  164. package/dist/core/verdict/cursor-acp-client.js +345 -0
  165. package/dist/core/verdict/egress-classify.d.ts +2 -2
  166. package/dist/core/verdict/egress-classify.js +2 -2
  167. package/dist/core/verdict/git-classifier.d.ts +26 -0
  168. package/dist/core/verdict/git-classifier.js +533 -0
  169. package/dist/core/verdict/judge-audit.d.ts +1 -0
  170. package/dist/core/verdict/judge-audit.js +33 -1
  171. package/dist/core/verdict/judge-baseline.d.ts +20 -0
  172. package/dist/core/verdict/judge-baseline.js +49 -0
  173. package/dist/core/verdict/judge-broker-service.d.ts +42 -0
  174. package/dist/core/verdict/judge-broker-service.js +291 -0
  175. package/dist/core/verdict/judge-catalog.js +1 -0
  176. package/dist/core/verdict/judge-cli-fingerprint.d.ts +6 -0
  177. package/dist/core/verdict/judge-cli-fingerprint.js +67 -0
  178. package/dist/core/verdict/judge-cli.d.ts +23 -0
  179. package/dist/core/verdict/judge-cli.js +139 -16
  180. package/dist/core/verdict/judge-factory.js +13 -4
  181. package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
  182. package/dist/core/verdict/judge-provider-matrix.js +66 -0
  183. package/dist/core/verdict/judge-runtime-config.d.ts +44 -0
  184. package/dist/core/verdict/judge-runtime-config.js +96 -0
  185. package/dist/core/verdict/judge-session-broker.d.ts +48 -0
  186. package/dist/core/verdict/judge-session-broker.js +200 -0
  187. package/dist/core/verdict/judge-session-guard.d.ts +27 -0
  188. package/dist/core/verdict/judge-session-guard.js +91 -0
  189. package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
  190. package/dist/core/verdict/judge-session-kill-switch.js +43 -0
  191. package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
  192. package/dist/core/verdict/judge-session-mutex.js +23 -0
  193. package/dist/core/verdict/judge-shadow.d.ts +24 -0
  194. package/dist/core/verdict/judge-shadow.js +116 -0
  195. package/dist/core/verdict/judge-transport.d.ts +44 -0
  196. package/dist/core/verdict/judge-transport.js +268 -0
  197. package/dist/core/verdict/judge.d.ts +14 -14
  198. package/dist/core/verdict/judge.js +5 -67
  199. package/dist/core/verdict/launcher-resolve.d.ts +2 -0
  200. package/dist/core/verdict/launcher-resolve.js +16 -0
  201. package/dist/core/verdict/parser.js +6 -1
  202. package/dist/core/verdict/prescan.d.ts +15 -0
  203. package/dist/core/verdict/prescan.js +68 -0
  204. package/dist/core/verdict/shell-policy.d.ts +25 -0
  205. package/dist/core/verdict/shell-policy.js +40 -0
  206. package/dist/core/verdict/shell-semantics.d.ts +24 -0
  207. package/dist/core/verdict/shell-semantics.js +225 -0
  208. package/dist/core/verdict/types.d.ts +26 -1
  209. package/dist/core/verdict/verdict.js +300 -179
  210. package/dist/corpus/adversarial-probe.d.ts +86 -0
  211. package/dist/corpus/adversarial-probe.js +220 -0
  212. package/dist/corpus/evaluate.d.ts +45 -12
  213. package/dist/corpus/evaluate.js +61 -8
  214. package/dist/corpus/gate-latency-budget.d.ts +39 -0
  215. package/dist/corpus/gate-latency-budget.js +33 -0
  216. package/dist/corpus/gates.d.ts +35 -0
  217. package/dist/corpus/gates.js +81 -0
  218. package/dist/corpus/judge-accuracy.d.ts +8 -0
  219. package/dist/corpus/judge-accuracy.js +39 -0
  220. package/dist/corpus/must-allow-commands.d.ts +5 -0
  221. package/dist/corpus/must-allow-commands.js +13 -0
  222. package/dist/corpus/mutators.d.ts +28 -0
  223. package/dist/corpus/mutators.js +162 -0
  224. package/dist/corpus/ratchet.d.ts +42 -0
  225. package/dist/corpus/ratchet.js +116 -0
  226. package/dist/corpus/runtime-match.d.ts +19 -0
  227. package/dist/corpus/runtime-match.js +43 -0
  228. package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
  229. package/dist/corpus/standing-allow-catalog.generated.js +99 -0
  230. package/dist/corpus/types.d.ts +47 -0
  231. package/dist/corpus/types.js +157 -0
  232. package/dist/installer.js +4 -1
  233. package/dist/judge-broker-daemon.d.ts +1 -0
  234. package/dist/judge-broker-daemon.js +137 -0
  235. package/dist/services/sandbox-service.d.ts +5 -0
  236. package/dist/services/sandbox-service.js +30 -2
  237. package/dist/types.d.ts +2 -2
  238. package/dist/version.d.ts +1 -1
  239. package/dist/version.js +1 -1
  240. package/package.json +7 -3
  241. package/skills/belay/SKILL.md +7 -1
  242. package/skills/belay/belay-approve.md +17 -0
@@ -0,0 +1,74 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { hashCapabilityRequests } from './capability-request-hash.js';
3
+ import { CAPABILITY_GRANT_VERSION } from './grant.js';
4
+ export const APPROVAL_STATE_VERSION_V3 = 3;
5
+ export function normalizeApprovalStateVersion(state) {
6
+ const version = state.version === APPROVAL_STATE_VERSION_V3
7
+ ? APPROVAL_STATE_VERSION_V3
8
+ : state.version === 2
9
+ ? 2
10
+ : 1;
11
+ return { ...state, version };
12
+ }
13
+ export function upgradeApprovalStateToV3(state) {
14
+ const normalized = normalizeApprovalStateVersion(state);
15
+ if (normalized.version === APPROVAL_STATE_VERSION_V3) {
16
+ return normalized;
17
+ }
18
+ return {
19
+ version: APPROVAL_STATE_VERSION_V3,
20
+ approvals: normalized.approvals.map((approval) => ({
21
+ ...approval,
22
+ capabilityRequestHash: approval.capabilityRequestHash ??
23
+ (approval.capabilityRequests?.length
24
+ ? hashCapabilityRequests(approval.capabilityRequests)
25
+ : undefined),
26
+ })),
27
+ };
28
+ }
29
+ export function attachCapabilityEnvelope(approval, capabilityRequests) {
30
+ if (!capabilityRequests?.length) {
31
+ return approval;
32
+ }
33
+ return {
34
+ ...approval,
35
+ capabilityRequests,
36
+ capabilityRequestHash: hashCapabilityRequests(capabilityRequests),
37
+ };
38
+ }
39
+ export function mintCapabilityGrant(params) {
40
+ const request = params.capabilityRequests[0];
41
+ if (!request) {
42
+ return null;
43
+ }
44
+ const expiresAt = params.approval.expiresAt;
45
+ return {
46
+ version: CAPABILITY_GRANT_VERSION,
47
+ grantId: `grant_${params.approval.approvalId}`,
48
+ principal: request.principal,
49
+ action: request.action,
50
+ resource: request.resource,
51
+ inputFingerprint: request.context.inputFingerprint,
52
+ issuedAt: params.approval.approvedAt ?? new Date().toISOString(),
53
+ expiresAt,
54
+ maxUses: params.maxUses ?? 1,
55
+ usesRemaining: params.maxUses ?? 1,
56
+ issuer: params.issuer ?? 'belay-approval-v3',
57
+ };
58
+ }
59
+ export function mintGrantForApprovedRecord(approval) {
60
+ if (approval.grant || !approval.capabilityRequests?.length) {
61
+ return approval;
62
+ }
63
+ const grant = mintCapabilityGrant({
64
+ approval,
65
+ capabilityRequests: approval.capabilityRequests,
66
+ });
67
+ if (!grant) {
68
+ return approval;
69
+ }
70
+ return { ...approval, grant };
71
+ }
72
+ export function newGrantId() {
73
+ return `grant_${randomUUID().replaceAll('-', '').slice(0, 16)}`;
74
+ }
@@ -0,0 +1,13 @@
1
+ export declare const BOUNDARY_ATTESTATION_VERSION: 1;
2
+ export type BoundaryDriverId = 'container' | 'cursor-sandbox' | 'seatbelt' | 'landlock' | 'host-integration';
3
+ export interface BoundaryAttestation {
4
+ version: typeof BOUNDARY_ATTESTATION_VERSION;
5
+ driver: BoundaryDriverId;
6
+ probedAt: string;
7
+ expiresAt: string;
8
+ deniesUngrantedEffects: boolean;
9
+ materializesGrants: boolean;
10
+ probeSignals: string[];
11
+ }
12
+ export declare function validateBoundaryAttestation(value: unknown): value is BoundaryAttestation;
13
+ export declare function isAttestationFresh(attestation: BoundaryAttestation, now?: number): boolean;
@@ -0,0 +1,55 @@
1
+ export const BOUNDARY_ATTESTATION_VERSION = 1;
2
+ const KNOWN_DRIVERS = new Set([
3
+ 'container',
4
+ 'cursor-sandbox',
5
+ 'seatbelt',
6
+ 'landlock',
7
+ 'host-integration',
8
+ ]);
9
+ export function validateBoundaryAttestation(value) {
10
+ if (!value || typeof value !== 'object') {
11
+ return false;
12
+ }
13
+ const record = value;
14
+ if (record.version !== BOUNDARY_ATTESTATION_VERSION) {
15
+ return false;
16
+ }
17
+ if (!KNOWN_DRIVERS.has(record.driver)) {
18
+ return false;
19
+ }
20
+ if (typeof record.probedAt !== 'string' || typeof record.expiresAt !== 'string') {
21
+ return false;
22
+ }
23
+ if (typeof record.deniesUngrantedEffects !== 'boolean') {
24
+ return false;
25
+ }
26
+ if (typeof record.materializesGrants !== 'boolean') {
27
+ return false;
28
+ }
29
+ if (!Array.isArray(record.probeSignals) ||
30
+ !record.probeSignals.every((s) => typeof s === 'string')) {
31
+ return false;
32
+ }
33
+ if (record.driver === 'host-integration' && record.materializesGrants) {
34
+ return false;
35
+ }
36
+ if (record.driver === 'container' &&
37
+ record.materializesGrants &&
38
+ !record.deniesUngrantedEffects) {
39
+ return false;
40
+ }
41
+ return true;
42
+ }
43
+ export function isAttestationFresh(attestation, now = Date.now()) {
44
+ const expires = Date.parse(attestation.expiresAt);
45
+ if (!Number.isFinite(expires)) {
46
+ return false;
47
+ }
48
+ if (expires <= now) {
49
+ return false;
50
+ }
51
+ if (attestation.driver === 'host-integration') {
52
+ return true;
53
+ }
54
+ return attestation.deniesUngrantedEffects;
55
+ }
@@ -0,0 +1,21 @@
1
+ import type { BoundaryAttestation } from './attestation.js';
2
+ export type BoundaryAttestationFileFormat = 'signed' | 'legacy' | 'missing' | 'invalid';
3
+ export interface SignedBoundaryAttestationFile {
4
+ version: 1;
5
+ repoRoot: string;
6
+ attestation: BoundaryAttestation;
7
+ signature: string;
8
+ }
9
+ export declare function signBoundaryAttestation(params: {
10
+ repoRoot: string;
11
+ attestation: BoundaryAttestation;
12
+ controlPlaneDir: string;
13
+ }): Promise<SignedBoundaryAttestationFile>;
14
+ export declare function verifySignedBoundaryAttestation(params: {
15
+ file: unknown;
16
+ expectedRepoRoot: string;
17
+ controlPlaneDir: string;
18
+ }): Promise<BoundaryAttestation | null>;
19
+ export declare function readSignedAttestationFile(filePath: string): Promise<unknown>;
20
+ export declare function isLegacyAttestationFormat(file: unknown): file is BoundaryAttestation;
21
+ export declare function inspectBoundaryAttestationFile(filePath: string): Promise<BoundaryAttestationFileFormat>;
@@ -0,0 +1,84 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ import { access, readFile } from 'node:fs/promises';
3
+ import { loadOrCreateApprovalSigningKey } from '../approval-token.js';
4
+ import { canonicalStringify } from '../fingerprint.js';
5
+ import { validateBoundaryAttestation } from './attestation.js';
6
+ function signBody(repoRoot, attestation, key) {
7
+ const payload = canonicalStringify({ repoRoot, attestation });
8
+ return createHmac('sha256', key).update(payload).digest('base64url');
9
+ }
10
+ export async function signBoundaryAttestation(params) {
11
+ const key = await loadOrCreateApprovalSigningKey(params.controlPlaneDir);
12
+ const signature = signBody(params.repoRoot, params.attestation, key);
13
+ return {
14
+ version: 1,
15
+ repoRoot: params.repoRoot,
16
+ attestation: params.attestation,
17
+ signature,
18
+ };
19
+ }
20
+ export async function verifySignedBoundaryAttestation(params) {
21
+ if (!params.file || typeof params.file !== 'object') {
22
+ return null;
23
+ }
24
+ const record = params.file;
25
+ if (record.version !== 1 || typeof record.repoRoot !== 'string' || !record.attestation) {
26
+ return null;
27
+ }
28
+ if (record.repoRoot !== params.expectedRepoRoot) {
29
+ return null;
30
+ }
31
+ if (!validateBoundaryAttestation(record.attestation)) {
32
+ return null;
33
+ }
34
+ if (typeof record.signature !== 'string' || !record.signature) {
35
+ return null;
36
+ }
37
+ const key = await loadOrCreateApprovalSigningKey(params.controlPlaneDir);
38
+ const expected = signBody(record.repoRoot, record.attestation, key);
39
+ const actualBuffer = Buffer.from(record.signature);
40
+ const expectedBuffer = Buffer.from(expected);
41
+ if (actualBuffer.length !== expectedBuffer.length ||
42
+ !timingSafeEqual(actualBuffer, expectedBuffer)) {
43
+ return null;
44
+ }
45
+ return record.attestation;
46
+ }
47
+ export async function readSignedAttestationFile(filePath) {
48
+ return JSON.parse(await readFile(filePath, 'utf8'));
49
+ }
50
+ export function isLegacyAttestationFormat(file) {
51
+ if (!file || typeof file !== 'object') {
52
+ return false;
53
+ }
54
+ const record = file;
55
+ return (record.version === 1 &&
56
+ typeof record.driver === 'string' &&
57
+ !('signature' in record) &&
58
+ !('attestation' in record));
59
+ }
60
+ export async function inspectBoundaryAttestationFile(filePath) {
61
+ try {
62
+ await access(filePath);
63
+ }
64
+ catch {
65
+ return 'missing';
66
+ }
67
+ try {
68
+ const raw = await readSignedAttestationFile(filePath);
69
+ if (isLegacyAttestationFormat(raw)) {
70
+ return validateBoundaryAttestation(raw) ? 'legacy' : 'invalid';
71
+ }
72
+ const record = raw;
73
+ if (record.version === 1 &&
74
+ typeof record.repoRoot === 'string' &&
75
+ record.attestation &&
76
+ typeof record.signature === 'string') {
77
+ return 'signed';
78
+ }
79
+ return 'invalid';
80
+ }
81
+ catch {
82
+ return 'invalid';
83
+ }
84
+ }
@@ -0,0 +1,7 @@
1
+ import type { BoundaryDriver } from './boundary-driver.js';
2
+ export declare function isDockerAvailable(): Promise<boolean>;
3
+ export declare function createContainerBoundaryDriver(options?: {
4
+ image?: string;
5
+ egressProxyEnv?: Record<string, string>;
6
+ repoRoot?: string;
7
+ }): BoundaryDriver;
@@ -0,0 +1,122 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { canonicalPath } from '../path-utils.js';
3
+ import { dockerEnvArgs, dockerNetworkArgs, ensureBelayContainerNetwork } from './boundary-egress.js';
4
+ import { materializeContainerBoundaryGrant } from './boundary-grant-materialize.js';
5
+ const ATTESTATION_TTL_MS = 15 * 60_000;
6
+ const DEFAULT_IMAGE = 'alpine:3.20';
7
+ export async function isDockerAvailable() {
8
+ return new Promise((resolve) => {
9
+ const child = spawn('docker', ['info'], { stdio: 'ignore' });
10
+ child.on('error', () => resolve(false));
11
+ child.on('close', (code) => resolve(code === 0));
12
+ });
13
+ }
14
+ function containerAttestation(proxyEnv) {
15
+ const probedAt = new Date().toISOString();
16
+ const signals = ['docker', 'container-driver', 'repo-mount-ro-default'];
17
+ if (Object.keys(proxyEnv).length > 0) {
18
+ signals.push('egress-proxy-chokepoint');
19
+ }
20
+ else {
21
+ signals.push('network-none');
22
+ }
23
+ return {
24
+ version: 1,
25
+ driver: 'container',
26
+ probedAt,
27
+ expiresAt: new Date(Date.now() + ATTESTATION_TTL_MS).toISOString(),
28
+ deniesUngrantedEffects: true,
29
+ materializesGrants: true,
30
+ probeSignals: signals,
31
+ };
32
+ }
33
+ function runDockerProbe(image) {
34
+ return new Promise((resolve) => {
35
+ const child = spawn('docker', ['run', '--rm', '--network', 'none', image, 'echo', 'ok'], {
36
+ stdio: 'ignore',
37
+ });
38
+ child.on('error', () => resolve(false));
39
+ child.on('close', (code) => resolve(code === 0));
40
+ });
41
+ }
42
+ function runInContainer(image, command, cwd, timeoutMs, proxyEnv, mountReadOnly, repoRoot) {
43
+ const mount = canonicalPath(cwd);
44
+ const mountSpec = mountReadOnly ? `${mount}:${mount}:ro` : `${mount}:${mount}`;
45
+ const proxyActive = Object.keys(proxyEnv).length > 0;
46
+ const networkArgs = dockerNetworkArgs(proxyActive, repoRoot);
47
+ const args = [
48
+ 'run',
49
+ '--rm',
50
+ ...networkArgs,
51
+ '-v',
52
+ mountSpec,
53
+ '-w',
54
+ mount,
55
+ ...dockerEnvArgs(proxyEnv),
56
+ image,
57
+ 'sh',
58
+ '-c',
59
+ command,
60
+ ];
61
+ return new Promise((resolve) => {
62
+ const child = spawn('docker', args, { stdio: 'ignore' });
63
+ let timedOut = false;
64
+ const timer = setTimeout(() => {
65
+ timedOut = true;
66
+ child.kill('SIGTERM');
67
+ }, timeoutMs);
68
+ child.on('error', () => {
69
+ clearTimeout(timer);
70
+ resolve({ exitCode: 1, signal: null, timedOut });
71
+ });
72
+ child.on('close', (exitCode, signal) => {
73
+ clearTimeout(timer);
74
+ resolve({
75
+ exitCode,
76
+ signal: signal ? String(signal) : null,
77
+ timedOut,
78
+ });
79
+ });
80
+ });
81
+ }
82
+ export function createContainerBoundaryDriver(options = {}) {
83
+ const image = options.image ?? DEFAULT_IMAGE;
84
+ const proxyEnv = options.egressProxyEnv ?? {};
85
+ const repoRoot = options.repoRoot;
86
+ let preparedNetwork = false;
87
+ return {
88
+ id: 'container',
89
+ async probe() {
90
+ if (!(await isDockerAvailable())) {
91
+ throw new Error('Docker is not available for container boundary driver');
92
+ }
93
+ if (!(await runDockerProbe(image))) {
94
+ throw new Error(`Docker probe failed for image ${image}`);
95
+ }
96
+ return containerAttestation(proxyEnv);
97
+ },
98
+ async prepare(context) {
99
+ if (context.egressProxyActive && context.repoRoot) {
100
+ await ensureBelayContainerNetwork(context.repoRoot);
101
+ preparedNetwork = true;
102
+ }
103
+ },
104
+ async run(command, cwd, timeoutMs, options) {
105
+ const mountReadOnly = options?.mountReadOnly !== false;
106
+ const proxyActive = Object.keys(proxyEnv).length > 0;
107
+ if (proxyActive && repoRoot && !preparedNetwork) {
108
+ await ensureBelayContainerNetwork(repoRoot);
109
+ }
110
+ return runInContainer(image, command, cwd, timeoutMs, proxyEnv, mountReadOnly, repoRoot);
111
+ },
112
+ materializeGrant(request, context) {
113
+ return materializeContainerBoundaryGrant(request, {
114
+ attestation: context.attestation,
115
+ mountRoot: context.mountRoot,
116
+ egressProxyActive: context.egressProxyActive,
117
+ existingGrants: context.existingGrants,
118
+ sensitivePaths: context.sensitivePaths,
119
+ });
120
+ },
121
+ };
122
+ }
@@ -0,0 +1,29 @@
1
+ import { type ShellRunResult } from '../transactional/git-worktree.js';
2
+ import type { BoundaryAttestation, BoundaryDriverId } from './attestation.js';
3
+ import type { BoundaryPrepareContext, BoundaryRunOptions } from './boundary-run.js';
4
+ import type { CapabilityGrantV1 } from './grant.js';
5
+ import type { CapabilityRequestV1 } from './request.js';
6
+ export type { ShellRunResult as BoundaryRunResult };
7
+ export interface BoundaryMaterializeContext {
8
+ attestation: BoundaryAttestation;
9
+ mountRoot: string;
10
+ egressProxyActive: boolean;
11
+ existingGrants?: CapabilityGrantV1[];
12
+ sensitivePaths?: string[];
13
+ }
14
+ export type { BoundaryRunOptions } from './boundary-run.js';
15
+ export { boundaryMountReadOnlyFromPrediction } from './boundary-run.js';
16
+ export interface BoundaryDriver {
17
+ id: BoundaryDriverId;
18
+ probe(): Promise<BoundaryAttestation>;
19
+ prepare?(context: BoundaryPrepareContext): Promise<void>;
20
+ run(command: string, cwd: string, timeoutMs: number, options?: BoundaryRunOptions): Promise<ShellRunResult>;
21
+ materializeGrant(request: CapabilityRequestV1, context: BoundaryMaterializeContext): CapabilityGrantV1 | null;
22
+ }
23
+ export interface BoundaryDriverOptions {
24
+ egressProxyEnv?: Record<string, string>;
25
+ image?: string;
26
+ repoRoot?: string;
27
+ }
28
+ export declare function createHostIntegrationDriver(): BoundaryDriver;
29
+ export declare function getDefaultBoundaryDriver(driverId?: BoundaryDriverId, options?: BoundaryDriverOptions): BoundaryDriver;
@@ -0,0 +1,43 @@
1
+ import { runShellCommand } from '../transactional/git-worktree.js';
2
+ import { createContainerBoundaryDriver } from './boundary-driver-container.js';
3
+ export { boundaryMountReadOnlyFromPrediction } from './boundary-run.js';
4
+ const ATTESTATION_TTL_MS = 15 * 60_000;
5
+ function hostIntegrationAttestation(driver) {
6
+ const probedAt = new Date().toISOString();
7
+ return {
8
+ version: 1,
9
+ driver,
10
+ probedAt,
11
+ expiresAt: new Date(Date.now() + ATTESTATION_TTL_MS).toISOString(),
12
+ deniesUngrantedEffects: false,
13
+ materializesGrants: false,
14
+ probeSignals: ['host-integration', 'l3-policy-only'],
15
+ };
16
+ }
17
+ export function createHostIntegrationDriver() {
18
+ return {
19
+ id: 'host-integration',
20
+ async probe() {
21
+ return hostIntegrationAttestation('host-integration');
22
+ },
23
+ async prepare() {
24
+ // L3 host path has no runtime preparation.
25
+ },
26
+ run(command, cwd, timeoutMs) {
27
+ return runShellCommand(command, cwd, timeoutMs);
28
+ },
29
+ materializeGrant() {
30
+ return null;
31
+ },
32
+ };
33
+ }
34
+ export function getDefaultBoundaryDriver(driverId = 'host-integration', options = {}) {
35
+ if (driverId === 'container') {
36
+ return createContainerBoundaryDriver({
37
+ image: options.image,
38
+ egressProxyEnv: options.egressProxyEnv,
39
+ repoRoot: options.repoRoot,
40
+ });
41
+ }
42
+ return createHostIntegrationDriver();
43
+ }
@@ -0,0 +1,21 @@
1
+ import type { BelayConfigV4 } from '../config.js';
2
+ import type { BoundaryDriverId } from './attestation.js';
3
+ export declare function isEgressProxyActive(params: {
4
+ config: BelayConfigV4;
5
+ running: boolean;
6
+ foreignProxy?: boolean;
7
+ repoRootMismatch?: boolean;
8
+ }): boolean;
9
+ export declare function egressProxyEnvFromConfig(config: BelayConfigV4, proxyActive: boolean): Record<string, string>;
10
+ /** Container workloads cannot reach 127.0.0.1 on the host; route via host.docker.internal. */
11
+ export declare function egressProxyEnvForContainer(config: BelayConfigV4, proxyActive: boolean): Record<string, string>;
12
+ export declare function resolveBoundaryEgressProxyEnv(params: {
13
+ driverId: BoundaryDriverId;
14
+ config: BelayConfigV4;
15
+ proxyActive: boolean;
16
+ }): Record<string, string>;
17
+ export declare function belayContainerNetworkName(repoRoot: string): string;
18
+ export declare function isBelayContainerNetworkReady(repoRoot: string): Promise<boolean>;
19
+ export declare function ensureBelayContainerNetwork(repoRoot: string): Promise<string>;
20
+ export declare function dockerNetworkArgs(proxyActive: boolean, repoRoot?: string): string[];
21
+ export declare function dockerEnvArgs(proxyEnv: Record<string, string>): string[];
@@ -0,0 +1,109 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { recommendedProxyEnv } from '../egress/env.js';
4
+ import { canonicalPath } from '../path-utils.js';
5
+ export function isEgressProxyActive(params) {
6
+ if (!params.config.egress.enabled) {
7
+ return false;
8
+ }
9
+ return params.running && !params.foreignProxy && !params.repoRootMismatch;
10
+ }
11
+ function proxyHostForContainer(listenHost) {
12
+ if (listenHost === '127.0.0.1' || listenHost === 'localhost' || listenHost === '::1') {
13
+ return 'host.docker.internal';
14
+ }
15
+ return listenHost;
16
+ }
17
+ export function egressProxyEnvFromConfig(config, proxyActive) {
18
+ if (!proxyActive || !config.egress.enabled) {
19
+ return {};
20
+ }
21
+ return recommendedProxyEnv(config.egress);
22
+ }
23
+ /** Container workloads cannot reach 127.0.0.1 on the host; route via host.docker.internal. */
24
+ export function egressProxyEnvForContainer(config, proxyActive) {
25
+ if (!proxyActive || !config.egress.enabled) {
26
+ return {};
27
+ }
28
+ const host = proxyHostForContainer(config.egress.listenHost);
29
+ const proxyUrl = `http://${host}:${config.egress.listenPort}`;
30
+ return {
31
+ HTTP_PROXY: proxyUrl,
32
+ HTTPS_PROXY: proxyUrl,
33
+ http_proxy: proxyUrl,
34
+ https_proxy: proxyUrl,
35
+ NO_PROXY: '127.0.0.1,localhost',
36
+ no_proxy: '127.0.0.1,localhost',
37
+ };
38
+ }
39
+ export function resolveBoundaryEgressProxyEnv(params) {
40
+ if (params.driverId === 'container') {
41
+ return egressProxyEnvForContainer(params.config, params.proxyActive);
42
+ }
43
+ return egressProxyEnvFromConfig(params.config, params.proxyActive);
44
+ }
45
+ export function belayContainerNetworkName(repoRoot) {
46
+ const hash = createHash('sha256').update(canonicalPath(repoRoot)).digest('hex').slice(0, 12);
47
+ return `belay-int-${hash}`;
48
+ }
49
+ function dockerNetworkInspect(name) {
50
+ return new Promise((resolve) => {
51
+ const child = spawn('docker', ['network', 'inspect', name], { stdio: 'ignore' });
52
+ child.on('error', () => resolve(false));
53
+ child.on('close', (code) => resolve(code === 0));
54
+ });
55
+ }
56
+ export async function isBelayContainerNetworkReady(repoRoot) {
57
+ return dockerNetworkInspect(belayContainerNetworkName(repoRoot));
58
+ }
59
+ function dockerNetworkCreateInternal(name) {
60
+ return new Promise((resolve, reject) => {
61
+ const child = spawn('docker', ['network', 'create', '--internal', '--label', 'belay=1', name], {
62
+ stdio: 'ignore',
63
+ });
64
+ child.on('error', reject);
65
+ child.on('close', (code) => {
66
+ if (code === 0) {
67
+ resolve();
68
+ return;
69
+ }
70
+ reject(new Error(`Failed to create internal docker network ${name}`));
71
+ });
72
+ });
73
+ }
74
+ export async function ensureBelayContainerNetwork(repoRoot) {
75
+ const name = belayContainerNetworkName(repoRoot);
76
+ if (await dockerNetworkInspect(name)) {
77
+ return name;
78
+ }
79
+ try {
80
+ await dockerNetworkCreateInternal(name);
81
+ }
82
+ catch (error) {
83
+ if (await dockerNetworkInspect(name)) {
84
+ return name;
85
+ }
86
+ throw error;
87
+ }
88
+ return name;
89
+ }
90
+ export function dockerNetworkArgs(proxyActive, repoRoot) {
91
+ if (!proxyActive) {
92
+ return ['--network', 'none'];
93
+ }
94
+ if (!repoRoot) {
95
+ return ['--network', 'none'];
96
+ }
97
+ return [
98
+ '--add-host=host.docker.internal:host-gateway',
99
+ '--network',
100
+ belayContainerNetworkName(repoRoot),
101
+ ];
102
+ }
103
+ export function dockerEnvArgs(proxyEnv) {
104
+ const args = [];
105
+ for (const [key, value] of Object.entries(proxyEnv)) {
106
+ args.push('-e', `${key}=${value}`);
107
+ }
108
+ return args;
109
+ }
@@ -0,0 +1,13 @@
1
+ import type { BoundaryAttestation } from './attestation.js';
2
+ import { type CapabilityGrantV1 } from './grant.js';
3
+ import type { CapabilityRequestV1 } from './request.js';
4
+ export declare const BOUNDARY_GRANT_ISSUER_CONTAINER: "boundary:container";
5
+ export declare function isPathWithinBoundaryMount(request: CapabilityRequestV1): boolean;
6
+ export interface MaterializeBoundaryGrantParams {
7
+ attestation: BoundaryAttestation;
8
+ mountRoot: string;
9
+ egressProxyActive: boolean;
10
+ existingGrants?: CapabilityGrantV1[];
11
+ sensitivePaths?: string[];
12
+ }
13
+ export declare function materializeContainerBoundaryGrant(request: CapabilityRequestV1, params: MaterializeBoundaryGrantParams): CapabilityGrantV1 | null;
@@ -0,0 +1,78 @@
1
+ import path from 'node:path';
2
+ import { matchesSensitivePath } from '../glob.js';
3
+ import { canonicalPath, pathWithinRoot } from '../path-utils.js';
4
+ import { newGrantId } from './approval-v3.js';
5
+ import { isAttestationFresh } from './attestation.js';
6
+ import { CAPABILITY_GRANT_VERSION } from './grant.js';
7
+ import { grantMatchesRequest } from './grant-match.js';
8
+ const BOUNDARY_GRANT_TTL_MS = 15 * 60_000;
9
+ export const BOUNDARY_GRANT_ISSUER_CONTAINER = 'boundary:container';
10
+ function resolveCapabilityPath(targetPath, cwd) {
11
+ const joined = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
12
+ return canonicalPath(joined);
13
+ }
14
+ export function isPathWithinBoundaryMount(request) {
15
+ if (request.resource.kind !== 'path') {
16
+ return true;
17
+ }
18
+ const mountRoot = canonicalPath(request.context.cwd);
19
+ const resolved = resolveCapabilityPath(request.resource.path, request.context.cwd);
20
+ return pathWithinRoot(mountRoot, resolved);
21
+ }
22
+ function mintBoundaryGrant(request, attestation, issuer) {
23
+ const now = Date.now();
24
+ const attestationExpiry = Date.parse(attestation.expiresAt);
25
+ const grantExpiry = now + BOUNDARY_GRANT_TTL_MS;
26
+ const expiresAt = new Date(Number.isFinite(attestationExpiry) ? Math.min(attestationExpiry, grantExpiry) : grantExpiry).toISOString();
27
+ return {
28
+ version: CAPABILITY_GRANT_VERSION,
29
+ grantId: newGrantId(),
30
+ principal: request.principal,
31
+ action: request.action,
32
+ resource: request.resource,
33
+ inputFingerprint: request.context.inputFingerprint,
34
+ issuedAt: new Date(now).toISOString(),
35
+ expiresAt,
36
+ maxUses: 1,
37
+ usesRemaining: 1,
38
+ issuer,
39
+ };
40
+ }
41
+ export function materializeContainerBoundaryGrant(request, params) {
42
+ if (!isAttestationFresh(params.attestation)) {
43
+ return null;
44
+ }
45
+ if (!params.attestation.materializesGrants || params.attestation.driver !== 'container') {
46
+ return null;
47
+ }
48
+ if (request.evidence.level !== 'certain') {
49
+ return null;
50
+ }
51
+ const repoRoot = canonicalPath(request.principal.repoRoot);
52
+ const mountRoot = canonicalPath(params.mountRoot);
53
+ if (request.action === 'fs.read' || request.action === 'fs.write') {
54
+ if (request.resource.kind !== 'path') {
55
+ return null;
56
+ }
57
+ const resolved = resolveCapabilityPath(request.resource.path, request.context.cwd);
58
+ if (!pathWithinRoot(repoRoot, resolved) || !pathWithinRoot(mountRoot, resolved)) {
59
+ return null;
60
+ }
61
+ const relative = resolved.slice(repoRoot.length).replace(/^[/\\]/, '');
62
+ if (params.sensitivePaths?.length && matchesSensitivePath(relative, params.sensitivePaths)) {
63
+ return null;
64
+ }
65
+ return mintBoundaryGrant(request, params.attestation, BOUNDARY_GRANT_ISSUER_CONTAINER);
66
+ }
67
+ if (request.action === 'network.connect' && request.resource.kind === 'network') {
68
+ if (!params.egressProxyActive) {
69
+ return null;
70
+ }
71
+ const approved = params.existingGrants?.some((grant) => grant.issuer !== BOUNDARY_GRANT_ISSUER_CONTAINER && grantMatchesRequest(grant, request));
72
+ if (!approved) {
73
+ return null;
74
+ }
75
+ return mintBoundaryGrant(request, params.attestation, BOUNDARY_GRANT_ISSUER_CONTAINER);
76
+ }
77
+ return null;
78
+ }