@guilz-dev/belay 0.8.0 → 0.9.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 +60 -10
- package/dist/adapters/shared/gate-runtime.d.ts +5 -0
- package/dist/adapters/shared/gate-runtime.js +237 -33
- package/dist/bundle/claude-runtime.mjs +4082 -1230
- package/dist/bundle/codex-runtime.mjs +4057 -1205
- package/dist/bundle/cursor-runtime.mjs +4057 -1205
- package/dist/commands/classify-for-report.js +5 -0
- package/dist/commands/doctor.js +90 -27
- package/dist/commands/explain.js +3 -2
- package/dist/commands/metrics.js +75 -4
- package/dist/commands/quality.js +2 -6
- package/dist/commands/recovery-checkpoints.d.ts +8 -1
- package/dist/commands/recovery-checkpoints.js +44 -11
- package/dist/commands/session.d.ts +5 -0
- package/dist/commands/session.js +8 -1
- package/dist/commands/status.js +1 -1
- package/dist/config-io.js +2 -2
- package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
- package/dist/conformance/contained-execution-guarantee.js +121 -0
- package/dist/conformance/guarantee-table.js +7 -0
- package/dist/core/audit-io.js +8 -0
- package/dist/core/audit-metrics.d.ts +34 -0
- package/dist/core/audit-metrics.js +87 -15
- package/dist/core/audit-recovery-metrics.d.ts +33 -0
- package/dist/core/audit-recovery-metrics.js +146 -0
- package/dist/core/audit-types.d.ts +7 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/bounded-output.d.ts +11 -0
- package/dist/core/bounded-output.js +26 -0
- package/dist/core/capability/attestation.d.ts +56 -0
- package/dist/core/capability/attestation.js +110 -3
- package/dist/core/capability/boundary-session.d.ts +5 -0
- package/dist/core/capability/boundary-session.js +38 -1
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/index.js +1 -1
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +89 -10
- package/dist/core/contained-execution/docker-policy.d.ts +18 -0
- package/dist/core/contained-execution/docker-policy.js +97 -0
- package/dist/core/contained-execution/docker.d.ts +223 -0
- package/dist/core/contained-execution/docker.js +846 -0
- package/dist/core/contained-execution/eligibility.d.ts +9 -0
- package/dist/core/contained-execution/eligibility.js +103 -0
- package/dist/core/contained-execution/failure.d.ts +18 -0
- package/dist/core/contained-execution/failure.js +105 -0
- package/dist/core/contained-execution/mirror.d.ts +54 -0
- package/dist/core/contained-execution/mirror.js +587 -0
- package/dist/core/contained-execution/policy.d.ts +10 -0
- package/dist/core/contained-execution/policy.js +20 -0
- package/dist/core/effect-ir/index.d.ts +1 -1
- package/dist/core/effect-ir/index.js +1 -1
- package/dist/core/effect-ir/normalize.d.ts +5 -0
- package/dist/core/effect-ir/normalize.js +17 -7
- package/dist/core/effect-ir/shell-build.js +2 -14
- package/dist/core/effect-ir/shell-lower.js +100 -25
- package/dist/core/gate-contract.d.ts +4 -1
- package/dist/core/gate-contract.js +2 -0
- package/dist/core/process-runner.d.ts +10 -3
- package/dist/core/process-runner.js +66 -10
- package/dist/core/recovery/checkpoint.d.ts +3 -0
- package/dist/core/recovery/checkpoint.js +33 -4
- package/dist/core/recovery/types.d.ts +1 -0
- package/dist/core/scrub.d.ts +17 -0
- package/dist/core/scrub.js +499 -5
- package/dist/core/standing-allow.d.ts +0 -20
- package/dist/core/standing-allow.js +0 -71
- package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
- package/dist/core/transactional/apply-observed-changes.js +11 -0
- package/dist/core/transactional/backend-selector.d.ts +2 -1
- package/dist/core/transactional/backend-selector.js +32 -29
- package/dist/core/transactional/backend.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-backend.js +242 -54
- package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-git.js +53 -0
- package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
- package/dist/core/transactional/git-worktree-backend.js +3 -0
- package/dist/core/transactional/git-worktree.d.ts +1 -0
- package/dist/core/transactional/git-worktree.js +1 -0
- package/dist/core/transactional/runner.js +59 -13
- package/dist/core/types.d.ts +13 -0
- package/dist/core/verdict/parser.d.ts +10 -0
- package/dist/core/verdict/parser.js +271 -40
- package/dist/core/verdict/shell-semantics.js +21 -0
- package/dist/corpus/adversarial-probe.d.ts +1 -1
- package/dist/corpus/adversarial-probe.js +3 -3
- package/dist/corpus/benign-probe-cores.d.ts +6 -0
- package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
- package/dist/corpus/coverage-compare.d.ts +36 -0
- package/dist/corpus/coverage-compare.js +197 -0
- package/dist/corpus/coverage-contexts.d.ts +17 -0
- package/dist/corpus/coverage-contexts.js +88 -0
- package/dist/corpus/coverage-matrix.d.ts +37 -0
- package/dist/corpus/coverage-matrix.js +159 -0
- package/dist/corpus/coverage-probe.d.ts +91 -0
- package/dist/corpus/coverage-probe.js +341 -0
- package/dist/corpus/evaluate.d.ts +1 -1
- package/dist/corpus/evaluate.js +1 -1
- package/dist/corpus/structural-fixture-root.d.ts +2 -0
- package/dist/corpus/structural-fixture-root.js +5 -0
- package/dist/operational-insights.d.ts +3 -0
- package/dist/operational-insights.js +7 -3
- package/dist/runtime-provenance.d.ts +8 -0
- package/dist/runtime-provenance.js +34 -0
- package/dist/types.d.ts +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -1
- package/skills/belay/SKILL.md +19 -0
- package/skills/belay/belay-status.md +5 -0
- package/dist/corpus/must-allow-commands.d.ts +0 -6
|
@@ -2,7 +2,8 @@ import type { TransactionalBackendContext, TransactionalBackendProbe, Transactio
|
|
|
2
2
|
export declare const FILE_CHECKPOINT_DISABLED = "file_checkpoint_disabled";
|
|
3
3
|
export declare const FILE_CHECKPOINT_NON_GIT_DISABLED = "file_checkpoint_non_git_disabled";
|
|
4
4
|
export declare const FILE_CHECKPOINT_DURABLE_REQUIRED = "file_checkpoint_durable_checkpoint_required";
|
|
5
|
-
export
|
|
5
|
+
export { FILE_CHECKPOINT_ISOLATION_UNAVAILABLE } from './file-checkpoint-isolation.js';
|
|
6
6
|
export declare const FILE_CHECKPOINT_NOT_IMPLEMENTED = "file_checkpoint_not_implemented";
|
|
7
7
|
export declare function probeTransactionalBackends(context: TransactionalBackendContext): Promise<TransactionalBackendProbe[]>;
|
|
8
|
+
export declare function probeFileCheckpointBackend(context: TransactionalBackendContext): Promise<TransactionalBackendProbe>;
|
|
8
9
|
export declare function selectTransactionalBackend(context: TransactionalBackendContext): Promise<TransactionalBackendSelection>;
|
|
@@ -1,35 +1,21 @@
|
|
|
1
|
-
import { attestsWorkspaceMountIsolation } from '../capability/attestation.js';
|
|
2
1
|
import { fileCheckpointBackend } from './file-checkpoint-backend.js';
|
|
2
|
+
import { fileCheckpointIsolationReason } from './file-checkpoint-isolation.js';
|
|
3
3
|
import { isDirtyWorktree, isGitWorktreeAvailable } from './git-worktree.js';
|
|
4
4
|
import { gitWorktreeBackend } from './git-worktree-backend.js';
|
|
5
5
|
export const FILE_CHECKPOINT_DISABLED = 'file_checkpoint_disabled';
|
|
6
6
|
export const FILE_CHECKPOINT_NON_GIT_DISABLED = 'file_checkpoint_non_git_disabled';
|
|
7
7
|
export const FILE_CHECKPOINT_DURABLE_REQUIRED = 'file_checkpoint_durable_checkpoint_required';
|
|
8
|
-
export
|
|
8
|
+
export { FILE_CHECKPOINT_ISOLATION_UNAVAILABLE } from './file-checkpoint-isolation.js';
|
|
9
9
|
export const FILE_CHECKPOINT_NOT_IMPLEMENTED = 'file_checkpoint_not_implemented';
|
|
10
|
-
function fileCheckpointProbe(reason, signals) {
|
|
10
|
+
function fileCheckpointProbe(reason, signals, resourceKind) {
|
|
11
11
|
return {
|
|
12
12
|
eligible: false,
|
|
13
13
|
backend: 'file_checkpoint',
|
|
14
|
+
resourceKind,
|
|
14
15
|
reason,
|
|
15
16
|
signals,
|
|
16
17
|
};
|
|
17
18
|
}
|
|
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
19
|
export async function probeTransactionalBackends(context) {
|
|
34
20
|
const probes = [await gitWorktreeBackend.probe(context)];
|
|
35
21
|
if (await isGitWorktreeAvailable(context.repoRoot)) {
|
|
@@ -40,6 +26,13 @@ export async function probeTransactionalBackends(context) {
|
|
|
40
26
|
}
|
|
41
27
|
return probes;
|
|
42
28
|
}
|
|
29
|
+
export async function probeFileCheckpointBackend(context) {
|
|
30
|
+
const probes = await probeTransactionalBackends(context);
|
|
31
|
+
const probe = probes.find((candidate) => candidate.backend === 'file_checkpoint');
|
|
32
|
+
if (!probe)
|
|
33
|
+
throw new Error(FILE_CHECKPOINT_NOT_IMPLEMENTED);
|
|
34
|
+
return probe;
|
|
35
|
+
}
|
|
43
36
|
async function gitWorkspaceSignals(context) {
|
|
44
37
|
const signals = ['git_repository'];
|
|
45
38
|
if (await isDirtyWorktree(context.repoRoot, { ignoreRoots: context.dirtyIgnoreRoots })) {
|
|
@@ -50,14 +43,14 @@ async function gitWorkspaceSignals(context) {
|
|
|
50
43
|
async function probeFileCheckpointForGit(context) {
|
|
51
44
|
const signals = await gitWorkspaceSignals(context);
|
|
52
45
|
if (!context.fileCheckpoint.enabled) {
|
|
53
|
-
return fileCheckpointProbe(FILE_CHECKPOINT_DISABLED, signals);
|
|
46
|
+
return fileCheckpointProbe(FILE_CHECKPOINT_DISABLED, signals, 'git_repository');
|
|
54
47
|
}
|
|
55
48
|
if (!context.durableCheckpointEnabled) {
|
|
56
|
-
return fileCheckpointProbe(FILE_CHECKPOINT_DURABLE_REQUIRED, signals);
|
|
49
|
+
return fileCheckpointProbe(FILE_CHECKPOINT_DURABLE_REQUIRED, signals, 'git_repository');
|
|
57
50
|
}
|
|
58
51
|
const isolationReason = fileCheckpointIsolationReason(context);
|
|
59
52
|
if (isolationReason) {
|
|
60
|
-
return fileCheckpointProbe(isolationReason, [...signals, 'isolation_unavailable']);
|
|
53
|
+
return fileCheckpointProbe(isolationReason, [...signals, 'isolation_unavailable'], 'git_repository');
|
|
61
54
|
}
|
|
62
55
|
const backendProbe = await fileCheckpointBackend.probe(context);
|
|
63
56
|
if (backendProbe.eligible) {
|
|
@@ -66,26 +59,30 @@ async function probeFileCheckpointForGit(context) {
|
|
|
66
59
|
return fileCheckpointProbe(backendProbe.reason ?? FILE_CHECKPOINT_NOT_IMPLEMENTED, [
|
|
67
60
|
...signals,
|
|
68
61
|
...(backendProbe.reason === FILE_CHECKPOINT_NOT_IMPLEMENTED ? ['not_implemented'] : []),
|
|
69
|
-
]);
|
|
62
|
+
], 'git_repository');
|
|
70
63
|
}
|
|
71
64
|
async function probeFileCheckpointForNonGit(context) {
|
|
72
65
|
if (!context.fileCheckpoint.enabled) {
|
|
73
|
-
return fileCheckpointProbe(FILE_CHECKPOINT_DISABLED, ['non_git_workspace']);
|
|
66
|
+
return fileCheckpointProbe(FILE_CHECKPOINT_DISABLED, ['non_git_workspace'], 'directory');
|
|
74
67
|
}
|
|
75
68
|
if (!context.fileCheckpoint.allowNonGit) {
|
|
76
|
-
return fileCheckpointProbe(FILE_CHECKPOINT_NON_GIT_DISABLED, ['non_git_workspace']);
|
|
69
|
+
return fileCheckpointProbe(FILE_CHECKPOINT_NON_GIT_DISABLED, ['non_git_workspace'], 'directory');
|
|
77
70
|
}
|
|
78
71
|
if (!context.durableCheckpointEnabled) {
|
|
79
|
-
return fileCheckpointProbe(FILE_CHECKPOINT_DURABLE_REQUIRED, ['non_git_workspace']);
|
|
72
|
+
return fileCheckpointProbe(FILE_CHECKPOINT_DURABLE_REQUIRED, ['non_git_workspace'], 'directory');
|
|
80
73
|
}
|
|
81
74
|
const isolationReason = fileCheckpointIsolationReason(context);
|
|
82
75
|
if (isolationReason) {
|
|
83
|
-
return fileCheckpointProbe(isolationReason, ['non_git_workspace', 'isolation_unavailable']);
|
|
76
|
+
return fileCheckpointProbe(isolationReason, ['non_git_workspace', 'isolation_unavailable'], 'directory');
|
|
84
77
|
}
|
|
85
|
-
|
|
78
|
+
const backendProbe = await fileCheckpointBackend.probe(context);
|
|
79
|
+
if (backendProbe.eligible) {
|
|
80
|
+
return backendProbe;
|
|
81
|
+
}
|
|
82
|
+
return fileCheckpointProbe(backendProbe.reason ?? FILE_CHECKPOINT_NOT_IMPLEMENTED, [
|
|
86
83
|
'non_git_workspace',
|
|
87
|
-
'not_implemented',
|
|
88
|
-
]);
|
|
84
|
+
...(backendProbe.reason === FILE_CHECKPOINT_NOT_IMPLEMENTED ? ['not_implemented'] : []),
|
|
85
|
+
], 'directory');
|
|
89
86
|
}
|
|
90
87
|
export async function selectTransactionalBackend(context) {
|
|
91
88
|
const gitProbe = await gitWorktreeBackend.probe(context);
|
|
@@ -110,6 +107,12 @@ export async function selectTransactionalBackend(context) {
|
|
|
110
107
|
};
|
|
111
108
|
}
|
|
112
109
|
const fileProbe = await probeFileCheckpointForNonGit(context);
|
|
110
|
+
if (fileProbe.eligible) {
|
|
111
|
+
return {
|
|
112
|
+
backend: fileCheckpointBackend,
|
|
113
|
+
probe: fileProbe,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
113
116
|
return {
|
|
114
117
|
backend: null,
|
|
115
118
|
probe: fileProbe,
|
|
@@ -5,6 +5,7 @@ export type TransactionalBackendId = 'git_worktree' | 'file_checkpoint';
|
|
|
5
5
|
export interface TransactionalBackendProbe {
|
|
6
6
|
eligible: boolean;
|
|
7
7
|
backend: TransactionalBackendId;
|
|
8
|
+
resourceKind: 'git_repository' | 'directory';
|
|
8
9
|
reason?: string;
|
|
9
10
|
signals: string[];
|
|
10
11
|
}
|
|
@@ -26,6 +27,8 @@ export interface TransactionalSnapshot {
|
|
|
26
27
|
executionCwdRelative?: string;
|
|
27
28
|
/** Revalidates the source tree and metadata immediately before apply. */
|
|
28
29
|
validateSourceState?(): Promise<void>;
|
|
30
|
+
/** Revalidates resource identity immediately before each real mutation. */
|
|
31
|
+
validateResourceIdentity?(): Promise<void>;
|
|
29
32
|
collectChanges(): Promise<TransactionalFileChange[]>;
|
|
30
33
|
cleanup(): Promise<void>;
|
|
31
34
|
}
|
|
@@ -1,37 +1,23 @@
|
|
|
1
|
-
import { cp, lstat, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { cp, lstat, mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { attestsWorkspaceMountIsolation } from '../capability/attestation.js';
|
|
5
4
|
import { currentRecoveryResourceIdentity } from '../recovery/resource-identity.js';
|
|
6
|
-
import { FILE_CHECKPOINT_DISABLED, FILE_CHECKPOINT_DURABLE_REQUIRED,
|
|
7
|
-
import { cloneBareWorktreeCopy, computeGitMetadataFingerprint, copyGitIndexState, execGit,
|
|
5
|
+
import { FILE_CHECKPOINT_DISABLED, FILE_CHECKPOINT_DURABLE_REQUIRED, FILE_CHECKPOINT_NON_GIT_DISABLED, } from './backend-selector.js';
|
|
6
|
+
import { assertRootGitMetadataUnchanged, cloneBareWorktreeCopy, computeGitMetadataFingerprint, copyGitIndexState, execGit, FILE_CHECKPOINT_GIT_METADATA_CHANGED, FILE_CHECKPOINT_PREPARE_FAILED, FILE_CHECKPOINT_SOURCE_CHANGED, removeAllGitRemotes, resolveExecutionCwdRelative, rethrowStableFileCheckpointError, } from './file-checkpoint-git.js';
|
|
7
|
+
import { fileCheckpointIsolationReason } from './file-checkpoint-isolation.js';
|
|
8
8
|
import { removeDeadOwnerStaging, writeOwnerMarker } from './file-checkpoint-staging.js';
|
|
9
9
|
import { cloneDirectoryTree, probeFileCloneStrategy } from './file-clone.js';
|
|
10
10
|
import { buildFileTreeIndex, diffFileTreeIndices, FILE_CHECKPOINT_PREPARE_TIMEOUT, FILE_CHECKPOINT_QUOTA_EXCEEDED, FileCheckpointDiagnosticError, } from './file-tree.js';
|
|
11
11
|
import { isDirtyWorktree, isGitWorktreeAvailable } from './git-worktree.js';
|
|
12
12
|
import { readSnapshotNode } from './snapshot-node.js';
|
|
13
13
|
export const FILE_CHECKPOINT_PROTECTED_PATH_CHANGED = 'file_checkpoint_protected_path_changed';
|
|
14
|
-
function fileCheckpointIsolationReason(context) {
|
|
15
|
-
const attestation = context.boundaryAttestation;
|
|
16
|
-
const fresh = context.boundaryAttestationFresh === true;
|
|
17
|
-
const driverId = context.boundaryDriverId;
|
|
18
|
-
if (!attestation || !fresh || !driverId) {
|
|
19
|
-
return FILE_CHECKPOINT_ISOLATION_UNAVAILABLE;
|
|
20
|
-
}
|
|
21
|
-
if (attestation.driver !== driverId) {
|
|
22
|
-
return FILE_CHECKPOINT_ISOLATION_UNAVAILABLE;
|
|
23
|
-
}
|
|
24
|
-
if (!attestsWorkspaceMountIsolation(attestation)) {
|
|
25
|
-
return FILE_CHECKPOINT_ISOLATION_UNAVAILABLE;
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
14
|
async function probeDirtyGitFileCheckpoint(context) {
|
|
30
15
|
const signals = ['git_repository', 'dirty_git_worktree'];
|
|
31
16
|
if (!(await isGitWorktreeAvailable(context.repoRoot))) {
|
|
32
17
|
return {
|
|
33
18
|
eligible: false,
|
|
34
19
|
backend: 'file_checkpoint',
|
|
20
|
+
resourceKind: 'git_repository',
|
|
35
21
|
reason: 'git_worktree_unavailable',
|
|
36
22
|
signals: ['git_repository'],
|
|
37
23
|
};
|
|
@@ -40,6 +26,7 @@ async function probeDirtyGitFileCheckpoint(context) {
|
|
|
40
26
|
return {
|
|
41
27
|
eligible: false,
|
|
42
28
|
backend: 'file_checkpoint',
|
|
29
|
+
resourceKind: 'git_repository',
|
|
43
30
|
reason: 'clean_git_worktree',
|
|
44
31
|
signals: ['git_repository', 'clean_git_worktree'],
|
|
45
32
|
};
|
|
@@ -48,6 +35,7 @@ async function probeDirtyGitFileCheckpoint(context) {
|
|
|
48
35
|
return {
|
|
49
36
|
eligible: false,
|
|
50
37
|
backend: 'file_checkpoint',
|
|
38
|
+
resourceKind: 'git_repository',
|
|
51
39
|
reason: FILE_CHECKPOINT_DISABLED,
|
|
52
40
|
signals,
|
|
53
41
|
};
|
|
@@ -56,6 +44,7 @@ async function probeDirtyGitFileCheckpoint(context) {
|
|
|
56
44
|
return {
|
|
57
45
|
eligible: false,
|
|
58
46
|
backend: 'file_checkpoint',
|
|
47
|
+
resourceKind: 'git_repository',
|
|
59
48
|
reason: FILE_CHECKPOINT_DURABLE_REQUIRED,
|
|
60
49
|
signals,
|
|
61
50
|
};
|
|
@@ -65,6 +54,7 @@ async function probeDirtyGitFileCheckpoint(context) {
|
|
|
65
54
|
return {
|
|
66
55
|
eligible: false,
|
|
67
56
|
backend: 'file_checkpoint',
|
|
57
|
+
resourceKind: 'git_repository',
|
|
68
58
|
reason: isolationReason,
|
|
69
59
|
signals: [...signals, 'isolation_unavailable'],
|
|
70
60
|
};
|
|
@@ -73,9 +63,66 @@ async function probeDirtyGitFileCheckpoint(context) {
|
|
|
73
63
|
return {
|
|
74
64
|
eligible: true,
|
|
75
65
|
backend: 'file_checkpoint',
|
|
66
|
+
resourceKind: 'git_repository',
|
|
76
67
|
signals: [...signals, 'dirty_git_file_checkpoint', copyStrategy],
|
|
77
68
|
};
|
|
78
69
|
}
|
|
70
|
+
async function probeNonGitFileCheckpoint(context) {
|
|
71
|
+
const signals = ['non_git_workspace'];
|
|
72
|
+
if (await isGitWorktreeAvailable(context.repoRoot)) {
|
|
73
|
+
return {
|
|
74
|
+
eligible: false,
|
|
75
|
+
backend: 'file_checkpoint',
|
|
76
|
+
resourceKind: 'directory',
|
|
77
|
+
reason: 'git_worktree_available',
|
|
78
|
+
signals: ['git_repository'],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (!context.fileCheckpoint.enabled) {
|
|
82
|
+
return {
|
|
83
|
+
eligible: false,
|
|
84
|
+
backend: 'file_checkpoint',
|
|
85
|
+
resourceKind: 'directory',
|
|
86
|
+
reason: FILE_CHECKPOINT_DISABLED,
|
|
87
|
+
signals,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (!context.fileCheckpoint.allowNonGit) {
|
|
91
|
+
return {
|
|
92
|
+
eligible: false,
|
|
93
|
+
backend: 'file_checkpoint',
|
|
94
|
+
resourceKind: 'directory',
|
|
95
|
+
reason: FILE_CHECKPOINT_NON_GIT_DISABLED,
|
|
96
|
+
signals,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (!context.durableCheckpointEnabled) {
|
|
100
|
+
return {
|
|
101
|
+
eligible: false,
|
|
102
|
+
backend: 'file_checkpoint',
|
|
103
|
+
resourceKind: 'directory',
|
|
104
|
+
reason: FILE_CHECKPOINT_DURABLE_REQUIRED,
|
|
105
|
+
signals,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const isolationReason = fileCheckpointIsolationReason(context);
|
|
109
|
+
if (isolationReason) {
|
|
110
|
+
return {
|
|
111
|
+
eligible: false,
|
|
112
|
+
backend: 'file_checkpoint',
|
|
113
|
+
resourceKind: 'directory',
|
|
114
|
+
reason: isolationReason,
|
|
115
|
+
signals: [...signals, 'isolation_unavailable'],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const copyStrategy = await probeFileCloneStrategy();
|
|
119
|
+
return {
|
|
120
|
+
eligible: true,
|
|
121
|
+
backend: 'file_checkpoint',
|
|
122
|
+
resourceKind: 'directory',
|
|
123
|
+
signals: [...signals, 'non_git_file_checkpoint', copyStrategy],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
79
126
|
async function protectedRootState(root) {
|
|
80
127
|
const node = await readSnapshotNode(root);
|
|
81
128
|
if (node.kind !== 'directory') {
|
|
@@ -163,6 +210,9 @@ async function prepareDirtyGitSnapshot(context) {
|
|
|
163
210
|
quotas,
|
|
164
211
|
deadlineMs,
|
|
165
212
|
});
|
|
213
|
+
if (sourceIndex.treeHash !== baselineCopy.sourceIndex.treeHash) {
|
|
214
|
+
throw new Error(FILE_CHECKPOINT_SOURCE_CHANGED);
|
|
215
|
+
}
|
|
166
216
|
if (sourceIndex.treeHash !== baselineIndex.treeHash) {
|
|
167
217
|
throw new Error(FILE_CHECKPOINT_SOURCE_CHANGED);
|
|
168
218
|
}
|
|
@@ -200,35 +250,190 @@ async function prepareDirtyGitSnapshot(context) {
|
|
|
200
250
|
if (error instanceof FileCheckpointDiagnosticError) {
|
|
201
251
|
throw error;
|
|
202
252
|
}
|
|
203
|
-
|
|
204
|
-
|
|
253
|
+
rethrowStableFileCheckpointError(error);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async function prepareNonGitSnapshot(context) {
|
|
257
|
+
const prepareStartedAt = Date.now();
|
|
258
|
+
const excludedRoots = context.dirtyIgnoreRoots ?? [];
|
|
259
|
+
const quotas = context.fileCheckpoint;
|
|
260
|
+
const deadlineMs = Date.now() + quotas.prepareTimeoutMs;
|
|
261
|
+
await removeDeadOwnerStaging(os.tmpdir());
|
|
262
|
+
const stagingRoot = await mkdtemp(path.join(os.tmpdir(), 'belay-file-checkpoint-'));
|
|
263
|
+
await writeOwnerMarker(stagingRoot, {
|
|
264
|
+
version: 1,
|
|
265
|
+
pid: process.pid,
|
|
266
|
+
createdAt: new Date().toISOString(),
|
|
267
|
+
resourceRoot: context.repoRoot,
|
|
268
|
+
backend: 'file_checkpoint',
|
|
269
|
+
});
|
|
270
|
+
const baselineRoot = path.join(stagingRoot, 'baseline');
|
|
271
|
+
const executionRoot = path.join(stagingRoot, 'execution');
|
|
272
|
+
try {
|
|
273
|
+
resolveExecutionCwdRelative(context.repoRoot, context.cwd);
|
|
274
|
+
const resourceIdentity = await currentRecoveryResourceIdentity(context.repoRoot, 'directory');
|
|
275
|
+
const baselineCopy = await cloneDirectoryTree(context.repoRoot, baselineRoot, {
|
|
276
|
+
excludedRoots,
|
|
277
|
+
quotas,
|
|
278
|
+
deadlineMs,
|
|
279
|
+
});
|
|
280
|
+
await mkdir(baselineRoot, { recursive: true });
|
|
281
|
+
const baselineIndex = await buildFileTreeIndex({
|
|
282
|
+
resourceRoot: baselineRoot,
|
|
283
|
+
excludedRoots,
|
|
284
|
+
quotas,
|
|
285
|
+
deadlineMs,
|
|
286
|
+
});
|
|
287
|
+
const sourceIndex = await buildFileTreeIndex({
|
|
288
|
+
resourceRoot: context.repoRoot,
|
|
289
|
+
excludedRoots,
|
|
290
|
+
quotas,
|
|
291
|
+
deadlineMs,
|
|
292
|
+
});
|
|
293
|
+
if (sourceIndex.treeHash !== baselineCopy.sourceIndex.treeHash) {
|
|
294
|
+
throw new Error(FILE_CHECKPOINT_SOURCE_CHANGED);
|
|
205
295
|
}
|
|
206
|
-
if (
|
|
207
|
-
throw
|
|
296
|
+
if (sourceIndex.treeHash !== baselineIndex.treeHash) {
|
|
297
|
+
throw new Error(FILE_CHECKPOINT_SOURCE_CHANGED);
|
|
208
298
|
}
|
|
209
|
-
|
|
210
|
-
|
|
299
|
+
await writeFile(path.join(stagingRoot, 'baseline-index.json'), `${JSON.stringify(baselineIndex)}\n`, 'utf8');
|
|
300
|
+
const executionCopy = await cloneDirectoryTree(baselineRoot, executionRoot, {
|
|
301
|
+
excludedRoots,
|
|
302
|
+
quotas,
|
|
303
|
+
deadlineMs,
|
|
211
304
|
});
|
|
305
|
+
await mkdir(executionRoot, { recursive: true });
|
|
306
|
+
const finalSourceIndex = await buildFileTreeIndex({
|
|
307
|
+
resourceRoot: context.repoRoot,
|
|
308
|
+
excludedRoots,
|
|
309
|
+
quotas,
|
|
310
|
+
deadlineMs,
|
|
311
|
+
});
|
|
312
|
+
if (finalSourceIndex.treeHash !== baselineIndex.treeHash) {
|
|
313
|
+
throw new Error(FILE_CHECKPOINT_SOURCE_CHANGED);
|
|
314
|
+
}
|
|
315
|
+
if ((await currentRecoveryResourceIdentity(context.repoRoot, 'directory')) !== resourceIdentity) {
|
|
316
|
+
throw new Error(FILE_CHECKPOINT_SOURCE_CHANGED);
|
|
317
|
+
}
|
|
318
|
+
const protectedRootStates = await captureProtectedRootStates(context.repoRoot, executionRoot, excludedRoots);
|
|
319
|
+
const workspaceBytes = await directoryByteSize(stagingRoot, deadlineMs);
|
|
320
|
+
if (workspaceBytes > quotas.maxWorkspaceBytes) {
|
|
321
|
+
throw new FileCheckpointDiagnosticError(FILE_CHECKPOINT_QUOTA_EXCEEDED, `snapshot workspaceBytes=${workspaceBytes} exceeds maxWorkspaceBytes=${quotas.maxWorkspaceBytes}`);
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
stagingRoot,
|
|
325
|
+
baselineRoot,
|
|
326
|
+
executionRoot,
|
|
327
|
+
baselineIndex,
|
|
328
|
+
resourceIdentity,
|
|
329
|
+
copyStrategy: executionCopy.strategy === baselineCopy.strategy ? executionCopy.strategy : 'copy',
|
|
330
|
+
workspaceBytes,
|
|
331
|
+
prepareMs: Date.now() - prepareStartedAt,
|
|
332
|
+
protectedRootStates,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
337
|
+
if (error instanceof FileCheckpointDiagnosticError) {
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
rethrowStableFileCheckpointError(error);
|
|
212
341
|
}
|
|
213
342
|
}
|
|
343
|
+
function collectObservedChanges(prepared, context) {
|
|
344
|
+
return async () => {
|
|
345
|
+
for (const [protectedRoot, before] of prepared.protectedRootStates) {
|
|
346
|
+
if ((await protectedRootState(protectedRoot)) !== before) {
|
|
347
|
+
throw new Error(FILE_CHECKPOINT_PROTECTED_PATH_CHANGED);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const executionIndex = await buildFileTreeIndex({
|
|
351
|
+
resourceRoot: prepared.executionRoot,
|
|
352
|
+
excludedRoots: context.dirtyIgnoreRoots ?? [],
|
|
353
|
+
quotas: context.fileCheckpoint,
|
|
354
|
+
});
|
|
355
|
+
const observed = diffFileTreeIndices(prepared.baselineIndex, executionIndex);
|
|
356
|
+
return observed.map((change) => ({
|
|
357
|
+
relativePath: change.relativePath,
|
|
358
|
+
kind: change.kind,
|
|
359
|
+
}));
|
|
360
|
+
};
|
|
361
|
+
}
|
|
214
362
|
export const fileCheckpointBackend = {
|
|
215
363
|
id: 'file_checkpoint',
|
|
216
|
-
probe
|
|
364
|
+
async probe(context) {
|
|
365
|
+
if (await isGitWorktreeAvailable(context.repoRoot)) {
|
|
366
|
+
return probeDirtyGitFileCheckpoint(context);
|
|
367
|
+
}
|
|
368
|
+
return probeNonGitFileCheckpoint(context);
|
|
369
|
+
},
|
|
217
370
|
async prepare(context) {
|
|
218
|
-
|
|
371
|
+
if (await isGitWorktreeAvailable(context.repoRoot)) {
|
|
372
|
+
const probe = await probeDirtyGitFileCheckpoint(context);
|
|
373
|
+
if (!probe.eligible) {
|
|
374
|
+
throw new Error(probe.reason ?? FILE_CHECKPOINT_PREPARE_FAILED);
|
|
375
|
+
}
|
|
376
|
+
const prepared = await prepareDirtyGitSnapshot(context);
|
|
377
|
+
const executionCwdRelative = resolveExecutionCwdRelative(context.repoRoot, context.cwd);
|
|
378
|
+
const resourceIdentity = await currentRecoveryResourceIdentity(context.repoRoot, 'git_repository');
|
|
379
|
+
return {
|
|
380
|
+
backend: 'file_checkpoint',
|
|
381
|
+
resourceRoot: context.repoRoot,
|
|
382
|
+
executionRoot: prepared.executionRoot,
|
|
383
|
+
baselineRoot: prepared.baselineRoot,
|
|
384
|
+
resourceKind: 'git_repository',
|
|
385
|
+
resourceIdentity,
|
|
386
|
+
baselineTreeHash: prepared.baselineIndex.treeHash,
|
|
387
|
+
excludedRoots: context.dirtyIgnoreRoots ?? [],
|
|
388
|
+
copyStrategy: prepared.copyStrategy,
|
|
389
|
+
snapshotFileCount: prepared.baselineIndex.fileCount,
|
|
390
|
+
snapshotSourceBytes: prepared.baselineIndex.totalFileBytes,
|
|
391
|
+
snapshotWorkspaceBytes: prepared.workspaceBytes,
|
|
392
|
+
snapshotPrepareMs: prepared.prepareMs,
|
|
393
|
+
executionCwdRelative,
|
|
394
|
+
async validateSourceState() {
|
|
395
|
+
const sourceIndex = await buildFileTreeIndex({
|
|
396
|
+
resourceRoot: context.repoRoot,
|
|
397
|
+
excludedRoots: context.dirtyIgnoreRoots ?? [],
|
|
398
|
+
quotas: context.fileCheckpoint,
|
|
399
|
+
});
|
|
400
|
+
if (sourceIndex.treeHash !== prepared.baselineIndex.treeHash ||
|
|
401
|
+
(await computeGitMetadataFingerprint(context.repoRoot)) !==
|
|
402
|
+
prepared.sourceGitMetadataFingerprint) {
|
|
403
|
+
throw new Error(FILE_CHECKPOINT_SOURCE_CHANGED);
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
async collectChanges() {
|
|
407
|
+
const afterFingerprint = await computeGitMetadataFingerprint(prepared.executionRoot);
|
|
408
|
+
if (afterFingerprint !== prepared.gitMetadataFingerprint) {
|
|
409
|
+
throw new Error(FILE_CHECKPOINT_GIT_METADATA_CHANGED);
|
|
410
|
+
}
|
|
411
|
+
return collectObservedChanges(prepared, context)();
|
|
412
|
+
},
|
|
413
|
+
async cleanup() {
|
|
414
|
+
await rm(prepared.stagingRoot, { recursive: true, force: true });
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const probe = await probeNonGitFileCheckpoint(context);
|
|
219
419
|
if (!probe.eligible) {
|
|
220
420
|
throw new Error(probe.reason ?? FILE_CHECKPOINT_PREPARE_FAILED);
|
|
221
421
|
}
|
|
222
|
-
const prepared = await
|
|
422
|
+
const prepared = await prepareNonGitSnapshot(context);
|
|
223
423
|
const executionCwdRelative = resolveExecutionCwdRelative(context.repoRoot, context.cwd);
|
|
224
|
-
const
|
|
424
|
+
const validateResourceIdentity = async () => {
|
|
425
|
+
if ((await currentRecoveryResourceIdentity(context.repoRoot, 'directory')) !==
|
|
426
|
+
prepared.resourceIdentity) {
|
|
427
|
+
throw new Error(FILE_CHECKPOINT_SOURCE_CHANGED);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
225
430
|
return {
|
|
226
431
|
backend: 'file_checkpoint',
|
|
227
432
|
resourceRoot: context.repoRoot,
|
|
228
433
|
executionRoot: prepared.executionRoot,
|
|
229
434
|
baselineRoot: prepared.baselineRoot,
|
|
230
|
-
resourceKind: '
|
|
231
|
-
resourceIdentity,
|
|
435
|
+
resourceKind: 'directory',
|
|
436
|
+
resourceIdentity: prepared.resourceIdentity,
|
|
232
437
|
baselineTreeHash: prepared.baselineIndex.treeHash,
|
|
233
438
|
excludedRoots: context.dirtyIgnoreRoots ?? [],
|
|
234
439
|
copyStrategy: prepared.copyStrategy,
|
|
@@ -243,32 +448,15 @@ export const fileCheckpointBackend = {
|
|
|
243
448
|
excludedRoots: context.dirtyIgnoreRoots ?? [],
|
|
244
449
|
quotas: context.fileCheckpoint,
|
|
245
450
|
});
|
|
246
|
-
if (sourceIndex.treeHash !== prepared.baselineIndex.treeHash
|
|
247
|
-
(await computeGitMetadataFingerprint(context.repoRoot)) !==
|
|
248
|
-
prepared.sourceGitMetadataFingerprint) {
|
|
451
|
+
if (sourceIndex.treeHash !== prepared.baselineIndex.treeHash) {
|
|
249
452
|
throw new Error(FILE_CHECKPOINT_SOURCE_CHANGED);
|
|
250
453
|
}
|
|
454
|
+
await validateResourceIdentity();
|
|
251
455
|
},
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
for (const [protectedRoot, before] of prepared.protectedRootStates) {
|
|
258
|
-
if ((await protectedRootState(protectedRoot)) !== before) {
|
|
259
|
-
throw new Error(FILE_CHECKPOINT_PROTECTED_PATH_CHANGED);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
const executionIndex = await buildFileTreeIndex({
|
|
263
|
-
resourceRoot: prepared.executionRoot,
|
|
264
|
-
excludedRoots: context.dirtyIgnoreRoots ?? [],
|
|
265
|
-
quotas: context.fileCheckpoint,
|
|
266
|
-
});
|
|
267
|
-
const observed = diffFileTreeIndices(prepared.baselineIndex, executionIndex);
|
|
268
|
-
return observed.map((change) => ({
|
|
269
|
-
relativePath: change.relativePath,
|
|
270
|
-
kind: change.kind,
|
|
271
|
-
}));
|
|
456
|
+
validateResourceIdentity,
|
|
457
|
+
collectChanges: async () => {
|
|
458
|
+
await assertRootGitMetadataUnchanged(prepared.baselineRoot, prepared.executionRoot);
|
|
459
|
+
return collectObservedChanges(prepared, context)();
|
|
272
460
|
},
|
|
273
461
|
async cleanup() {
|
|
274
462
|
await rm(prepared.stagingRoot, { recursive: true, force: true });
|
|
@@ -3,6 +3,9 @@ export declare const FILE_CHECKPOINT_SOURCE_CHANGED = "file_checkpoint_source_ch
|
|
|
3
3
|
export declare const FILE_CHECKPOINT_CWD_OUTSIDE_ROOT = "file_checkpoint_cwd_outside_root";
|
|
4
4
|
export declare const FILE_CHECKPOINT_PREPARE_FAILED = "file_checkpoint_prepare_failed";
|
|
5
5
|
export declare const FILE_CHECKPOINT_SPLIT_INDEX_UNSUPPORTED = "file_checkpoint_split_index_unsupported";
|
|
6
|
+
export declare function rethrowStableFileCheckpointError(error: unknown): never;
|
|
7
|
+
export declare function rootGitMetadataPresent(repoRoot: string): Promise<boolean>;
|
|
8
|
+
export declare function assertRootGitMetadataUnchanged(baselineRoot: string, executionRoot: string): Promise<void>;
|
|
6
9
|
export declare function execGit(repoRoot: string, args: string[]): Promise<string>;
|
|
7
10
|
export declare function cloneBareWorktreeCopy(sourceRoot: string, destinationRoot: string): Promise<void>;
|
|
8
11
|
export declare function removeAllGitRemotes(repoRoot: string): Promise<void>;
|
|
@@ -8,6 +8,59 @@ export const FILE_CHECKPOINT_SOURCE_CHANGED = 'file_checkpoint_source_changed';
|
|
|
8
8
|
export const FILE_CHECKPOINT_CWD_OUTSIDE_ROOT = 'file_checkpoint_cwd_outside_root';
|
|
9
9
|
export const FILE_CHECKPOINT_PREPARE_FAILED = 'file_checkpoint_prepare_failed';
|
|
10
10
|
export const FILE_CHECKPOINT_SPLIT_INDEX_UNSUPPORTED = 'file_checkpoint_split_index_unsupported';
|
|
11
|
+
const STABLE_FILE_CHECKPOINT_ERRORS = new Set([
|
|
12
|
+
FILE_CHECKPOINT_GIT_METADATA_CHANGED,
|
|
13
|
+
FILE_CHECKPOINT_SOURCE_CHANGED,
|
|
14
|
+
FILE_CHECKPOINT_CWD_OUTSIDE_ROOT,
|
|
15
|
+
FILE_CHECKPOINT_PREPARE_FAILED,
|
|
16
|
+
FILE_CHECKPOINT_SPLIT_INDEX_UNSUPPORTED,
|
|
17
|
+
'file_checkpoint_disabled',
|
|
18
|
+
'file_checkpoint_non_git_disabled',
|
|
19
|
+
'file_checkpoint_durable_checkpoint_required',
|
|
20
|
+
'file_checkpoint_isolation_unavailable',
|
|
21
|
+
'file_checkpoint_not_implemented',
|
|
22
|
+
'file_checkpoint_nested_repository',
|
|
23
|
+
'file_checkpoint_unsupported_node',
|
|
24
|
+
'file_checkpoint_hardlink_unsupported',
|
|
25
|
+
'file_checkpoint_quota_exceeded',
|
|
26
|
+
'file_checkpoint_prepare_timeout',
|
|
27
|
+
'file_checkpoint_copy_failed',
|
|
28
|
+
'file_checkpoint_protected_path_changed',
|
|
29
|
+
'file_checkpoint_path_escape',
|
|
30
|
+
]);
|
|
31
|
+
export function rethrowStableFileCheckpointError(error) {
|
|
32
|
+
if (error instanceof Error && STABLE_FILE_CHECKPOINT_ERRORS.has(error.message)) {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
throw new Error(FILE_CHECKPOINT_PREPARE_FAILED, { cause: error });
|
|
36
|
+
}
|
|
37
|
+
export async function rootGitMetadataPresent(repoRoot) {
|
|
38
|
+
try {
|
|
39
|
+
await lstat(path.join(repoRoot, '.git'));
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function assertRootGitMetadataUnchanged(baselineRoot, executionRoot) {
|
|
47
|
+
const [baselinePresent, executionPresent] = await Promise.all([
|
|
48
|
+
rootGitMetadataPresent(baselineRoot),
|
|
49
|
+
rootGitMetadataPresent(executionRoot),
|
|
50
|
+
]);
|
|
51
|
+
if (baselinePresent !== executionPresent) {
|
|
52
|
+
throw new Error(FILE_CHECKPOINT_GIT_METADATA_CHANGED);
|
|
53
|
+
}
|
|
54
|
+
if (executionPresent) {
|
|
55
|
+
const [baselineFingerprint, executionFingerprint] = await Promise.all([
|
|
56
|
+
computeGitMetadataFingerprint(baselineRoot),
|
|
57
|
+
computeGitMetadataFingerprint(executionRoot),
|
|
58
|
+
]);
|
|
59
|
+
if (baselineFingerprint !== executionFingerprint) {
|
|
60
|
+
throw new Error(FILE_CHECKPOINT_GIT_METADATA_CHANGED);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
11
64
|
export function execGit(repoRoot, args) {
|
|
12
65
|
return new Promise((resolve, reject) => {
|
|
13
66
|
const child = spawn('git', ['-C', repoRoot, ...args], {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { TransactionalBackendContext } from './backend.js';
|
|
2
|
+
export declare const FILE_CHECKPOINT_ISOLATION_UNAVAILABLE = "file_checkpoint_isolation_unavailable";
|
|
3
|
+
export declare function fileCheckpointIsolationReason(context: TransactionalBackendContext): string | null;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { attestsWorkspaceMountIsolation, isAttestationCurrent } from '../capability/attestation.js';
|
|
2
|
+
export const FILE_CHECKPOINT_ISOLATION_UNAVAILABLE = 'file_checkpoint_isolation_unavailable';
|
|
3
|
+
export function fileCheckpointIsolationReason(context) {
|
|
4
|
+
const attestation = context.boundaryAttestation;
|
|
5
|
+
const driverId = context.boundaryDriverId;
|
|
6
|
+
if (!attestation || !isAttestationCurrent(attestation) || !driverId) {
|
|
7
|
+
return FILE_CHECKPOINT_ISOLATION_UNAVAILABLE;
|
|
8
|
+
}
|
|
9
|
+
if (attestation.driver !== driverId) {
|
|
10
|
+
return FILE_CHECKPOINT_ISOLATION_UNAVAILABLE;
|
|
11
|
+
}
|
|
12
|
+
if (!attestsWorkspaceMountIsolation(attestation)) {
|
|
13
|
+
return FILE_CHECKPOINT_ISOLATION_UNAVAILABLE;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
@@ -4,6 +4,7 @@ async function probeGitWorktreeBackend(context) {
|
|
|
4
4
|
return {
|
|
5
5
|
eligible: false,
|
|
6
6
|
backend: 'git_worktree',
|
|
7
|
+
resourceKind: 'git_repository',
|
|
7
8
|
reason: 'git_worktree_unavailable',
|
|
8
9
|
signals: [],
|
|
9
10
|
};
|
|
@@ -12,6 +13,7 @@ async function probeGitWorktreeBackend(context) {
|
|
|
12
13
|
return {
|
|
13
14
|
eligible: false,
|
|
14
15
|
backend: 'git_worktree',
|
|
16
|
+
resourceKind: 'git_repository',
|
|
15
17
|
reason: 'dirty_worktree',
|
|
16
18
|
signals: ['dirty_git_worktree'],
|
|
17
19
|
};
|
|
@@ -19,6 +21,7 @@ async function probeGitWorktreeBackend(context) {
|
|
|
19
21
|
return {
|
|
20
22
|
eligible: true,
|
|
21
23
|
backend: 'git_worktree',
|
|
24
|
+
resourceKind: 'git_repository',
|
|
22
25
|
signals: ['clean_git_worktree'],
|
|
23
26
|
};
|
|
24
27
|
}
|
|
@@ -15,4 +15,5 @@ export declare function applyWorktreeChanges(worktreePath: string, repoRoot: str
|
|
|
15
15
|
/** Runs while rollback backups are still available. */
|
|
16
16
|
afterApply?: () => Promise<void>;
|
|
17
17
|
observedChanges?: Awaited<ReturnType<typeof buildObservedChangesFromTransactional>>;
|
|
18
|
+
beforeMutation?: () => Promise<void>;
|
|
18
19
|
}): Promise<void>;
|