wave-agent-sdk 0.19.8 → 1.0.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/builtin/plugins/sdd/.wave-plugin/plugin.json +8 -0
- package/builtin/plugins/sdd/hooks/hooks.json +14 -0
- package/builtin/plugins/sdd/scripts/session-start.js +24 -0
- package/builtin/plugins/sdd/scripts/spec-count.js +77 -0
- package/builtin/plugins/sdd/skills/specify/SKILL.md +47 -0
- package/builtin/plugins/sdd/skills/specify/templates/spec-template.md +47 -0
- package/builtin/skills/settings/ENV.md +15 -9
- package/builtin/skills/settings/HOOKS.md +27 -2
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +26 -12
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts +25 -0
- package/dist/managers/aiManager.js +172 -51
- package/dist/managers/backgroundTaskManager.d.ts +6 -0
- package/dist/managers/backgroundTaskManager.js +11 -0
- package/dist/managers/bangManager.d.ts +6 -0
- package/dist/managers/bangManager.js +11 -0
- package/dist/managers/hookManager.d.ts +8 -2
- package/dist/managers/hookManager.js +14 -4
- package/dist/managers/mcpManager.d.ts +18 -4
- package/dist/managers/mcpManager.js +40 -18
- package/dist/managers/permissionManager.d.ts +7 -0
- package/dist/managers/permissionManager.js +102 -142
- package/dist/managers/pluginManager.d.ts +7 -0
- package/dist/managers/pluginManager.js +31 -0
- package/dist/managers/toolManager.js +5 -0
- package/dist/prompts/index.d.ts +12 -1
- package/dist/prompts/index.js +133 -45
- package/dist/services/aiService.d.ts +1 -17
- package/dist/services/aiService.js +3 -85
- package/dist/services/configurationService.d.ts +21 -2
- package/dist/services/configurationService.js +72 -23
- package/dist/services/initializationService.js +14 -4
- package/dist/services/interactionService.js +35 -7
- package/dist/services/remoteSettingsService.d.ts +12 -0
- package/dist/services/remoteSettingsService.js +15 -1
- package/dist/services/session.d.ts +3 -1
- package/dist/services/session.js +12 -4
- package/dist/services/taskManager.d.ts +1 -0
- package/dist/services/taskManager.js +41 -6
- package/dist/tools/bashTool.js +1 -0
- package/dist/tools/editTool.js +24 -10
- package/dist/tools/enterWorktreeTool.js +14 -3
- package/dist/tools/exitWorktreeTool.js +11 -10
- package/dist/tools/grepTool.js +8 -2
- package/dist/tools/types.d.ts +7 -0
- package/dist/tools/writeTool.js +36 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/hooks.d.ts +2 -2
- package/dist/utils/bashParser.d.ts +25 -0
- package/dist/utils/bashParser.js +103 -0
- package/dist/utils/configPaths.d.ts +4 -0
- package/dist/utils/configPaths.js +6 -0
- package/dist/utils/containerSetup.js +1 -1
- package/dist/utils/fileSearch.js +4 -2
- package/dist/utils/openaiClient.js +2 -1
- package/dist/utils/pathEncoder.js +7 -2
- package/dist/utils/worktreeUtils.d.ts +17 -0
- package/dist/utils/worktreeUtils.js +339 -1
- package/package.json +1 -1
- package/src/agent.ts +26 -12
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +238 -66
- package/src/managers/backgroundTaskManager.ts +15 -0
- package/src/managers/bangManager.ts +15 -0
- package/src/managers/hookManager.ts +20 -5
- package/src/managers/mcpManager.ts +60 -18
- package/src/managers/permissionManager.ts +116 -168
- package/src/managers/pluginManager.ts +29 -0
- package/src/managers/toolManager.ts +7 -0
- package/src/prompts/index.ts +144 -37
- package/src/services/aiService.ts +9 -128
- package/src/services/configurationService.ts +84 -23
- package/src/services/initializationService.ts +17 -4
- package/src/services/interactionService.ts +49 -6
- package/src/services/remoteSettingsService.ts +16 -1
- package/src/services/session.ts +18 -4
- package/src/services/taskManager.ts +56 -8
- package/src/tools/bashTool.ts +1 -0
- package/src/tools/editTool.ts +29 -11
- package/src/tools/enterWorktreeTool.ts +19 -2
- package/src/tools/exitWorktreeTool.ts +15 -12
- package/src/tools/grepTool.ts +11 -2
- package/src/tools/types.ts +7 -0
- package/src/tools/writeTool.ts +43 -0
- package/src/types/config.ts +2 -0
- package/src/types/hooks.ts +2 -2
- package/src/utils/bashParser.ts +106 -0
- package/src/utils/configPaths.ts +7 -0
- package/src/utils/containerSetup.ts +3 -1
- package/src/utils/fileSearch.ts +6 -2
- package/src/utils/openaiClient.ts +2 -0
- package/src/utils/pathEncoder.ts +7 -2
- package/src/utils/worktreeUtils.ts +401 -1
|
@@ -9,6 +9,7 @@ import { ChatCompletionFunctionTool } from "openai/resources.js";
|
|
|
9
9
|
import { createMcpToolPlugin, findToolServer } from "../utils/mcpUtils.js";
|
|
10
10
|
import type { ToolPlugin, ToolResult, ToolContext } from "../tools/types.js";
|
|
11
11
|
import { Container } from "../utils/container.js";
|
|
12
|
+
import type { ConfigurationService } from "../services/configurationService.js";
|
|
12
13
|
import type {
|
|
13
14
|
Logger,
|
|
14
15
|
McpServerConfig,
|
|
@@ -42,7 +43,21 @@ export interface McpManagerOptions {
|
|
|
42
43
|
*/
|
|
43
44
|
const WAVE_TEMPLATE_VARS = ["WAVE_PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT"];
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Expand environment variables in a string value.
|
|
48
|
+
* Supports ${VAR} and ${VAR:-default} patterns.
|
|
49
|
+
*
|
|
50
|
+
* @param env - Merged env to resolve against (session snapshot over OS env).
|
|
51
|
+
* Defaults to `process.env` for backward compatibility with tests
|
|
52
|
+
* and standalone callers.
|
|
53
|
+
*/
|
|
54
|
+
export function expandEnvVars(
|
|
55
|
+
value: string,
|
|
56
|
+
env: Record<string, string | undefined> = process.env as Record<
|
|
57
|
+
string,
|
|
58
|
+
string | undefined
|
|
59
|
+
>,
|
|
60
|
+
): string {
|
|
46
61
|
return value.replace(/\$\{([^}]+)\}/g, (_match, expr: string) => {
|
|
47
62
|
const [varName, ...rest] = expr.split(":-");
|
|
48
63
|
const defaultValue = rest.join(":-");
|
|
@@ -50,45 +65,53 @@ export function expandEnvVars(value: string): string {
|
|
|
50
65
|
if (WAVE_TEMPLATE_VARS.includes(varName)) {
|
|
51
66
|
return _match; // return original ${...} string untouched
|
|
52
67
|
}
|
|
53
|
-
return process.env[varName] ?? defaultValue;
|
|
68
|
+
return env[varName] ?? process.env[varName] ?? defaultValue;
|
|
54
69
|
});
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
/**
|
|
58
73
|
* Walk an MCP config and resolve environment variables in all string fields.
|
|
59
|
-
*
|
|
60
|
-
* handled at spawn time
|
|
74
|
+
* Expands ${VAR} against the session env snapshot (over OS env); falls back to
|
|
75
|
+
* process.env. WAVE_PLUGIN_ROOT is skipped — handled at spawn time.
|
|
61
76
|
*/
|
|
62
|
-
export function resolveMcpConfig(
|
|
77
|
+
export function resolveMcpConfig(
|
|
78
|
+
config: McpConfig,
|
|
79
|
+
env: Record<string, string | undefined> = process.env as Record<
|
|
80
|
+
string,
|
|
81
|
+
string | undefined
|
|
82
|
+
>,
|
|
83
|
+
): McpConfig {
|
|
63
84
|
const resolved: McpConfig = { mcpServers: {} };
|
|
64
85
|
|
|
65
86
|
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
66
87
|
const resolvedServer: McpServerConfig = { ...serverConfig };
|
|
67
88
|
|
|
68
89
|
if (resolvedServer.command) {
|
|
69
|
-
resolvedServer.command = expandEnvVars(resolvedServer.command);
|
|
90
|
+
resolvedServer.command = expandEnvVars(resolvedServer.command, env);
|
|
70
91
|
}
|
|
71
92
|
|
|
72
93
|
if (resolvedServer.args) {
|
|
73
|
-
resolvedServer.args = resolvedServer.args.map(
|
|
94
|
+
resolvedServer.args = resolvedServer.args.map((a) =>
|
|
95
|
+
expandEnvVars(a, env),
|
|
96
|
+
);
|
|
74
97
|
}
|
|
75
98
|
|
|
76
99
|
if (resolvedServer.env) {
|
|
77
100
|
const resolvedEnv: Record<string, string> = {};
|
|
78
101
|
for (const [key, val] of Object.entries(resolvedServer.env)) {
|
|
79
|
-
resolvedEnv[key] = expandEnvVars(val);
|
|
102
|
+
resolvedEnv[key] = expandEnvVars(val, env);
|
|
80
103
|
}
|
|
81
104
|
resolvedServer.env = resolvedEnv;
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
if (resolvedServer.url) {
|
|
85
|
-
resolvedServer.url = expandEnvVars(resolvedServer.url);
|
|
108
|
+
resolvedServer.url = expandEnvVars(resolvedServer.url, env);
|
|
86
109
|
}
|
|
87
110
|
|
|
88
111
|
if (resolvedServer.headers) {
|
|
89
112
|
const resolvedHeaders: Record<string, string> = {};
|
|
90
113
|
for (const [key, val] of Object.entries(resolvedServer.headers)) {
|
|
91
|
-
resolvedHeaders[key] = expandEnvVars(val);
|
|
114
|
+
resolvedHeaders[key] = expandEnvVars(val, env);
|
|
92
115
|
}
|
|
93
116
|
resolvedServer.headers = resolvedHeaders;
|
|
94
117
|
}
|
|
@@ -119,6 +142,19 @@ export class McpManager {
|
|
|
119
142
|
this.mcpServers = options.mcpServers;
|
|
120
143
|
}
|
|
121
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Merged env for this session: OS env overlaid with the per-session settings
|
|
147
|
+
* snapshot. MCP env-var expansion (${VAR}) resolves against this so multiple
|
|
148
|
+
* sessions in one `wave --stdio` process don't read each other's settings env.
|
|
149
|
+
*/
|
|
150
|
+
private get envSnapshot(): Record<string, string> {
|
|
151
|
+
return (
|
|
152
|
+
this.container
|
|
153
|
+
.get<ConfigurationService>("ConfigurationService")
|
|
154
|
+
?.getMergedEnv?.() ?? (process.env as Record<string, string>)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
122
158
|
/**
|
|
123
159
|
* Initialize MCP manager with working directory and optionally auto-connect
|
|
124
160
|
*/
|
|
@@ -189,7 +225,7 @@ export class McpManager {
|
|
|
189
225
|
try {
|
|
190
226
|
const configContent = await fs.readFile(this.configPath, "utf-8");
|
|
191
227
|
const rawConfig: McpConfig = JSON.parse(configContent);
|
|
192
|
-
const workspaceConfig = resolveMcpConfig(rawConfig);
|
|
228
|
+
const workspaceConfig = resolveMcpConfig(rawConfig, this.envSnapshot);
|
|
193
229
|
|
|
194
230
|
// Extract original (pre-resolution) URLs for safe display
|
|
195
231
|
const originalUrls: Record<string, string | undefined> = {};
|
|
@@ -284,28 +320,31 @@ export class McpManager {
|
|
|
284
320
|
// Capture original URL before any resolution for safe display
|
|
285
321
|
const originalUrl = config.url;
|
|
286
322
|
|
|
287
|
-
// Expand env vars
|
|
323
|
+
// Expand env vars against the session snapshot (over OS env, e.g. ${TAVILY_API_KEY})
|
|
324
|
+
const env = this.envSnapshot;
|
|
288
325
|
const resolvedConfig: McpServerConfig = { ...config };
|
|
289
326
|
if (resolvedConfig.command) {
|
|
290
|
-
resolvedConfig.command = expandEnvVars(resolvedConfig.command);
|
|
327
|
+
resolvedConfig.command = expandEnvVars(resolvedConfig.command, env);
|
|
291
328
|
}
|
|
292
329
|
if (resolvedConfig.args) {
|
|
293
|
-
resolvedConfig.args = resolvedConfig.args.map(
|
|
330
|
+
resolvedConfig.args = resolvedConfig.args.map((a) =>
|
|
331
|
+
expandEnvVars(a, env),
|
|
332
|
+
);
|
|
294
333
|
}
|
|
295
334
|
if (resolvedConfig.env) {
|
|
296
335
|
const resolvedEnv: Record<string, string> = {};
|
|
297
336
|
for (const [key, val] of Object.entries(resolvedConfig.env)) {
|
|
298
|
-
resolvedEnv[key] = expandEnvVars(val);
|
|
337
|
+
resolvedEnv[key] = expandEnvVars(val, env);
|
|
299
338
|
}
|
|
300
339
|
resolvedConfig.env = resolvedEnv;
|
|
301
340
|
}
|
|
302
341
|
if (resolvedConfig.url) {
|
|
303
|
-
resolvedConfig.url = expandEnvVars(resolvedConfig.url);
|
|
342
|
+
resolvedConfig.url = expandEnvVars(resolvedConfig.url, env);
|
|
304
343
|
}
|
|
305
344
|
if (resolvedConfig.headers) {
|
|
306
345
|
const resolvedHeaders: Record<string, string> = {};
|
|
307
346
|
for (const [key, val] of Object.entries(resolvedConfig.headers)) {
|
|
308
|
-
resolvedHeaders[key] = expandEnvVars(val);
|
|
347
|
+
resolvedHeaders[key] = expandEnvVars(val, env);
|
|
309
348
|
}
|
|
310
349
|
resolvedConfig.headers = resolvedHeaders;
|
|
311
350
|
}
|
|
@@ -433,8 +472,11 @@ export class McpManager {
|
|
|
433
472
|
);
|
|
434
473
|
}
|
|
435
474
|
|
|
475
|
+
// Base env = OS env overlaid with this session's settings snapshot, so
|
|
476
|
+
// custom env vars from settings.json reach the MCP server subprocess
|
|
477
|
+
// without polluting other sessions sharing one `wave --stdio` process.
|
|
436
478
|
const env: Record<string, string> = {
|
|
437
|
-
...
|
|
479
|
+
...this.envSnapshot,
|
|
438
480
|
...(server.config.env || {}),
|
|
439
481
|
};
|
|
440
482
|
|
|
@@ -23,7 +23,11 @@ import {
|
|
|
23
23
|
hasWriteRedirections,
|
|
24
24
|
getSmartPrefix,
|
|
25
25
|
isDangerousFind,
|
|
26
|
+
hasCommandSubstitution,
|
|
27
|
+
hasProcessSubstitution,
|
|
28
|
+
hasSedInPlace,
|
|
26
29
|
DANGEROUS_COMMANDS,
|
|
30
|
+
READ_ONLY_COMMANDS,
|
|
27
31
|
} from "../utils/bashParser.js";
|
|
28
32
|
import { isPathInside } from "../utils/pathSafety.js";
|
|
29
33
|
import {
|
|
@@ -37,23 +41,6 @@ import { Container } from "../utils/container.js";
|
|
|
37
41
|
import { ConfigurationService } from "../services/configurationService.js";
|
|
38
42
|
import type { WorktreeSession } from "../utils/worktreeSession.js";
|
|
39
43
|
|
|
40
|
-
const SAFE_COMMANDS = [
|
|
41
|
-
"cd",
|
|
42
|
-
"ls",
|
|
43
|
-
"pwd",
|
|
44
|
-
"true",
|
|
45
|
-
"false",
|
|
46
|
-
"grep",
|
|
47
|
-
"rg",
|
|
48
|
-
"cat",
|
|
49
|
-
"head",
|
|
50
|
-
"tail",
|
|
51
|
-
"wc",
|
|
52
|
-
"sleep",
|
|
53
|
-
"find",
|
|
54
|
-
"sort",
|
|
55
|
-
];
|
|
56
|
-
|
|
57
44
|
const DEFAULT_ALLOWED_RULES = [
|
|
58
45
|
"Bash(git status*)",
|
|
59
46
|
"Bash(git diff*)",
|
|
@@ -747,6 +734,14 @@ export class PermissionManager {
|
|
|
747
734
|
if (hasWriteRedirections(part)) {
|
|
748
735
|
return true;
|
|
749
736
|
}
|
|
737
|
+
// Command/process substitution and sed -i are dangerous (FR-019.5, FR-019.6)
|
|
738
|
+
if (
|
|
739
|
+
hasCommandSubstitution(part) ||
|
|
740
|
+
hasProcessSubstitution(part) ||
|
|
741
|
+
hasSedInPlace(part)
|
|
742
|
+
) {
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
750
745
|
const processedPart = stripRedirections(stripEnvVars(part));
|
|
751
746
|
const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
|
|
752
747
|
if (commandMatch) {
|
|
@@ -862,122 +857,115 @@ export class PermissionManager {
|
|
|
862
857
|
}
|
|
863
858
|
|
|
864
859
|
/**
|
|
865
|
-
* Check if a
|
|
860
|
+
* Check if a single bash command part is auto-allowed (read-only and safe).
|
|
861
|
+
* Auto-allowed commands skip the confirmation dialog entirely.
|
|
862
|
+
* FR-019.2 through FR-019.7: read-only commands without write redirections,
|
|
863
|
+
* command substitution, process substitution, or sed -i are auto-allowed.
|
|
866
864
|
*/
|
|
867
|
-
private
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
)
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
if (SAFE_COMMANDS.includes(cmd)) {
|
|
892
|
-
if (
|
|
893
|
-
cmd === "pwd" ||
|
|
894
|
-
cmd === "true" ||
|
|
895
|
-
cmd === "false" ||
|
|
896
|
-
cmd === "ls" ||
|
|
897
|
-
cmd === "grep" ||
|
|
898
|
-
cmd === "rg" ||
|
|
899
|
-
cmd === "cat" ||
|
|
900
|
-
cmd === "head" ||
|
|
901
|
-
cmd === "tail" ||
|
|
902
|
-
cmd === "wc" ||
|
|
903
|
-
cmd === "sleep" ||
|
|
904
|
-
cmd === "sort" ||
|
|
905
|
-
(cmd === "find" && !isDangerousFind(part))
|
|
906
|
-
) {
|
|
907
|
-
return true;
|
|
908
|
-
}
|
|
865
|
+
private isAutoAllowedPart(part: string, workdir?: string): boolean {
|
|
866
|
+
// Write redirections disqualify (FR-019.4)
|
|
867
|
+
if (hasWriteRedirections(part)) return false;
|
|
868
|
+
// Command substitution $(...) or `...` disqualifies (FR-019.6)
|
|
869
|
+
if (hasCommandSubstitution(part)) return false;
|
|
870
|
+
// Process substitution <(...) or >(...) disqualifies (FR-019.6)
|
|
871
|
+
if (hasProcessSubstitution(part)) return false;
|
|
872
|
+
|
|
873
|
+
const processedPart = stripRedirections(stripEnvVars(part));
|
|
874
|
+
const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
|
|
875
|
+
if (!commandMatch) return false;
|
|
876
|
+
|
|
877
|
+
const cmd = commandMatch[1];
|
|
878
|
+
const args = commandMatch[2]?.trim() || "";
|
|
879
|
+
|
|
880
|
+
// sed -i (in-place edit) disqualifies (FR-019.5)
|
|
881
|
+
if (hasSedInPlace(part)) return false;
|
|
882
|
+
|
|
883
|
+
// Read-only commands are auto-allowed (FR-019.2, FR-019.3)
|
|
884
|
+
if (READ_ONLY_COMMANDS.includes(cmd)) {
|
|
885
|
+
// find with dangerous flags (e.g. -exec, -delete) disqualifies
|
|
886
|
+
if (cmd === "find" && isDangerousFind(part)) return false;
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
909
889
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
|
|
925
|
-
const { isInside } = this.isInsideSafeZone(
|
|
926
|
-
cleanPath,
|
|
927
|
-
workdir,
|
|
928
|
-
);
|
|
929
|
-
return isInside;
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
if (allPathsSafe) {
|
|
933
|
-
return true;
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
}
|
|
890
|
+
// cd is not read-only but is safe if all paths are within the Safe Zone
|
|
891
|
+
if (cmd === "cd") {
|
|
892
|
+
if (!workdir) return false;
|
|
893
|
+
const pathArgs =
|
|
894
|
+
(args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter(
|
|
895
|
+
(arg) => !arg.startsWith("-"),
|
|
896
|
+
) || [];
|
|
897
|
+
if (pathArgs.length === 0) return true; // cd without args = current dir
|
|
898
|
+
return pathArgs.every((pathArg) => {
|
|
899
|
+
const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
|
|
900
|
+
const { isInside } = this.isInsideSafeZone(cleanPath, workdir);
|
|
901
|
+
return isInside;
|
|
902
|
+
});
|
|
903
|
+
}
|
|
939
904
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
return false;
|
|
943
|
-
}
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
944
907
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
908
|
+
/**
|
|
909
|
+
* Check if a tool call is allowed by persistent or temporary rules
|
|
910
|
+
*/
|
|
911
|
+
private isAllowedByRule(context: ToolPermissionContext): boolean {
|
|
912
|
+
// All allow-rule sources are matched as a union: each part of a chained
|
|
913
|
+
// command only needs to hit at least one rule across all sources, so
|
|
914
|
+
// different parts may be covered by different sources.
|
|
915
|
+
const explicitRules = [
|
|
916
|
+
...this.instanceAllowedRules,
|
|
917
|
+
...this.temporaryRules,
|
|
918
|
+
...this.allowedRules,
|
|
919
|
+
];
|
|
920
|
+
|
|
921
|
+
if (context.toolName === BASH_TOOL_NAME && context.toolInput?.command) {
|
|
922
|
+
const command = String(context.toolInput.command);
|
|
923
|
+
const parts = splitBashCommand(command);
|
|
924
|
+
if (parts.length === 0) return false;
|
|
953
925
|
|
|
954
|
-
|
|
926
|
+
const workdir = context.toolInput?.workdir as string | undefined;
|
|
955
927
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
928
|
+
return parts.every((part) => {
|
|
929
|
+
// Check for auto-allowed read-only commands (FR-019.2 through FR-019.7)
|
|
930
|
+
if (this.isAutoAllowedPart(part, workdir)) {
|
|
931
|
+
return true;
|
|
932
|
+
}
|
|
959
933
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
934
|
+
// We create a temporary context with just this part of the command
|
|
935
|
+
const partContext = {
|
|
936
|
+
...context,
|
|
937
|
+
toolInput: { ...context.toolInput, command: part },
|
|
938
|
+
};
|
|
963
939
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
}
|
|
940
|
+
if (explicitRules.some((rule) => this.matchesRule(partContext, rule))) {
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
968
943
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
944
|
+
// Default rules must not auto-allow dangerous variants (write
|
|
945
|
+
// redirections, substitutions, sed -i, dangerous find) through broad
|
|
946
|
+
// rules like Bash(echo*) or Bash(cat*)
|
|
947
|
+
const isDangerousVariant =
|
|
948
|
+
hasWriteRedirections(part) ||
|
|
949
|
+
isDangerousFind(part) ||
|
|
950
|
+
hasCommandSubstitution(part) ||
|
|
951
|
+
hasProcessSubstitution(part) ||
|
|
952
|
+
hasSedInPlace(part);
|
|
953
|
+
if (
|
|
954
|
+
!isDangerousVariant &&
|
|
955
|
+
DEFAULT_ALLOWED_RULES.some((rule) =>
|
|
956
|
+
this.matchesRule(partContext, rule),
|
|
957
|
+
)
|
|
958
|
+
) {
|
|
959
|
+
return true;
|
|
960
|
+
}
|
|
973
961
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
return true;
|
|
962
|
+
return !this.isRestrictedTool(context.toolName);
|
|
963
|
+
});
|
|
977
964
|
}
|
|
978
965
|
|
|
979
|
-
//
|
|
980
|
-
|
|
966
|
+
// For other tools, check if any rule matches
|
|
967
|
+
const allRules = [...explicitRules, ...DEFAULT_ALLOWED_RULES];
|
|
968
|
+
return allRules.some((rule) => this.matchesRule(context, rule));
|
|
981
969
|
}
|
|
982
970
|
|
|
983
971
|
/**
|
|
@@ -996,52 +984,8 @@ export class PermissionManager {
|
|
|
996
984
|
const hasWrite = hasWriteRedirections(part);
|
|
997
985
|
const processedPart = stripRedirections(stripEnvVars(part));
|
|
998
986
|
|
|
999
|
-
// Check for
|
|
1000
|
-
const
|
|
1001
|
-
let isSafe = false;
|
|
1002
|
-
|
|
1003
|
-
if (commandMatch && !hasWrite) {
|
|
1004
|
-
const cmd = commandMatch[1];
|
|
1005
|
-
const args = commandMatch[2]?.trim() || "";
|
|
1006
|
-
|
|
1007
|
-
if (SAFE_COMMANDS.includes(cmd)) {
|
|
1008
|
-
if (
|
|
1009
|
-
cmd === "pwd" ||
|
|
1010
|
-
cmd === "true" ||
|
|
1011
|
-
cmd === "false" ||
|
|
1012
|
-
cmd === "ls" ||
|
|
1013
|
-
cmd === "grep" ||
|
|
1014
|
-
cmd === "rg" ||
|
|
1015
|
-
cmd === "cat" ||
|
|
1016
|
-
cmd === "head" ||
|
|
1017
|
-
cmd === "tail" ||
|
|
1018
|
-
cmd === "wc" ||
|
|
1019
|
-
cmd === "sleep" ||
|
|
1020
|
-
(cmd === "find" && !isDangerousFind(part))
|
|
1021
|
-
) {
|
|
1022
|
-
isSafe = true;
|
|
1023
|
-
} else {
|
|
1024
|
-
// For cd, check paths
|
|
1025
|
-
const pathArgs =
|
|
1026
|
-
(args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter(
|
|
1027
|
-
(arg) => !arg.startsWith("-"),
|
|
1028
|
-
) || [];
|
|
1029
|
-
|
|
1030
|
-
if (pathArgs.length === 0) {
|
|
1031
|
-
isSafe = true;
|
|
1032
|
-
} else {
|
|
1033
|
-
const allPathsSafe = pathArgs.every((pathArg) => {
|
|
1034
|
-
const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
|
|
1035
|
-
const { isInside } = this.isInsideSafeZone(cleanPath, workdir);
|
|
1036
|
-
return isInside;
|
|
1037
|
-
});
|
|
1038
|
-
if (allPathsSafe) {
|
|
1039
|
-
isSafe = true;
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
987
|
+
// Check for auto-allowed read-only commands
|
|
988
|
+
const isSafe = this.isAutoAllowedPart(part, workdir);
|
|
1045
989
|
|
|
1046
990
|
if (!isSafe) {
|
|
1047
991
|
// Check if command is dangerous or out-of-bounds
|
|
@@ -1050,7 +994,11 @@ export class PermissionManager {
|
|
|
1050
994
|
const cmd = commandMatch[1];
|
|
1051
995
|
const args = commandMatch[2]?.trim() || "";
|
|
1052
996
|
|
|
1053
|
-
if (
|
|
997
|
+
if (
|
|
998
|
+
DANGEROUS_COMMANDS.includes(cmd) ||
|
|
999
|
+
isDangerousFind(part) ||
|
|
1000
|
+
hasSedInPlace(part)
|
|
1001
|
+
) {
|
|
1054
1002
|
continue;
|
|
1055
1003
|
}
|
|
1056
1004
|
|
|
@@ -2,6 +2,8 @@ import { logger } from "../utils/globalLogger.js";
|
|
|
2
2
|
import { Plugin, PluginConfig } from "../types/index.js";
|
|
3
3
|
import { PluginLoader } from "../services/pluginLoader.js";
|
|
4
4
|
import * as path from "path";
|
|
5
|
+
import { existsSync, readdirSync } from "fs";
|
|
6
|
+
import { getBuiltinPluginsDir } from "../utils/configPaths.js";
|
|
5
7
|
import { SkillManager } from "./skillManager.js";
|
|
6
8
|
import { HookManager } from "./hookManager.js";
|
|
7
9
|
import { LspManager } from "./lspManager.js";
|
|
@@ -98,6 +100,8 @@ export class PluginManager {
|
|
|
98
100
|
|
|
99
101
|
const [name, marketplaceName] = pluginId.split("@");
|
|
100
102
|
if (!name || !marketplaceName) continue;
|
|
103
|
+
// `@builtin` entries are loaded by loadBuiltinPlugins, not the marketplace.
|
|
104
|
+
if (marketplaceName === "builtin") continue;
|
|
101
105
|
|
|
102
106
|
const isInstalled = installedRegistry.plugins.some(
|
|
103
107
|
(p) => p.name === name && p.marketplace === marketplaceName,
|
|
@@ -283,6 +287,31 @@ export class PluginManager {
|
|
|
283
287
|
|
|
284
288
|
// Load installed plugins from marketplace
|
|
285
289
|
await this.loadInstalledPlugins();
|
|
290
|
+
|
|
291
|
+
// Load built-in plugins bundled with the SDK (lowest priority)
|
|
292
|
+
await this.loadBuiltinPlugins();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Load built-in plugins bundled with the SDK (e.g. sdd).
|
|
297
|
+
* Opt-in via `enabledPlugins`, keyed `<name>@builtin` (default off, consistent
|
|
298
|
+
* with marketplace plugins). Lowest priority: explicit config and marketplace
|
|
299
|
+
* plugins win on name conflicts (loadSinglePlugin skips duplicates).
|
|
300
|
+
*/
|
|
301
|
+
private async loadBuiltinPlugins(): Promise<void> {
|
|
302
|
+
try {
|
|
303
|
+
const builtinDir = getBuiltinPluginsDir();
|
|
304
|
+
if (!existsSync(builtinDir)) return;
|
|
305
|
+
|
|
306
|
+
// The builtin plugin directory name is the plugin name by convention.
|
|
307
|
+
for (const entry of readdirSync(builtinDir, { withFileTypes: true })) {
|
|
308
|
+
if (!entry.isDirectory()) continue;
|
|
309
|
+
if (this.enabledPlugins[`${entry.name}@builtin`] !== true) continue;
|
|
310
|
+
await this.loadSinglePlugin(path.join(builtinDir, entry.name));
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
logger?.error("Failed to load built-in plugins:", error);
|
|
314
|
+
}
|
|
286
315
|
}
|
|
287
316
|
|
|
288
317
|
/**
|
|
@@ -253,6 +253,13 @@ class ToolManager {
|
|
|
253
253
|
"WorkflowManager",
|
|
254
254
|
)
|
|
255
255
|
: undefined,
|
|
256
|
+
sessionEnv: this.container.has("ConfigurationService")
|
|
257
|
+
? this.container
|
|
258
|
+
.get<
|
|
259
|
+
import("../services/configurationService.js").ConfigurationService
|
|
260
|
+
>("ConfigurationService")
|
|
261
|
+
?.getMergedEnv?.()
|
|
262
|
+
: undefined,
|
|
256
263
|
sessionId: context.sessionId,
|
|
257
264
|
toolCallId: context.toolCallId,
|
|
258
265
|
};
|