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.
Files changed (135) hide show
  1. package/IMPLEMENTATION_PLAN.md +2194 -0
  2. package/LICENSE +201 -0
  3. package/README.md +260 -0
  4. package/dist/audit/index.d.ts +38 -0
  5. package/dist/audit/index.d.ts.map +1 -0
  6. package/dist/audit/index.js +132 -0
  7. package/dist/audit/index.js.map +1 -0
  8. package/dist/cli.d.ts +3 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +406 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/cloud/index.d.ts +40 -0
  13. package/dist/cloud/index.d.ts.map +1 -0
  14. package/dist/cloud/index.js +115 -0
  15. package/dist/cloud/index.js.map +1 -0
  16. package/dist/compiler/builtins.d.ts +6 -0
  17. package/dist/compiler/builtins.d.ts.map +1 -0
  18. package/dist/compiler/builtins.js +129 -0
  19. package/dist/compiler/builtins.js.map +1 -0
  20. package/dist/compiler/cache.d.ts +6 -0
  21. package/dist/compiler/cache.d.ts.map +1 -0
  22. package/dist/compiler/cache.js +49 -0
  23. package/dist/compiler/cache.js.map +1 -0
  24. package/dist/compiler/index.d.ts +3 -0
  25. package/dist/compiler/index.d.ts.map +1 -0
  26. package/dist/compiler/index.js +48 -0
  27. package/dist/compiler/index.js.map +1 -0
  28. package/dist/compiler/llm.d.ts +3 -0
  29. package/dist/compiler/llm.d.ts.map +1 -0
  30. package/dist/compiler/llm.js +69 -0
  31. package/dist/compiler/llm.js.map +1 -0
  32. package/dist/compiler/prompt.d.ts +2 -0
  33. package/dist/compiler/prompt.d.ts.map +1 -0
  34. package/dist/compiler/prompt.js +37 -0
  35. package/dist/compiler/prompt.js.map +1 -0
  36. package/dist/config/loader.d.ts +22 -0
  37. package/dist/config/loader.d.ts.map +1 -0
  38. package/dist/config/loader.js +100 -0
  39. package/dist/config/loader.js.map +1 -0
  40. package/dist/config/schema.d.ts +42 -0
  41. package/dist/config/schema.d.ts.map +1 -0
  42. package/dist/config/schema.js +93 -0
  43. package/dist/config/schema.js.map +1 -0
  44. package/dist/matcher.d.ts +22 -0
  45. package/dist/matcher.d.ts.map +1 -0
  46. package/dist/matcher.js +69 -0
  47. package/dist/matcher.js.map +1 -0
  48. package/dist/native/aider.d.ts +10 -0
  49. package/dist/native/aider.d.ts.map +1 -0
  50. package/dist/native/aider.js +120 -0
  51. package/dist/native/aider.js.map +1 -0
  52. package/dist/native/claude-code.d.ts +14 -0
  53. package/dist/native/claude-code.d.ts.map +1 -0
  54. package/dist/native/claude-code.js +273 -0
  55. package/dist/native/claude-code.js.map +1 -0
  56. package/dist/native/cursor.d.ts +11 -0
  57. package/dist/native/cursor.d.ts.map +1 -0
  58. package/dist/native/cursor.js +105 -0
  59. package/dist/native/cursor.js.map +1 -0
  60. package/dist/native/index.d.ts +35 -0
  61. package/dist/native/index.d.ts.map +1 -0
  62. package/dist/native/index.js +171 -0
  63. package/dist/native/index.js.map +1 -0
  64. package/dist/native/opencode.d.ts +22 -0
  65. package/dist/native/opencode.d.ts.map +1 -0
  66. package/dist/native/opencode.js +225 -0
  67. package/dist/native/opencode.js.map +1 -0
  68. package/dist/native/windsurf.d.ts +14 -0
  69. package/dist/native/windsurf.d.ts.map +1 -0
  70. package/dist/native/windsurf.js +198 -0
  71. package/dist/native/windsurf.js.map +1 -0
  72. package/dist/types.d.ts +38 -0
  73. package/dist/types.d.ts.map +1 -0
  74. package/dist/types.js +11 -0
  75. package/dist/types.js.map +1 -0
  76. package/dist/ui/colors.d.ts +21 -0
  77. package/dist/ui/colors.d.ts.map +1 -0
  78. package/dist/ui/colors.js +41 -0
  79. package/dist/ui/colors.js.map +1 -0
  80. package/dist/watchdog/index.d.ts +25 -0
  81. package/dist/watchdog/index.d.ts.map +1 -0
  82. package/dist/watchdog/index.js +57 -0
  83. package/dist/watchdog/index.js.map +1 -0
  84. package/dist/watchdog/restore.d.ts +16 -0
  85. package/dist/watchdog/restore.d.ts.map +1 -0
  86. package/dist/watchdog/restore.js +56 -0
  87. package/dist/watchdog/restore.js.map +1 -0
  88. package/dist/watchdog/snapshot.d.ts +38 -0
  89. package/dist/watchdog/snapshot.d.ts.map +1 -0
  90. package/dist/watchdog/snapshot.js +166 -0
  91. package/dist/watchdog/snapshot.js.map +1 -0
  92. package/dist/watchdog/watcher.d.ts +28 -0
  93. package/dist/watchdog/watcher.d.ts.map +1 -0
  94. package/dist/watchdog/watcher.js +117 -0
  95. package/dist/watchdog/watcher.js.map +1 -0
  96. package/dist/wrapper/daemon.d.ts +12 -0
  97. package/dist/wrapper/daemon.d.ts.map +1 -0
  98. package/dist/wrapper/daemon.js +103 -0
  99. package/dist/wrapper/daemon.js.map +1 -0
  100. package/dist/wrapper/shims.d.ts +4 -0
  101. package/dist/wrapper/shims.d.ts.map +1 -0
  102. package/dist/wrapper/shims.js +390 -0
  103. package/dist/wrapper/shims.js.map +1 -0
  104. package/dist/wrapper/spawn.d.ts +4 -0
  105. package/dist/wrapper/spawn.d.ts.map +1 -0
  106. package/dist/wrapper/spawn.js +35 -0
  107. package/dist/wrapper/spawn.js.map +1 -0
  108. package/package.json +46 -0
  109. package/src/audit/index.ts +172 -0
  110. package/src/cli.ts +503 -0
  111. package/src/cloud/index.ts +139 -0
  112. package/src/compiler/builtins.ts +137 -0
  113. package/src/compiler/cache.ts +51 -0
  114. package/src/compiler/index.ts +59 -0
  115. package/src/compiler/llm.ts +83 -0
  116. package/src/compiler/prompt.ts +37 -0
  117. package/src/config/loader.ts +126 -0
  118. package/src/config/schema.ts +136 -0
  119. package/src/matcher.ts +89 -0
  120. package/src/native/aider.ts +150 -0
  121. package/src/native/claude-code.ts +308 -0
  122. package/src/native/cursor.ts +131 -0
  123. package/src/native/index.ts +233 -0
  124. package/src/native/opencode.ts +310 -0
  125. package/src/native/windsurf.ts +231 -0
  126. package/src/types.ts +48 -0
  127. package/src/ui/colors.ts +50 -0
  128. package/src/watchdog/index.ts +82 -0
  129. package/src/watchdog/restore.ts +74 -0
  130. package/src/watchdog/snapshot.ts +209 -0
  131. package/src/watchdog/watcher.ts +150 -0
  132. package/src/wrapper/daemon.ts +133 -0
  133. package/src/wrapper/shims.ts +409 -0
  134. package/src/wrapper/spawn.ts +47 -0
  135. 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
+ }