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/native/cursor.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
// src/native/cursor.ts
|
|
2
|
-
// Cursor integration via .cursorrules
|
|
3
|
-
// Cursor doesn't have a hook/permission system - only AI instruction rules
|
|
4
|
-
// We generate .cursorrules that instruct the AI to respect restrictions
|
|
5
|
-
|
|
6
|
-
import { existsSync, writeFileSync, readFileSync, appendFileSync } from 'fs';
|
|
7
|
-
import { join } from 'path';
|
|
8
|
-
import type { Policy } from '../types.js';
|
|
9
|
-
import { COLORS, SYMBOLS } from '../ui/colors.js';
|
|
10
|
-
|
|
11
|
-
const CURSORRULES_FILE = '.cursorrules';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Install veto-leash instructions into .cursorrules
|
|
15
|
-
* Note: This only provides AI guidance, not enforcement.
|
|
16
|
-
* For actual enforcement, use wrapper mode: leash cursor "..."
|
|
17
|
-
*/
|
|
18
|
-
export async function installCursorRules(): Promise<void> {
|
|
19
|
-
console.log(`\n${COLORS.info}Installing veto-leash for Cursor...${COLORS.reset}\n`);
|
|
20
|
-
|
|
21
|
-
const policies = loadStoredPolicies();
|
|
22
|
-
|
|
23
|
-
if (policies.length === 0) {
|
|
24
|
-
console.log(`${COLORS.warning}${SYMBOLS.warning} No policies found. Add policies first:${COLORS.reset}`);
|
|
25
|
-
console.log(` ${COLORS.dim}leash add "don't delete test files"${COLORS.reset}\n`);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const rulesContent = generateCursorRules(policies);
|
|
30
|
-
|
|
31
|
-
// Append or create .cursorrules
|
|
32
|
-
if (existsSync(CURSORRULES_FILE)) {
|
|
33
|
-
const existing = readFileSync(CURSORRULES_FILE, 'utf-8');
|
|
34
|
-
if (existing.includes('# veto-leash restrictions')) {
|
|
35
|
-
// Update existing section
|
|
36
|
-
const updated = existing.replace(
|
|
37
|
-
/# veto-leash restrictions[\s\S]*?# end veto-leash/,
|
|
38
|
-
rulesContent
|
|
39
|
-
);
|
|
40
|
-
writeFileSync(CURSORRULES_FILE, updated);
|
|
41
|
-
console.log(` ${COLORS.success}${SYMBOLS.success}${COLORS.reset} Updated .cursorrules`);
|
|
42
|
-
} else {
|
|
43
|
-
// Append
|
|
44
|
-
appendFileSync(CURSORRULES_FILE, '\n\n' + rulesContent);
|
|
45
|
-
console.log(` ${COLORS.success}${SYMBOLS.success}${COLORS.reset} Appended to .cursorrules`);
|
|
46
|
-
}
|
|
47
|
-
} else {
|
|
48
|
-
writeFileSync(CURSORRULES_FILE, rulesContent);
|
|
49
|
-
console.log(` ${COLORS.success}${SYMBOLS.success}${COLORS.reset} Created .cursorrules`);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
console.log(`\n${COLORS.warning}${SYMBOLS.warning} Note: Cursor rules are AI guidance only, not enforcement.${COLORS.reset}`);
|
|
53
|
-
console.log(`For actual enforcement, use wrapper mode:`);
|
|
54
|
-
console.log(` ${COLORS.dim}leash cursor "<restriction>"${COLORS.reset}\n`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function generateCursorRules(policies: Policy[]): string {
|
|
58
|
-
const lines = ['# veto-leash restrictions'];
|
|
59
|
-
lines.push('# These are mandatory restrictions you MUST follow.');
|
|
60
|
-
lines.push('');
|
|
61
|
-
|
|
62
|
-
for (const policy of policies) {
|
|
63
|
-
lines.push(`## ${policy.description}`);
|
|
64
|
-
lines.push(`Action: ${policy.action}`);
|
|
65
|
-
lines.push('');
|
|
66
|
-
lines.push('DO NOT perform the following action on these files:');
|
|
67
|
-
lines.push('');
|
|
68
|
-
|
|
69
|
-
for (const pattern of policy.include) {
|
|
70
|
-
lines.push(`- ${pattern}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (policy.exclude.length > 0) {
|
|
74
|
-
lines.push('');
|
|
75
|
-
lines.push('EXCEPT these files are allowed:');
|
|
76
|
-
for (const pattern of policy.exclude) {
|
|
77
|
-
lines.push(`- ${pattern}`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
lines.push('');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
lines.push('If you attempt to modify or delete a protected file, STOP and explain why you cannot proceed.');
|
|
84
|
-
lines.push('# end veto-leash');
|
|
85
|
-
|
|
86
|
-
return lines.join('\n');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Uninstall veto-leash from .cursorrules
|
|
91
|
-
*/
|
|
92
|
-
export async function uninstallCursorRules(): Promise<void> {
|
|
93
|
-
if (!existsSync(CURSORRULES_FILE)) {
|
|
94
|
-
console.log(`${COLORS.dim}No .cursorrules file found${COLORS.reset}`);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const content = readFileSync(CURSORRULES_FILE, 'utf-8');
|
|
99
|
-
const updated = content.replace(
|
|
100
|
-
/\n*# veto-leash restrictions[\s\S]*?# end veto-leash\n*/,
|
|
101
|
-
''
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
if (updated.trim()) {
|
|
105
|
-
writeFileSync(CURSORRULES_FILE, updated);
|
|
106
|
-
console.log(`${COLORS.success}${SYMBOLS.success} Removed veto-leash from .cursorrules${COLORS.reset}`);
|
|
107
|
-
} else {
|
|
108
|
-
// File would be empty, delete it
|
|
109
|
-
require('fs').unlinkSync(CURSORRULES_FILE);
|
|
110
|
-
console.log(`${COLORS.success}${SYMBOLS.success} Removed .cursorrules${COLORS.reset}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function loadStoredPolicies(): Policy[] {
|
|
115
|
-
const policiesFile = join(
|
|
116
|
-
require('os').homedir(),
|
|
117
|
-
'.config',
|
|
118
|
-
'veto-leash',
|
|
119
|
-
'policies.json'
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
if (existsSync(policiesFile)) {
|
|
124
|
-
const data = JSON.parse(readFileSync(policiesFile, 'utf-8'));
|
|
125
|
-
return data.policies?.map((p: { policy: Policy }) => p.policy) || [];
|
|
126
|
-
}
|
|
127
|
-
} catch {
|
|
128
|
-
// Ignore
|
|
129
|
-
}
|
|
130
|
-
return [];
|
|
131
|
-
}
|
package/src/native/index.ts
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
// src/native/index.ts
|
|
2
|
-
// Agent registry and unified interface for native integrations
|
|
3
|
-
|
|
4
|
-
import type { Policy } from '../types.js';
|
|
5
|
-
import { COLORS, SYMBOLS } from '../ui/colors.js';
|
|
6
|
-
|
|
7
|
-
// Import all agent integrations
|
|
8
|
-
import {
|
|
9
|
-
installClaudeCodeHook,
|
|
10
|
-
addClaudeCodePolicy,
|
|
11
|
-
uninstallClaudeCodeHook,
|
|
12
|
-
} from './claude-code.js';
|
|
13
|
-
import {
|
|
14
|
-
installOpenCodePermissions,
|
|
15
|
-
uninstallOpenCodePermissions,
|
|
16
|
-
savePolicy as saveOpenCodePolicy,
|
|
17
|
-
} from './opencode.js';
|
|
18
|
-
import {
|
|
19
|
-
installWindsurfHooks,
|
|
20
|
-
addWindsurfPolicy,
|
|
21
|
-
uninstallWindsurfHooks,
|
|
22
|
-
} from './windsurf.js';
|
|
23
|
-
import {
|
|
24
|
-
installCursorRules,
|
|
25
|
-
uninstallCursorRules,
|
|
26
|
-
} from './cursor.js';
|
|
27
|
-
import {
|
|
28
|
-
installAiderConfig,
|
|
29
|
-
uninstallAiderConfig,
|
|
30
|
-
} from './aider.js';
|
|
31
|
-
|
|
32
|
-
export interface AgentInfo {
|
|
33
|
-
id: string;
|
|
34
|
-
name: string;
|
|
35
|
-
aliases: string[];
|
|
36
|
-
hasNativeHooks: boolean;
|
|
37
|
-
description: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export const AGENTS: AgentInfo[] = [
|
|
41
|
-
{
|
|
42
|
-
id: 'claude-code',
|
|
43
|
-
name: 'Claude Code',
|
|
44
|
-
aliases: ['cc', 'claude', 'claude-code'],
|
|
45
|
-
hasNativeHooks: true,
|
|
46
|
-
description: 'PreToolUse hooks for Bash/Write/Edit',
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
id: 'opencode',
|
|
50
|
-
name: 'OpenCode',
|
|
51
|
-
aliases: ['oc', 'opencode'],
|
|
52
|
-
hasNativeHooks: true,
|
|
53
|
-
description: 'permission.bash rules in opencode.json',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
id: 'windsurf',
|
|
57
|
-
name: 'Windsurf',
|
|
58
|
-
aliases: ['ws', 'windsurf', 'cascade'],
|
|
59
|
-
hasNativeHooks: true,
|
|
60
|
-
description: 'Cascade hooks for pre_write_code/pre_run_command',
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
id: 'cursor',
|
|
64
|
-
name: 'Cursor',
|
|
65
|
-
aliases: ['cursor'],
|
|
66
|
-
hasNativeHooks: false,
|
|
67
|
-
description: '.cursorrules AI guidance (not enforcement)',
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
id: 'aider',
|
|
71
|
-
name: 'Aider',
|
|
72
|
-
aliases: ['aider'],
|
|
73
|
-
hasNativeHooks: false,
|
|
74
|
-
description: '.aider.conf.yml read-only files',
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
id: 'codex',
|
|
78
|
-
name: 'Codex CLI',
|
|
79
|
-
aliases: ['codex', 'codex-cli'],
|
|
80
|
-
hasNativeHooks: false,
|
|
81
|
-
description: 'OS sandbox - use watchdog mode',
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
id: 'copilot',
|
|
85
|
-
name: 'GitHub Copilot',
|
|
86
|
-
aliases: ['copilot', 'gh-copilot'],
|
|
87
|
-
hasNativeHooks: false,
|
|
88
|
-
description: 'No hook system - use wrapper mode',
|
|
89
|
-
},
|
|
90
|
-
];
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Resolve agent alias to agent ID
|
|
94
|
-
*/
|
|
95
|
-
export function resolveAgent(input: string): AgentInfo | null {
|
|
96
|
-
const normalized = input?.toLowerCase().trim();
|
|
97
|
-
if (!normalized) return null;
|
|
98
|
-
|
|
99
|
-
for (const agent of AGENTS) {
|
|
100
|
-
if (agent.aliases.includes(normalized)) {
|
|
101
|
-
return agent;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Install native integration for an agent
|
|
109
|
-
*/
|
|
110
|
-
export async function installAgent(
|
|
111
|
-
agentId: string,
|
|
112
|
-
options: { global?: boolean } = {}
|
|
113
|
-
): Promise<boolean> {
|
|
114
|
-
const agent = resolveAgent(agentId);
|
|
115
|
-
|
|
116
|
-
if (!agent) {
|
|
117
|
-
console.error(`${COLORS.error}${SYMBOLS.error} Unknown agent: ${agentId}${COLORS.reset}`);
|
|
118
|
-
printSupportedAgents();
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
switch (agent.id) {
|
|
123
|
-
case 'claude-code':
|
|
124
|
-
await installClaudeCodeHook();
|
|
125
|
-
return true;
|
|
126
|
-
|
|
127
|
-
case 'opencode':
|
|
128
|
-
await installOpenCodePermissions(options.global ? 'global' : 'project');
|
|
129
|
-
return true;
|
|
130
|
-
|
|
131
|
-
case 'windsurf':
|
|
132
|
-
await installWindsurfHooks(options.global ? 'user' : 'workspace');
|
|
133
|
-
return true;
|
|
134
|
-
|
|
135
|
-
case 'cursor':
|
|
136
|
-
await installCursorRules();
|
|
137
|
-
return true;
|
|
138
|
-
|
|
139
|
-
case 'aider':
|
|
140
|
-
await installAiderConfig(options.global ? 'global' : 'project');
|
|
141
|
-
return true;
|
|
142
|
-
|
|
143
|
-
case 'codex':
|
|
144
|
-
console.log(`\n${COLORS.warning}${SYMBOLS.warning} Codex CLI uses OS-level sandboxing.${COLORS.reset}`);
|
|
145
|
-
console.log(`Use watchdog mode for file protection:`);
|
|
146
|
-
console.log(` ${COLORS.dim}leash watch "protect test files"${COLORS.reset}\n`);
|
|
147
|
-
return false;
|
|
148
|
-
|
|
149
|
-
case 'copilot':
|
|
150
|
-
console.log(`\n${COLORS.warning}${SYMBOLS.warning} GitHub Copilot has no hook system.${COLORS.reset}`);
|
|
151
|
-
console.log(`Use wrapper mode or watchdog:`);
|
|
152
|
-
console.log(` ${COLORS.dim}leash watch "protect .env"${COLORS.reset}\n`);
|
|
153
|
-
return false;
|
|
154
|
-
|
|
155
|
-
default:
|
|
156
|
-
console.error(`${COLORS.error}${SYMBOLS.error} No native integration for ${agent.name}${COLORS.reset}`);
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Uninstall native integration for an agent
|
|
163
|
-
*/
|
|
164
|
-
export async function uninstallAgent(
|
|
165
|
-
agentId: string,
|
|
166
|
-
options: { global?: boolean } = {}
|
|
167
|
-
): Promise<boolean> {
|
|
168
|
-
const agent = resolveAgent(agentId);
|
|
169
|
-
|
|
170
|
-
if (!agent) {
|
|
171
|
-
console.error(`${COLORS.error}${SYMBOLS.error} Unknown agent: ${agentId}${COLORS.reset}`);
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
switch (agent.id) {
|
|
176
|
-
case 'claude-code':
|
|
177
|
-
await uninstallClaudeCodeHook();
|
|
178
|
-
return true;
|
|
179
|
-
|
|
180
|
-
case 'opencode':
|
|
181
|
-
await uninstallOpenCodePermissions(options.global ? 'global' : 'project');
|
|
182
|
-
return true;
|
|
183
|
-
|
|
184
|
-
case 'windsurf':
|
|
185
|
-
await uninstallWindsurfHooks(options.global ? 'user' : 'workspace');
|
|
186
|
-
return true;
|
|
187
|
-
|
|
188
|
-
case 'cursor':
|
|
189
|
-
await uninstallCursorRules();
|
|
190
|
-
return true;
|
|
191
|
-
|
|
192
|
-
case 'aider':
|
|
193
|
-
await uninstallAiderConfig(options.global ? 'global' : 'project');
|
|
194
|
-
return true;
|
|
195
|
-
|
|
196
|
-
default:
|
|
197
|
-
console.log(`${COLORS.dim}No native integration to remove for ${agent.name}${COLORS.reset}`);
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Add a policy to all installed native integrations
|
|
204
|
-
*/
|
|
205
|
-
export async function addPolicyToAgents(
|
|
206
|
-
policy: Policy,
|
|
207
|
-
name: string
|
|
208
|
-
): Promise<void> {
|
|
209
|
-
// Always save to veto-leash config
|
|
210
|
-
saveOpenCodePolicy(name, policy);
|
|
211
|
-
|
|
212
|
-
// Claude Code
|
|
213
|
-
await addClaudeCodePolicy(policy, name);
|
|
214
|
-
|
|
215
|
-
// Windsurf
|
|
216
|
-
await addWindsurfPolicy(policy, name);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function printSupportedAgents(): void {
|
|
220
|
-
console.log(`\nSupported agents:`);
|
|
221
|
-
for (const agent of AGENTS) {
|
|
222
|
-
const hookStatus = agent.hasNativeHooks ? COLORS.success + 'native' : COLORS.dim + 'wrapper';
|
|
223
|
-
console.log(` ${COLORS.dim}${agent.aliases[0].padEnd(12)}${COLORS.reset} ${agent.name} (${hookStatus}${COLORS.reset})`);
|
|
224
|
-
}
|
|
225
|
-
console.log('');
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Re-export individual modules
|
|
229
|
-
export * from './claude-code.js';
|
|
230
|
-
export * from './opencode.js';
|
|
231
|
-
export * from './windsurf.js';
|
|
232
|
-
export * from './cursor.js';
|
|
233
|
-
export * from './aider.js';
|
package/src/native/opencode.ts
DELETED
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
// src/native/opencode.ts
|
|
2
|
-
// OpenCode native permission integration
|
|
3
|
-
// Generates permission rules for OpenCode's opencode.json config
|
|
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 OPENCODE_GLOBAL_CONFIG = join(
|
|
12
|
-
homedir(),
|
|
13
|
-
'.config',
|
|
14
|
-
'opencode',
|
|
15
|
-
'opencode.json'
|
|
16
|
-
);
|
|
17
|
-
const OPENCODE_PROJECT_CONFIG = 'opencode.json';
|
|
18
|
-
const VETO_LEASH_CONFIG_DIR = join(homedir(), '.config', 'veto-leash');
|
|
19
|
-
const POLICIES_FILE = join(VETO_LEASH_CONFIG_DIR, 'policies.json');
|
|
20
|
-
|
|
21
|
-
interface OpenCodeConfig {
|
|
22
|
-
$schema?: string;
|
|
23
|
-
permission?: {
|
|
24
|
-
edit?: string | Record<string, string>;
|
|
25
|
-
bash?: string | Record<string, string>;
|
|
26
|
-
[key: string]: unknown;
|
|
27
|
-
};
|
|
28
|
-
[key: string]: unknown;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface StoredPolicies {
|
|
32
|
-
policies: Array<{
|
|
33
|
-
restriction: string;
|
|
34
|
-
policy: Policy;
|
|
35
|
-
}>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Convert a veto-leash policy to OpenCode permission rules
|
|
40
|
-
*/
|
|
41
|
-
function policyToOpenCodeRules(policy: Policy): Record<string, string> {
|
|
42
|
-
const rules: Record<string, string> = {};
|
|
43
|
-
|
|
44
|
-
// Generate bash permission rules based on action and patterns
|
|
45
|
-
if (policy.action === 'delete') {
|
|
46
|
-
// Block rm commands for protected files
|
|
47
|
-
for (const pattern of policy.include) {
|
|
48
|
-
// Convert glob to OpenCode wildcard format
|
|
49
|
-
const ocPattern = pattern
|
|
50
|
-
.replace(/\*\*\//g, '*/') // **/ -> */
|
|
51
|
-
.replace(/\*\*/g, '*'); // ** -> *
|
|
52
|
-
|
|
53
|
-
rules[`rm ${ocPattern}`] = 'deny';
|
|
54
|
-
rules[`rm -f ${ocPattern}`] = 'deny';
|
|
55
|
-
rules[`rm -rf ${ocPattern}`] = 'deny';
|
|
56
|
-
rules[`rm -r ${ocPattern}`] = 'deny';
|
|
57
|
-
rules[`git rm ${ocPattern}`] = 'deny';
|
|
58
|
-
rules[`git rm -f ${ocPattern}`] = 'deny';
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Allow excluded patterns
|
|
62
|
-
for (const pattern of policy.exclude) {
|
|
63
|
-
const ocPattern = pattern
|
|
64
|
-
.replace(/\*\*\//g, '*/')
|
|
65
|
-
.replace(/\*\*/g, '*');
|
|
66
|
-
|
|
67
|
-
rules[`rm ${ocPattern}`] = 'allow';
|
|
68
|
-
rules[`rm -f ${ocPattern}`] = 'allow';
|
|
69
|
-
rules[`rm -rf ${ocPattern}`] = 'allow';
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (policy.action === 'modify') {
|
|
74
|
-
// Block modification commands for protected files
|
|
75
|
-
for (const pattern of policy.include) {
|
|
76
|
-
const ocPattern = pattern
|
|
77
|
-
.replace(/\*\*\//g, '*/')
|
|
78
|
-
.replace(/\*\*/g, '*');
|
|
79
|
-
|
|
80
|
-
rules[`mv ${ocPattern} *`] = 'deny';
|
|
81
|
-
rules[`cp * ${ocPattern}`] = 'deny';
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (policy.action === 'execute') {
|
|
86
|
-
// Block execution for protected patterns
|
|
87
|
-
for (const pattern of policy.include) {
|
|
88
|
-
const ocPattern = pattern
|
|
89
|
-
.replace(/\*\*\//g, '*/')
|
|
90
|
-
.replace(/\*\*/g, '*');
|
|
91
|
-
|
|
92
|
-
// For migrations, block common migration commands
|
|
93
|
-
if (pattern.includes('migrat')) {
|
|
94
|
-
rules['*migrate*'] = 'deny';
|
|
95
|
-
rules['prisma migrate*'] = 'deny';
|
|
96
|
-
rules['npx prisma migrate*'] = 'deny';
|
|
97
|
-
rules['drizzle-kit *'] = 'deny';
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return rules;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Install veto-leash permissions into OpenCode config
|
|
107
|
-
*/
|
|
108
|
-
export async function installOpenCodePermissions(
|
|
109
|
-
target: 'global' | 'project' = 'project'
|
|
110
|
-
): Promise<void> {
|
|
111
|
-
console.log(
|
|
112
|
-
`\n${COLORS.info}Installing veto-leash for OpenCode (${target})...${COLORS.reset}\n`
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
// Load existing policies
|
|
116
|
-
const storedPolicies = loadStoredPolicies();
|
|
117
|
-
if (storedPolicies.policies.length === 0) {
|
|
118
|
-
console.log(
|
|
119
|
-
`${COLORS.warning}${SYMBOLS.warning} No policies found. Add policies first:${COLORS.reset}`
|
|
120
|
-
);
|
|
121
|
-
console.log(` ${COLORS.dim}leash add "don't delete test files"${COLORS.reset}\n`);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Determine config file path
|
|
126
|
-
const configPath =
|
|
127
|
-
target === 'global' ? OPENCODE_GLOBAL_CONFIG : OPENCODE_PROJECT_CONFIG;
|
|
128
|
-
|
|
129
|
-
// Load existing config
|
|
130
|
-
let config: OpenCodeConfig = {
|
|
131
|
-
$schema: 'https://opencode.ai/config.json',
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
if (existsSync(configPath)) {
|
|
135
|
-
try {
|
|
136
|
-
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
137
|
-
} catch {
|
|
138
|
-
// Keep default if parse fails
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Ensure permission object exists
|
|
143
|
-
if (!config.permission) {
|
|
144
|
-
config.permission = {};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Convert existing bash permission to object if it's a string
|
|
148
|
-
if (typeof config.permission.bash === 'string') {
|
|
149
|
-
const oldValue = config.permission.bash;
|
|
150
|
-
config.permission.bash = { '*': oldValue };
|
|
151
|
-
} else if (!config.permission.bash) {
|
|
152
|
-
config.permission.bash = {};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Generate and merge rules from all policies
|
|
156
|
-
for (const { policy } of storedPolicies.policies) {
|
|
157
|
-
const rules = policyToOpenCodeRules(policy);
|
|
158
|
-
config.permission.bash = {
|
|
159
|
-
...(config.permission.bash as Record<string, string>),
|
|
160
|
-
...rules,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Write config
|
|
165
|
-
if (target === 'global') {
|
|
166
|
-
mkdirSync(join(homedir(), '.config', 'opencode'), { recursive: true });
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
170
|
-
console.log(
|
|
171
|
-
` ${COLORS.success}${SYMBOLS.success}${COLORS.reset} Updated: ${configPath}`
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
console.log(
|
|
175
|
-
`\n${COLORS.success}${SYMBOLS.success} veto-leash permissions installed for OpenCode${COLORS.reset}\n`
|
|
176
|
-
);
|
|
177
|
-
console.log(`${COLORS.dim}Policies are now enforced via OpenCode's native permission system.${COLORS.reset}`);
|
|
178
|
-
console.log(`${COLORS.dim}OpenCode will deny matching commands automatically.${COLORS.reset}\n`);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Show what permissions would be generated without installing
|
|
183
|
-
*/
|
|
184
|
-
export function previewOpenCodePermissions(): void {
|
|
185
|
-
const storedPolicies = loadStoredPolicies();
|
|
186
|
-
|
|
187
|
-
if (storedPolicies.policies.length === 0) {
|
|
188
|
-
console.log(`\n${COLORS.warning}No policies stored. Add policies first.${COLORS.reset}\n`);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
console.log(`\n${COLORS.bold}OpenCode Permission Preview${COLORS.reset}`);
|
|
193
|
-
console.log('═'.repeat(30) + '\n');
|
|
194
|
-
|
|
195
|
-
const allRules: Record<string, string> = {};
|
|
196
|
-
|
|
197
|
-
for (const { restriction, policy } of storedPolicies.policies) {
|
|
198
|
-
console.log(`${COLORS.dim}Policy:${COLORS.reset} "${restriction}"`);
|
|
199
|
-
console.log(`${COLORS.dim}Action:${COLORS.reset} ${policy.action}\n`);
|
|
200
|
-
|
|
201
|
-
const rules = policyToOpenCodeRules(policy);
|
|
202
|
-
Object.assign(allRules, rules);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
console.log(`${COLORS.bold}Generated Rules:${COLORS.reset}`);
|
|
206
|
-
console.log(JSON.stringify({ permission: { bash: allRules } }, null, 2));
|
|
207
|
-
console.log(`\n${COLORS.dim}Run 'leash install oc' to apply.${COLORS.reset}\n`);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Remove veto-leash permissions from OpenCode config
|
|
212
|
-
*/
|
|
213
|
-
export async function uninstallOpenCodePermissions(
|
|
214
|
-
target: 'global' | 'project' = 'project'
|
|
215
|
-
): Promise<void> {
|
|
216
|
-
const configPath =
|
|
217
|
-
target === 'global' ? OPENCODE_GLOBAL_CONFIG : OPENCODE_PROJECT_CONFIG;
|
|
218
|
-
|
|
219
|
-
if (!existsSync(configPath)) {
|
|
220
|
-
console.log(`${COLORS.dim}No config file found at ${configPath}${COLORS.reset}`);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
try {
|
|
225
|
-
const config: OpenCodeConfig = JSON.parse(
|
|
226
|
-
readFileSync(configPath, 'utf-8')
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
// Remove veto-leash rules (those with deny for rm/mv/cp patterns)
|
|
230
|
-
if (config.permission?.bash && typeof config.permission.bash === 'object') {
|
|
231
|
-
const bash = config.permission.bash as Record<string, string>;
|
|
232
|
-
for (const key of Object.keys(bash)) {
|
|
233
|
-
if (
|
|
234
|
-
key.startsWith('rm ') ||
|
|
235
|
-
key.startsWith('git rm ') ||
|
|
236
|
-
key.startsWith('mv ') ||
|
|
237
|
-
key.startsWith('cp ')
|
|
238
|
-
) {
|
|
239
|
-
delete bash[key];
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
245
|
-
console.log(
|
|
246
|
-
`${COLORS.success}${SYMBOLS.success} Removed veto-leash rules from ${configPath}${COLORS.reset}`
|
|
247
|
-
);
|
|
248
|
-
} catch {
|
|
249
|
-
console.log(
|
|
250
|
-
`${COLORS.warning}${SYMBOLS.warning} Could not parse config file${COLORS.reset}`
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Load stored policies from veto-leash config
|
|
257
|
-
*/
|
|
258
|
-
function loadStoredPolicies(): StoredPolicies {
|
|
259
|
-
try {
|
|
260
|
-
if (existsSync(POLICIES_FILE)) {
|
|
261
|
-
return JSON.parse(readFileSync(POLICIES_FILE, 'utf-8'));
|
|
262
|
-
}
|
|
263
|
-
} catch {
|
|
264
|
-
// Return empty if can't load
|
|
265
|
-
}
|
|
266
|
-
return { policies: [] };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Save a policy to the stored policies file
|
|
271
|
-
*/
|
|
272
|
-
export function savePolicy(restriction: string, policy: Policy): void {
|
|
273
|
-
const stored = loadStoredPolicies();
|
|
274
|
-
|
|
275
|
-
// Check for duplicate
|
|
276
|
-
const existingIndex = stored.policies.findIndex(
|
|
277
|
-
(p) => p.restriction === restriction
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
if (existingIndex >= 0) {
|
|
281
|
-
stored.policies[existingIndex] = { restriction, policy };
|
|
282
|
-
} else {
|
|
283
|
-
stored.policies.push({ restriction, policy });
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
mkdirSync(VETO_LEASH_CONFIG_DIR, { recursive: true });
|
|
287
|
-
writeFileSync(POLICIES_FILE, JSON.stringify(stored, null, 2));
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* List all stored policies
|
|
292
|
-
*/
|
|
293
|
-
export function listPolicies(): void {
|
|
294
|
-
const stored = loadStoredPolicies();
|
|
295
|
-
|
|
296
|
-
if (stored.policies.length === 0) {
|
|
297
|
-
console.log(`\n${COLORS.dim}No policies stored.${COLORS.reset}\n`);
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
console.log(`\n${COLORS.bold}Stored Policies${COLORS.reset}`);
|
|
302
|
-
console.log('═'.repeat(20) + '\n');
|
|
303
|
-
|
|
304
|
-
for (let i = 0; i < stored.policies.length; i++) {
|
|
305
|
-
const { restriction, policy } = stored.policies[i];
|
|
306
|
-
console.log(`${i + 1}. ${COLORS.info}"${restriction}"${COLORS.reset}`);
|
|
307
|
-
console.log(` ${COLORS.dim}Action:${COLORS.reset} ${policy.action}`);
|
|
308
|
-
console.log(` ${COLORS.dim}Description:${COLORS.reset} ${policy.description}\n`);
|
|
309
|
-
}
|
|
310
|
-
}
|