wave-agent-sdk 1.1.5 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +128 -28
- package/dist/agent.js +201 -49
- package/dist/builtin/index.js +2 -0
- package/dist/builtin/plugins.js +11 -20
- package/dist/builtin/skills/settings.js +7 -20
- package/dist/builtin/skills/wave-daemon.d.ts +1 -0
- package/dist/builtin/skills/wave-daemon.js +194 -0
- package/dist/constants/images.d.ts +26 -0
- package/dist/constants/images.js +26 -0
- package/dist/constants/index.d.ts +16 -0
- package/dist/constants/index.js +16 -0
- package/dist/constants/memory.d.ts +26 -0
- package/dist/constants/memory.js +34 -0
- package/dist/constants/messages.d.ts +11 -0
- package/dist/constants/messages.js +11 -0
- package/dist/constants/plugins.d.ts +8 -0
- package/dist/constants/plugins.js +8 -0
- package/dist/constants/tools.d.ts +1 -0
- package/dist/constants/tools.js +1 -0
- package/dist/core/plugin.d.ts +54 -10
- package/dist/core/plugin.js +137 -23
- package/dist/core/session.d.ts +1 -1
- package/dist/core/session.js +1 -1
- package/dist/exec/catalog.d.ts +140 -0
- package/dist/exec/catalog.js +470 -0
- package/dist/exec/catalogAnnouncement.d.ts +89 -0
- package/dist/exec/catalogAnnouncement.js +293 -0
- package/dist/exec/constants.d.ts +51 -0
- package/dist/exec/constants.js +51 -0
- package/dist/exec/execRuntime.d.ts +55 -0
- package/dist/exec/execRuntime.js +217 -0
- package/dist/exec/workerSource.d.ts +28 -0
- package/dist/exec/workerSource.js +299 -0
- package/dist/host/index.d.ts +23 -0
- package/dist/host/index.js +23 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +8 -1
- package/dist/managers/MemoryRuleManager.d.ts +6 -0
- package/dist/managers/MemoryRuleManager.js +12 -0
- package/dist/managers/aiManager.d.ts +35 -25
- package/dist/managers/aiManager.js +204 -202
- package/dist/managers/backgroundTaskManager.js +14 -0
- package/dist/managers/bashModeManager.d.ts +33 -0
- package/dist/managers/bashModeManager.js +110 -0
- package/dist/managers/hookManager.d.ts +18 -0
- package/dist/managers/hookManager.js +37 -3
- package/dist/managers/liveConfigManager.d.ts +33 -0
- package/dist/managers/liveConfigManager.js +106 -11
- package/dist/managers/lspManager.d.ts +9 -0
- package/dist/managers/lspManager.js +47 -18
- package/dist/managers/mcpManager.d.ts +68 -10
- package/dist/managers/mcpManager.js +265 -15
- package/dist/managers/messageManager.d.ts +60 -18
- package/dist/managers/messageManager.js +170 -81
- package/dist/managers/permissionManager.d.ts +69 -0
- package/dist/managers/permissionManager.js +221 -78
- package/dist/managers/planManager.d.ts +9 -0
- package/dist/managers/planManager.js +19 -1
- package/dist/managers/pluginManager.d.ts +46 -2
- package/dist/managers/pluginManager.js +117 -11
- package/dist/managers/pluginScopeManager.d.ts +15 -2
- package/dist/managers/pluginScopeManager.js +20 -1
- package/dist/managers/skillManager.d.ts +50 -0
- package/dist/managers/skillManager.js +166 -12
- package/dist/managers/slashCommandManager.d.ts +10 -0
- package/dist/managers/slashCommandManager.js +44 -31
- package/dist/managers/subagentManager.d.ts +15 -0
- package/dist/managers/subagentManager.js +81 -9
- package/dist/managers/toolManager.d.ts +29 -3
- package/dist/managers/toolManager.js +87 -13
- package/dist/managers/workflowManager.js +6 -0
- package/dist/prompts/autoMemory.d.ts +9 -0
- package/dist/prompts/autoMemory.js +30 -31
- package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
- package/dist/prompts/autoMemoryExtraction.js +8 -111
- package/dist/prompts/index.d.ts +0 -1
- package/dist/prompts/index.js +0 -4
- package/dist/prompts/memoryTypes.d.ts +63 -0
- package/dist/prompts/memoryTypes.js +191 -0
- package/dist/services/GitService.d.ts +7 -0
- package/dist/services/GitService.js +23 -0
- package/dist/services/MarketplaceService.d.ts +101 -17
- package/dist/services/MarketplaceService.js +323 -102
- package/dist/services/artifactContent.d.ts +84 -0
- package/dist/services/artifactContent.js +204 -0
- package/dist/services/artifactSession.d.ts +6 -0
- package/dist/services/artifactSession.js +17 -0
- package/dist/services/autoMemoryService.js +5 -13
- package/dist/services/configurationService.d.ts +92 -9
- package/dist/services/configurationService.js +246 -64
- package/dist/services/contentSummarizer.d.ts +15 -0
- package/dist/services/contentSummarizer.js +45 -0
- package/dist/services/execAvailability.d.ts +9 -0
- package/dist/services/execAvailability.js +32 -0
- package/dist/services/fileWatcher.js +61 -6
- package/dist/services/initializationService.js +21 -17
- package/dist/services/interactionService.d.ts +9 -1
- package/dist/services/interactionService.js +28 -8
- package/dist/services/jsonlHandler.d.ts +98 -0
- package/dist/services/jsonlHandler.js +250 -12
- package/dist/services/memory.d.ts +17 -1
- package/dist/services/memory.js +44 -7
- package/dist/services/officialMarketplaceMirror.d.ts +85 -0
- package/dist/services/officialMarketplaceMirror.js +290 -0
- package/dist/services/pluginLoader.d.ts +12 -4
- package/dist/services/pluginLoader.js +38 -7
- package/dist/services/remoteSettingsService.js +20 -6
- package/dist/services/session.d.ts +74 -0
- package/dist/services/session.js +174 -16
- package/dist/services/sessionEntries.d.ts +2 -0
- package/dist/services/sessionEntries.js +20 -0
- package/dist/services/worktreeHooks.js +6 -1
- package/dist/stdio/index.d.ts +12 -0
- package/dist/stdio/index.js +12 -0
- package/dist/stdio/notificationRouter.d.ts +38 -0
- package/dist/stdio/notificationRouter.js +97 -0
- package/dist/stdio/rpcClient.d.ts +18 -0
- package/dist/stdio/rpcClient.js +10 -0
- package/dist/stdio/stdioAgent.d.ts +229 -0
- package/dist/stdio/stdioAgent.js +360 -0
- package/dist/tools/artifactTool.js +406 -273
- package/dist/tools/bashTool.js +10 -6
- package/dist/tools/editTool.js +6 -3
- package/dist/tools/execTool.d.ts +2 -0
- package/dist/tools/execTool.js +165 -0
- package/dist/tools/exitPlanMode.js +10 -2
- package/dist/tools/grepTool.js +7 -1
- package/dist/tools/readTool.js +30 -2
- package/dist/tools/types.d.ts +34 -8
- package/dist/tools/webFetchTool.js +15 -166
- package/dist/tools/workflowTool.js +40 -8
- package/dist/tools/writeTool.js +6 -3
- package/dist/types/agent.d.ts +24 -1
- package/dist/types/commands.d.ts +7 -0
- package/dist/types/configuration.d.ts +45 -2
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.js +19 -0
- package/dist/types/marketplace.d.ts +40 -2
- package/dist/types/mcp.d.ts +42 -0
- package/dist/types/messaging.d.ts +1 -8
- package/dist/types/permissions.d.ts +22 -0
- package/dist/types/permissions.js +17 -0
- package/dist/types/plugins.d.ts +26 -2
- package/dist/types/skills.d.ts +26 -0
- package/dist/utils/bashParser.d.ts +17 -0
- package/dist/utils/bashParser.js +72 -0
- package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
- package/dist/utils/bashStructure/bashLexer.js +676 -0
- package/dist/utils/bashStructure/bashParser.d.ts +144 -0
- package/dist/utils/bashStructure/bashParser.js +606 -0
- package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
- package/dist/utils/bashStructure/bashSemantics.js +477 -0
- package/dist/utils/bashStructure/index.d.ts +26 -0
- package/dist/utils/bashStructure/index.js +27 -0
- package/dist/utils/bashStructure/types.d.ts +62 -0
- package/dist/utils/bashStructure/types.js +47 -0
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/constants.js +10 -0
- package/dist/utils/containerSetup.js +48 -6
- package/dist/utils/convertMessagesForAPI.d.ts +7 -1
- package/dist/utils/convertMessagesForAPI.js +64 -14
- package/dist/utils/fileChangeReminder.d.ts +20 -0
- package/dist/utils/fileChangeReminder.js +153 -0
- package/dist/utils/fileSearch.js +4 -3
- package/dist/utils/fileUtils.d.ts +44 -0
- package/dist/utils/fileUtils.js +118 -0
- package/dist/utils/frontmatterYaml.d.ts +33 -0
- package/dist/utils/frontmatterYaml.js +192 -0
- package/dist/utils/imageBudget.d.ts +85 -0
- package/dist/utils/imageBudget.js +109 -0
- package/dist/utils/imageDimensions.d.ts +83 -0
- package/dist/utils/imageDimensions.js +232 -0
- package/dist/utils/imageProcessor.d.ts +66 -0
- package/dist/utils/imageProcessor.js +84 -0
- package/dist/utils/imageRewrite.d.ts +29 -0
- package/dist/utils/imageRewrite.js +251 -0
- package/dist/utils/markdownParser.d.ts +5 -1
- package/dist/utils/markdownParser.js +9 -51
- package/dist/utils/mcpInstructions.d.ts +61 -0
- package/dist/utils/mcpInstructions.js +126 -0
- package/dist/utils/mcpUtils.d.ts +7 -0
- package/dist/utils/mcpUtils.js +11 -2
- package/dist/utils/memoryAge.d.ts +32 -0
- package/dist/utils/memoryAge.js +47 -0
- package/dist/utils/memoryEntrypoint.d.ts +20 -0
- package/dist/utils/memoryEntrypoint.js +49 -0
- package/dist/utils/memoryIndex.d.ts +30 -0
- package/dist/utils/memoryIndex.js +76 -0
- package/dist/utils/messageOperations.d.ts +6 -20
- package/dist/utils/messageOperations.js +40 -91
- package/dist/utils/nestedMemory.d.ts +22 -0
- package/dist/utils/nestedMemory.js +61 -0
- package/dist/utils/npmTarball.d.ts +19 -0
- package/dist/utils/npmTarball.js +92 -0
- package/dist/utils/pluginSource.d.ts +37 -0
- package/dist/utils/pluginSource.js +73 -0
- package/dist/utils/ripgrep.d.ts +18 -4
- package/dist/utils/ripgrep.js +56 -4
- package/dist/utils/runtimeDeps.d.ts +35 -0
- package/dist/utils/runtimeDeps.js +426 -0
- package/dist/utils/skillParser.js +22 -52
- package/dist/utils/subagentParser.js +48 -45
- package/dist/utils/tokenCalculation.js +0 -8
- package/dist/utils/userSettings.d.ts +90 -0
- package/dist/utils/userSettings.js +291 -0
- package/dist/utils/worktreeUtils.d.ts +2 -1
- package/dist/utils/worktreeUtils.js +64 -34
- package/package.json +12 -4
- package/dist/managers/bangManager.d.ts +0 -26
- package/dist/managers/bangManager.js +0 -78
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
* handles custom callback integration.
|
|
7
7
|
*/
|
|
8
8
|
import path from "node:path";
|
|
9
|
+
import os from "node:os";
|
|
9
10
|
import { minimatch } from "minimatch";
|
|
10
|
-
import { RESTRICTED_TOOLS } from "../types/permissions.js";
|
|
11
|
-
import { splitBashCommand, stripEnvVars, stripRedirections, hasWriteRedirections, getSmartPrefix, isDangerousFind, hasCommandSubstitution, hasProcessSubstitution, hasSedInPlace, stripGitScopePrefix, DANGEROUS_COMMANDS, READ_ONLY_COMMANDS, } from "../utils/bashParser.js";
|
|
11
|
+
import { RESTRICTED_TOOLS, USER_INTERACTION_REQUIRED_TOOLS, } from "../types/permissions.js";
|
|
12
|
+
import { splitBashCommand, stripEnvVars, stripRedirections, hasWriteRedirections, getSmartPrefix, isDangerousFind, hasCommandSubstitution, hasProcessSubstitution, hasSedInPlace, stripGitScopePrefix, DANGEROUS_COMMANDS, READ_ONLY_COMMANDS, PATH_COMMANDS, extractPathArgs, } from "../utils/bashParser.js";
|
|
13
|
+
import { parseBashStructure, } from "../utils/bashStructure/index.js";
|
|
12
14
|
import { isPathInside } from "../utils/pathSafety.js";
|
|
13
15
|
import { toWindowsPath } from "../utils/path.js";
|
|
14
|
-
import { BASH_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, READ_TOOL_NAME,
|
|
16
|
+
import { BASH_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, READ_TOOL_NAME, } from "../constants/tools.js";
|
|
15
17
|
const DEFAULT_ALLOWED_RULES = [
|
|
16
18
|
"Bash(git status*)",
|
|
17
19
|
"Bash(git diff*)",
|
|
@@ -55,6 +57,22 @@ const DEFAULT_ALLOWED_RULES = [
|
|
|
55
57
|
"Bash(sleep*)",
|
|
56
58
|
];
|
|
57
59
|
import { logger } from "../utils/globalLogger.js";
|
|
60
|
+
/**
|
|
61
|
+
* Resolve an additional-directory config entry to an absolute path.
|
|
62
|
+
* Supports `~` / `~/` prefixes (expanded against the user home directory,
|
|
63
|
+
* aligned with Claude Code's expandPath) alongside absolute paths and paths
|
|
64
|
+
* relative to the working directory.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveAdditionalDirectory(dir, workdir) {
|
|
67
|
+
if (dir === "~" || dir.startsWith("~/")) {
|
|
68
|
+
const expanded = dir === "~" ? os.homedir() : path.join(os.homedir(), dir.slice(2));
|
|
69
|
+
return path.resolve(expanded);
|
|
70
|
+
}
|
|
71
|
+
if (workdir && !path.isAbsolute(dir)) {
|
|
72
|
+
return path.resolve(workdir, dir);
|
|
73
|
+
}
|
|
74
|
+
return path.resolve(dir);
|
|
75
|
+
}
|
|
58
76
|
export class PermissionManager {
|
|
59
77
|
constructor(container, options = {}) {
|
|
60
78
|
this.container = container;
|
|
@@ -69,6 +87,7 @@ export class PermissionManager {
|
|
|
69
87
|
this.hasExitedPlanMode = false;
|
|
70
88
|
this.needsPlanModeExitAttachment = false;
|
|
71
89
|
this.configuredPermissionMode = options.configuredPermissionMode;
|
|
90
|
+
this.bypassAuthorization = options.bypassAuthorization;
|
|
72
91
|
this.allowedRules = options.allowedRules || [];
|
|
73
92
|
this.deniedRules = options.deniedRules || [];
|
|
74
93
|
this.instanceAllowedRules = options.instanceAllowedRules || [];
|
|
@@ -117,6 +136,28 @@ export class PermissionManager {
|
|
|
117
136
|
getConfiguredPermissionMode() {
|
|
118
137
|
return this.configuredPermissionMode;
|
|
119
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Freeze the session-level bypass authorization from the permission mode in
|
|
141
|
+
* effect when the session was created (CLI mode > configured default mode >
|
|
142
|
+
* `default`). No-op once captured, so switching to `bypassPermissions`
|
|
143
|
+
* mid-session does not grant it.
|
|
144
|
+
*/
|
|
145
|
+
captureBypassAuthorization() {
|
|
146
|
+
if (this.bypassAuthorization !== undefined) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const sessionMode = this.container.has("PermissionMode")
|
|
150
|
+
? this.container.get("PermissionMode")
|
|
151
|
+
: undefined;
|
|
152
|
+
this.bypassAuthorization =
|
|
153
|
+
this.getCurrentEffectiveMode(sessionMode) === "bypassPermissions";
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Whether this session keeps bypass semantics while in plan mode
|
|
157
|
+
*/
|
|
158
|
+
getBypassAuthorization() {
|
|
159
|
+
return this.bypassAuthorization === true;
|
|
160
|
+
}
|
|
120
161
|
/**
|
|
121
162
|
* Get all currently allowed rules (user-defined)
|
|
122
163
|
*/
|
|
@@ -150,6 +191,20 @@ export class PermissionManager {
|
|
|
150
191
|
this.instanceAllowedRules.push(rule);
|
|
151
192
|
}
|
|
152
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Remove an instance-level allowed rule added via addInstanceAllowedRule.
|
|
196
|
+
* Used when a builtin plugin that contributed a helper-script grant is
|
|
197
|
+
* unloaded, so the grant does not outlive the plugin.
|
|
198
|
+
* @returns true if the rule was present
|
|
199
|
+
*/
|
|
200
|
+
removeInstanceAllowedRule(rule) {
|
|
201
|
+
const index = this.instanceAllowedRules.indexOf(rule);
|
|
202
|
+
if (index === -1) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
this.instanceAllowedRules.splice(index, 1);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
153
208
|
/**
|
|
154
209
|
* Get all additional directories
|
|
155
210
|
*/
|
|
@@ -220,22 +275,13 @@ export class PermissionManager {
|
|
|
220
275
|
* Update the additional directories (e.g., when configuration reloads)
|
|
221
276
|
*/
|
|
222
277
|
updateAdditionalDirectories(directories) {
|
|
223
|
-
|
|
224
|
-
this.additionalDirectories = directories.map((dir) => {
|
|
225
|
-
if (workdir && !path.isAbsolute(dir)) {
|
|
226
|
-
return path.resolve(workdir, dir);
|
|
227
|
-
}
|
|
228
|
-
return path.resolve(dir);
|
|
229
|
-
});
|
|
278
|
+
this.additionalDirectories = directories.map((dir) => resolveAdditionalDirectory(dir, this.workdir));
|
|
230
279
|
}
|
|
231
280
|
/**
|
|
232
281
|
* Add an instance-level additional directory (session-level, e.g. /add-dir command)
|
|
233
282
|
*/
|
|
234
283
|
addInstanceAdditionalDirectory(directory) {
|
|
235
|
-
const
|
|
236
|
-
const resolvedPath = workdir && !path.isAbsolute(directory)
|
|
237
|
-
? path.resolve(workdir, directory)
|
|
238
|
-
: path.resolve(directory);
|
|
284
|
+
const resolvedPath = resolveAdditionalDirectory(directory, this.workdir);
|
|
239
285
|
if (!this.instanceAdditionalDirectories.includes(resolvedPath)) {
|
|
240
286
|
this.instanceAdditionalDirectories.push(resolvedPath);
|
|
241
287
|
}
|
|
@@ -244,14 +290,21 @@ export class PermissionManager {
|
|
|
244
290
|
* Add a system-level additional directory that is persistent across configuration reloads
|
|
245
291
|
*/
|
|
246
292
|
addSystemAdditionalDirectory(directory) {
|
|
247
|
-
const
|
|
248
|
-
const resolvedPath = workdir && !path.isAbsolute(directory)
|
|
249
|
-
? path.resolve(workdir, directory)
|
|
250
|
-
: path.resolve(directory);
|
|
293
|
+
const resolvedPath = resolveAdditionalDirectory(directory, this.workdir);
|
|
251
294
|
if (!this.systemAdditionalDirectories.includes(resolvedPath)) {
|
|
252
295
|
this.systemAdditionalDirectories.push(resolvedPath);
|
|
253
296
|
}
|
|
254
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Remove a system-level additional directory (idempotent). Used when a
|
|
300
|
+
* settings.json change revokes a privilege that was granted at construction
|
|
301
|
+
* time — e.g. turning auto-memory off removes its directory from the safe
|
|
302
|
+
* zone (core/agent-config.md scenario 6).
|
|
303
|
+
*/
|
|
304
|
+
removeSystemAdditionalDirectory(directory) {
|
|
305
|
+
const resolvedPath = resolveAdditionalDirectory(directory, this.workdir);
|
|
306
|
+
this.systemAdditionalDirectories = this.systemAdditionalDirectories.filter((dir) => dir !== resolvedPath);
|
|
307
|
+
}
|
|
255
308
|
/**
|
|
256
309
|
* Set the current plan file path
|
|
257
310
|
*/
|
|
@@ -428,12 +481,17 @@ export class PermissionManager {
|
|
|
428
481
|
}
|
|
429
482
|
}
|
|
430
483
|
}
|
|
431
|
-
// If bypassPermissions mode
|
|
432
|
-
//
|
|
433
|
-
//
|
|
484
|
+
// If bypassPermissions mode — or plan mode in a session that was authorized
|
|
485
|
+
// to bypass at creation time — always allow.
|
|
486
|
+
// Exception: tools that require user interaction (AskUserQuestion and
|
|
487
|
+
// ExitPlanMode) must still prompt, matching Claude Code's
|
|
488
|
+
// requiresUserInteraction behavior (its step 1e returns the tool's ask
|
|
489
|
+
// result before the bypass step).
|
|
434
490
|
// Worktree safety check above runs unconditionally, so bypass never skips it.
|
|
435
|
-
|
|
436
|
-
|
|
491
|
+
const bypassesPermissionChecks = context.permissionMode === "bypassPermissions" ||
|
|
492
|
+
(context.permissionMode === "plan" && this.getBypassAuthorization());
|
|
493
|
+
if (bypassesPermissionChecks) {
|
|
494
|
+
const requiresUserInteraction = USER_INTERACTION_REQUIRED_TOOLS.includes(context.toolName);
|
|
437
495
|
if (!requiresUserInteraction) {
|
|
438
496
|
return { behavior: "allow" };
|
|
439
497
|
}
|
|
@@ -609,9 +667,12 @@ export class PermissionManager {
|
|
|
609
667
|
const targetPath = toolInput?.file_path;
|
|
610
668
|
const workdir = toolInput?.workdir;
|
|
611
669
|
if (targetPath) {
|
|
612
|
-
const { isInside } = this.isInsideSafeZone(targetPath, workdir);
|
|
670
|
+
const { isInside, resolvedPath } = this.isInsideSafeZone(targetPath, workdir);
|
|
613
671
|
if (!isInside) {
|
|
614
672
|
context.hidePersistentOption = true;
|
|
673
|
+
// Surface the target's directory so the confirmation UI can offer
|
|
674
|
+
// adding it to the session Safe Zone.
|
|
675
|
+
context.outsideSafeZoneDirectory = path.dirname(resolvedPath);
|
|
615
676
|
}
|
|
616
677
|
}
|
|
617
678
|
}
|
|
@@ -634,20 +695,12 @@ export class PermissionManager {
|
|
|
634
695
|
const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
|
|
635
696
|
if (commandMatch) {
|
|
636
697
|
const cmd = commandMatch[1];
|
|
637
|
-
const args = commandMatch[2]?.trim() || "";
|
|
638
698
|
// Check blacklist
|
|
639
699
|
if (DANGEROUS_COMMANDS.includes(cmd)) {
|
|
640
700
|
return true;
|
|
641
701
|
}
|
|
642
|
-
// Check out-of-bounds for cd
|
|
643
|
-
|
|
644
|
-
const pathArgs = (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter((arg) => !arg.startsWith("-")) || [];
|
|
645
|
-
return pathArgs.some((pathArg) => {
|
|
646
|
-
const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
|
|
647
|
-
const { isInside } = this.isInsideSafeZone(cleanPath, workdir);
|
|
648
|
-
return !isInside;
|
|
649
|
-
});
|
|
650
|
-
}
|
|
702
|
+
// Check out-of-bounds for cd and path-taking read-only commands
|
|
703
|
+
return this.isOutOfSafeZonePart(part, workdir);
|
|
651
704
|
}
|
|
652
705
|
return false;
|
|
653
706
|
});
|
|
@@ -758,6 +811,19 @@ export class PermissionManager {
|
|
|
758
811
|
// find with dangerous flags (e.g. -exec, -delete) disqualifies
|
|
759
812
|
if (cmd === "find" && isDangerousFind(part))
|
|
760
813
|
return false;
|
|
814
|
+
// Path-taking read-only commands must stay within the Safe Zone:
|
|
815
|
+
// accessing a path outside workdir / additional directories
|
|
816
|
+
// disqualifies (aligned with Claude Code's path constraints)
|
|
817
|
+
if (PATH_COMMANDS.includes(cmd)) {
|
|
818
|
+
const pathArgs = extractPathArgs(args);
|
|
819
|
+
if (pathArgs.length > 0) {
|
|
820
|
+
if (!workdir)
|
|
821
|
+
return false;
|
|
822
|
+
if (!pathArgs.every((pathArg) => this.isInsideSafeZone(pathArg, workdir).isInside)) {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
761
827
|
return true;
|
|
762
828
|
}
|
|
763
829
|
// cd is not read-only but is safe if all paths are within the Safe Zone
|
|
@@ -775,6 +841,26 @@ export class PermissionManager {
|
|
|
775
841
|
}
|
|
776
842
|
return false;
|
|
777
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* Check whether a bash command part takes path arguments (cd or a
|
|
846
|
+
* path-taking read-only command) that escape the Safe Zone. Used to
|
|
847
|
+
* disqualify auto-allow via DEFAULT_ALLOWED_RULES and to hide the
|
|
848
|
+
* "don't ask again" persistence option for out-of-bounds access.
|
|
849
|
+
*/
|
|
850
|
+
isOutOfSafeZonePart(part, workdir) {
|
|
851
|
+
const processedPart = stripRedirections(stripEnvVars(part));
|
|
852
|
+
const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
|
|
853
|
+
if (!commandMatch)
|
|
854
|
+
return false;
|
|
855
|
+
const cmd = commandMatch[1];
|
|
856
|
+
if (cmd !== "cd" && !PATH_COMMANDS.includes(cmd))
|
|
857
|
+
return false;
|
|
858
|
+
const args = commandMatch[2]?.trim() || "";
|
|
859
|
+
const pathArgs = extractPathArgs(args);
|
|
860
|
+
if (pathArgs.length === 0)
|
|
861
|
+
return false;
|
|
862
|
+
return pathArgs.some((pathArg) => !this.isInsideSafeZone(pathArg, workdir).isInside);
|
|
863
|
+
}
|
|
778
864
|
/**
|
|
779
865
|
* Check if a tool call is allowed by persistent or temporary rules
|
|
780
866
|
*/
|
|
@@ -789,42 +875,80 @@ export class PermissionManager {
|
|
|
789
875
|
];
|
|
790
876
|
if (context.toolName === BASH_TOOL_NAME && context.toolInput?.command) {
|
|
791
877
|
const command = String(context.toolInput.command);
|
|
792
|
-
const parts = splitBashCommand(command);
|
|
793
|
-
if (parts.length === 0)
|
|
794
|
-
return false;
|
|
795
878
|
const workdir = context.toolInput?.workdir;
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
hasSedInPlace(part);
|
|
817
|
-
if (!isDangerousVariant &&
|
|
818
|
-
DEFAULT_ALLOWED_RULES.some((rule) => this.matchesRule(partContext, rule))) {
|
|
819
|
-
return true;
|
|
820
|
-
}
|
|
821
|
-
return !this.isRestrictedTool(context.toolName);
|
|
822
|
-
});
|
|
879
|
+
// Structure-aware expansion: compound commands and command substitutions
|
|
880
|
+
// are reduced to their leaf simple commands; each leaf is classified
|
|
881
|
+
// exactly like a top-level part of a chained command would be (spec:
|
|
882
|
+
// "复合命令与命令替换的结构感知判定").
|
|
883
|
+
const structure = parseBashStructure(command);
|
|
884
|
+
if (structure.status === "ok") {
|
|
885
|
+
if (structure.leaves.length === 0)
|
|
886
|
+
return false;
|
|
887
|
+
return structure.leaves.every((leaf) => this.isBashLeafAllowed(leaf, workdir, context, explicitRules));
|
|
888
|
+
}
|
|
889
|
+
// Fail-closed (heredoc / process substitution / arithmetic / brace /
|
|
890
|
+
// parse failure / eval / trap / …): the command cannot be statically
|
|
891
|
+
// reduced to leaves, so it is never auto-allowed and never matches the
|
|
892
|
+
// built-in default rules. Only an explicit user rule covering the whole
|
|
893
|
+
// command may allow it.
|
|
894
|
+
const wholeContext = {
|
|
895
|
+
...context,
|
|
896
|
+
toolInput: { ...context.toolInput, command },
|
|
897
|
+
};
|
|
898
|
+
return explicitRules.some((rule) => this.matchesRule(wholeContext, rule));
|
|
823
899
|
}
|
|
824
900
|
// For other tools, check if any rule matches
|
|
825
901
|
const allRules = [...explicitRules, ...DEFAULT_ALLOWED_RULES];
|
|
826
902
|
return allRules.some((rule) => this.matchesRule(context, rule));
|
|
827
903
|
}
|
|
904
|
+
/**
|
|
905
|
+
* Classify one expanded leaf command. Every leaf of a compound command or
|
|
906
|
+
* command substitution is judged with the exact rules used for a plain
|
|
907
|
+
* command, with two structure-aware additions:
|
|
908
|
+
*
|
|
909
|
+
* - a leaf the parser flagged `unsafe` (a bare reference to a variable whose
|
|
910
|
+
* value is not statically known, or a command substitution inside the
|
|
911
|
+
* leaf's own argument words) is never auto-allowed and never matches the
|
|
912
|
+
* built-in default rules — only an explicit allow rule may cover it;
|
|
913
|
+
* - `read` (e.g. the condition of `while read …`) only reads stdin, so it
|
|
914
|
+
* is treated as safe when it carries no redirection of its own.
|
|
915
|
+
*/
|
|
916
|
+
isBashLeafAllowed(leaf, workdir, context, explicitRules) {
|
|
917
|
+
// Check for auto-allowed read-only commands (FR-019.2 through FR-019.7)
|
|
918
|
+
if (!leaf.unsafe) {
|
|
919
|
+
if (leaf.command === "read" && !/[<>]/.test(leaf.text))
|
|
920
|
+
return true;
|
|
921
|
+
if (this.isAutoAllowedPart(leaf.text, workdir))
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
if (explicitRules.some((rule) => this.matchesRule(this.ruleContext(context, leaf.text), rule)) ||
|
|
925
|
+
explicitRules.some((rule) => this.matchesRule(this.ruleContext(context, unquotedLeafCommand(leaf)), rule))) {
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
// Default rules must not auto-allow dangerous variants (write
|
|
929
|
+
// redirections, substitutions, sed -i, dangerous find, out-of-safe-zone
|
|
930
|
+
// paths, bare-unknown-variable / in-word substitution leaves) through
|
|
931
|
+
// broad rules like Bash(echo*) or Bash(cat*)
|
|
932
|
+
const isDangerousVariant = leaf.unsafe ||
|
|
933
|
+
hasWriteRedirections(leaf.text) ||
|
|
934
|
+
isDangerousFind(leaf.text) ||
|
|
935
|
+
hasCommandSubstitution(leaf.text) ||
|
|
936
|
+
hasProcessSubstitution(leaf.text) ||
|
|
937
|
+
hasSedInPlace(leaf.text) ||
|
|
938
|
+
this.isOutOfSafeZonePart(leaf.text, workdir);
|
|
939
|
+
if (!isDangerousVariant &&
|
|
940
|
+
DEFAULT_ALLOWED_RULES.some((rule) => this.matchesRule(this.ruleContext(context, leaf.text), rule))) {
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
return !this.isRestrictedTool(context.toolName);
|
|
944
|
+
}
|
|
945
|
+
/** A context whose Bash command is a single part/leaf text. */
|
|
946
|
+
ruleContext(context, command) {
|
|
947
|
+
return {
|
|
948
|
+
...context,
|
|
949
|
+
toolInput: { ...context.toolInput, command },
|
|
950
|
+
};
|
|
951
|
+
}
|
|
828
952
|
/**
|
|
829
953
|
* Expand a bash command into individual permission rules, filtering out safe commands.
|
|
830
954
|
* Used when saving permissions to the allow list.
|
|
@@ -834,7 +958,13 @@ export class PermissionManager {
|
|
|
834
958
|
* @returns Array of permission rules in "Bash(cmd)" format
|
|
835
959
|
*/
|
|
836
960
|
expandBashRule(command, workdir) {
|
|
837
|
-
|
|
961
|
+
// Expand compound commands structurally when possible so control-structure
|
|
962
|
+
// keywords (do/done/then/…) never leak into persisted rules. Constructs the
|
|
963
|
+
// parser cannot reduce fall back to the legacy textual part splitter.
|
|
964
|
+
const structure = parseBashStructure(command);
|
|
965
|
+
const parts = structure.status === "ok" && structure.leaves.length > 0
|
|
966
|
+
? structure.leaves.map((leaf) => leaf.text)
|
|
967
|
+
: splitBashCommand(command);
|
|
838
968
|
const rules = [];
|
|
839
969
|
for (const part of parts) {
|
|
840
970
|
const hasWrite = hasWriteRedirections(part);
|
|
@@ -846,23 +976,12 @@ export class PermissionManager {
|
|
|
846
976
|
const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
|
|
847
977
|
if (commandMatch) {
|
|
848
978
|
const cmd = commandMatch[1];
|
|
849
|
-
const args = commandMatch[2]?.trim() || "";
|
|
850
979
|
if (DANGEROUS_COMMANDS.includes(cmd) ||
|
|
851
980
|
isDangerousFind(part) ||
|
|
852
|
-
hasSedInPlace(part)
|
|
981
|
+
hasSedInPlace(part) ||
|
|
982
|
+
this.isOutOfSafeZonePart(part, workdir)) {
|
|
853
983
|
continue;
|
|
854
984
|
}
|
|
855
|
-
if (cmd === "cd") {
|
|
856
|
-
const pathArgs = (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter((arg) => !arg.startsWith("-")) || [];
|
|
857
|
-
const isOutOfBounds = pathArgs.some((pathArg) => {
|
|
858
|
-
const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
|
|
859
|
-
const { isInside } = this.isInsideSafeZone(cleanPath, workdir);
|
|
860
|
-
return !isInside;
|
|
861
|
-
});
|
|
862
|
-
if (isOutOfBounds) {
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
985
|
}
|
|
867
986
|
const smartPrefix = hasWrite ? null : getSmartPrefix(processedPart);
|
|
868
987
|
if (smartPrefix) {
|
|
@@ -918,3 +1037,27 @@ export class PermissionManager {
|
|
|
918
1037
|
}
|
|
919
1038
|
}
|
|
920
1039
|
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Build a rule-matching form of a leaf command with quoting removed from words
|
|
1042
|
+
* that are a single quoted segment (`node "scripts/x"` → `node scripts/x`).
|
|
1043
|
+
* Shell quoting that does not change the executed words must not prevent a
|
|
1044
|
+
* rule such as `Bash(node scripts*)` from matching a leaf like
|
|
1045
|
+
* `node "scripts/$f" --dry-run`. Redirect operators/targets (the text tail
|
|
1046
|
+
* beyond argv) are preserved verbatim so write-redirection guards still apply.
|
|
1047
|
+
*/
|
|
1048
|
+
function unquotedLeafCommand(leaf) {
|
|
1049
|
+
const rawCommand = leaf.argv.join(" ");
|
|
1050
|
+
const tail = leaf.text.slice(rawCommand.length);
|
|
1051
|
+
return leaf.argv.map(unquoteWord).join(" ") + tail;
|
|
1052
|
+
}
|
|
1053
|
+
/** Strip one layer of enclosing single/double quotes from a whole word. */
|
|
1054
|
+
function unquoteWord(raw) {
|
|
1055
|
+
if (raw.length < 2)
|
|
1056
|
+
return raw;
|
|
1057
|
+
const first = raw[0];
|
|
1058
|
+
const last = raw[raw.length - 1];
|
|
1059
|
+
if ((first === '"' || first === "'") && first === last) {
|
|
1060
|
+
return raw.slice(1, -1);
|
|
1061
|
+
}
|
|
1062
|
+
return raw;
|
|
1063
|
+
}
|
|
@@ -5,6 +5,9 @@ export declare class PlanManager {
|
|
|
5
5
|
private planDir;
|
|
6
6
|
private currentPlanFilePath;
|
|
7
7
|
private planEntryReminderPending;
|
|
8
|
+
/** Promise for the in-flight plan file path generation, resolved when the
|
|
9
|
+
* path is set on the PermissionManager. Cleared when leaving plan mode. */
|
|
10
|
+
private planFilePathPromise;
|
|
8
11
|
constructor(container: Container);
|
|
9
12
|
/**
|
|
10
13
|
* Ensures the plan directory exists and generates a new plan file path with a random name
|
|
@@ -22,6 +25,12 @@ export declare class PlanManager {
|
|
|
22
25
|
* @param mode - The current effective permission mode
|
|
23
26
|
*/
|
|
24
27
|
handlePlanModeTransition(mode: PermissionMode): void;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve once the plan file path for the current plan-mode entry has been
|
|
30
|
+
* generated and set on the PermissionManager. Resolves `undefined` when no
|
|
31
|
+
* path generation is in flight (not in plan mode, or generation failed).
|
|
32
|
+
*/
|
|
33
|
+
awaitPlanFilePath(): Promise<string | undefined>;
|
|
25
34
|
isPlanEntryReminderPending(): boolean;
|
|
26
35
|
consumePlanEntryReminder(): void;
|
|
27
36
|
}
|
|
@@ -11,6 +11,9 @@ export class PlanManager {
|
|
|
11
11
|
this.container = container;
|
|
12
12
|
this.currentPlanFilePath = null;
|
|
13
13
|
this.planEntryReminderPending = false;
|
|
14
|
+
/** Promise for the in-flight plan file path generation, resolved when the
|
|
15
|
+
* path is set on the PermissionManager. Cleared when leaving plan mode. */
|
|
16
|
+
this.planFilePathPromise = null;
|
|
14
17
|
this.planDir = path.join(os.homedir(), ".wave", "plans");
|
|
15
18
|
}
|
|
16
19
|
/**
|
|
@@ -60,13 +63,18 @@ export class PlanManager {
|
|
|
60
63
|
// (prevents sending both plan_mode and plan_mode_exit on rapid toggle)
|
|
61
64
|
permissionManager?.setNeedsPlanModeExitAttachment(false);
|
|
62
65
|
this.planEntryReminderPending = true;
|
|
63
|
-
|
|
66
|
+
// Store the promise so callers (e.g. the CLI /plan command) can await
|
|
67
|
+
// path readiness before triggering a query — the plan mode reminder
|
|
68
|
+
// injected by AIManager only fires once the path is set.
|
|
69
|
+
this.planFilePathPromise = this.getOrGeneratePlanFilePath(messageManager?.getRootSessionId())
|
|
64
70
|
.then(({ path }) => {
|
|
65
71
|
logger?.debug("Plan file path generated", { path });
|
|
66
72
|
permissionManager?.setPlanFilePath(path);
|
|
73
|
+
return path;
|
|
67
74
|
})
|
|
68
75
|
.catch((error) => {
|
|
69
76
|
logger?.error("Failed to generate plan file path", error);
|
|
77
|
+
return undefined;
|
|
70
78
|
});
|
|
71
79
|
}
|
|
72
80
|
else if (previousMode === "plan") {
|
|
@@ -75,11 +83,21 @@ export class PlanManager {
|
|
|
75
83
|
permissionManager?.setNeedsPlanModeExitAttachment(true);
|
|
76
84
|
permissionManager?.setPlanFilePath(undefined);
|
|
77
85
|
this.planEntryReminderPending = false;
|
|
86
|
+
this.planFilePathPromise = null;
|
|
78
87
|
}
|
|
79
88
|
else {
|
|
80
89
|
permissionManager?.setPlanFilePath(undefined);
|
|
90
|
+
this.planFilePathPromise = null;
|
|
81
91
|
}
|
|
82
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolve once the plan file path for the current plan-mode entry has been
|
|
95
|
+
* generated and set on the PermissionManager. Resolves `undefined` when no
|
|
96
|
+
* path generation is in flight (not in plan mode, or generation failed).
|
|
97
|
+
*/
|
|
98
|
+
awaitPlanFilePath() {
|
|
99
|
+
return this.planFilePathPromise ?? Promise.resolve(undefined);
|
|
100
|
+
}
|
|
83
101
|
isPlanEntryReminderPending() {
|
|
84
102
|
return this.planEntryReminderPending;
|
|
85
103
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Plugin, PluginConfig } from "../types/index.js";
|
|
1
|
+
import { Plugin, PluginConfig, PluginLoadFailure, PluginReloadResult } from "../types/index.js";
|
|
2
2
|
import { Container } from "../utils/container.js";
|
|
3
3
|
export interface PluginManagerOptions {
|
|
4
4
|
workdir: string;
|
|
@@ -17,6 +17,17 @@ export declare class PluginManager {
|
|
|
17
17
|
private plugins;
|
|
18
18
|
private workdir;
|
|
19
19
|
private enabledPlugins;
|
|
20
|
+
/** Explicit configs from the last load, replayed by reloadAllPlugins(). */
|
|
21
|
+
private lastLoadConfigs;
|
|
22
|
+
/** Failures collected by the current loadPlugins() run. */
|
|
23
|
+
private loadFailures;
|
|
24
|
+
/**
|
|
25
|
+
* Serializes plugin (re)loads. A managed-settings sync (or an early
|
|
26
|
+
* `/reload-plugins`) can ask for a reload while the startup load is still
|
|
27
|
+
* running; interleaving them would leave half-unloaded capability registries.
|
|
28
|
+
*/
|
|
29
|
+
private loadChain;
|
|
30
|
+
private serialize;
|
|
20
31
|
constructor(container: Container, options: PluginManagerOptions);
|
|
21
32
|
private get permissionManager();
|
|
22
33
|
private get skillManager();
|
|
@@ -41,8 +52,41 @@ export declare class PluginManager {
|
|
|
41
52
|
/**
|
|
42
53
|
* Load plugins from configuration
|
|
43
54
|
* @param configs Array of plugin configurations
|
|
55
|
+
* @returns the plugins that failed to load
|
|
56
|
+
*/
|
|
57
|
+
loadPlugins(configs: PluginConfig[]): Promise<PluginLoadFailure[]>;
|
|
58
|
+
/**
|
|
59
|
+
* The actual load. Reached through [loadPlugins] (which serializes concurrent
|
|
60
|
+
* requests) and directly from [reloadAllPlugins], which already holds the slot.
|
|
61
|
+
*/
|
|
62
|
+
private doLoadPlugins;
|
|
63
|
+
/**
|
|
64
|
+
* Unload a single plugin: drop everything it contributed from the six
|
|
65
|
+
* capability registries, then forget it. Other plugins and the running
|
|
66
|
+
* session are untouched.
|
|
67
|
+
* @returns true if the plugin was loaded
|
|
68
|
+
*/
|
|
69
|
+
unloadPlugin(name: string): Promise<boolean>;
|
|
70
|
+
/**
|
|
71
|
+
* Re-read every plugin in place — the `/reload-plugins` command.
|
|
72
|
+
*
|
|
73
|
+
* Unloads everything currently loaded, re-reads `enabledPlugins` from the
|
|
74
|
+
* configuration chain (so a settings edit is honored), then replays the
|
|
75
|
+
* original load order (explicit configs → installed → builtin). The session,
|
|
76
|
+
* transcript and sessionId are untouched; only the capability registries are
|
|
77
|
+
* swapped, which is what makes this an in-place reload instead of a rebuild.
|
|
78
|
+
*
|
|
79
|
+
* Unload must happen before any caller-side mutation of the plugin cache dir:
|
|
80
|
+
* hooks, MCP, LSP, skills and agents all bake absolute plugin paths at
|
|
81
|
+
* registration time, so an entry left pointing at a deleted directory is
|
|
82
|
+
* worse than a merely stale one.
|
|
83
|
+
*/
|
|
84
|
+
reloadAllPlugins(): Promise<PluginReloadResult>;
|
|
85
|
+
/**
|
|
86
|
+
* Re-read enabled plugins from the merged configuration chain, falling back
|
|
87
|
+
* to the last known value when the service is unavailable.
|
|
44
88
|
*/
|
|
45
|
-
|
|
89
|
+
private refreshEnabledPlugins;
|
|
46
90
|
/**
|
|
47
91
|
* Load built-in plugins bundled with the SDK (e.g. sdd).
|
|
48
92
|
* Opt-in via `enabledPlugins`, keyed `<name>@builtin` (default off, consistent
|