wave-agent-sdk 1.0.4 → 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 (194) 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 +19 -9
  7. package/dist/managers/slashCommandManager.js +28 -0
  8. package/dist/managers/subagentManager.d.ts +0 -2
  9. package/dist/services/session.js +2 -9
  10. package/dist/telemetry/instrumentation.d.ts +1 -1
  11. package/dist/telemetry/instrumentation.js +57 -28
  12. package/dist/tools/bashTool.js +16 -6
  13. package/dist/tools/grepTool.js +11 -3
  14. package/dist/types/index.d.ts +3 -0
  15. package/dist/types/messaging.d.ts +2 -0
  16. package/dist/utils/containerSetup.js +0 -3
  17. package/dist/utils/messageOperations.d.ts +10 -1
  18. package/dist/utils/messageOperations.js +19 -1
  19. package/dist/utils/notificationXml.js +5 -0
  20. package/dist/utils/ripgrep.d.ts +4 -4
  21. package/dist/utils/ripgrep.js +4 -27
  22. package/package.json +20 -8
  23. package/bin/rg +0 -79
  24. package/dist/managers/forkedAgentManager.d.ts +0 -50
  25. package/dist/managers/forkedAgentManager.js +0 -130
  26. package/scripts/install_ripgrep.js +0 -111
  27. package/scripts/postinstall.js +0 -38
  28. package/src/agent.ts +0 -1271
  29. package/src/constants/subagents.ts +0 -4
  30. package/src/constants/toolLimits.ts +0 -15
  31. package/src/constants/tools.ts +0 -24
  32. package/src/core/plugin.ts +0 -237
  33. package/src/core/session.ts +0 -9
  34. package/src/index.ts +0 -38
  35. package/src/managers/MemoryRuleManager.ts +0 -198
  36. package/src/managers/aiManager.ts +0 -2429
  37. package/src/managers/backgroundTaskManager.ts +0 -480
  38. package/src/managers/bangManager.ts +0 -119
  39. package/src/managers/cronManager.ts +0 -401
  40. package/src/managers/foregroundTaskManager.ts +0 -30
  41. package/src/managers/forkedAgentManager.ts +0 -216
  42. package/src/managers/hookManager.ts +0 -1089
  43. package/src/managers/liveConfigManager.ts +0 -445
  44. package/src/managers/lspManager.ts +0 -470
  45. package/src/managers/mcpManager.ts +0 -1010
  46. package/src/managers/messageManager.ts +0 -1180
  47. package/src/managers/messageQueue.ts +0 -157
  48. package/src/managers/permissionManager.ts +0 -1140
  49. package/src/managers/planManager.ts +0 -112
  50. package/src/managers/pluginManager.ts +0 -330
  51. package/src/managers/pluginScopeManager.ts +0 -124
  52. package/src/managers/reversionManager.ts +0 -178
  53. package/src/managers/skillManager.ts +0 -578
  54. package/src/managers/slashCommandManager.ts +0 -564
  55. package/src/managers/subagentManager.ts +0 -915
  56. package/src/managers/toolManager.ts +0 -489
  57. package/src/managers/workflowManager.ts +0 -493
  58. package/src/prompts/autoMemory.ts +0 -33
  59. package/src/prompts/autoMemoryExtraction.ts +0 -146
  60. package/src/prompts/index.ts +0 -506
  61. package/src/prompts/planModeReminders.ts +0 -126
  62. package/src/services/GitService.ts +0 -131
  63. package/src/services/MarketplaceService.ts +0 -1052
  64. package/src/services/MemoryRuleService.ts +0 -71
  65. package/src/services/aiService.ts +0 -919
  66. package/src/services/authService.ts +0 -565
  67. package/src/services/autoMemoryService.ts +0 -280
  68. package/src/services/configurationService.ts +0 -1520
  69. package/src/services/fileWatcher.ts +0 -319
  70. package/src/services/hook.ts +0 -348
  71. package/src/services/initializationService.ts +0 -357
  72. package/src/services/interactionService.ts +0 -229
  73. package/src/services/jsonlHandler.ts +0 -295
  74. package/src/services/memory.ts +0 -234
  75. package/src/services/pluginLoader.ts +0 -252
  76. package/src/services/remoteSettingsService.ts +0 -366
  77. package/src/services/reversionService.ts +0 -122
  78. package/src/services/session.ts +0 -922
  79. package/src/services/taskManager.ts +0 -321
  80. package/src/telemetry/events.ts +0 -65
  81. package/src/telemetry/instrumentation.ts +0 -499
  82. package/src/telemetry/sessionTracing.ts +0 -348
  83. package/src/tools/agentTool.ts +0 -322
  84. package/src/tools/askUserQuestion.ts +0 -160
  85. package/src/tools/bashTool.ts +0 -621
  86. package/src/tools/buildTool.ts +0 -61
  87. package/src/tools/cronCreateTool.ts +0 -161
  88. package/src/tools/cronDeleteTool.ts +0 -51
  89. package/src/tools/cronListTool.ts +0 -47
  90. package/src/tools/editTool.ts +0 -313
  91. package/src/tools/enterPlanMode.ts +0 -124
  92. package/src/tools/enterWorktreeTool.ts +0 -190
  93. package/src/tools/exitPlanMode.ts +0 -139
  94. package/src/tools/exitWorktreeTool.ts +0 -236
  95. package/src/tools/globTool.ts +0 -167
  96. package/src/tools/grepTool.ts +0 -399
  97. package/src/tools/lspTool.ts +0 -883
  98. package/src/tools/readTool.ts +0 -426
  99. package/src/tools/skillTool.ts +0 -248
  100. package/src/tools/taskManagementTools.ts +0 -574
  101. package/src/tools/taskStopTool.ts +0 -78
  102. package/src/tools/types.ts +0 -137
  103. package/src/tools/webFetchTool.ts +0 -373
  104. package/src/tools/workflowTool.ts +0 -205
  105. package/src/tools/writeTool.ts +0 -267
  106. package/src/types/agent.ts +0 -128
  107. package/src/types/auth.ts +0 -19
  108. package/src/types/commands.ts +0 -30
  109. package/src/types/config.ts +0 -43
  110. package/src/types/configuration.ts +0 -168
  111. package/src/types/core.ts +0 -105
  112. package/src/types/cron.ts +0 -13
  113. package/src/types/environment.ts +0 -106
  114. package/src/types/fileSearch.ts +0 -4
  115. package/src/types/history.ts +0 -7
  116. package/src/types/hooks.ts +0 -256
  117. package/src/types/index.ts +0 -41
  118. package/src/types/lsp.ts +0 -98
  119. package/src/types/marketplace.ts +0 -71
  120. package/src/types/mcp.ts +0 -43
  121. package/src/types/memoryRule.ts +0 -31
  122. package/src/types/messaging.ts +0 -114
  123. package/src/types/permissions.ts +0 -80
  124. package/src/types/plugins.ts +0 -39
  125. package/src/types/processes.ts +0 -99
  126. package/src/types/reversion.ts +0 -29
  127. package/src/types/session.ts +0 -12
  128. package/src/types/skills.ts +0 -108
  129. package/src/types/tasks.ts +0 -13
  130. package/src/types/telemetry.ts +0 -98
  131. package/src/types/tools.ts +0 -44
  132. package/src/types/workflow.ts +0 -6
  133. package/src/utils/abortUtils.ts +0 -118
  134. package/src/utils/atomicWrite.ts +0 -61
  135. package/src/utils/bashParser.ts +0 -899
  136. package/src/utils/cacheControlUtils.ts +0 -426
  137. package/src/utils/commandArgumentParser.ts +0 -161
  138. package/src/utils/commandPathResolver.ts +0 -88
  139. package/src/utils/configPaths.ts +0 -220
  140. package/src/utils/configValidator.ts +0 -100
  141. package/src/utils/constants.ts +0 -39
  142. package/src/utils/container.ts +0 -92
  143. package/src/utils/containerSetup.ts +0 -371
  144. package/src/utils/convertMessagesForAPI.ts +0 -326
  145. package/src/utils/cronTasks.ts +0 -128
  146. package/src/utils/cronTasksLock.ts +0 -182
  147. package/src/utils/cronToHuman.ts +0 -99
  148. package/src/utils/customCommands.ts +0 -95
  149. package/src/utils/editUtils.ts +0 -20
  150. package/src/utils/fileFormat.ts +0 -40
  151. package/src/utils/fileSearch.ts +0 -151
  152. package/src/utils/fileUtils.ts +0 -223
  153. package/src/utils/gitUtils.ts +0 -315
  154. package/src/utils/globalLogger.ts +0 -128
  155. package/src/utils/groupMessagesByApiRound.ts +0 -121
  156. package/src/utils/hookMatcher.ts +0 -176
  157. package/src/utils/markdownParser.ts +0 -317
  158. package/src/utils/mcpUtils.ts +0 -138
  159. package/src/utils/messageOperations.ts +0 -641
  160. package/src/utils/modelCapabilities.ts +0 -30
  161. package/src/utils/nameGenerator.ts +0 -99
  162. package/src/utils/notificationXml.ts +0 -57
  163. package/src/utils/openaiClient.ts +0 -251
  164. package/src/utils/parseCronExpression.ts +0 -78
  165. package/src/utils/path.ts +0 -72
  166. package/src/utils/pathEncoder.ts +0 -400
  167. package/src/utils/pathSafety.ts +0 -40
  168. package/src/utils/promptHistory.ts +0 -168
  169. package/src/utils/ripgrep.ts +0 -35
  170. package/src/utils/shellResolver.ts +0 -190
  171. package/src/utils/skillParser.ts +0 -246
  172. package/src/utils/stringUtils.ts +0 -156
  173. package/src/utils/subagentParser.ts +0 -279
  174. package/src/utils/taskReminder.ts +0 -96
  175. package/src/utils/tokenCalculation.ts +0 -43
  176. package/src/utils/tokenEstimate.ts +0 -34
  177. package/src/utils/toolResultStorage.ts +0 -117
  178. package/src/utils/worktreeSession.ts +0 -26
  179. package/src/utils/worktreeUtils.ts +0 -786
  180. package/src/workflow/budgetTracker.ts +0 -34
  181. package/src/workflow/concurrencyLimiter.ts +0 -47
  182. package/src/workflow/journal.ts +0 -95
  183. package/src/workflow/progressReporter.ts +0 -141
  184. package/src/workflow/runState.ts +0 -65
  185. package/src/workflow/scriptRuntime.ts +0 -274
  186. package/src/workflow/structuredOutput.ts +0 -123
  187. package/src/workflow/types.ts +0 -101
  188. package/src/workflow/workflowApis.ts +0 -410
  189. package/vendor/ripgrep/linux-aarch64/rg +0 -0
  190. package/vendor/ripgrep/linux-x86_64/rg +0 -0
  191. package/vendor/ripgrep/macos-aarch64/rg +0 -0
  192. package/vendor/ripgrep/macos-x86_64/rg +0 -0
  193. package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
  194. 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
- }