@guilz-dev/belay 0.6.0 → 0.8.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 +39 -20
- 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 +443 -225
- package/dist/bundle/claude-runtime.mjs +13027 -7271
- package/dist/bundle/codex-runtime.mjs +12700 -6977
- package/dist/bundle/cursor-runtime.mjs +12695 -6977
- package/dist/cli.js +37 -1
- package/dist/commands/approve.js +34 -10
- package/dist/commands/doctor.js +58 -0
- package/dist/commands/dogfood.js +1 -1
- package/dist/commands/explain.js +2 -2
- package/dist/commands/harvest.d.ts +0 -1
- package/dist/commands/harvest.js +1 -4
- package/dist/commands/metrics.js +6 -0
- package/dist/commands/quality.js +1 -3
- 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 +137 -9
- package/dist/conformance/types.d.ts +2 -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-metrics.d.ts +1 -0
- package/dist/core/audit-metrics.js +12 -4
- package/dist/core/audit-types.d.ts +16 -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 +2 -2
- package/dist/core/capability/index.js +1 -1
- package/dist/core/capability/policy-engine.d.ts +38 -1
- package/dist/core/capability/policy-engine.js +388 -15
- package/dist/core/capability/request.d.ts +12 -0
- package/dist/core/capability/resolver.d.ts +0 -1
- package/dist/core/capability/resolver.js +0 -1
- package/dist/core/config.d.ts +20 -0
- package/dist/core/config.js +62 -2
- package/dist/core/effect-ir/audit.d.ts +9 -0
- package/dist/core/effect-ir/audit.js +87 -0
- package/dist/core/effect-ir/build.d.ts +31 -0
- package/dist/core/effect-ir/build.js +281 -0
- package/dist/core/effect-ir/index.d.ts +13 -0
- package/dist/core/effect-ir/index.js +9 -0
- package/dist/core/effect-ir/normalize.d.ts +4 -0
- package/dist/core/effect-ir/normalize.js +110 -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 +14 -0
- package/dist/core/effect-ir/policy.js +164 -0
- package/dist/core/effect-ir/shell-build.d.ts +43 -0
- package/dist/core/effect-ir/shell-build.js +77 -0
- package/dist/core/effect-ir/shell-lower.d.ts +14 -0
- package/dist/core/effect-ir/shell-lower.js +1509 -0
- package/dist/core/effect-ir/types.d.ts +62 -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 +82 -25
- package/dist/core/git-resource-identity.d.ts +26 -0
- package/dist/core/git-resource-identity.js +284 -0
- package/dist/core/harvest.d.ts +3 -7
- package/dist/core/harvest.js +4 -27
- package/dist/core/index.d.ts +5 -1
- package/dist/core/index.js +3 -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/path-utils.js +42 -23
- 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/shell-substitution.d.ts +1 -0
- package/dist/core/shell-substitution.js +61 -0
- package/dist/core/standing-allow.d.ts +1 -2
- package/dist/core/standing-allow.js +3 -20
- 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 +13 -1
- package/dist/core/verdict/adapter.js +23 -15
- package/dist/core/verdict/containment.js +4 -0
- package/dist/core/verdict/egress-classify.d.ts +12 -0
- package/dist/core/verdict/egress-classify.js +746 -0
- package/dist/core/verdict/git-classifier.d.ts +12 -0
- package/dist/core/verdict/git-classifier.js +320 -0
- package/dist/core/verdict/launcher-resolve.js +79 -37
- package/dist/core/verdict/parser.d.ts +2 -0
- package/dist/core/verdict/parser.js +95 -1
- package/dist/core/verdict/types.d.ts +6 -2
- package/dist/core/verdict/verdict.js +103 -935
- package/dist/corpus/must-allow-commands.d.ts +2 -1
- package/dist/corpus/must-allow-commands.js +2 -1
- package/dist/corpus/runtime-match.d.ts +2 -1
- package/dist/corpus/runtime-match.js +2 -1
- package/dist/corpus/types.d.ts +2 -2
- package/dist/templates.js +7 -2
- package/dist/types.d.ts +11 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/skills/belay/SKILL.md +8 -3
- package/skills/belay/belay-approve.md +10 -7
- package/dist/core/verdict/overrides.d.ts +0 -7
- package/dist/core/verdict/overrides.js +0 -37
- package/dist/core/verdict/shell-policy.d.ts +0 -25
- package/dist/core/verdict/shell-policy.js +0 -40
- package/dist/corpus/standing-allow-catalog.generated.d.ts +0 -13
- package/dist/corpus/standing-allow-catalog.generated.js +0 -99
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { chmod, copyFile, lstat, mkdir, open, readFile, readlink, rm, rmdir, symlink, 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
|
+
export const RECOVERY_UNSUPPORTED_FILE_KIND = 'recovery_unsupported_file_kind';
|
|
8
|
+
export function validRecoveryRelativePath(relativePath) {
|
|
9
|
+
if (!relativePath || relativePath.includes('\0') || path.isAbsolute(relativePath))
|
|
10
|
+
return false;
|
|
11
|
+
const normalized = path.normalize(relativePath);
|
|
12
|
+
return normalized !== '.' && normalized !== '..' && !normalized.startsWith(`..${path.sep}`);
|
|
13
|
+
}
|
|
14
|
+
export async function assertRecoverySafeTarget(resourceRoot, relativePath) {
|
|
15
|
+
if (!validRecoveryRelativePath(relativePath))
|
|
16
|
+
throw new Error('recovery_path_escape');
|
|
17
|
+
const root = canonicalPath(resourceRoot);
|
|
18
|
+
const target = path.resolve(root, relativePath);
|
|
19
|
+
const relative = path.relative(root, target);
|
|
20
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
21
|
+
throw new Error('recovery_path_escape');
|
|
22
|
+
}
|
|
23
|
+
let current = root;
|
|
24
|
+
const parentParts = path.relative(root, path.dirname(target)).split(path.sep).filter(Boolean);
|
|
25
|
+
for (const part of parentParts) {
|
|
26
|
+
current = path.join(current, part);
|
|
27
|
+
if (!existsSync(current))
|
|
28
|
+
break;
|
|
29
|
+
const info = await lstat(current);
|
|
30
|
+
if (info.isSymbolicLink())
|
|
31
|
+
throw new Error('recovery_symlink_escape');
|
|
32
|
+
if (!info.isDirectory())
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
return target;
|
|
36
|
+
}
|
|
37
|
+
async function fsyncPath(filePath) {
|
|
38
|
+
const handle = await open(filePath, 'r');
|
|
39
|
+
try {
|
|
40
|
+
await handle.sync();
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await handle.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function normalizedMode(mode) {
|
|
47
|
+
return mode & 0o777;
|
|
48
|
+
}
|
|
49
|
+
export function recoveryDirectoryHash(mode) {
|
|
50
|
+
return hashValue(`directory:${normalizedMode(mode)}`);
|
|
51
|
+
}
|
|
52
|
+
export function recoverySnapshotHash(snapshot) {
|
|
53
|
+
return hashValue(canonicalStringify(snapshot));
|
|
54
|
+
}
|
|
55
|
+
export function withoutRecoveryBlob(snapshot) {
|
|
56
|
+
if (snapshot.kind !== 'file')
|
|
57
|
+
return { ...snapshot };
|
|
58
|
+
const { blob: _blob, ...rest } = snapshot;
|
|
59
|
+
return rest;
|
|
60
|
+
}
|
|
61
|
+
export async function captureRecoverySnapshot(filePath, options) {
|
|
62
|
+
let info;
|
|
63
|
+
try {
|
|
64
|
+
info = await lstat(filePath);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (!['ENOENT', 'ENOTDIR'].includes(error.code ?? ''))
|
|
68
|
+
throw error;
|
|
69
|
+
return { kind: 'absent' };
|
|
70
|
+
}
|
|
71
|
+
if (info.isSymbolicLink()) {
|
|
72
|
+
const symlinkTarget = await readlink(filePath);
|
|
73
|
+
return {
|
|
74
|
+
kind: 'symlink',
|
|
75
|
+
symlinkTarget,
|
|
76
|
+
hash: hashValue(`symlink:${symlinkTarget}`),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (info.isDirectory()) {
|
|
80
|
+
const mode = normalizedMode(info.mode);
|
|
81
|
+
return { kind: 'directory', mode, hash: recoveryDirectoryHash(mode) };
|
|
82
|
+
}
|
|
83
|
+
if (!info.isFile())
|
|
84
|
+
throw new Error(RECOVERY_UNSUPPORTED_FILE_KIND);
|
|
85
|
+
const content = await readFile(filePath);
|
|
86
|
+
const hash = createHash('sha256').update(content).digest('hex');
|
|
87
|
+
let blob;
|
|
88
|
+
if (options?.blobDir) {
|
|
89
|
+
await mkdir(options.blobDir, { recursive: true, mode: 0o700 });
|
|
90
|
+
const blobPath = path.join(options.blobDir, hash);
|
|
91
|
+
if (!existsSync(blobPath)) {
|
|
92
|
+
await writeFile(blobPath, content, { mode: 0o600 });
|
|
93
|
+
await fsyncPath(blobPath);
|
|
94
|
+
}
|
|
95
|
+
blob = `blobs/${hash}`;
|
|
96
|
+
}
|
|
97
|
+
return { kind: 'file', mode: normalizedMode(info.mode), hash, ...(blob ? { blob } : {}) };
|
|
98
|
+
}
|
|
99
|
+
export async function recoverySnapshotMatches(filePath, expected) {
|
|
100
|
+
try {
|
|
101
|
+
const current = await captureRecoverySnapshot(filePath);
|
|
102
|
+
return (recoverySnapshotHash(withoutRecoveryBlob(current)) ===
|
|
103
|
+
recoverySnapshotHash(withoutRecoveryBlob(expected)));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function validateRecoverySnapshot(params) {
|
|
110
|
+
const { snapshot } = params;
|
|
111
|
+
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
|
|
112
|
+
throw new Error(params.corruptReason);
|
|
113
|
+
}
|
|
114
|
+
const record = snapshot;
|
|
115
|
+
const hasExactKeys = (allowed) => {
|
|
116
|
+
const keys = Object.keys(record);
|
|
117
|
+
return keys.length === allowed.length && keys.every((key) => allowed.includes(key));
|
|
118
|
+
};
|
|
119
|
+
const validHash = (value) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
120
|
+
const validMode = (value) => typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 0o777;
|
|
121
|
+
if (record.kind === 'absent') {
|
|
122
|
+
if (!hasExactKeys(['kind']))
|
|
123
|
+
throw new Error(params.corruptReason);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (record.kind === 'directory') {
|
|
127
|
+
if (params.manifestVersion !== 2 ||
|
|
128
|
+
!hasExactKeys(['kind', 'mode', 'hash']) ||
|
|
129
|
+
!validMode(record.mode) ||
|
|
130
|
+
!validHash(record.hash) ||
|
|
131
|
+
record.hash !== recoveryDirectoryHash(record.mode)) {
|
|
132
|
+
throw new Error(params.corruptReason);
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (record.kind === 'symlink') {
|
|
137
|
+
if (!hasExactKeys(['kind', 'symlinkTarget', 'hash']) ||
|
|
138
|
+
typeof record.symlinkTarget !== 'string' ||
|
|
139
|
+
!validHash(record.hash) ||
|
|
140
|
+
record.hash !== hashValue(`symlink:${record.symlinkTarget}`)) {
|
|
141
|
+
throw new Error(params.corruptReason);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (record.kind !== 'file')
|
|
146
|
+
throw new Error(params.corruptReason);
|
|
147
|
+
const allowedKeys = record.blob === undefined ? ['kind', 'mode', 'hash'] : ['kind', 'mode', 'hash', 'blob'];
|
|
148
|
+
if (!hasExactKeys(allowedKeys) ||
|
|
149
|
+
!validMode(record.mode) ||
|
|
150
|
+
!validHash(record.hash) ||
|
|
151
|
+
(record.blob !== undefined && typeof record.blob !== 'string')) {
|
|
152
|
+
throw new Error(params.corruptReason);
|
|
153
|
+
}
|
|
154
|
+
if (params.side === 'before' && !record.blob)
|
|
155
|
+
throw new Error(params.corruptReason);
|
|
156
|
+
if (!record.blob)
|
|
157
|
+
return;
|
|
158
|
+
if (record.blob !== `blobs/${record.hash}`)
|
|
159
|
+
throw new Error(params.corruptReason);
|
|
160
|
+
let content;
|
|
161
|
+
try {
|
|
162
|
+
content = await readFile(path.join(params.artifactDir, record.blob));
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
throw new Error(params.corruptReason);
|
|
166
|
+
}
|
|
167
|
+
if (createHash('sha256').update(content).digest('hex') !== record.hash) {
|
|
168
|
+
throw new Error(params.corruptReason);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function removeTargetWithoutRecursion(target) {
|
|
172
|
+
let info;
|
|
173
|
+
try {
|
|
174
|
+
info = await lstat(target);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (error.code === 'ENOENT')
|
|
178
|
+
return;
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
if (info.isDirectory() && !info.isSymbolicLink()) {
|
|
182
|
+
await rmdir(target);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
await rm(target, { force: true, recursive: false });
|
|
186
|
+
}
|
|
187
|
+
export async function applyRecoverySnapshot(target, snapshot, artifactDir, corruptReason) {
|
|
188
|
+
if (snapshot.kind === 'absent') {
|
|
189
|
+
await removeTargetWithoutRecursion(target);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
let current;
|
|
193
|
+
try {
|
|
194
|
+
current = await lstat(target);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error.code !== 'ENOENT')
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
if (snapshot.kind === 'directory') {
|
|
201
|
+
if (current && (!current.isDirectory() || current.isSymbolicLink())) {
|
|
202
|
+
await removeTargetWithoutRecursion(target);
|
|
203
|
+
current = undefined;
|
|
204
|
+
}
|
|
205
|
+
if (!current)
|
|
206
|
+
await mkdir(target, { recursive: false, mode: snapshot.mode });
|
|
207
|
+
await chmod(target, snapshot.mode);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (current)
|
|
211
|
+
await removeTargetWithoutRecursion(target);
|
|
212
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
213
|
+
if (snapshot.kind === 'symlink') {
|
|
214
|
+
if (snapshot.symlinkTarget === undefined)
|
|
215
|
+
throw new Error(corruptReason);
|
|
216
|
+
await symlink(snapshot.symlinkTarget, target);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (!snapshot.blob || !snapshot.hash)
|
|
220
|
+
throw new Error(corruptReason);
|
|
221
|
+
const blobPath = path.join(artifactDir, snapshot.blob);
|
|
222
|
+
let content;
|
|
223
|
+
try {
|
|
224
|
+
content = await readFile(blobPath);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
throw new Error(corruptReason);
|
|
228
|
+
}
|
|
229
|
+
if (createHash('sha256').update(content).digest('hex') !== snapshot.hash) {
|
|
230
|
+
throw new Error(corruptReason);
|
|
231
|
+
}
|
|
232
|
+
await copyFile(blobPath, target);
|
|
233
|
+
if (snapshot.mode !== undefined)
|
|
234
|
+
await chmod(target, snapshot.mode);
|
|
235
|
+
}
|
|
236
|
+
function pathDepth(relativePath) {
|
|
237
|
+
return relativePath.split(path.sep).length;
|
|
238
|
+
}
|
|
239
|
+
export function sortRecoveryEntriesForSide(entries, side) {
|
|
240
|
+
const rank = (entry) => {
|
|
241
|
+
if (entry[side].kind === 'absent')
|
|
242
|
+
return 0;
|
|
243
|
+
if (entry[side].kind === 'directory')
|
|
244
|
+
return 1;
|
|
245
|
+
return 2;
|
|
246
|
+
};
|
|
247
|
+
return [...entries].sort((left, right) => {
|
|
248
|
+
const rankDifference = rank(left) - rank(right);
|
|
249
|
+
if (rankDifference !== 0)
|
|
250
|
+
return rankDifference;
|
|
251
|
+
const depthDifference = pathDepth(left.path) - pathDepth(right.path);
|
|
252
|
+
if (left[side].kind === 'absent' && depthDifference !== 0)
|
|
253
|
+
return -depthDifference;
|
|
254
|
+
if (depthDifference !== 0)
|
|
255
|
+
return depthDifference;
|
|
256
|
+
return left.path.localeCompare(right.path);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
export function recoveryStateHash(entries, side) {
|
|
260
|
+
return hashValue(canonicalStringify(entries.map((entry) => ({ path: entry.path, state: entry[side] }))));
|
|
261
|
+
}
|
|
@@ -7,11 +7,95 @@ export interface RecoveryProofV1 {
|
|
|
7
7
|
inputFingerprint: string;
|
|
8
8
|
resourceScope: string;
|
|
9
9
|
baseStateHash: string;
|
|
10
|
-
|
|
10
|
+
/** The proof covers only filesystem effects observed inside the repository. */
|
|
11
|
+
effectClosure: 'repo_local_fs_observed';
|
|
11
12
|
issuedAt: string;
|
|
12
13
|
expiresAt: string;
|
|
13
14
|
probeSignals: string[];
|
|
14
15
|
}
|
|
16
|
+
export type RecoveryCheckpointState = 'prepared' | 'applying' | 'applied' | 'restoring' | 'restored' | 'conflict' | 'corrupt' | 'needs_manual_repair';
|
|
17
|
+
export type RecoveryFileKind = 'absent' | 'file' | 'symlink';
|
|
18
|
+
export interface RecoveryFileSnapshotV1 {
|
|
19
|
+
kind: RecoveryFileKind;
|
|
20
|
+
mode?: number;
|
|
21
|
+
hash?: string;
|
|
22
|
+
blob?: string;
|
|
23
|
+
symlinkTarget?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface RecoveryCheckpointEntryV1 {
|
|
26
|
+
path: string;
|
|
27
|
+
before: RecoveryFileSnapshotV1;
|
|
28
|
+
after: RecoveryFileSnapshotV1;
|
|
29
|
+
}
|
|
30
|
+
export interface RecoveryCheckpointManifestV1 {
|
|
31
|
+
version: 1;
|
|
32
|
+
checkpointId: string;
|
|
33
|
+
backend: RecoveryBackend;
|
|
34
|
+
repoRoot: string;
|
|
35
|
+
repoIdentity: string;
|
|
36
|
+
commandFingerprint: string;
|
|
37
|
+
createdAt: string;
|
|
38
|
+
expiresAt: string;
|
|
39
|
+
proof: RecoveryProofV1;
|
|
40
|
+
entries: RecoveryCheckpointEntryV1[];
|
|
41
|
+
}
|
|
42
|
+
export type RecoveryFileSnapshotV2 = RecoveryFileSnapshotV1 | {
|
|
43
|
+
kind: 'directory';
|
|
44
|
+
mode: number;
|
|
45
|
+
hash: string;
|
|
46
|
+
};
|
|
47
|
+
export interface RecoveryCheckpointEntryV2 {
|
|
48
|
+
path: string;
|
|
49
|
+
before: RecoveryFileSnapshotV2;
|
|
50
|
+
after: RecoveryFileSnapshotV2;
|
|
51
|
+
}
|
|
52
|
+
export type RecoveryCheckpointEntry = RecoveryCheckpointEntryV1 | RecoveryCheckpointEntryV2;
|
|
53
|
+
export interface RecoveryCheckpointManifestV2 {
|
|
54
|
+
version: 2;
|
|
55
|
+
checkpointId: string;
|
|
56
|
+
backend: RecoveryBackend;
|
|
57
|
+
repoRoot: string;
|
|
58
|
+
resourceKind: 'git_repository' | 'directory';
|
|
59
|
+
repoIdentity: string;
|
|
60
|
+
commandFingerprint: string;
|
|
61
|
+
createdAt: string;
|
|
62
|
+
expiresAt: string;
|
|
63
|
+
proof: RecoveryProofV1;
|
|
64
|
+
entries: RecoveryCheckpointEntryV2[];
|
|
65
|
+
}
|
|
66
|
+
export type RecoveryCheckpointManifest = RecoveryCheckpointManifestV1 | RecoveryCheckpointManifestV2;
|
|
67
|
+
export interface RecoveryCheckpointStateV1 {
|
|
68
|
+
version: 1;
|
|
69
|
+
state: RecoveryCheckpointState;
|
|
70
|
+
updatedAt: string;
|
|
71
|
+
manifestHash: string;
|
|
72
|
+
detail?: string;
|
|
73
|
+
}
|
|
74
|
+
export interface RecoveryReceiptV1 {
|
|
75
|
+
version: 1;
|
|
76
|
+
checkpointId: string;
|
|
77
|
+
manifestHash: string;
|
|
78
|
+
proofHash: string;
|
|
79
|
+
postStateHash: string;
|
|
80
|
+
appliedAt: string;
|
|
81
|
+
changeCount: number;
|
|
82
|
+
}
|
|
83
|
+
export interface RecoveryCheckpointSummary {
|
|
84
|
+
checkpointId: string;
|
|
85
|
+
state: RecoveryCheckpointState;
|
|
86
|
+
/** Operator-facing detail from durable checkpoint state (e.g. mixed-state reason). */
|
|
87
|
+
stateDetail?: string;
|
|
88
|
+
backend: RecoveryBackend;
|
|
89
|
+
repoRoot: string;
|
|
90
|
+
commandFingerprint: string;
|
|
91
|
+
createdAt: string;
|
|
92
|
+
expiresAt: string;
|
|
93
|
+
changeCount: number;
|
|
94
|
+
manifestHash: string;
|
|
95
|
+
receiptHash?: string;
|
|
96
|
+
proofHash?: string;
|
|
97
|
+
postStateHash?: string;
|
|
98
|
+
}
|
|
15
99
|
export interface RecoveryProofContext {
|
|
16
100
|
repoRoot: string;
|
|
17
101
|
cwd: string;
|
|
@@ -4,3 +4,4 @@ export { MAX_SUBSTITUTION_DEPTH };
|
|
|
4
4
|
* Finds inner commands for $(...) and backtick substitution, respecting escapes and nesting.
|
|
5
5
|
*/
|
|
6
6
|
export declare function findCommandSubstitutions(command: string): string[];
|
|
7
|
+
export declare function findStructuralCommandSubstitutions(command: string): string[];
|
|
@@ -68,6 +68,67 @@ export function findCommandSubstitutions(command) {
|
|
|
68
68
|
}
|
|
69
69
|
return results;
|
|
70
70
|
}
|
|
71
|
+
export function findStructuralCommandSubstitutions(command) {
|
|
72
|
+
const results = [];
|
|
73
|
+
let index = 0;
|
|
74
|
+
let inSingle = false;
|
|
75
|
+
let inDouble = false;
|
|
76
|
+
let escaping = false;
|
|
77
|
+
while (index < command.length) {
|
|
78
|
+
const char = command[index];
|
|
79
|
+
if (escaping) {
|
|
80
|
+
escaping = false;
|
|
81
|
+
index += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (char === '\\' && !inSingle) {
|
|
85
|
+
escaping = true;
|
|
86
|
+
index += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!inDouble && char === "'") {
|
|
90
|
+
inSingle = !inSingle;
|
|
91
|
+
index += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!inSingle && char === '"') {
|
|
95
|
+
inDouble = !inDouble;
|
|
96
|
+
index += 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (inSingle) {
|
|
100
|
+
index += 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (char === '`') {
|
|
104
|
+
const end = findClosingBacktick(command, index + 1);
|
|
105
|
+
if (end === -1) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
const inner = command.slice(index + 1, end).trim();
|
|
109
|
+
if (inner) {
|
|
110
|
+
results.push(inner);
|
|
111
|
+
}
|
|
112
|
+
index = end + 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (char === '$' && command[index + 1] === '(') {
|
|
116
|
+
const closed = extractBalancedParenContent(command, index + 2);
|
|
117
|
+
if (!closed) {
|
|
118
|
+
index += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const inner = closed.content.trim();
|
|
122
|
+
if (inner) {
|
|
123
|
+
results.push(inner);
|
|
124
|
+
}
|
|
125
|
+
index = closed.endIndex;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
index += 1;
|
|
129
|
+
}
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
71
132
|
function findClosingBacktick(command, start) {
|
|
72
133
|
let index = start;
|
|
73
134
|
while (index < command.length) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BelayConfigV4 } from './config.js';
|
|
2
2
|
import type { GatedActionKind } from './gate-contract.js';
|
|
3
3
|
import type { ClassifyResult } from './types.js';
|
|
4
|
-
export type StandingAllowSource = '
|
|
4
|
+
export type StandingAllowSource = 'operator' | 'availability-reconfirmed';
|
|
5
5
|
export interface StandingAllowEntry {
|
|
6
6
|
kind: GatedActionKind;
|
|
7
7
|
fingerprint: string;
|
|
@@ -18,7 +18,6 @@ export interface StandingAllowFile {
|
|
|
18
18
|
}
|
|
19
19
|
export interface StandingAllowMatch {
|
|
20
20
|
source: StandingAllowSource;
|
|
21
|
-
catalogCommand?: string;
|
|
22
21
|
entryId?: string;
|
|
23
22
|
}
|
|
24
23
|
/** Default TTL for operator / availability-reconfirmed standing-allow entries. */
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { STANDING_ALLOW_CATALOG } from '../corpus/standing-allow-catalog.generated.js';
|
|
5
4
|
import { belayStateDir } from './config.js';
|
|
6
5
|
const EMPTY_STANDING_ALLOW = {
|
|
7
6
|
version: 1,
|
|
@@ -9,8 +8,6 @@ const EMPTY_STANDING_ALLOW = {
|
|
|
9
8
|
};
|
|
10
9
|
/** Default TTL for operator / availability-reconfirmed standing-allow entries. */
|
|
11
10
|
export const DEFAULT_STANDING_ALLOW_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
12
|
-
const PROVABLY_BENIGN_COMMANDS = new Set(STANDING_ALLOW_CATALOG.shell.provablyBenign.map((entry) => entry.normalizedCommand));
|
|
13
|
-
const MUST_ALLOW_COMMANDS = new Set(STANDING_ALLOW_CATALOG.shell.mustAllow.map((entry) => entry.normalizedCommand));
|
|
14
11
|
/** Reasons that must never be silenced via standing-allow (defense in depth with signal checks). */
|
|
15
12
|
const STANDING_ALLOW_BLOCKED_REASONS = new Set([
|
|
16
13
|
'external_effect',
|
|
@@ -95,18 +92,6 @@ export async function saveStandingAllow(filePath, state) {
|
|
|
95
92
|
const compacted = compactStandingAllow(state);
|
|
96
93
|
await writeFile(filePath, `${JSON.stringify(compacted, null, 2)}\n`, 'utf8');
|
|
97
94
|
}
|
|
98
|
-
function matchBundledCatalog(kind, normalizedCommand) {
|
|
99
|
-
if (kind !== 'shell' || !normalizedCommand) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
if (PROVABLY_BENIGN_COMMANDS.has(normalizedCommand)) {
|
|
103
|
-
return { source: 'provably-benign-corpus', catalogCommand: normalizedCommand };
|
|
104
|
-
}
|
|
105
|
-
if (MUST_ALLOW_COMMANDS.has(normalizedCommand)) {
|
|
106
|
-
return { source: 'must-allow-catalog', catalogCommand: normalizedCommand };
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
95
|
function matchStateEntry(params) {
|
|
111
96
|
const now = params.now ?? Date.now();
|
|
112
97
|
const match = params.state.entries.find((entry) => entry.kind === params.kind &&
|
|
@@ -119,17 +104,15 @@ function matchStateEntry(params) {
|
|
|
119
104
|
return { source: match.source, entryId: match.fingerprint };
|
|
120
105
|
}
|
|
121
106
|
export function resolveStandingAllowMatch(params) {
|
|
107
|
+
if (params.kind === 'shell') {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
122
110
|
if (params.result.verdict !== 'deny_pending_approval') {
|
|
123
111
|
return null;
|
|
124
112
|
}
|
|
125
113
|
if (isTier0StandingAllowBlocked(params.result)) {
|
|
126
114
|
return null;
|
|
127
115
|
}
|
|
128
|
-
const normalizedCommand = params.result.normalizedCommand ?? params.result.summary ?? '';
|
|
129
|
-
const catalogMatch = matchBundledCatalog(params.kind, normalizedCommand);
|
|
130
|
-
if (catalogMatch) {
|
|
131
|
-
return catalogMatch;
|
|
132
|
-
}
|
|
133
116
|
return matchStateEntry({
|
|
134
117
|
kind: params.kind,
|
|
135
118
|
fingerprint: params.result.fingerprint,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ObservedFileChange } from './file-tree.js';
|
|
2
|
+
import type { TransactionalFileChange } from './types.js';
|
|
3
|
+
export declare const TRANSACTIONAL_APPLY_TOCTOU = "transactional_apply_toctou";
|
|
4
|
+
export declare const TRANSACTIONAL_APPLY_CONFLICT = "transactional_apply_conflict";
|
|
5
|
+
export declare const TRANSACTIONAL_APPLY_ROLLBACK_FAILED = "transactional_apply_rollback_failed";
|
|
6
|
+
export interface ApplyObservedChangesParams {
|
|
7
|
+
sourceRoot: string;
|
|
8
|
+
targetRoot: string;
|
|
9
|
+
changes: ObservedFileChange[];
|
|
10
|
+
afterApply?: () => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export declare function applyObservedChanges(params: ApplyObservedChangesParams): Promise<void>;
|
|
13
|
+
export declare function buildObservedChangesFromTransactional(baselineRoot: string, executionRoot: string, changes: TransactionalFileChange[]): Promise<ObservedFileChange[]>;
|