@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,104 @@
1
+ import type { CapabilityRequestV1 } from '../capability/request.js';
2
+ /** Backends that can mint a verifiable RecoveryProof (ADR: proof-only execution). */
3
+ export type RecoveryBackend = 'git_worktree' | 'file_checkpoint';
4
+ export interface RecoveryProofV1 {
5
+ version: 1;
6
+ backend: RecoveryBackend;
7
+ inputFingerprint: string;
8
+ resourceScope: string;
9
+ baseStateHash: string;
10
+ /** The proof covers only filesystem effects observed inside the repository. */
11
+ effectClosure: 'repo_local_fs_observed';
12
+ issuedAt: string;
13
+ expiresAt: string;
14
+ probeSignals: string[];
15
+ }
16
+ export type RecoveryCheckpointState = 'prepared' | 'applying' | 'applied' | 'restoring' | 'restored' | 'conflict' | 'corrupt' | 'needs_manual_repair';
17
+ export type RecoveryFileKind = 'absent' | 'file' | 'symlink';
18
+ export interface RecoveryFileSnapshotV1 {
19
+ kind: RecoveryFileKind;
20
+ mode?: number;
21
+ hash?: string;
22
+ blob?: string;
23
+ symlinkTarget?: string;
24
+ }
25
+ export interface RecoveryCheckpointEntryV1 {
26
+ path: string;
27
+ before: RecoveryFileSnapshotV1;
28
+ after: RecoveryFileSnapshotV1;
29
+ }
30
+ export interface RecoveryCheckpointManifestV1 {
31
+ version: 1;
32
+ checkpointId: string;
33
+ backend: RecoveryBackend;
34
+ repoRoot: string;
35
+ repoIdentity: string;
36
+ commandFingerprint: string;
37
+ createdAt: string;
38
+ expiresAt: string;
39
+ proof: RecoveryProofV1;
40
+ entries: RecoveryCheckpointEntryV1[];
41
+ }
42
+ export type RecoveryFileSnapshotV2 = RecoveryFileSnapshotV1 | {
43
+ kind: 'directory';
44
+ mode: number;
45
+ hash: string;
46
+ };
47
+ export interface RecoveryCheckpointEntryV2 {
48
+ path: string;
49
+ before: RecoveryFileSnapshotV2;
50
+ after: RecoveryFileSnapshotV2;
51
+ }
52
+ export type RecoveryCheckpointEntry = RecoveryCheckpointEntryV1 | RecoveryCheckpointEntryV2;
53
+ export interface RecoveryCheckpointManifestV2 {
54
+ version: 2;
55
+ checkpointId: string;
56
+ backend: RecoveryBackend;
57
+ repoRoot: string;
58
+ resourceKind: 'git_repository' | 'directory';
59
+ repoIdentity: string;
60
+ commandFingerprint: string;
61
+ createdAt: string;
62
+ expiresAt: string;
63
+ proof: RecoveryProofV1;
64
+ entries: RecoveryCheckpointEntryV2[];
65
+ }
66
+ export type RecoveryCheckpointManifest = RecoveryCheckpointManifestV1 | RecoveryCheckpointManifestV2;
67
+ export interface RecoveryCheckpointStateV1 {
68
+ version: 1;
69
+ state: RecoveryCheckpointState;
70
+ updatedAt: string;
71
+ manifestHash: string;
72
+ detail?: string;
73
+ }
74
+ export interface RecoveryReceiptV1 {
75
+ version: 1;
76
+ checkpointId: string;
77
+ manifestHash: string;
78
+ proofHash: string;
79
+ postStateHash: string;
80
+ appliedAt: string;
81
+ changeCount: number;
82
+ }
83
+ export interface RecoveryCheckpointSummary {
84
+ checkpointId: string;
85
+ state: RecoveryCheckpointState;
86
+ /** Operator-facing detail from durable checkpoint state (e.g. mixed-state reason). */
87
+ stateDetail?: string;
88
+ backend: RecoveryBackend;
89
+ repoRoot: string;
90
+ commandFingerprint: string;
91
+ createdAt: string;
92
+ expiresAt: string;
93
+ changeCount: number;
94
+ manifestHash: string;
95
+ receiptHash?: string;
96
+ proofHash?: string;
97
+ postStateHash?: string;
98
+ }
99
+ export interface RecoveryProofContext {
100
+ repoRoot: string;
101
+ cwd: string;
102
+ command: string;
103
+ requests: CapabilityRequestV1[];
104
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -18,6 +18,9 @@ const STANDING_ALLOW_BLOCKED_REASONS = new Set([
18
18
  'protected_artifact',
19
19
  'pipe_to_shell',
20
20
  'command_substitution',
21
+ 'outside_repo_mutation',
22
+ 'outside_repo_redirect',
23
+ 'control_plane_mutation',
21
24
  ]);
22
25
  export function standingAllowFile(config, repoLocalStateDir) {
23
26
  return `${belayStateDir(config, repoLocalStateDir)}/standing-allow.json`;
@@ -0,0 +1,13 @@
1
+ import type { ObservedFileChange } from './file-tree.js';
2
+ import type { TransactionalFileChange } from './types.js';
3
+ export declare const TRANSACTIONAL_APPLY_TOCTOU = "transactional_apply_toctou";
4
+ export declare const TRANSACTIONAL_APPLY_CONFLICT = "transactional_apply_conflict";
5
+ export declare const TRANSACTIONAL_APPLY_ROLLBACK_FAILED = "transactional_apply_rollback_failed";
6
+ export interface ApplyObservedChangesParams {
7
+ sourceRoot: string;
8
+ targetRoot: string;
9
+ changes: ObservedFileChange[];
10
+ afterApply?: () => Promise<void>;
11
+ }
12
+ export declare function applyObservedChanges(params: ApplyObservedChangesParams): Promise<void>;
13
+ export declare function buildObservedChangesFromTransactional(baselineRoot: string, executionRoot: string, changes: TransactionalFileChange[]): Promise<ObservedFileChange[]>;
@@ -0,0 +1,275 @@
1
+ import { copyFile, lstat, mkdir, mkdtemp, readlink, rm, rmdir, symlink } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { compareRelativePathsBytewise, joinRelativePath, validateRelativePath, } from './file-tree-path.js';
5
+ import { readSnapshotNode, snapshotNodesEqual } from './snapshot-node.js';
6
+ export const TRANSACTIONAL_APPLY_TOCTOU = 'transactional_apply_toctou';
7
+ export const TRANSACTIONAL_APPLY_CONFLICT = 'transactional_apply_conflict';
8
+ export const TRANSACTIONAL_APPLY_ROLLBACK_FAILED = 'transactional_apply_rollback_failed';
9
+ async function chmodSafe(target, mode) {
10
+ try {
11
+ const { chmod } = await import('node:fs/promises');
12
+ await chmod(target, mode & 0o777);
13
+ }
14
+ catch {
15
+ // best effort
16
+ }
17
+ }
18
+ async function removePathIfExists(target) {
19
+ let info;
20
+ try {
21
+ info = await lstat(target);
22
+ }
23
+ catch (error) {
24
+ if (error.code === 'ENOENT') {
25
+ return;
26
+ }
27
+ throw error;
28
+ }
29
+ if (info.isDirectory() && !info.isSymbolicLink()) {
30
+ await rmdir(target);
31
+ return;
32
+ }
33
+ await rm(target, { force: true });
34
+ }
35
+ async function copyPathPreservingType(source, target) {
36
+ const info = await lstat(source);
37
+ if (info.isDirectory() && !info.isSymbolicLink()) {
38
+ try {
39
+ const targetInfo = await lstat(target);
40
+ if (targetInfo.isDirectory() && !targetInfo.isSymbolicLink()) {
41
+ await chmodSafe(target, info.mode);
42
+ return;
43
+ }
44
+ }
45
+ catch (error) {
46
+ if (error.code !== 'ENOENT')
47
+ throw error;
48
+ }
49
+ }
50
+ await removePathIfExists(target);
51
+ await mkdir(path.dirname(target), { recursive: true });
52
+ if (info.isSymbolicLink()) {
53
+ await symlink(await readlink(source), target);
54
+ return;
55
+ }
56
+ if (info.isDirectory() && !info.isSymbolicLink()) {
57
+ await mkdir(target, { recursive: true, mode: info.mode & 0o777 });
58
+ return;
59
+ }
60
+ if (!info.isFile()) {
61
+ throw new Error(TRANSACTIONAL_APPLY_CONFLICT);
62
+ }
63
+ await copyFile(source, target);
64
+ await chmodSafe(target, info.mode);
65
+ }
66
+ async function assertParentChainSafe(targetRoot, relativePath, plannedDirectories = new Set()) {
67
+ const segments = relativePath.split(path.sep).filter(Boolean);
68
+ if (segments.length <= 1) {
69
+ return;
70
+ }
71
+ for (let index = 1; index < segments.length; index++) {
72
+ const prefix = segments.slice(0, index).join(path.sep);
73
+ const absolute = joinRelativePath(targetRoot, prefix);
74
+ let info;
75
+ try {
76
+ info = await lstat(absolute);
77
+ }
78
+ catch (error) {
79
+ if (error.code === 'ENOENT') {
80
+ continue;
81
+ }
82
+ throw error;
83
+ }
84
+ if (info.isSymbolicLink()) {
85
+ throw new Error(TRANSACTIONAL_APPLY_CONFLICT);
86
+ }
87
+ if (!info.isDirectory() && !plannedDirectories.has(prefix)) {
88
+ throw new Error(TRANSACTIONAL_APPLY_CONFLICT);
89
+ }
90
+ }
91
+ }
92
+ async function restorePathFromBackup(backupPath, target, rollbackRoot) {
93
+ const stagingRoot = await mkdtemp(path.join(rollbackRoot, 'restore-'));
94
+ const staged = path.join(stagingRoot, 'node');
95
+ try {
96
+ await copyPathPreservingType(backupPath, staged);
97
+ await copyPathPreservingType(staged, target);
98
+ }
99
+ finally {
100
+ await rm(stagingRoot, { recursive: true, force: true });
101
+ }
102
+ }
103
+ async function rollbackAppliedChanges(actions, changes, targetRoot, rollbackRoot) {
104
+ let hadFailure = false;
105
+ for (const action of [...actions].reverse()) {
106
+ try {
107
+ await assertParentChainSafe(targetRoot, action.relativePath);
108
+ if (action.type === 'restore') {
109
+ await restorePathFromBackup(action.backupPath, action.target, rollbackRoot);
110
+ }
111
+ else {
112
+ await removePathIfExists(action.target);
113
+ }
114
+ }
115
+ catch {
116
+ hadFailure = true;
117
+ }
118
+ }
119
+ for (const change of changes) {
120
+ try {
121
+ await assertTargetMatches(targetRoot, change);
122
+ }
123
+ catch {
124
+ hadFailure = true;
125
+ }
126
+ }
127
+ return !hadFailure;
128
+ }
129
+ async function assertTargetMatches(targetRoot, change) {
130
+ const target = joinRelativePath(targetRoot, change.relativePath);
131
+ const current = await readSnapshotNode(target);
132
+ if (!snapshotNodesEqual(current, change.before)) {
133
+ throw new Error(TRANSACTIONAL_APPLY_TOCTOU);
134
+ }
135
+ }
136
+ async function assertTargetMatchesAfter(targetRoot, change) {
137
+ const target = joinRelativePath(targetRoot, change.relativePath);
138
+ const current = await readSnapshotNode(target);
139
+ if (!snapshotNodesEqual(current, change.after)) {
140
+ throw new Error(TRANSACTIONAL_APPLY_CONFLICT);
141
+ }
142
+ }
143
+ function applyRank(change) {
144
+ if (change.after.kind === 'absent') {
145
+ return 0;
146
+ }
147
+ if (change.after.kind === 'directory') {
148
+ return 1;
149
+ }
150
+ return 2;
151
+ }
152
+ function compareApplyOrder(left, right) {
153
+ const leftRank = applyRank(left);
154
+ const rightRank = applyRank(right);
155
+ const rankDiff = leftRank - rightRank;
156
+ if (rankDiff !== 0) {
157
+ return rankDiff;
158
+ }
159
+ if (leftRank === 0) {
160
+ return compareRelativePathsBytewise(right.relativePath, left.relativePath);
161
+ }
162
+ return compareRelativePathsBytewise(left.relativePath, right.relativePath);
163
+ }
164
+ function sortedChanges(changes) {
165
+ return [...changes].sort(compareApplyOrder);
166
+ }
167
+ async function applySingleChange(sourceRoot, targetRoot, change) {
168
+ const target = joinRelativePath(targetRoot, change.relativePath);
169
+ await assertParentChainSafe(targetRoot, change.relativePath);
170
+ if (change.after.kind === 'absent') {
171
+ if (change.before.kind === 'directory') {
172
+ await rmdir(target);
173
+ return;
174
+ }
175
+ await rm(target, { force: true });
176
+ return;
177
+ }
178
+ if (change.after.kind === 'directory') {
179
+ if (change.before.kind !== 'directory') {
180
+ await removePathIfExists(target);
181
+ }
182
+ await mkdir(target, { recursive: true, mode: change.after.mode & 0o777 });
183
+ await chmodSafe(target, change.after.mode);
184
+ return;
185
+ }
186
+ const source = joinRelativePath(sourceRoot, change.relativePath);
187
+ await copyPathPreservingType(source, target);
188
+ }
189
+ export async function applyObservedChanges(params) {
190
+ const { sourceRoot, targetRoot, changes } = params;
191
+ const plannedDirectories = new Set(changes
192
+ .filter((change) => change.after.kind === 'directory')
193
+ .map((change) => change.relativePath));
194
+ for (const change of changes) {
195
+ validateRelativePath(change.relativePath);
196
+ }
197
+ for (const change of changes) {
198
+ await assertParentChainSafe(targetRoot, change.relativePath, plannedDirectories);
199
+ await assertTargetMatches(targetRoot, change);
200
+ }
201
+ const backupRoot = await mkdtemp(path.join(os.tmpdir(), 'belay-tx-rollback-'));
202
+ const rollbackActions = [];
203
+ let mutationAttempted = false;
204
+ try {
205
+ for (const change of sortedChanges(changes)) {
206
+ const target = joinRelativePath(targetRoot, change.relativePath);
207
+ await assertParentChainSafe(targetRoot, change.relativePath);
208
+ await assertTargetMatches(targetRoot, change);
209
+ let targetExists = true;
210
+ try {
211
+ await lstat(target);
212
+ }
213
+ catch (error) {
214
+ if (error.code !== 'ENOENT') {
215
+ throw error;
216
+ }
217
+ targetExists = false;
218
+ }
219
+ if (targetExists) {
220
+ const backupPath = path.join(backupRoot, String(rollbackActions.length), change.relativePath);
221
+ await mkdir(path.dirname(backupPath), { recursive: true });
222
+ await copyPathPreservingType(target, backupPath);
223
+ await assertParentChainSafe(targetRoot, change.relativePath);
224
+ rollbackActions.push({
225
+ type: 'restore',
226
+ target,
227
+ backupPath,
228
+ relativePath: change.relativePath,
229
+ });
230
+ }
231
+ else if (change.after.kind !== 'absent') {
232
+ await assertParentChainSafe(targetRoot, change.relativePath);
233
+ rollbackActions.push({ type: 'remove', target, relativePath: change.relativePath });
234
+ }
235
+ mutationAttempted = true;
236
+ await applySingleChange(sourceRoot, targetRoot, change);
237
+ }
238
+ for (const change of changes) {
239
+ await assertTargetMatchesAfter(targetRoot, change);
240
+ }
241
+ try {
242
+ await params.afterApply?.();
243
+ }
244
+ catch {
245
+ // Cleanup/audit failure after a verified apply must not roll back or reject success.
246
+ }
247
+ }
248
+ catch (error) {
249
+ if (!mutationAttempted)
250
+ throw error;
251
+ const rollbackOk = await rollbackAppliedChanges(rollbackActions, changes, targetRoot, backupRoot);
252
+ if (!rollbackOk) {
253
+ throw new Error(TRANSACTIONAL_APPLY_ROLLBACK_FAILED);
254
+ }
255
+ throw error;
256
+ }
257
+ finally {
258
+ await rm(backupRoot, { recursive: true, force: true });
259
+ }
260
+ }
261
+ export async function buildObservedChangesFromTransactional(baselineRoot, executionRoot, changes) {
262
+ const observed = [];
263
+ for (const change of changes) {
264
+ validateRelativePath(change.relativePath);
265
+ const before = await readSnapshotNode(joinRelativePath(baselineRoot, change.relativePath));
266
+ const after = await readSnapshotNode(joinRelativePath(executionRoot, change.relativePath));
267
+ observed.push({
268
+ relativePath: change.relativePath,
269
+ kind: change.kind,
270
+ before,
271
+ after,
272
+ });
273
+ }
274
+ return observed;
275
+ }
@@ -0,0 +1,8 @@
1
+ import type { TransactionalBackendContext, TransactionalBackendProbe, TransactionalBackendSelection } from './backend.js';
2
+ export declare const FILE_CHECKPOINT_DISABLED = "file_checkpoint_disabled";
3
+ export declare const FILE_CHECKPOINT_NON_GIT_DISABLED = "file_checkpoint_non_git_disabled";
4
+ export declare const FILE_CHECKPOINT_DURABLE_REQUIRED = "file_checkpoint_durable_checkpoint_required";
5
+ export declare const FILE_CHECKPOINT_ISOLATION_UNAVAILABLE = "file_checkpoint_isolation_unavailable";
6
+ export declare const FILE_CHECKPOINT_NOT_IMPLEMENTED = "file_checkpoint_not_implemented";
7
+ export declare function probeTransactionalBackends(context: TransactionalBackendContext): Promise<TransactionalBackendProbe[]>;
8
+ export declare function selectTransactionalBackend(context: TransactionalBackendContext): Promise<TransactionalBackendSelection>;
@@ -0,0 +1,118 @@
1
+ import { attestsWorkspaceMountIsolation } from '../capability/attestation.js';
2
+ import { fileCheckpointBackend } from './file-checkpoint-backend.js';
3
+ import { isDirtyWorktree, isGitWorktreeAvailable } from './git-worktree.js';
4
+ import { gitWorktreeBackend } from './git-worktree-backend.js';
5
+ export const FILE_CHECKPOINT_DISABLED = 'file_checkpoint_disabled';
6
+ export const FILE_CHECKPOINT_NON_GIT_DISABLED = 'file_checkpoint_non_git_disabled';
7
+ export const FILE_CHECKPOINT_DURABLE_REQUIRED = 'file_checkpoint_durable_checkpoint_required';
8
+ export const FILE_CHECKPOINT_ISOLATION_UNAVAILABLE = 'file_checkpoint_isolation_unavailable';
9
+ export const FILE_CHECKPOINT_NOT_IMPLEMENTED = 'file_checkpoint_not_implemented';
10
+ function fileCheckpointProbe(reason, signals) {
11
+ return {
12
+ eligible: false,
13
+ backend: 'file_checkpoint',
14
+ reason,
15
+ signals,
16
+ };
17
+ }
18
+ function fileCheckpointIsolationReason(context) {
19
+ const attestation = context.boundaryAttestation;
20
+ const fresh = context.boundaryAttestationFresh === true;
21
+ const driverId = context.boundaryDriverId;
22
+ if (!attestation || !fresh || !driverId) {
23
+ return FILE_CHECKPOINT_ISOLATION_UNAVAILABLE;
24
+ }
25
+ if (attestation.driver !== driverId) {
26
+ return FILE_CHECKPOINT_ISOLATION_UNAVAILABLE;
27
+ }
28
+ if (!attestsWorkspaceMountIsolation(attestation)) {
29
+ return FILE_CHECKPOINT_ISOLATION_UNAVAILABLE;
30
+ }
31
+ return null;
32
+ }
33
+ export async function probeTransactionalBackends(context) {
34
+ const probes = [await gitWorktreeBackend.probe(context)];
35
+ if (await isGitWorktreeAvailable(context.repoRoot)) {
36
+ probes.push(await probeFileCheckpointForGit(context));
37
+ }
38
+ else {
39
+ probes.push(await probeFileCheckpointForNonGit(context));
40
+ }
41
+ return probes;
42
+ }
43
+ async function gitWorkspaceSignals(context) {
44
+ const signals = ['git_repository'];
45
+ if (await isDirtyWorktree(context.repoRoot, { ignoreRoots: context.dirtyIgnoreRoots })) {
46
+ signals.push('dirty_git_worktree');
47
+ }
48
+ return signals;
49
+ }
50
+ async function probeFileCheckpointForGit(context) {
51
+ const signals = await gitWorkspaceSignals(context);
52
+ if (!context.fileCheckpoint.enabled) {
53
+ return fileCheckpointProbe(FILE_CHECKPOINT_DISABLED, signals);
54
+ }
55
+ if (!context.durableCheckpointEnabled) {
56
+ return fileCheckpointProbe(FILE_CHECKPOINT_DURABLE_REQUIRED, signals);
57
+ }
58
+ const isolationReason = fileCheckpointIsolationReason(context);
59
+ if (isolationReason) {
60
+ return fileCheckpointProbe(isolationReason, [...signals, 'isolation_unavailable']);
61
+ }
62
+ const backendProbe = await fileCheckpointBackend.probe(context);
63
+ if (backendProbe.eligible) {
64
+ return backendProbe;
65
+ }
66
+ return fileCheckpointProbe(backendProbe.reason ?? FILE_CHECKPOINT_NOT_IMPLEMENTED, [
67
+ ...signals,
68
+ ...(backendProbe.reason === FILE_CHECKPOINT_NOT_IMPLEMENTED ? ['not_implemented'] : []),
69
+ ]);
70
+ }
71
+ async function probeFileCheckpointForNonGit(context) {
72
+ if (!context.fileCheckpoint.enabled) {
73
+ return fileCheckpointProbe(FILE_CHECKPOINT_DISABLED, ['non_git_workspace']);
74
+ }
75
+ if (!context.fileCheckpoint.allowNonGit) {
76
+ return fileCheckpointProbe(FILE_CHECKPOINT_NON_GIT_DISABLED, ['non_git_workspace']);
77
+ }
78
+ if (!context.durableCheckpointEnabled) {
79
+ return fileCheckpointProbe(FILE_CHECKPOINT_DURABLE_REQUIRED, ['non_git_workspace']);
80
+ }
81
+ const isolationReason = fileCheckpointIsolationReason(context);
82
+ if (isolationReason) {
83
+ return fileCheckpointProbe(isolationReason, ['non_git_workspace', 'isolation_unavailable']);
84
+ }
85
+ return fileCheckpointProbe(FILE_CHECKPOINT_NOT_IMPLEMENTED, [
86
+ 'non_git_workspace',
87
+ 'not_implemented',
88
+ ]);
89
+ }
90
+ export async function selectTransactionalBackend(context) {
91
+ const gitProbe = await gitWorktreeBackend.probe(context);
92
+ if (gitProbe.eligible) {
93
+ return {
94
+ backend: gitWorktreeBackend,
95
+ probe: gitProbe,
96
+ };
97
+ }
98
+ if (await isGitWorktreeAvailable(context.repoRoot)) {
99
+ const fileProbe = await probeFileCheckpointForGit(context);
100
+ if (fileProbe.eligible) {
101
+ return {
102
+ backend: fileCheckpointBackend,
103
+ probe: fileProbe,
104
+ };
105
+ }
106
+ return {
107
+ backend: null,
108
+ probe: fileProbe,
109
+ skipReason: 'dirty_worktree',
110
+ };
111
+ }
112
+ const fileProbe = await probeFileCheckpointForNonGit(context);
113
+ return {
114
+ backend: null,
115
+ probe: fileProbe,
116
+ skipReason: 'git_worktree_unavailable',
117
+ };
118
+ }
@@ -0,0 +1,55 @@
1
+ import type { BoundaryAttestation, BoundaryDriverId } from '../capability/attestation.js';
2
+ import type { BelayFileCheckpointConfig } from '../config.js';
3
+ import type { TransactionalFileChange } from './types.js';
4
+ export type TransactionalBackendId = 'git_worktree' | 'file_checkpoint';
5
+ export interface TransactionalBackendProbe {
6
+ eligible: boolean;
7
+ backend: TransactionalBackendId;
8
+ reason?: string;
9
+ signals: string[];
10
+ }
11
+ export interface TransactionalSnapshot {
12
+ backend: TransactionalBackendId;
13
+ resourceRoot: string;
14
+ executionRoot: string;
15
+ baselineRoot?: string;
16
+ resourceKind?: 'git_repository' | 'directory';
17
+ resourceIdentity: string;
18
+ baselineTreeHash: string;
19
+ excludedRoots: string[];
20
+ copyStrategy?: 'clonefile' | 'reflink' | 'copy';
21
+ snapshotFileCount?: number;
22
+ snapshotSourceBytes?: number;
23
+ snapshotWorkspaceBytes?: number;
24
+ snapshotPrepareMs?: number;
25
+ /** Relative cwd inside resourceRoot for isolated workspace mounts. */
26
+ executionCwdRelative?: string;
27
+ /** Revalidates the source tree and metadata immediately before apply. */
28
+ validateSourceState?(): Promise<void>;
29
+ collectChanges(): Promise<TransactionalFileChange[]>;
30
+ cleanup(): Promise<void>;
31
+ }
32
+ export interface TransactionalBackendContext {
33
+ repoRoot: string;
34
+ stateDir: string;
35
+ cwd: string;
36
+ dirtyIgnoreRoots?: string[];
37
+ fileCheckpoint: BelayFileCheckpointConfig;
38
+ /** Required for file_checkpoint eligibility once implemented. */
39
+ durableCheckpointEnabled: boolean;
40
+ /** Verified boundary attestation for file_checkpoint isolation checks. */
41
+ boundaryAttestation?: BoundaryAttestation | null;
42
+ boundaryAttestationFresh?: boolean;
43
+ /** Resolved boundary driver; attestation must match this id for file_checkpoint. */
44
+ boundaryDriverId?: BoundaryDriverId;
45
+ }
46
+ export interface TransactionalBackend {
47
+ id: TransactionalBackendId;
48
+ probe(context: TransactionalBackendContext): Promise<TransactionalBackendProbe>;
49
+ prepare(context: TransactionalBackendContext): Promise<TransactionalSnapshot>;
50
+ }
51
+ export interface TransactionalBackendSelection {
52
+ backend: TransactionalBackend | null;
53
+ probe: TransactionalBackendProbe;
54
+ skipReason?: string;
55
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -51,7 +51,10 @@ export function evaluateTransactionalDiff(changes, ctx) {
51
51
  categories.add('large_deletion');
52
52
  }
53
53
  const categoryList = [...categories];
54
- const dangerous = categoryList.includes('large_deletion');
54
+ const dangerous = categoryList.includes('large_deletion') ||
55
+ categoryList.includes('repo_outside') ||
56
+ categoryList.includes('sensitive_path') ||
57
+ categoryList.includes('control_plane');
55
58
  const base = {
56
59
  categories: categoryList,
57
60
  changes,
@@ -1,3 +1,4 @@
1
+ import { capabilityRequestsBlockRecovery } from '../recovery/capability.js';
1
2
  const EXCLUDED_REASONS = new Set([
2
3
  'unparseable_shell',
3
4
  'external_effect',
@@ -26,6 +27,9 @@ export function isTransactionalEligible(config, kind, result) {
26
27
  if (EXCLUDED_REASONS.has(result.reason)) {
27
28
  return false;
28
29
  }
30
+ if (capabilityRequestsBlockRecovery(result.capabilityRequests)) {
31
+ return false;
32
+ }
29
33
  const { assessment } = result;
30
34
  if (assessment.external) {
31
35
  return false;
@@ -0,0 +1,3 @@
1
+ import type { TransactionalBackend } from './backend.js';
2
+ export declare const FILE_CHECKPOINT_PROTECTED_PATH_CHANGED = "file_checkpoint_protected_path_changed";
3
+ export declare const fileCheckpointBackend: TransactionalBackend;