@guilz-dev/belay 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +20 -1
  2. package/dist/adapters/claude/runtime-entry.js +54 -25
  3. package/dist/adapters/shared/gate-runtime.d.ts +7 -0
  4. package/dist/adapters/shared/gate-runtime.js +420 -220
  5. package/dist/bundle/claude-runtime.mjs +13215 -8864
  6. package/dist/bundle/codex-runtime.mjs +10771 -6453
  7. package/dist/bundle/cursor-runtime.mjs +10797 -6484
  8. package/dist/cli.js +37 -1
  9. package/dist/commands/approve.js +34 -10
  10. package/dist/commands/doctor.js +51 -0
  11. package/dist/commands/recovery-checkpoints.d.ts +164 -0
  12. package/dist/commands/recovery-checkpoints.js +347 -0
  13. package/dist/commands/revoke.js +19 -7
  14. package/dist/commands/status.js +7 -0
  15. package/dist/config-io.js +49 -18
  16. package/dist/conformance/guarantee-table.js +6 -0
  17. package/dist/conformance/types.d.ts +1 -0
  18. package/dist/core/approval-replay.d.ts +1 -1
  19. package/dist/core/approval-replay.js +4 -1
  20. package/dist/core/approval-service.d.ts +6 -1
  21. package/dist/core/approval-service.js +86 -27
  22. package/dist/core/approval.d.ts +2 -0
  23. package/dist/core/approval.js +6 -3
  24. package/dist/core/audit-types.d.ts +13 -0
  25. package/dist/core/capability/approval-state-mutation.d.ts +19 -0
  26. package/dist/core/capability/approval-state-mutation.js +76 -3
  27. package/dist/core/capability/approval-v3.d.ts +18 -1
  28. package/dist/core/capability/approval-v3.js +63 -23
  29. package/dist/core/capability/attestation.d.ts +3 -0
  30. package/dist/core/capability/attestation.js +7 -0
  31. package/dist/core/capability/boundary-driver-container.d.ts +12 -0
  32. package/dist/core/capability/boundary-driver-container.js +101 -34
  33. package/dist/core/capability/boundary-driver.js +1 -0
  34. package/dist/core/capability/boundary-grant-materialize.d.ts +1 -0
  35. package/dist/core/capability/boundary-grant-materialize.js +15 -0
  36. package/dist/core/capability/boundary-run.d.ts +25 -0
  37. package/dist/core/capability/boundary-run.js +37 -9
  38. package/dist/core/capability/boundary-session.d.ts +4 -0
  39. package/dist/core/capability/boundary-session.js +9 -0
  40. package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
  41. package/dist/core/capability/boundary-workspace-mount.js +68 -0
  42. package/dist/core/capability/gate-policy-shadow.js +23 -1
  43. package/dist/core/capability/grant-consumption.d.ts +4 -0
  44. package/dist/core/capability/grant-consumption.js +11 -0
  45. package/dist/core/capability/grant-lease.d.ts +36 -1
  46. package/dist/core/capability/grant-lease.js +213 -16
  47. package/dist/core/capability/grant-match.js +3 -0
  48. package/dist/core/capability/index.d.ts +1 -1
  49. package/dist/core/capability/index.js +1 -1
  50. package/dist/core/capability/policy-engine.d.ts +7 -0
  51. package/dist/core/capability/policy-engine.js +202 -15
  52. package/dist/core/capability/request.d.ts +3 -0
  53. package/dist/core/config.d.ts +20 -0
  54. package/dist/core/config.js +62 -0
  55. package/dist/core/effect-ir/audit.d.ts +9 -0
  56. package/dist/core/effect-ir/audit.js +86 -0
  57. package/dist/core/effect-ir/build.d.ts +31 -0
  58. package/dist/core/effect-ir/build.js +277 -0
  59. package/dist/core/effect-ir/index.d.ts +8 -0
  60. package/dist/core/effect-ir/index.js +7 -0
  61. package/dist/core/effect-ir/normalize.d.ts +4 -0
  62. package/dist/core/effect-ir/normalize.js +104 -0
  63. package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
  64. package/dist/core/effect-ir/package-exec-eval.js +152 -0
  65. package/dist/core/effect-ir/package-exec.d.ts +29 -0
  66. package/dist/core/effect-ir/package-exec.js +292 -0
  67. package/dist/core/effect-ir/policy.d.ts +13 -0
  68. package/dist/core/effect-ir/policy.js +77 -0
  69. package/dist/core/effect-ir/types.d.ts +55 -0
  70. package/dist/core/effect-ir/types.js +1 -0
  71. package/dist/core/egress/allowlist.d.ts +1 -0
  72. package/dist/core/egress/allowlist.js +8 -0
  73. package/dist/core/egress-approval.js +45 -51
  74. package/dist/core/gate-contract.d.ts +2 -0
  75. package/dist/core/gate-contract.js +2 -0
  76. package/dist/core/gate-engine.js +26 -9
  77. package/dist/core/index.d.ts +3 -1
  78. package/dist/core/index.js +2 -1
  79. package/dist/core/judge-cloud-consent.js +29 -15
  80. package/dist/core/network-endpoint.d.ts +10 -0
  81. package/dist/core/network-endpoint.js +51 -0
  82. package/dist/core/process-runner.d.ts +10 -0
  83. package/dist/core/process-runner.js +124 -0
  84. package/dist/core/recover-advice.js +2 -1
  85. package/dist/core/recovery/artifact-store.d.ts +29 -0
  86. package/dist/core/recovery/artifact-store.js +336 -0
  87. package/dist/core/recovery/checkpoint.d.ts +25 -0
  88. package/dist/core/recovery/checkpoint.js +260 -0
  89. package/dist/core/recovery/index.d.ts +3 -1
  90. package/dist/core/recovery/index.js +2 -0
  91. package/dist/core/recovery/operator-guidance.d.ts +7 -0
  92. package/dist/core/recovery/operator-guidance.js +39 -0
  93. package/dist/core/recovery/reconcile.d.ts +3 -0
  94. package/dist/core/recovery/reconcile.js +48 -0
  95. package/dist/core/recovery/resource-identity.d.ts +4 -0
  96. package/dist/core/recovery/resource-identity.js +34 -0
  97. package/dist/core/recovery/restore.d.ts +13 -0
  98. package/dist/core/recovery/restore.js +114 -0
  99. package/dist/core/recovery/snapshot-node.d.ts +27 -0
  100. package/dist/core/recovery/snapshot-node.js +261 -0
  101. package/dist/core/recovery/types.d.ts +85 -1
  102. package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
  103. package/dist/core/transactional/apply-observed-changes.js +275 -0
  104. package/dist/core/transactional/backend-selector.d.ts +8 -0
  105. package/dist/core/transactional/backend-selector.js +118 -0
  106. package/dist/core/transactional/backend.d.ts +55 -0
  107. package/dist/core/transactional/backend.js +1 -0
  108. package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
  109. package/dist/core/transactional/file-checkpoint-backend.js +278 -0
  110. package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
  111. package/dist/core/transactional/file-checkpoint-git.js +217 -0
  112. package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
  113. package/dist/core/transactional/file-checkpoint-staging.js +54 -0
  114. package/dist/core/transactional/file-clone.d.ts +15 -0
  115. package/dist/core/transactional/file-clone.js +139 -0
  116. package/dist/core/transactional/file-tree-path.d.ts +7 -0
  117. package/dist/core/transactional/file-tree-path.js +50 -0
  118. package/dist/core/transactional/file-tree.d.ts +41 -0
  119. package/dist/core/transactional/file-tree.js +186 -0
  120. package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
  121. package/dist/core/transactional/git-worktree-backend.js +50 -0
  122. package/dist/core/transactional/git-worktree.d.ts +9 -11
  123. package/dist/core/transactional/git-worktree.js +24 -119
  124. package/dist/core/transactional/index.d.ts +11 -0
  125. package/dist/core/transactional/index.js +10 -0
  126. package/dist/core/transactional/reasons.js +1 -0
  127. package/dist/core/transactional/runner.js +157 -25
  128. package/dist/core/transactional/snapshot-node.d.ts +26 -0
  129. package/dist/core/transactional/snapshot-node.js +87 -0
  130. package/dist/core/transactional/types.d.ts +17 -1
  131. package/dist/core/types.d.ts +10 -1
  132. package/dist/core/verdict/adapter.js +8 -0
  133. package/dist/core/verdict/launcher-resolve.js +2 -20
  134. package/dist/core/verdict/shell-policy.d.ts +1 -0
  135. package/dist/core/verdict/shell-policy.js +1 -1
  136. package/dist/core/verdict/types.d.ts +5 -0
  137. package/dist/core/verdict/verdict.js +37 -20
  138. package/dist/types.d.ts +11 -0
  139. package/dist/version.d.ts +1 -1
  140. package/dist/version.js +1 -1
  141. package/package.json +1 -1
  142. package/skills/belay/SKILL.md +8 -3
  143. package/skills/belay/belay-approve.md +10 -7
@@ -1,9 +1,33 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { canonicalPath } from '../path-utils.js';
4
+ import { runProcessWithBoundedOutput } from '../process-runner.js';
3
5
  import { dockerEnvArgs, dockerNetworkArgs, ensureBelayContainerNetwork } from './boundary-egress.js';
4
6
  import { materializeContainerBoundaryGrant } from './boundary-grant-materialize.js';
7
+ import { BoundaryCleanupError, } from './boundary-run.js';
8
+ import { buildWorkspaceMountSpec, resolveGuestWorkdir, workspaceMountEnvArgs, } from './boundary-workspace-mount.js';
5
9
  const ATTESTATION_TTL_MS = 15 * 60_000;
6
10
  const DEFAULT_IMAGE = 'alpine:3.20';
11
+ const CONTAINER_CLEANUP_TIMEOUT_MS = 10_000;
12
+ const CONTAINER_INSPECT_TIMEOUT_MS = 2_000;
13
+ const CONTAINER_ABSENCE_ATTEMPTS = 10;
14
+ const CONTAINER_ABSENCE_RETRY_MS = 100;
15
+ function containerDoesNotExist(result) {
16
+ return /no such container/i.test(`${result.stderr ?? ''}\n${result.stdout ?? ''}`);
17
+ }
18
+ async function confirmContainerAbsent(containerName) {
19
+ for (let attempt = 0; attempt < CONTAINER_ABSENCE_ATTEMPTS; attempt += 1) {
20
+ const inspection = await runProcessWithBoundedOutput('docker', ['inspect', '--type', 'container', containerName], {}, CONTAINER_INSPECT_TIMEOUT_MS);
21
+ if (!inspection.timedOut && inspection.exitCode !== 0 && containerDoesNotExist(inspection)) {
22
+ return true;
23
+ }
24
+ if (inspection.timedOut || (inspection.exitCode !== 0 && !containerDoesNotExist(inspection))) {
25
+ return false;
26
+ }
27
+ await new Promise((resolve) => setTimeout(resolve, CONTAINER_ABSENCE_RETRY_MS));
28
+ }
29
+ return false;
30
+ }
7
31
  export async function isDockerAvailable() {
8
32
  return new Promise((resolve) => {
9
33
  const child = spawn('docker', ['info'], { stdio: 'ignore' });
@@ -13,7 +37,12 @@ export async function isDockerAvailable() {
13
37
  }
14
38
  function containerAttestation(proxyEnv) {
15
39
  const probedAt = new Date().toISOString();
16
- const signals = ['docker', 'container-driver', 'repo-mount-ro-default'];
40
+ const signals = [
41
+ 'docker',
42
+ 'container-driver',
43
+ 'repo-mount-ro-default',
44
+ 'workspace-mount-isolation',
45
+ ];
17
46
  if (Object.keys(proxyEnv).length > 0) {
18
47
  signals.push('egress-proxy-chokepoint');
19
48
  }
@@ -25,9 +54,12 @@ function containerAttestation(proxyEnv) {
25
54
  driver: 'container',
26
55
  probedAt,
27
56
  expiresAt: new Date(Date.now() + ATTESTATION_TTL_MS).toISOString(),
28
- deniesUngrantedEffects: true,
29
- materializesGrants: true,
30
- probeSignals: signals,
57
+ // Mount and network classes are isolated, but exact grants are not yet
58
+ // passed to and enforced by the process runner.
59
+ deniesUngrantedEffects: false,
60
+ materializesGrants: false,
61
+ isolatesWorkspaceMounts: true,
62
+ probeSignals: [...signals, 'exact-grant-enforcement-unavailable'],
31
63
  };
32
64
  }
33
65
  function runDockerProbe(image) {
@@ -39,45 +71,80 @@ function runDockerProbe(image) {
39
71
  child.on('close', (code) => resolve(code === 0));
40
72
  });
41
73
  }
42
- function 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 = [
74
+ export function buildContainerRunArgs(params) {
75
+ const proxyActive = Object.keys(params.proxyEnv).length > 0;
76
+ const networkArgs = dockerNetworkArgs(proxyActive, params.repoRoot);
77
+ const containerNameArgs = params.containerName ? ['--name', params.containerName] : [];
78
+ const workspaceMount = params.runOptions?.workspaceMount;
79
+ if (workspaceMount) {
80
+ if (!params.repoRoot) {
81
+ throw new Error('boundary_workspace_mount_missing_resource_root');
82
+ }
83
+ if (canonicalPath(workspaceMount.guestTargetRoot) !== canonicalPath(params.repoRoot)) {
84
+ throw new Error('boundary_workspace_mount_target_mismatch');
85
+ }
86
+ const workdir = resolveGuestWorkdir(workspaceMount);
87
+ if (canonicalPath(params.cwd) !== workdir) {
88
+ throw new Error('boundary_workspace_mount_cwd_mismatch');
89
+ }
90
+ return [
91
+ 'run',
92
+ '--rm',
93
+ ...containerNameArgs,
94
+ ...networkArgs,
95
+ '--mount',
96
+ buildWorkspaceMountSpec(workspaceMount),
97
+ '-w',
98
+ workdir,
99
+ ...workspaceMountEnvArgs(workspaceMount),
100
+ ...dockerEnvArgs(params.proxyEnv),
101
+ params.image,
102
+ 'sh',
103
+ '-c',
104
+ params.command,
105
+ ];
106
+ }
107
+ const mount = canonicalPath(params.cwd);
108
+ const mountSpec = params.mountReadOnly ? `${mount}:${mount}:ro` : `${mount}:${mount}`;
109
+ return [
48
110
  'run',
49
111
  '--rm',
112
+ ...containerNameArgs,
50
113
  ...networkArgs,
51
114
  '-v',
52
115
  mountSpec,
53
116
  '-w',
54
117
  mount,
55
- ...dockerEnvArgs(proxyEnv),
56
- image,
118
+ ...dockerEnvArgs(params.proxyEnv),
119
+ params.image,
57
120
  'sh',
58
121
  '-c',
59
- command,
122
+ params.command,
60
123
  ];
61
- 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
- });
124
+ }
125
+ async function runInContainer(image, command, cwd, timeoutMs, proxyEnv, mountReadOnly, repoRoot, runOptions) {
126
+ const containerName = `belay-run-${randomUUID()}`;
127
+ const args = buildContainerRunArgs({
128
+ image,
129
+ command,
130
+ cwd,
131
+ proxyEnv,
132
+ mountReadOnly,
133
+ repoRoot,
134
+ runOptions,
135
+ containerName,
80
136
  });
137
+ const result = await runProcessWithBoundedOutput('docker', args, {}, timeoutMs);
138
+ if (result.timedOut) {
139
+ const cleanup = await runProcessWithBoundedOutput('docker', ['rm', '-f', containerName], {}, CONTAINER_CLEANUP_TIMEOUT_MS);
140
+ const cleanupFailed = cleanup.timedOut || cleanup.exitCode !== 0;
141
+ const absent = containerDoesNotExist(cleanup) ||
142
+ (cleanupFailed && (await confirmContainerAbsent(containerName)));
143
+ if (cleanupFailed && !absent) {
144
+ throw new BoundaryCleanupError('container', containerName);
145
+ }
146
+ }
147
+ return result;
81
148
  }
82
149
  export function createContainerBoundaryDriver(options = {}) {
83
150
  const image = options.image ?? DEFAULT_IMAGE;
@@ -102,12 +169,12 @@ export function createContainerBoundaryDriver(options = {}) {
102
169
  }
103
170
  },
104
171
  async run(command, cwd, timeoutMs, options) {
105
- const mountReadOnly = options?.mountReadOnly !== false;
172
+ const mountReadOnly = options?.mountReadOnly !== false && !options?.workspaceMount;
106
173
  const proxyActive = Object.keys(proxyEnv).length > 0;
107
174
  if (proxyActive && repoRoot && !preparedNetwork) {
108
175
  await ensureBelayContainerNetwork(repoRoot);
109
176
  }
110
- return runInContainer(image, command, cwd, timeoutMs, proxyEnv, mountReadOnly, repoRoot);
177
+ return runInContainer(image, command, cwd, timeoutMs, proxyEnv, mountReadOnly, repoRoot, options);
111
178
  },
112
179
  materializeGrant(request, context) {
113
180
  return materializeContainerBoundaryGrant(request, {
@@ -11,6 +11,7 @@ function hostIntegrationAttestation(driver) {
11
11
  expiresAt: new Date(Date.now() + ATTESTATION_TTL_MS).toISOString(),
12
12
  deniesUngrantedEffects: false,
13
13
  materializesGrants: false,
14
+ isolatesWorkspaceMounts: false,
14
15
  probeSignals: ['host-integration', 'l3-policy-only'],
15
16
  };
16
17
  }
@@ -11,3 +11,4 @@ export interface MaterializeBoundaryGrantParams {
11
11
  sensitivePaths?: string[];
12
12
  }
13
13
  export declare function materializeContainerBoundaryGrant(request: CapabilityRequestV1, params: MaterializeBoundaryGrantParams): CapabilityGrantV1 | null;
14
+ export declare function materializeContainerBoundaryGrants(requests: readonly CapabilityRequestV1[], params: MaterializeBoundaryGrantParams): CapabilityGrantV1[];
@@ -76,3 +76,18 @@ export function materializeContainerBoundaryGrant(request, params) {
76
76
  }
77
77
  return null;
78
78
  }
79
+ export function materializeContainerBoundaryGrants(requests, params) {
80
+ const grants = [];
81
+ let existing = [...(params.existingGrants ?? [])];
82
+ for (const request of requests) {
83
+ const materialized = materializeContainerBoundaryGrant(request, {
84
+ ...params,
85
+ existingGrants: existing,
86
+ });
87
+ if (materialized) {
88
+ grants.push(materialized);
89
+ existing = [...existing, materialized];
90
+ }
91
+ }
92
+ return grants;
93
+ }
@@ -1,8 +1,33 @@
1
1
  import type { ShellRunResult } from '../transactional/git-worktree.js';
2
2
  import type { ClassifyResult } from '../types.js';
3
+ export interface BoundaryCleanupFailure {
4
+ code: 'BOUNDARY_CLEANUP_UNCONFIRMED';
5
+ resourceKind: 'container';
6
+ resourceId: string;
7
+ executionStarted: true;
8
+ cleanupConfirmed: false;
9
+ }
10
+ export declare class BoundaryCleanupError extends Error implements BoundaryCleanupFailure {
11
+ readonly resourceKind: 'container';
12
+ readonly resourceId: string;
13
+ readonly code: "BOUNDARY_CLEANUP_UNCONFIRMED";
14
+ readonly executionStarted: true;
15
+ readonly cleanupConfirmed: false;
16
+ constructor(resourceKind: 'container', resourceId: string);
17
+ }
18
+ export declare function isBoundaryCleanupError(value: unknown): value is BoundaryCleanupFailure;
19
+ export declare function safeBoundaryCleanupResourceId(value: unknown): string | undefined;
20
+ export interface BoundaryWorkspaceMount {
21
+ hostSourceRoot: string;
22
+ guestTargetRoot: string;
23
+ cwdRelative: string;
24
+ writable: boolean;
25
+ hideHostSourcePath: boolean;
26
+ }
3
27
  export interface BoundaryRunOptions {
4
28
  /** When true, container driver mounts the working directory read-only. */
5
29
  mountReadOnly?: boolean;
30
+ workspaceMount?: BoundaryWorkspaceMount;
6
31
  }
7
32
  export interface BoundaryPrepareContext {
8
33
  repoRoot?: string;
@@ -1,17 +1,45 @@
1
+ const SAFE_CONTAINER_RESOURCE_ID = /^belay-run-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
2
+ export class BoundaryCleanupError extends Error {
3
+ resourceKind;
4
+ resourceId;
5
+ code = 'BOUNDARY_CLEANUP_UNCONFIRMED';
6
+ executionStarted = true;
7
+ cleanupConfirmed = false;
8
+ constructor(resourceKind, resourceId) {
9
+ super('Boundary cleanup could not be confirmed');
10
+ this.resourceKind = resourceKind;
11
+ this.resourceId = resourceId;
12
+ this.name = 'BoundaryCleanupError';
13
+ }
14
+ }
15
+ export function isBoundaryCleanupError(value) {
16
+ if (!value || typeof value !== 'object') {
17
+ return false;
18
+ }
19
+ const candidate = value;
20
+ return (candidate.code === 'BOUNDARY_CLEANUP_UNCONFIRMED' &&
21
+ candidate.resourceKind === 'container' &&
22
+ typeof candidate.resourceId === 'string' &&
23
+ candidate.executionStarted === true &&
24
+ candidate.cleanupConfirmed === false);
25
+ }
26
+ export function safeBoundaryCleanupResourceId(value) {
27
+ if (!isBoundaryCleanupError(value) || !SAFE_CONTAINER_RESOURCE_ID.test(value.resourceId)) {
28
+ return undefined;
29
+ }
30
+ return value.resourceId;
31
+ }
1
32
  export function boundaryMountReadOnlyFromPrediction(predicted) {
2
33
  const effect = predicted.axes?.effect;
3
34
  if (effect === 'local_mutation' || effect === 'remote_mutation') {
4
35
  return false;
5
36
  }
6
- const action = predicted.capabilityRequests?.[0]?.action;
7
- if (action === 'fs.write' || action === 'git.ref.write' || action === 'control_plane.write') {
8
- return false;
9
- }
10
- if (action === 'fs.read') {
11
- return true;
12
- }
13
- if (action === 'network.connect' || action === 'process.exec' || action === 'indeterminate') {
14
- return true;
37
+ for (const request of predicted.capabilityRequests ?? []) {
38
+ if (request.action === 'fs.write' ||
39
+ request.action === 'git.ref.write' ||
40
+ request.action === 'control_plane.write') {
41
+ return false;
42
+ }
15
43
  }
16
44
  return true;
17
45
  }
@@ -12,6 +12,9 @@ export interface ResolvedBoundaryDriverContext {
12
12
  proxyActive: boolean;
13
13
  proxyEnv: Record<string, string>;
14
14
  prepareContext: BoundaryPrepareContext;
15
+ attestationPath: string;
16
+ attestation: BoundaryAttestation | null;
17
+ attestationFresh: boolean;
15
18
  }
16
19
  export declare function boundaryAttestationPath(repoRoot: string, config: BelayConfigV4): string;
17
20
  export declare function loadBoundaryAttestation(filePath: string, expectedRepoRoot?: string, controlPlaneDir?: string): Promise<BoundaryAttestation | null>;
@@ -53,5 +56,6 @@ export declare function runBoundaryAgentCommand(params: {
53
56
  command: string;
54
57
  cwd?: string;
55
58
  timeoutMs?: number;
59
+ runOptions?: BoundaryRunOptions;
56
60
  }): Promise<Awaited<ReturnType<BoundaryDriver['run']>>>;
57
61
  export { dockerNetworkArgs };
@@ -51,6 +51,9 @@ export function hostIntegrationBoundaryContext(repoRoot) {
51
51
  egressProxyActive: false,
52
52
  proxyEnv: {},
53
53
  },
54
+ attestationPath: '',
55
+ attestation: null,
56
+ attestationFresh: false,
54
57
  };
55
58
  }
56
59
  export async function resolveBoundaryDriverContext(params) {
@@ -74,6 +77,8 @@ export async function resolveBoundaryDriverContext(params) {
74
77
  egressProxyEnv: proxyEnv,
75
78
  repoRoot: params.repoRoot,
76
79
  });
80
+ const attestationPath = boundaryAttestationPath(params.repoRoot, params.config);
81
+ const attestation = await loadBoundaryAttestation(attestationPath, params.repoRoot, configuredControlPlaneDir(params.config));
77
82
  return {
78
83
  driver,
79
84
  driverId,
@@ -84,6 +89,9 @@ export async function resolveBoundaryDriverContext(params) {
84
89
  egressProxyActive: proxyActive,
85
90
  proxyEnv,
86
91
  },
92
+ attestationPath,
93
+ attestation,
94
+ attestationFresh: attestation ? isAttestationFresh(attestation) : false,
87
95
  };
88
96
  }
89
97
  export async function runWithBoundaryDriver(params) {
@@ -124,6 +132,7 @@ export async function runBoundaryAgentCommand(params) {
124
132
  command: params.command,
125
133
  cwd: params.cwd ?? params.repoRoot,
126
134
  timeoutMs: params.timeoutMs ?? 30 * 60_000,
135
+ runOptions: params.runOptions,
127
136
  });
128
137
  }
129
138
  export { dockerNetworkArgs };
@@ -0,0 +1,5 @@
1
+ import type { BoundaryWorkspaceMount } from './boundary-run.js';
2
+ export declare function validateWorkspaceMount(mount: BoundaryWorkspaceMount): void;
3
+ export declare function resolveGuestWorkdir(mount: BoundaryWorkspaceMount): string;
4
+ export declare function buildWorkspaceMountSpec(mount: BoundaryWorkspaceMount): string;
5
+ export declare function workspaceMountEnvArgs(mount: BoundaryWorkspaceMount): string[];
@@ -0,0 +1,68 @@
1
+ import path from 'node:path';
2
+ import { canonicalPath, pathWithinRoot } from '../path-utils.js';
3
+ const HOST_PATH_ENV_VARS = [
4
+ 'BELAY_EGRESS_REPO_ROOT',
5
+ 'BELAY_JUDGE_BROKER_REPO_ROOT',
6
+ 'BELAY_JUDGE_BROKER_STATE_DIR',
7
+ 'BELAY_REPO_ROOT',
8
+ ];
9
+ export function validateWorkspaceMount(mount) {
10
+ const hostSourceRoot = canonicalPath(mount.hostSourceRoot);
11
+ const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
12
+ if (!hostSourceRoot || !guestTargetRoot) {
13
+ throw new Error('boundary_workspace_mount_invalid_root');
14
+ }
15
+ if (hostSourceRoot.includes(',') || guestTargetRoot.includes(',')) {
16
+ throw new Error('boundary_workspace_mount_invalid_root');
17
+ }
18
+ if (hostSourceRoot === guestTargetRoot) {
19
+ throw new Error('boundary_workspace_mount_source_equals_target');
20
+ }
21
+ if (pathWithinRoot(hostSourceRoot, guestTargetRoot)) {
22
+ throw new Error('boundary_workspace_mount_source_contains_target');
23
+ }
24
+ if (mount.cwdRelative.includes('\0')) {
25
+ throw new Error('boundary_workspace_mount_invalid_cwd');
26
+ }
27
+ const normalizedRelative = path.posix.normalize(mount.cwdRelative.replace(/\\/g, '/'));
28
+ if (normalizedRelative === '..' || normalizedRelative.startsWith('../')) {
29
+ throw new Error('boundary_workspace_mount_invalid_cwd');
30
+ }
31
+ if (path.posix.isAbsolute(normalizedRelative)) {
32
+ throw new Error('boundary_workspace_mount_invalid_cwd');
33
+ }
34
+ }
35
+ export function resolveGuestWorkdir(mount) {
36
+ validateWorkspaceMount(mount);
37
+ const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
38
+ const normalizedRelative = path.posix.normalize(mount.cwdRelative.replace(/\\/g, '/'));
39
+ if (normalizedRelative === '.' || normalizedRelative === '') {
40
+ return guestTargetRoot;
41
+ }
42
+ const segments = normalizedRelative.split('/').filter(Boolean);
43
+ return path.posix.join(guestTargetRoot, ...segments);
44
+ }
45
+ export function buildWorkspaceMountSpec(mount) {
46
+ validateWorkspaceMount(mount);
47
+ const hostSourceRoot = canonicalPath(mount.hostSourceRoot);
48
+ const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
49
+ const readonly = mount.writable ? '' : ',readonly';
50
+ return `type=bind,src=${hostSourceRoot},dst=${guestTargetRoot}${readonly}`;
51
+ }
52
+ export function workspaceMountEnvArgs(mount) {
53
+ const workdir = resolveGuestWorkdir(mount);
54
+ if (!mount.hideHostSourcePath) {
55
+ return ['-e', `PWD=${workdir}`];
56
+ }
57
+ const guestTargetRoot = canonicalPath(mount.guestTargetRoot);
58
+ const hostSourceRoot = canonicalPath(mount.hostSourceRoot);
59
+ const args = [];
60
+ for (const key of HOST_PATH_ENV_VARS) {
61
+ args.push('-e', `${key}=`);
62
+ }
63
+ args.push('-e', `PWD=${workdir}`, '-e', `OLDPWD=${workdir}`, '-e', `BELAY_REPO_ROOT=${guestTargetRoot}`);
64
+ if (hostSourceRoot !== guestTargetRoot) {
65
+ args.push('-e', `BELAY_EGRESS_REPO_ROOT=${guestTargetRoot}`);
66
+ }
67
+ return args;
68
+ }
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { belayStateDir } from '../config.js';
3
+ import { effectPlanAuditFields } from '../effect-ir/audit.js';
3
4
  import { buildTier1Prompt } from '../verdict/judge.js';
4
5
  import { normalizeJudgeRuntimeConfig } from '../verdict/judge-runtime-config.js';
5
6
  import { judgeShadowAuditFields, recordShadowComparison, shouldRunShadowComparison, triggerJudgeSessionKillSwitch, } from '../verdict/judge-shadow.js';
@@ -7,7 +8,16 @@ import { evaluateWithJudgeTransport } from '../verdict/judge-transport.js';
7
8
  import { recordPolicyJudgeComparison } from './gate-shadow-ratchet.js';
8
9
  export { recordGateApprovalAsk } from './gate-shadow-ratchet.js';
9
10
  export function capabilityDecisionAuditFields(result) {
10
- const fields = {};
11
+ const fields = {
12
+ ...effectPlanAuditFields(result.effectPlan, {
13
+ capabilityRequests: result.capabilityRequests,
14
+ decisions: result.effectPlanPolicyDecisions,
15
+ authorizationDecision: result.authorizationDecision,
16
+ }),
17
+ executionRoute: 'downstream_host',
18
+ enforcementStatus: 'prediction_only',
19
+ boundaryEnforcement: 'prediction_only',
20
+ };
11
21
  if (result.capabilityRequests?.length) {
12
22
  fields.capabilityRequestCount = result.capabilityRequests.length;
13
23
  fields.capabilityActions = result.capabilityRequests.map((request) => request.action);
@@ -19,6 +29,18 @@ export function capabilityDecisionAuditFields(result) {
19
29
  fields.policyMatchedRule = result.authorizationDecision.matchedRule;
20
30
  }
21
31
  }
32
+ const decisions = result.effectPlanPolicyDecisions ?? [];
33
+ if (decisions.length > 0 &&
34
+ decisions.every((decision) => decision.matchedRule === 'boundary.verified')) {
35
+ fields.authorizationMode = 'boundary_policy';
36
+ }
37
+ else if (decisions.length > 0 &&
38
+ decisions.every((decision) => decision.matchedRule === 'grant.exact')) {
39
+ fields.authorizationMode = 'exact_grant';
40
+ }
41
+ else {
42
+ fields.authorizationMode = result.authorizationDecision ? 'policy_decision' : 'none';
43
+ }
22
44
  if (result.boundaryProfile) {
23
45
  fields.boundaryProfile = result.boundaryProfile;
24
46
  }
@@ -0,0 +1,4 @@
1
+ import type { ClassifyResult } from '../types.js';
2
+ export declare function capabilityGrantLeaseRequired(result: Pick<ClassifyResult, 'reason' | 'authorizationDecision'>): boolean;
3
+ /** Fail closed when a grant lease is required but capability requests are missing. */
4
+ export declare function canConsumeCapabilityGrantLease(result: ClassifyResult): boolean;
@@ -0,0 +1,11 @@
1
+ export function capabilityGrantLeaseRequired(result) {
2
+ return (result.reason === 'capability_grant' ||
3
+ result.authorizationDecision?.matchedRule === 'grant.exact');
4
+ }
5
+ /** Fail closed when a grant lease is required but capability requests are missing. */
6
+ export function canConsumeCapabilityGrantLease(result) {
7
+ if (!capabilityGrantLeaseRequired(result)) {
8
+ return true;
9
+ }
10
+ return (result.capabilityRequests?.length ?? 0) > 0;
11
+ }
@@ -1,9 +1,44 @@
1
- import type { ApprovalStateFile } from '../types.js';
1
+ import type { ApprovalRecord, ApprovalStateFile } from '../types.js';
2
2
  import type { CapabilityGrantV1 } from './grant.js';
3
3
  import type { CapabilityRequestV1 } from './request.js';
4
+ export type GrantBundleValidationFailureReason = 'legacy_record' | 'empty_requests' | 'cardinality_mismatch' | 'grant_scope_too_broad' | 'grant_inactive' | 'grant_mismatch';
5
+ export type GrantBundleValidationResult = {
6
+ ok: true;
7
+ approval: ApprovalRecord;
8
+ } | {
9
+ ok: false;
10
+ reason: GrantBundleValidationFailureReason;
11
+ };
12
+ export declare function grantsFromApproval(approval: {
13
+ grant?: CapabilityGrantV1;
14
+ grants?: CapabilityGrantV1[];
15
+ }): CapabilityGrantV1[];
16
+ export declare function approvalGrantBundleExhausted(approval: {
17
+ grant?: CapabilityGrantV1;
18
+ grants?: CapabilityGrantV1[];
19
+ }): boolean;
20
+ export declare function consumeApprovalGrantBundle(approval: ApprovalRecord, requests: readonly CapabilityRequestV1[]): {
21
+ approval: ApprovalRecord;
22
+ consumed: boolean;
23
+ };
24
+ /** Validate and consume a marked exact bundle without mutating on any failure. */
25
+ export declare function validateAndConsumeGrantBundle(approval: ApprovalRecord, requests: readonly CapabilityRequestV1[], now?: number): GrantBundleValidationResult;
26
+ /** Revalidate a marked bundle during an active replay lease without consuming it twice. */
27
+ export declare function validateGrantBundleForLeaseReuse(approval: ApprovalRecord, requests: readonly CapabilityRequestV1[], now?: number): GrantBundleValidationResult;
28
+ /** Consume every grant in an approved record after fingerprint/hash replay validation. */
29
+ export declare function consumeApprovedRecordGrantBundle(approval: ApprovalRecord): {
30
+ approval: ApprovalRecord;
31
+ consumed: boolean;
32
+ };
33
+ export declare function decrementApprovalLegacyGrant(approval: ApprovalRecord): ApprovalRecord;
4
34
  export declare function grantsFromApprovedState(state: ApprovalStateFile, repoRoot: string): CapabilityGrantV1[];
5
35
  export declare function consumeGrantLease(state: ApprovalStateFile, grantId: string): {
6
36
  state: ApprovalStateFile;
7
37
  consumed: boolean;
8
38
  };
39
+ /** Consume one lease for each request; all must succeed (atomic bundle consumption). */
40
+ export declare function consumeGrantLeasesForRequests(state: ApprovalStateFile, requests: CapabilityRequestV1[]): {
41
+ state: ApprovalStateFile;
42
+ consumed: boolean;
43
+ };
9
44
  export declare function findMatchingGrant(grants: CapabilityGrantV1[] | undefined, request: CapabilityRequestV1): CapabilityGrantV1 | undefined;