wave-agent-sdk 0.11.4 → 0.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/managers/backgroundTaskManager.js +3 -3
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +36 -14
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +35 -3
- package/dist/utils/bashParser.d.ts +8 -0
- package/dist/utils/bashParser.d.ts.map +1 -1
- package/dist/utils/bashParser.js +46 -1
- package/package.json +1 -1
- package/src/managers/backgroundTaskManager.ts +3 -3
- package/src/managers/mcpManager.ts +41 -17
- package/src/managers/permissionManager.ts +38 -2
- package/src/utils/bashParser.ts +58 -1
|
@@ -16,7 +16,7 @@ export class BackgroundTaskManager {
|
|
|
16
16
|
this.callbacks.onBackgroundTasksChange?.(Array.from(this.tasks.values()));
|
|
17
17
|
}
|
|
18
18
|
generateId() {
|
|
19
|
-
return `task_${this.nextId++}`;
|
|
19
|
+
return `task_${process.pid}_${this.nextId++}`;
|
|
20
20
|
}
|
|
21
21
|
addTask(task) {
|
|
22
22
|
this.tasks.set(task.id, task);
|
|
@@ -41,7 +41,7 @@ export class BackgroundTaskManager {
|
|
|
41
41
|
});
|
|
42
42
|
// Create log file
|
|
43
43
|
const logPath = path.join(os.tmpdir(), `wave-task-${id}.log`);
|
|
44
|
-
const logStream = fs.createWriteStream(logPath, { flags: "
|
|
44
|
+
const logStream = fs.createWriteStream(logPath, { flags: "w" });
|
|
45
45
|
const shell = {
|
|
46
46
|
id,
|
|
47
47
|
type: "shell",
|
|
@@ -156,7 +156,7 @@ export class BackgroundTaskManager {
|
|
|
156
156
|
const startTime = Date.now();
|
|
157
157
|
// Create log file
|
|
158
158
|
const logPath = path.join(os.tmpdir(), `wave-task-${id}.log`);
|
|
159
|
-
const logStream = fs.createWriteStream(logPath, { flags: "
|
|
159
|
+
const logStream = fs.createWriteStream(logPath, { flags: "w" });
|
|
160
160
|
// Write initial output to log file
|
|
161
161
|
if (initialStdout) {
|
|
162
162
|
logStream.write(stripAnsiColors(initialStdout));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcpManager.d.ts","sourceRoot":"","sources":["../../src/managers/mcpManager.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,EACV,MAAM,EACN,eAAe,EACf,SAAS,EACT,OAAO,EACP,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAQ3B,MAAM,WAAW,mBAAmB;IAClC,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,IAAI,CAAC;CACxD;AAID,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,UAAU;IASnB,OAAO,CAAC,SAAS;IARnB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,OAAO,CAA2C;IAC1D,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,UAAU,CAAc;IAChC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,SAAS,CAAsB;gBAG7B,SAAS,EAAE,SAAS,EAC5B,OAAO,GAAE,iBAAsB;IAKjC;;OAEG;IACG,UAAU,CACd,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,OAAe,GAC3B,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"mcpManager.d.ts","sourceRoot":"","sources":["../../src/managers/mcpManager.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,EACV,MAAM,EACN,eAAe,EACf,SAAS,EACT,OAAO,EACP,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAQ3B,MAAM,WAAW,mBAAmB;IAClC,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,IAAI,CAAC;CACxD;AAID,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,UAAU;IASnB,OAAO,CAAC,SAAS;IARnB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,OAAO,CAA2C;IAC1D,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,UAAU,CAAc;IAChC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,SAAS,CAAsB;gBAG7B,SAAS,EAAE,SAAS,EAC5B,OAAO,GAAE,iBAAsB;IAKjC;;OAEG;IACG,UAAU,CACd,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,OAAe,GAC3B,OAAO,CAAC,IAAI,CAAC;IAuCV,kBAAkB,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAO/C,UAAU,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA0CvC,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAWrD,SAAS,IAAI,SAAS,GAAG,IAAI;IAI7B,aAAa,IAAI,eAAe,EAAE;IAIlC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAIpD,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI;IASzE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO;IAyBzD,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAgB7B,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqH7C,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA2BtD,oBAAoB,IAAI,OAAO,EAAE;IAW3B,cAAc,CAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,MAAM,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACtD,CAAC;YAsDY,uBAAuB;IA8D/B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAS9B;;OAEG;IACH,iBAAiB,IAAI,UAAU,EAAE;IA6BjC;;OAEG;IACH,iBAAiB,IAAI,0BAA0B,EAAE;IAIjD;;OAEG;IACG,wBAAwB,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC;IActB;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CAcjC"}
|
|
@@ -25,27 +25,25 @@ export class McpManager {
|
|
|
25
25
|
// Ensure MCP configuration is loaded
|
|
26
26
|
const config = await this.ensureConfigLoaded();
|
|
27
27
|
if (config && config.mcpServers) {
|
|
28
|
-
// Connect to all configured servers
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
// Connect to all configured servers in background to avoid blocking agent initialization
|
|
29
|
+
Object.keys(config.mcpServers).forEach((serverName) => {
|
|
30
|
+
logger?.debug(`Connecting to MCP server: ${serverName}`);
|
|
31
|
+
this.connectServer(serverName)
|
|
32
|
+
.then((success) => {
|
|
33
33
|
if (success) {
|
|
34
34
|
logger?.debug(`Successfully connected to MCP server: ${serverName}`);
|
|
35
35
|
}
|
|
36
36
|
else {
|
|
37
37
|
logger?.warn(`Failed to connect to MCP server: ${serverName}`);
|
|
38
38
|
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
logger?.error(`
|
|
42
|
-
}
|
|
39
|
+
})
|
|
40
|
+
.catch((error) => {
|
|
41
|
+
logger?.error(`Background connection to MCP server ${serverName} failed:`, error);
|
|
42
|
+
});
|
|
43
43
|
});
|
|
44
|
-
// Wait for all connection attempts to complete
|
|
45
|
-
await Promise.all(connectionPromises);
|
|
46
44
|
}
|
|
47
|
-
logger?.debug("MCP servers initialization
|
|
48
|
-
// Trigger state change callback after initialization
|
|
45
|
+
logger?.debug("MCP servers initialization started in background");
|
|
46
|
+
// Trigger state change callback after starting initialization
|
|
49
47
|
this.callbacks.onServersChange?.(this.getAllServers());
|
|
50
48
|
}
|
|
51
49
|
}
|
|
@@ -168,9 +166,33 @@ export class McpManager {
|
|
|
168
166
|
const transport = new StdioClientTransport({
|
|
169
167
|
command: server.config.command,
|
|
170
168
|
args: server.config.args || [],
|
|
171
|
-
env:
|
|
169
|
+
env: {
|
|
170
|
+
...process.env,
|
|
171
|
+
...(server.config.env || {}),
|
|
172
|
+
},
|
|
172
173
|
cwd: this.workdir, // Use the agent's workdir as the process working directory
|
|
174
|
+
stderr: "pipe", // Pipe stderr to capture it
|
|
173
175
|
});
|
|
176
|
+
// Handle stderr output
|
|
177
|
+
const stderr = transport.stderr;
|
|
178
|
+
if (stderr) {
|
|
179
|
+
let buffer = "";
|
|
180
|
+
stderr.on("data", (chunk) => {
|
|
181
|
+
buffer += chunk.toString();
|
|
182
|
+
const lines = buffer.split("\n");
|
|
183
|
+
buffer = lines.pop() || "";
|
|
184
|
+
for (const line of lines) {
|
|
185
|
+
if (line.trim()) {
|
|
186
|
+
logger?.error(`[MCP Server ${name}] ${line}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
stderr.on("end", () => {
|
|
191
|
+
if (buffer.trim()) {
|
|
192
|
+
logger?.error(`[MCP Server ${name}] ${buffer}`);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
174
196
|
// Create client
|
|
175
197
|
const client = new Client({
|
|
176
198
|
name: "wave-code",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissionManager.d.ts","sourceRoot":"","sources":["../../src/managers/permissionManager.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EACV,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,cAAc,EACf,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"permissionManager.d.ts","sourceRoot":"","sources":["../../src/managers/permissionManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,7 +8,7 @@
|
|
|
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, DANGEROUS_COMMANDS, } from "../utils/bashParser.js";
|
|
11
|
+
import { splitBashCommand, stripEnvVars, stripRedirections, hasWriteRedirections, isBashHeredocWrite, getSmartPrefix, DANGEROUS_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, } from "../constants/tools.js";
|
|
14
14
|
const SAFE_COMMANDS = [
|
|
@@ -23,6 +23,7 @@ const SAFE_COMMANDS = [
|
|
|
23
23
|
"head",
|
|
24
24
|
"tail",
|
|
25
25
|
"wc",
|
|
26
|
+
"sleep",
|
|
26
27
|
];
|
|
27
28
|
const DEFAULT_ALLOWED_RULES = [
|
|
28
29
|
"Bash(git status*)",
|
|
@@ -63,6 +64,7 @@ const DEFAULT_ALLOWED_RULES = [
|
|
|
63
64
|
"Bash(head*)",
|
|
64
65
|
"Bash(tail*)",
|
|
65
66
|
"Bash(wc*)",
|
|
67
|
+
"Bash(sleep*)",
|
|
66
68
|
];
|
|
67
69
|
import { logger } from "../utils/globalLogger.js";
|
|
68
70
|
export class PermissionManager {
|
|
@@ -264,6 +266,34 @@ export class PermissionManager {
|
|
|
264
266
|
* Called by individual tools after validation/diff, before real operation
|
|
265
267
|
*/
|
|
266
268
|
async checkPermission(context) {
|
|
269
|
+
// 0. Intercept Bash EOF writing operations
|
|
270
|
+
if (context.toolName === BASH_TOOL_NAME && context.toolInput?.command) {
|
|
271
|
+
const command = String(context.toolInput.command);
|
|
272
|
+
if (isBashHeredocWrite(command)) {
|
|
273
|
+
// Check if this specific command is explicitly allowed by a rule that includes redirection
|
|
274
|
+
const isExplicitlyAllowed = [
|
|
275
|
+
...this.instanceAllowedRules,
|
|
276
|
+
...this.temporaryRules,
|
|
277
|
+
...this.allowedRules,
|
|
278
|
+
...DEFAULT_ALLOWED_RULES,
|
|
279
|
+
].some((rule) => {
|
|
280
|
+
if (rule.startsWith("Bash(") && rule.endsWith(")")) {
|
|
281
|
+
const pattern = rule.substring(5, rule.length - 1);
|
|
282
|
+
// If the pattern itself has write redirections, we check if it matches
|
|
283
|
+
if (hasWriteRedirections(pattern)) {
|
|
284
|
+
return this.matchesRule(context, rule);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
});
|
|
289
|
+
if (!isExplicitlyAllowed) {
|
|
290
|
+
return {
|
|
291
|
+
behavior: "deny",
|
|
292
|
+
message: "Bash-based file writing operations using heredocs (e.g., 'cat <<EOF > file') are not allowed. Please use the dedicated 'Write' or 'Edit' tools instead for file modifications.",
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
267
297
|
// 0. Check instance-specific denied rules first - Deny always takes precedence
|
|
268
298
|
for (const rule of this.instanceDeniedRules) {
|
|
269
299
|
if (this.matchesRule(context, rule)) {
|
|
@@ -627,7 +657,8 @@ export class PermissionManager {
|
|
|
627
657
|
cmd === "cat" ||
|
|
628
658
|
cmd === "head" ||
|
|
629
659
|
cmd === "tail" ||
|
|
630
|
-
cmd === "wc"
|
|
660
|
+
cmd === "wc" ||
|
|
661
|
+
cmd === "sleep") {
|
|
631
662
|
return true;
|
|
632
663
|
}
|
|
633
664
|
if (workdir) {
|
|
@@ -715,7 +746,8 @@ export class PermissionManager {
|
|
|
715
746
|
cmd === "cat" ||
|
|
716
747
|
cmd === "head" ||
|
|
717
748
|
cmd === "tail" ||
|
|
718
|
-
cmd === "wc"
|
|
749
|
+
cmd === "wc" ||
|
|
750
|
+
cmd === "sleep") {
|
|
719
751
|
isSafe = true;
|
|
720
752
|
}
|
|
721
753
|
else {
|
|
@@ -15,6 +15,14 @@ export declare function stripRedirections(command: string): string;
|
|
|
15
15
|
* Checks if a bash command contains any write redirections (>, >>, &>, 2>, >|).
|
|
16
16
|
*/
|
|
17
17
|
export declare function hasWriteRedirections(command: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Checks if a bash command contains any heredocs (<<, <<-).
|
|
20
|
+
*/
|
|
21
|
+
export declare function hasHeredoc(command: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Checks if a bash command is a heredoc write operation (e.g., cat <<EOF > file).
|
|
24
|
+
*/
|
|
25
|
+
export declare function isBashHeredocWrite(command: string): boolean;
|
|
18
26
|
/**
|
|
19
27
|
* Blacklist of dangerous commands that should not be safely prefix-matched
|
|
20
28
|
* and should not have persistent permissions.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bashParser.d.ts","sourceRoot":"","sources":["../../src/utils/bashParser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"bashParser.d.ts","sourceRoot":"","sources":["../../src/utils/bashParser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAoH1D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA2CpD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAuHzD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAkG7D;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAsCnD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,UAa9B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA8L7D"}
|
package/dist/utils/bashParser.js
CHANGED
|
@@ -53,7 +53,7 @@ export function splitBashCommand(command) {
|
|
|
53
53
|
opLen = 1;
|
|
54
54
|
else if (char === "|")
|
|
55
55
|
opLen = 1;
|
|
56
|
-
else if (char === "&" && nextChar !== ">")
|
|
56
|
+
else if (char === "&" && nextChar !== ">" && command[i - 1] !== ">")
|
|
57
57
|
opLen = 1;
|
|
58
58
|
if (opLen > 0) {
|
|
59
59
|
// Check if preceded by an odd number of backslashes
|
|
@@ -350,11 +350,56 @@ export function hasWriteRedirections(command) {
|
|
|
350
350
|
i = k - 1; // Move the main loop index to the end of the target
|
|
351
351
|
continue;
|
|
352
352
|
}
|
|
353
|
+
// Ignore file descriptor redirections like 2>&1, >&2, etc.
|
|
354
|
+
if (target.startsWith("&") && /^\d+$/.test(target.substring(1))) {
|
|
355
|
+
i = k - 1;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Checks if a bash command contains any heredocs (<<, <<-).
|
|
365
|
+
*/
|
|
366
|
+
export function hasHeredoc(command) {
|
|
367
|
+
let inSingleQuote = false;
|
|
368
|
+
let inDoubleQuote = false;
|
|
369
|
+
let escaped = false;
|
|
370
|
+
for (let i = 0; i < command.length; i++) {
|
|
371
|
+
const char = command[i];
|
|
372
|
+
if (escaped) {
|
|
373
|
+
escaped = false;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (char === "\\") {
|
|
377
|
+
escaped = true;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (char === "'" && !inDoubleQuote) {
|
|
381
|
+
inSingleQuote = !inSingleQuote;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (char === '"' && !inSingleQuote) {
|
|
385
|
+
inDoubleQuote = !inDoubleQuote;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (char === "<" && command[i + 1] === "<") {
|
|
353
392
|
return true;
|
|
354
393
|
}
|
|
355
394
|
}
|
|
356
395
|
return false;
|
|
357
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Checks if a bash command is a heredoc write operation (e.g., cat <<EOF > file).
|
|
399
|
+
*/
|
|
400
|
+
export function isBashHeredocWrite(command) {
|
|
401
|
+
return hasHeredoc(command) && hasWriteRedirections(command);
|
|
402
|
+
}
|
|
358
403
|
/**
|
|
359
404
|
* Blacklist of dangerous commands that should not be safely prefix-matched
|
|
360
405
|
* and should not have persistent permissions.
|
package/package.json
CHANGED
|
@@ -35,7 +35,7 @@ export class BackgroundTaskManager {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
public generateId(): string {
|
|
38
|
-
return `task_${this.nextId++}`;
|
|
38
|
+
return `task_${process.pid}_${this.nextId++}`;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
public addTask(task: BackgroundTask): void {
|
|
@@ -69,7 +69,7 @@ export class BackgroundTaskManager {
|
|
|
69
69
|
|
|
70
70
|
// Create log file
|
|
71
71
|
const logPath = path.join(os.tmpdir(), `wave-task-${id}.log`);
|
|
72
|
-
const logStream = fs.createWriteStream(logPath, { flags: "
|
|
72
|
+
const logStream = fs.createWriteStream(logPath, { flags: "w" });
|
|
73
73
|
|
|
74
74
|
const shell: BackgroundShell = {
|
|
75
75
|
id,
|
|
@@ -198,7 +198,7 @@ export class BackgroundTaskManager {
|
|
|
198
198
|
|
|
199
199
|
// Create log file
|
|
200
200
|
const logPath = path.join(os.tmpdir(), `wave-task-${id}.log`);
|
|
201
|
-
const logStream = fs.createWriteStream(logPath, { flags: "
|
|
201
|
+
const logStream = fs.createWriteStream(logPath, { flags: "w" });
|
|
202
202
|
|
|
203
203
|
// Write initial output to log file
|
|
204
204
|
if (initialStdout) {
|
|
@@ -63,12 +63,11 @@ export class McpManager {
|
|
|
63
63
|
const config = await this.ensureConfigLoaded();
|
|
64
64
|
|
|
65
65
|
if (config && config.mcpServers) {
|
|
66
|
-
// Connect to all configured servers
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const success = await this.connectServer(serverName);
|
|
66
|
+
// Connect to all configured servers in background to avoid blocking agent initialization
|
|
67
|
+
Object.keys(config.mcpServers).forEach((serverName) => {
|
|
68
|
+
logger?.debug(`Connecting to MCP server: ${serverName}`);
|
|
69
|
+
this.connectServer(serverName)
|
|
70
|
+
.then((success) => {
|
|
72
71
|
if (success) {
|
|
73
72
|
logger?.debug(
|
|
74
73
|
`Successfully connected to MCP server: ${serverName}`,
|
|
@@ -76,18 +75,18 @@ export class McpManager {
|
|
|
76
75
|
} else {
|
|
77
76
|
logger?.warn(`Failed to connect to MCP server: ${serverName}`);
|
|
78
77
|
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
78
|
+
})
|
|
79
|
+
.catch((error) => {
|
|
80
|
+
logger?.error(
|
|
81
|
+
`Background connection to MCP server ${serverName} failed:`,
|
|
82
|
+
error,
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
logger?.debug("MCP servers initialization
|
|
90
|
-
// Trigger state change callback after initialization
|
|
88
|
+
logger?.debug("MCP servers initialization started in background");
|
|
89
|
+
// Trigger state change callback after starting initialization
|
|
91
90
|
this.callbacks.onServersChange?.(this.getAllServers());
|
|
92
91
|
}
|
|
93
92
|
}
|
|
@@ -228,10 +227,35 @@ export class McpManager {
|
|
|
228
227
|
const transport = new StdioClientTransport({
|
|
229
228
|
command: server.config.command,
|
|
230
229
|
args: server.config.args || [],
|
|
231
|
-
env:
|
|
230
|
+
env: {
|
|
231
|
+
...(process.env as Record<string, string>),
|
|
232
|
+
...(server.config.env || {}),
|
|
233
|
+
},
|
|
232
234
|
cwd: this.workdir, // Use the agent's workdir as the process working directory
|
|
235
|
+
stderr: "pipe", // Pipe stderr to capture it
|
|
233
236
|
});
|
|
234
237
|
|
|
238
|
+
// Handle stderr output
|
|
239
|
+
const stderr = transport.stderr;
|
|
240
|
+
if (stderr) {
|
|
241
|
+
let buffer = "";
|
|
242
|
+
stderr.on("data", (chunk: Buffer) => {
|
|
243
|
+
buffer += chunk.toString();
|
|
244
|
+
const lines = buffer.split("\n");
|
|
245
|
+
buffer = lines.pop() || "";
|
|
246
|
+
for (const line of lines) {
|
|
247
|
+
if (line.trim()) {
|
|
248
|
+
logger?.error(`[MCP Server ${name}] ${line}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
stderr.on("end", () => {
|
|
253
|
+
if (buffer.trim()) {
|
|
254
|
+
logger?.error(`[MCP Server ${name}] ${buffer}`);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
235
259
|
// Create client
|
|
236
260
|
const client = new Client(
|
|
237
261
|
{
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
stripEnvVars,
|
|
22
22
|
stripRedirections,
|
|
23
23
|
hasWriteRedirections,
|
|
24
|
+
isBashHeredocWrite,
|
|
24
25
|
getSmartPrefix,
|
|
25
26
|
DANGEROUS_COMMANDS,
|
|
26
27
|
} from "../utils/bashParser.js";
|
|
@@ -46,6 +47,7 @@ const SAFE_COMMANDS = [
|
|
|
46
47
|
"head",
|
|
47
48
|
"tail",
|
|
48
49
|
"wc",
|
|
50
|
+
"sleep",
|
|
49
51
|
];
|
|
50
52
|
|
|
51
53
|
const DEFAULT_ALLOWED_RULES = [
|
|
@@ -87,6 +89,7 @@ const DEFAULT_ALLOWED_RULES = [
|
|
|
87
89
|
"Bash(head*)",
|
|
88
90
|
"Bash(tail*)",
|
|
89
91
|
"Bash(wc*)",
|
|
92
|
+
"Bash(sleep*)",
|
|
90
93
|
];
|
|
91
94
|
|
|
92
95
|
import { logger } from "../utils/globalLogger.js";
|
|
@@ -367,6 +370,37 @@ export class PermissionManager {
|
|
|
367
370
|
async checkPermission(
|
|
368
371
|
context: ToolPermissionContext,
|
|
369
372
|
): Promise<PermissionDecision> {
|
|
373
|
+
// 0. Intercept Bash EOF writing operations
|
|
374
|
+
if (context.toolName === BASH_TOOL_NAME && context.toolInput?.command) {
|
|
375
|
+
const command = String(context.toolInput.command);
|
|
376
|
+
if (isBashHeredocWrite(command)) {
|
|
377
|
+
// Check if this specific command is explicitly allowed by a rule that includes redirection
|
|
378
|
+
const isExplicitlyAllowed = [
|
|
379
|
+
...this.instanceAllowedRules,
|
|
380
|
+
...this.temporaryRules,
|
|
381
|
+
...this.allowedRules,
|
|
382
|
+
...DEFAULT_ALLOWED_RULES,
|
|
383
|
+
].some((rule) => {
|
|
384
|
+
if (rule.startsWith("Bash(") && rule.endsWith(")")) {
|
|
385
|
+
const pattern = rule.substring(5, rule.length - 1);
|
|
386
|
+
// If the pattern itself has write redirections, we check if it matches
|
|
387
|
+
if (hasWriteRedirections(pattern)) {
|
|
388
|
+
return this.matchesRule(context, rule);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return false;
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
if (!isExplicitlyAllowed) {
|
|
395
|
+
return {
|
|
396
|
+
behavior: "deny",
|
|
397
|
+
message:
|
|
398
|
+
"Bash-based file writing operations using heredocs (e.g., 'cat <<EOF > file') are not allowed. Please use the dedicated 'Write' or 'Edit' tools instead for file modifications.",
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
370
404
|
// 0. Check instance-specific denied rules first - Deny always takes precedence
|
|
371
405
|
for (const rule of this.instanceDeniedRules) {
|
|
372
406
|
if (this.matchesRule(context, rule)) {
|
|
@@ -816,7 +850,8 @@ export class PermissionManager {
|
|
|
816
850
|
cmd === "cat" ||
|
|
817
851
|
cmd === "head" ||
|
|
818
852
|
cmd === "tail" ||
|
|
819
|
-
cmd === "wc"
|
|
853
|
+
cmd === "wc" ||
|
|
854
|
+
cmd === "sleep"
|
|
820
855
|
) {
|
|
821
856
|
return true;
|
|
822
857
|
}
|
|
@@ -929,7 +964,8 @@ export class PermissionManager {
|
|
|
929
964
|
cmd === "cat" ||
|
|
930
965
|
cmd === "head" ||
|
|
931
966
|
cmd === "tail" ||
|
|
932
|
-
cmd === "wc"
|
|
967
|
+
cmd === "wc" ||
|
|
968
|
+
cmd === "sleep"
|
|
933
969
|
) {
|
|
934
970
|
isSafe = true;
|
|
935
971
|
} else {
|
package/src/utils/bashParser.ts
CHANGED
|
@@ -58,7 +58,8 @@ export function splitBashCommand(command: string): string[] {
|
|
|
58
58
|
else if (char === "|" && nextChar === "&") opLen = 2;
|
|
59
59
|
else if (char === ";") opLen = 1;
|
|
60
60
|
else if (char === "|") opLen = 1;
|
|
61
|
-
else if (char === "&" && nextChar !== ">"
|
|
61
|
+
else if (char === "&" && nextChar !== ">" && command[i - 1] !== ">")
|
|
62
|
+
opLen = 1;
|
|
62
63
|
|
|
63
64
|
if (opLen > 0) {
|
|
64
65
|
// Check if preceded by an odd number of backslashes
|
|
@@ -381,6 +382,12 @@ export function hasWriteRedirections(command: string): boolean {
|
|
|
381
382
|
continue;
|
|
382
383
|
}
|
|
383
384
|
|
|
385
|
+
// Ignore file descriptor redirections like 2>&1, >&2, etc.
|
|
386
|
+
if (target.startsWith("&") && /^\d+$/.test(target.substring(1))) {
|
|
387
|
+
i = k - 1;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
384
391
|
return true;
|
|
385
392
|
}
|
|
386
393
|
}
|
|
@@ -388,6 +395,56 @@ export function hasWriteRedirections(command: string): boolean {
|
|
|
388
395
|
return false;
|
|
389
396
|
}
|
|
390
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Checks if a bash command contains any heredocs (<<, <<-).
|
|
400
|
+
*/
|
|
401
|
+
export function hasHeredoc(command: string): boolean {
|
|
402
|
+
let inSingleQuote = false;
|
|
403
|
+
let inDoubleQuote = false;
|
|
404
|
+
let escaped = false;
|
|
405
|
+
|
|
406
|
+
for (let i = 0; i < command.length; i++) {
|
|
407
|
+
const char = command[i];
|
|
408
|
+
|
|
409
|
+
if (escaped) {
|
|
410
|
+
escaped = false;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (char === "\\") {
|
|
415
|
+
escaped = true;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (char === "'" && !inDoubleQuote) {
|
|
420
|
+
inSingleQuote = !inSingleQuote;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (char === '"' && !inSingleQuote) {
|
|
425
|
+
inDoubleQuote = !inDoubleQuote;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (char === "<" && command[i + 1] === "<") {
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Checks if a bash command is a heredoc write operation (e.g., cat <<EOF > file).
|
|
443
|
+
*/
|
|
444
|
+
export function isBashHeredocWrite(command: string): boolean {
|
|
445
|
+
return hasHeredoc(command) && hasWriteRedirections(command);
|
|
446
|
+
}
|
|
447
|
+
|
|
391
448
|
/**
|
|
392
449
|
* Blacklist of dangerous commands that should not be safely prefix-matched
|
|
393
450
|
* and should not have persistent permissions.
|