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.
- package/dist/agent.d.ts +23 -1
- package/dist/agent.js +34 -4
- package/dist/managers/aiManager.d.ts +36 -0
- package/dist/managers/aiManager.js +240 -8
- package/dist/managers/messageManager.d.ts +1 -3
- package/dist/managers/messageManager.js +13 -6
- package/dist/managers/slashCommandManager.js +28 -0
- package/dist/managers/subagentManager.d.ts +0 -2
- package/dist/telemetry/instrumentation.d.ts +1 -1
- package/dist/telemetry/instrumentation.js +57 -28
- package/dist/tools/bashTool.js +16 -6
- package/dist/tools/grepTool.js +11 -3
- package/dist/types/index.d.ts +3 -0
- package/dist/types/messaging.d.ts +2 -0
- package/dist/utils/containerSetup.js +0 -3
- package/dist/utils/messageOperations.d.ts +2 -1
- package/dist/utils/messageOperations.js +2 -1
- package/dist/utils/notificationXml.js +5 -0
- package/dist/utils/ripgrep.d.ts +4 -4
- package/dist/utils/ripgrep.js +4 -27
- package/package.json +20 -8
- package/bin/rg +0 -79
- package/dist/managers/forkedAgentManager.d.ts +0 -50
- package/dist/managers/forkedAgentManager.js +0 -130
- package/scripts/install_ripgrep.js +0 -111
- package/scripts/postinstall.js +0 -38
- package/src/agent.ts +0 -1271
- package/src/constants/subagents.ts +0 -4
- package/src/constants/toolLimits.ts +0 -15
- package/src/constants/tools.ts +0 -24
- package/src/core/plugin.ts +0 -237
- package/src/core/session.ts +0 -9
- package/src/index.ts +0 -38
- package/src/managers/MemoryRuleManager.ts +0 -198
- package/src/managers/aiManager.ts +0 -2429
- package/src/managers/backgroundTaskManager.ts +0 -480
- package/src/managers/bangManager.ts +0 -119
- package/src/managers/cronManager.ts +0 -401
- package/src/managers/foregroundTaskManager.ts +0 -30
- package/src/managers/forkedAgentManager.ts +0 -216
- package/src/managers/hookManager.ts +0 -1089
- package/src/managers/liveConfigManager.ts +0 -445
- package/src/managers/lspManager.ts +0 -470
- package/src/managers/mcpManager.ts +0 -1010
- package/src/managers/messageManager.ts +0 -1184
- package/src/managers/messageQueue.ts +0 -157
- package/src/managers/permissionManager.ts +0 -1140
- package/src/managers/planManager.ts +0 -112
- package/src/managers/pluginManager.ts +0 -330
- package/src/managers/pluginScopeManager.ts +0 -124
- package/src/managers/reversionManager.ts +0 -178
- package/src/managers/skillManager.ts +0 -578
- package/src/managers/slashCommandManager.ts +0 -564
- package/src/managers/subagentManager.ts +0 -915
- package/src/managers/toolManager.ts +0 -489
- package/src/managers/workflowManager.ts +0 -493
- package/src/prompts/autoMemory.ts +0 -33
- package/src/prompts/autoMemoryExtraction.ts +0 -146
- package/src/prompts/index.ts +0 -506
- package/src/prompts/planModeReminders.ts +0 -126
- package/src/services/GitService.ts +0 -131
- package/src/services/MarketplaceService.ts +0 -1052
- package/src/services/MemoryRuleService.ts +0 -71
- package/src/services/aiService.ts +0 -919
- package/src/services/authService.ts +0 -565
- package/src/services/autoMemoryService.ts +0 -280
- package/src/services/configurationService.ts +0 -1520
- package/src/services/fileWatcher.ts +0 -319
- package/src/services/hook.ts +0 -348
- package/src/services/initializationService.ts +0 -357
- package/src/services/interactionService.ts +0 -229
- package/src/services/jsonlHandler.ts +0 -295
- package/src/services/memory.ts +0 -234
- package/src/services/pluginLoader.ts +0 -252
- package/src/services/remoteSettingsService.ts +0 -366
- package/src/services/reversionService.ts +0 -122
- package/src/services/session.ts +0 -917
- package/src/services/taskManager.ts +0 -321
- package/src/telemetry/events.ts +0 -65
- package/src/telemetry/instrumentation.ts +0 -499
- package/src/telemetry/sessionTracing.ts +0 -348
- package/src/tools/agentTool.ts +0 -322
- package/src/tools/askUserQuestion.ts +0 -160
- package/src/tools/bashTool.ts +0 -621
- package/src/tools/buildTool.ts +0 -61
- package/src/tools/cronCreateTool.ts +0 -161
- package/src/tools/cronDeleteTool.ts +0 -51
- package/src/tools/cronListTool.ts +0 -47
- package/src/tools/editTool.ts +0 -313
- package/src/tools/enterPlanMode.ts +0 -124
- package/src/tools/enterWorktreeTool.ts +0 -190
- package/src/tools/exitPlanMode.ts +0 -139
- package/src/tools/exitWorktreeTool.ts +0 -236
- package/src/tools/globTool.ts +0 -167
- package/src/tools/grepTool.ts +0 -399
- package/src/tools/lspTool.ts +0 -883
- package/src/tools/readTool.ts +0 -426
- package/src/tools/skillTool.ts +0 -248
- package/src/tools/taskManagementTools.ts +0 -574
- package/src/tools/taskStopTool.ts +0 -78
- package/src/tools/types.ts +0 -137
- package/src/tools/webFetchTool.ts +0 -373
- package/src/tools/workflowTool.ts +0 -205
- package/src/tools/writeTool.ts +0 -267
- package/src/types/agent.ts +0 -128
- package/src/types/auth.ts +0 -19
- package/src/types/commands.ts +0 -30
- package/src/types/config.ts +0 -43
- package/src/types/configuration.ts +0 -168
- package/src/types/core.ts +0 -105
- package/src/types/cron.ts +0 -13
- package/src/types/environment.ts +0 -106
- package/src/types/fileSearch.ts +0 -4
- package/src/types/history.ts +0 -7
- package/src/types/hooks.ts +0 -256
- package/src/types/index.ts +0 -41
- package/src/types/lsp.ts +0 -98
- package/src/types/marketplace.ts +0 -71
- package/src/types/mcp.ts +0 -43
- package/src/types/memoryRule.ts +0 -31
- package/src/types/messaging.ts +0 -114
- package/src/types/permissions.ts +0 -80
- package/src/types/plugins.ts +0 -39
- package/src/types/processes.ts +0 -99
- package/src/types/reversion.ts +0 -29
- package/src/types/session.ts +0 -12
- package/src/types/skills.ts +0 -108
- package/src/types/tasks.ts +0 -13
- package/src/types/telemetry.ts +0 -98
- package/src/types/tools.ts +0 -44
- package/src/types/workflow.ts +0 -6
- package/src/utils/abortUtils.ts +0 -118
- package/src/utils/atomicWrite.ts +0 -61
- package/src/utils/bashParser.ts +0 -899
- package/src/utils/cacheControlUtils.ts +0 -426
- package/src/utils/commandArgumentParser.ts +0 -161
- package/src/utils/commandPathResolver.ts +0 -88
- package/src/utils/configPaths.ts +0 -220
- package/src/utils/configValidator.ts +0 -100
- package/src/utils/constants.ts +0 -39
- package/src/utils/container.ts +0 -92
- package/src/utils/containerSetup.ts +0 -371
- package/src/utils/convertMessagesForAPI.ts +0 -326
- package/src/utils/cronTasks.ts +0 -128
- package/src/utils/cronTasksLock.ts +0 -182
- package/src/utils/cronToHuman.ts +0 -99
- package/src/utils/customCommands.ts +0 -95
- package/src/utils/editUtils.ts +0 -20
- package/src/utils/fileFormat.ts +0 -40
- package/src/utils/fileSearch.ts +0 -151
- package/src/utils/fileUtils.ts +0 -223
- package/src/utils/gitUtils.ts +0 -315
- package/src/utils/globalLogger.ts +0 -128
- package/src/utils/groupMessagesByApiRound.ts +0 -121
- package/src/utils/hookMatcher.ts +0 -176
- package/src/utils/markdownParser.ts +0 -317
- package/src/utils/mcpUtils.ts +0 -138
- package/src/utils/messageOperations.ts +0 -659
- package/src/utils/modelCapabilities.ts +0 -30
- package/src/utils/nameGenerator.ts +0 -99
- package/src/utils/notificationXml.ts +0 -57
- package/src/utils/openaiClient.ts +0 -251
- package/src/utils/parseCronExpression.ts +0 -78
- package/src/utils/path.ts +0 -72
- package/src/utils/pathEncoder.ts +0 -400
- package/src/utils/pathSafety.ts +0 -40
- package/src/utils/promptHistory.ts +0 -168
- package/src/utils/ripgrep.ts +0 -35
- package/src/utils/shellResolver.ts +0 -190
- package/src/utils/skillParser.ts +0 -246
- package/src/utils/stringUtils.ts +0 -156
- package/src/utils/subagentParser.ts +0 -279
- package/src/utils/taskReminder.ts +0 -96
- package/src/utils/tokenCalculation.ts +0 -43
- package/src/utils/tokenEstimate.ts +0 -34
- package/src/utils/toolResultStorage.ts +0 -117
- package/src/utils/worktreeSession.ts +0 -26
- package/src/utils/worktreeUtils.ts +0 -786
- package/src/workflow/budgetTracker.ts +0 -34
- package/src/workflow/concurrencyLimiter.ts +0 -47
- package/src/workflow/journal.ts +0 -95
- package/src/workflow/progressReporter.ts +0 -141
- package/src/workflow/runState.ts +0 -65
- package/src/workflow/scriptRuntime.ts +0 -274
- package/src/workflow/structuredOutput.ts +0 -123
- package/src/workflow/types.ts +0 -101
- package/src/workflow/workflowApis.ts +0 -410
- package/vendor/ripgrep/linux-aarch64/rg +0 -0
- package/vendor/ripgrep/linux-x86_64/rg +0 -0
- package/vendor/ripgrep/macos-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-x86_64/rg +0 -0
- package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
- 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
|
-
}
|