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
|
@@ -11,7 +11,15 @@ import { logger } from "../utils/globalLogger.js";
|
|
|
11
11
|
* Supports ${VAR} and ${VAR:-default} patterns.
|
|
12
12
|
*/
|
|
13
13
|
const WAVE_TEMPLATE_VARS = ["WAVE_PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT"];
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Expand environment variables in a string value.
|
|
16
|
+
* Supports ${VAR} and ${VAR:-default} patterns.
|
|
17
|
+
*
|
|
18
|
+
* @param env - Merged env to resolve against (session snapshot over OS env).
|
|
19
|
+
* Defaults to `process.env` for backward compatibility with tests
|
|
20
|
+
* and standalone callers.
|
|
21
|
+
*/
|
|
22
|
+
export function expandEnvVars(value, env = process.env) {
|
|
15
23
|
return value.replace(/\$\{([^}]+)\}/g, (_match, expr) => {
|
|
16
24
|
const [varName, ...rest] = expr.split(":-");
|
|
17
25
|
const defaultValue = rest.join(":-");
|
|
@@ -19,38 +27,38 @@ export function expandEnvVars(value) {
|
|
|
19
27
|
if (WAVE_TEMPLATE_VARS.includes(varName)) {
|
|
20
28
|
return _match; // return original ${...} string untouched
|
|
21
29
|
}
|
|
22
|
-
return process.env[varName] ?? defaultValue;
|
|
30
|
+
return env[varName] ?? process.env[varName] ?? defaultValue;
|
|
23
31
|
});
|
|
24
32
|
}
|
|
25
33
|
/**
|
|
26
34
|
* Walk an MCP config and resolve environment variables in all string fields.
|
|
27
|
-
*
|
|
28
|
-
* handled at spawn time
|
|
35
|
+
* Expands ${VAR} against the session env snapshot (over OS env); falls back to
|
|
36
|
+
* process.env. WAVE_PLUGIN_ROOT is skipped — handled at spawn time.
|
|
29
37
|
*/
|
|
30
|
-
export function resolveMcpConfig(config) {
|
|
38
|
+
export function resolveMcpConfig(config, env = process.env) {
|
|
31
39
|
const resolved = { mcpServers: {} };
|
|
32
40
|
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
33
41
|
const resolvedServer = { ...serverConfig };
|
|
34
42
|
if (resolvedServer.command) {
|
|
35
|
-
resolvedServer.command = expandEnvVars(resolvedServer.command);
|
|
43
|
+
resolvedServer.command = expandEnvVars(resolvedServer.command, env);
|
|
36
44
|
}
|
|
37
45
|
if (resolvedServer.args) {
|
|
38
|
-
resolvedServer.args = resolvedServer.args.map(expandEnvVars);
|
|
46
|
+
resolvedServer.args = resolvedServer.args.map((a) => expandEnvVars(a, env));
|
|
39
47
|
}
|
|
40
48
|
if (resolvedServer.env) {
|
|
41
49
|
const resolvedEnv = {};
|
|
42
50
|
for (const [key, val] of Object.entries(resolvedServer.env)) {
|
|
43
|
-
resolvedEnv[key] = expandEnvVars(val);
|
|
51
|
+
resolvedEnv[key] = expandEnvVars(val, env);
|
|
44
52
|
}
|
|
45
53
|
resolvedServer.env = resolvedEnv;
|
|
46
54
|
}
|
|
47
55
|
if (resolvedServer.url) {
|
|
48
|
-
resolvedServer.url = expandEnvVars(resolvedServer.url);
|
|
56
|
+
resolvedServer.url = expandEnvVars(resolvedServer.url, env);
|
|
49
57
|
}
|
|
50
58
|
if (resolvedServer.headers) {
|
|
51
59
|
const resolvedHeaders = {};
|
|
52
60
|
for (const [key, val] of Object.entries(resolvedServer.headers)) {
|
|
53
|
-
resolvedHeaders[key] = expandEnvVars(val);
|
|
61
|
+
resolvedHeaders[key] = expandEnvVars(val, env);
|
|
54
62
|
}
|
|
55
63
|
resolvedServer.headers = resolvedHeaders;
|
|
56
64
|
}
|
|
@@ -71,6 +79,16 @@ export class McpManager {
|
|
|
71
79
|
this.callbacks = options.callbacks || {};
|
|
72
80
|
this.mcpServers = options.mcpServers;
|
|
73
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Merged env for this session: OS env overlaid with the per-session settings
|
|
84
|
+
* snapshot. MCP env-var expansion (${VAR}) resolves against this so multiple
|
|
85
|
+
* sessions in one `wave --stdio` process don't read each other's settings env.
|
|
86
|
+
*/
|
|
87
|
+
get envSnapshot() {
|
|
88
|
+
return (this.container
|
|
89
|
+
.get("ConfigurationService")
|
|
90
|
+
?.getMergedEnv?.() ?? process.env);
|
|
91
|
+
}
|
|
74
92
|
/**
|
|
75
93
|
* Initialize MCP manager with working directory and optionally auto-connect
|
|
76
94
|
*/
|
|
@@ -125,7 +143,7 @@ export class McpManager {
|
|
|
125
143
|
try {
|
|
126
144
|
const configContent = await fs.readFile(this.configPath, "utf-8");
|
|
127
145
|
const rawConfig = JSON.parse(configContent);
|
|
128
|
-
const workspaceConfig = resolveMcpConfig(rawConfig);
|
|
146
|
+
const workspaceConfig = resolveMcpConfig(rawConfig, this.envSnapshot);
|
|
129
147
|
// Extract original (pre-resolution) URLs for safe display
|
|
130
148
|
const originalUrls = {};
|
|
131
149
|
for (const [name, serverConfig] of Object.entries(rawConfig.mcpServers)) {
|
|
@@ -210,28 +228,29 @@ export class McpManager {
|
|
|
210
228
|
}
|
|
211
229
|
// Capture original URL before any resolution for safe display
|
|
212
230
|
const originalUrl = config.url;
|
|
213
|
-
// Expand env vars
|
|
231
|
+
// Expand env vars against the session snapshot (over OS env, e.g. ${TAVILY_API_KEY})
|
|
232
|
+
const env = this.envSnapshot;
|
|
214
233
|
const resolvedConfig = { ...config };
|
|
215
234
|
if (resolvedConfig.command) {
|
|
216
|
-
resolvedConfig.command = expandEnvVars(resolvedConfig.command);
|
|
235
|
+
resolvedConfig.command = expandEnvVars(resolvedConfig.command, env);
|
|
217
236
|
}
|
|
218
237
|
if (resolvedConfig.args) {
|
|
219
|
-
resolvedConfig.args = resolvedConfig.args.map(expandEnvVars);
|
|
238
|
+
resolvedConfig.args = resolvedConfig.args.map((a) => expandEnvVars(a, env));
|
|
220
239
|
}
|
|
221
240
|
if (resolvedConfig.env) {
|
|
222
241
|
const resolvedEnv = {};
|
|
223
242
|
for (const [key, val] of Object.entries(resolvedConfig.env)) {
|
|
224
|
-
resolvedEnv[key] = expandEnvVars(val);
|
|
243
|
+
resolvedEnv[key] = expandEnvVars(val, env);
|
|
225
244
|
}
|
|
226
245
|
resolvedConfig.env = resolvedEnv;
|
|
227
246
|
}
|
|
228
247
|
if (resolvedConfig.url) {
|
|
229
|
-
resolvedConfig.url = expandEnvVars(resolvedConfig.url);
|
|
248
|
+
resolvedConfig.url = expandEnvVars(resolvedConfig.url, env);
|
|
230
249
|
}
|
|
231
250
|
if (resolvedConfig.headers) {
|
|
232
251
|
const resolvedHeaders = {};
|
|
233
252
|
for (const [key, val] of Object.entries(resolvedConfig.headers)) {
|
|
234
|
-
resolvedHeaders[key] = expandEnvVars(val);
|
|
253
|
+
resolvedHeaders[key] = expandEnvVars(val, env);
|
|
235
254
|
}
|
|
236
255
|
resolvedConfig.headers = resolvedHeaders;
|
|
237
256
|
}
|
|
@@ -332,8 +351,11 @@ export class McpManager {
|
|
|
332
351
|
if (!server.config.command) {
|
|
333
352
|
throw new Error(`MCP server ${name} with type "stdio" requires a 'command'`);
|
|
334
353
|
}
|
|
354
|
+
// Base env = OS env overlaid with this session's settings snapshot, so
|
|
355
|
+
// custom env vars from settings.json reach the MCP server subprocess
|
|
356
|
+
// without polluting other sessions sharing one `wave --stdio` process.
|
|
335
357
|
const env = {
|
|
336
|
-
...
|
|
358
|
+
...this.envSnapshot,
|
|
337
359
|
...(server.config.env || {}),
|
|
338
360
|
};
|
|
339
361
|
// For plugin servers, substitute ${WAVE_PLUGIN_ROOT} and ${CLAUDE_PLUGIN_ROOT}
|
|
@@ -164,6 +164,13 @@ export declare class PermissionManager {
|
|
|
164
164
|
* Check if a tool call matches a specific permission rule
|
|
165
165
|
*/
|
|
166
166
|
private matchesRule;
|
|
167
|
+
/**
|
|
168
|
+
* Check if a single bash command part is auto-allowed (read-only and safe).
|
|
169
|
+
* Auto-allowed commands skip the confirmation dialog entirely.
|
|
170
|
+
* FR-019.2 through FR-019.7: read-only commands without write redirections,
|
|
171
|
+
* command substitution, process substitution, or sed -i are auto-allowed.
|
|
172
|
+
*/
|
|
173
|
+
private isAutoAllowedPart;
|
|
167
174
|
/**
|
|
168
175
|
* Check if a tool call is allowed by persistent or temporary rules
|
|
169
176
|
*/
|
|
@@ -8,25 +8,9 @@
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { minimatch } from "minimatch";
|
|
10
10
|
import { RESTRICTED_TOOLS } from "../types/permissions.js";
|
|
11
|
-
import { splitBashCommand, stripEnvVars, stripRedirections, hasWriteRedirections, getSmartPrefix, isDangerousFind, DANGEROUS_COMMANDS, } from "../utils/bashParser.js";
|
|
11
|
+
import { splitBashCommand, stripEnvVars, stripRedirections, hasWriteRedirections, getSmartPrefix, isDangerousFind, hasCommandSubstitution, hasProcessSubstitution, hasSedInPlace, DANGEROUS_COMMANDS, READ_ONLY_COMMANDS, } from "../utils/bashParser.js";
|
|
12
12
|
import { isPathInside } from "../utils/pathSafety.js";
|
|
13
13
|
import { BASH_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, READ_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "../constants/tools.js";
|
|
14
|
-
const SAFE_COMMANDS = [
|
|
15
|
-
"cd",
|
|
16
|
-
"ls",
|
|
17
|
-
"pwd",
|
|
18
|
-
"true",
|
|
19
|
-
"false",
|
|
20
|
-
"grep",
|
|
21
|
-
"rg",
|
|
22
|
-
"cat",
|
|
23
|
-
"head",
|
|
24
|
-
"tail",
|
|
25
|
-
"wc",
|
|
26
|
-
"sleep",
|
|
27
|
-
"find",
|
|
28
|
-
"sort",
|
|
29
|
-
];
|
|
30
14
|
const DEFAULT_ALLOWED_RULES = [
|
|
31
15
|
"Bash(git status*)",
|
|
32
16
|
"Bash(git diff*)",
|
|
@@ -573,6 +557,12 @@ export class PermissionManager {
|
|
|
573
557
|
if (hasWriteRedirections(part)) {
|
|
574
558
|
return true;
|
|
575
559
|
}
|
|
560
|
+
// Command/process substitution and sed -i are dangerous (FR-019.5, FR-019.6)
|
|
561
|
+
if (hasCommandSubstitution(part) ||
|
|
562
|
+
hasProcessSubstitution(part) ||
|
|
563
|
+
hasSedInPlace(part)) {
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
576
566
|
const processedPart = stripRedirections(stripEnvVars(part));
|
|
577
567
|
const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
|
|
578
568
|
if (commandMatch) {
|
|
@@ -665,95 +655,101 @@ export class PermissionManager {
|
|
|
665
655
|
return false;
|
|
666
656
|
}
|
|
667
657
|
/**
|
|
668
|
-
* Check if a
|
|
658
|
+
* Check if a single bash command part is auto-allowed (read-only and safe).
|
|
659
|
+
* Auto-allowed commands skip the confirmation dialog entirely.
|
|
660
|
+
* FR-019.2 through FR-019.7: read-only commands without write redirections,
|
|
661
|
+
* command substitution, process substitution, or sed -i are auto-allowed.
|
|
669
662
|
*/
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
cmd === "cat" ||
|
|
695
|
-
cmd === "head" ||
|
|
696
|
-
cmd === "tail" ||
|
|
697
|
-
cmd === "wc" ||
|
|
698
|
-
cmd === "sleep" ||
|
|
699
|
-
cmd === "sort" ||
|
|
700
|
-
(cmd === "find" && !isDangerousFind(part))) {
|
|
701
|
-
return true;
|
|
702
|
-
}
|
|
703
|
-
if (workdir) {
|
|
704
|
-
// For cd, check paths
|
|
705
|
-
const pathArgs = (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter((arg) => !arg.startsWith("-")) || [];
|
|
706
|
-
if (pathArgs.length === 0) {
|
|
707
|
-
// cd without arguments operates on current dir (workdir)
|
|
708
|
-
return true;
|
|
709
|
-
}
|
|
710
|
-
const allPathsSafe = pathArgs.every((pathArg) => {
|
|
711
|
-
// Remove quotes if present
|
|
712
|
-
const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
|
|
713
|
-
const { isInside } = this.isInsideSafeZone(cleanPath, workdir);
|
|
714
|
-
return isInside;
|
|
715
|
-
});
|
|
716
|
-
if (allPathsSafe) {
|
|
717
|
-
return true;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
// Check if this specific part is allowed by any rule
|
|
724
|
-
if (isDefaultRules && (hasWrite || isDangerousFind(part))) {
|
|
725
|
-
return false;
|
|
726
|
-
}
|
|
727
|
-
// We create a temporary context with just this part of the command
|
|
728
|
-
const partContext = {
|
|
729
|
-
...ctx,
|
|
730
|
-
toolInput: { ...ctx.toolInput, command: part },
|
|
731
|
-
};
|
|
732
|
-
const allowedByRule = rules.some((rule) => {
|
|
733
|
-
return this.matchesRule(partContext, rule);
|
|
734
|
-
});
|
|
735
|
-
if (allowedByRule)
|
|
736
|
-
return true;
|
|
737
|
-
return !this.isRestrictedTool(ctx.toolName);
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
// For other tools, check if any rule matches
|
|
741
|
-
return rules.some((rule) => this.matchesRule(ctx, rule));
|
|
742
|
-
};
|
|
743
|
-
// Check instance-specific allowed rules first
|
|
744
|
-
if (isAllowedByRuleList(context, this.instanceAllowedRules)) {
|
|
663
|
+
isAutoAllowedPart(part, workdir) {
|
|
664
|
+
// Write redirections disqualify (FR-019.4)
|
|
665
|
+
if (hasWriteRedirections(part))
|
|
666
|
+
return false;
|
|
667
|
+
// Command substitution $(...) or `...` disqualifies (FR-019.6)
|
|
668
|
+
if (hasCommandSubstitution(part))
|
|
669
|
+
return false;
|
|
670
|
+
// Process substitution <(...) or >(...) disqualifies (FR-019.6)
|
|
671
|
+
if (hasProcessSubstitution(part))
|
|
672
|
+
return false;
|
|
673
|
+
const processedPart = stripRedirections(stripEnvVars(part));
|
|
674
|
+
const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
|
|
675
|
+
if (!commandMatch)
|
|
676
|
+
return false;
|
|
677
|
+
const cmd = commandMatch[1];
|
|
678
|
+
const args = commandMatch[2]?.trim() || "";
|
|
679
|
+
// sed -i (in-place edit) disqualifies (FR-019.5)
|
|
680
|
+
if (hasSedInPlace(part))
|
|
681
|
+
return false;
|
|
682
|
+
// Read-only commands are auto-allowed (FR-019.2, FR-019.3)
|
|
683
|
+
if (READ_ONLY_COMMANDS.includes(cmd)) {
|
|
684
|
+
// find with dangerous flags (e.g. -exec, -delete) disqualifies
|
|
685
|
+
if (cmd === "find" && isDangerousFind(part))
|
|
686
|
+
return false;
|
|
745
687
|
return true;
|
|
746
688
|
}
|
|
747
|
-
//
|
|
748
|
-
if (
|
|
749
|
-
|
|
689
|
+
// cd is not read-only but is safe if all paths are within the Safe Zone
|
|
690
|
+
if (cmd === "cd") {
|
|
691
|
+
if (!workdir)
|
|
692
|
+
return false;
|
|
693
|
+
const pathArgs = (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter((arg) => !arg.startsWith("-")) || [];
|
|
694
|
+
if (pathArgs.length === 0)
|
|
695
|
+
return true; // cd without args = current dir
|
|
696
|
+
return pathArgs.every((pathArg) => {
|
|
697
|
+
const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
|
|
698
|
+
const { isInside } = this.isInsideSafeZone(cleanPath, workdir);
|
|
699
|
+
return isInside;
|
|
700
|
+
});
|
|
750
701
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Check if a tool call is allowed by persistent or temporary rules
|
|
706
|
+
*/
|
|
707
|
+
isAllowedByRule(context) {
|
|
708
|
+
// All allow-rule sources are matched as a union: each part of a chained
|
|
709
|
+
// command only needs to hit at least one rule across all sources, so
|
|
710
|
+
// different parts may be covered by different sources.
|
|
711
|
+
const explicitRules = [
|
|
712
|
+
...this.instanceAllowedRules,
|
|
713
|
+
...this.temporaryRules,
|
|
714
|
+
...this.allowedRules,
|
|
715
|
+
];
|
|
716
|
+
if (context.toolName === BASH_TOOL_NAME && context.toolInput?.command) {
|
|
717
|
+
const command = String(context.toolInput.command);
|
|
718
|
+
const parts = splitBashCommand(command);
|
|
719
|
+
if (parts.length === 0)
|
|
720
|
+
return false;
|
|
721
|
+
const workdir = context.toolInput?.workdir;
|
|
722
|
+
return parts.every((part) => {
|
|
723
|
+
// Check for auto-allowed read-only commands (FR-019.2 through FR-019.7)
|
|
724
|
+
if (this.isAutoAllowedPart(part, workdir)) {
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
// We create a temporary context with just this part of the command
|
|
728
|
+
const partContext = {
|
|
729
|
+
...context,
|
|
730
|
+
toolInput: { ...context.toolInput, command: part },
|
|
731
|
+
};
|
|
732
|
+
if (explicitRules.some((rule) => this.matchesRule(partContext, rule))) {
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
// Default rules must not auto-allow dangerous variants (write
|
|
736
|
+
// redirections, substitutions, sed -i, dangerous find) through broad
|
|
737
|
+
// rules like Bash(echo*) or Bash(cat*)
|
|
738
|
+
const isDangerousVariant = hasWriteRedirections(part) ||
|
|
739
|
+
isDangerousFind(part) ||
|
|
740
|
+
hasCommandSubstitution(part) ||
|
|
741
|
+
hasProcessSubstitution(part) ||
|
|
742
|
+
hasSedInPlace(part);
|
|
743
|
+
if (!isDangerousVariant &&
|
|
744
|
+
DEFAULT_ALLOWED_RULES.some((rule) => this.matchesRule(partContext, rule))) {
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
return !this.isRestrictedTool(context.toolName);
|
|
748
|
+
});
|
|
754
749
|
}
|
|
755
|
-
//
|
|
756
|
-
|
|
750
|
+
// For other tools, check if any rule matches
|
|
751
|
+
const allRules = [...explicitRules, ...DEFAULT_ALLOWED_RULES];
|
|
752
|
+
return allRules.some((rule) => this.matchesRule(context, rule));
|
|
757
753
|
}
|
|
758
754
|
/**
|
|
759
755
|
* Expand a bash command into individual permission rules, filtering out safe commands.
|
|
@@ -769,53 +765,17 @@ export class PermissionManager {
|
|
|
769
765
|
for (const part of parts) {
|
|
770
766
|
const hasWrite = hasWriteRedirections(part);
|
|
771
767
|
const processedPart = stripRedirections(stripEnvVars(part));
|
|
772
|
-
// Check for
|
|
773
|
-
const
|
|
774
|
-
let isSafe = false;
|
|
775
|
-
if (commandMatch && !hasWrite) {
|
|
776
|
-
const cmd = commandMatch[1];
|
|
777
|
-
const args = commandMatch[2]?.trim() || "";
|
|
778
|
-
if (SAFE_COMMANDS.includes(cmd)) {
|
|
779
|
-
if (cmd === "pwd" ||
|
|
780
|
-
cmd === "true" ||
|
|
781
|
-
cmd === "false" ||
|
|
782
|
-
cmd === "ls" ||
|
|
783
|
-
cmd === "grep" ||
|
|
784
|
-
cmd === "rg" ||
|
|
785
|
-
cmd === "cat" ||
|
|
786
|
-
cmd === "head" ||
|
|
787
|
-
cmd === "tail" ||
|
|
788
|
-
cmd === "wc" ||
|
|
789
|
-
cmd === "sleep" ||
|
|
790
|
-
(cmd === "find" && !isDangerousFind(part))) {
|
|
791
|
-
isSafe = true;
|
|
792
|
-
}
|
|
793
|
-
else {
|
|
794
|
-
// For cd, check paths
|
|
795
|
-
const pathArgs = (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter((arg) => !arg.startsWith("-")) || [];
|
|
796
|
-
if (pathArgs.length === 0) {
|
|
797
|
-
isSafe = true;
|
|
798
|
-
}
|
|
799
|
-
else {
|
|
800
|
-
const allPathsSafe = pathArgs.every((pathArg) => {
|
|
801
|
-
const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
|
|
802
|
-
const { isInside } = this.isInsideSafeZone(cleanPath, workdir);
|
|
803
|
-
return isInside;
|
|
804
|
-
});
|
|
805
|
-
if (allPathsSafe) {
|
|
806
|
-
isSafe = true;
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
768
|
+
// Check for auto-allowed read-only commands
|
|
769
|
+
const isSafe = this.isAutoAllowedPart(part, workdir);
|
|
812
770
|
if (!isSafe) {
|
|
813
771
|
// Check if command is dangerous or out-of-bounds
|
|
814
772
|
const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
|
|
815
773
|
if (commandMatch) {
|
|
816
774
|
const cmd = commandMatch[1];
|
|
817
775
|
const args = commandMatch[2]?.trim() || "";
|
|
818
|
-
if (DANGEROUS_COMMANDS.includes(cmd) ||
|
|
776
|
+
if (DANGEROUS_COMMANDS.includes(cmd) ||
|
|
777
|
+
isDangerousFind(part) ||
|
|
778
|
+
hasSedInPlace(part)) {
|
|
819
779
|
continue;
|
|
820
780
|
}
|
|
821
781
|
if (cmd === "cd") {
|
|
@@ -34,6 +34,13 @@ export declare class PluginManager {
|
|
|
34
34
|
* @param configs Array of plugin configurations
|
|
35
35
|
*/
|
|
36
36
|
loadPlugins(configs: PluginConfig[]): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Load built-in plugins bundled with the SDK (e.g. sdd).
|
|
39
|
+
* Opt-in via `enabledPlugins`, keyed `<name>@builtin` (default off, consistent
|
|
40
|
+
* with marketplace plugins). Lowest priority: explicit config and marketplace
|
|
41
|
+
* plugins win on name conflicts (loadSinglePlugin skips duplicates).
|
|
42
|
+
*/
|
|
43
|
+
private loadBuiltinPlugins;
|
|
37
44
|
/**
|
|
38
45
|
* Get all loaded plugins
|
|
39
46
|
*/
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { logger } from "../utils/globalLogger.js";
|
|
2
2
|
import { PluginLoader } from "../services/pluginLoader.js";
|
|
3
3
|
import * as path from "path";
|
|
4
|
+
import { existsSync, readdirSync } from "fs";
|
|
5
|
+
import { getBuiltinPluginsDir } from "../utils/configPaths.js";
|
|
4
6
|
import { MarketplaceService } from "../services/MarketplaceService.js";
|
|
5
7
|
export class PluginManager {
|
|
6
8
|
constructor(container, options) {
|
|
@@ -61,6 +63,9 @@ export class PluginManager {
|
|
|
61
63
|
const [name, marketplaceName] = pluginId.split("@");
|
|
62
64
|
if (!name || !marketplaceName)
|
|
63
65
|
continue;
|
|
66
|
+
// `@builtin` entries are loaded by loadBuiltinPlugins, not the marketplace.
|
|
67
|
+
if (marketplaceName === "builtin")
|
|
68
|
+
continue;
|
|
64
69
|
const isInstalled = installedRegistry.plugins.some((p) => p.name === name && p.marketplace === marketplaceName);
|
|
65
70
|
if (!isInstalled) {
|
|
66
71
|
const isMarketplaceKnown = knownMarketplaces.some((m) => m.name === marketplaceName);
|
|
@@ -195,6 +200,32 @@ export class PluginManager {
|
|
|
195
200
|
}
|
|
196
201
|
// Load installed plugins from marketplace
|
|
197
202
|
await this.loadInstalledPlugins();
|
|
203
|
+
// Load built-in plugins bundled with the SDK (lowest priority)
|
|
204
|
+
await this.loadBuiltinPlugins();
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Load built-in plugins bundled with the SDK (e.g. sdd).
|
|
208
|
+
* Opt-in via `enabledPlugins`, keyed `<name>@builtin` (default off, consistent
|
|
209
|
+
* with marketplace plugins). Lowest priority: explicit config and marketplace
|
|
210
|
+
* plugins win on name conflicts (loadSinglePlugin skips duplicates).
|
|
211
|
+
*/
|
|
212
|
+
async loadBuiltinPlugins() {
|
|
213
|
+
try {
|
|
214
|
+
const builtinDir = getBuiltinPluginsDir();
|
|
215
|
+
if (!existsSync(builtinDir))
|
|
216
|
+
return;
|
|
217
|
+
// The builtin plugin directory name is the plugin name by convention.
|
|
218
|
+
for (const entry of readdirSync(builtinDir, { withFileTypes: true })) {
|
|
219
|
+
if (!entry.isDirectory())
|
|
220
|
+
continue;
|
|
221
|
+
if (this.enabledPlugins[`${entry.name}@builtin`] !== true)
|
|
222
|
+
continue;
|
|
223
|
+
await this.loadSinglePlugin(path.join(builtinDir, entry.name));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
logger?.error("Failed to load built-in plugins:", error);
|
|
228
|
+
}
|
|
198
229
|
}
|
|
199
230
|
/**
|
|
200
231
|
* Get all loaded plugins
|
|
@@ -179,6 +179,11 @@ class ToolManager {
|
|
|
179
179
|
workflowManager: this.container.has("WorkflowManager")
|
|
180
180
|
? this.container.get("WorkflowManager")
|
|
181
181
|
: undefined,
|
|
182
|
+
sessionEnv: this.container.has("ConfigurationService")
|
|
183
|
+
? this.container
|
|
184
|
+
.get("ConfigurationService")
|
|
185
|
+
?.getMergedEnv?.()
|
|
186
|
+
: undefined,
|
|
182
187
|
sessionId: context.sessionId,
|
|
183
188
|
toolCallId: context.toolCallId,
|
|
184
189
|
};
|
package/dist/prompts/index.d.ts
CHANGED
|
@@ -21,7 +21,18 @@ export interface SystemPromptBlock {
|
|
|
21
21
|
text: string;
|
|
22
22
|
cacheable: boolean;
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Builds the compact instruction sent as the trailing user message on the
|
|
26
|
+
* fork path. Aligned with Claude Code's getCompactPrompt().
|
|
27
|
+
*/
|
|
28
|
+
export declare function getCompactPrompt(customInstructions?: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Formats the compact summary by stripping the <analysis> drafting scratchpad
|
|
31
|
+
* and extracting the <summary> section. Raw text passes through unchanged
|
|
32
|
+
* when no <summary> tag is present. Aligned with Claude Code's
|
|
33
|
+
* formatCompactSummary().
|
|
34
|
+
*/
|
|
35
|
+
export declare function formatCompactSummary(summary: string): string;
|
|
25
36
|
export declare const WEB_CONTENT_SYSTEM_PROMPT = "You are a helpful assistant that extracts information from web content. The content is provided in Markdown format.";
|
|
26
37
|
export declare const BTW_SYSTEM_PROMPT = "You are a helpful assistant. Answer the user's side question based on the conversation history. \nDo NOT say things like \"Let me try...\", \"I'll now...\", \"Let me check...\", or promise to take any action. \nIf you don't know the answer, say so - do not offer to look it up or investigate. \nSimply answer the question with the information you have.";
|
|
27
38
|
export declare function buildSystemPrompt(basePrompt: string | undefined, tools: ToolPlugin[], options?: {
|