veto-leash 0.1.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/IMPLEMENTATION_PLAN.md +2194 -0
- package/LICENSE +201 -0
- package/README.md +260 -0
- package/dist/audit/index.d.ts +38 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +132 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +406 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloud/index.d.ts +40 -0
- package/dist/cloud/index.d.ts.map +1 -0
- package/dist/cloud/index.js +115 -0
- package/dist/cloud/index.js.map +1 -0
- package/dist/compiler/builtins.d.ts +6 -0
- package/dist/compiler/builtins.d.ts.map +1 -0
- package/dist/compiler/builtins.js +129 -0
- package/dist/compiler/builtins.js.map +1 -0
- package/dist/compiler/cache.d.ts +6 -0
- package/dist/compiler/cache.d.ts.map +1 -0
- package/dist/compiler/cache.js +49 -0
- package/dist/compiler/cache.js.map +1 -0
- package/dist/compiler/index.d.ts +3 -0
- package/dist/compiler/index.d.ts.map +1 -0
- package/dist/compiler/index.js +48 -0
- package/dist/compiler/index.js.map +1 -0
- package/dist/compiler/llm.d.ts +3 -0
- package/dist/compiler/llm.d.ts.map +1 -0
- package/dist/compiler/llm.js +69 -0
- package/dist/compiler/llm.js.map +1 -0
- package/dist/compiler/prompt.d.ts +2 -0
- package/dist/compiler/prompt.d.ts.map +1 -0
- package/dist/compiler/prompt.js +37 -0
- package/dist/compiler/prompt.js.map +1 -0
- package/dist/config/loader.d.ts +22 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +100 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +42 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +93 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/matcher.d.ts +22 -0
- package/dist/matcher.d.ts.map +1 -0
- package/dist/matcher.js +69 -0
- package/dist/matcher.js.map +1 -0
- package/dist/native/aider.d.ts +10 -0
- package/dist/native/aider.d.ts.map +1 -0
- package/dist/native/aider.js +120 -0
- package/dist/native/aider.js.map +1 -0
- package/dist/native/claude-code.d.ts +14 -0
- package/dist/native/claude-code.d.ts.map +1 -0
- package/dist/native/claude-code.js +273 -0
- package/dist/native/claude-code.js.map +1 -0
- package/dist/native/cursor.d.ts +11 -0
- package/dist/native/cursor.d.ts.map +1 -0
- package/dist/native/cursor.js +105 -0
- package/dist/native/cursor.js.map +1 -0
- package/dist/native/index.d.ts +35 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +171 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/opencode.d.ts +22 -0
- package/dist/native/opencode.d.ts.map +1 -0
- package/dist/native/opencode.js +225 -0
- package/dist/native/opencode.js.map +1 -0
- package/dist/native/windsurf.d.ts +14 -0
- package/dist/native/windsurf.d.ts.map +1 -0
- package/dist/native/windsurf.js +198 -0
- package/dist/native/windsurf.js.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/colors.d.ts +21 -0
- package/dist/ui/colors.d.ts.map +1 -0
- package/dist/ui/colors.js +41 -0
- package/dist/ui/colors.js.map +1 -0
- package/dist/watchdog/index.d.ts +25 -0
- package/dist/watchdog/index.d.ts.map +1 -0
- package/dist/watchdog/index.js +57 -0
- package/dist/watchdog/index.js.map +1 -0
- package/dist/watchdog/restore.d.ts +16 -0
- package/dist/watchdog/restore.d.ts.map +1 -0
- package/dist/watchdog/restore.js +56 -0
- package/dist/watchdog/restore.js.map +1 -0
- package/dist/watchdog/snapshot.d.ts +38 -0
- package/dist/watchdog/snapshot.d.ts.map +1 -0
- package/dist/watchdog/snapshot.js +166 -0
- package/dist/watchdog/snapshot.js.map +1 -0
- package/dist/watchdog/watcher.d.ts +28 -0
- package/dist/watchdog/watcher.d.ts.map +1 -0
- package/dist/watchdog/watcher.js +117 -0
- package/dist/watchdog/watcher.js.map +1 -0
- package/dist/wrapper/daemon.d.ts +12 -0
- package/dist/wrapper/daemon.d.ts.map +1 -0
- package/dist/wrapper/daemon.js +103 -0
- package/dist/wrapper/daemon.js.map +1 -0
- package/dist/wrapper/shims.d.ts +4 -0
- package/dist/wrapper/shims.d.ts.map +1 -0
- package/dist/wrapper/shims.js +390 -0
- package/dist/wrapper/shims.js.map +1 -0
- package/dist/wrapper/spawn.d.ts +4 -0
- package/dist/wrapper/spawn.d.ts.map +1 -0
- package/dist/wrapper/spawn.js +35 -0
- package/dist/wrapper/spawn.js.map +1 -0
- package/package.json +46 -0
- package/src/audit/index.ts +172 -0
- package/src/cli.ts +503 -0
- package/src/cloud/index.ts +139 -0
- package/src/compiler/builtins.ts +137 -0
- package/src/compiler/cache.ts +51 -0
- package/src/compiler/index.ts +59 -0
- package/src/compiler/llm.ts +83 -0
- package/src/compiler/prompt.ts +37 -0
- package/src/config/loader.ts +126 -0
- package/src/config/schema.ts +136 -0
- package/src/matcher.ts +89 -0
- package/src/native/aider.ts +150 -0
- package/src/native/claude-code.ts +308 -0
- package/src/native/cursor.ts +131 -0
- package/src/native/index.ts +233 -0
- package/src/native/opencode.ts +310 -0
- package/src/native/windsurf.ts +231 -0
- package/src/types.ts +48 -0
- package/src/ui/colors.ts +50 -0
- package/src/watchdog/index.ts +82 -0
- package/src/watchdog/restore.ts +74 -0
- package/src/watchdog/snapshot.ts +209 -0
- package/src/watchdog/watcher.ts +150 -0
- package/src/wrapper/daemon.ts +133 -0
- package/src/wrapper/shims.ts +409 -0
- package/src/wrapper/spawn.ts +47 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,233 @@
|
|
|
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';
|
|
@@ -0,0 +1,310 @@
|
|
|
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
|
+
}
|