@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.
- package/README.md +27 -2
- package/dist/adapters/claude/runtime-entry.js +54 -25
- package/dist/adapters/layouts/scope.js +3 -2
- package/dist/adapters/shared/gate-runtime.d.ts +8 -2
- package/dist/adapters/shared/gate-runtime.js +579 -181
- package/dist/bundle/claude-runtime.mjs +13574 -6334
- package/dist/bundle/codex-runtime.mjs +13108 -5903
- package/dist/bundle/cursor-runtime.mjs +12676 -5476
- package/dist/cli.js +80 -2
- package/dist/commands/approve.js +34 -10
- package/dist/commands/classify-for-report.js +1 -1
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +134 -11
- package/dist/commands/doctor.js +87 -1
- package/dist/commands/explain.js +11 -1
- package/dist/commands/judge.js +3 -2
- package/dist/commands/recovery-checkpoints.d.ts +164 -0
- package/dist/commands/recovery-checkpoints.js +347 -0
- package/dist/commands/revoke.js +19 -7
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.js +51 -0
- package/dist/commands/status.js +7 -0
- package/dist/config-io.js +91 -23
- package/dist/conformance/guarantee-posture.d.ts +21 -0
- package/dist/conformance/guarantee-posture.js +46 -0
- package/dist/conformance/guarantee-table.d.ts +1 -0
- package/dist/conformance/guarantee-table.js +20 -9
- package/dist/conformance/types.d.ts +1 -0
- package/dist/core/approval-replay.d.ts +1 -1
- package/dist/core/approval-replay.js +4 -1
- package/dist/core/approval-service.d.ts +6 -1
- package/dist/core/approval-service.js +87 -24
- package/dist/core/approval.d.ts +4 -0
- package/dist/core/approval.js +19 -5
- package/dist/core/audit-types.d.ts +13 -0
- package/dist/core/capability/approval-state-mutation.d.ts +32 -0
- package/dist/core/capability/approval-state-mutation.js +165 -0
- package/dist/core/capability/approval-v3.d.ts +32 -0
- package/dist/core/capability/approval-v3.js +114 -0
- package/dist/core/capability/attestation.d.ts +16 -0
- package/dist/core/capability/attestation.js +62 -0
- package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
- package/dist/core/capability/boundary-attestation-sign.js +84 -0
- package/dist/core/capability/boundary-driver-container.d.ts +19 -0
- package/dist/core/capability/boundary-driver-container.js +189 -0
- package/dist/core/capability/boundary-driver.d.ts +29 -0
- package/dist/core/capability/boundary-driver.js +44 -0
- package/dist/core/capability/boundary-egress.d.ts +21 -0
- package/dist/core/capability/boundary-egress.js +109 -0
- package/dist/core/capability/boundary-grant-materialize.d.ts +14 -0
- package/dist/core/capability/boundary-grant-materialize.js +93 -0
- package/dist/core/capability/boundary-profile.d.ts +15 -0
- package/dist/core/capability/boundary-profile.js +25 -0
- package/dist/core/capability/boundary-run.d.ts +48 -0
- package/dist/core/capability/boundary-run.js +51 -0
- package/dist/core/capability/boundary-session.d.ts +61 -0
- package/dist/core/capability/boundary-session.js +138 -0
- package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
- package/dist/core/capability/boundary-workspace-mount.js +68 -0
- package/dist/core/capability/capability-request-hash.d.ts +2 -0
- package/dist/core/capability/capability-request-hash.js +8 -0
- package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
- package/dist/core/capability/gate-latency-ratchet.js +18 -0
- package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
- package/dist/core/capability/gate-policy-shadow.js +130 -0
- package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
- package/dist/core/capability/gate-shadow-audit.js +1 -0
- package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
- package/dist/core/capability/gate-shadow-ratchet.js +81 -0
- package/dist/core/capability/grant-consumption.d.ts +4 -0
- package/dist/core/capability/grant-consumption.js +11 -0
- package/dist/core/capability/grant-lease.d.ts +44 -0
- package/dist/core/capability/grant-lease.js +243 -0
- package/dist/core/capability/grant-loader.d.ts +12 -0
- package/dist/core/capability/grant-loader.js +23 -0
- package/dist/core/capability/grant-match.d.ts +6 -0
- package/dist/core/capability/grant-match.js +96 -0
- package/dist/core/capability/grant.d.ts +16 -0
- package/dist/core/capability/grant.js +16 -0
- package/dist/core/capability/index.d.ts +8 -0
- package/dist/core/capability/index.js +7 -0
- package/dist/core/capability/limits.d.ts +7 -0
- package/dist/core/capability/limits.js +55 -0
- package/dist/core/capability/policy-bridge.d.ts +18 -0
- package/dist/core/capability/policy-bridge.js +51 -0
- package/dist/core/capability/policy-engine.d.ts +77 -0
- package/dist/core/capability/policy-engine.js +663 -0
- package/dist/core/capability/policy-types.d.ts +20 -0
- package/dist/core/capability/policy-types.js +1 -0
- package/dist/core/capability/request.d.ts +48 -0
- package/dist/core/capability/request.js +1 -0
- package/dist/core/capability/resolver.d.ts +4 -0
- package/dist/core/capability/resolver.js +4 -0
- package/dist/core/classify-subagent.d.ts +2 -1
- package/dist/core/classify-subagent.js +38 -2
- package/dist/core/classify-tool.js +126 -62
- package/dist/core/config.d.ts +34 -3
- package/dist/core/config.js +118 -3
- package/dist/core/effect-ir/audit.d.ts +9 -0
- package/dist/core/effect-ir/audit.js +86 -0
- package/dist/core/effect-ir/build.d.ts +31 -0
- package/dist/core/effect-ir/build.js +277 -0
- package/dist/core/effect-ir/index.d.ts +8 -0
- package/dist/core/effect-ir/index.js +7 -0
- package/dist/core/effect-ir/normalize.d.ts +4 -0
- package/dist/core/effect-ir/normalize.js +104 -0
- package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
- package/dist/core/effect-ir/package-exec-eval.js +152 -0
- package/dist/core/effect-ir/package-exec.d.ts +29 -0
- package/dist/core/effect-ir/package-exec.js +292 -0
- package/dist/core/effect-ir/policy.d.ts +13 -0
- package/dist/core/effect-ir/policy.js +77 -0
- package/dist/core/effect-ir/types.d.ts +55 -0
- package/dist/core/effect-ir/types.js +1 -0
- package/dist/core/egress/allowlist.d.ts +1 -0
- package/dist/core/egress/allowlist.js +8 -0
- package/dist/core/egress-approval.js +45 -51
- package/dist/core/gate-contract.d.ts +5 -0
- package/dist/core/gate-contract.js +5 -0
- package/dist/core/gate-engine.js +30 -8
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +2 -1
- package/dist/core/judge-cloud-consent.js +29 -15
- package/dist/core/judge-doctor.js +39 -14
- package/dist/core/judge-fallback-hints.d.ts +13 -0
- package/dist/core/judge-fallback-hints.js +108 -0
- package/dist/core/network-endpoint.d.ts +10 -0
- package/dist/core/network-endpoint.js +51 -0
- package/dist/core/path-utils.d.ts +1 -0
- package/dist/core/path-utils.js +5 -2
- package/dist/core/process-runner.d.ts +10 -0
- package/dist/core/process-runner.js +124 -0
- package/dist/core/recover-advice.js +2 -1
- package/dist/core/recovery/artifact-store.d.ts +29 -0
- package/dist/core/recovery/artifact-store.js +336 -0
- package/dist/core/recovery/capability.d.ts +3 -0
- package/dist/core/recovery/capability.js +22 -0
- package/dist/core/recovery/checkpoint.d.ts +25 -0
- package/dist/core/recovery/checkpoint.js +260 -0
- package/dist/core/recovery/fail-closed.d.ts +6 -0
- package/dist/core/recovery/fail-closed.js +25 -0
- package/dist/core/recovery/index.d.ts +5 -0
- package/dist/core/recovery/index.js +4 -0
- package/dist/core/recovery/operator-guidance.d.ts +7 -0
- package/dist/core/recovery/operator-guidance.js +39 -0
- package/dist/core/recovery/reconcile.d.ts +3 -0
- package/dist/core/recovery/reconcile.js +48 -0
- package/dist/core/recovery/resource-identity.d.ts +4 -0
- package/dist/core/recovery/resource-identity.js +34 -0
- package/dist/core/recovery/restore.d.ts +13 -0
- package/dist/core/recovery/restore.js +114 -0
- package/dist/core/recovery/snapshot-node.d.ts +27 -0
- package/dist/core/recovery/snapshot-node.js +261 -0
- package/dist/core/recovery/types.d.ts +104 -0
- package/dist/core/recovery/types.js +1 -0
- package/dist/core/standing-allow.js +3 -0
- package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
- package/dist/core/transactional/apply-observed-changes.js +275 -0
- package/dist/core/transactional/backend-selector.d.ts +8 -0
- package/dist/core/transactional/backend-selector.js +118 -0
- package/dist/core/transactional/backend.d.ts +55 -0
- package/dist/core/transactional/backend.js +1 -0
- package/dist/core/transactional/diff-evaluator.js +4 -1
- package/dist/core/transactional/eligibility.js +4 -0
- package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-backend.js +278 -0
- package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
- package/dist/core/transactional/file-checkpoint-git.js +217 -0
- package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
- package/dist/core/transactional/file-checkpoint-staging.js +54 -0
- package/dist/core/transactional/file-clone.d.ts +15 -0
- package/dist/core/transactional/file-clone.js +139 -0
- package/dist/core/transactional/file-tree-path.d.ts +7 -0
- package/dist/core/transactional/file-tree-path.js +50 -0
- package/dist/core/transactional/file-tree.d.ts +41 -0
- package/dist/core/transactional/file-tree.js +186 -0
- package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
- package/dist/core/transactional/git-worktree-backend.js +50 -0
- package/dist/core/transactional/git-worktree.d.ts +14 -9
- package/dist/core/transactional/git-worktree.js +62 -87
- package/dist/core/transactional/index.d.ts +11 -0
- package/dist/core/transactional/index.js +10 -0
- package/dist/core/transactional/reasons.d.ts +1 -0
- package/dist/core/transactional/reasons.js +5 -0
- package/dist/core/transactional/runner.js +171 -23
- package/dist/core/transactional/snapshot-node.d.ts +26 -0
- package/dist/core/transactional/snapshot-node.js +87 -0
- package/dist/core/transactional/types.d.ts +21 -1
- package/dist/core/types.d.ts +30 -3
- package/dist/core/verdict/adapter.d.ts +3 -4
- package/dist/core/verdict/adapter.js +24 -12
- package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
- package/dist/core/verdict/cursor-acp-client.js +345 -0
- package/dist/core/verdict/egress-classify.d.ts +2 -2
- package/dist/core/verdict/egress-classify.js +2 -2
- package/dist/core/verdict/git-classifier.d.ts +26 -0
- package/dist/core/verdict/git-classifier.js +533 -0
- package/dist/core/verdict/judge-audit.js +1 -0
- package/dist/core/verdict/judge-baseline.d.ts +1 -1
- package/dist/core/verdict/judge-baseline.js +1 -0
- package/dist/core/verdict/judge-broker-service.js +12 -0
- package/dist/core/verdict/judge-catalog.js +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +6 -0
- package/dist/core/verdict/judge-cli.js +5 -3
- package/dist/core/verdict/judge-provider-matrix.js +1 -1
- package/dist/core/verdict/judge-runtime-config.d.ts +2 -0
- package/dist/core/verdict/judge-runtime-config.js +10 -6
- package/dist/core/verdict/judge-session-broker.d.ts +1 -1
- package/dist/core/verdict/judge-session-broker.js +10 -5
- package/dist/core/verdict/judge-transport.d.ts +5 -1
- package/dist/core/verdict/judge-transport.js +24 -14
- package/dist/core/verdict/judge.d.ts +2 -15
- package/dist/core/verdict/judge.js +5 -68
- package/dist/core/verdict/launcher-resolve.d.ts +2 -0
- package/dist/core/verdict/launcher-resolve.js +14 -16
- package/dist/core/verdict/prescan.d.ts +15 -0
- package/dist/core/verdict/prescan.js +68 -0
- package/dist/core/verdict/shell-policy.d.ts +26 -0
- package/dist/core/verdict/shell-policy.js +40 -0
- package/dist/core/verdict/shell-semantics.d.ts +24 -0
- package/dist/core/verdict/shell-semantics.js +225 -0
- package/dist/core/verdict/types.d.ts +18 -1
- package/dist/core/verdict/verdict.js +313 -178
- package/dist/corpus/evaluate.js +3 -7
- package/dist/corpus/gate-latency-budget.d.ts +39 -0
- package/dist/corpus/gate-latency-budget.js +33 -0
- package/dist/corpus/mutators.d.ts +1 -1
- package/dist/corpus/mutators.js +11 -0
- package/dist/corpus/runtime-match.js +1 -3
- package/dist/corpus/standing-allow-catalog.generated.js +29 -9
- package/dist/judge-broker-daemon.js +15 -1
- package/dist/services/sandbox-service.d.ts +4 -0
- package/dist/services/sandbox-service.js +28 -2
- package/dist/types.d.ts +13 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/belay/SKILL.md +8 -3
- package/skills/belay/belay-approve.md +10 -7
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
4
|
-
import { copyFile, mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
5
2
|
import os from 'node:os';
|
|
6
3
|
import path from 'node:path';
|
|
7
|
-
import { canonicalPath } from '../path-utils.js';
|
|
4
|
+
import { canonicalPath, isPathOutsideRoot, pathWithinRoot } from '../path-utils.js';
|
|
5
|
+
import { runProcessWithBoundedOutput } from '../process-runner.js';
|
|
6
|
+
import { applyObservedChanges, buildObservedChangesFromTransactional, TRANSACTIONAL_APPLY_ROLLBACK_FAILED, TRANSACTIONAL_APPLY_TOCTOU, } from './apply-observed-changes.js';
|
|
7
|
+
export { TRANSACTIONAL_APPLY_ROLLBACK_FAILED, TRANSACTIONAL_APPLY_TOCTOU };
|
|
8
8
|
function execGit(repoRoot, args) {
|
|
9
9
|
return new Promise((resolve, reject) => {
|
|
10
10
|
const child = spawn('git', ['-C', repoRoot, ...args], {
|
|
@@ -24,6 +24,22 @@ function execGit(repoRoot, args) {
|
|
|
24
24
|
});
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
|
+
function porcelainRelativePath(line) {
|
|
28
|
+
if (line.length < 4) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const relativePath = line.slice(3);
|
|
32
|
+
return relativePath || null;
|
|
33
|
+
}
|
|
34
|
+
function isIgnoredDirtyPath(repoRoot, relativePath, ignoreRoots) {
|
|
35
|
+
if (ignoreRoots.length === 0) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const absolutePath = canonicalPath(path.join(repoRoot, relativePath));
|
|
39
|
+
return ignoreRoots.some((root) => pathWithinRoot(root, absolutePath) ||
|
|
40
|
+
root === absolutePath ||
|
|
41
|
+
pathWithinRoot(absolutePath, root));
|
|
42
|
+
}
|
|
27
43
|
export async function isGitWorktreeAvailable(repoRoot) {
|
|
28
44
|
try {
|
|
29
45
|
await execGit(repoRoot, ['rev-parse', '--git-dir']);
|
|
@@ -33,18 +49,33 @@ export async function isGitWorktreeAvailable(repoRoot) {
|
|
|
33
49
|
return false;
|
|
34
50
|
}
|
|
35
51
|
}
|
|
36
|
-
export async function isDirtyWorktree(repoRoot) {
|
|
52
|
+
export async function isDirtyWorktree(repoRoot, options) {
|
|
37
53
|
try {
|
|
38
|
-
const status = await execGit(repoRoot, ['status', '--porcelain'
|
|
39
|
-
|
|
54
|
+
const status = await execGit(repoRoot, ['status', '--porcelain']);
|
|
55
|
+
const ignoreRoots = (options?.ignoreRoots ?? []).map((root) => canonicalPath(root));
|
|
56
|
+
const repoRootCanonical = canonicalPath(repoRoot);
|
|
57
|
+
for (const line of status.split('\n')) {
|
|
58
|
+
if (!line.trim()) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const relativePath = porcelainRelativePath(line);
|
|
62
|
+
if (!relativePath) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (isIgnoredDirtyPath(repoRootCanonical, relativePath, ignoreRoots)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
40
71
|
}
|
|
41
72
|
catch {
|
|
42
73
|
return true;
|
|
43
74
|
}
|
|
44
75
|
}
|
|
45
|
-
export async function createGitWorktreeSnapshot(repoRoot,
|
|
46
|
-
const
|
|
47
|
-
await
|
|
76
|
+
export async function createGitWorktreeSnapshot(repoRoot, _stateDir) {
|
|
77
|
+
const { mkdtemp, rm } = await import('node:fs/promises');
|
|
78
|
+
const worktreePath = await mkdtemp(path.join(os.tmpdir(), 'belay-tx-'));
|
|
48
79
|
await execGit(repoRoot, ['worktree', 'add', '--detach', worktreePath, 'HEAD']);
|
|
49
80
|
return {
|
|
50
81
|
worktreePath,
|
|
@@ -67,7 +98,7 @@ export async function createGitWorktreeSnapshot(repoRoot, stateDir) {
|
|
|
67
98
|
export function resolveWorktreeCwd(repoRoot, worktreePath, cwd) {
|
|
68
99
|
const resolvedCwd = canonicalPath(cwd);
|
|
69
100
|
const relative = path.relative(canonicalPath(repoRoot), resolvedCwd);
|
|
70
|
-
if (relative
|
|
101
|
+
if (isPathOutsideRoot(relative) || path.isAbsolute(relative)) {
|
|
71
102
|
return worktreePath;
|
|
72
103
|
}
|
|
73
104
|
if (relative === '') {
|
|
@@ -76,38 +107,14 @@ export function resolveWorktreeCwd(repoRoot, worktreePath, cwd) {
|
|
|
76
107
|
return path.join(worktreePath, relative);
|
|
77
108
|
}
|
|
78
109
|
export function runShellCommand(command, cwd, timeoutMs) {
|
|
79
|
-
return
|
|
80
|
-
const child = spawn(command, {
|
|
81
|
-
cwd,
|
|
82
|
-
shell: true,
|
|
83
|
-
stdio: 'ignore',
|
|
84
|
-
env: process.env,
|
|
85
|
-
});
|
|
86
|
-
let timedOut = false;
|
|
87
|
-
const timer = setTimeout(() => {
|
|
88
|
-
timedOut = true;
|
|
89
|
-
child.kill('SIGTERM');
|
|
90
|
-
}, timeoutMs);
|
|
91
|
-
child.on('error', () => {
|
|
92
|
-
clearTimeout(timer);
|
|
93
|
-
resolve({ exitCode: 1, signal: null, timedOut });
|
|
94
|
-
});
|
|
95
|
-
child.on('close', (exitCode, signal) => {
|
|
96
|
-
clearTimeout(timer);
|
|
97
|
-
resolve({
|
|
98
|
-
exitCode,
|
|
99
|
-
signal: signal ? String(signal) : null,
|
|
100
|
-
timedOut,
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
});
|
|
110
|
+
return runProcessWithBoundedOutput(command, [], { cwd, shell: true, env: process.env }, timeoutMs);
|
|
104
111
|
}
|
|
105
112
|
function parseStatusLine(line) {
|
|
106
113
|
if (line.length < 4) {
|
|
107
114
|
return null;
|
|
108
115
|
}
|
|
109
116
|
const status = line.slice(0, 2);
|
|
110
|
-
const relativePath = line.slice(3)
|
|
117
|
+
const relativePath = line.slice(3);
|
|
111
118
|
if (!relativePath) {
|
|
112
119
|
return null;
|
|
113
120
|
}
|
|
@@ -123,11 +130,17 @@ function parseStatusLine(line) {
|
|
|
123
130
|
return { relativePath, kind: 'modified' };
|
|
124
131
|
}
|
|
125
132
|
export async function collectWorktreeChanges(worktreePath) {
|
|
126
|
-
const status = await execGit(worktreePath, [
|
|
133
|
+
const status = await execGit(worktreePath, [
|
|
134
|
+
'status',
|
|
135
|
+
'--porcelain=v1',
|
|
136
|
+
'-z',
|
|
137
|
+
'-uall',
|
|
138
|
+
'--no-renames',
|
|
139
|
+
]);
|
|
127
140
|
const changes = [];
|
|
128
141
|
const seen = new Set();
|
|
129
|
-
for (const line of status.split('\
|
|
130
|
-
if (!line
|
|
142
|
+
for (const line of status.split('\0')) {
|
|
143
|
+
if (!line) {
|
|
131
144
|
continue;
|
|
132
145
|
}
|
|
133
146
|
const change = parseStatusLine(line);
|
|
@@ -139,51 +152,13 @@ export async function collectWorktreeChanges(worktreePath) {
|
|
|
139
152
|
}
|
|
140
153
|
return changes;
|
|
141
154
|
}
|
|
142
|
-
async function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
// best effort
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
export async function applyWorktreeChanges(worktreePath, repoRoot, changes) {
|
|
159
|
-
const backupRoot = await mkdtemp(path.join(os.tmpdir(), 'belay-tx-rollback-'));
|
|
160
|
-
const rollbackActions = [];
|
|
161
|
-
try {
|
|
162
|
-
for (const change of changes) {
|
|
163
|
-
const target = path.join(repoRoot, change.relativePath);
|
|
164
|
-
if (existsSync(target)) {
|
|
165
|
-
const backupPath = path.join(backupRoot, change.relativePath);
|
|
166
|
-
await mkdir(path.dirname(backupPath), { recursive: true });
|
|
167
|
-
await copyFile(target, backupPath);
|
|
168
|
-
rollbackActions.push({ type: 'restore', target, backupPath });
|
|
169
|
-
}
|
|
170
|
-
else if (change.kind !== 'deleted') {
|
|
171
|
-
rollbackActions.push({ type: 'remove', target });
|
|
172
|
-
}
|
|
173
|
-
if (change.kind === 'deleted') {
|
|
174
|
-
await rm(target, { force: true });
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
const source = path.join(worktreePath, change.relativePath);
|
|
178
|
-
await mkdir(path.dirname(target), { recursive: true });
|
|
179
|
-
await copyFile(source, target);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
await rollbackAppliedChanges(rollbackActions);
|
|
184
|
-
throw error;
|
|
185
|
-
}
|
|
186
|
-
finally {
|
|
187
|
-
await rm(backupRoot, { recursive: true, force: true });
|
|
188
|
-
}
|
|
155
|
+
export async function applyWorktreeChanges(worktreePath, repoRoot, changes, options) {
|
|
156
|
+
const observed = options?.observedChanges ??
|
|
157
|
+
(await buildObservedChangesFromTransactional(repoRoot, worktreePath, changes));
|
|
158
|
+
await applyObservedChanges({
|
|
159
|
+
sourceRoot: worktreePath,
|
|
160
|
+
targetRoot: repoRoot,
|
|
161
|
+
changes: observed,
|
|
162
|
+
afterApply: options?.afterApply,
|
|
163
|
+
});
|
|
189
164
|
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
export { applyObservedChanges, buildObservedChangesFromTransactional, TRANSACTIONAL_APPLY_CONFLICT, TRANSACTIONAL_APPLY_ROLLBACK_FAILED, TRANSACTIONAL_APPLY_TOCTOU, } from './apply-observed-changes.js';
|
|
2
|
+
export type { TransactionalBackend, TransactionalBackendContext, TransactionalBackendId, TransactionalBackendProbe, TransactionalBackendSelection, TransactionalSnapshot, } from './backend.js';
|
|
3
|
+
export { FILE_CHECKPOINT_DISABLED, FILE_CHECKPOINT_DURABLE_REQUIRED, FILE_CHECKPOINT_ISOLATION_UNAVAILABLE, FILE_CHECKPOINT_NON_GIT_DISABLED, FILE_CHECKPOINT_NOT_IMPLEMENTED, probeTransactionalBackends, selectTransactionalBackend, } from './backend-selector.js';
|
|
1
4
|
export { evaluateTransactionalDiff } from './diff-evaluator.js';
|
|
2
5
|
export { isTransactionalEligible } from './eligibility.js';
|
|
6
|
+
export { FILE_CHECKPOINT_PROTECTED_PATH_CHANGED, fileCheckpointBackend, } from './file-checkpoint-backend.js';
|
|
7
|
+
export { FILE_CHECKPOINT_CWD_OUTSIDE_ROOT, FILE_CHECKPOINT_GIT_METADATA_CHANGED, FILE_CHECKPOINT_SOURCE_CHANGED, FILE_CHECKPOINT_SPLIT_INDEX_UNSUPPORTED, resolveExecutionCwdRelative, } from './file-checkpoint-git.js';
|
|
8
|
+
export { collectDeadOwnerStaging, type FileCheckpointOwnerMarker, isOwnerProcessAlive, removeDeadOwnerStaging, writeOwnerMarker, } from './file-checkpoint-staging.js';
|
|
9
|
+
export { cloneDirectoryTree, type FileCloneResult, type FileCloneStrategy, probeFileCloneStrategy, } from './file-clone.js';
|
|
10
|
+
export { buildFileTreeIndex, diffFileTreeIndices, FileCheckpointDiagnosticError, type FileTreeEntry, type FileTreeIndex, type ObservedFileChange, readObservedChanges, } from './file-tree.js';
|
|
11
|
+
export { compareRelativePathsBytewise, joinRelativePath, validateRelativePath, } from './file-tree-path.js';
|
|
12
|
+
export { gitWorktreeBackend } from './git-worktree-backend.js';
|
|
3
13
|
export { TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, TRANSACTIONAL_OBSERVED_RISK, } from './reasons.js';
|
|
4
14
|
export { runTransactionalExecution } from './runner.js';
|
|
15
|
+
export { hashDirectoryNode, hashFileContent, type PresentSnapshotNode, readSnapshotNode, type SnapshotNode, snapshotNodesEqual, } from './snapshot-node.js';
|
|
5
16
|
export type { TransactionalDiffEvaluation, TransactionalExecutionResult, TransactionalFileChange, TransactionalRunnerParams, } from './types.js';
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
export { applyObservedChanges, buildObservedChangesFromTransactional, TRANSACTIONAL_APPLY_CONFLICT, TRANSACTIONAL_APPLY_ROLLBACK_FAILED, TRANSACTIONAL_APPLY_TOCTOU, } from './apply-observed-changes.js';
|
|
2
|
+
export { FILE_CHECKPOINT_DISABLED, FILE_CHECKPOINT_DURABLE_REQUIRED, FILE_CHECKPOINT_ISOLATION_UNAVAILABLE, FILE_CHECKPOINT_NON_GIT_DISABLED, FILE_CHECKPOINT_NOT_IMPLEMENTED, probeTransactionalBackends, selectTransactionalBackend, } from './backend-selector.js';
|
|
1
3
|
export { evaluateTransactionalDiff } from './diff-evaluator.js';
|
|
2
4
|
export { isTransactionalEligible } from './eligibility.js';
|
|
5
|
+
export { FILE_CHECKPOINT_PROTECTED_PATH_CHANGED, fileCheckpointBackend, } from './file-checkpoint-backend.js';
|
|
6
|
+
export { FILE_CHECKPOINT_CWD_OUTSIDE_ROOT, FILE_CHECKPOINT_GIT_METADATA_CHANGED, FILE_CHECKPOINT_SOURCE_CHANGED, FILE_CHECKPOINT_SPLIT_INDEX_UNSUPPORTED, resolveExecutionCwdRelative, } from './file-checkpoint-git.js';
|
|
7
|
+
export { collectDeadOwnerStaging, isOwnerProcessAlive, removeDeadOwnerStaging, writeOwnerMarker, } from './file-checkpoint-staging.js';
|
|
8
|
+
export { cloneDirectoryTree, probeFileCloneStrategy, } from './file-clone.js';
|
|
9
|
+
export { buildFileTreeIndex, diffFileTreeIndices, FileCheckpointDiagnosticError, readObservedChanges, } from './file-tree.js';
|
|
10
|
+
export { compareRelativePathsBytewise, joinRelativePath, validateRelativePath, } from './file-tree-path.js';
|
|
11
|
+
export { gitWorktreeBackend } from './git-worktree-backend.js';
|
|
3
12
|
export { TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, TRANSACTIONAL_OBSERVED_RISK, } from './reasons.js';
|
|
4
13
|
export { runTransactionalExecution } from './runner.js';
|
|
14
|
+
export { hashDirectoryNode, hashFileContent, readSnapshotNode, snapshotNodesEqual, } from './snapshot-node.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const TRANSACTIONAL_ALREADY_APPLIED = "transactional_already_applied";
|
|
2
2
|
export declare const TRANSACTIONAL_OBSERVED_RISK = "transactional_observed_risk";
|
|
3
3
|
export declare const TRANSACTIONAL_APPLY_FAILED = "transactional_apply_failed";
|
|
4
|
+
export { RECOVERY_DIRTY_WORKTREE, RECOVERY_EXECUTION_FAILED, RECOVERY_SUBSTRATE_UNAVAILABLE, } from '../recovery/fail-closed.js';
|
|
4
5
|
export declare const TRANSACTIONAL_APPROVAL_BYPASS_REASONS: Set<string>;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
export const TRANSACTIONAL_ALREADY_APPLIED = 'transactional_already_applied';
|
|
2
2
|
export const TRANSACTIONAL_OBSERVED_RISK = 'transactional_observed_risk';
|
|
3
3
|
export const TRANSACTIONAL_APPLY_FAILED = 'transactional_apply_failed';
|
|
4
|
+
export { RECOVERY_DIRTY_WORKTREE, RECOVERY_EXECUTION_FAILED, RECOVERY_SUBSTRATE_UNAVAILABLE, } from '../recovery/fail-closed.js';
|
|
4
5
|
export const TRANSACTIONAL_APPROVAL_BYPASS_REASONS = new Set([
|
|
5
6
|
TRANSACTIONAL_OBSERVED_RISK,
|
|
6
7
|
TRANSACTIONAL_ALREADY_APPLIED,
|
|
7
8
|
TRANSACTIONAL_APPLY_FAILED,
|
|
9
|
+
'recovery_substrate_unavailable',
|
|
10
|
+
'recovery_execution_failed',
|
|
11
|
+
'recovery_dirty_worktree',
|
|
12
|
+
'file_checkpoint_isolation_unavailable',
|
|
8
13
|
]);
|
|
@@ -1,31 +1,91 @@
|
|
|
1
|
+
import { boundaryMountReadOnlyFromPrediction, runWithBoundaryRunnable, } from '../capability/boundary-run.js';
|
|
2
|
+
import { hostIntegrationBoundaryContext } from '../capability/boundary-session.js';
|
|
3
|
+
import { resolveGuestWorkdir } from '../capability/boundary-workspace-mount.js';
|
|
4
|
+
import { discardPreparedRecoveryCheckpoint, listRecoveryCheckpoints, markRecoveryCheckpointApplied, markRecoveryCheckpointApplying, prepareRecoveryCheckpoint, reconcileRecoveryCheckpoint, } from '../recovery/checkpoint.js';
|
|
5
|
+
import { buildObservedChangesFromTransactional } from './apply-observed-changes.js';
|
|
6
|
+
import { selectTransactionalBackend } from './backend-selector.js';
|
|
1
7
|
import { evaluateTransactionalDiff } from './diff-evaluator.js';
|
|
2
|
-
import {
|
|
8
|
+
import { FileCheckpointDiagnosticError } from './file-tree.js';
|
|
9
|
+
import { applyWorktreeChanges, resolveWorktreeCwd, TRANSACTIONAL_APPLY_ROLLBACK_FAILED, TRANSACTIONAL_APPLY_TOCTOU, } from './git-worktree.js';
|
|
3
10
|
import { TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPLY_FAILED, TRANSACTIONAL_OBSERVED_RISK, } from './reasons.js';
|
|
4
11
|
export async function runTransactionalExecution(params) {
|
|
5
12
|
const { predicted, repoRoot, stateDir, command, cwd, timeoutMs, diffContext } = params;
|
|
6
|
-
|
|
13
|
+
const backendContext = {
|
|
14
|
+
repoRoot,
|
|
15
|
+
stateDir,
|
|
16
|
+
cwd,
|
|
17
|
+
dirtyIgnoreRoots: params.dirtyIgnoreRoots,
|
|
18
|
+
fileCheckpoint: params.fileCheckpoint,
|
|
19
|
+
durableCheckpointEnabled: params.checkpoint?.enabled === true,
|
|
20
|
+
boundaryAttestation: params.boundaryContext?.attestation ?? null,
|
|
21
|
+
boundaryAttestationFresh: params.boundaryContext?.attestationFresh ?? false,
|
|
22
|
+
boundaryDriverId: params.boundaryContext?.driverId,
|
|
23
|
+
};
|
|
24
|
+
const selection = await selectTransactionalBackend(backendContext);
|
|
25
|
+
if (!selection.backend) {
|
|
26
|
+
const skipReason = params.fileCheckpoint.enabled
|
|
27
|
+
? (selection.probe.reason ?? selection.skipReason)
|
|
28
|
+
: (selection.skipReason ?? selection.probe.reason);
|
|
7
29
|
return {
|
|
8
30
|
ok: false,
|
|
9
31
|
skipped: true,
|
|
10
|
-
skipReason: '
|
|
32
|
+
skipReason: skipReason ?? 'transactional_execution_failed',
|
|
11
33
|
predicted,
|
|
12
34
|
result: predicted,
|
|
13
35
|
};
|
|
14
36
|
}
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
if (params.checkpoint?.enabled) {
|
|
38
|
+
try {
|
|
39
|
+
await listRecoveryCheckpoints(stateDir, repoRoot);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
skipped: true,
|
|
45
|
+
skipReason: error instanceof Error ? error.message : 'recovery_checkpoint_reconcile_failed',
|
|
46
|
+
predicted,
|
|
47
|
+
result: predicted,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
23
50
|
}
|
|
24
51
|
let snapshot = null;
|
|
25
52
|
try {
|
|
26
|
-
snapshot = await
|
|
27
|
-
const
|
|
28
|
-
|
|
53
|
+
snapshot = await selection.backend.prepare(backendContext);
|
|
54
|
+
const snapshotAudit = {
|
|
55
|
+
transactionalBackend: snapshot.backend,
|
|
56
|
+
resourceKind: snapshot.resourceKind,
|
|
57
|
+
baselineTreeHash: snapshot.baselineTreeHash,
|
|
58
|
+
snapshotFileCount: snapshot.snapshotFileCount,
|
|
59
|
+
snapshotSourceBytes: snapshot.snapshotSourceBytes,
|
|
60
|
+
snapshotWorkspaceBytes: snapshot.snapshotWorkspaceBytes,
|
|
61
|
+
snapshotCopyStrategy: snapshot.copyStrategy,
|
|
62
|
+
snapshotPrepareMs: snapshot.snapshotPrepareMs,
|
|
63
|
+
};
|
|
64
|
+
const runOptions = snapshot.backend === 'file_checkpoint' && snapshot.executionCwdRelative !== undefined
|
|
65
|
+
? {
|
|
66
|
+
mountReadOnly: boundaryMountReadOnlyFromPrediction(predicted),
|
|
67
|
+
workspaceMount: {
|
|
68
|
+
hostSourceRoot: snapshot.executionRoot,
|
|
69
|
+
guestTargetRoot: snapshot.resourceRoot,
|
|
70
|
+
cwdRelative: snapshot.executionCwdRelative,
|
|
71
|
+
writable: true,
|
|
72
|
+
hideHostSourcePath: true,
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
: {
|
|
76
|
+
mountReadOnly: boundaryMountReadOnlyFromPrediction(predicted),
|
|
77
|
+
};
|
|
78
|
+
const execCwd = snapshot.backend === 'file_checkpoint' && runOptions.workspaceMount
|
|
79
|
+
? resolveGuestWorkdir(runOptions.workspaceMount)
|
|
80
|
+
: resolveWorktreeCwd(repoRoot, snapshot.executionRoot, cwd);
|
|
81
|
+
const boundaryContext = params.boundaryContext ?? hostIntegrationBoundaryContext(params.repoRoot);
|
|
82
|
+
const shellResult = await runWithBoundaryRunnable(boundaryContext.driver, {
|
|
83
|
+
prepareContext: boundaryContext.prepareContext,
|
|
84
|
+
command,
|
|
85
|
+
cwd: execCwd,
|
|
86
|
+
timeoutMs,
|
|
87
|
+
runOptions,
|
|
88
|
+
});
|
|
29
89
|
if (shellResult.timedOut) {
|
|
30
90
|
return {
|
|
31
91
|
ok: false,
|
|
@@ -33,6 +93,7 @@ export async function runTransactionalExecution(params) {
|
|
|
33
93
|
skipReason: 'transactional_timed_out',
|
|
34
94
|
predicted,
|
|
35
95
|
result: predicted,
|
|
96
|
+
...snapshotAudit,
|
|
36
97
|
commandExitCode: shellResult.exitCode,
|
|
37
98
|
commandSignal: shellResult.signal,
|
|
38
99
|
timedOut: true,
|
|
@@ -45,17 +106,77 @@ export async function runTransactionalExecution(params) {
|
|
|
45
106
|
skipReason: 'transactional_command_failed',
|
|
46
107
|
predicted,
|
|
47
108
|
result: predicted,
|
|
109
|
+
...snapshotAudit,
|
|
48
110
|
commandExitCode: shellResult.exitCode,
|
|
49
111
|
commandSignal: shellResult.signal,
|
|
50
112
|
};
|
|
51
113
|
}
|
|
52
|
-
const changes = await
|
|
114
|
+
const changes = await snapshot.collectChanges();
|
|
53
115
|
const observed = evaluateTransactionalDiff(changes, diffContext);
|
|
54
116
|
if (observed.verdict === 'allow') {
|
|
117
|
+
await snapshot.validateSourceState?.();
|
|
118
|
+
const observedChanges = await buildObservedChangesFromTransactional(snapshot.baselineRoot ?? repoRoot, snapshot.executionRoot, changes);
|
|
119
|
+
const checkpoint = params.checkpoint?.enabled && changes.length > 0
|
|
120
|
+
? await prepareRecoveryCheckpoint({
|
|
121
|
+
stateDir,
|
|
122
|
+
repoRoot,
|
|
123
|
+
baselinePath: snapshot.baselineRoot ?? repoRoot,
|
|
124
|
+
worktreePath: snapshot.executionRoot,
|
|
125
|
+
commandFingerprint: predicted.fingerprint,
|
|
126
|
+
changes,
|
|
127
|
+
protectedRoots: diffContext.protectedRoots,
|
|
128
|
+
config: params.checkpoint,
|
|
129
|
+
backend: snapshot.backend,
|
|
130
|
+
})
|
|
131
|
+
: null;
|
|
55
132
|
try {
|
|
56
|
-
|
|
133
|
+
if (checkpoint) {
|
|
134
|
+
await markRecoveryCheckpointApplying(stateDir, checkpoint);
|
|
135
|
+
}
|
|
136
|
+
let receipt;
|
|
137
|
+
await applyWorktreeChanges(snapshot.executionRoot, repoRoot, changes, {
|
|
138
|
+
observedChanges,
|
|
139
|
+
afterApply: checkpoint
|
|
140
|
+
? async () => {
|
|
141
|
+
receipt = await markRecoveryCheckpointApplied(stateDir, checkpoint);
|
|
142
|
+
}
|
|
143
|
+
: undefined,
|
|
144
|
+
});
|
|
145
|
+
const result = {
|
|
146
|
+
...predicted,
|
|
147
|
+
verdict: 'allow',
|
|
148
|
+
reason: TRANSACTIONAL_ALREADY_APPLIED,
|
|
149
|
+
assessment: observed.assessment,
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
predicted,
|
|
154
|
+
observed,
|
|
155
|
+
result,
|
|
156
|
+
worktreePath: snapshot.executionRoot,
|
|
157
|
+
...snapshotAudit,
|
|
158
|
+
commandExitCode: shellResult.exitCode,
|
|
159
|
+
commandSignal: shellResult.signal,
|
|
160
|
+
timedOut: shellResult.timedOut,
|
|
161
|
+
...(checkpoint && receipt
|
|
162
|
+
? {
|
|
163
|
+
recoveryCheckpointId: checkpoint.checkpointId,
|
|
164
|
+
recoveryBackend: snapshot.backend,
|
|
165
|
+
recoveryProofHash: receipt.proofHash,
|
|
166
|
+
recoveryState: 'applied',
|
|
167
|
+
}
|
|
168
|
+
: {}),
|
|
169
|
+
};
|
|
57
170
|
}
|
|
58
|
-
catch {
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (checkpoint) {
|
|
173
|
+
const recoveryState = await reconcileRecoveryCheckpoint(stateDir, checkpoint.checkpointId);
|
|
174
|
+
if (recoveryState === 'prepared') {
|
|
175
|
+
await discardPreparedRecoveryCheckpoint(stateDir, checkpoint.checkpointId);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const toctou = error instanceof Error && error.message === TRANSACTIONAL_APPLY_TOCTOU;
|
|
179
|
+
const rollbackFailed = error instanceof Error && error.message === TRANSACTIONAL_APPLY_ROLLBACK_FAILED;
|
|
59
180
|
const result = {
|
|
60
181
|
...predicted,
|
|
61
182
|
verdict: 'deny_pending_approval',
|
|
@@ -64,7 +185,14 @@ export async function runTransactionalExecution(params) {
|
|
|
64
185
|
...observed.assessment,
|
|
65
186
|
reversibility: 'irreversible',
|
|
66
187
|
confidence: 1,
|
|
67
|
-
signals: [
|
|
188
|
+
signals: [
|
|
189
|
+
...observed.assessment.signals,
|
|
190
|
+
rollbackFailed
|
|
191
|
+
? 'transactional_apply_rollback_failed'
|
|
192
|
+
: toctou
|
|
193
|
+
? 'transactional_apply_toctou'
|
|
194
|
+
: 'transactional_apply_failed',
|
|
195
|
+
],
|
|
68
196
|
},
|
|
69
197
|
};
|
|
70
198
|
return {
|
|
@@ -72,7 +200,8 @@ export async function runTransactionalExecution(params) {
|
|
|
72
200
|
predicted,
|
|
73
201
|
observed,
|
|
74
202
|
result,
|
|
75
|
-
worktreePath: snapshot.
|
|
203
|
+
worktreePath: snapshot.executionRoot,
|
|
204
|
+
...snapshotAudit,
|
|
76
205
|
commandExitCode: shellResult.exitCode,
|
|
77
206
|
commandSignal: shellResult.signal,
|
|
78
207
|
timedOut: shellResult.timedOut,
|
|
@@ -81,8 +210,8 @@ export async function runTransactionalExecution(params) {
|
|
|
81
210
|
}
|
|
82
211
|
const result = {
|
|
83
212
|
...predicted,
|
|
84
|
-
verdict:
|
|
85
|
-
reason:
|
|
213
|
+
verdict: 'deny_pending_approval',
|
|
214
|
+
reason: TRANSACTIONAL_OBSERVED_RISK,
|
|
86
215
|
assessment: observed.assessment,
|
|
87
216
|
};
|
|
88
217
|
return {
|
|
@@ -90,7 +219,8 @@ export async function runTransactionalExecution(params) {
|
|
|
90
219
|
predicted,
|
|
91
220
|
observed,
|
|
92
221
|
result,
|
|
93
|
-
worktreePath: snapshot.
|
|
222
|
+
worktreePath: snapshot.executionRoot,
|
|
223
|
+
...snapshotAudit,
|
|
94
224
|
commandExitCode: shellResult.exitCode,
|
|
95
225
|
commandSignal: shellResult.signal,
|
|
96
226
|
timedOut: shellResult.timedOut,
|
|
@@ -101,13 +231,31 @@ export async function runTransactionalExecution(params) {
|
|
|
101
231
|
ok: false,
|
|
102
232
|
skipped: true,
|
|
103
233
|
skipReason: error instanceof Error ? error.message : 'transactional_execution_failed',
|
|
234
|
+
...(error instanceof FileCheckpointDiagnosticError ? { skipDetail: error.diagnostic } : {}),
|
|
104
235
|
predicted,
|
|
105
236
|
result: predicted,
|
|
237
|
+
...(snapshot
|
|
238
|
+
? {
|
|
239
|
+
transactionalBackend: snapshot.backend,
|
|
240
|
+
resourceKind: snapshot.resourceKind,
|
|
241
|
+
baselineTreeHash: snapshot.baselineTreeHash,
|
|
242
|
+
snapshotFileCount: snapshot.snapshotFileCount,
|
|
243
|
+
snapshotSourceBytes: snapshot.snapshotSourceBytes,
|
|
244
|
+
snapshotWorkspaceBytes: snapshot.snapshotWorkspaceBytes,
|
|
245
|
+
snapshotCopyStrategy: snapshot.copyStrategy,
|
|
246
|
+
snapshotPrepareMs: snapshot.snapshotPrepareMs,
|
|
247
|
+
}
|
|
248
|
+
: {}),
|
|
106
249
|
};
|
|
107
250
|
}
|
|
108
251
|
finally {
|
|
109
252
|
if (snapshot) {
|
|
110
|
-
|
|
253
|
+
try {
|
|
254
|
+
await snapshot.cleanup();
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Cleanup is best effort and must not overturn a verified apply or observed-risk result.
|
|
258
|
+
}
|
|
111
259
|
}
|
|
112
260
|
}
|
|
113
261
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type SnapshotNode = {
|
|
2
|
+
kind: 'absent';
|
|
3
|
+
} | {
|
|
4
|
+
kind: 'file';
|
|
5
|
+
mode: number;
|
|
6
|
+
size: number;
|
|
7
|
+
hash: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'symlink';
|
|
10
|
+
target: string;
|
|
11
|
+
hash: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'directory';
|
|
14
|
+
mode: number;
|
|
15
|
+
hash: string;
|
|
16
|
+
};
|
|
17
|
+
export type PresentSnapshotNode = Exclude<SnapshotNode, {
|
|
18
|
+
kind: 'absent';
|
|
19
|
+
}>;
|
|
20
|
+
export declare function hashFileContent(filePath: string): Promise<string>;
|
|
21
|
+
export declare function hashSymlinkTarget(target: string): string;
|
|
22
|
+
export declare function hashDirectoryNode(mode: number): string;
|
|
23
|
+
export declare function hashFileNode(mode: number, contentHash: string): string;
|
|
24
|
+
export declare function snapshotNodesEqual(left: SnapshotNode, right: SnapshotNode): boolean;
|
|
25
|
+
export declare function readSnapshotNode(absolutePath: string): Promise<SnapshotNode>;
|
|
26
|
+
export declare function presentNodeKind(node: PresentSnapshotNode): 'file' | 'symlink' | 'directory';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { lstat, readlink } from 'node:fs/promises';
|
|
4
|
+
export async function hashFileContent(filePath) {
|
|
5
|
+
const hash = createHash('sha256');
|
|
6
|
+
await new Promise((resolve, reject) => {
|
|
7
|
+
const stream = createReadStream(filePath);
|
|
8
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
9
|
+
stream.on('error', reject);
|
|
10
|
+
stream.on('end', () => resolve());
|
|
11
|
+
});
|
|
12
|
+
return hash.digest('hex');
|
|
13
|
+
}
|
|
14
|
+
export function hashSymlinkTarget(target) {
|
|
15
|
+
return createHash('sha256').update(`symlink:${target}`).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
export function hashDirectoryNode(mode) {
|
|
18
|
+
return createHash('sha256')
|
|
19
|
+
.update(`directory:${mode & 0o777}`)
|
|
20
|
+
.digest('hex');
|
|
21
|
+
}
|
|
22
|
+
export function hashFileNode(mode, contentHash) {
|
|
23
|
+
return createHash('sha256')
|
|
24
|
+
.update(`file:${mode & 0o777}:${contentHash}`)
|
|
25
|
+
.digest('hex');
|
|
26
|
+
}
|
|
27
|
+
export function snapshotNodesEqual(left, right) {
|
|
28
|
+
if (left.kind === 'absent' || right.kind === 'absent') {
|
|
29
|
+
return left.kind === right.kind;
|
|
30
|
+
}
|
|
31
|
+
if (left.kind !== right.kind) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
switch (left.kind) {
|
|
35
|
+
case 'file':
|
|
36
|
+
return (right.kind === 'file' &&
|
|
37
|
+
left.mode === right.mode &&
|
|
38
|
+
left.size === right.size &&
|
|
39
|
+
left.hash === right.hash);
|
|
40
|
+
case 'symlink':
|
|
41
|
+
return right.kind === 'symlink' && left.target === right.target && left.hash === right.hash;
|
|
42
|
+
case 'directory':
|
|
43
|
+
return right.kind === 'directory' && left.mode === right.mode && left.hash === right.hash;
|
|
44
|
+
default:
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function readSnapshotNode(absolutePath) {
|
|
49
|
+
let info;
|
|
50
|
+
try {
|
|
51
|
+
info = await lstat(absolutePath);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (['ENOENT', 'ENOTDIR'].includes(error.code ?? '')) {
|
|
55
|
+
return { kind: 'absent' };
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
if (info.isSymbolicLink()) {
|
|
60
|
+
const target = await readlink(absolutePath);
|
|
61
|
+
return {
|
|
62
|
+
kind: 'symlink',
|
|
63
|
+
target,
|
|
64
|
+
hash: hashSymlinkTarget(target),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (info.isFile()) {
|
|
68
|
+
const contentHash = await hashFileContent(absolutePath);
|
|
69
|
+
return {
|
|
70
|
+
kind: 'file',
|
|
71
|
+
mode: info.mode,
|
|
72
|
+
size: info.size,
|
|
73
|
+
hash: hashFileNode(info.mode, contentHash),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (info.isDirectory()) {
|
|
77
|
+
return {
|
|
78
|
+
kind: 'directory',
|
|
79
|
+
mode: info.mode,
|
|
80
|
+
hash: hashDirectoryNode(info.mode),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
throw new Error('file_checkpoint_unsupported_node');
|
|
84
|
+
}
|
|
85
|
+
export function presentNodeKind(node) {
|
|
86
|
+
return node.kind;
|
|
87
|
+
}
|