@guilz-dev/belay 0.4.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 (240) hide show
  1. package/README.md +27 -2
  2. package/dist/adapters/claude/runtime-entry.js +54 -25
  3. package/dist/adapters/layouts/scope.js +3 -2
  4. package/dist/adapters/shared/gate-runtime.d.ts +8 -2
  5. package/dist/adapters/shared/gate-runtime.js +579 -181
  6. package/dist/bundle/claude-runtime.mjs +13574 -6334
  7. package/dist/bundle/codex-runtime.mjs +13108 -5903
  8. package/dist/bundle/cursor-runtime.mjs +12676 -5476
  9. package/dist/cli.js +80 -2
  10. package/dist/commands/approve.js +34 -10
  11. package/dist/commands/classify-for-report.js +1 -1
  12. package/dist/commands/config.d.ts +8 -0
  13. package/dist/commands/config.js +134 -11
  14. package/dist/commands/doctor.js +87 -1
  15. package/dist/commands/explain.js +11 -1
  16. package/dist/commands/judge.js +3 -2
  17. package/dist/commands/recovery-checkpoints.d.ts +164 -0
  18. package/dist/commands/recovery-checkpoints.js +347 -0
  19. package/dist/commands/revoke.js +19 -7
  20. package/dist/commands/session.d.ts +20 -0
  21. package/dist/commands/session.js +51 -0
  22. package/dist/commands/status.js +7 -0
  23. package/dist/config-io.js +91 -23
  24. package/dist/conformance/guarantee-posture.d.ts +21 -0
  25. package/dist/conformance/guarantee-posture.js +46 -0
  26. package/dist/conformance/guarantee-table.d.ts +1 -0
  27. package/dist/conformance/guarantee-table.js +20 -9
  28. package/dist/conformance/types.d.ts +1 -0
  29. package/dist/core/approval-replay.d.ts +1 -1
  30. package/dist/core/approval-replay.js +4 -1
  31. package/dist/core/approval-service.d.ts +6 -1
  32. package/dist/core/approval-service.js +87 -24
  33. package/dist/core/approval.d.ts +4 -0
  34. package/dist/core/approval.js +19 -5
  35. package/dist/core/audit-types.d.ts +13 -0
  36. package/dist/core/capability/approval-state-mutation.d.ts +32 -0
  37. package/dist/core/capability/approval-state-mutation.js +165 -0
  38. package/dist/core/capability/approval-v3.d.ts +32 -0
  39. package/dist/core/capability/approval-v3.js +114 -0
  40. package/dist/core/capability/attestation.d.ts +16 -0
  41. package/dist/core/capability/attestation.js +62 -0
  42. package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
  43. package/dist/core/capability/boundary-attestation-sign.js +84 -0
  44. package/dist/core/capability/boundary-driver-container.d.ts +19 -0
  45. package/dist/core/capability/boundary-driver-container.js +189 -0
  46. package/dist/core/capability/boundary-driver.d.ts +29 -0
  47. package/dist/core/capability/boundary-driver.js +44 -0
  48. package/dist/core/capability/boundary-egress.d.ts +21 -0
  49. package/dist/core/capability/boundary-egress.js +109 -0
  50. package/dist/core/capability/boundary-grant-materialize.d.ts +14 -0
  51. package/dist/core/capability/boundary-grant-materialize.js +93 -0
  52. package/dist/core/capability/boundary-profile.d.ts +15 -0
  53. package/dist/core/capability/boundary-profile.js +25 -0
  54. package/dist/core/capability/boundary-run.d.ts +48 -0
  55. package/dist/core/capability/boundary-run.js +51 -0
  56. package/dist/core/capability/boundary-session.d.ts +61 -0
  57. package/dist/core/capability/boundary-session.js +138 -0
  58. package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
  59. package/dist/core/capability/boundary-workspace-mount.js +68 -0
  60. package/dist/core/capability/capability-request-hash.d.ts +2 -0
  61. package/dist/core/capability/capability-request-hash.js +8 -0
  62. package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
  63. package/dist/core/capability/gate-latency-ratchet.js +18 -0
  64. package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
  65. package/dist/core/capability/gate-policy-shadow.js +130 -0
  66. package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
  67. package/dist/core/capability/gate-shadow-audit.js +1 -0
  68. package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
  69. package/dist/core/capability/gate-shadow-ratchet.js +81 -0
  70. package/dist/core/capability/grant-consumption.d.ts +4 -0
  71. package/dist/core/capability/grant-consumption.js +11 -0
  72. package/dist/core/capability/grant-lease.d.ts +44 -0
  73. package/dist/core/capability/grant-lease.js +243 -0
  74. package/dist/core/capability/grant-loader.d.ts +12 -0
  75. package/dist/core/capability/grant-loader.js +23 -0
  76. package/dist/core/capability/grant-match.d.ts +6 -0
  77. package/dist/core/capability/grant-match.js +96 -0
  78. package/dist/core/capability/grant.d.ts +16 -0
  79. package/dist/core/capability/grant.js +16 -0
  80. package/dist/core/capability/index.d.ts +8 -0
  81. package/dist/core/capability/index.js +7 -0
  82. package/dist/core/capability/limits.d.ts +7 -0
  83. package/dist/core/capability/limits.js +55 -0
  84. package/dist/core/capability/policy-bridge.d.ts +18 -0
  85. package/dist/core/capability/policy-bridge.js +51 -0
  86. package/dist/core/capability/policy-engine.d.ts +77 -0
  87. package/dist/core/capability/policy-engine.js +663 -0
  88. package/dist/core/capability/policy-types.d.ts +20 -0
  89. package/dist/core/capability/policy-types.js +1 -0
  90. package/dist/core/capability/request.d.ts +48 -0
  91. package/dist/core/capability/request.js +1 -0
  92. package/dist/core/capability/resolver.d.ts +4 -0
  93. package/dist/core/capability/resolver.js +4 -0
  94. package/dist/core/classify-subagent.d.ts +2 -1
  95. package/dist/core/classify-subagent.js +38 -2
  96. package/dist/core/classify-tool.js +126 -62
  97. package/dist/core/config.d.ts +34 -3
  98. package/dist/core/config.js +118 -3
  99. package/dist/core/effect-ir/audit.d.ts +9 -0
  100. package/dist/core/effect-ir/audit.js +86 -0
  101. package/dist/core/effect-ir/build.d.ts +31 -0
  102. package/dist/core/effect-ir/build.js +277 -0
  103. package/dist/core/effect-ir/index.d.ts +8 -0
  104. package/dist/core/effect-ir/index.js +7 -0
  105. package/dist/core/effect-ir/normalize.d.ts +4 -0
  106. package/dist/core/effect-ir/normalize.js +104 -0
  107. package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
  108. package/dist/core/effect-ir/package-exec-eval.js +152 -0
  109. package/dist/core/effect-ir/package-exec.d.ts +29 -0
  110. package/dist/core/effect-ir/package-exec.js +292 -0
  111. package/dist/core/effect-ir/policy.d.ts +13 -0
  112. package/dist/core/effect-ir/policy.js +77 -0
  113. package/dist/core/effect-ir/types.d.ts +55 -0
  114. package/dist/core/effect-ir/types.js +1 -0
  115. package/dist/core/egress/allowlist.d.ts +1 -0
  116. package/dist/core/egress/allowlist.js +8 -0
  117. package/dist/core/egress-approval.js +45 -51
  118. package/dist/core/gate-contract.d.ts +5 -0
  119. package/dist/core/gate-contract.js +5 -0
  120. package/dist/core/gate-engine.js +30 -8
  121. package/dist/core/index.d.ts +3 -1
  122. package/dist/core/index.js +2 -1
  123. package/dist/core/judge-cloud-consent.js +29 -15
  124. package/dist/core/judge-doctor.js +39 -14
  125. package/dist/core/judge-fallback-hints.d.ts +13 -0
  126. package/dist/core/judge-fallback-hints.js +108 -0
  127. package/dist/core/network-endpoint.d.ts +10 -0
  128. package/dist/core/network-endpoint.js +51 -0
  129. package/dist/core/path-utils.d.ts +1 -0
  130. package/dist/core/path-utils.js +5 -2
  131. package/dist/core/process-runner.d.ts +10 -0
  132. package/dist/core/process-runner.js +124 -0
  133. package/dist/core/recover-advice.js +2 -1
  134. package/dist/core/recovery/artifact-store.d.ts +29 -0
  135. package/dist/core/recovery/artifact-store.js +336 -0
  136. package/dist/core/recovery/capability.d.ts +3 -0
  137. package/dist/core/recovery/capability.js +22 -0
  138. package/dist/core/recovery/checkpoint.d.ts +25 -0
  139. package/dist/core/recovery/checkpoint.js +260 -0
  140. package/dist/core/recovery/fail-closed.d.ts +6 -0
  141. package/dist/core/recovery/fail-closed.js +25 -0
  142. package/dist/core/recovery/index.d.ts +5 -0
  143. package/dist/core/recovery/index.js +4 -0
  144. package/dist/core/recovery/operator-guidance.d.ts +7 -0
  145. package/dist/core/recovery/operator-guidance.js +39 -0
  146. package/dist/core/recovery/reconcile.d.ts +3 -0
  147. package/dist/core/recovery/reconcile.js +48 -0
  148. package/dist/core/recovery/resource-identity.d.ts +4 -0
  149. package/dist/core/recovery/resource-identity.js +34 -0
  150. package/dist/core/recovery/restore.d.ts +13 -0
  151. package/dist/core/recovery/restore.js +114 -0
  152. package/dist/core/recovery/snapshot-node.d.ts +27 -0
  153. package/dist/core/recovery/snapshot-node.js +261 -0
  154. package/dist/core/recovery/types.d.ts +104 -0
  155. package/dist/core/recovery/types.js +1 -0
  156. package/dist/core/standing-allow.js +3 -0
  157. package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
  158. package/dist/core/transactional/apply-observed-changes.js +275 -0
  159. package/dist/core/transactional/backend-selector.d.ts +8 -0
  160. package/dist/core/transactional/backend-selector.js +118 -0
  161. package/dist/core/transactional/backend.d.ts +55 -0
  162. package/dist/core/transactional/backend.js +1 -0
  163. package/dist/core/transactional/diff-evaluator.js +4 -1
  164. package/dist/core/transactional/eligibility.js +4 -0
  165. package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
  166. package/dist/core/transactional/file-checkpoint-backend.js +278 -0
  167. package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
  168. package/dist/core/transactional/file-checkpoint-git.js +217 -0
  169. package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
  170. package/dist/core/transactional/file-checkpoint-staging.js +54 -0
  171. package/dist/core/transactional/file-clone.d.ts +15 -0
  172. package/dist/core/transactional/file-clone.js +139 -0
  173. package/dist/core/transactional/file-tree-path.d.ts +7 -0
  174. package/dist/core/transactional/file-tree-path.js +50 -0
  175. package/dist/core/transactional/file-tree.d.ts +41 -0
  176. package/dist/core/transactional/file-tree.js +186 -0
  177. package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
  178. package/dist/core/transactional/git-worktree-backend.js +50 -0
  179. package/dist/core/transactional/git-worktree.d.ts +14 -9
  180. package/dist/core/transactional/git-worktree.js +62 -87
  181. package/dist/core/transactional/index.d.ts +11 -0
  182. package/dist/core/transactional/index.js +10 -0
  183. package/dist/core/transactional/reasons.d.ts +1 -0
  184. package/dist/core/transactional/reasons.js +5 -0
  185. package/dist/core/transactional/runner.js +171 -23
  186. package/dist/core/transactional/snapshot-node.d.ts +26 -0
  187. package/dist/core/transactional/snapshot-node.js +87 -0
  188. package/dist/core/transactional/types.d.ts +21 -1
  189. package/dist/core/types.d.ts +30 -3
  190. package/dist/core/verdict/adapter.d.ts +3 -4
  191. package/dist/core/verdict/adapter.js +24 -12
  192. package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
  193. package/dist/core/verdict/cursor-acp-client.js +345 -0
  194. package/dist/core/verdict/egress-classify.d.ts +2 -2
  195. package/dist/core/verdict/egress-classify.js +2 -2
  196. package/dist/core/verdict/git-classifier.d.ts +26 -0
  197. package/dist/core/verdict/git-classifier.js +533 -0
  198. package/dist/core/verdict/judge-audit.js +1 -0
  199. package/dist/core/verdict/judge-baseline.d.ts +1 -1
  200. package/dist/core/verdict/judge-baseline.js +1 -0
  201. package/dist/core/verdict/judge-broker-service.js +12 -0
  202. package/dist/core/verdict/judge-catalog.js +1 -0
  203. package/dist/core/verdict/judge-cli-fingerprint.d.ts +1 -0
  204. package/dist/core/verdict/judge-cli-fingerprint.js +6 -0
  205. package/dist/core/verdict/judge-cli.js +5 -3
  206. package/dist/core/verdict/judge-provider-matrix.js +1 -1
  207. package/dist/core/verdict/judge-runtime-config.d.ts +2 -0
  208. package/dist/core/verdict/judge-runtime-config.js +10 -6
  209. package/dist/core/verdict/judge-session-broker.d.ts +1 -1
  210. package/dist/core/verdict/judge-session-broker.js +10 -5
  211. package/dist/core/verdict/judge-transport.d.ts +5 -1
  212. package/dist/core/verdict/judge-transport.js +24 -14
  213. package/dist/core/verdict/judge.d.ts +2 -15
  214. package/dist/core/verdict/judge.js +5 -68
  215. package/dist/core/verdict/launcher-resolve.d.ts +2 -0
  216. package/dist/core/verdict/launcher-resolve.js +14 -16
  217. package/dist/core/verdict/prescan.d.ts +15 -0
  218. package/dist/core/verdict/prescan.js +68 -0
  219. package/dist/core/verdict/shell-policy.d.ts +26 -0
  220. package/dist/core/verdict/shell-policy.js +40 -0
  221. package/dist/core/verdict/shell-semantics.d.ts +24 -0
  222. package/dist/core/verdict/shell-semantics.js +225 -0
  223. package/dist/core/verdict/types.d.ts +18 -1
  224. package/dist/core/verdict/verdict.js +313 -178
  225. package/dist/corpus/evaluate.js +3 -7
  226. package/dist/corpus/gate-latency-budget.d.ts +39 -0
  227. package/dist/corpus/gate-latency-budget.js +33 -0
  228. package/dist/corpus/mutators.d.ts +1 -1
  229. package/dist/corpus/mutators.js +11 -0
  230. package/dist/corpus/runtime-match.js +1 -3
  231. package/dist/corpus/standing-allow-catalog.generated.js +29 -9
  232. package/dist/judge-broker-daemon.js +15 -1
  233. package/dist/services/sandbox-service.d.ts +4 -0
  234. package/dist/services/sandbox-service.js +28 -2
  235. package/dist/types.d.ts +13 -2
  236. package/dist/version.d.ts +1 -1
  237. package/dist/version.js +1 -1
  238. package/package.json +1 -1
  239. package/skills/belay/SKILL.md +8 -3
  240. package/skills/belay/belay-approve.md +10 -7
package/dist/config-io.js CHANGED
@@ -1,8 +1,10 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { existsSync } from 'node:fs';
2
- import { chmod, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { chmod, mkdir, open, readFile, rename, unlink, writeFile } from 'node:fs/promises';
3
4
  import path from 'node:path';
4
5
  import { getAdapterLayout } from './adapters/layouts/index.js';
5
6
  import { compactApprovals, isExpired, mergeApprovalStates } from './core/approval.js';
7
+ import { mutateApprovalStateWithRetry } from './core/capability/approval-state-mutation.js';
6
8
  import { approvedApprovalsFile, belayStateDir, configuredControlPlaneDir, mergeConfig, pendingApprovalsFile, } from './core/config.js';
7
9
  import { resolveLayeredConfig, teamConfigPath, } from './core/config-layers.js';
8
10
  export function resolveAdapterName(config) {
@@ -49,6 +51,42 @@ const APPROVAL_STATE_FILES = ['pending-approvals.json', 'approved-approvals.json
49
51
  function approvalFilesExist(dir) {
50
52
  return APPROVAL_STATE_FILES.some((fileName) => existsSync(path.join(dir, fileName)));
51
53
  }
54
+ async function isCorruptApprovalStateFile(filePath) {
55
+ const raw = await readFile(filePath, 'utf8');
56
+ try {
57
+ JSON.parse(raw);
58
+ return false;
59
+ }
60
+ catch (error) {
61
+ return error instanceof SyntaxError;
62
+ }
63
+ }
64
+ function normalizeApprovalStateFile(parsed) {
65
+ const version = parsed.version === 3 ? 3 : parsed.version === 2 ? 2 : 1;
66
+ return {
67
+ version,
68
+ revision: typeof parsed.revision === 'number' ? parsed.revision : undefined,
69
+ approvals: Array.isArray(parsed.approvals) ? parsed.approvals : [],
70
+ };
71
+ }
72
+ /** Gate reads: corrupt JSON is treated as empty state (fail-closed). */
73
+ async function readApprovalStateFileForGate(filePath) {
74
+ const raw = await readFile(filePath, 'utf8');
75
+ try {
76
+ return normalizeApprovalStateFile(JSON.parse(raw));
77
+ }
78
+ catch (error) {
79
+ if (error instanceof SyntaxError) {
80
+ return { version: 1, approvals: [] };
81
+ }
82
+ throw error;
83
+ }
84
+ }
85
+ /** Migration reads: corrupt JSON must not be treated as empty. */
86
+ async function readApprovalStateFileStrict(filePath) {
87
+ const raw = await readFile(filePath, 'utf8');
88
+ return normalizeApprovalStateFile(JSON.parse(raw));
89
+ }
52
90
  async function repoLocalApprovalsEmpty(repoRoot, config) {
53
91
  const repoLocalDir = repoLocalStateDirFor(repoRoot, config);
54
92
  if (!approvalFilesExist(repoLocalDir)) {
@@ -59,28 +97,43 @@ async function repoLocalApprovalsEmpty(repoRoot, config) {
59
97
  if (!existsSync(filePath)) {
60
98
  continue;
61
99
  }
62
- const state = await readApprovalStateFile(filePath);
100
+ if (await isCorruptApprovalStateFile(filePath)) {
101
+ return false;
102
+ }
103
+ const state = await readApprovalStateFileForGate(filePath);
63
104
  if (state.approvals.length > 0) {
64
105
  return false;
65
106
  }
66
107
  }
67
108
  return true;
68
109
  }
69
- async function readApprovalStateFile(filePath) {
70
- const raw = await readFile(filePath, 'utf8');
71
- const parsed = JSON.parse(raw);
72
- return {
73
- version: parsed.version === 2 ? 2 : 1,
74
- approvals: Array.isArray(parsed.approvals) ? parsed.approvals : [],
75
- };
76
- }
77
110
  async function writeApprovalStateFile(filePath, state) {
78
- await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
79
- await writeFile(filePath, `${JSON.stringify(compactApprovals(state), null, 2)}\n`, {
80
- encoding: 'utf8',
81
- mode: 0o600,
82
- });
83
- await chmod(filePath, 0o600);
111
+ const directory = path.dirname(filePath);
112
+ await mkdir(directory, { recursive: true, mode: 0o700 });
113
+ const tempPath = path.join(directory, `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
114
+ let handle = null;
115
+ try {
116
+ handle = await open(tempPath, 'wx', 0o600);
117
+ await handle.writeFile(`${JSON.stringify(compactApprovals(state), null, 2)}\n`, 'utf8');
118
+ await handle.sync();
119
+ await handle.close();
120
+ handle = null;
121
+ await rename(tempPath, filePath);
122
+ await chmod(filePath, 0o600);
123
+ const directoryHandle = await open(directory, 'r');
124
+ try {
125
+ await directoryHandle.sync().catch(() => undefined);
126
+ }
127
+ finally {
128
+ await directoryHandle.close();
129
+ }
130
+ }
131
+ finally {
132
+ if (handle) {
133
+ await handle.close().catch(() => undefined);
134
+ }
135
+ await unlink(tempPath).catch(() => undefined);
136
+ }
84
137
  }
85
138
  async function migrateApprovalFilesBetween(sourceDir, targetDir) {
86
139
  await mkdir(targetDir, { recursive: true, mode: 0o700 });
@@ -90,14 +143,29 @@ async function migrateApprovalFilesBetween(sourceDir, targetDir) {
90
143
  if (!existsSync(from)) {
91
144
  continue;
92
145
  }
93
- if (!existsSync(to)) {
94
- await copyFile(from, to);
95
- await chmod(to, 0o600);
146
+ if ((await isCorruptApprovalStateFile(from)) ||
147
+ (existsSync(to) && (await isCorruptApprovalStateFile(to)))) {
96
148
  continue;
97
149
  }
98
- const targetState = await readApprovalStateFile(to);
99
- const sourceState = await readApprovalStateFile(from);
100
- await writeApprovalStateFile(to, mergeApprovalStates(targetState, sourceState));
150
+ const sourceState = await readApprovalStateFileStrict(from);
151
+ const migrated = await mutateApprovalStateWithRetry({
152
+ load: async () => ({
153
+ filePath: to,
154
+ state: existsSync(to)
155
+ ? await readApprovalStateFileStrict(to)
156
+ : { version: 1, approvals: [] },
157
+ }),
158
+ write: writeApprovalStateFile,
159
+ mutate: (targetState) => ({
160
+ state: targetState.approvals.length === 0
161
+ ? sourceState
162
+ : mergeApprovalStates(targetState, sourceState),
163
+ result: true,
164
+ }),
165
+ });
166
+ if (migrated !== true) {
167
+ throw new Error(`Failed to migrate approval state to ${to}`);
168
+ }
101
169
  }
102
170
  }
103
171
  export async function migrateRepoLocalApprovalsToControlPlane(repoRoot, config) {
@@ -172,7 +240,7 @@ export async function loadApprovalState(repoRoot, fileName, config) {
172
240
  if (!existsSync(filePath)) {
173
241
  return { version: 1, approvals: [] };
174
242
  }
175
- return readApprovalStateFile(filePath);
243
+ return readApprovalStateFileForGate(filePath);
176
244
  }
177
245
  export async function saveApprovalState(repoRoot, fileName, state, config) {
178
246
  const filePath = fileName === 'pending-approvals.json'
@@ -0,0 +1,21 @@
1
+ import type { BoundaryAttestation } from '../core/capability/attestation.js';
2
+ import type { BelayConfigV4 } from '../core/config.js';
3
+ import type { LayerProfileId } from './types.js';
4
+ export interface GuaranteePosture {
5
+ configuredProfile: LayerProfileId;
6
+ attestedProfile: LayerProfileId;
7
+ attestationFresh: boolean;
8
+ l1FullConfigured: boolean;
9
+ l1FullAttested: boolean;
10
+ postureMismatch: boolean;
11
+ }
12
+ export declare function resolveConfiguredProfile(config: BelayConfigV4, egressProxyRunning: boolean): LayerProfileId;
13
+ export declare function resolveAttestedProfile(params: {
14
+ attestation: BoundaryAttestation | null;
15
+ l1FullConfigured: boolean;
16
+ }): LayerProfileId;
17
+ export declare function evaluateGuaranteePosture(params: {
18
+ config: BelayConfigV4;
19
+ attestation: BoundaryAttestation | null;
20
+ egressProxyRunning: boolean;
21
+ }): GuaranteePosture;
@@ -0,0 +1,46 @@
1
+ import { isAttestationFresh } from '../core/capability/attestation.js';
2
+ import { evaluateL1FullStatus } from '../core/capability/broker.js';
3
+ export function resolveConfiguredProfile(config, egressProxyRunning) {
4
+ const l1Full = evaluateL1FullStatus({ config, egressProxyRunning });
5
+ if (l1Full.active) {
6
+ return 'l1-full';
7
+ }
8
+ if (config.egress.enabled) {
9
+ return 'l1-partial-egress';
10
+ }
11
+ if (config.policy.transactional.enabled) {
12
+ return 'l1-l2-transactional';
13
+ }
14
+ return 'l3-l4-only';
15
+ }
16
+ export function resolveAttestedProfile(params) {
17
+ if (!params.attestation || !isAttestationFresh(params.attestation)) {
18
+ return 'l3-l4-only';
19
+ }
20
+ if (params.l1FullConfigured &&
21
+ params.attestation.deniesUngrantedEffects &&
22
+ params.attestation.materializesGrants) {
23
+ return 'l1-full';
24
+ }
25
+ return 'l3-l4-only';
26
+ }
27
+ export function evaluateGuaranteePosture(params) {
28
+ const l1Full = evaluateL1FullStatus({
29
+ config: params.config,
30
+ egressProxyRunning: params.egressProxyRunning,
31
+ });
32
+ const configuredProfile = resolveConfiguredProfile(params.config, params.egressProxyRunning);
33
+ const attestedProfile = resolveAttestedProfile({
34
+ attestation: params.attestation,
35
+ l1FullConfigured: l1Full.active,
36
+ });
37
+ return {
38
+ configuredProfile,
39
+ attestedProfile,
40
+ attestationFresh: params.attestation ? isAttestationFresh(params.attestation) : false,
41
+ l1FullConfigured: l1Full.active,
42
+ l1FullAttested: attestedProfile === 'l1-full' &&
43
+ Boolean(params.attestation?.deniesUngrantedEffects && params.attestation.materializesGrants),
44
+ postureMismatch: configuredProfile === 'l1-full' && attestedProfile !== 'l1-full',
45
+ };
46
+ }
@@ -1,4 +1,5 @@
1
1
  import type { LayerConformanceScenario, LayerProfileId } from './types.js';
2
+ export { evaluateGuaranteePosture, type GuaranteePosture } from './guarantee-posture.js';
2
3
  export type { LayerConformanceScenario, LayerProfileId } from './types.js';
3
4
  export interface GuaranteeTableRow {
4
5
  profile: LayerProfileId;
@@ -1,3 +1,4 @@
1
+ export { evaluateGuaranteePosture } from './guarantee-posture.js';
1
2
  /** Normative rows — keep in sync with docs/guarantee-table.md */
2
3
  export const GUARANTEE_TABLE_ROWS = [
3
4
  {
@@ -33,9 +34,10 @@ export const GUARANTEE_SCENARIOS = {
33
34
  permission: 'allow',
34
35
  },
35
36
  {
36
- id: 'l3-allow-read-egress',
37
+ id: 'l3-deny-read-egress',
37
38
  command: 'curl https://example.com',
38
- permission: 'allow',
39
+ permission: 'deny',
40
+ reason: 'external_effect',
39
41
  },
40
42
  ],
41
43
  'l1-partial-egress': [
@@ -45,9 +47,10 @@ export const GUARANTEE_SCENARIOS = {
45
47
  permission: 'allow',
46
48
  },
47
49
  {
48
- id: 'l1p-allow-read-egress',
50
+ id: 'l1p-deny-read-egress',
49
51
  command: 'curl https://example.com',
50
- permission: 'allow',
52
+ permission: 'deny',
53
+ reason: 'external_effect',
51
54
  },
52
55
  {
53
56
  id: 'l1p-deny-write-egress',
@@ -63,9 +66,16 @@ export const GUARANTEE_SCENARIOS = {
63
66
  permission: 'allow',
64
67
  },
65
68
  {
66
- id: 'l2-allow-read-egress',
67
- command: 'curl https://example.com',
69
+ id: 'l2-allow-dirty-git-file-checkpoint',
70
+ command: 'touch notes.txt',
68
71
  permission: 'allow',
72
+ substrate: 'dirty_git_file_checkpoint',
73
+ },
74
+ {
75
+ id: 'l2-deny-read-egress',
76
+ command: 'curl https://example.com',
77
+ permission: 'deny',
78
+ reason: 'external_effect',
69
79
  },
70
80
  ],
71
81
  'l1-full': [
@@ -75,9 +85,10 @@ export const GUARANTEE_SCENARIOS = {
75
85
  permission: 'allow',
76
86
  },
77
87
  {
78
- id: 'l1f-allow-read-egress',
88
+ id: 'l1f-deny-read-egress',
79
89
  command: 'curl https://example.com',
80
- permission: 'allow',
90
+ permission: 'deny',
91
+ reason: 'external_effect',
81
92
  },
82
93
  {
83
94
  id: 'l1f-deny-write-egress',
@@ -90,7 +101,7 @@ export const GUARANTEE_SCENARIOS = {
90
101
  id: 'l1f-deny-outside-repo',
91
102
  command: 'echo hi > ../../outside.txt',
92
103
  permission: 'deny',
93
- reason: 'outside_repo_redirect',
104
+ reason: 'outside_repo_mutation',
94
105
  },
95
106
  {
96
107
  id: 'l1f-deny-outside-repo-write',
@@ -5,4 +5,5 @@ export interface LayerConformanceScenario {
5
5
  reason?: string;
6
6
  kind?: 'shell' | 'tool';
7
7
  toolPayload?: Record<string, unknown>;
8
+ substrate?: 'clean_git_worktree' | 'dirty_git_file_checkpoint' | 'non_git_file_checkpoint';
8
9
  }
@@ -40,6 +40,6 @@ export declare function buildReplayEnvelopeFields(input: ReplayEnvelopeInput): P
40
40
  export declare function hasReplayEnvelope(approval: ApprovalRecord): boolean;
41
41
  export declare function validateReplayEnvelope(approval: ApprovalRecord, action: ReplayActionContext): boolean;
42
42
  export declare function canAutoReplay(config: BelayConfigV4, kind: ApprovalRecord['kind'], adapter?: ReplayAdapterId): boolean;
43
- export declare function buildRetryInstructionForConfig(config: BelayConfigV4, tokenPrefix: string, approvalId: string): string;
43
+ export declare function buildRetryInstructionForConfig(config: BelayConfigV4, tokenPrefix: string, approvalId: string, kind?: ApprovalRecord['kind'], adapter?: ReplayAdapterId): string;
44
44
  export declare function buildApprovalRecordedMessage(config: BelayConfigV4, approval: ApprovalRecord, adapter?: ReplayAdapterId): string;
45
45
  export declare function buildReplayHint(config: BelayConfigV4, approval: ApprovalRecord, adapter?: ReplayAdapterId): ApprovalReplayHint | null;
@@ -103,7 +103,10 @@ export function canAutoReplay(config, kind, adapter) {
103
103
  }
104
104
  return kind === 'shell' && scopes?.shell !== false;
105
105
  }
106
- export function buildRetryInstructionForConfig(config, tokenPrefix, approvalId) {
106
+ export function buildRetryInstructionForConfig(config, tokenPrefix, approvalId, kind, adapter) {
107
+ if (kind === 'shell' && canAutoReplay(config, kind, adapter)) {
108
+ return `To approve and run this exact shell action once, send ${tokenPrefix} ${approvalId}. No follow-up prompt is required.`;
109
+ }
107
110
  if (approvalFlow(config) === 'one_step') {
108
111
  return `To allow this action once, send ${tokenPrefix} ${approvalId}. After approval, retry the same action unchanged.`;
109
112
  }
@@ -26,11 +26,16 @@ export declare function recordApproval(params: {
26
26
  message: string;
27
27
  approval?: ApprovalStateFile['approvals'][number];
28
28
  }>;
29
- /** CLI `--replay` already executed the shell command; drop the one-shot grant. */
29
+ /** @deprecated Replay callers must claim before execution with `claimApprovedForReplay`. */
30
30
  export declare function consumeApprovedAfterCliReplay(params: {
31
31
  approvalId: string;
32
32
  store: ApprovalStore;
33
33
  }): Promise<void>;
34
+ /** Atomically spend and return a one-shot approval before executing its replay. */
35
+ export declare function claimApprovedForReplay(params: {
36
+ approvalId: string;
37
+ store: ApprovalStore;
38
+ }): Promise<ApprovalStateFile['approvals'][number] | null>;
34
39
  export declare function createGateApprovalStore(repoRoot: string, config: BelayConfigV3): ApprovalStore;
35
40
  export declare function gateApprovalStoreFromDeps(deps: {
36
41
  loadApprovals: (fileName: 'pending-approvals.json' | 'approved-approvals.json') => Promise<{
@@ -2,17 +2,25 @@ import { approvedApprovalsPath, loadApprovalState, pendingApprovalsPath, saveApp
2
2
  import { compactApprovals } from './approval.js';
3
3
  import { buildApprovalRecordedMessage } from './approval-replay.js';
4
4
  import { verifyApprovalToken } from './approval-token.js';
5
+ import { mutateApprovalStateWithRetry, mutatePendingAndApprovedWithRetry, } from './capability/approval-state-mutation.js';
6
+ import { APPROVAL_STATE_VERSION_V3, mintGrantForApprovedRecord } from './capability/approval-v3.js';
5
7
  import { configuredControlPlaneDir } from './config.js';
6
8
  export async function recordApproval(params) {
7
9
  const { approvalId, config, store, token, requireSignedToken = false, adapter } = params;
8
- const pending = await store.loadPending();
10
+ const [pending, approved] = await Promise.all([store.loadPending(), store.loadApproved()]);
9
11
  pending.state = compactApprovals(pending.state);
10
- const index = pending.state.approvals.findIndex((approval) => approval.approvalId === approvalId);
11
- if (index === -1) {
12
- await store.writePending(pending.filePath, pending.state);
12
+ approved.state = compactApprovals(approved.state);
13
+ const pendingApproval = pending.state.approvals.find((entry) => entry.approvalId === approvalId);
14
+ const approvedApproval = approved.state.approvals.find((entry) => entry.approvalId === approvalId);
15
+ const approval = pendingApproval ?? approvedApproval;
16
+ if (!approval) {
17
+ await mutateApprovalStateWithRetry({
18
+ load: store.loadPending,
19
+ write: store.writePending,
20
+ mutate: (state) => ({ state: compactApprovals(state), result: true }),
21
+ });
13
22
  return { ok: false, message: 'Belay approval not found or expired.' };
14
23
  }
15
- const [approval] = pending.state.approvals.slice(index, index + 1);
16
24
  if (requireSignedToken) {
17
25
  if (!token) {
18
26
  return { ok: false, message: 'Signed approval token required for out-of-band approval.' };
@@ -26,31 +34,86 @@ export async function recordApproval(params) {
26
34
  return { ok: false, message: 'Signed approval token does not match the pending approval.' };
27
35
  }
28
36
  }
29
- pending.state.approvals.splice(index, 1);
30
- await store.writePending(pending.filePath, pending.state);
31
- const approved = await store.loadApproved();
32
- approved.state = compactApprovals(approved.state);
33
- approved.state.approvals.push({
34
- ...approval,
35
- approvedAt: new Date().toISOString(),
37
+ if (!pendingApproval && approvedApproval) {
38
+ return {
39
+ ok: true,
40
+ message: buildApprovalRecordedMessage(config, approvedApproval, adapter),
41
+ approval: approvedApproval,
42
+ };
43
+ }
44
+ const recorded = await mutatePendingAndApprovedWithRetry({
45
+ loadPending: store.loadPending,
46
+ loadApproved: store.loadApproved,
47
+ writePending: store.writePending,
48
+ writeApproved: store.writeApproved,
49
+ mutate: (pendingState, approvedState) => {
50
+ const compactedPending = compactApprovals(pendingState);
51
+ const compactedApproved = compactApprovals(approvedState);
52
+ const existing = compactedApproved.approvals.find((entry) => entry.approvalId === approvalId);
53
+ if (existing &&
54
+ (existing.fingerprint !== approval.fingerprint || existing.repoRoot !== approval.repoRoot)) {
55
+ return null;
56
+ }
57
+ const pendingIndex = compactedPending.approvals.findIndex((entry) => entry.approvalId === approvalId &&
58
+ entry.fingerprint === approval.fingerprint &&
59
+ entry.repoRoot === approval.repoRoot);
60
+ if (pendingIndex === -1) {
61
+ return existing
62
+ ? {
63
+ pending: compactedPending,
64
+ approved: compactedApproved,
65
+ result: existing,
66
+ }
67
+ : null;
68
+ }
69
+ const [claimed] = compactedPending.approvals.splice(pendingIndex, 1);
70
+ if (!claimed) {
71
+ return null;
72
+ }
73
+ const approvedRecord = existing ??
74
+ mintGrantForApprovedRecord({
75
+ ...claimed,
76
+ approvedAt: new Date().toISOString(),
77
+ });
78
+ if (!existing) {
79
+ compactedApproved.version = APPROVAL_STATE_VERSION_V3;
80
+ compactedApproved.approvals.push(approvedRecord);
81
+ }
82
+ return {
83
+ pending: compactedPending,
84
+ approved: compactedApproved,
85
+ result: approvedRecord,
86
+ };
87
+ },
36
88
  });
37
- await store.writeApproved(approved.filePath, approved.state);
89
+ if (!recorded) {
90
+ return { ok: false, message: 'Belay approval not found, expired, or already claimed.' };
91
+ }
38
92
  return {
39
93
  ok: true,
40
- message: buildApprovalRecordedMessage(config, approval, adapter),
41
- approval,
94
+ message: buildApprovalRecordedMessage(config, recorded, adapter),
95
+ approval: recorded,
42
96
  };
43
97
  }
44
- /** CLI `--replay` already executed the shell command; drop the one-shot grant. */
98
+ /** @deprecated Replay callers must claim before execution with `claimApprovedForReplay`. */
45
99
  export async function consumeApprovedAfterCliReplay(params) {
46
- const approved = await params.store.loadApproved();
47
- approved.state = compactApprovals(approved.state);
48
- const remaining = approved.state.approvals.filter((approval) => approval.approvalId !== params.approvalId);
49
- if (remaining.length === approved.state.approvals.length) {
50
- return;
51
- }
52
- approved.state.approvals = remaining;
53
- await params.store.writeApproved(approved.filePath, approved.state);
100
+ await claimApprovedForReplay(params);
101
+ }
102
+ /** Atomically spend and return a one-shot approval before executing its replay. */
103
+ export async function claimApprovedForReplay(params) {
104
+ return mutateApprovalStateWithRetry({
105
+ load: params.store.loadApproved,
106
+ write: params.store.writeApproved,
107
+ mutate: (state) => {
108
+ const compacted = compactApprovals(state);
109
+ const index = compacted.approvals.findIndex((approval) => approval.approvalId === params.approvalId);
110
+ if (index === -1) {
111
+ return null;
112
+ }
113
+ const [claimed] = compacted.approvals.splice(index, 1);
114
+ return { state: compacted, result: claimed ?? null };
115
+ },
116
+ });
54
117
  }
55
118
  export function createGateApprovalStore(repoRoot, config) {
56
119
  return {
@@ -24,6 +24,8 @@ export declare function createApprovalRecordWithEnvelope(params: {
24
24
  payload?: Record<string, unknown>;
25
25
  };
26
26
  scopeHint?: ApprovalRecord['scopeHint'];
27
+ capabilityRequests?: ApprovalRecord['capabilityRequests'];
28
+ effectPlanHash?: string;
27
29
  }): ApprovalRecord;
28
30
  export declare function createApprovalRecord(params: {
29
31
  kind: ApprovalRecord['kind'];
@@ -40,4 +42,6 @@ export declare function createApprovalRecord(params: {
40
42
  payloadHash?: string;
41
43
  payloadJson?: string;
42
44
  scopeHint?: ApprovalRecord['scopeHint'];
45
+ capabilityRequests?: ApprovalRecord['capabilityRequests'];
46
+ effectPlanHash?: string;
43
47
  }): ApprovalRecord;
@@ -1,4 +1,5 @@
1
1
  import { buildReplayEnvelopeFields } from './approval-replay.js';
2
+ import { attachCapabilityEnvelope } from './capability/approval-v3.js';
2
3
  import { DEFAULT_APPROVAL_CONFIG } from './config.js';
3
4
  /** @deprecated Use `DEFAULT_APPROVAL_CONFIG.executionLeaseMs` */
4
5
  export const APPROVAL_EXECUTION_LEASE_MS = DEFAULT_APPROVAL_CONFIG.executionLeaseMs;
@@ -6,19 +7,25 @@ export function nowIso() {
6
7
  return new Date().toISOString();
7
8
  }
8
9
  export function isExpired(approval) {
9
- return Date.parse(approval.expiresAt) <= Date.now();
10
+ const expiresAt = Date.parse(approval.expiresAt);
11
+ return !Number.isFinite(expiresAt) || expiresAt <= Date.now();
10
12
  }
11
13
  export function isExecutionLeaseExpired(approval) {
12
14
  if (!approval.executionLeaseExpiresAt) {
13
15
  return false;
14
16
  }
15
- return Date.parse(approval.executionLeaseExpiresAt) <= Date.now();
17
+ const expiresAt = Date.parse(approval.executionLeaseExpiresAt);
18
+ return !Number.isFinite(expiresAt) || expiresAt <= Date.now();
16
19
  }
17
20
  export function compactApprovals(state) {
18
- return {
21
+ const compacted = {
19
22
  version: state.version,
20
23
  approvals: state.approvals.filter((approval) => !isExpired(approval) && !isExecutionLeaseExpired(approval)),
21
24
  };
25
+ if (state.revision !== undefined) {
26
+ compacted.revision = state.revision;
27
+ }
28
+ return compacted;
22
29
  }
23
30
  export function mergeApprovalStates(target, source) {
24
31
  const byId = new Map();
@@ -31,7 +38,12 @@ export function mergeApprovalStates(target, source) {
31
38
  }
32
39
  }
33
40
  return compactApprovals({
34
- version: target.version === 2 || source.version === 2 ? 2 : 1,
41
+ version: target.version === 3 || source.version === 3
42
+ ? 3
43
+ : target.version === 2 || source.version === 2
44
+ ? 2
45
+ : 1,
46
+ revision: Math.max(target.revision ?? 0, source.revision ?? 0),
35
47
  approvals: [...byId.values()],
36
48
  });
37
49
  }
@@ -78,6 +90,8 @@ export function createApprovalRecordWithEnvelope(params) {
78
90
  payloadHash: envelope.payloadHash,
79
91
  payloadJson: envelope.payloadJson,
80
92
  scopeHint: params.scopeHint,
93
+ capabilityRequests: params.capabilityRequests,
94
+ effectPlanHash: params.effectPlanHash,
81
95
  });
82
96
  }
83
97
  export function createApprovalRecord(params) {
@@ -112,5 +126,5 @@ export function createApprovalRecord(params) {
112
126
  if (params.scopeHint) {
113
127
  record.scopeHint = params.scopeHint;
114
128
  }
115
- return record;
129
+ return attachCapabilityEnvelope(record, params.capabilityRequests, params.effectPlanHash);
116
130
  }
@@ -20,10 +20,23 @@ export interface AuditRecord {
20
20
  predictedAssessment?: Assessment;
21
21
  observedAssessment?: Assessment;
22
22
  transactional?: boolean;
23
+ transactionalBackend?: 'git_worktree' | 'file_checkpoint';
24
+ resourceKind?: 'git_repository' | 'directory';
25
+ baselineTreeHash?: string;
26
+ snapshotFileCount?: number;
27
+ snapshotSourceBytes?: number;
28
+ snapshotWorkspaceBytes?: number;
29
+ snapshotCopyStrategy?: 'clonefile' | 'reflink' | 'copy';
30
+ snapshotPrepareMs?: number;
23
31
  transactionalReason?: string;
24
32
  transactionalCategories?: string[];
25
33
  transactionalChangeCount?: number;
26
34
  transactionalSkipReason?: string;
35
+ transactionalSkipDetail?: string;
36
+ recoveryCheckpointId?: string;
37
+ recoveryBackend?: string;
38
+ recoveryProofHash?: string;
39
+ recoveryState?: string;
27
40
  replayContext?: AuditReplayContext;
28
41
  actionSnapshot?: AuditActionSnapshot;
29
42
  [key: string]: unknown;
@@ -0,0 +1,32 @@
1
+ import type { ApprovalStateFile } from '../types.js';
2
+ export declare function withStateFileLock<T>(filePath: string, operation: () => Promise<T>, maxRetries?: number): Promise<T>;
3
+ export declare function mutateApprovalStateWithRetry<T>(params: {
4
+ load: () => Promise<{
5
+ filePath: string;
6
+ state: ApprovalStateFile;
7
+ }>;
8
+ write: (filePath: string, state: ApprovalStateFile) => Promise<void>;
9
+ mutate: (state: ApprovalStateFile) => {
10
+ state: ApprovalStateFile;
11
+ result: T;
12
+ } | null;
13
+ maxRetries?: number;
14
+ }): Promise<T | null>;
15
+ export declare function mutatePendingAndApprovedWithRetry<T>(params: {
16
+ loadPending: () => Promise<{
17
+ filePath: string;
18
+ state: ApprovalStateFile;
19
+ }>;
20
+ loadApproved: () => Promise<{
21
+ filePath: string;
22
+ state: ApprovalStateFile;
23
+ }>;
24
+ writePending: (filePath: string, state: ApprovalStateFile) => Promise<void>;
25
+ writeApproved: (filePath: string, state: ApprovalStateFile) => Promise<void>;
26
+ mutate: (pending: ApprovalStateFile, approved: ApprovalStateFile) => {
27
+ pending: ApprovalStateFile;
28
+ approved: ApprovalStateFile;
29
+ result: T;
30
+ } | null;
31
+ maxRetries?: number;
32
+ }): Promise<T | null>;