@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
@@ -0,0 +1,164 @@
1
+ export type RecoveryCheckpointSubcommand = 'status' | 'list' | 'show' | 'apply';
2
+ export declare function recoveryCheckpointCommand(options: {
3
+ targetDir?: string;
4
+ subcommand: RecoveryCheckpointSubcommand;
5
+ checkpointId?: string;
6
+ }): Promise<{
7
+ ok: boolean;
8
+ subcommand: string;
9
+ repoRoot: string;
10
+ backend: string;
11
+ availableBackends: string[];
12
+ backendCounts: {
13
+ [k: string]: number;
14
+ };
15
+ fileCheckpoint: {
16
+ enabled: boolean;
17
+ allowNonGit: boolean;
18
+ isolation: string | null;
19
+ copyStrategy: import("../core/transactional/file-clone.js").FileCloneStrategy | undefined;
20
+ probe: string;
21
+ };
22
+ enabled: boolean;
23
+ checkpointCount: number;
24
+ storageBytes: number;
25
+ recoverableCount: number;
26
+ needsManualRepairCount: number;
27
+ verifiedReceiptCount: number;
28
+ states: {
29
+ [k: string]: number;
30
+ };
31
+ advisories: string[];
32
+ limits: {
33
+ enabled: boolean;
34
+ appliedRetentionHours: number;
35
+ restoredRetentionHours: number;
36
+ maxCheckpoints: number;
37
+ maxBytes: number;
38
+ };
39
+ checkpoints?: undefined;
40
+ manifest?: undefined;
41
+ state?: undefined;
42
+ manifestHash?: undefined;
43
+ receipt?: undefined;
44
+ verdict?: undefined;
45
+ checkpointId?: undefined;
46
+ approvalId?: undefined;
47
+ paths?: undefined;
48
+ auditRecorded?: undefined;
49
+ message?: undefined;
50
+ } | {
51
+ ok: boolean;
52
+ subcommand: string;
53
+ repoRoot: string;
54
+ checkpoints: import("../core/index.js").RecoveryCheckpointSummary[];
55
+ backend?: undefined;
56
+ availableBackends?: undefined;
57
+ backendCounts?: undefined;
58
+ fileCheckpoint?: undefined;
59
+ enabled?: undefined;
60
+ checkpointCount?: undefined;
61
+ storageBytes?: undefined;
62
+ recoverableCount?: undefined;
63
+ needsManualRepairCount?: undefined;
64
+ verifiedReceiptCount?: undefined;
65
+ states?: undefined;
66
+ advisories?: undefined;
67
+ limits?: undefined;
68
+ manifest?: undefined;
69
+ state?: undefined;
70
+ manifestHash?: undefined;
71
+ receipt?: undefined;
72
+ verdict?: undefined;
73
+ checkpointId?: undefined;
74
+ approvalId?: undefined;
75
+ paths?: undefined;
76
+ auditRecorded?: undefined;
77
+ message?: undefined;
78
+ } | {
79
+ ok: boolean;
80
+ subcommand: string;
81
+ manifest: import("../core/index.js").RecoveryCheckpointManifest;
82
+ state: import("../core/recovery/types.js").RecoveryCheckpointStateV1;
83
+ manifestHash: string;
84
+ receipt: import("../core/index.js").RecoveryReceiptV1 | undefined;
85
+ repoRoot?: undefined;
86
+ backend?: undefined;
87
+ availableBackends?: undefined;
88
+ backendCounts?: undefined;
89
+ fileCheckpoint?: undefined;
90
+ enabled?: undefined;
91
+ checkpointCount?: undefined;
92
+ storageBytes?: undefined;
93
+ recoverableCount?: undefined;
94
+ needsManualRepairCount?: undefined;
95
+ verifiedReceiptCount?: undefined;
96
+ states?: undefined;
97
+ advisories?: undefined;
98
+ limits?: undefined;
99
+ checkpoints?: undefined;
100
+ verdict?: undefined;
101
+ checkpointId?: undefined;
102
+ approvalId?: undefined;
103
+ paths?: undefined;
104
+ auditRecorded?: undefined;
105
+ message?: undefined;
106
+ } | {
107
+ ok: boolean;
108
+ subcommand: string;
109
+ verdict: string;
110
+ checkpointId: string;
111
+ approvalId: string;
112
+ manifestHash: string;
113
+ paths: string[];
114
+ auditRecorded: boolean;
115
+ message: string;
116
+ repoRoot?: undefined;
117
+ backend?: undefined;
118
+ availableBackends?: undefined;
119
+ backendCounts?: undefined;
120
+ fileCheckpoint?: undefined;
121
+ enabled?: undefined;
122
+ checkpointCount?: undefined;
123
+ storageBytes?: undefined;
124
+ recoverableCount?: undefined;
125
+ needsManualRepairCount?: undefined;
126
+ verifiedReceiptCount?: undefined;
127
+ states?: undefined;
128
+ advisories?: undefined;
129
+ limits?: undefined;
130
+ checkpoints?: undefined;
131
+ manifest?: undefined;
132
+ state?: undefined;
133
+ receipt?: undefined;
134
+ } | {
135
+ auditRecorded: boolean;
136
+ message: string;
137
+ manifestHash: string;
138
+ changeCount: number;
139
+ ok: boolean;
140
+ subcommand: string;
141
+ verdict: string;
142
+ checkpointId: string;
143
+ repoRoot?: undefined;
144
+ backend?: undefined;
145
+ availableBackends?: undefined;
146
+ backendCounts?: undefined;
147
+ fileCheckpoint?: undefined;
148
+ enabled?: undefined;
149
+ checkpointCount?: undefined;
150
+ storageBytes?: undefined;
151
+ recoverableCount?: undefined;
152
+ needsManualRepairCount?: undefined;
153
+ verifiedReceiptCount?: undefined;
154
+ states?: undefined;
155
+ advisories?: undefined;
156
+ limits?: undefined;
157
+ checkpoints?: undefined;
158
+ manifest?: undefined;
159
+ state?: undefined;
160
+ receipt?: undefined;
161
+ approvalId?: undefined;
162
+ paths?: undefined;
163
+ }>;
164
+ export declare function formatRecoveryCheckpointResult(result: Record<string, unknown>): string;
@@ -0,0 +1,347 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { ensureBelayStateDir, loadConfigFile } from '../config-io.js';
4
+ import { compactApprovals, createApprovalRecordWithEnvelope } from '../core/approval.js';
5
+ import { createGateApprovalStore } from '../core/approval-service.js';
6
+ import { issueApprovalToken } from '../core/approval-token.js';
7
+ import { appendCliAuditEvent } from '../core/audit-io.js';
8
+ import { mutateApprovalStateWithRetry } from '../core/capability/approval-state-mutation.js';
9
+ import { boundarySessionStatus } from '../core/capability/boundary-session.js';
10
+ import { configuredControlPlaneDir, DEFAULT_RECOVERY_CHECKPOINT } from '../core/config.js';
11
+ import { notifyDeny } from '../core/notify.js';
12
+ import { canonicalPath } from '../core/path-utils.js';
13
+ import { listRecoveryCheckpoints, RECOVERY_CHECKPOINT_CORRUPT, RECOVERY_RESTORE_CONFLICT, RECOVERY_RESTORE_REASON, recoveryCheckpointStorageBytes, recoveryRestoreBinding, restoreRecoveryCheckpoint, showRecoveryCheckpoint, } from '../core/recovery/checkpoint.js';
14
+ import { formatRecoveryStateDiagnostic, recoveryNotificationConfigured, recoveryNotificationSetupWarning, summarizeRecoveryCheckpointDiagnostics, } from '../core/recovery/operator-guidance.js';
15
+ import { probeFileCloneStrategy } from '../core/transactional/file-clone.js';
16
+ async function appendRecoveryAudit(repoRoot, config, event) {
17
+ try {
18
+ await appendCliAuditEvent(repoRoot, config, event);
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ async function consumeRecoveryApproval(params) {
26
+ const store = createGateApprovalStore(params.repoRoot, params.config);
27
+ return mutateApprovalStateWithRetry({
28
+ load: store.loadApproved,
29
+ write: store.writeApproved,
30
+ mutate: (state) => {
31
+ const compacted = compactApprovals(state);
32
+ const index = compacted.approvals.findIndex((entry) => entry.kind === 'tool' &&
33
+ entry.reason === RECOVERY_RESTORE_REASON &&
34
+ entry.fingerprint === params.fingerprint &&
35
+ entry.repoRoot === params.repoRoot);
36
+ if (index === -1)
37
+ return null;
38
+ const [approval] = compacted.approvals.splice(index, 1);
39
+ return approval ? { state: compacted, result: approval } : null;
40
+ },
41
+ });
42
+ }
43
+ async function clearRecoveryPending(params) {
44
+ const store = createGateApprovalStore(params.repoRoot, params.config);
45
+ await mutateApprovalStateWithRetry({
46
+ load: store.loadPending,
47
+ write: store.writePending,
48
+ mutate: (state) => {
49
+ const compacted = compactApprovals(state);
50
+ const remaining = compacted.approvals.filter((entry) => !(entry.kind === 'tool' &&
51
+ entry.reason === RECOVERY_RESTORE_REASON &&
52
+ entry.fingerprint === params.fingerprint &&
53
+ entry.repoRoot === params.repoRoot));
54
+ if (remaining.length === compacted.approvals.length)
55
+ return null;
56
+ compacted.approvals = remaining;
57
+ return { state: compacted, result: undefined };
58
+ },
59
+ });
60
+ }
61
+ async function ensureRecoveryPending(params) {
62
+ const store = createGateApprovalStore(params.repoRoot, params.config);
63
+ const request = createApprovalRecordWithEnvelope({
64
+ kind: 'tool',
65
+ fingerprint: params.fingerprint,
66
+ repoRoot: params.repoRoot,
67
+ reason: RECOVERY_RESTORE_REASON,
68
+ summary: `Restore recovery checkpoint ${params.checkpointId}`,
69
+ approvalTtlMinutes: params.config.approvalTtlMinutes,
70
+ approvalId: `belay_${randomUUID().replaceAll('-', '').slice(0, 12)}`,
71
+ approvalInput: {
72
+ input: 'recovery.restore',
73
+ inputKind: 'tool',
74
+ cwd: params.repoRoot,
75
+ toolName: 'recovery.restore',
76
+ payload: {
77
+ checkpointId: params.checkpointId,
78
+ manifestHash: params.manifestHash,
79
+ postStateHash: params.postStateHash,
80
+ paths: params.paths,
81
+ },
82
+ },
83
+ });
84
+ const result = await mutateApprovalStateWithRetry({
85
+ load: store.loadPending,
86
+ write: store.writePending,
87
+ mutate: (state) => {
88
+ const compacted = compactApprovals(state);
89
+ const existing = compacted.approvals.find((entry) => entry.kind === 'tool' &&
90
+ entry.reason === RECOVERY_RESTORE_REASON &&
91
+ entry.fingerprint === params.fingerprint &&
92
+ entry.repoRoot === params.repoRoot);
93
+ if (existing)
94
+ return { state: compacted, result: existing };
95
+ compacted.approvals.push(request);
96
+ return { state: compacted, result: request };
97
+ },
98
+ });
99
+ if (!result)
100
+ throw new Error('recovery_approval_state_busy');
101
+ return result;
102
+ }
103
+ export async function recoveryCheckpointCommand(options) {
104
+ const repoRoot = canonicalPath(path.resolve(options.targetDir ?? process.cwd()));
105
+ const config = await loadConfigFile(repoRoot);
106
+ const stateDir = await ensureBelayStateDir(config, repoRoot);
107
+ const checkpointConfig = config.policy.transactional.checkpoint ?? DEFAULT_RECOVERY_CHECKPOINT;
108
+ if (options.subcommand === 'status') {
109
+ const checkpoints = await listRecoveryCheckpoints(stateDir, repoRoot);
110
+ const states = Object.fromEntries([...new Set(checkpoints.map((checkpoint) => checkpoint.state))].map((state) => [
111
+ state,
112
+ checkpoints.filter((checkpoint) => checkpoint.state === state).length,
113
+ ]));
114
+ const advisories = summarizeRecoveryCheckpointDiagnostics(checkpoints);
115
+ const backendCounts = Object.fromEntries([...new Set(checkpoints.map((checkpoint) => checkpoint.backend))].map((backend) => [
116
+ backend,
117
+ checkpoints.filter((checkpoint) => checkpoint.backend === backend).length,
118
+ ]));
119
+ const checkpointBackends = Object.keys(backendCounts);
120
+ const fileCheckpointConfig = config.policy.transactional.fileCheckpoint;
121
+ const configuredBoundary = config.capability?.boundaryDriver ??
122
+ (config.sandbox.runtime === 'container' ? 'container' : null);
123
+ let isolation = configuredBoundary;
124
+ let isolationAvailable = false;
125
+ if (configuredBoundary) {
126
+ try {
127
+ const session = await boundarySessionStatus({ repoRoot, config });
128
+ isolationAvailable =
129
+ session.fresh === true && session.attestation?.isolatesWorkspaceMounts === true;
130
+ isolation = isolationAvailable ? (session.attestation?.driver ?? configuredBoundary) : null;
131
+ }
132
+ catch {
133
+ isolation = null;
134
+ }
135
+ }
136
+ const copyStrategy = fileCheckpointConfig.enabled ? await probeFileCloneStrategy() : null;
137
+ const fileCheckpointAvailable = fileCheckpointConfig.enabled &&
138
+ config.policy.transactional.enabled &&
139
+ checkpointConfig.enabled &&
140
+ isolationAvailable;
141
+ return {
142
+ ok: true,
143
+ subcommand: 'status',
144
+ repoRoot,
145
+ backend: checkpointBackends.length === 1
146
+ ? checkpointBackends[0]
147
+ : checkpointBackends.length > 1
148
+ ? 'mixed'
149
+ : 'git_worktree',
150
+ availableBackends: ['git_worktree', 'file_checkpoint'],
151
+ backendCounts,
152
+ fileCheckpoint: {
153
+ enabled: fileCheckpointConfig.enabled,
154
+ allowNonGit: fileCheckpointConfig.allowNonGit,
155
+ isolation,
156
+ copyStrategy: copyStrategy ?? undefined,
157
+ probe: fileCheckpointAvailable ? 'available' : 'unavailable',
158
+ },
159
+ enabled: checkpointConfig.enabled,
160
+ checkpointCount: checkpoints.length,
161
+ storageBytes: await recoveryCheckpointStorageBytes(stateDir, repoRoot),
162
+ recoverableCount: checkpoints.filter((checkpoint) => checkpoint.state === 'applied').length,
163
+ needsManualRepairCount: checkpoints.filter((checkpoint) => checkpoint.state === 'needs_manual_repair').length,
164
+ verifiedReceiptCount: checkpoints.filter((checkpoint) => checkpoint.receiptHash).length,
165
+ states,
166
+ advisories,
167
+ limits: checkpointConfig,
168
+ };
169
+ }
170
+ if (options.subcommand === 'list') {
171
+ return {
172
+ ok: true,
173
+ subcommand: 'list',
174
+ repoRoot,
175
+ checkpoints: await listRecoveryCheckpoints(stateDir, repoRoot),
176
+ };
177
+ }
178
+ if (!options.checkpointId) {
179
+ throw new Error(`recover ${options.subcommand} requires a checkpoint id`);
180
+ }
181
+ if (options.subcommand === 'show') {
182
+ const loaded = await showRecoveryCheckpoint(stateDir, options.checkpointId, repoRoot);
183
+ return {
184
+ ok: true,
185
+ subcommand: 'show',
186
+ manifest: loaded.manifest,
187
+ state: loaded.state,
188
+ manifestHash: loaded.manifestHash,
189
+ receipt: loaded.receipt,
190
+ };
191
+ }
192
+ let binding;
193
+ try {
194
+ binding = await recoveryRestoreBinding(stateDir, options.checkpointId, repoRoot);
195
+ }
196
+ catch (error) {
197
+ const reason = error instanceof Error ? error.message : RECOVERY_CHECKPOINT_CORRUPT;
198
+ await appendRecoveryAudit(repoRoot, config, {
199
+ event: reason === RECOVERY_RESTORE_CONFLICT
200
+ ? 'recoveryConflict'
201
+ : reason === RECOVERY_CHECKPOINT_CORRUPT
202
+ ? 'recoveryCorrupt'
203
+ : 'recoveryRejected',
204
+ recoveryCheckpointId: options.checkpointId,
205
+ reason,
206
+ });
207
+ throw error;
208
+ }
209
+ const approval = await consumeRecoveryApproval({
210
+ repoRoot,
211
+ config,
212
+ fingerprint: binding.fingerprint,
213
+ });
214
+ if (!approval) {
215
+ const request = await ensureRecoveryPending({
216
+ repoRoot,
217
+ config,
218
+ fingerprint: binding.fingerprint,
219
+ checkpointId: options.checkpointId,
220
+ manifestHash: binding.manifestHash,
221
+ postStateHash: binding.postStateHash,
222
+ paths: binding.paths,
223
+ });
224
+ try {
225
+ const current = await recoveryRestoreBinding(stateDir, options.checkpointId, repoRoot);
226
+ if (current.fingerprint !== binding.fingerprint) {
227
+ throw new Error(RECOVERY_RESTORE_CONFLICT);
228
+ }
229
+ }
230
+ catch (error) {
231
+ await clearRecoveryPending({ repoRoot, config, fingerprint: binding.fingerprint }).catch(() => { });
232
+ throw error;
233
+ }
234
+ const notificationConfigured = recoveryNotificationConfigured(config);
235
+ if (notificationConfigured) {
236
+ const approvalToken = await issueApprovalToken({
237
+ approvalId: request.approvalId,
238
+ fingerprint: request.fingerprint,
239
+ repoRoot: request.repoRoot,
240
+ issuedAt: request.createdAt,
241
+ expiresAt: request.expiresAt,
242
+ }, configuredControlPlaneDir(config));
243
+ await notifyDeny(config.notifications, {
244
+ approvalId: request.approvalId,
245
+ reason: request.reason,
246
+ summary: request.summary,
247
+ repoRoot: request.repoRoot,
248
+ fingerprint: request.fingerprint,
249
+ approvalToken,
250
+ });
251
+ }
252
+ const auditRecorded = await appendRecoveryAudit(repoRoot, config, {
253
+ event: 'recoveryApprovalRequested',
254
+ recoveryCheckpointId: options.checkpointId,
255
+ approvalId: request.approvalId,
256
+ manifestHash: binding.manifestHash,
257
+ postStateHash: binding.postStateHash,
258
+ pathCount: binding.paths.length,
259
+ });
260
+ return {
261
+ ok: false,
262
+ subcommand: 'apply',
263
+ verdict: 'deny_pending_approval',
264
+ checkpointId: options.checkpointId,
265
+ approvalId: request.approvalId,
266
+ manifestHash: binding.manifestHash,
267
+ paths: binding.paths,
268
+ auditRecorded,
269
+ message: notificationConfigured
270
+ ? `Signed out-of-band approval required for ${request.approvalId}. Use the token delivered through the configured notification channel, then run \`belay approve ${request.approvalId} --token <signed-token>\` and repeat this command.`
271
+ : `${recoveryNotificationSetupWarning()} Pending approval id: ${request.approvalId}.`,
272
+ };
273
+ }
274
+ try {
275
+ const result = await restoreRecoveryCheckpoint(stateDir, options.checkpointId, repoRoot);
276
+ await clearRecoveryPending({ repoRoot, config, fingerprint: binding.fingerprint }).catch(() => { });
277
+ const auditRecorded = await appendRecoveryAudit(repoRoot, config, {
278
+ event: 'recoveryApplied',
279
+ recoveryCheckpointId: options.checkpointId,
280
+ approvalId: approval.approvalId,
281
+ manifestHash: result.manifestHash,
282
+ changeCount: result.changeCount,
283
+ });
284
+ return {
285
+ ok: true,
286
+ subcommand: 'apply',
287
+ verdict: 'restored',
288
+ checkpointId: options.checkpointId,
289
+ ...result,
290
+ auditRecorded,
291
+ message: `Recovery checkpoint ${options.checkpointId} restored.`,
292
+ };
293
+ }
294
+ catch (error) {
295
+ const reason = error instanceof Error ? error.message : 'recovery_restore_failed';
296
+ await appendRecoveryAudit(repoRoot, config, {
297
+ event: reason === RECOVERY_RESTORE_CONFLICT ? 'recoveryConflict' : 'recoveryRejected',
298
+ recoveryCheckpointId: options.checkpointId,
299
+ approvalId: approval.approvalId,
300
+ reason,
301
+ });
302
+ throw error;
303
+ }
304
+ }
305
+ export function formatRecoveryCheckpointResult(result) {
306
+ if (result.subcommand === 'status') {
307
+ const lines = [
308
+ `belay recover status for ${result.repoRoot}`,
309
+ `Backend: ${result.backend}`,
310
+ `Checkpointing: ${result.enabled ? 'enabled' : 'disabled'}`,
311
+ `Checkpoints: ${result.checkpointCount} (${result.recoverableCount} recoverable)`,
312
+ `Needs manual repair: ${result.needsManualRepairCount ?? 0}`,
313
+ `Verified receipts: ${result.verifiedReceiptCount}`,
314
+ `Storage: ${result.storageBytes} bytes`,
315
+ `States: ${JSON.stringify(result.states)}`,
316
+ ];
317
+ const advisories = result.advisories;
318
+ if (advisories && advisories.length > 0) {
319
+ lines.push('', 'Advisories:');
320
+ for (const advisory of advisories) {
321
+ lines.push(`- ${advisory}`);
322
+ }
323
+ }
324
+ lines.push('');
325
+ return `${lines.join('\n')}\n`;
326
+ }
327
+ if (result.subcommand === 'list') {
328
+ const checkpoints = result.checkpoints;
329
+ if (checkpoints.length === 0)
330
+ return 'No recovery checkpoints found.\n';
331
+ const lines = checkpoints.map((item) => {
332
+ const diagnostic = formatRecoveryStateDiagnostic(item.state, item.stateDetail);
333
+ const base = `${item.checkpointId}\t${item.state}\t${item.changeCount} changes\treceipt:${item.receiptHash?.slice(0, 12) ?? '-'}\t${item.createdAt}`;
334
+ return diagnostic ? `${base}\n ${diagnostic}` : base;
335
+ });
336
+ return `${lines.join('\n')}\n`;
337
+ }
338
+ if (result.subcommand === 'show') {
339
+ const state = result.state;
340
+ const diagnostic = state?.state !== undefined
341
+ ? formatRecoveryStateDiagnostic(state.state, state.detail)
342
+ : null;
343
+ const payload = diagnostic ? { ...result, diagnostic } : result;
344
+ return `${JSON.stringify(payload, null, 2)}\n`;
345
+ }
346
+ return `${String(result.message ?? 'Recovery command completed.')}\n`;
347
+ }
@@ -1,20 +1,32 @@
1
1
  import path from 'node:path';
2
- import { loadApprovalState, loadConfigFile, saveApprovalState } from '../config-io.js';
2
+ import { loadApprovalState, loadConfigFile, pendingApprovalsPath, saveApprovalState, } from '../config-io.js';
3
3
  import { compactApprovals } from '../core/approval.js';
4
+ import { mutateApprovalStateWithRetry } from '../core/capability/approval-state-mutation.js';
4
5
  export async function revokeApproval(options) {
5
6
  const repoRoot = path.resolve(options.targetDir ?? process.cwd());
6
7
  const config = await loadConfigFile(repoRoot);
7
- const pending = await loadApprovalState(repoRoot, 'pending-approvals.json', config);
8
- const compacted = compactApprovals(pending);
9
- const index = compacted.approvals.findIndex((approval) => approval.approvalId === options.approvalId);
10
- if (index === -1) {
8
+ const revoked = await mutateApprovalStateWithRetry({
9
+ load: async () => ({
10
+ filePath: pendingApprovalsPath(repoRoot, config),
11
+ state: await loadApprovalState(repoRoot, 'pending-approvals.json', config),
12
+ }),
13
+ write: async (_filePath, state) => saveApprovalState(repoRoot, 'pending-approvals.json', state, config),
14
+ mutate: (state) => {
15
+ const compacted = compactApprovals(state);
16
+ const index = compacted.approvals.findIndex((approval) => approval.approvalId === options.approvalId);
17
+ if (index === -1) {
18
+ return null;
19
+ }
20
+ compacted.approvals.splice(index, 1);
21
+ return { state: compacted, result: true };
22
+ },
23
+ });
24
+ if (revoked !== true) {
11
25
  return {
12
26
  ok: false,
13
27
  message: `Pending approval ${options.approvalId} not found or already expired.`,
14
28
  };
15
29
  }
16
- compacted.approvals.splice(index, 1);
17
- await saveApprovalState(repoRoot, 'pending-approvals.json', compacted, config);
18
30
  return {
19
31
  ok: true,
20
32
  message: `Revoked pending approval ${options.approvalId}.`,
@@ -0,0 +1,20 @@
1
+ import { boundarySessionStatus } from '../core/capability/boundary-session.js';
2
+ export declare function sessionStartProject(params: {
3
+ targetDir?: string;
4
+ agentCommand?: string;
5
+ }): Promise<{
6
+ ok: boolean;
7
+ message: string;
8
+ attestationPath?: string;
9
+ exitCode?: number | null;
10
+ }>;
11
+ export declare function sessionStatusProject(params: {
12
+ targetDir?: string;
13
+ json?: boolean;
14
+ }): Promise<{
15
+ ok: boolean;
16
+ attestationPath: string;
17
+ fresh: boolean;
18
+ attestation: Awaited<ReturnType<typeof boundarySessionStatus>>['attestation'];
19
+ }>;
20
+ export declare function formatSessionStatusReport(report: Awaited<ReturnType<typeof sessionStatusProject>>): string;
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import process from 'node:process';
3
+ import { loadConfigFile } from '../config-io.js';
4
+ import { boundarySessionStatus, runBoundaryAgentCommand, startBoundarySession, } from '../core/capability/boundary-session.js';
5
+ export async function sessionStartProject(params) {
6
+ const repoRoot = path.resolve(params.targetDir ?? process.cwd());
7
+ const config = await loadConfigFile(repoRoot);
8
+ const started = await startBoundarySession({ repoRoot, config });
9
+ if (!params.agentCommand) {
10
+ return {
11
+ ok: true,
12
+ message: `Boundary session started (${started.attestation.driver}); attestation written to ${path.relative(repoRoot, started.attestationPath)}.`,
13
+ attestationPath: started.attestationPath,
14
+ };
15
+ }
16
+ const run = await runBoundaryAgentCommand({
17
+ repoRoot,
18
+ config,
19
+ command: params.agentCommand,
20
+ });
21
+ const exitCode = run.exitCode ?? 1;
22
+ return {
23
+ ok: exitCode === 0,
24
+ message: `Boundary session started (${started.attestation.driver}); agent exited with code ${exitCode}.`,
25
+ attestationPath: started.attestationPath,
26
+ exitCode,
27
+ };
28
+ }
29
+ export async function sessionStatusProject(params) {
30
+ const repoRoot = path.resolve(params.targetDir ?? process.cwd());
31
+ const config = await loadConfigFile(repoRoot);
32
+ const status = await boundarySessionStatus({ repoRoot, config });
33
+ return {
34
+ ok: status.fresh,
35
+ attestationPath: status.attestationPath,
36
+ fresh: status.fresh,
37
+ attestation: status.attestation,
38
+ };
39
+ }
40
+ export function formatSessionStatusReport(report) {
41
+ if (!report.attestation) {
42
+ return `No boundary attestation at ${report.attestationPath}. Run belay session start.`;
43
+ }
44
+ return [
45
+ `Attestation: ${report.attestationPath}`,
46
+ `Driver: ${report.attestation.driver}`,
47
+ `Fresh: ${report.fresh ? 'yes' : 'no'}`,
48
+ `Expires: ${report.attestation.expiresAt}`,
49
+ `Materializes grants: ${report.attestation.materializesGrants ? 'yes' : 'no'}`,
50
+ ].join('\n');
51
+ }
@@ -23,6 +23,11 @@ export async function statusProject(options = {}) {
23
23
  dogfood: operational.dogfood,
24
24
  health,
25
25
  visibility,
26
+ fileCheckpoint: {
27
+ ...config.policy.transactional.fileCheckpoint,
28
+ transactionalEnabled: config.policy.transactional.enabled,
29
+ durableCheckpointEnabled: config.policy.transactional.checkpoint?.enabled === true,
30
+ },
26
31
  };
27
32
  }
28
33
  export function formatStatusReport(report) {
@@ -51,6 +56,8 @@ export function formatStatusReport(report) {
51
56
  `Dogfood: ${report.dogfood.active ? 'active' : 'inactive'} (mode=${report.dogfood.mode}, unknownLocalEffect=${report.dogfood.unknownLocalEffect})`,
52
57
  `Metrics: ${report.dogfood.gateEvents} gate events, ${report.dogfood.wouldBlockCount} would-block (${(report.dogfood.wouldBlockRate * 100).toFixed(1)}%)`,
53
58
  `Ready for enforce: ${report.dogfood.readyForEnforce ? 'yes' : 'not yet'}`,
59
+ `File checkpoint: ${report.fileCheckpoint.enabled ? 'enabled' : 'disabled'} (transactional=${report.fileCheckpoint.transactionalEnabled}, durable=${report.fileCheckpoint.durableCheckpointEnabled}, nonGit=${report.fileCheckpoint.allowNonGit})`,
60
+ `File checkpoint limits: files=${report.fileCheckpoint.maxFiles}, sourceBytes=${report.fileCheckpoint.maxSourceBytes}, workspaceBytes=${report.fileCheckpoint.maxWorkspaceBytes}, prepareTimeoutMs=${report.fileCheckpoint.prepareTimeoutMs}, copyConcurrency=${report.fileCheckpoint.copyConcurrency}`,
54
61
  '',
55
62
  'Audit visibility:',
56
63
  ` Gate events: ${report.visibility.gateEvents}`,