veto-leash 0.1.1 → 0.1.2
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/dist/cli.js +26 -5
- package/dist/cli.js.map +1 -1
- package/dist/watchdog/index.d.ts +1 -1
- package/dist/watchdog/index.d.ts.map +1 -1
- package/dist/watchdog/index.js +6 -1
- package/dist/watchdog/index.js.map +1 -1
- package/dist/wrapper/daemon.d.ts +3 -1
- package/dist/wrapper/daemon.d.ts.map +1 -1
- package/dist/wrapper/daemon.js +9 -1
- package/dist/wrapper/daemon.js.map +1 -1
- package/dist/wrapper/sessions.d.ts +29 -0
- package/dist/wrapper/sessions.d.ts.map +1 -0
- package/dist/wrapper/sessions.js +101 -0
- package/dist/wrapper/sessions.js.map +1 -0
- package/package.json +11 -3
- package/IMPLEMENTATION_PLAN.md +0 -2194
- package/src/audit/index.ts +0 -172
- package/src/cli.ts +0 -503
- package/src/cloud/index.ts +0 -139
- package/src/compiler/builtins.ts +0 -137
- package/src/compiler/cache.ts +0 -51
- package/src/compiler/index.ts +0 -59
- package/src/compiler/llm.ts +0 -83
- package/src/compiler/prompt.ts +0 -37
- package/src/config/loader.ts +0 -126
- package/src/config/schema.ts +0 -136
- package/src/matcher.ts +0 -89
- package/src/native/aider.ts +0 -150
- package/src/native/claude-code.ts +0 -308
- package/src/native/cursor.ts +0 -131
- package/src/native/index.ts +0 -233
- package/src/native/opencode.ts +0 -310
- package/src/native/windsurf.ts +0 -231
- package/src/types.ts +0 -48
- package/src/ui/colors.ts +0 -50
- package/src/watchdog/index.ts +0 -82
- package/src/watchdog/restore.ts +0 -74
- package/src/watchdog/snapshot.ts +0 -209
- package/src/watchdog/watcher.ts +0 -150
- package/src/wrapper/daemon.ts +0 -133
- package/src/wrapper/shims.ts +0 -409
- package/src/wrapper/spawn.ts +0 -47
- package/tsconfig.json +0 -20
package/src/native/windsurf.ts
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
// src/native/windsurf.ts
|
|
2
|
-
// Windsurf native hook integration (Cascade Hooks)
|
|
3
|
-
// Very similar to Claude Code - supports pre_write_code, pre_run_command hooks
|
|
4
|
-
|
|
5
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
6
|
-
import { join } from 'path';
|
|
7
|
-
import { homedir } from 'os';
|
|
8
|
-
import type { Policy } from '../types.js';
|
|
9
|
-
import { COLORS, SYMBOLS } from '../ui/colors.js';
|
|
10
|
-
|
|
11
|
-
const WINDSURF_USER_CONFIG = join(homedir(), '.codeium', 'windsurf', 'hooks.json');
|
|
12
|
-
const WINDSURF_WORKSPACE_CONFIG = '.windsurf/hooks.json';
|
|
13
|
-
const VETO_SCRIPTS_DIR = join(homedir(), '.codeium', 'windsurf', 'veto-leash');
|
|
14
|
-
|
|
15
|
-
interface WindsurfHooksConfig {
|
|
16
|
-
hooks?: {
|
|
17
|
-
pre_write_code?: Array<{ command: string; show_output?: boolean }>;
|
|
18
|
-
pre_run_command?: Array<{ command: string; show_output?: boolean }>;
|
|
19
|
-
pre_read_code?: Array<{ command: string; show_output?: boolean }>;
|
|
20
|
-
[key: string]: unknown;
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Install veto-leash as Windsurf Cascade hooks
|
|
26
|
-
*/
|
|
27
|
-
export async function installWindsurfHooks(
|
|
28
|
-
target: 'user' | 'workspace' = 'user'
|
|
29
|
-
): Promise<void> {
|
|
30
|
-
console.log(`\n${COLORS.info}Installing veto-leash for Windsurf (${target})...${COLORS.reset}\n`);
|
|
31
|
-
|
|
32
|
-
// Create scripts directory and validator
|
|
33
|
-
mkdirSync(VETO_SCRIPTS_DIR, { recursive: true });
|
|
34
|
-
mkdirSync(join(VETO_SCRIPTS_DIR, 'policies'), { recursive: true });
|
|
35
|
-
|
|
36
|
-
const validatorPath = join(VETO_SCRIPTS_DIR, 'validator.py');
|
|
37
|
-
writeFileSync(validatorPath, VALIDATOR_SCRIPT, { mode: 0o755 });
|
|
38
|
-
console.log(` ${COLORS.success}${SYMBOLS.success}${COLORS.reset} Created validator: ${validatorPath}`);
|
|
39
|
-
|
|
40
|
-
// Determine config path
|
|
41
|
-
const configPath = target === 'user' ? WINDSURF_USER_CONFIG : WINDSURF_WORKSPACE_CONFIG;
|
|
42
|
-
const configDir = target === 'user'
|
|
43
|
-
? join(homedir(), '.codeium', 'windsurf')
|
|
44
|
-
: '.windsurf';
|
|
45
|
-
|
|
46
|
-
// Load existing config
|
|
47
|
-
let config: WindsurfHooksConfig = { hooks: {} };
|
|
48
|
-
if (existsSync(configPath)) {
|
|
49
|
-
try {
|
|
50
|
-
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
51
|
-
} catch {
|
|
52
|
-
// Start fresh
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (!config.hooks) config.hooks = {};
|
|
57
|
-
|
|
58
|
-
// Add veto-leash hooks
|
|
59
|
-
const hookCommand = `python3 "${validatorPath}"`;
|
|
60
|
-
|
|
61
|
-
// Helper to add hook if not exists
|
|
62
|
-
const addHook = (hookName: string) => {
|
|
63
|
-
if (!config.hooks![hookName]) {
|
|
64
|
-
config.hooks![hookName] = [];
|
|
65
|
-
}
|
|
66
|
-
const hooks = config.hooks![hookName] as Array<{ command: string; show_output?: boolean }>;
|
|
67
|
-
const exists = hooks.some(h => h.command.includes('veto-leash'));
|
|
68
|
-
if (!exists) {
|
|
69
|
-
hooks.unshift({ command: hookCommand, show_output: true });
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
addHook('pre_write_code');
|
|
74
|
-
addHook('pre_run_command');
|
|
75
|
-
|
|
76
|
-
// Write config
|
|
77
|
-
mkdirSync(configDir, { recursive: true });
|
|
78
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
79
|
-
console.log(` ${COLORS.success}${SYMBOLS.success}${COLORS.reset} Updated: ${configPath}`);
|
|
80
|
-
|
|
81
|
-
console.log(`\n${COLORS.success}${SYMBOLS.success} veto-leash installed for Windsurf${COLORS.reset}\n`);
|
|
82
|
-
console.log(`Add policies with: ${COLORS.dim}leash add "don't delete test files"${COLORS.reset}\n`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Add a policy for Windsurf
|
|
87
|
-
*/
|
|
88
|
-
export async function addWindsurfPolicy(policy: Policy, name: string): Promise<void> {
|
|
89
|
-
const policiesDir = join(VETO_SCRIPTS_DIR, 'policies');
|
|
90
|
-
mkdirSync(policiesDir, { recursive: true });
|
|
91
|
-
|
|
92
|
-
const policyFile = join(policiesDir, `${name}.json`);
|
|
93
|
-
writeFileSync(policyFile, JSON.stringify(policy, null, 2));
|
|
94
|
-
console.log(`${COLORS.success}${SYMBOLS.success}${COLORS.reset} Windsurf policy saved: ${policyFile}`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Uninstall veto-leash from Windsurf
|
|
99
|
-
*/
|
|
100
|
-
export async function uninstallWindsurfHooks(
|
|
101
|
-
target: 'user' | 'workspace' = 'user'
|
|
102
|
-
): Promise<void> {
|
|
103
|
-
const configPath = target === 'user' ? WINDSURF_USER_CONFIG : WINDSURF_WORKSPACE_CONFIG;
|
|
104
|
-
|
|
105
|
-
if (!existsSync(configPath)) {
|
|
106
|
-
console.log(`${COLORS.dim}No Windsurf config found at ${configPath}${COLORS.reset}`);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
const config: WindsurfHooksConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
112
|
-
|
|
113
|
-
if (config.hooks) {
|
|
114
|
-
for (const hookName of Object.keys(config.hooks)) {
|
|
115
|
-
const hooks = config.hooks[hookName];
|
|
116
|
-
if (Array.isArray(hooks)) {
|
|
117
|
-
config.hooks[hookName] = hooks.filter(
|
|
118
|
-
(h: { command: string }) => !h.command.includes('veto-leash')
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
125
|
-
console.log(`${COLORS.success}${SYMBOLS.success} Removed veto-leash from Windsurf${COLORS.reset}`);
|
|
126
|
-
} catch {
|
|
127
|
-
console.log(`${COLORS.warning}${SYMBOLS.warning} Could not parse Windsurf config${COLORS.reset}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Python validator script for Windsurf Cascade hooks.
|
|
133
|
-
* Handles pre_write_code and pre_run_command events.
|
|
134
|
-
*/
|
|
135
|
-
const VALIDATOR_SCRIPT = `#!/usr/bin/env python3
|
|
136
|
-
"""
|
|
137
|
-
veto-leash validator for Windsurf Cascade hooks.
|
|
138
|
-
Exit code 2 blocks the action.
|
|
139
|
-
"""
|
|
140
|
-
|
|
141
|
-
import json
|
|
142
|
-
import sys
|
|
143
|
-
import os
|
|
144
|
-
import re
|
|
145
|
-
from pathlib import Path
|
|
146
|
-
from fnmatch import fnmatch
|
|
147
|
-
|
|
148
|
-
POLICIES_DIR = Path(__file__).parent / "policies"
|
|
149
|
-
|
|
150
|
-
def load_policies():
|
|
151
|
-
policies = []
|
|
152
|
-
if POLICIES_DIR.exists():
|
|
153
|
-
for f in POLICIES_DIR.glob("*.json"):
|
|
154
|
-
try:
|
|
155
|
-
policies.append(json.loads(f.read_text()))
|
|
156
|
-
except:
|
|
157
|
-
pass
|
|
158
|
-
return policies
|
|
159
|
-
|
|
160
|
-
def normalize_path(p):
|
|
161
|
-
return str(p).replace("\\\\", "/")
|
|
162
|
-
|
|
163
|
-
def matches_pattern(target, pattern):
|
|
164
|
-
target = normalize_path(target)
|
|
165
|
-
pattern = normalize_path(pattern)
|
|
166
|
-
basename = os.path.basename(target)
|
|
167
|
-
return fnmatch(target, pattern) or fnmatch(basename, pattern)
|
|
168
|
-
|
|
169
|
-
def is_protected(target, policy):
|
|
170
|
-
matches_include = any(matches_pattern(target, p) for p in policy.get("include", []))
|
|
171
|
-
if not matches_include:
|
|
172
|
-
return False
|
|
173
|
-
matches_exclude = any(matches_pattern(target, p) for p in policy.get("exclude", []))
|
|
174
|
-
return not matches_exclude
|
|
175
|
-
|
|
176
|
-
def parse_command_targets(command, action):
|
|
177
|
-
targets = []
|
|
178
|
-
if action == "delete":
|
|
179
|
-
rm_match = re.search(r'\\brm\\s+(?:-[rfiv]+\\s+)*(.+)', command)
|
|
180
|
-
if rm_match:
|
|
181
|
-
for arg in rm_match.group(1).split():
|
|
182
|
-
if not arg.startswith('-'):
|
|
183
|
-
targets.append(arg)
|
|
184
|
-
git_rm = re.search(r'\\bgit\\s+rm\\s+(?:-[rf]+\\s+)*(.+)', command)
|
|
185
|
-
if git_rm:
|
|
186
|
-
for arg in git_rm.group(1).split():
|
|
187
|
-
if not arg.startswith('-'):
|
|
188
|
-
targets.append(arg)
|
|
189
|
-
return targets
|
|
190
|
-
|
|
191
|
-
def main():
|
|
192
|
-
try:
|
|
193
|
-
input_data = json.load(sys.stdin)
|
|
194
|
-
except:
|
|
195
|
-
sys.exit(0)
|
|
196
|
-
|
|
197
|
-
action_name = input_data.get("agent_action_name", "")
|
|
198
|
-
tool_info = input_data.get("tool_info", {})
|
|
199
|
-
policies = load_policies()
|
|
200
|
-
|
|
201
|
-
if not policies:
|
|
202
|
-
sys.exit(0)
|
|
203
|
-
|
|
204
|
-
targets = []
|
|
205
|
-
|
|
206
|
-
if action_name == "pre_write_code":
|
|
207
|
-
file_path = tool_info.get("file_path", "")
|
|
208
|
-
if file_path:
|
|
209
|
-
targets.append(file_path)
|
|
210
|
-
|
|
211
|
-
elif action_name == "pre_run_command":
|
|
212
|
-
command = tool_info.get("command_line", "")
|
|
213
|
-
for policy in policies:
|
|
214
|
-
action = policy.get("action", "modify")
|
|
215
|
-
targets.extend(parse_command_targets(command, action))
|
|
216
|
-
|
|
217
|
-
for target in targets:
|
|
218
|
-
for policy in policies:
|
|
219
|
-
if is_protected(target, policy):
|
|
220
|
-
desc = policy.get("description", "Protected file")
|
|
221
|
-
print(f"veto-leash: BLOCKED", file=sys.stderr)
|
|
222
|
-
print(f" Target: {target}", file=sys.stderr)
|
|
223
|
-
print(f" Policy: {desc}", file=sys.stderr)
|
|
224
|
-
print(f" Filesystem unchanged.", file=sys.stderr)
|
|
225
|
-
sys.exit(2)
|
|
226
|
-
|
|
227
|
-
sys.exit(0)
|
|
228
|
-
|
|
229
|
-
if __name__ == "__main__":
|
|
230
|
-
main()
|
|
231
|
-
`;
|
package/src/types.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// src/types.ts
|
|
2
|
-
|
|
3
|
-
export interface Policy {
|
|
4
|
-
action: 'delete' | 'modify' | 'execute' | 'read';
|
|
5
|
-
include: string[];
|
|
6
|
-
exclude: string[];
|
|
7
|
-
description: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface CheckRequest {
|
|
11
|
-
action: string;
|
|
12
|
-
target: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface CheckResponse {
|
|
16
|
-
allowed: boolean;
|
|
17
|
-
reason?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface SessionState {
|
|
21
|
-
pid: number;
|
|
22
|
-
agent: string;
|
|
23
|
-
policy: Policy;
|
|
24
|
-
startTime: Date;
|
|
25
|
-
blockedCount: number;
|
|
26
|
-
allowedCount: number;
|
|
27
|
-
blockedActions: Array<{ time: Date; action: string; target: string }>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface Config {
|
|
31
|
-
failClosed: boolean;
|
|
32
|
-
fallbackToBuiltins: boolean;
|
|
33
|
-
warnBroadPatterns: boolean;
|
|
34
|
-
maxSnapshotFiles: number;
|
|
35
|
-
maxMemoryCacheSize: number;
|
|
36
|
-
auditLog: boolean;
|
|
37
|
-
verbose: boolean;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export const DEFAULT_CONFIG: Config = {
|
|
41
|
-
failClosed: true,
|
|
42
|
-
fallbackToBuiltins: true,
|
|
43
|
-
warnBroadPatterns: true,
|
|
44
|
-
maxSnapshotFiles: 10000,
|
|
45
|
-
maxMemoryCacheSize: 100 * 1024,
|
|
46
|
-
auditLog: false,
|
|
47
|
-
verbose: false,
|
|
48
|
-
};
|
package/src/ui/colors.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// src/ui/colors.ts
|
|
2
|
-
|
|
3
|
-
const isTTY = process.stdout.isTTY && process.stderr.isTTY;
|
|
4
|
-
const noColor = process.env.NO_COLOR !== undefined || process.env.TERM === 'dumb';
|
|
5
|
-
|
|
6
|
-
function color(code: string): string {
|
|
7
|
-
return isTTY && !noColor ? code : '';
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const COLORS = {
|
|
11
|
-
success: color('\x1b[32m'),
|
|
12
|
-
error: color('\x1b[31m'),
|
|
13
|
-
warning: color('\x1b[33m'),
|
|
14
|
-
info: color('\x1b[36m'),
|
|
15
|
-
dim: color('\x1b[90m'),
|
|
16
|
-
bold: color('\x1b[1m'),
|
|
17
|
-
reset: color('\x1b[0m'),
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const SYMBOLS = {
|
|
21
|
-
success: '\u2713',
|
|
22
|
-
error: '\u2717',
|
|
23
|
-
blocked: '\u26D4',
|
|
24
|
-
warning: '\u26A0',
|
|
25
|
-
arrow: '\u2192',
|
|
26
|
-
bullet: '\u2022',
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const SPINNER_FRAMES = ['\u25D0', '\u25D3', '\u25D1', '\u25D2'];
|
|
30
|
-
|
|
31
|
-
export function createSpinner(message: string): { stop: () => void } {
|
|
32
|
-
if (!isTTY) {
|
|
33
|
-
console.log(message);
|
|
34
|
-
return { stop: () => {} };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
let i = 0;
|
|
38
|
-
const interval = setInterval(() => {
|
|
39
|
-
process.stdout.write(
|
|
40
|
-
`\r${COLORS.dim}${SPINNER_FRAMES[i++ % 4]} ${message}${COLORS.reset}`
|
|
41
|
-
);
|
|
42
|
-
}, 100);
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
stop: () => {
|
|
46
|
-
clearInterval(interval);
|
|
47
|
-
process.stdout.write('\r\x1b[K'); // Clear line
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
package/src/watchdog/index.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// src/watchdog/index.ts
|
|
2
|
-
// Watchdog mode orchestrator
|
|
3
|
-
|
|
4
|
-
import type { FSWatcher } from 'chokidar';
|
|
5
|
-
import type { Policy } from '../types.js';
|
|
6
|
-
import { createSnapshot, cleanupSnapshot, generateSessionId, type Snapshot } from './snapshot.js';
|
|
7
|
-
import { createWatcher, type WatcherStats } from './watcher.js';
|
|
8
|
-
import { COLORS, SYMBOLS } from '../ui/colors.js';
|
|
9
|
-
|
|
10
|
-
export interface WatchdogSession {
|
|
11
|
-
sessionId: string;
|
|
12
|
-
rootDir: string;
|
|
13
|
-
policy: Policy;
|
|
14
|
-
snapshot: Snapshot;
|
|
15
|
-
watcher: FSWatcher;
|
|
16
|
-
stats: WatcherStats;
|
|
17
|
-
startTime: Date;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Start watchdog mode - monitors and auto-restores protected files
|
|
22
|
-
*/
|
|
23
|
-
export async function startWatchdog(
|
|
24
|
-
rootDir: string,
|
|
25
|
-
policy: Policy
|
|
26
|
-
): Promise<WatchdogSession> {
|
|
27
|
-
const sessionId = generateSessionId();
|
|
28
|
-
const startTime = new Date();
|
|
29
|
-
|
|
30
|
-
// Create snapshot of protected files
|
|
31
|
-
const snapshot = await createSnapshot(rootDir, policy, sessionId);
|
|
32
|
-
|
|
33
|
-
// Start filesystem watcher
|
|
34
|
-
const { watcher, stats } = createWatcher({
|
|
35
|
-
rootDir,
|
|
36
|
-
policy,
|
|
37
|
-
snapshot,
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
sessionId,
|
|
42
|
-
rootDir,
|
|
43
|
-
policy,
|
|
44
|
-
snapshot,
|
|
45
|
-
watcher,
|
|
46
|
-
stats,
|
|
47
|
-
startTime,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Stop watchdog mode and print summary
|
|
53
|
-
*/
|
|
54
|
-
export async function stopWatchdog(session: WatchdogSession): Promise<void> {
|
|
55
|
-
const duration = Date.now() - session.startTime.getTime();
|
|
56
|
-
const minutes = Math.floor(duration / 60000);
|
|
57
|
-
const seconds = Math.floor((duration % 60000) / 1000);
|
|
58
|
-
|
|
59
|
-
// Close watcher
|
|
60
|
-
await session.watcher.close();
|
|
61
|
-
|
|
62
|
-
// Print summary
|
|
63
|
-
console.log(`\n${COLORS.success}${SYMBOLS.success} veto-leash watchdog ended${COLORS.reset}\n`);
|
|
64
|
-
console.log(` Duration: ${minutes}m ${seconds}s`);
|
|
65
|
-
console.log(` Files protected: ${session.snapshot.files.size}`);
|
|
66
|
-
console.log(` Auto-restored: ${session.stats.restored}`);
|
|
67
|
-
|
|
68
|
-
if (session.stats.events.length > 0) {
|
|
69
|
-
console.log(`\n Recent events:`);
|
|
70
|
-
for (const event of session.stats.events.slice(-5)) {
|
|
71
|
-
console.log(` ${SYMBOLS.bullet} ${event.action} ${event.path}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
console.log('');
|
|
75
|
-
|
|
76
|
-
// Cleanup snapshot (optional - keep for debugging)
|
|
77
|
-
// cleanupSnapshot(session.sessionId);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export { createSnapshot, cleanupSnapshot, generateSessionId } from './snapshot.js';
|
|
81
|
-
export { restoreFile, restoreAll } from './restore.js';
|
|
82
|
-
export { createWatcher } from './watcher.js';
|
package/src/watchdog/restore.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// src/watchdog/restore.ts
|
|
2
|
-
// File restoration from snapshots
|
|
3
|
-
|
|
4
|
-
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
5
|
-
import { join, dirname } from 'path';
|
|
6
|
-
import type { Snapshot } from './snapshot.js';
|
|
7
|
-
import { getSnapshotContent } from './snapshot.js';
|
|
8
|
-
import { normalizePath } from '../matcher.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Restore a file from snapshot
|
|
12
|
-
* Returns true if restored, false if no snapshot exists
|
|
13
|
-
*/
|
|
14
|
-
export function restoreFile(
|
|
15
|
-
snapshot: Snapshot,
|
|
16
|
-
filePath: string,
|
|
17
|
-
rootDir: string
|
|
18
|
-
): boolean {
|
|
19
|
-
const normalizedPath = normalizePath(filePath);
|
|
20
|
-
const content = getSnapshotContent(snapshot, normalizedPath);
|
|
21
|
-
|
|
22
|
-
if (!content) return false;
|
|
23
|
-
|
|
24
|
-
const fullPath = join(rootDir, normalizedPath);
|
|
25
|
-
const dir = dirname(fullPath);
|
|
26
|
-
|
|
27
|
-
// Ensure directory exists
|
|
28
|
-
if (!existsSync(dir)) {
|
|
29
|
-
mkdirSync(dir, { recursive: true });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Write restored content
|
|
33
|
-
writeFileSync(fullPath, content);
|
|
34
|
-
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Restore all files from snapshot
|
|
40
|
-
* Returns count of restored files
|
|
41
|
-
*/
|
|
42
|
-
export function restoreAll(snapshot: Snapshot, rootDir: string): number {
|
|
43
|
-
let restored = 0;
|
|
44
|
-
|
|
45
|
-
for (const [filePath] of snapshot.files) {
|
|
46
|
-
const fullPath = join(rootDir, filePath);
|
|
47
|
-
|
|
48
|
-
// Only restore if file is missing or different
|
|
49
|
-
if (!existsSync(fullPath)) {
|
|
50
|
-
if (restoreFile(snapshot, filePath, rootDir)) {
|
|
51
|
-
restored++;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return restored;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Check what files would be restored
|
|
61
|
-
*/
|
|
62
|
-
export function previewRestore(snapshot: Snapshot, rootDir: string): string[] {
|
|
63
|
-
const toRestore: string[] = [];
|
|
64
|
-
|
|
65
|
-
for (const [filePath] of snapshot.files) {
|
|
66
|
-
const fullPath = join(rootDir, filePath);
|
|
67
|
-
|
|
68
|
-
if (!existsSync(fullPath)) {
|
|
69
|
-
toRestore.push(filePath);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return toRestore;
|
|
74
|
-
}
|
package/src/watchdog/snapshot.ts
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
// src/watchdog/snapshot.ts
|
|
2
|
-
// File snapshot system for watchdog mode
|
|
3
|
-
|
|
4
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, statSync, unlinkSync } from 'fs';
|
|
5
|
-
import { join, dirname, relative } from 'path';
|
|
6
|
-
import { homedir } from 'os';
|
|
7
|
-
import { createHash } from 'crypto';
|
|
8
|
-
import { glob } from 'glob';
|
|
9
|
-
import type { Policy } from '../types.js';
|
|
10
|
-
import { isProtected, normalizePath } from '../matcher.js';
|
|
11
|
-
|
|
12
|
-
const SNAPSHOT_DIR = join(homedir(), '.config', 'veto-leash', 'snapshots');
|
|
13
|
-
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB max per file
|
|
14
|
-
const MAX_TOTAL_SIZE = 100 * 1024 * 1024; // 100MB max total
|
|
15
|
-
|
|
16
|
-
export interface Snapshot {
|
|
17
|
-
sessionId: string;
|
|
18
|
-
rootDir: string;
|
|
19
|
-
files: Map<string, SnapshotEntry>;
|
|
20
|
-
createdAt: Date;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface SnapshotEntry {
|
|
24
|
-
path: string;
|
|
25
|
-
hash: string;
|
|
26
|
-
size: number;
|
|
27
|
-
snapshotPath: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Create a snapshot of all files matching the policy
|
|
32
|
-
*/
|
|
33
|
-
export async function createSnapshot(
|
|
34
|
-
rootDir: string,
|
|
35
|
-
policy: Policy,
|
|
36
|
-
sessionId: string
|
|
37
|
-
): Promise<Snapshot> {
|
|
38
|
-
const sessionDir = join(SNAPSHOT_DIR, sessionId);
|
|
39
|
-
mkdirSync(sessionDir, { recursive: true });
|
|
40
|
-
|
|
41
|
-
const snapshot: Snapshot = {
|
|
42
|
-
sessionId,
|
|
43
|
-
rootDir: normalizePath(rootDir),
|
|
44
|
-
files: new Map(),
|
|
45
|
-
createdAt: new Date(),
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// Find all files matching include patterns
|
|
49
|
-
const matchedFiles = await findMatchingFiles(rootDir, policy);
|
|
50
|
-
|
|
51
|
-
let totalSize = 0;
|
|
52
|
-
|
|
53
|
-
for (const filePath of matchedFiles) {
|
|
54
|
-
const fullPath = join(rootDir, filePath);
|
|
55
|
-
|
|
56
|
-
if (!existsSync(fullPath)) continue;
|
|
57
|
-
|
|
58
|
-
const stat = statSync(fullPath);
|
|
59
|
-
if (!stat.isFile()) continue;
|
|
60
|
-
if (stat.size > MAX_FILE_SIZE) continue;
|
|
61
|
-
if (totalSize + stat.size > MAX_TOTAL_SIZE) break;
|
|
62
|
-
|
|
63
|
-
const content = readFileSync(fullPath);
|
|
64
|
-
const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
65
|
-
|
|
66
|
-
// Store with hash prefix for deduplication
|
|
67
|
-
const snapshotPath = join(sessionDir, hash);
|
|
68
|
-
if (!existsSync(snapshotPath)) {
|
|
69
|
-
writeFileSync(snapshotPath, content);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
snapshot.files.set(filePath, {
|
|
73
|
-
path: filePath,
|
|
74
|
-
hash,
|
|
75
|
-
size: stat.size,
|
|
76
|
-
snapshotPath,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
totalSize += stat.size;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Write manifest
|
|
83
|
-
const manifest = {
|
|
84
|
-
sessionId,
|
|
85
|
-
rootDir: snapshot.rootDir,
|
|
86
|
-
createdAt: snapshot.createdAt.toISOString(),
|
|
87
|
-
files: Array.from(snapshot.files.entries()).map(([path, entry]) => ({
|
|
88
|
-
path,
|
|
89
|
-
hash: entry.hash,
|
|
90
|
-
size: entry.size,
|
|
91
|
-
})),
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
writeFileSync(join(sessionDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
95
|
-
|
|
96
|
-
return snapshot;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Find all files matching the policy's include patterns
|
|
101
|
-
*/
|
|
102
|
-
async function findMatchingFiles(rootDir: string, policy: Policy): Promise<string[]> {
|
|
103
|
-
const allFiles: string[] = [];
|
|
104
|
-
|
|
105
|
-
for (const pattern of policy.include) {
|
|
106
|
-
const matches = await glob(pattern, {
|
|
107
|
-
cwd: rootDir,
|
|
108
|
-
dot: true,
|
|
109
|
-
nodir: true,
|
|
110
|
-
ignore: ['node_modules/**', '.git/**'],
|
|
111
|
-
});
|
|
112
|
-
allFiles.push(...matches);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Dedupe and filter by policy
|
|
116
|
-
const unique = [...new Set(allFiles)];
|
|
117
|
-
return unique.filter(f => isProtected(f, policy));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Get the snapshot content for a file
|
|
122
|
-
*/
|
|
123
|
-
export function getSnapshotContent(snapshot: Snapshot, filePath: string): Buffer | null {
|
|
124
|
-
const entry = snapshot.files.get(normalizePath(filePath));
|
|
125
|
-
if (!entry) return null;
|
|
126
|
-
|
|
127
|
-
if (!existsSync(entry.snapshotPath)) return null;
|
|
128
|
-
|
|
129
|
-
return readFileSync(entry.snapshotPath);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Check if a file has been modified since snapshot
|
|
134
|
-
*/
|
|
135
|
-
export function hasChanged(snapshot: Snapshot, filePath: string, rootDir: string): boolean {
|
|
136
|
-
const normalizedPath = normalizePath(filePath);
|
|
137
|
-
const entry = snapshot.files.get(normalizedPath);
|
|
138
|
-
|
|
139
|
-
if (!entry) return false; // Not in snapshot, can't detect change
|
|
140
|
-
|
|
141
|
-
const fullPath = join(rootDir, normalizedPath);
|
|
142
|
-
|
|
143
|
-
if (!existsSync(fullPath)) return true; // Deleted
|
|
144
|
-
|
|
145
|
-
const content = readFileSync(fullPath);
|
|
146
|
-
const currentHash = createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
147
|
-
|
|
148
|
-
return currentHash !== entry.hash;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Load an existing snapshot from disk
|
|
153
|
-
*/
|
|
154
|
-
export function loadSnapshot(sessionId: string): Snapshot | null {
|
|
155
|
-
const sessionDir = join(SNAPSHOT_DIR, sessionId);
|
|
156
|
-
const manifestPath = join(sessionDir, 'manifest.json');
|
|
157
|
-
|
|
158
|
-
if (!existsSync(manifestPath)) return null;
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
162
|
-
const snapshot: Snapshot = {
|
|
163
|
-
sessionId: manifest.sessionId,
|
|
164
|
-
rootDir: manifest.rootDir,
|
|
165
|
-
files: new Map(),
|
|
166
|
-
createdAt: new Date(manifest.createdAt),
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
for (const file of manifest.files) {
|
|
170
|
-
snapshot.files.set(file.path, {
|
|
171
|
-
path: file.path,
|
|
172
|
-
hash: file.hash,
|
|
173
|
-
size: file.size,
|
|
174
|
-
snapshotPath: join(sessionDir, file.hash),
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return snapshot;
|
|
179
|
-
} catch {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Clean up a snapshot session
|
|
186
|
-
*/
|
|
187
|
-
export function cleanupSnapshot(sessionId: string): void {
|
|
188
|
-
const sessionDir = join(SNAPSHOT_DIR, sessionId);
|
|
189
|
-
|
|
190
|
-
if (!existsSync(sessionDir)) return;
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
const files = readdirSync(sessionDir);
|
|
194
|
-
for (const file of files) {
|
|
195
|
-
unlinkSync(join(sessionDir, file));
|
|
196
|
-
}
|
|
197
|
-
// Remove directory
|
|
198
|
-
require('fs').rmdirSync(sessionDir);
|
|
199
|
-
} catch {
|
|
200
|
-
// Ignore cleanup errors
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Generate a unique session ID
|
|
206
|
-
*/
|
|
207
|
-
export function generateSessionId(): string {
|
|
208
|
-
return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
209
|
-
}
|