@guilz-dev/belay 0.6.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 +20 -1
- package/dist/adapters/claude/runtime-entry.js +54 -25
- package/dist/adapters/shared/gate-runtime.d.ts +7 -0
- package/dist/adapters/shared/gate-runtime.js +420 -220
- package/dist/bundle/claude-runtime.mjs +13215 -8864
- package/dist/bundle/codex-runtime.mjs +10771 -6453
- package/dist/bundle/cursor-runtime.mjs +10797 -6484
- package/dist/cli.js +37 -1
- package/dist/commands/approve.js +34 -10
- package/dist/commands/doctor.js +51 -0
- 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/status.js +7 -0
- package/dist/config-io.js +49 -18
- package/dist/conformance/guarantee-table.js +6 -0
- 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 +86 -27
- package/dist/core/approval.d.ts +2 -0
- package/dist/core/approval.js +6 -3
- package/dist/core/audit-types.d.ts +13 -0
- package/dist/core/capability/approval-state-mutation.d.ts +19 -0
- package/dist/core/capability/approval-state-mutation.js +76 -3
- package/dist/core/capability/approval-v3.d.ts +18 -1
- package/dist/core/capability/approval-v3.js +63 -23
- package/dist/core/capability/attestation.d.ts +3 -0
- package/dist/core/capability/attestation.js +7 -0
- package/dist/core/capability/boundary-driver-container.d.ts +12 -0
- package/dist/core/capability/boundary-driver-container.js +101 -34
- package/dist/core/capability/boundary-driver.js +1 -0
- package/dist/core/capability/boundary-grant-materialize.d.ts +1 -0
- package/dist/core/capability/boundary-grant-materialize.js +15 -0
- package/dist/core/capability/boundary-run.d.ts +25 -0
- package/dist/core/capability/boundary-run.js +37 -9
- package/dist/core/capability/boundary-session.d.ts +4 -0
- package/dist/core/capability/boundary-session.js +9 -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/gate-policy-shadow.js +23 -1
- 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 +36 -1
- package/dist/core/capability/grant-lease.js +213 -16
- package/dist/core/capability/grant-match.js +3 -0
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/index.js +1 -1
- package/dist/core/capability/policy-engine.d.ts +7 -0
- package/dist/core/capability/policy-engine.js +202 -15
- package/dist/core/capability/request.d.ts +3 -0
- package/dist/core/config.d.ts +20 -0
- package/dist/core/config.js +62 -0
- 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 +2 -0
- package/dist/core/gate-contract.js +2 -0
- package/dist/core/gate-engine.js +26 -9
- 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/network-endpoint.d.ts +10 -0
- package/dist/core/network-endpoint.js +51 -0
- 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/checkpoint.d.ts +25 -0
- package/dist/core/recovery/checkpoint.js +260 -0
- package/dist/core/recovery/index.d.ts +3 -1
- package/dist/core/recovery/index.js +2 -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 +85 -1
- 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/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 +9 -11
- package/dist/core/transactional/git-worktree.js +24 -119
- package/dist/core/transactional/index.d.ts +11 -0
- package/dist/core/transactional/index.js +10 -0
- package/dist/core/transactional/reasons.js +1 -0
- package/dist/core/transactional/runner.js +157 -25
- 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 +17 -1
- package/dist/core/types.d.ts +10 -1
- package/dist/core/verdict/adapter.js +8 -0
- package/dist/core/verdict/launcher-resolve.js +2 -20
- package/dist/core/verdict/shell-policy.d.ts +1 -0
- package/dist/core/verdict/shell-policy.js +1 -1
- package/dist/core/verdict/types.d.ts +5 -0
- package/dist/core/verdict/verdict.js +37 -20
- package/dist/types.d.ts +11 -0
- 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
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { lstat, mkdir, open, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { canonicalStringify, hashValue } from '../fingerprint.js';
|
|
6
|
+
import { canonicalPath } from '../path-utils.js';
|
|
7
|
+
import { recoveryStateHash, validateRecoverySnapshot, validRecoveryRelativePath, } from './snapshot-node.js';
|
|
8
|
+
export const RECOVERY_CHECKPOINT_CORRUPT = 'recovery_checkpoint_corrupt';
|
|
9
|
+
const RECOVERY_STATES = new Set([
|
|
10
|
+
'prepared',
|
|
11
|
+
'applying',
|
|
12
|
+
'applied',
|
|
13
|
+
'restoring',
|
|
14
|
+
'restored',
|
|
15
|
+
'conflict',
|
|
16
|
+
'corrupt',
|
|
17
|
+
'needs_manual_repair',
|
|
18
|
+
]);
|
|
19
|
+
const STAGING_STALE_MS = 5 * 60_000;
|
|
20
|
+
export function checkpointsRoot(stateDir) {
|
|
21
|
+
return path.join(stateDir, 'recovery', 'checkpoints');
|
|
22
|
+
}
|
|
23
|
+
export function checkpointDir(stateDir, checkpointId) {
|
|
24
|
+
if (!/^cp_[a-f0-9]{24}$/.test(checkpointId)) {
|
|
25
|
+
throw new Error('invalid_recovery_checkpoint_id');
|
|
26
|
+
}
|
|
27
|
+
return path.join(checkpointsRoot(stateDir), checkpointId);
|
|
28
|
+
}
|
|
29
|
+
export async function fsyncPath(filePath) {
|
|
30
|
+
const handle = await open(filePath, 'r');
|
|
31
|
+
try {
|
|
32
|
+
await handle.sync();
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await handle.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function atomicWriteJson(filePath, value) {
|
|
39
|
+
await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
40
|
+
const temporary = `${filePath}.tmp-${randomUUID()}`;
|
|
41
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
42
|
+
await fsyncPath(temporary);
|
|
43
|
+
await rename(temporary, filePath);
|
|
44
|
+
await fsyncPath(path.dirname(filePath));
|
|
45
|
+
}
|
|
46
|
+
export async function writeRecoveryState(artifactDir, state, manifestHash, detail) {
|
|
47
|
+
const value = {
|
|
48
|
+
version: 1,
|
|
49
|
+
state,
|
|
50
|
+
updatedAt: new Date().toISOString(),
|
|
51
|
+
manifestHash,
|
|
52
|
+
...(detail ? { detail } : {}),
|
|
53
|
+
};
|
|
54
|
+
await atomicWriteJson(path.join(artifactDir, 'state.json'), value);
|
|
55
|
+
}
|
|
56
|
+
export async function directorySize(root) {
|
|
57
|
+
if (!existsSync(root))
|
|
58
|
+
return 0;
|
|
59
|
+
let total = 0;
|
|
60
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
61
|
+
const entryPath = path.join(root, entry.name);
|
|
62
|
+
if (entry.isDirectory())
|
|
63
|
+
total += await directorySize(entryPath);
|
|
64
|
+
else
|
|
65
|
+
total += (await lstat(entryPath)).size;
|
|
66
|
+
}
|
|
67
|
+
return total;
|
|
68
|
+
}
|
|
69
|
+
function isManifestShape(value, checkpointId) {
|
|
70
|
+
if (!value || typeof value !== 'object')
|
|
71
|
+
return false;
|
|
72
|
+
const manifest = value;
|
|
73
|
+
const proof = manifest.proof;
|
|
74
|
+
const manifestKeys = [
|
|
75
|
+
'version',
|
|
76
|
+
'checkpointId',
|
|
77
|
+
'backend',
|
|
78
|
+
'repoRoot',
|
|
79
|
+
'repoIdentity',
|
|
80
|
+
'commandFingerprint',
|
|
81
|
+
'createdAt',
|
|
82
|
+
'expiresAt',
|
|
83
|
+
'proof',
|
|
84
|
+
'entries',
|
|
85
|
+
...(manifest.version === 2 ? ['resourceKind'] : []),
|
|
86
|
+
];
|
|
87
|
+
const proofKeys = [
|
|
88
|
+
'version',
|
|
89
|
+
'backend',
|
|
90
|
+
'inputFingerprint',
|
|
91
|
+
'resourceScope',
|
|
92
|
+
'baseStateHash',
|
|
93
|
+
'effectClosure',
|
|
94
|
+
'issuedAt',
|
|
95
|
+
'expiresAt',
|
|
96
|
+
'probeSignals',
|
|
97
|
+
];
|
|
98
|
+
if (![1, 2].includes(manifest.version) ||
|
|
99
|
+
Object.keys(manifest).length !== manifestKeys.length ||
|
|
100
|
+
!Object.keys(manifest).every((key) => manifestKeys.includes(key)) ||
|
|
101
|
+
manifest.checkpointId !== checkpointId ||
|
|
102
|
+
!['git_worktree', 'file_checkpoint'].includes(String(manifest.backend)) ||
|
|
103
|
+
typeof manifest.repoRoot !== 'string' ||
|
|
104
|
+
typeof manifest.repoIdentity !== 'string' ||
|
|
105
|
+
typeof manifest.commandFingerprint !== 'string' ||
|
|
106
|
+
typeof manifest.createdAt !== 'string' ||
|
|
107
|
+
!Number.isFinite(Date.parse(manifest.createdAt)) ||
|
|
108
|
+
typeof manifest.expiresAt !== 'string' ||
|
|
109
|
+
!Number.isFinite(Date.parse(manifest.expiresAt)) ||
|
|
110
|
+
!Array.isArray(manifest.entries) ||
|
|
111
|
+
!proof ||
|
|
112
|
+
Object.keys(proof).length !== proofKeys.length ||
|
|
113
|
+
!Object.keys(proof).every((key) => proofKeys.includes(key)) ||
|
|
114
|
+
proof.version !== 1 ||
|
|
115
|
+
proof.backend !== manifest.backend ||
|
|
116
|
+
typeof proof.inputFingerprint !== 'string' ||
|
|
117
|
+
proof.inputFingerprint !== manifest.commandFingerprint ||
|
|
118
|
+
typeof proof.resourceScope !== 'string' ||
|
|
119
|
+
proof.resourceScope !== manifest.repoRoot ||
|
|
120
|
+
typeof proof.baseStateHash !== 'string' ||
|
|
121
|
+
proof.effectClosure !== 'repo_local_fs_observed' ||
|
|
122
|
+
typeof proof.issuedAt !== 'string' ||
|
|
123
|
+
!Number.isFinite(Date.parse(proof.issuedAt)) ||
|
|
124
|
+
proof.issuedAt !== manifest.createdAt ||
|
|
125
|
+
typeof proof.expiresAt !== 'string' ||
|
|
126
|
+
!Number.isFinite(Date.parse(proof.expiresAt)) ||
|
|
127
|
+
proof.expiresAt !== manifest.expiresAt ||
|
|
128
|
+
!Array.isArray(proof.probeSignals) ||
|
|
129
|
+
!proof.probeSignals.every((signal) => typeof signal === 'string')) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
if (manifest.version === 1)
|
|
133
|
+
return !('resourceKind' in manifest);
|
|
134
|
+
return ['git_repository', 'directory'].includes(String(manifest.resourceKind));
|
|
135
|
+
}
|
|
136
|
+
export async function readRecoveryArtifact(stateDir, checkpointId) {
|
|
137
|
+
const artifactDir = checkpointDir(stateDir, checkpointId);
|
|
138
|
+
let rawManifest;
|
|
139
|
+
let state;
|
|
140
|
+
try {
|
|
141
|
+
rawManifest = JSON.parse(await readFile(path.join(artifactDir, 'manifest.json'), 'utf8'));
|
|
142
|
+
state = JSON.parse(await readFile(path.join(artifactDir, 'state.json'), 'utf8'));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
throw new Error(RECOVERY_CHECKPOINT_CORRUPT);
|
|
146
|
+
}
|
|
147
|
+
if (!isManifestShape(rawManifest, checkpointId))
|
|
148
|
+
throw new Error(RECOVERY_CHECKPOINT_CORRUPT);
|
|
149
|
+
const manifest = rawManifest;
|
|
150
|
+
const manifestHash = hashValue(canonicalStringify(manifest));
|
|
151
|
+
const stateKeys = state && typeof state === 'object' ? Object.keys(state) : [];
|
|
152
|
+
const expectedStateKeys = ['version', 'state', 'updatedAt', 'manifestHash'];
|
|
153
|
+
if (state && typeof state === 'object' && 'detail' in state)
|
|
154
|
+
expectedStateKeys.push('detail');
|
|
155
|
+
if (!state ||
|
|
156
|
+
typeof state !== 'object' ||
|
|
157
|
+
Array.isArray(state) ||
|
|
158
|
+
stateKeys.length !== expectedStateKeys.length ||
|
|
159
|
+
!stateKeys.every((key) => expectedStateKeys.includes(key)) ||
|
|
160
|
+
state.version !== 1 ||
|
|
161
|
+
!RECOVERY_STATES.has(state.state) ||
|
|
162
|
+
typeof state.updatedAt !== 'string' ||
|
|
163
|
+
!Number.isFinite(Date.parse(state.updatedAt)) ||
|
|
164
|
+
(state.detail !== undefined && typeof state.detail !== 'string') ||
|
|
165
|
+
state.manifestHash !== manifestHash) {
|
|
166
|
+
throw new Error(RECOVERY_CHECKPOINT_CORRUPT);
|
|
167
|
+
}
|
|
168
|
+
const entryPaths = new Set();
|
|
169
|
+
for (const entry of manifest.entries) {
|
|
170
|
+
if (!entry ||
|
|
171
|
+
typeof entry !== 'object' ||
|
|
172
|
+
Array.isArray(entry) ||
|
|
173
|
+
Object.keys(entry).length !== 3 ||
|
|
174
|
+
!Object.keys(entry).every((key) => ['path', 'before', 'after'].includes(key)) ||
|
|
175
|
+
typeof entry.path !== 'string' ||
|
|
176
|
+
!('before' in entry) ||
|
|
177
|
+
!('after' in entry) ||
|
|
178
|
+
!validRecoveryRelativePath(entry.path) ||
|
|
179
|
+
entryPaths.has(path.normalize(entry.path))) {
|
|
180
|
+
throw new Error(RECOVERY_CHECKPOINT_CORRUPT);
|
|
181
|
+
}
|
|
182
|
+
entryPaths.add(path.normalize(entry.path));
|
|
183
|
+
for (const [side, snapshot] of [
|
|
184
|
+
['before', entry.before],
|
|
185
|
+
['after', entry.after],
|
|
186
|
+
]) {
|
|
187
|
+
await validateRecoverySnapshot({
|
|
188
|
+
snapshot,
|
|
189
|
+
side,
|
|
190
|
+
manifestVersion: manifest.version,
|
|
191
|
+
artifactDir,
|
|
192
|
+
corruptReason: RECOVERY_CHECKPOINT_CORRUPT,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const receiptPath = path.join(artifactDir, 'receipt.json');
|
|
197
|
+
let receipt;
|
|
198
|
+
if (['applied', 'restoring', 'restored', 'conflict'].includes(state.state) ||
|
|
199
|
+
existsSync(receiptPath)) {
|
|
200
|
+
receipt = await readAndValidateRecoveryReceipt(artifactDir, manifest, manifestHash);
|
|
201
|
+
}
|
|
202
|
+
return { artifactDir, manifest, state, manifestHash, ...(receipt ? { receipt } : {}) };
|
|
203
|
+
}
|
|
204
|
+
async function readAndValidateRecoveryReceipt(artifactDir, manifest, manifestHash) {
|
|
205
|
+
let rawReceipt;
|
|
206
|
+
try {
|
|
207
|
+
rawReceipt = JSON.parse(await readFile(path.join(artifactDir, 'receipt.json'), 'utf8'));
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
throw new Error(RECOVERY_CHECKPOINT_CORRUPT);
|
|
211
|
+
}
|
|
212
|
+
if (!rawReceipt || typeof rawReceipt !== 'object' || Array.isArray(rawReceipt)) {
|
|
213
|
+
throw new Error(RECOVERY_CHECKPOINT_CORRUPT);
|
|
214
|
+
}
|
|
215
|
+
const receipt = rawReceipt;
|
|
216
|
+
const receiptKeys = [
|
|
217
|
+
'version',
|
|
218
|
+
'checkpointId',
|
|
219
|
+
'manifestHash',
|
|
220
|
+
'proofHash',
|
|
221
|
+
'postStateHash',
|
|
222
|
+
'appliedAt',
|
|
223
|
+
'changeCount',
|
|
224
|
+
];
|
|
225
|
+
if (Object.keys(receipt).length !== receiptKeys.length ||
|
|
226
|
+
!Object.keys(receipt).every((key) => receiptKeys.includes(key)) ||
|
|
227
|
+
receipt.version !== 1 ||
|
|
228
|
+
receipt.checkpointId !== manifest.checkpointId ||
|
|
229
|
+
receipt.manifestHash !== manifestHash ||
|
|
230
|
+
receipt.proofHash !== hashValue(canonicalStringify(manifest.proof)) ||
|
|
231
|
+
receipt.postStateHash !== recoveryStateHash(manifest.entries, 'after') ||
|
|
232
|
+
receipt.changeCount !== manifest.entries.length ||
|
|
233
|
+
typeof receipt.appliedAt !== 'string' ||
|
|
234
|
+
!Number.isFinite(Date.parse(receipt.appliedAt))) {
|
|
235
|
+
throw new Error(RECOVERY_CHECKPOINT_CORRUPT);
|
|
236
|
+
}
|
|
237
|
+
return receipt;
|
|
238
|
+
}
|
|
239
|
+
export async function ensureRecoveryReceipt(artifactDir, manifest, manifestHash) {
|
|
240
|
+
const receiptPath = path.join(artifactDir, 'receipt.json');
|
|
241
|
+
if (existsSync(receiptPath)) {
|
|
242
|
+
return readAndValidateRecoveryReceipt(artifactDir, manifest, manifestHash);
|
|
243
|
+
}
|
|
244
|
+
const receipt = {
|
|
245
|
+
version: 1,
|
|
246
|
+
checkpointId: manifest.checkpointId,
|
|
247
|
+
manifestHash,
|
|
248
|
+
proofHash: hashValue(canonicalStringify(manifest.proof)),
|
|
249
|
+
postStateHash: recoveryStateHash(manifest.entries, 'after'),
|
|
250
|
+
appliedAt: new Date().toISOString(),
|
|
251
|
+
changeCount: manifest.entries.length,
|
|
252
|
+
};
|
|
253
|
+
await atomicWriteJson(receiptPath, receipt);
|
|
254
|
+
return receipt;
|
|
255
|
+
}
|
|
256
|
+
export async function checkpointIds(stateDir) {
|
|
257
|
+
const root = checkpointsRoot(stateDir);
|
|
258
|
+
if (!existsSync(root))
|
|
259
|
+
return [];
|
|
260
|
+
return (await readdir(root, { withFileTypes: true }))
|
|
261
|
+
.filter((entry) => entry.isDirectory() && /^cp_[a-f0-9]{24}$/.test(entry.name))
|
|
262
|
+
.map((entry) => entry.name);
|
|
263
|
+
}
|
|
264
|
+
export async function artifactRepoRoot(stateDir, checkpointId) {
|
|
265
|
+
const artifactDir = checkpointDir(stateDir, checkpointId);
|
|
266
|
+
try {
|
|
267
|
+
const manifest = JSON.parse(await readFile(path.join(artifactDir, 'manifest.json'), 'utf8'));
|
|
268
|
+
if (typeof manifest.repoRoot === 'string' && manifest.repoRoot) {
|
|
269
|
+
return canonicalPath(manifest.repoRoot);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// Fall through to the immutable staging owner used for quota attribution.
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const owner = JSON.parse(await readFile(path.join(artifactDir, 'owner.json'), 'utf8'));
|
|
277
|
+
return typeof owner.repoRoot === 'string' && owner.repoRoot
|
|
278
|
+
? canonicalPath(owner.repoRoot)
|
|
279
|
+
: null;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
export async function checkpointIdsForRepo(stateDir, repoRoot) {
|
|
286
|
+
const expected = canonicalPath(repoRoot);
|
|
287
|
+
const matching = [];
|
|
288
|
+
for (const id of await checkpointIds(stateDir)) {
|
|
289
|
+
if ((await artifactRepoRoot(stateDir, id)) === expected)
|
|
290
|
+
matching.push(id);
|
|
291
|
+
}
|
|
292
|
+
return matching;
|
|
293
|
+
}
|
|
294
|
+
export async function cleanupOrphanedStaging(stateDir) {
|
|
295
|
+
const root = checkpointsRoot(stateDir);
|
|
296
|
+
if (!existsSync(root))
|
|
297
|
+
return;
|
|
298
|
+
const now = Date.now();
|
|
299
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
300
|
+
if (!entry.isDirectory() || !/^\.tmp-cp_[a-f0-9]{24}$/.test(entry.name))
|
|
301
|
+
continue;
|
|
302
|
+
const stagingPath = path.join(root, entry.name);
|
|
303
|
+
let stale = false;
|
|
304
|
+
try {
|
|
305
|
+
const owner = JSON.parse(await readFile(path.join(stagingPath, 'owner.json'), 'utf8'));
|
|
306
|
+
const pid = typeof owner.pid === 'number' ? owner.pid : Number.NaN;
|
|
307
|
+
const createdAt = typeof owner.createdAt === 'string' ? Date.parse(owner.createdAt) : NaN;
|
|
308
|
+
let alive = false;
|
|
309
|
+
if (Number.isInteger(pid) && pid > 0) {
|
|
310
|
+
try {
|
|
311
|
+
process.kill(pid, 0);
|
|
312
|
+
alive = true;
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
alive = false;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
stale = !alive || !Number.isFinite(createdAt);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
const info = await lstat(stagingPath);
|
|
322
|
+
stale = now - info.mtimeMs >= STAGING_STALE_MS;
|
|
323
|
+
}
|
|
324
|
+
if (stale)
|
|
325
|
+
await rm(stagingPath, { recursive: true, force: true });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
export async function markRecoveryCheckpointApplying(stateDir, checkpoint) {
|
|
329
|
+
await writeRecoveryState(checkpointDir(stateDir, checkpoint.checkpointId), 'applying', checkpoint.manifestHash);
|
|
330
|
+
}
|
|
331
|
+
export async function markRecoveryCheckpointApplied(stateDir, checkpoint) {
|
|
332
|
+
const artifactDir = checkpointDir(stateDir, checkpoint.checkpointId);
|
|
333
|
+
const receipt = await ensureRecoveryReceipt(artifactDir, checkpoint.manifest, checkpoint.manifestHash);
|
|
334
|
+
await writeRecoveryState(artifactDir, 'applied', checkpoint.manifestHash);
|
|
335
|
+
return receipt;
|
|
336
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BelayTransactionalConfig } from '../config.js';
|
|
2
|
+
import type { TransactionalFileChange } from '../transactional/types.js';
|
|
3
|
+
import { markRecoveryCheckpointApplied, markRecoveryCheckpointApplying, type PreparedRecoveryCheckpoint, RECOVERY_CHECKPOINT_CORRUPT } from './artifact-store.js';
|
|
4
|
+
import { reconcileRecoveryCheckpoint } from './reconcile.js';
|
|
5
|
+
import { RECOVERY_RESTORE_CONFLICT, RECOVERY_RESTORE_REASON, recoveryRestoreBinding, restoreRecoveryCheckpoint } from './restore.js';
|
|
6
|
+
import type { RecoveryBackend, RecoveryCheckpointSummary } from './types.js';
|
|
7
|
+
export declare const RECOVERY_CHECKPOINT_QUOTA = "recovery_checkpoint_quota_exceeded";
|
|
8
|
+
type CheckpointConfig = NonNullable<BelayTransactionalConfig['checkpoint']>;
|
|
9
|
+
export declare function prepareRecoveryCheckpoint(params: {
|
|
10
|
+
stateDir: string;
|
|
11
|
+
repoRoot: string;
|
|
12
|
+
/** Immutable pre-command source for recovery blobs, when available. */
|
|
13
|
+
baselinePath?: string;
|
|
14
|
+
worktreePath: string;
|
|
15
|
+
commandFingerprint: string;
|
|
16
|
+
changes: TransactionalFileChange[];
|
|
17
|
+
protectedRoots?: string[];
|
|
18
|
+
config: CheckpointConfig;
|
|
19
|
+
backend?: RecoveryBackend;
|
|
20
|
+
}): Promise<PreparedRecoveryCheckpoint>;
|
|
21
|
+
export declare function discardPreparedRecoveryCheckpoint(stateDir: string, checkpointId: string): Promise<boolean>;
|
|
22
|
+
export declare function listRecoveryCheckpoints(stateDir: string, repoRoot?: string): Promise<RecoveryCheckpointSummary[]>;
|
|
23
|
+
export declare function recoveryCheckpointStorageBytes(stateDir: string, repoRoot?: string): Promise<number>;
|
|
24
|
+
export declare function showRecoveryCheckpoint(stateDir: string, checkpointId: string, expectedRepoRoot?: string): Promise<import("./artifact-store.js").RecoveryArtifact>;
|
|
25
|
+
export { markRecoveryCheckpointApplied, markRecoveryCheckpointApplying, RECOVERY_CHECKPOINT_CORRUPT, RECOVERY_RESTORE_CONFLICT, RECOVERY_RESTORE_REASON, reconcileRecoveryCheckpoint, recoveryRestoreBinding, restoreRecoveryCheckpoint, };
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readdir, readFile, rename, rm } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { canonicalStringify, hashValue } from '../fingerprint.js';
|
|
6
|
+
import { canonicalPath } from '../path-utils.js';
|
|
7
|
+
import { artifactRepoRoot, atomicWriteJson, checkpointDir, checkpointIds, checkpointIdsForRepo, checkpointsRoot, cleanupOrphanedStaging, directorySize, fsyncPath, markRecoveryCheckpointApplied, markRecoveryCheckpointApplying, RECOVERY_CHECKPOINT_CORRUPT, readRecoveryArtifact, writeRecoveryState, } from './artifact-store.js';
|
|
8
|
+
import { matchRecoverySide, reconcileRecoveryCheckpoint } from './reconcile.js';
|
|
9
|
+
import { currentRecoveryResourceIdentity } from './resource-identity.js';
|
|
10
|
+
import { RECOVERY_RESTORE_CONFLICT, RECOVERY_RESTORE_REASON, recoveryRestoreBinding, restoreRecoveryCheckpoint, } from './restore.js';
|
|
11
|
+
import { assertRecoverySafeTarget, captureRecoverySnapshot, withoutRecoveryBlob, } from './snapshot-node.js';
|
|
12
|
+
function recoveryProofProbeSignals(backend) {
|
|
13
|
+
if (backend === 'file_checkpoint') {
|
|
14
|
+
return ['dirty_git_worktree', 'file_checkpoint', 'observed_repo_local_diff'];
|
|
15
|
+
}
|
|
16
|
+
return ['clean_git_worktree', 'observed_repo_local_diff'];
|
|
17
|
+
}
|
|
18
|
+
export const RECOVERY_CHECKPOINT_QUOTA = 'recovery_checkpoint_quota_exceeded';
|
|
19
|
+
async function garbageCollect(stateDir, config, repoRoot) {
|
|
20
|
+
const artifacts = [];
|
|
21
|
+
for (const id of await checkpointIdsForRepo(stateDir, repoRoot)) {
|
|
22
|
+
try {
|
|
23
|
+
const loaded = await readRecoveryArtifact(stateDir, id);
|
|
24
|
+
artifacts.push({
|
|
25
|
+
id,
|
|
26
|
+
state: loaded.state.state,
|
|
27
|
+
updatedAt: Date.parse(loaded.state.updatedAt),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Corrupt checkpoints are retained for inspection.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const removable = artifacts
|
|
36
|
+
.filter((entry) => entry.state === 'applied' || entry.state === 'restored')
|
|
37
|
+
.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
38
|
+
for (const entry of removable) {
|
|
39
|
+
const retentionHours = entry.state === 'restored' ? config.restoredRetentionHours : config.appliedRetentionHours;
|
|
40
|
+
const expired = now - entry.updatedAt >= retentionHours * 60 * 60 * 1000;
|
|
41
|
+
if (!expired)
|
|
42
|
+
continue;
|
|
43
|
+
await rm(checkpointDir(stateDir, entry.id), { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function prepareRecoveryCheckpoint(params) {
|
|
47
|
+
const backend = params.backend ?? 'git_worktree';
|
|
48
|
+
await mkdir(checkpointsRoot(params.stateDir), { recursive: true, mode: 0o700 });
|
|
49
|
+
await cleanupOrphanedStaging(params.stateDir);
|
|
50
|
+
await garbageCollect(params.stateDir, params.config, params.repoRoot);
|
|
51
|
+
const existing = await checkpointIdsForRepo(params.stateDir, params.repoRoot);
|
|
52
|
+
if (existing.length >= params.config.maxCheckpoints) {
|
|
53
|
+
throw new Error(RECOVERY_CHECKPOINT_QUOTA);
|
|
54
|
+
}
|
|
55
|
+
const checkpointId = `cp_${randomUUID().replaceAll('-', '').slice(0, 24)}`;
|
|
56
|
+
const temporary = path.join(checkpointsRoot(params.stateDir), `.tmp-${checkpointId}`);
|
|
57
|
+
const finalDir = checkpointDir(params.stateDir, checkpointId);
|
|
58
|
+
await mkdir(temporary, { recursive: true, mode: 0o700 });
|
|
59
|
+
await atomicWriteJson(path.join(temporary, 'owner.json'), {
|
|
60
|
+
version: 1,
|
|
61
|
+
pid: process.pid,
|
|
62
|
+
createdAt: new Date().toISOString(),
|
|
63
|
+
repoRoot: canonicalPath(params.repoRoot),
|
|
64
|
+
});
|
|
65
|
+
await mkdir(path.join(temporary, 'blobs'), { recursive: true, mode: 0o700 });
|
|
66
|
+
try {
|
|
67
|
+
const entries = [];
|
|
68
|
+
const protectedRoots = (params.protectedRoots ?? []).map(canonicalPath);
|
|
69
|
+
for (const change of [...params.changes].sort((left, right) => left.relativePath.localeCompare(right.relativePath))) {
|
|
70
|
+
const target = await assertRecoverySafeTarget(params.repoRoot, change.relativePath);
|
|
71
|
+
if (protectedRoots.some((root) => {
|
|
72
|
+
const relative = path.relative(root, target);
|
|
73
|
+
return (relative === '' ||
|
|
74
|
+
(relative !== '..' &&
|
|
75
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
76
|
+
!path.isAbsolute(relative)));
|
|
77
|
+
})) {
|
|
78
|
+
throw new Error('recovery_protected_path');
|
|
79
|
+
}
|
|
80
|
+
const baseline = await assertRecoverySafeTarget(params.baselinePath ?? params.repoRoot, change.relativePath);
|
|
81
|
+
const source = await assertRecoverySafeTarget(params.worktreePath, change.relativePath);
|
|
82
|
+
entries.push({
|
|
83
|
+
path: change.relativePath,
|
|
84
|
+
before: await captureRecoverySnapshot(baseline, {
|
|
85
|
+
blobDir: path.join(temporary, 'blobs'),
|
|
86
|
+
}),
|
|
87
|
+
after: withoutRecoveryBlob(await captureRecoverySnapshot(source)),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const createdAt = new Date().toISOString();
|
|
91
|
+
const proof = {
|
|
92
|
+
version: 1,
|
|
93
|
+
backend,
|
|
94
|
+
inputFingerprint: params.commandFingerprint,
|
|
95
|
+
resourceScope: canonicalPath(params.repoRoot),
|
|
96
|
+
baseStateHash: hashValue(canonicalStringify(entries.map((entry) => entry.before))),
|
|
97
|
+
effectClosure: 'repo_local_fs_observed',
|
|
98
|
+
issuedAt: createdAt,
|
|
99
|
+
expiresAt: new Date(Date.now() + params.config.appliedRetentionHours * 60 * 60 * 1000).toISOString(),
|
|
100
|
+
probeSignals: recoveryProofProbeSignals(backend),
|
|
101
|
+
};
|
|
102
|
+
const manifest = {
|
|
103
|
+
version: 2,
|
|
104
|
+
checkpointId,
|
|
105
|
+
backend,
|
|
106
|
+
repoRoot: canonicalPath(params.repoRoot),
|
|
107
|
+
resourceKind: 'git_repository',
|
|
108
|
+
repoIdentity: await currentRecoveryResourceIdentity(params.repoRoot, 'git_repository'),
|
|
109
|
+
commandFingerprint: params.commandFingerprint,
|
|
110
|
+
createdAt,
|
|
111
|
+
expiresAt: proof.expiresAt,
|
|
112
|
+
proof,
|
|
113
|
+
entries,
|
|
114
|
+
};
|
|
115
|
+
const manifestHash = hashValue(canonicalStringify(manifest));
|
|
116
|
+
await atomicWriteJson(path.join(temporary, 'manifest.json'), manifest);
|
|
117
|
+
await writeRecoveryState(temporary, 'prepared', manifestHash);
|
|
118
|
+
await fsyncPath(temporary);
|
|
119
|
+
const projectedBytes = await recoveryCheckpointStorageBytes(params.stateDir, params.repoRoot);
|
|
120
|
+
if (projectedBytes > params.config.maxBytes)
|
|
121
|
+
throw new Error(RECOVERY_CHECKPOINT_QUOTA);
|
|
122
|
+
await rename(temporary, finalDir);
|
|
123
|
+
await fsyncPath(checkpointsRoot(params.stateDir));
|
|
124
|
+
return {
|
|
125
|
+
checkpointId,
|
|
126
|
+
manifest,
|
|
127
|
+
manifestHash,
|
|
128
|
+
proofHash: hashValue(canonicalStringify(proof)),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
await rm(temporary, { recursive: true, force: true });
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export async function discardPreparedRecoveryCheckpoint(stateDir, checkpointId) {
|
|
137
|
+
const state = await reconcileRecoveryCheckpoint(stateDir, checkpointId);
|
|
138
|
+
if (state !== 'prepared')
|
|
139
|
+
return false;
|
|
140
|
+
const loaded = await readRecoveryArtifact(stateDir, checkpointId);
|
|
141
|
+
if (!(await matchRecoverySide(loaded.manifest.repoRoot, loaded.manifest.entries, 'before'))) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
await rm(loaded.artifactDir, { recursive: true, force: true });
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
export async function listRecoveryCheckpoints(stateDir, repoRoot) {
|
|
148
|
+
await cleanupOrphanedStaging(stateDir);
|
|
149
|
+
const summaries = [];
|
|
150
|
+
for (const id of await checkpointIds(stateDir)) {
|
|
151
|
+
if (repoRoot) {
|
|
152
|
+
let rootFromArtifact;
|
|
153
|
+
try {
|
|
154
|
+
rootFromArtifact = (await readRecoveryArtifact(stateDir, id)).manifest.repoRoot;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
try {
|
|
158
|
+
const raw = JSON.parse(await readFile(path.join(checkpointDir(stateDir, id), 'manifest.json'), 'utf8'));
|
|
159
|
+
rootFromArtifact =
|
|
160
|
+
typeof raw.repoRoot === 'string' && raw.repoRoot ? raw.repoRoot : undefined;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
rootFromArtifact = undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!rootFromArtifact || canonicalPath(rootFromArtifact) !== canonicalPath(repoRoot))
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const state = await reconcileRecoveryCheckpoint(stateDir, id);
|
|
170
|
+
try {
|
|
171
|
+
const loaded = await readRecoveryArtifact(stateDir, id);
|
|
172
|
+
if (repoRoot && canonicalPath(loaded.manifest.repoRoot) !== canonicalPath(repoRoot))
|
|
173
|
+
continue;
|
|
174
|
+
summaries.push({
|
|
175
|
+
checkpointId: id,
|
|
176
|
+
state,
|
|
177
|
+
...(loaded.state.detail ? { stateDetail: loaded.state.detail } : {}),
|
|
178
|
+
backend: loaded.manifest.backend,
|
|
179
|
+
repoRoot: loaded.manifest.repoRoot,
|
|
180
|
+
commandFingerprint: loaded.manifest.commandFingerprint,
|
|
181
|
+
createdAt: loaded.manifest.createdAt,
|
|
182
|
+
expiresAt: loaded.manifest.expiresAt,
|
|
183
|
+
changeCount: loaded.manifest.entries.length,
|
|
184
|
+
manifestHash: loaded.manifestHash,
|
|
185
|
+
...(loaded.receipt
|
|
186
|
+
? {
|
|
187
|
+
receiptHash: hashValue(canonicalStringify(loaded.receipt)),
|
|
188
|
+
proofHash: loaded.receipt.proofHash,
|
|
189
|
+
postStateHash: loaded.receipt.postStateHash,
|
|
190
|
+
}
|
|
191
|
+
: {}),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
try {
|
|
196
|
+
const manifest = JSON.parse(await readFile(path.join(checkpointDir(stateDir, id), 'manifest.json'), 'utf8'));
|
|
197
|
+
if (manifest.checkpointId !== id || ![1, 2].includes(manifest.version))
|
|
198
|
+
continue;
|
|
199
|
+
if (repoRoot && canonicalPath(manifest.repoRoot) !== canonicalPath(repoRoot))
|
|
200
|
+
continue;
|
|
201
|
+
summaries.push({
|
|
202
|
+
checkpointId: id,
|
|
203
|
+
state: 'corrupt',
|
|
204
|
+
backend: manifest.backend,
|
|
205
|
+
repoRoot: manifest.repoRoot,
|
|
206
|
+
commandFingerprint: manifest.commandFingerprint,
|
|
207
|
+
createdAt: manifest.createdAt,
|
|
208
|
+
expiresAt: manifest.expiresAt,
|
|
209
|
+
changeCount: Array.isArray(manifest.entries) ? manifest.entries.length : 0,
|
|
210
|
+
manifestHash: hashValue(canonicalStringify(manifest)),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// No trustworthy repository identity is available for this artifact.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return summaries.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
219
|
+
}
|
|
220
|
+
export async function recoveryCheckpointStorageBytes(stateDir, repoRoot) {
|
|
221
|
+
const root = checkpointsRoot(stateDir);
|
|
222
|
+
if (!repoRoot)
|
|
223
|
+
return directorySize(root);
|
|
224
|
+
if (!existsSync(root))
|
|
225
|
+
return 0;
|
|
226
|
+
const expected = canonicalPath(repoRoot);
|
|
227
|
+
let total = 0;
|
|
228
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
229
|
+
if (!entry.isDirectory())
|
|
230
|
+
continue;
|
|
231
|
+
const entryPath = path.join(root, entry.name);
|
|
232
|
+
if (/^cp_[a-f0-9]{24}$/.test(entry.name)) {
|
|
233
|
+
if ((await artifactRepoRoot(stateDir, entry.name)) === expected) {
|
|
234
|
+
total += await directorySize(entryPath);
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (/^\.tmp-cp_[a-f0-9]{24}$/.test(entry.name)) {
|
|
239
|
+
try {
|
|
240
|
+
const owner = JSON.parse(await readFile(path.join(entryPath, 'owner.json'), 'utf8'));
|
|
241
|
+
if (typeof owner.repoRoot === 'string' && canonicalPath(owner.repoRoot) === expected) {
|
|
242
|
+
total += await directorySize(entryPath);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// Unattributable staging data is reconciled once it is stale.
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return total;
|
|
251
|
+
}
|
|
252
|
+
export async function showRecoveryCheckpoint(stateDir, checkpointId, expectedRepoRoot) {
|
|
253
|
+
if (expectedRepoRoot &&
|
|
254
|
+
(await artifactRepoRoot(stateDir, checkpointId)) !== canonicalPath(expectedRepoRoot)) {
|
|
255
|
+
throw new Error('recovery_checkpoint_repo_mismatch');
|
|
256
|
+
}
|
|
257
|
+
await reconcileRecoveryCheckpoint(stateDir, checkpointId);
|
|
258
|
+
return readRecoveryArtifact(stateDir, checkpointId);
|
|
259
|
+
}
|
|
260
|
+
export { markRecoveryCheckpointApplied, markRecoveryCheckpointApplying, RECOVERY_CHECKPOINT_CORRUPT, RECOVERY_RESTORE_CONFLICT, RECOVERY_RESTORE_REASON, reconcileRecoveryCheckpoint, recoveryRestoreBinding, restoreRecoveryCheckpoint, };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { capabilityRequestsBlockRecovery } from './capability.js';
|
|
2
|
+
export { discardPreparedRecoveryCheckpoint, listRecoveryCheckpoints, markRecoveryCheckpointApplied, markRecoveryCheckpointApplying, prepareRecoveryCheckpoint, RECOVERY_CHECKPOINT_CORRUPT, RECOVERY_CHECKPOINT_QUOTA, RECOVERY_RESTORE_CONFLICT, reconcileRecoveryCheckpoint, recoveryCheckpointStorageBytes, recoveryRestoreBinding, restoreRecoveryCheckpoint, showRecoveryCheckpoint, } from './checkpoint.js';
|
|
2
3
|
export { RECOVERY_DIRTY_WORKTREE, RECOVERY_EXECUTION_FAILED, RECOVERY_SUBSTRATE_UNAVAILABLE, recoveryFailClosedResult, recoveryFailReasonFromSkip, } from './fail-closed.js';
|
|
3
|
-
export
|
|
4
|
+
export { formatRecoveryStateDiagnostic, recoveryApprovalSetupNotes, recoveryNotificationConfigured, recoveryNotificationSetupWarning, summarizeRecoveryCheckpointDiagnostics, } from './operator-guidance.js';
|
|
5
|
+
export type { RecoveryBackend, RecoveryCheckpointEntry, RecoveryCheckpointEntryV1, RecoveryCheckpointEntryV2, RecoveryCheckpointManifest, RecoveryCheckpointManifestV1, RecoveryCheckpointManifestV2, RecoveryCheckpointState, RecoveryCheckpointSummary, RecoveryFileSnapshotV1, RecoveryFileSnapshotV2, RecoveryProofContext, RecoveryProofV1, RecoveryReceiptV1, } from './types.js';
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { capabilityRequestsBlockRecovery } from './capability.js';
|
|
2
|
+
export { discardPreparedRecoveryCheckpoint, listRecoveryCheckpoints, markRecoveryCheckpointApplied, markRecoveryCheckpointApplying, prepareRecoveryCheckpoint, RECOVERY_CHECKPOINT_CORRUPT, RECOVERY_CHECKPOINT_QUOTA, RECOVERY_RESTORE_CONFLICT, reconcileRecoveryCheckpoint, recoveryCheckpointStorageBytes, recoveryRestoreBinding, restoreRecoveryCheckpoint, showRecoveryCheckpoint, } from './checkpoint.js';
|
|
2
3
|
export { RECOVERY_DIRTY_WORKTREE, RECOVERY_EXECUTION_FAILED, RECOVERY_SUBSTRATE_UNAVAILABLE, recoveryFailClosedResult, recoveryFailReasonFromSkip, } from './fail-closed.js';
|
|
4
|
+
export { formatRecoveryStateDiagnostic, recoveryApprovalSetupNotes, recoveryNotificationConfigured, recoveryNotificationSetupWarning, summarizeRecoveryCheckpointDiagnostics, } from './operator-guidance.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BelayConfigV4 } from '../config.js';
|
|
2
|
+
import type { RecoveryCheckpointState, RecoveryCheckpointSummary } from './types.js';
|
|
3
|
+
export declare function recoveryNotificationConfigured(config: BelayConfigV4): boolean;
|
|
4
|
+
export declare function recoveryApprovalSetupNotes(): string[];
|
|
5
|
+
export declare function recoveryNotificationSetupWarning(): string;
|
|
6
|
+
export declare function formatRecoveryStateDiagnostic(state: RecoveryCheckpointState, detail?: string): string | null;
|
|
7
|
+
export declare function summarizeRecoveryCheckpointDiagnostics(checkpoints: RecoveryCheckpointSummary[]): string[];
|