veto-leash 0.1.0 → 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 +27 -6
- 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/watchdog/watcher.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
// src/watchdog/watcher.ts
|
|
2
|
-
// Filesystem watcher using chokidar
|
|
3
|
-
|
|
4
|
-
import chokidar from 'chokidar';
|
|
5
|
-
import type { FSWatcher } from 'chokidar';
|
|
6
|
-
import type { Policy } from '../types.js';
|
|
7
|
-
import type { Snapshot } from './snapshot.js';
|
|
8
|
-
import { isProtected, normalizePath } from '../matcher.js';
|
|
9
|
-
import { restoreFile } from './restore.js';
|
|
10
|
-
import { COLORS, SYMBOLS } from '../ui/colors.js';
|
|
11
|
-
import { logRestored } from '../audit/index.js';
|
|
12
|
-
|
|
13
|
-
export interface WatcherStats {
|
|
14
|
-
restored: number;
|
|
15
|
-
blocked: number;
|
|
16
|
-
events: Array<{ time: Date; event: string; path: string; action: 'restored' | 'blocked' }>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface WatcherOptions {
|
|
20
|
-
rootDir: string;
|
|
21
|
-
policy: Policy;
|
|
22
|
-
snapshot: Snapshot;
|
|
23
|
-
onRestore?: (path: string) => void;
|
|
24
|
-
onBlock?: (path: string, event: string) => void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Create a filesystem watcher that auto-restores protected files
|
|
29
|
-
*/
|
|
30
|
-
export function createWatcher(options: WatcherOptions): { watcher: FSWatcher; stats: WatcherStats } {
|
|
31
|
-
const { rootDir, policy, snapshot, onRestore, onBlock } = options;
|
|
32
|
-
|
|
33
|
-
const stats: WatcherStats = {
|
|
34
|
-
restored: 0,
|
|
35
|
-
blocked: 0,
|
|
36
|
-
events: [],
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// Build glob patterns for chokidar
|
|
40
|
-
const watchPatterns = policy.include.map(p => {
|
|
41
|
-
// Ensure patterns work with chokidar
|
|
42
|
-
if (p.startsWith('**/')) return p;
|
|
43
|
-
if (p.startsWith('/')) return p.slice(1);
|
|
44
|
-
return p;
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const watcher = chokidar.watch(watchPatterns, {
|
|
48
|
-
cwd: rootDir,
|
|
49
|
-
ignored: [
|
|
50
|
-
'node_modules/**',
|
|
51
|
-
'.git/**',
|
|
52
|
-
...policy.exclude,
|
|
53
|
-
],
|
|
54
|
-
persistent: true,
|
|
55
|
-
ignoreInitial: true,
|
|
56
|
-
awaitWriteFinish: {
|
|
57
|
-
stabilityThreshold: 100,
|
|
58
|
-
pollInterval: 50,
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Handle file deletion - restore immediately
|
|
63
|
-
watcher.on('unlink', (path) => {
|
|
64
|
-
const normalizedPath = normalizePath(path);
|
|
65
|
-
|
|
66
|
-
if (!isProtected(normalizedPath, policy)) return;
|
|
67
|
-
|
|
68
|
-
const restored = restoreFile(snapshot, normalizedPath, rootDir);
|
|
69
|
-
|
|
70
|
-
if (restored) {
|
|
71
|
-
stats.restored++;
|
|
72
|
-
stats.events.push({
|
|
73
|
-
time: new Date(),
|
|
74
|
-
event: 'unlink',
|
|
75
|
-
path: normalizedPath,
|
|
76
|
-
action: 'restored',
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
printRestored('delete', normalizedPath, policy.description);
|
|
80
|
-
logRestored(normalizedPath, 'delete', policy.description);
|
|
81
|
-
onRestore?.(normalizedPath);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Handle file modification - restore if content changed
|
|
86
|
-
watcher.on('change', (path) => {
|
|
87
|
-
const normalizedPath = normalizePath(path);
|
|
88
|
-
|
|
89
|
-
// Only act on modify policies
|
|
90
|
-
if (policy.action !== 'modify') return;
|
|
91
|
-
if (!isProtected(normalizedPath, policy)) return;
|
|
92
|
-
|
|
93
|
-
const restored = restoreFile(snapshot, normalizedPath, rootDir);
|
|
94
|
-
|
|
95
|
-
if (restored) {
|
|
96
|
-
stats.restored++;
|
|
97
|
-
stats.events.push({
|
|
98
|
-
time: new Date(),
|
|
99
|
-
event: 'change',
|
|
100
|
-
path: normalizedPath,
|
|
101
|
-
action: 'restored',
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
printRestored('modify', normalizedPath, policy.description);
|
|
105
|
-
logRestored(normalizedPath, 'modify', policy.description);
|
|
106
|
-
onRestore?.(normalizedPath);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Handle directory deletion
|
|
111
|
-
watcher.on('unlinkDir', (path) => {
|
|
112
|
-
const normalizedPath = normalizePath(path);
|
|
113
|
-
|
|
114
|
-
// Find all files in snapshot that were under this directory
|
|
115
|
-
for (const [filePath] of snapshot.files) {
|
|
116
|
-
if (filePath.startsWith(normalizedPath + '/')) {
|
|
117
|
-
const restored = restoreFile(snapshot, filePath, rootDir);
|
|
118
|
-
|
|
119
|
-
if (restored) {
|
|
120
|
-
stats.restored++;
|
|
121
|
-
stats.events.push({
|
|
122
|
-
time: new Date(),
|
|
123
|
-
event: 'unlinkDir',
|
|
124
|
-
path: filePath,
|
|
125
|
-
action: 'restored',
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
printRestored('delete', filePath, policy.description);
|
|
129
|
-
logRestored(filePath, 'delete', policy.description);
|
|
130
|
-
onRestore?.(filePath);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
watcher.on('error', (err) => {
|
|
137
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
138
|
-
console.error(`${COLORS.error}${SYMBOLS.error} Watcher error: ${message}${COLORS.reset}`);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
return { watcher, stats };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function printRestored(action: string, path: string, policyDesc: string): void {
|
|
145
|
-
console.log(`\n${COLORS.warning}${SYMBOLS.blocked} RESTORED${COLORS.reset}`);
|
|
146
|
-
console.log(` ${COLORS.dim}Action:${COLORS.reset} ${action}`);
|
|
147
|
-
console.log(` ${COLORS.dim}Target:${COLORS.reset} ${path}`);
|
|
148
|
-
console.log(` ${COLORS.dim}Policy:${COLORS.reset} ${policyDesc}`);
|
|
149
|
-
console.log(`\n File automatically restored from snapshot.\n`);
|
|
150
|
-
}
|
package/src/wrapper/daemon.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
// src/wrapper/daemon.ts
|
|
2
|
-
|
|
3
|
-
import * as net from 'net';
|
|
4
|
-
import type {
|
|
5
|
-
Policy,
|
|
6
|
-
CheckRequest,
|
|
7
|
-
CheckResponse,
|
|
8
|
-
SessionState,
|
|
9
|
-
} from '../types.js';
|
|
10
|
-
import { isProtected } from '../matcher.js';
|
|
11
|
-
import { COLORS, SYMBOLS } from '../ui/colors.js';
|
|
12
|
-
import { logBlocked } from '../audit/index.js';
|
|
13
|
-
|
|
14
|
-
export class VetoDaemon {
|
|
15
|
-
private server: net.Server | null = null;
|
|
16
|
-
private policy: Policy;
|
|
17
|
-
private state: SessionState;
|
|
18
|
-
|
|
19
|
-
constructor(policy: Policy, agent: string) {
|
|
20
|
-
this.policy = policy;
|
|
21
|
-
this.state = {
|
|
22
|
-
pid: process.pid,
|
|
23
|
-
agent,
|
|
24
|
-
policy,
|
|
25
|
-
startTime: new Date(),
|
|
26
|
-
blockedCount: 0,
|
|
27
|
-
allowedCount: 0,
|
|
28
|
-
blockedActions: [],
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async start(): Promise<number> {
|
|
33
|
-
return new Promise((resolve, reject) => {
|
|
34
|
-
this.server = net.createServer((socket) => {
|
|
35
|
-
let buffer = '';
|
|
36
|
-
|
|
37
|
-
socket.on('data', (data) => {
|
|
38
|
-
buffer += data.toString();
|
|
39
|
-
const lines = buffer.split('\n');
|
|
40
|
-
buffer = lines.pop() || '';
|
|
41
|
-
|
|
42
|
-
for (const line of lines) {
|
|
43
|
-
if (!line.trim()) continue;
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const req: CheckRequest = JSON.parse(line);
|
|
47
|
-
const res = this.check(req);
|
|
48
|
-
socket.write(JSON.stringify(res) + '\n');
|
|
49
|
-
} catch {
|
|
50
|
-
socket.write('{"allowed":true}\n');
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
socket.on('error', () => {
|
|
56
|
-
// Ignore socket errors
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
this.server.listen(0, '127.0.0.1', () => {
|
|
61
|
-
const addr = this.server!.address() as net.AddressInfo;
|
|
62
|
-
resolve(addr.port);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
this.server.on('error', reject);
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
check(req: CheckRequest): CheckResponse {
|
|
70
|
-
// Action must match policy
|
|
71
|
-
if (req.action !== this.policy.action) {
|
|
72
|
-
this.state.allowedCount++;
|
|
73
|
-
return { allowed: true };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Check if target is protected
|
|
77
|
-
if (isProtected(req.target, this.policy)) {
|
|
78
|
-
this.state.blockedCount++;
|
|
79
|
-
this.state.blockedActions.push({
|
|
80
|
-
time: new Date(),
|
|
81
|
-
action: req.action,
|
|
82
|
-
target: req.target,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Log to audit
|
|
86
|
-
logBlocked(req.target, req.action, this.policy.description, this.state.agent);
|
|
87
|
-
|
|
88
|
-
// Print block notification
|
|
89
|
-
console.log(
|
|
90
|
-
`\n${COLORS.error}${SYMBOLS.blocked} BLOCKED${COLORS.reset}`
|
|
91
|
-
);
|
|
92
|
-
console.log(` ${COLORS.dim}Action:${COLORS.reset} ${req.action}`);
|
|
93
|
-
console.log(` ${COLORS.dim}Target:${COLORS.reset} ${req.target}`);
|
|
94
|
-
console.log(
|
|
95
|
-
` ${COLORS.dim}Policy:${COLORS.reset} ${this.policy.description}`
|
|
96
|
-
);
|
|
97
|
-
console.log(`\n ${COLORS.success}Filesystem unchanged.${COLORS.reset}\n`);
|
|
98
|
-
|
|
99
|
-
return { allowed: false, reason: this.policy.description };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
this.state.allowedCount++;
|
|
103
|
-
return { allowed: true };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
getState(): SessionState {
|
|
107
|
-
return this.state;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
stop(): void {
|
|
111
|
-
// Print session summary
|
|
112
|
-
const duration = Date.now() - this.state.startTime.getTime();
|
|
113
|
-
const minutes = Math.floor(duration / 60000);
|
|
114
|
-
const seconds = Math.floor((duration % 60000) / 1000);
|
|
115
|
-
|
|
116
|
-
console.log(
|
|
117
|
-
`\n${COLORS.success}${SYMBOLS.success} veto-leash session ended${COLORS.reset}\n`
|
|
118
|
-
);
|
|
119
|
-
console.log(` Duration: ${minutes}m ${seconds}s`);
|
|
120
|
-
console.log(` Blocked: ${this.state.blockedCount} actions`);
|
|
121
|
-
console.log(` Allowed: ${this.state.allowedCount} actions`);
|
|
122
|
-
|
|
123
|
-
if (this.state.blockedActions.length > 0) {
|
|
124
|
-
console.log(`\n Blocked actions:`);
|
|
125
|
-
for (const action of this.state.blockedActions.slice(-5)) {
|
|
126
|
-
console.log(` ${SYMBOLS.bullet} ${action.action} ${action.target}`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
console.log('');
|
|
130
|
-
|
|
131
|
-
this.server?.close();
|
|
132
|
-
}
|
|
133
|
-
}
|
package/src/wrapper/shims.ts
DELETED
|
@@ -1,409 +0,0 @@
|
|
|
1
|
-
// src/wrapper/shims.ts
|
|
2
|
-
|
|
3
|
-
import { mkdtempSync, writeFileSync, rmSync } from 'fs';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import { tmpdir } from 'os';
|
|
6
|
-
import type { Policy } from '../types.js';
|
|
7
|
-
|
|
8
|
-
const IS_WINDOWS = process.platform === 'win32';
|
|
9
|
-
|
|
10
|
-
// Unix command mappings
|
|
11
|
-
const UNIX_COMMANDS: Record<string, string[]> = {
|
|
12
|
-
delete: ['rm', 'unlink', 'rmdir'],
|
|
13
|
-
modify: ['mv', 'cp', 'touch', 'chmod', 'chown', 'tee'],
|
|
14
|
-
execute: ['node', 'python', 'python3', 'bash', 'sh', 'npx', 'pnpm', 'npm', 'yarn'],
|
|
15
|
-
read: ['cat', 'less', 'head', 'tail', 'more'],
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
// Windows command mappings (PowerShell equivalents)
|
|
19
|
-
const WINDOWS_COMMANDS: Record<string, string[]> = {
|
|
20
|
-
delete: ['Remove-Item', 'del', 'rd', 'rmdir'],
|
|
21
|
-
modify: ['Move-Item', 'Copy-Item', 'mv', 'cp', 'Set-Content'],
|
|
22
|
-
execute: ['node', 'python', 'npx', 'pnpm', 'npm', 'yarn'],
|
|
23
|
-
read: ['Get-Content', 'cat', 'type'],
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export function createWrapperDir(port: number, policy: Policy): string {
|
|
27
|
-
const dir = mkdtempSync(join(tmpdir(), 'veto-'));
|
|
28
|
-
|
|
29
|
-
// Write the Node shim helper script (cross-platform)
|
|
30
|
-
const shimHelper = createNodeShimHelper(port, policy.action);
|
|
31
|
-
const shimHelperPath = join(dir, '_veto_check.mjs');
|
|
32
|
-
writeFileSync(shimHelperPath, shimHelper, { mode: 0o755 });
|
|
33
|
-
|
|
34
|
-
if (IS_WINDOWS) {
|
|
35
|
-
createWindowsShims(dir, shimHelperPath, policy);
|
|
36
|
-
} else {
|
|
37
|
-
createUnixShims(dir, shimHelperPath, policy);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return dir;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function createUnixShims(dir: string, shimHelperPath: string, policy: Policy): void {
|
|
44
|
-
const commands = UNIX_COMMANDS[policy.action] || [];
|
|
45
|
-
for (const cmd of commands) {
|
|
46
|
-
const script = createUnixShim(cmd, shimHelperPath);
|
|
47
|
-
writeFileSync(join(dir, cmd), script, { mode: 0o755 });
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Always wrap git for delete/modify actions
|
|
51
|
-
if (policy.action === 'delete' || policy.action === 'modify') {
|
|
52
|
-
const gitShim = createGitShim(shimHelperPath);
|
|
53
|
-
writeFileSync(join(dir, 'git'), gitShim, { mode: 0o755 });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function createWindowsShims(dir: string, shimHelperPath: string, policy: Policy): void {
|
|
58
|
-
const commands = WINDOWS_COMMANDS[policy.action] || [];
|
|
59
|
-
|
|
60
|
-
for (const cmd of commands) {
|
|
61
|
-
// Create both .ps1 and .cmd wrappers
|
|
62
|
-
const ps1Script = createPowerShellShim(cmd, shimHelperPath);
|
|
63
|
-
writeFileSync(join(dir, `${cmd}.ps1`), ps1Script);
|
|
64
|
-
|
|
65
|
-
// CMD wrapper that invokes PowerShell
|
|
66
|
-
const cmdScript = createCmdWrapper(cmd);
|
|
67
|
-
writeFileSync(join(dir, `${cmd}.cmd`), cmdScript);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Wrap git on Windows
|
|
71
|
-
if (policy.action === 'delete' || policy.action === 'modify') {
|
|
72
|
-
const gitPs1 = createPowerShellGitShim(shimHelperPath);
|
|
73
|
-
writeFileSync(join(dir, 'git.ps1'), gitPs1);
|
|
74
|
-
writeFileSync(join(dir, 'git.cmd'), createCmdWrapper('git'));
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Node-based shim helper that handles:
|
|
80
|
-
* - Directory walking for recursive deletes
|
|
81
|
-
* - Proper JSON encoding
|
|
82
|
-
* - TCP communication without netcat
|
|
83
|
-
* - Path normalization without realpath
|
|
84
|
-
*/
|
|
85
|
-
function createNodeShimHelper(port: number, action: string): string {
|
|
86
|
-
return `#!/usr/bin/env node
|
|
87
|
-
// veto-leash shim helper - checks files against policy daemon
|
|
88
|
-
|
|
89
|
-
import { createConnection } from 'net';
|
|
90
|
-
import { statSync, readdirSync } from 'fs';
|
|
91
|
-
import { relative, resolve, join } from 'path';
|
|
92
|
-
|
|
93
|
-
const PORT = ${port};
|
|
94
|
-
const ACTION = '${action}';
|
|
95
|
-
const MAX_FILES = 10000;
|
|
96
|
-
const MAX_DEPTH = 50;
|
|
97
|
-
|
|
98
|
-
// Get all files to check from arguments
|
|
99
|
-
const targets = process.argv.slice(2);
|
|
100
|
-
|
|
101
|
-
async function checkTarget(target) {
|
|
102
|
-
return new Promise((resolve) => {
|
|
103
|
-
const socket = createConnection({ port: PORT, host: '127.0.0.1' }, () => {
|
|
104
|
-
const relPath = relative(process.cwd(), target) || target;
|
|
105
|
-
const req = JSON.stringify({ action: ACTION, target: relPath });
|
|
106
|
-
socket.write(req + '\\n');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
let data = '';
|
|
110
|
-
socket.on('data', (chunk) => {
|
|
111
|
-
data += chunk.toString();
|
|
112
|
-
if (data.includes('\\n')) {
|
|
113
|
-
try {
|
|
114
|
-
const resp = JSON.parse(data.trim());
|
|
115
|
-
resolve(resp.allowed === true);
|
|
116
|
-
} catch {
|
|
117
|
-
resolve(false); // Fail closed
|
|
118
|
-
}
|
|
119
|
-
socket.end();
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
socket.on('error', () => resolve(false)); // Fail closed
|
|
124
|
-
socket.setTimeout(1000, () => {
|
|
125
|
-
socket.destroy();
|
|
126
|
-
resolve(false); // Fail closed on timeout
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Walk directory recursively and collect all files
|
|
132
|
-
function walkDir(dir, depth = 0, files = []) {
|
|
133
|
-
if (depth > MAX_DEPTH || files.length > MAX_FILES) return files;
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
137
|
-
for (const entry of entries) {
|
|
138
|
-
if (files.length > MAX_FILES) break;
|
|
139
|
-
const fullPath = join(dir, entry.name);
|
|
140
|
-
files.push(fullPath);
|
|
141
|
-
if (entry.isDirectory()) {
|
|
142
|
-
walkDir(fullPath, depth + 1, files);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
} catch {
|
|
146
|
-
// Ignore permission errors
|
|
147
|
-
}
|
|
148
|
-
return files;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async function main() {
|
|
152
|
-
const filesToCheck = [];
|
|
153
|
-
|
|
154
|
-
for (const target of targets) {
|
|
155
|
-
try {
|
|
156
|
-
const resolved = resolve(target);
|
|
157
|
-
const stat = statSync(resolved);
|
|
158
|
-
|
|
159
|
-
if (stat.isDirectory()) {
|
|
160
|
-
// For directories, check all contained files
|
|
161
|
-
const contained = walkDir(resolved);
|
|
162
|
-
filesToCheck.push(...contained);
|
|
163
|
-
} else {
|
|
164
|
-
filesToCheck.push(resolved);
|
|
165
|
-
}
|
|
166
|
-
} catch {
|
|
167
|
-
// File doesn't exist, let the real command handle the error
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Check all files
|
|
173
|
-
for (const file of filesToCheck) {
|
|
174
|
-
const allowed = await checkTarget(file);
|
|
175
|
-
if (!allowed) {
|
|
176
|
-
process.exit(1);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
process.exit(0);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
main().catch(() => process.exit(1));
|
|
184
|
-
`;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* PowerShell shim for Windows
|
|
189
|
-
*/
|
|
190
|
-
function createPowerShellShim(cmd: string, helperPath: string): string {
|
|
191
|
-
return `# veto-leash PowerShell shim for ${cmd}
|
|
192
|
-
$ErrorActionPreference = "Stop"
|
|
193
|
-
|
|
194
|
-
# Find real command
|
|
195
|
-
$realCmd = Get-Command ${cmd} -CommandType Application -ErrorAction SilentlyContinue |
|
|
196
|
-
Where-Object { $_.Source -notlike "*veto*" } |
|
|
197
|
-
Select-Object -First 1
|
|
198
|
-
|
|
199
|
-
if (-not $realCmd) {
|
|
200
|
-
Write-Error "veto-leash: cannot find real ${cmd}"
|
|
201
|
-
exit 127
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
# Collect file arguments
|
|
205
|
-
$files = @()
|
|
206
|
-
foreach ($arg in $args) {
|
|
207
|
-
if ($arg -notlike "-*" -and (Test-Path $arg -ErrorAction SilentlyContinue)) {
|
|
208
|
-
$files += $arg
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
# Check with Node helper
|
|
213
|
-
if ($files.Count -gt 0) {
|
|
214
|
-
$result = & node "${helperPath.replace(/\\/g, '\\\\')}" @files
|
|
215
|
-
if ($LASTEXITCODE -ne 0) {
|
|
216
|
-
exit 1
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
# Run real command
|
|
221
|
-
& $realCmd.Source @args
|
|
222
|
-
exit $LASTEXITCODE
|
|
223
|
-
`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* CMD wrapper that invokes PowerShell script
|
|
228
|
-
*/
|
|
229
|
-
function createCmdWrapper(cmd: string): string {
|
|
230
|
-
return `@echo off
|
|
231
|
-
powershell -ExecutionPolicy Bypass -File "%~dp0${cmd}.ps1" %*
|
|
232
|
-
exit /b %ERRORLEVEL%
|
|
233
|
-
`;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* PowerShell git shim for Windows
|
|
238
|
-
*/
|
|
239
|
-
function createPowerShellGitShim(helperPath: string): string {
|
|
240
|
-
return `# veto-leash PowerShell git shim
|
|
241
|
-
$ErrorActionPreference = "Stop"
|
|
242
|
-
|
|
243
|
-
$realGit = Get-Command git -CommandType Application -ErrorAction SilentlyContinue |
|
|
244
|
-
Where-Object { $_.Source -notlike "*veto*" } |
|
|
245
|
-
Select-Object -First 1
|
|
246
|
-
|
|
247
|
-
if (-not $realGit) {
|
|
248
|
-
Write-Error "veto-leash: cannot find real git"
|
|
249
|
-
exit 127
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
$subcommand = $args[0]
|
|
253
|
-
|
|
254
|
-
switch ($subcommand) {
|
|
255
|
-
"rm" {
|
|
256
|
-
$files = @()
|
|
257
|
-
foreach ($arg in $args[1..$args.Length]) {
|
|
258
|
-
if ($arg -notlike "-*" -and (Test-Path $arg -ErrorAction SilentlyContinue)) {
|
|
259
|
-
$files += $arg
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if ($files.Count -gt 0) {
|
|
263
|
-
& node "${helperPath.replace(/\\/g, '\\\\')}" @files
|
|
264
|
-
if ($LASTEXITCODE -ne 0) { exit 1 }
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
"checkout" {
|
|
268
|
-
if ($args[1] -eq "." -or ($args[1] -eq "--" -and $args[2] -eq ".")) {
|
|
269
|
-
Write-Error "veto-leash: 'git checkout .' blocked - would modify protected files"
|
|
270
|
-
exit 1
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
"reset" {
|
|
274
|
-
if ($args -contains "--hard") {
|
|
275
|
-
Write-Error "veto-leash: 'git reset --hard' blocked - would modify protected files"
|
|
276
|
-
exit 1
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
& $realGit.Source @args
|
|
282
|
-
exit $LASTEXITCODE
|
|
283
|
-
`;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Bash shim that calls the Node helper for checking,
|
|
288
|
-
* then executes the real command if allowed.
|
|
289
|
-
*/
|
|
290
|
-
function createUnixShim(cmd: string, helperPath: string): string {
|
|
291
|
-
return `#!/bin/bash
|
|
292
|
-
set -e
|
|
293
|
-
|
|
294
|
-
# Find real binary (skip our wrapper directory)
|
|
295
|
-
WRAPPER_DIR="$(dirname "$0")"
|
|
296
|
-
REAL_CMD=$(which -a ${cmd} 2>/dev/null | grep -v "$WRAPPER_DIR" | head -1)
|
|
297
|
-
|
|
298
|
-
if [ -z "$REAL_CMD" ]; then
|
|
299
|
-
echo "veto-leash: cannot find real ${cmd} binary" >&2
|
|
300
|
-
exit 127
|
|
301
|
-
fi
|
|
302
|
-
|
|
303
|
-
# Collect file arguments (skip flags)
|
|
304
|
-
FILES=()
|
|
305
|
-
for arg in "$@"; do
|
|
306
|
-
[[ "$arg" == -* ]] && continue
|
|
307
|
-
FILES+=("$arg")
|
|
308
|
-
done
|
|
309
|
-
|
|
310
|
-
# Check files with Node helper if any exist
|
|
311
|
-
if [ \${#FILES[@]} -gt 0 ]; then
|
|
312
|
-
node "${helperPath}" "\${FILES[@]}" || exit 1
|
|
313
|
-
fi
|
|
314
|
-
|
|
315
|
-
# All approved, run real command
|
|
316
|
-
exec "$REAL_CMD" "$@"
|
|
317
|
-
`;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Git shim that handles destructive git commands:
|
|
322
|
-
* - git rm: check file args
|
|
323
|
-
* - git clean: run dry-run, check candidates
|
|
324
|
-
* - git checkout .: block by default (restores files)
|
|
325
|
-
* - git reset --hard: block by default
|
|
326
|
-
*/
|
|
327
|
-
function createGitShim(helperPath: string): string {
|
|
328
|
-
return `#!/bin/bash
|
|
329
|
-
set -e
|
|
330
|
-
|
|
331
|
-
# Find real git (skip our wrapper directory)
|
|
332
|
-
WRAPPER_DIR="$(dirname "$0")"
|
|
333
|
-
REAL_GIT=$(which -a git 2>/dev/null | grep -v "$WRAPPER_DIR" | head -1)
|
|
334
|
-
|
|
335
|
-
if [ -z "$REAL_GIT" ]; then
|
|
336
|
-
echo "veto-leash: cannot find real git binary" >&2
|
|
337
|
-
exit 127
|
|
338
|
-
fi
|
|
339
|
-
|
|
340
|
-
case "$1" in
|
|
341
|
-
rm)
|
|
342
|
-
# Check file arguments
|
|
343
|
-
FILES=()
|
|
344
|
-
for arg in "\${@:2}"; do
|
|
345
|
-
[[ "$arg" == -* ]] && continue
|
|
346
|
-
FILES+=("$arg")
|
|
347
|
-
done
|
|
348
|
-
if [ \${#FILES[@]} -gt 0 ]; then
|
|
349
|
-
node "${helperPath}" "\${FILES[@]}" || exit 1
|
|
350
|
-
fi
|
|
351
|
-
;;
|
|
352
|
-
|
|
353
|
-
clean)
|
|
354
|
-
# Check if it's a destructive clean
|
|
355
|
-
if [[ "$*" == *"-f"* ]] || [[ "$*" == *"-d"* ]] || [[ "$*" == *"-x"* ]]; then
|
|
356
|
-
# Get list of files that would be deleted
|
|
357
|
-
CANDIDATES=$("$REAL_GIT" clean -n "\${@:2}" 2>/dev/null | sed 's/^Would remove //' || true)
|
|
358
|
-
if [ -n "$CANDIDATES" ]; then
|
|
359
|
-
echo "$CANDIDATES" | while read -r file; do
|
|
360
|
-
[ -n "$file" ] && node "${helperPath}" "$file" || exit 1
|
|
361
|
-
done
|
|
362
|
-
# If the while loop exited with error, propagate it
|
|
363
|
-
if [ \${PIPESTATUS[1]} -ne 0 ]; then
|
|
364
|
-
exit 1
|
|
365
|
-
fi
|
|
366
|
-
fi
|
|
367
|
-
fi
|
|
368
|
-
;;
|
|
369
|
-
|
|
370
|
-
checkout)
|
|
371
|
-
# Block 'git checkout .' and 'git checkout -- .' (restores tracked files)
|
|
372
|
-
if [[ "$2" == "." ]] || [[ "$2" == "--" && "$3" == "." ]]; then
|
|
373
|
-
echo "veto-leash: 'git checkout .' blocked - would modify protected files" >&2
|
|
374
|
-
echo " Use 'git checkout <specific-file>' instead" >&2
|
|
375
|
-
exit 1
|
|
376
|
-
fi
|
|
377
|
-
# Check specific file args
|
|
378
|
-
FILES=()
|
|
379
|
-
for arg in "\${@:2}"; do
|
|
380
|
-
[[ "$arg" == -* ]] && continue
|
|
381
|
-
[[ "$arg" == "--" ]] && continue
|
|
382
|
-
[ -e "$arg" ] && FILES+=("$arg")
|
|
383
|
-
done
|
|
384
|
-
if [ \${#FILES[@]} -gt 0 ]; then
|
|
385
|
-
node "${helperPath}" "\${FILES[@]}" || exit 1
|
|
386
|
-
fi
|
|
387
|
-
;;
|
|
388
|
-
|
|
389
|
-
reset)
|
|
390
|
-
# Block 'git reset --hard' (destroys uncommitted changes)
|
|
391
|
-
if [[ "$*" == *"--hard"* ]]; then
|
|
392
|
-
echo "veto-leash: 'git reset --hard' blocked - would modify protected files" >&2
|
|
393
|
-
echo " Use 'git stash' or commit your changes first" >&2
|
|
394
|
-
exit 1
|
|
395
|
-
fi
|
|
396
|
-
;;
|
|
397
|
-
esac
|
|
398
|
-
|
|
399
|
-
exec "$REAL_GIT" "$@"
|
|
400
|
-
`;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
export function cleanupWrapperDir(dir: string): void {
|
|
404
|
-
try {
|
|
405
|
-
rmSync(dir, { recursive: true, force: true });
|
|
406
|
-
} catch {
|
|
407
|
-
// Ignore cleanup errors
|
|
408
|
-
}
|
|
409
|
-
}
|