wave-agent-sdk 1.0.5 → 1.0.6

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 (193) hide show
  1. package/dist/agent.d.ts +23 -1
  2. package/dist/agent.js +34 -4
  3. package/dist/managers/aiManager.d.ts +36 -0
  4. package/dist/managers/aiManager.js +240 -8
  5. package/dist/managers/messageManager.d.ts +1 -3
  6. package/dist/managers/messageManager.js +13 -6
  7. package/dist/managers/slashCommandManager.js +28 -0
  8. package/dist/managers/subagentManager.d.ts +0 -2
  9. package/dist/telemetry/instrumentation.d.ts +1 -1
  10. package/dist/telemetry/instrumentation.js +57 -28
  11. package/dist/tools/bashTool.js +16 -6
  12. package/dist/tools/grepTool.js +11 -3
  13. package/dist/types/index.d.ts +3 -0
  14. package/dist/types/messaging.d.ts +2 -0
  15. package/dist/utils/containerSetup.js +0 -3
  16. package/dist/utils/messageOperations.d.ts +2 -1
  17. package/dist/utils/messageOperations.js +2 -1
  18. package/dist/utils/notificationXml.js +5 -0
  19. package/dist/utils/ripgrep.d.ts +4 -4
  20. package/dist/utils/ripgrep.js +4 -27
  21. package/package.json +20 -8
  22. package/bin/rg +0 -79
  23. package/dist/managers/forkedAgentManager.d.ts +0 -50
  24. package/dist/managers/forkedAgentManager.js +0 -130
  25. package/scripts/install_ripgrep.js +0 -111
  26. package/scripts/postinstall.js +0 -38
  27. package/src/agent.ts +0 -1271
  28. package/src/constants/subagents.ts +0 -4
  29. package/src/constants/toolLimits.ts +0 -15
  30. package/src/constants/tools.ts +0 -24
  31. package/src/core/plugin.ts +0 -237
  32. package/src/core/session.ts +0 -9
  33. package/src/index.ts +0 -38
  34. package/src/managers/MemoryRuleManager.ts +0 -198
  35. package/src/managers/aiManager.ts +0 -2429
  36. package/src/managers/backgroundTaskManager.ts +0 -480
  37. package/src/managers/bangManager.ts +0 -119
  38. package/src/managers/cronManager.ts +0 -401
  39. package/src/managers/foregroundTaskManager.ts +0 -30
  40. package/src/managers/forkedAgentManager.ts +0 -216
  41. package/src/managers/hookManager.ts +0 -1089
  42. package/src/managers/liveConfigManager.ts +0 -445
  43. package/src/managers/lspManager.ts +0 -470
  44. package/src/managers/mcpManager.ts +0 -1010
  45. package/src/managers/messageManager.ts +0 -1184
  46. package/src/managers/messageQueue.ts +0 -157
  47. package/src/managers/permissionManager.ts +0 -1140
  48. package/src/managers/planManager.ts +0 -112
  49. package/src/managers/pluginManager.ts +0 -330
  50. package/src/managers/pluginScopeManager.ts +0 -124
  51. package/src/managers/reversionManager.ts +0 -178
  52. package/src/managers/skillManager.ts +0 -578
  53. package/src/managers/slashCommandManager.ts +0 -564
  54. package/src/managers/subagentManager.ts +0 -915
  55. package/src/managers/toolManager.ts +0 -489
  56. package/src/managers/workflowManager.ts +0 -493
  57. package/src/prompts/autoMemory.ts +0 -33
  58. package/src/prompts/autoMemoryExtraction.ts +0 -146
  59. package/src/prompts/index.ts +0 -506
  60. package/src/prompts/planModeReminders.ts +0 -126
  61. package/src/services/GitService.ts +0 -131
  62. package/src/services/MarketplaceService.ts +0 -1052
  63. package/src/services/MemoryRuleService.ts +0 -71
  64. package/src/services/aiService.ts +0 -919
  65. package/src/services/authService.ts +0 -565
  66. package/src/services/autoMemoryService.ts +0 -280
  67. package/src/services/configurationService.ts +0 -1520
  68. package/src/services/fileWatcher.ts +0 -319
  69. package/src/services/hook.ts +0 -348
  70. package/src/services/initializationService.ts +0 -357
  71. package/src/services/interactionService.ts +0 -229
  72. package/src/services/jsonlHandler.ts +0 -295
  73. package/src/services/memory.ts +0 -234
  74. package/src/services/pluginLoader.ts +0 -252
  75. package/src/services/remoteSettingsService.ts +0 -366
  76. package/src/services/reversionService.ts +0 -122
  77. package/src/services/session.ts +0 -917
  78. package/src/services/taskManager.ts +0 -321
  79. package/src/telemetry/events.ts +0 -65
  80. package/src/telemetry/instrumentation.ts +0 -499
  81. package/src/telemetry/sessionTracing.ts +0 -348
  82. package/src/tools/agentTool.ts +0 -322
  83. package/src/tools/askUserQuestion.ts +0 -160
  84. package/src/tools/bashTool.ts +0 -621
  85. package/src/tools/buildTool.ts +0 -61
  86. package/src/tools/cronCreateTool.ts +0 -161
  87. package/src/tools/cronDeleteTool.ts +0 -51
  88. package/src/tools/cronListTool.ts +0 -47
  89. package/src/tools/editTool.ts +0 -313
  90. package/src/tools/enterPlanMode.ts +0 -124
  91. package/src/tools/enterWorktreeTool.ts +0 -190
  92. package/src/tools/exitPlanMode.ts +0 -139
  93. package/src/tools/exitWorktreeTool.ts +0 -236
  94. package/src/tools/globTool.ts +0 -167
  95. package/src/tools/grepTool.ts +0 -399
  96. package/src/tools/lspTool.ts +0 -883
  97. package/src/tools/readTool.ts +0 -426
  98. package/src/tools/skillTool.ts +0 -248
  99. package/src/tools/taskManagementTools.ts +0 -574
  100. package/src/tools/taskStopTool.ts +0 -78
  101. package/src/tools/types.ts +0 -137
  102. package/src/tools/webFetchTool.ts +0 -373
  103. package/src/tools/workflowTool.ts +0 -205
  104. package/src/tools/writeTool.ts +0 -267
  105. package/src/types/agent.ts +0 -128
  106. package/src/types/auth.ts +0 -19
  107. package/src/types/commands.ts +0 -30
  108. package/src/types/config.ts +0 -43
  109. package/src/types/configuration.ts +0 -168
  110. package/src/types/core.ts +0 -105
  111. package/src/types/cron.ts +0 -13
  112. package/src/types/environment.ts +0 -106
  113. package/src/types/fileSearch.ts +0 -4
  114. package/src/types/history.ts +0 -7
  115. package/src/types/hooks.ts +0 -256
  116. package/src/types/index.ts +0 -41
  117. package/src/types/lsp.ts +0 -98
  118. package/src/types/marketplace.ts +0 -71
  119. package/src/types/mcp.ts +0 -43
  120. package/src/types/memoryRule.ts +0 -31
  121. package/src/types/messaging.ts +0 -114
  122. package/src/types/permissions.ts +0 -80
  123. package/src/types/plugins.ts +0 -39
  124. package/src/types/processes.ts +0 -99
  125. package/src/types/reversion.ts +0 -29
  126. package/src/types/session.ts +0 -12
  127. package/src/types/skills.ts +0 -108
  128. package/src/types/tasks.ts +0 -13
  129. package/src/types/telemetry.ts +0 -98
  130. package/src/types/tools.ts +0 -44
  131. package/src/types/workflow.ts +0 -6
  132. package/src/utils/abortUtils.ts +0 -118
  133. package/src/utils/atomicWrite.ts +0 -61
  134. package/src/utils/bashParser.ts +0 -899
  135. package/src/utils/cacheControlUtils.ts +0 -426
  136. package/src/utils/commandArgumentParser.ts +0 -161
  137. package/src/utils/commandPathResolver.ts +0 -88
  138. package/src/utils/configPaths.ts +0 -220
  139. package/src/utils/configValidator.ts +0 -100
  140. package/src/utils/constants.ts +0 -39
  141. package/src/utils/container.ts +0 -92
  142. package/src/utils/containerSetup.ts +0 -371
  143. package/src/utils/convertMessagesForAPI.ts +0 -326
  144. package/src/utils/cronTasks.ts +0 -128
  145. package/src/utils/cronTasksLock.ts +0 -182
  146. package/src/utils/cronToHuman.ts +0 -99
  147. package/src/utils/customCommands.ts +0 -95
  148. package/src/utils/editUtils.ts +0 -20
  149. package/src/utils/fileFormat.ts +0 -40
  150. package/src/utils/fileSearch.ts +0 -151
  151. package/src/utils/fileUtils.ts +0 -223
  152. package/src/utils/gitUtils.ts +0 -315
  153. package/src/utils/globalLogger.ts +0 -128
  154. package/src/utils/groupMessagesByApiRound.ts +0 -121
  155. package/src/utils/hookMatcher.ts +0 -176
  156. package/src/utils/markdownParser.ts +0 -317
  157. package/src/utils/mcpUtils.ts +0 -138
  158. package/src/utils/messageOperations.ts +0 -659
  159. package/src/utils/modelCapabilities.ts +0 -30
  160. package/src/utils/nameGenerator.ts +0 -99
  161. package/src/utils/notificationXml.ts +0 -57
  162. package/src/utils/openaiClient.ts +0 -251
  163. package/src/utils/parseCronExpression.ts +0 -78
  164. package/src/utils/path.ts +0 -72
  165. package/src/utils/pathEncoder.ts +0 -400
  166. package/src/utils/pathSafety.ts +0 -40
  167. package/src/utils/promptHistory.ts +0 -168
  168. package/src/utils/ripgrep.ts +0 -35
  169. package/src/utils/shellResolver.ts +0 -190
  170. package/src/utils/skillParser.ts +0 -246
  171. package/src/utils/stringUtils.ts +0 -156
  172. package/src/utils/subagentParser.ts +0 -279
  173. package/src/utils/taskReminder.ts +0 -96
  174. package/src/utils/tokenCalculation.ts +0 -43
  175. package/src/utils/tokenEstimate.ts +0 -34
  176. package/src/utils/toolResultStorage.ts +0 -117
  177. package/src/utils/worktreeSession.ts +0 -26
  178. package/src/utils/worktreeUtils.ts +0 -786
  179. package/src/workflow/budgetTracker.ts +0 -34
  180. package/src/workflow/concurrencyLimiter.ts +0 -47
  181. package/src/workflow/journal.ts +0 -95
  182. package/src/workflow/progressReporter.ts +0 -141
  183. package/src/workflow/runState.ts +0 -65
  184. package/src/workflow/scriptRuntime.ts +0 -274
  185. package/src/workflow/structuredOutput.ts +0 -123
  186. package/src/workflow/types.ts +0 -101
  187. package/src/workflow/workflowApis.ts +0 -410
  188. package/vendor/ripgrep/linux-aarch64/rg +0 -0
  189. package/vendor/ripgrep/linux-x86_64/rg +0 -0
  190. package/vendor/ripgrep/macos-aarch64/rg +0 -0
  191. package/vendor/ripgrep/macos-x86_64/rg +0 -0
  192. package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
  193. package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
@@ -1,1140 +0,0 @@
1
- /**
2
- * Permission Manager for handling tool permission checks
3
- *
4
- * This manager provides utilities for checking permissions before tool execution.
5
- * It implements the permission logic for different modes (default vs bypass) and
6
- * handles custom callback integration.
7
- */
8
-
9
- import path from "node:path";
10
- import { minimatch } from "minimatch";
11
- import type {
12
- PermissionDecision,
13
- ToolPermissionContext,
14
- PermissionCallback,
15
- PermissionMode,
16
- } from "../types/permissions.js";
17
- import { RESTRICTED_TOOLS } from "../types/permissions.js";
18
- import type { Logger } from "../types/index.js";
19
- import {
20
- splitBashCommand,
21
- stripEnvVars,
22
- stripRedirections,
23
- hasWriteRedirections,
24
- getSmartPrefix,
25
- isDangerousFind,
26
- hasCommandSubstitution,
27
- hasProcessSubstitution,
28
- hasSedInPlace,
29
- DANGEROUS_COMMANDS,
30
- READ_ONLY_COMMANDS,
31
- } from "../utils/bashParser.js";
32
- import { isPathInside } from "../utils/pathSafety.js";
33
- import {
34
- BASH_TOOL_NAME,
35
- EDIT_TOOL_NAME,
36
- WRITE_TOOL_NAME,
37
- READ_TOOL_NAME,
38
- ASK_USER_QUESTION_TOOL_NAME,
39
- } from "../constants/tools.js";
40
- import { Container } from "../utils/container.js";
41
- import { ConfigurationService } from "../services/configurationService.js";
42
- import type { WorktreeSession } from "../utils/worktreeSession.js";
43
-
44
- const DEFAULT_ALLOWED_RULES = [
45
- "Bash(git status*)",
46
- "Bash(git diff*)",
47
- "Bash(git log*)",
48
- "Bash(git show*)",
49
- "Bash(git branch)",
50
- "Bash(git branch --list*)",
51
- "Bash(git branch -a*)",
52
- "Bash(git branch -r*)",
53
- "Bash(git branch --show-current*)",
54
- "Bash(git branch --merged*)",
55
- "Bash(git branch --no-merged*)",
56
- "Bash(git branch --contains*)",
57
- "Bash(git branch --no-contains*)",
58
- "Bash(git branch --verbose*)",
59
- "Bash(git branch -v*)",
60
- "Bash(git branch -vv*)",
61
- "Bash(git tag*)",
62
- "Bash(git remote*)",
63
- "Bash(git ls-files*)",
64
- "Bash(git rev-parse*)",
65
- "Bash(git config --list*)",
66
- "Bash(git config -l*)",
67
- "Bash(git cat-file*)",
68
- "Bash(git count-objects*)",
69
- "Bash(echo*)",
70
- "Bash(ls*)",
71
- "Bash(find*)",
72
- "Bash(which*)",
73
- "Bash(type*)",
74
- "Bash(hostname*)",
75
- "Bash(whoami*)",
76
- "Bash(date*)",
77
- "Bash(uptime*)",
78
- "Bash(grep*)",
79
- "Bash(rg*)",
80
- "Bash(cat*)",
81
- "Bash(head*)",
82
- "Bash(tail*)",
83
- "Bash(wc*)",
84
- "Bash(sleep*)",
85
- ];
86
-
87
- import { logger } from "../utils/globalLogger.js";
88
-
89
- export interface PermissionManagerOptions {
90
- /** Configured permission mode from settings */
91
- configuredPermissionMode?: PermissionMode;
92
- /** Allowed rules from settings */
93
- allowedRules?: string[];
94
- /** Denied rules from settings */
95
- deniedRules?: string[];
96
- /** Instance-specific allowed rules (from AgentOptions) */
97
- instanceAllowedRules?: string[];
98
- /** Instance-specific denied rules (from AgentOptions) */
99
- instanceDeniedRules?: string[];
100
- /** Additional directories considered part of the Safe Zone */
101
- additionalDirectories?: string[];
102
- /** Instance-specific additional directories (from AgentOptions, session-level) */
103
- instanceAdditionalDirectories?: string[];
104
- /** System additional directories (persistent across reloads) */
105
- systemAdditionalDirectories?: string[];
106
- /** Path to the current plan file */
107
- planFilePath?: string;
108
- /** Optional logger */
109
- logger?: Logger;
110
- }
111
-
112
- export class PermissionManager {
113
- private configuredPermissionMode?: PermissionMode;
114
- private allowedRules: string[] = [];
115
- private deniedRules: string[] = [];
116
- private instanceAllowedRules: string[] = [];
117
- private instanceDeniedRules: string[] = [];
118
- private temporaryRules: string[] = [];
119
- private additionalDirectories: string[] = [];
120
- private instanceAdditionalDirectories: string[] = [];
121
- private systemAdditionalDirectories: string[] = [];
122
- private planFilePath?: string;
123
- private hasExitedPlanMode: boolean = false;
124
- private needsPlanModeExitAttachment: boolean = false;
125
- private workdir?: string;
126
- private worktreeName?: string;
127
- private mainRepoRoot?: string;
128
- private onConfiguredPermissionModeChange?: (mode: PermissionMode) => void;
129
- private _logger?: Logger;
130
-
131
- constructor(
132
- private container: Container,
133
- options: PermissionManagerOptions = {},
134
- ) {
135
- this.configuredPermissionMode = options.configuredPermissionMode;
136
- this.allowedRules = options.allowedRules || [];
137
- this.deniedRules = options.deniedRules || [];
138
- this.instanceAllowedRules = options.instanceAllowedRules || [];
139
- this.instanceDeniedRules = options.instanceDeniedRules || [];
140
- this.planFilePath = options.planFilePath;
141
- this._logger = options.logger;
142
- // Read workdir first so relative additional directories resolve against it.
143
- this.workdir = this.container.get<string>("Workdir");
144
- this.worktreeName = this.container.get<string | undefined>("WorktreeName");
145
- this.mainRepoRoot = this.container.get<string | undefined>("MainRepoRoot");
146
- this.updateAdditionalDirectories(options.additionalDirectories || []);
147
- for (const dir of options.instanceAdditionalDirectories || []) {
148
- this.addInstanceAdditionalDirectory(dir);
149
- }
150
- for (const dir of options.systemAdditionalDirectories || []) {
151
- this.addSystemAdditionalDirectory(dir);
152
- }
153
- }
154
-
155
- /**
156
- * Resolve the working directory from the DI container
157
- */
158
- private getWorkdir(): string | undefined {
159
- return this.container.get<string>("Workdir");
160
- }
161
-
162
- /**
163
- * Set a callback to be notified when the effective permission mode changes due to configuration updates
164
- */
165
- public setOnConfiguredPermissionModeChange(
166
- callback: (mode: PermissionMode) => void,
167
- ): void {
168
- this.onConfiguredPermissionModeChange = callback;
169
- }
170
-
171
- /**
172
- * Update the configured default mode (e.g., when configuration reloads)
173
- */
174
- updateConfiguredPermissionMode(permissionMode?: PermissionMode): void {
175
- const oldEffectiveMode = this.getCurrentEffectiveMode();
176
-
177
- this.configuredPermissionMode = permissionMode;
178
-
179
- const newEffectiveMode = this.getCurrentEffectiveMode();
180
- if (
181
- oldEffectiveMode !== newEffectiveMode &&
182
- this.onConfiguredPermissionModeChange
183
- ) {
184
- this.onConfiguredPermissionModeChange(newEffectiveMode);
185
- }
186
- }
187
-
188
- /**
189
- * Get the configured default mode
190
- */
191
- public getConfiguredPermissionMode(): PermissionMode | undefined {
192
- return this.configuredPermissionMode;
193
- }
194
-
195
- /**
196
- * Get all currently allowed rules (user-defined)
197
- */
198
- public getAllowedRules(): string[] {
199
- return [...this.allowedRules];
200
- }
201
-
202
- /**
203
- * Get all currently denied rules
204
- */
205
- public getDeniedRules(): string[] {
206
- return [...this.deniedRules];
207
- }
208
-
209
- /**
210
- * Get all instance-specific allowed rules
211
- */
212
- public getInstanceAllowedRules(): string[] {
213
- return [...this.instanceAllowedRules];
214
- }
215
-
216
- /**
217
- * Get all instance-specific denied rules
218
- */
219
- public getInstanceDeniedRules(): string[] {
220
- return [...this.instanceDeniedRules];
221
- }
222
-
223
- /**
224
- * Get all additional directories
225
- */
226
- public getAdditionalDirectories(): string[] {
227
- return [...this.additionalDirectories];
228
- }
229
-
230
- /**
231
- * Get all instance-specific additional directories (session-level, from AgentOptions or /add-dir)
232
- */
233
- public getInstanceAdditionalDirectories(): string[] {
234
- return [...this.instanceAdditionalDirectories];
235
- }
236
-
237
- /**
238
- * Get the effective union of additional directories (config + instance, deduplicated).
239
- * This is the data source for system prompt injection and Safe Zone checks.
240
- */
241
- public getEffectiveAdditionalDirectories(): string[] {
242
- const seen = new Set<string>();
243
- const result: string[] = [];
244
- for (const dir of [
245
- ...this.additionalDirectories,
246
- ...this.instanceAdditionalDirectories,
247
- ]) {
248
- if (!seen.has(dir)) {
249
- seen.add(dir);
250
- result.push(dir);
251
- }
252
- }
253
- return result;
254
- }
255
-
256
- /**
257
- * Get all system additional directories
258
- */
259
- public getSystemAdditionalDirectories(): string[] {
260
- return [...this.systemAdditionalDirectories];
261
- }
262
-
263
- /**
264
- * Get all default allowed rules
265
- */
266
- public getDefaultAllowedRules(): string[] {
267
- return [...DEFAULT_ALLOWED_RULES];
268
- }
269
-
270
- /**
271
- * Update the allowed rules (e.g., when configuration reloads)
272
- */
273
- updateAllowedRules(rules: string[]): void {
274
- this.allowedRules = rules;
275
- }
276
-
277
- /**
278
- * Update the denied rules (e.g., when configuration reloads)
279
- */
280
- updateDeniedRules(rules: string[]): void {
281
- this.deniedRules = rules;
282
- }
283
-
284
- /**
285
- * Add temporary rules for the current session
286
- */
287
- public addTemporaryRules(rules: string[]): void {
288
- this.temporaryRules.push(...rules);
289
- }
290
-
291
- /**
292
- * Clear all temporary rules
293
- */
294
- public clearTemporaryRules(): void {
295
- this.temporaryRules = [];
296
- }
297
-
298
- /**
299
- * Update the additional directories (e.g., when configuration reloads)
300
- */
301
- updateAdditionalDirectories(directories: string[]): void {
302
- const workdir = this.workdir;
303
- this.additionalDirectories = directories.map((dir) => {
304
- if (workdir && !path.isAbsolute(dir)) {
305
- return path.resolve(workdir, dir);
306
- }
307
- return path.resolve(dir);
308
- });
309
- }
310
-
311
- /**
312
- * Add an instance-level additional directory (session-level, e.g. /add-dir command)
313
- */
314
- public addInstanceAdditionalDirectory(directory: string): void {
315
- const workdir = this.workdir;
316
- const resolvedPath =
317
- workdir && !path.isAbsolute(directory)
318
- ? path.resolve(workdir, directory)
319
- : path.resolve(directory);
320
-
321
- if (!this.instanceAdditionalDirectories.includes(resolvedPath)) {
322
- this.instanceAdditionalDirectories.push(resolvedPath);
323
- }
324
- }
325
-
326
- /**
327
- * Add a system-level additional directory that is persistent across configuration reloads
328
- */
329
- public addSystemAdditionalDirectory(directory: string): void {
330
- const workdir = this.workdir;
331
- const resolvedPath =
332
- workdir && !path.isAbsolute(directory)
333
- ? path.resolve(workdir, directory)
334
- : path.resolve(directory);
335
-
336
- if (!this.systemAdditionalDirectories.includes(resolvedPath)) {
337
- this.systemAdditionalDirectories.push(resolvedPath);
338
- }
339
- }
340
-
341
- /**
342
- * Set the current plan file path
343
- */
344
- public setPlanFilePath(path: string | undefined): void {
345
- this.planFilePath = path;
346
- }
347
-
348
- /**
349
- * Get the current plan file path
350
- */
351
- public getPlanFilePath(): string | undefined {
352
- return this.planFilePath;
353
- }
354
-
355
- public setHasExitedPlanMode(value: boolean): void {
356
- this.hasExitedPlanMode = value;
357
- }
358
-
359
- public hasExitedPlanModeInSession(): boolean {
360
- return this.hasExitedPlanMode;
361
- }
362
-
363
- public setNeedsPlanModeExitAttachment(value: boolean): void {
364
- this.needsPlanModeExitAttachment = value;
365
- }
366
-
367
- public getNeedsPlanModeExitAttachment(): boolean {
368
- return this.needsPlanModeExitAttachment;
369
- }
370
-
371
- /**
372
- * Public wrapper for isInsideSafeZone to check if a path is in the safe zone
373
- */
374
- public isPathInSafeZone(targetPath: string): boolean {
375
- return this.isInsideSafeZone(targetPath).isInside;
376
- }
377
-
378
- /**
379
- * Check if a path is inside the Safe Zone (workdir + additionalDirectories)
380
- */
381
- private isInsideSafeZone(
382
- targetPath: string,
383
- workdir?: string,
384
- ): { isInside: boolean; resolvedPath: string } {
385
- const effectiveWorkdir = this.workdir || workdir;
386
-
387
- // Resolve the target path relative to effectiveWorkdir if it's not absolute
388
- const absolutePath =
389
- effectiveWorkdir && !path.isAbsolute(targetPath)
390
- ? path.resolve(effectiveWorkdir, targetPath)
391
- : path.resolve(targetPath);
392
-
393
- // Check workdir
394
- if (effectiveWorkdir && isPathInside(absolutePath, effectiveWorkdir)) {
395
- return { isInside: true, resolvedPath: absolutePath };
396
- }
397
-
398
- // Check additional directories
399
- for (const dir of this.additionalDirectories) {
400
- if (isPathInside(absolutePath, dir)) {
401
- return { isInside: true, resolvedPath: absolutePath };
402
- }
403
- }
404
-
405
- // Check instance additional directories
406
- for (const dir of this.instanceAdditionalDirectories) {
407
- if (isPathInside(absolutePath, dir)) {
408
- return { isInside: true, resolvedPath: absolutePath };
409
- }
410
- }
411
-
412
- // Check system additional directories
413
- for (const dir of this.systemAdditionalDirectories) {
414
- if (isPathInside(absolutePath, dir)) {
415
- return { isInside: true, resolvedPath: absolutePath };
416
- }
417
- }
418
-
419
- return { isInside: false, resolvedPath: absolutePath };
420
- }
421
-
422
- /**
423
- * Get the current effective permission mode for tool execution context
424
- */
425
- getCurrentEffectiveMode(cliPermissionMode?: PermissionMode): PermissionMode {
426
- return this.resolveEffectivePermissionMode(cliPermissionMode);
427
- }
428
-
429
- /**
430
- * Resolve the effective permission mode based on CLI override and configured default
431
- */
432
- resolveEffectivePermissionMode(
433
- cliPermissionMode?: PermissionMode,
434
- ): PermissionMode {
435
- // CLI override takes highest precedence
436
- if (cliPermissionMode !== undefined) {
437
- return cliPermissionMode;
438
- }
439
-
440
- // Use configured default mode if available
441
- if (this.configuredPermissionMode !== undefined) {
442
- return this.configuredPermissionMode;
443
- }
444
-
445
- // Fall back to system default
446
- return "default";
447
- }
448
-
449
- /**
450
- * Check if a tool execution requires permission and handle the authorization flow
451
- * Called by individual tools after validation/diff, before real operation
452
- */
453
- async checkPermission(
454
- context: ToolPermissionContext,
455
- ): Promise<PermissionDecision> {
456
- // 0. Check instance-specific denied rules first - Deny always takes precedence
457
- for (const rule of this.instanceDeniedRules) {
458
- if (this.matchesRule(context, rule)) {
459
- logger?.warn("Permission denied by instance rule", {
460
- toolName: context.toolName,
461
- rule,
462
- });
463
- return {
464
- behavior: "deny",
465
- message: `Access to tool '${context.toolName}' is explicitly denied by instance rule: ${rule}`,
466
- };
467
- }
468
- }
469
-
470
- // 0. Check denied rules first - Deny always takes precedence
471
- for (const rule of this.deniedRules) {
472
- if (this.matchesRule(context, rule)) {
473
- logger?.warn("Permission denied by rule", {
474
- toolName: context.toolName,
475
- rule,
476
- });
477
- return {
478
- behavior: "deny",
479
- message: `Access to tool '${context.toolName}' is explicitly denied by rule: ${rule}`,
480
- };
481
- }
482
- }
483
-
484
- // Check worktree safety for Write and Edit tools — unconditional safety
485
- // check, applied regardless of permission mode (same as read-before-edit).
486
- // Support both CLI -w sessions (container-registered) and EnterWorktree mid-session
487
- // (per-agent WorktreeSession stored in this session's container)
488
- const worktreeSession = this.container.get<WorktreeSession | null>(
489
- "WorktreeSession",
490
- );
491
- const effectiveWorktreeName =
492
- this.worktreeName || worktreeSession?.worktreeName;
493
- const effectiveWorkdir = this.workdir || worktreeSession?.worktreePath;
494
- const effectiveMainRepoRoot =
495
- this.mainRepoRoot || worktreeSession?.repoRoot;
496
-
497
- if (
498
- effectiveWorktreeName &&
499
- effectiveMainRepoRoot &&
500
- effectiveWorkdir &&
501
- (context.toolName === WRITE_TOOL_NAME ||
502
- context.toolName === EDIT_TOOL_NAME)
503
- ) {
504
- const targetPath = context.toolInput?.file_path as string | undefined;
505
- if (targetPath) {
506
- const absoluteTargetPath = path.resolve(effectiveWorkdir, targetPath);
507
- const isInsideMainRepo = isPathInside(
508
- absoluteTargetPath,
509
- effectiveMainRepoRoot,
510
- );
511
- const isInsideWorktree = isPathInside(
512
- absoluteTargetPath,
513
- effectiveWorkdir,
514
- );
515
-
516
- // Allow the plan file even if outside worktree
517
- if (this.planFilePath) {
518
- const absolutePlanPath = path.resolve(this.planFilePath);
519
- if (absoluteTargetPath === absolutePlanPath) {
520
- // Fall through — plan file is allowed
521
- } else if (isInsideMainRepo && !isInsideWorktree) {
522
- logger?.warn("Worktree safety violation", {
523
- toolName: context.toolName,
524
- targetPath,
525
- worktreeName: effectiveWorktreeName,
526
- mainRepoRoot: effectiveMainRepoRoot,
527
- workdir: effectiveWorkdir,
528
- });
529
- return {
530
- behavior: "deny",
531
- message: `Access denied: You are currently in a worktree session ("${effectiveWorktreeName}"). Modifying files in the main repository (outside the worktree) is not allowed. Please only modify files within the worktree directory: ${effectiveWorkdir}`,
532
- };
533
- }
534
- } else if (isInsideMainRepo && !isInsideWorktree) {
535
- logger?.warn("Worktree safety violation", {
536
- toolName: context.toolName,
537
- targetPath,
538
- worktreeName: effectiveWorktreeName,
539
- mainRepoRoot: effectiveMainRepoRoot,
540
- workdir: effectiveWorkdir,
541
- });
542
- return {
543
- behavior: "deny",
544
- message: `Access denied: You are currently in a worktree session ("${effectiveWorktreeName}"). Modifying files in the main repository (outside the worktree) is not allowed. Please only modify files within the worktree directory: ${effectiveWorkdir}`,
545
- };
546
- }
547
- }
548
- }
549
-
550
- // If bypassPermissions mode, always allow
551
- // Exception: tools that require user interaction (e.g. AskUserQuestion)
552
- // must still prompt the user, matching Claude Code's requiresUserInteraction behavior.
553
- // Worktree safety check above runs unconditionally, so bypass never skips it.
554
- if (context.permissionMode === "bypassPermissions") {
555
- const requiresUserInteraction =
556
- context.toolName === ASK_USER_QUESTION_TOOL_NAME;
557
- if (!requiresUserInteraction) {
558
- return { behavior: "allow" };
559
- }
560
- }
561
-
562
- // If acceptEdits mode, allow Edit, Write, and mkdir in safe zone
563
- if (context.permissionMode === "acceptEdits") {
564
- const autoAcceptedTools = [EDIT_TOOL_NAME, WRITE_TOOL_NAME];
565
- if (autoAcceptedTools.includes(context.toolName)) {
566
- // Enforce Safe Zone for file operations
567
- const targetPath = context.toolInput?.file_path as string | undefined;
568
- const workdir = context.toolInput?.workdir as string | undefined;
569
-
570
- if (targetPath) {
571
- const { isInside, resolvedPath } = this.isInsideSafeZone(
572
- targetPath,
573
- workdir,
574
- );
575
- if (!isInside) {
576
- logger?.info(
577
- "File operation outside the Safe Zone in acceptEdits mode, falling back to manual confirmation",
578
- {
579
- toolName: context.toolName,
580
- targetPath,
581
- resolvedPath,
582
- },
583
- );
584
- // Fall through to normal permission check flow to trigger confirmation prompt
585
- } else {
586
- return { behavior: "allow" };
587
- }
588
- }
589
- }
590
-
591
- // Special case for mkdir in Bash tool
592
- if (context.toolName === BASH_TOOL_NAME && context.toolInput?.command) {
593
- const command = String(context.toolInput.command).trim();
594
- if (command.startsWith("mkdir ")) {
595
- const parts = splitBashCommand(command);
596
- // Check if it's a simple mkdir command (first part is mkdir)
597
- if (parts.length === 1) {
598
- const processedPart = stripEnvVars(parts[0]);
599
- if (processedPart.startsWith("mkdir ")) {
600
- const args = processedPart.slice(6).trim();
601
- const pathArgs =
602
- (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter(
603
- (arg) => !arg.startsWith("-"),
604
- ) || [];
605
-
606
- if (pathArgs.length > 0) {
607
- const allPathsSafe = pathArgs.every((pathArg) => {
608
- const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
609
- const { isInside } = this.isInsideSafeZone(
610
- cleanPath,
611
- context.toolInput?.workdir as string | undefined,
612
- );
613
- return isInside;
614
- });
615
-
616
- if (allPathsSafe) {
617
- return { behavior: "allow" };
618
- }
619
- }
620
- }
621
- }
622
- }
623
- }
624
- }
625
-
626
- // 1.2 Check if tool call is allowed by persistent or temporary rules
627
- if (this.isAllowedByRule(context)) {
628
- return { behavior: "allow" };
629
- }
630
-
631
- // 1.3 If plan mode, allow Read-only tools and Edit/Write for plan file
632
- if (context.permissionMode === "plan") {
633
- const writeTools = [EDIT_TOOL_NAME, WRITE_TOOL_NAME];
634
- if (writeTools.includes(context.toolName)) {
635
- const targetPath = context.toolInput?.file_path as string | undefined;
636
-
637
- if (this.planFilePath && targetPath) {
638
- const absoluteTargetPath = path.resolve(targetPath);
639
- const absolutePlanPath = path.resolve(this.planFilePath);
640
-
641
- if (absoluteTargetPath === absolutePlanPath) {
642
- return { behavior: "allow" };
643
- }
644
- }
645
-
646
- return {
647
- behavior: "deny",
648
- message: `In plan mode, you are only allowed to edit the designated plan file: ${this.planFilePath || "not set"}.`,
649
- };
650
- }
651
- }
652
-
653
- // 2. If not a restricted tool, always allow
654
- if (!this.isRestrictedTool(context.toolName)) {
655
- return { behavior: "allow" };
656
- }
657
-
658
- // 2.1 If dontAsk mode, auto-deny restricted tools that were not allowed by rules above
659
- if (context.permissionMode === "dontAsk") {
660
- logger?.info("Restricted tool automatically denied in dontAsk mode", {
661
- toolName: context.toolName,
662
- });
663
- return {
664
- behavior: "deny",
665
- message: `Tool '${context.toolName}' was automatically denied because 'dontAsk' permission mode is active and no pre-approved rule matches this call.`,
666
- };
667
- }
668
-
669
- // 3. If custom callback provided, call it and return result
670
- if (context.canUseToolCallback) {
671
- try {
672
- const decision = await context.canUseToolCallback(context);
673
- if (decision.behavior !== "allow") {
674
- logger?.debug("Custom callback returned decision", {
675
- toolName: context.toolName,
676
- decision,
677
- });
678
- }
679
- return decision;
680
- } catch (error) {
681
- const errorMessage =
682
- error instanceof Error ? error.message : String(error);
683
- logger?.error("Error in permission callback", {
684
- toolName: context.toolName,
685
- error: errorMessage,
686
- });
687
- return {
688
- behavior: "deny",
689
- message: "Error in permission callback",
690
- };
691
- }
692
- }
693
-
694
- // 4. For default mode on restricted tools without callback, integrate with CLI confirmation
695
- // Note: CLI confirmation integration will be implemented in Phase 2
696
- logger?.warn(
697
- "No permission callback provided for restricted tool in default mode",
698
- {
699
- toolName: context.toolName,
700
- },
701
- );
702
- return {
703
- behavior: "deny",
704
- message: `Tool '${context.toolName}' requires permission approval. No permission callback configured.`,
705
- };
706
- }
707
-
708
- /**
709
- * Determine if a tool requires permission checks based on its name
710
- */
711
- isRestrictedTool(toolName: string): boolean {
712
- return (
713
- (RESTRICTED_TOOLS as readonly string[]).includes(toolName) ||
714
- toolName.startsWith("mcp__")
715
- );
716
- }
717
-
718
- /**
719
- * Check if a tool is completely denied by name in instance or global rules
720
- */
721
- public isToolDenied(toolName: string): boolean {
722
- // Check instance-specific denied rules
723
- if (this.instanceDeniedRules.includes(toolName)) {
724
- return true;
725
- }
726
-
727
- // Check global denied rules
728
- if (this.deniedRules.includes(toolName)) {
729
- return true;
730
- }
731
-
732
- return false;
733
- }
734
-
735
- /**
736
- * Helper method to create a permission context for CLI integration
737
- */
738
- createContext(
739
- toolName: string,
740
- permissionMode: PermissionMode,
741
- callback?: PermissionCallback,
742
- toolInput?: Record<string, unknown>,
743
- toolCallId?: string,
744
- planContent?: string,
745
- ): ToolPermissionContext {
746
- let suggestedPrefix: string | undefined;
747
- if (toolName === BASH_TOOL_NAME && toolInput?.command) {
748
- const command = String(toolInput.command);
749
- const parts = splitBashCommand(command);
750
- // Only suggest prefix for single commands to avoid confusion with complex chains
751
- if (parts.length === 1) {
752
- const processedPart = stripRedirections(stripEnvVars(parts[0]));
753
- suggestedPrefix = getSmartPrefix(processedPart) ?? undefined;
754
- }
755
- } else if (toolName.startsWith("mcp__")) {
756
- suggestedPrefix = toolName;
757
- }
758
-
759
- const context: ToolPermissionContext = {
760
- toolName,
761
- permissionMode,
762
- canUseToolCallback: callback,
763
- toolInput,
764
- suggestedPrefix,
765
- toolCallId,
766
- planContent,
767
- };
768
-
769
- // Set hidePersistentOption for out-of-bounds file operations
770
- const fileTools = [EDIT_TOOL_NAME, WRITE_TOOL_NAME];
771
- if (fileTools.includes(toolName)) {
772
- const targetPath = toolInput?.file_path as string | undefined;
773
- const workdir = toolInput?.workdir as string | undefined;
774
-
775
- if (targetPath) {
776
- const { isInside } = this.isInsideSafeZone(targetPath, workdir);
777
- if (!isInside) {
778
- context.hidePersistentOption = true;
779
- }
780
- }
781
- }
782
-
783
- // Set hidePersistentOption for dangerous or out-of-bounds bash commands
784
- if (toolName === BASH_TOOL_NAME && toolInput?.command) {
785
- const command = String(toolInput.command);
786
- const workdir = toolInput.workdir as string | undefined;
787
- const parts = splitBashCommand(command);
788
-
789
- const isDangerous = parts.some((part) => {
790
- if (hasWriteRedirections(part)) {
791
- return true;
792
- }
793
- // Command/process substitution and sed -i are dangerous (FR-019.5, FR-019.6)
794
- if (
795
- hasCommandSubstitution(part) ||
796
- hasProcessSubstitution(part) ||
797
- hasSedInPlace(part)
798
- ) {
799
- return true;
800
- }
801
- const processedPart = stripRedirections(stripEnvVars(part));
802
- const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
803
- if (commandMatch) {
804
- const cmd = commandMatch[1];
805
- const args = commandMatch[2]?.trim() || "";
806
-
807
- // Check blacklist
808
- if (DANGEROUS_COMMANDS.includes(cmd)) {
809
- return true;
810
- }
811
-
812
- // Check out-of-bounds for cd
813
- if (cmd === "cd") {
814
- const pathArgs =
815
- (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter(
816
- (arg) => !arg.startsWith("-"),
817
- ) || [];
818
-
819
- return pathArgs.some((pathArg) => {
820
- const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
821
- const { isInside } = this.isInsideSafeZone(cleanPath, workdir);
822
- return !isInside;
823
- });
824
- }
825
- }
826
- return false;
827
- });
828
-
829
- if (isDangerous) {
830
- context.hidePersistentOption = true;
831
- }
832
- }
833
-
834
- return context;
835
- }
836
-
837
- /**
838
- * Check if a tool call matches a specific permission rule
839
- */
840
- private matchesRule(context: ToolPermissionContext, rule: string): boolean {
841
- // 1. Simple tool name match (e.g., "Bash", "Write")
842
- if (rule === context.toolName) {
843
- return true;
844
- }
845
-
846
- // 2. Tool with pattern match (e.g., "Bash(rm *)", "Read(**/*.env)")
847
- const match = rule.match(/^(\w+)\((.*)\)$/);
848
- if (!match) {
849
- return false;
850
- }
851
-
852
- const [, toolName, pattern] = match;
853
- if (toolName !== context.toolName) {
854
- return false;
855
- }
856
-
857
- // Handle Bash command rules
858
- if (toolName === BASH_TOOL_NAME) {
859
- const command = String(context.toolInput?.command || "");
860
- const hasWriteInPattern = hasWriteRedirections(pattern);
861
- const hasWriteInCommand = hasWriteRedirections(command);
862
-
863
- // If the command has write redirections, it must match a pattern that also has write redirections
864
- if (hasWriteInCommand && !hasWriteInPattern) {
865
- return false;
866
- }
867
-
868
- const processedPart = hasWriteInPattern
869
- ? stripEnvVars(command)
870
- : stripRedirections(stripEnvVars(command));
871
- // For Bash commands, we want '*' to match everything including slashes and spaces
872
- // minimatch's default behavior for '*' is to not match across directory separators
873
- // We use a regex to replace '*' with '.*' (match anything)
874
- const regexPattern = pattern
875
- .replace(/[.+^${}()|[\]\\?]/g, "\\$&") // Escape regex special chars including ?
876
- .replace(/\*/g, ".*"); // Replace * with .*
877
- const regex = new RegExp(`^${regexPattern}$`, "s");
878
- const matched = regex.test(processedPart);
879
- return matched;
880
- }
881
-
882
- // Handle path-based rules (e.g., "Read(**/*.env)")
883
- const pathTools = [READ_TOOL_NAME, WRITE_TOOL_NAME, EDIT_TOOL_NAME];
884
- if (pathTools.includes(toolName)) {
885
- const targetPath = (context.toolInput?.file_path ||
886
- context.toolInput?.path) as string | undefined;
887
-
888
- if (targetPath) {
889
- if (minimatch(targetPath, pattern, { dot: true })) {
890
- return true;
891
- }
892
-
893
- // If direct match fails, try matching relative path if targetPath is absolute and pattern is relative
894
- const currentWorkdir = this.getWorkdir();
895
- if (
896
- path.isAbsolute(targetPath) &&
897
- !path.isAbsolute(pattern) &&
898
- currentWorkdir
899
- ) {
900
- const relativePath = path.relative(currentWorkdir, targetPath);
901
- // Ensure the path is not outside the workdir (doesn't start with ..)
902
- if (
903
- !relativePath.startsWith("..") &&
904
- !path.isAbsolute(relativePath)
905
- ) {
906
- return minimatch(relativePath, pattern, { dot: true });
907
- }
908
- }
909
- }
910
- }
911
-
912
- return false;
913
- }
914
-
915
- /**
916
- * Check if a single bash command part is auto-allowed (read-only and safe).
917
- * Auto-allowed commands skip the confirmation dialog entirely.
918
- * FR-019.2 through FR-019.7: read-only commands without write redirections,
919
- * command substitution, process substitution, or sed -i are auto-allowed.
920
- */
921
- private isAutoAllowedPart(part: string, workdir?: string): boolean {
922
- // Write redirections disqualify (FR-019.4)
923
- if (hasWriteRedirections(part)) return false;
924
- // Command substitution $(...) or `...` disqualifies (FR-019.6)
925
- if (hasCommandSubstitution(part)) return false;
926
- // Process substitution <(...) or >(...) disqualifies (FR-019.6)
927
- if (hasProcessSubstitution(part)) return false;
928
-
929
- const processedPart = stripRedirections(stripEnvVars(part));
930
- const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
931
- if (!commandMatch) return false;
932
-
933
- const cmd = commandMatch[1];
934
- const args = commandMatch[2]?.trim() || "";
935
-
936
- // sed -i (in-place edit) disqualifies (FR-019.5)
937
- if (hasSedInPlace(part)) return false;
938
-
939
- // Read-only commands are auto-allowed (FR-019.2, FR-019.3)
940
- if (READ_ONLY_COMMANDS.includes(cmd)) {
941
- // find with dangerous flags (e.g. -exec, -delete) disqualifies
942
- if (cmd === "find" && isDangerousFind(part)) return false;
943
- return true;
944
- }
945
-
946
- // cd is not read-only but is safe if all paths are within the Safe Zone
947
- if (cmd === "cd") {
948
- if (!workdir) return false;
949
- const pathArgs =
950
- (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter(
951
- (arg) => !arg.startsWith("-"),
952
- ) || [];
953
- if (pathArgs.length === 0) return true; // cd without args = current dir
954
- return pathArgs.every((pathArg) => {
955
- const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
956
- const { isInside } = this.isInsideSafeZone(cleanPath, workdir);
957
- return isInside;
958
- });
959
- }
960
-
961
- return false;
962
- }
963
-
964
- /**
965
- * Check if a tool call is allowed by persistent or temporary rules
966
- */
967
- private isAllowedByRule(context: ToolPermissionContext): boolean {
968
- // All allow-rule sources are matched as a union: each part of a chained
969
- // command only needs to hit at least one rule across all sources, so
970
- // different parts may be covered by different sources.
971
- const explicitRules = [
972
- ...this.instanceAllowedRules,
973
- ...this.temporaryRules,
974
- ...this.allowedRules,
975
- ];
976
-
977
- if (context.toolName === BASH_TOOL_NAME && context.toolInput?.command) {
978
- const command = String(context.toolInput.command);
979
- const parts = splitBashCommand(command);
980
- if (parts.length === 0) return false;
981
-
982
- const workdir = context.toolInput?.workdir as string | undefined;
983
-
984
- return parts.every((part) => {
985
- // Check for auto-allowed read-only commands (FR-019.2 through FR-019.7)
986
- if (this.isAutoAllowedPart(part, workdir)) {
987
- return true;
988
- }
989
-
990
- // We create a temporary context with just this part of the command
991
- const partContext = {
992
- ...context,
993
- toolInput: { ...context.toolInput, command: part },
994
- };
995
-
996
- if (explicitRules.some((rule) => this.matchesRule(partContext, rule))) {
997
- return true;
998
- }
999
-
1000
- // Default rules must not auto-allow dangerous variants (write
1001
- // redirections, substitutions, sed -i, dangerous find) through broad
1002
- // rules like Bash(echo*) or Bash(cat*)
1003
- const isDangerousVariant =
1004
- hasWriteRedirections(part) ||
1005
- isDangerousFind(part) ||
1006
- hasCommandSubstitution(part) ||
1007
- hasProcessSubstitution(part) ||
1008
- hasSedInPlace(part);
1009
- if (
1010
- !isDangerousVariant &&
1011
- DEFAULT_ALLOWED_RULES.some((rule) =>
1012
- this.matchesRule(partContext, rule),
1013
- )
1014
- ) {
1015
- return true;
1016
- }
1017
-
1018
- return !this.isRestrictedTool(context.toolName);
1019
- });
1020
- }
1021
-
1022
- // For other tools, check if any rule matches
1023
- const allRules = [...explicitRules, ...DEFAULT_ALLOWED_RULES];
1024
- return allRules.some((rule) => this.matchesRule(context, rule));
1025
- }
1026
-
1027
- /**
1028
- * Expand a bash command into individual permission rules, filtering out safe commands.
1029
- * Used when saving permissions to the allow list.
1030
- *
1031
- * @param command The full bash command string
1032
- * @param workdir The working directory for path safety checks
1033
- * @returns Array of permission rules in "Bash(cmd)" format
1034
- */
1035
- public expandBashRule(command: string, workdir: string): string[] {
1036
- const parts = splitBashCommand(command);
1037
- const rules: string[] = [];
1038
-
1039
- for (const part of parts) {
1040
- const hasWrite = hasWriteRedirections(part);
1041
- const processedPart = stripRedirections(stripEnvVars(part));
1042
-
1043
- // Check for auto-allowed read-only commands
1044
- const isSafe = this.isAutoAllowedPart(part, workdir);
1045
-
1046
- if (!isSafe) {
1047
- // Check if command is dangerous or out-of-bounds
1048
- const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
1049
- if (commandMatch) {
1050
- const cmd = commandMatch[1];
1051
- const args = commandMatch[2]?.trim() || "";
1052
-
1053
- if (
1054
- DANGEROUS_COMMANDS.includes(cmd) ||
1055
- isDangerousFind(part) ||
1056
- hasSedInPlace(part)
1057
- ) {
1058
- continue;
1059
- }
1060
-
1061
- if (cmd === "cd") {
1062
- const pathArgs =
1063
- (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter(
1064
- (arg) => !arg.startsWith("-"),
1065
- ) || [];
1066
-
1067
- const isOutOfBounds = pathArgs.some((pathArg) => {
1068
- const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
1069
- const { isInside } = this.isInsideSafeZone(cleanPath, workdir);
1070
- return !isInside;
1071
- });
1072
-
1073
- if (isOutOfBounds) {
1074
- continue;
1075
- }
1076
- }
1077
- }
1078
-
1079
- const smartPrefix = hasWrite ? null : getSmartPrefix(processedPart);
1080
- if (smartPrefix) {
1081
- rules.push(`Bash(${smartPrefix}*)`);
1082
- } else {
1083
- rules.push(`Bash(${hasWrite ? stripEnvVars(part) : processedPart})`);
1084
- }
1085
- }
1086
- }
1087
-
1088
- return rules;
1089
- }
1090
-
1091
- /**
1092
- * Add a persistent permission rule
1093
- * @param rule - The rule to add (e.g., "Bash(ls)")
1094
- */
1095
- public async addPermissionRule(rule: string): Promise<void> {
1096
- const workdir = this.workdir;
1097
- if (!workdir) {
1098
- throw new Error("Working directory not set in PermissionManager");
1099
- }
1100
-
1101
- // 1. Expand rule if it's a Bash command
1102
- let rulesToAdd = [rule];
1103
- const bashMatch = rule.match(/^Bash\((.*)\)$/);
1104
- if (bashMatch) {
1105
- const command = bashMatch[1];
1106
- rulesToAdd = this.expandBashRule(command, workdir);
1107
- }
1108
-
1109
- const configurationService = this.container.get<ConfigurationService>(
1110
- "ConfigurationService",
1111
- );
1112
-
1113
- for (const ruleToAdd of rulesToAdd) {
1114
- // 2. Update PermissionManager state
1115
- const currentRules = this.getAllowedRules();
1116
- const defaultRules = this.getDefaultAllowedRules();
1117
- if (
1118
- !currentRules.includes(ruleToAdd) &&
1119
- !defaultRules.includes(ruleToAdd)
1120
- ) {
1121
- this.updateAllowedRules([...currentRules, ruleToAdd]);
1122
-
1123
- // 3. Persist to settings.local.json
1124
- try {
1125
- if (configurationService) {
1126
- await configurationService.addAllowedRule(workdir, ruleToAdd);
1127
- this._logger?.debug("Persistent permission rule added", {
1128
- rule: ruleToAdd,
1129
- });
1130
- }
1131
- } catch (error) {
1132
- this._logger?.error("Failed to persist permission rule", {
1133
- rule: ruleToAdd,
1134
- error: error instanceof Error ? error.message : String(error),
1135
- });
1136
- }
1137
- }
1138
- }
1139
- }
1140
- }