wave-agent-sdk 0.8.2 → 0.8.3
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/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +47 -29
- package/dist/managers/pluginManager.d.ts.map +1 -1
- package/dist/managers/pluginManager.js +28 -1
- package/dist/utils/bashParser.d.ts +4 -0
- package/dist/utils/bashParser.d.ts.map +1 -1
- package/dist/utils/bashParser.js +39 -2
- package/package.json +2 -5
- package/src/managers/permissionManager.ts +60 -37
- package/src/managers/pluginManager.ts +39 -1
- package/src/utils/bashParser.ts +50 -2
|
@@ -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, getSmartPrefix, DANGEROUS_COMMANDS, } from "../utils/bashParser.js";
|
|
11
|
+
import { splitBashCommand, stripEnvVars, stripRedirections, hasWriteRedirections, 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, LS_TOOL_NAME, } from "../constants/tools.js";
|
|
14
14
|
const SAFE_COMMANDS = ["cd", "ls", "pwd", "true", "false"];
|
|
@@ -343,6 +343,9 @@ export class PermissionManager {
|
|
|
343
343
|
const workdir = toolInput.workdir;
|
|
344
344
|
const parts = splitBashCommand(command);
|
|
345
345
|
const isDangerous = parts.some((part) => {
|
|
346
|
+
if (hasWriteRedirections(part)) {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
346
349
|
const processedPart = stripRedirections(stripEnvVars(part));
|
|
347
350
|
const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
|
|
348
351
|
if (commandMatch) {
|
|
@@ -390,7 +393,15 @@ export class PermissionManager {
|
|
|
390
393
|
// Handle Bash command rules
|
|
391
394
|
if (toolName === BASH_TOOL_NAME) {
|
|
392
395
|
const command = String(context.toolInput?.command || "");
|
|
393
|
-
const
|
|
396
|
+
const hasWriteInPattern = hasWriteRedirections(pattern);
|
|
397
|
+
const hasWriteInCommand = hasWriteRedirections(command);
|
|
398
|
+
// If the command has write redirections, it must match a pattern that also has write redirections
|
|
399
|
+
if (hasWriteInCommand && !hasWriteInPattern) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
const processedPart = hasWriteInPattern
|
|
403
|
+
? stripEnvVars(command)
|
|
404
|
+
: stripRedirections(stripEnvVars(command));
|
|
394
405
|
// For Bash commands, we want '*' to match everything including slashes and spaces
|
|
395
406
|
// minimatch's default behavior for '*' is to not match across directory separators
|
|
396
407
|
// We use a regex to replace '*' with '.*' (match anything)
|
|
@@ -421,7 +432,7 @@ export class PermissionManager {
|
|
|
421
432
|
* Check if a tool call is allowed by persistent or temporary rules
|
|
422
433
|
*/
|
|
423
434
|
isAllowedByRule(context) {
|
|
424
|
-
const isAllowedByRuleList = (ctx, rules) => {
|
|
435
|
+
const isAllowedByRuleList = (ctx, rules, isDefaultRules = false) => {
|
|
425
436
|
if (ctx.toolName === BASH_TOOL_NAME && ctx.toolInput?.command) {
|
|
426
437
|
const command = String(ctx.toolInput.command);
|
|
427
438
|
const parts = splitBashCommand(command);
|
|
@@ -429,40 +440,46 @@ export class PermissionManager {
|
|
|
429
440
|
return false;
|
|
430
441
|
const workdir = ctx.toolInput?.workdir;
|
|
431
442
|
return parts.every((part) => {
|
|
443
|
+
const hasWrite = hasWriteRedirections(part);
|
|
432
444
|
const processedPart = stripRedirections(stripEnvVars(part));
|
|
433
445
|
// Check for safe commands
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (cmd
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
if (workdir) {
|
|
443
|
-
// For cd and ls, check paths
|
|
444
|
-
const pathArgs = (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter((arg) => !arg.startsWith("-")) || [];
|
|
445
|
-
if (pathArgs.length === 0) {
|
|
446
|
-
// cd or ls without arguments operates on current dir (workdir)
|
|
446
|
+
if (!hasWrite) {
|
|
447
|
+
const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
|
|
448
|
+
if (commandMatch) {
|
|
449
|
+
const cmd = commandMatch[1];
|
|
450
|
+
const args = commandMatch[2]?.trim() || "";
|
|
451
|
+
if (SAFE_COMMANDS.includes(cmd)) {
|
|
452
|
+
if (cmd === "pwd" || cmd === "true" || cmd === "false") {
|
|
447
453
|
return true;
|
|
448
454
|
}
|
|
449
|
-
|
|
450
|
-
//
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
455
|
+
if (workdir) {
|
|
456
|
+
// For cd and ls, check paths
|
|
457
|
+
const pathArgs = (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter((arg) => !arg.startsWith("-")) || [];
|
|
458
|
+
if (pathArgs.length === 0) {
|
|
459
|
+
// cd or ls without arguments operates on current dir (workdir)
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
const allPathsSafe = pathArgs.every((pathArg) => {
|
|
463
|
+
// Remove quotes if present
|
|
464
|
+
const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
|
|
465
|
+
const { isInside } = this.isInsideSafeZone(cleanPath, workdir);
|
|
466
|
+
return isInside;
|
|
467
|
+
});
|
|
468
|
+
if (allPathsSafe) {
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
457
471
|
}
|
|
458
472
|
}
|
|
459
473
|
}
|
|
460
474
|
}
|
|
461
475
|
// Check if this specific part is allowed by any rule
|
|
476
|
+
if (hasWrite && isDefaultRules) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
462
479
|
// We create a temporary context with just this part of the command
|
|
463
480
|
const partContext = {
|
|
464
481
|
...ctx,
|
|
465
|
-
toolInput: { ...ctx.toolInput, command:
|
|
482
|
+
toolInput: { ...ctx.toolInput, command: part },
|
|
466
483
|
};
|
|
467
484
|
const allowedByRule = rules.some((rule) => {
|
|
468
485
|
return this.matchesRule(partContext, rule);
|
|
@@ -484,7 +501,7 @@ export class PermissionManager {
|
|
|
484
501
|
return true;
|
|
485
502
|
}
|
|
486
503
|
// Check default allowed rules
|
|
487
|
-
return isAllowedByRuleList(context, DEFAULT_ALLOWED_RULES);
|
|
504
|
+
return isAllowedByRuleList(context, DEFAULT_ALLOWED_RULES, true);
|
|
488
505
|
}
|
|
489
506
|
/**
|
|
490
507
|
* Expand a bash command into individual permission rules, filtering out safe commands.
|
|
@@ -498,11 +515,12 @@ export class PermissionManager {
|
|
|
498
515
|
const parts = splitBashCommand(command);
|
|
499
516
|
const rules = [];
|
|
500
517
|
for (const part of parts) {
|
|
518
|
+
const hasWrite = hasWriteRedirections(part);
|
|
501
519
|
const processedPart = stripRedirections(stripEnvVars(part));
|
|
502
520
|
// Check for safe commands
|
|
503
521
|
const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
|
|
504
522
|
let isSafe = false;
|
|
505
|
-
if (commandMatch) {
|
|
523
|
+
if (commandMatch && !hasWrite) {
|
|
506
524
|
const cmd = commandMatch[1];
|
|
507
525
|
const args = commandMatch[2]?.trim() || "";
|
|
508
526
|
if (SAFE_COMMANDS.includes(cmd)) {
|
|
@@ -549,12 +567,12 @@ export class PermissionManager {
|
|
|
549
567
|
}
|
|
550
568
|
}
|
|
551
569
|
}
|
|
552
|
-
const smartPrefix = getSmartPrefix(processedPart);
|
|
570
|
+
const smartPrefix = hasWrite ? null : getSmartPrefix(processedPart);
|
|
553
571
|
if (smartPrefix) {
|
|
554
572
|
rules.push(`Bash(${smartPrefix}*)`);
|
|
555
573
|
}
|
|
556
574
|
else {
|
|
557
|
-
rules.push(`Bash(${processedPart})`);
|
|
575
|
+
rules.push(`Bash(${hasWrite ? stripEnvVars(part) : processedPart})`);
|
|
558
576
|
}
|
|
559
577
|
}
|
|
560
578
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pluginManager.d.ts","sourceRoot":"","sources":["../../src/managers/pluginManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAUzD,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED,qBAAa,aAAa;IAMtB,OAAO,CAAC,SAAS;IALnB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAA0B;gBAGtC,SAAS,EAAE,SAAS,EAC5B,OAAO,EAAE,oBAAoB;IAM/B,OAAO,KAAK,YAAY,GAEvB;IAED,OAAO,KAAK,WAAW,GAEtB;IAED,OAAO,KAAK,UAAU,GAErB;IAED,OAAO,KAAK,UAAU,GAErB;IAED,OAAO,KAAK,mBAAmB,GAE9B;IAED,OAAO,KAAK,oBAAoB,GAE/B;IAED;;OAEG;IACH,oBAAoB,CAAC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAInE;;OAEG;YACW,oBAAoB;
|
|
1
|
+
{"version":3,"file":"pluginManager.d.ts","sourceRoot":"","sources":["../../src/managers/pluginManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAUzD,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED,qBAAa,aAAa;IAMtB,OAAO,CAAC,SAAS;IALnB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAA0B;gBAGtC,SAAS,EAAE,SAAS,EAC5B,OAAO,EAAE,oBAAoB;IAM/B,OAAO,KAAK,YAAY,GAEvB;IAED,OAAO,KAAK,WAAW,GAEtB;IAED,OAAO,KAAK,UAAU,GAErB;IAED,OAAO,KAAK,UAAU,GAErB;IAED,OAAO,KAAK,mBAAmB,GAE9B;IAED,OAAO,KAAK,oBAAoB,GAE/B;IAED;;OAEG;IACH,oBAAoB,CAAC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAInE;;OAEG;YACW,oBAAoB;IA+DlC;;OAEG;YACW,gBAAgB;IAyD9B;;;OAGG;IACG,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBzD;;OAEG;IACH,UAAU,IAAI,MAAM,EAAE;IAItB;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAG5C"}
|
|
@@ -43,7 +43,34 @@ export class PluginManager {
|
|
|
43
43
|
this.enabledPlugins = this.configurationService.getMergedEnabledPlugins(this.workdir);
|
|
44
44
|
}
|
|
45
45
|
const marketplaceService = new MarketplaceService();
|
|
46
|
-
|
|
46
|
+
let installedRegistry = await marketplaceService.getInstalledPlugins();
|
|
47
|
+
const knownMarketplaces = await marketplaceService.listMarketplaces();
|
|
48
|
+
// Identify missing enabled plugins and auto-install them if marketplace is known
|
|
49
|
+
for (const pluginId of Object.keys(this.enabledPlugins)) {
|
|
50
|
+
if (this.enabledPlugins[pluginId] !== true)
|
|
51
|
+
continue;
|
|
52
|
+
const [name, marketplaceName] = pluginId.split("@");
|
|
53
|
+
if (!name || !marketplaceName)
|
|
54
|
+
continue;
|
|
55
|
+
const isInstalled = installedRegistry.plugins.some((p) => p.name === name && p.marketplace === marketplaceName);
|
|
56
|
+
if (!isInstalled) {
|
|
57
|
+
const isMarketplaceKnown = knownMarketplaces.some((m) => m.name === marketplaceName);
|
|
58
|
+
if (isMarketplaceKnown) {
|
|
59
|
+
logger?.info(`Auto-installing missing plugin: ${pluginId}`);
|
|
60
|
+
try {
|
|
61
|
+
await marketplaceService.installPlugin(pluginId);
|
|
62
|
+
}
|
|
63
|
+
catch (installError) {
|
|
64
|
+
logger?.error(`Failed to auto-install plugin ${pluginId}:`, installError);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
logger?.warn(`Plugin ${pluginId} is enabled but marketplace ${marketplaceName} is unknown. Skipping auto-install.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Refresh registry after potential auto-installs
|
|
73
|
+
installedRegistry = await marketplaceService.getInstalledPlugins();
|
|
47
74
|
for (const p of installedRegistry.plugins) {
|
|
48
75
|
const pluginId = `${p.name}@${p.marketplace}`;
|
|
49
76
|
if (this.enabledPlugins[pluginId] !== true) {
|
|
@@ -11,6 +11,10 @@ export declare function stripEnvVars(command: string): string;
|
|
|
11
11
|
* Removes redirections (e.g., echo "data" > output.txt -> echo "data").
|
|
12
12
|
*/
|
|
13
13
|
export declare function stripRedirections(command: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Checks if a bash command contains any write redirections (>, >>, &>, 2>, >|).
|
|
16
|
+
*/
|
|
17
|
+
export declare function hasWriteRedirections(command: string): boolean;
|
|
14
18
|
/**
|
|
15
19
|
* Blacklist of dangerous commands that should not be safely prefix-matched
|
|
16
20
|
* 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,CAmH1D;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,CAsC7D;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,UAa9B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuL7D"}
|
package/dist/utils/bashParser.js
CHANGED
|
@@ -98,8 +98,11 @@ export function splitBashCommand(command) {
|
|
|
98
98
|
parts.push(lastPart);
|
|
99
99
|
const finalResult = [];
|
|
100
100
|
for (const part of parts) {
|
|
101
|
-
const
|
|
102
|
-
|
|
101
|
+
const envStripped = stripEnvVars(part);
|
|
102
|
+
const stripped = stripRedirections(envStripped);
|
|
103
|
+
if (stripped.startsWith("(") &&
|
|
104
|
+
stripped.endsWith(")") &&
|
|
105
|
+
stripped === envStripped) {
|
|
103
106
|
const inner = stripped.substring(1, stripped.length - 1).trim();
|
|
104
107
|
if (inner) {
|
|
105
108
|
finalResult.push(...splitBashCommand(inner));
|
|
@@ -268,6 +271,40 @@ export function stripRedirections(command) {
|
|
|
268
271
|
}
|
|
269
272
|
return result.trim();
|
|
270
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Checks if a bash command contains any write redirections (>, >>, &>, 2>, >|).
|
|
276
|
+
*/
|
|
277
|
+
export function hasWriteRedirections(command) {
|
|
278
|
+
let inSingleQuote = false;
|
|
279
|
+
let inDoubleQuote = false;
|
|
280
|
+
let escaped = false;
|
|
281
|
+
for (let i = 0; i < command.length; i++) {
|
|
282
|
+
const char = command[i];
|
|
283
|
+
if (escaped) {
|
|
284
|
+
escaped = false;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (char === "\\") {
|
|
288
|
+
escaped = true;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (char === "'" && !inDoubleQuote) {
|
|
292
|
+
inSingleQuote = !inSingleQuote;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (char === '"' && !inSingleQuote) {
|
|
296
|
+
inDoubleQuote = !inDoubleQuote;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (char === ">") {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
271
308
|
/**
|
|
272
309
|
* Blacklist of dangerous commands that should not be safely prefix-matched
|
|
273
310
|
* and should not have persistent permissions.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-agent-sdk",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "SDK for building AI-powered development tools and agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -49,9 +49,6 @@
|
|
|
49
49
|
"test": "vitest run --reporter=dot",
|
|
50
50
|
"test:coverage": "vitest run --coverage --reporter=dot",
|
|
51
51
|
"lint": "eslint --cache",
|
|
52
|
-
"format": "prettier --write ."
|
|
53
|
-
"version:patch": "node ../../scripts/version.js patch",
|
|
54
|
-
"version:minor": "node ../../scripts/version.js minor",
|
|
55
|
-
"version:major": "node ../../scripts/version.js major"
|
|
52
|
+
"format": "prettier --write ."
|
|
56
53
|
}
|
|
57
54
|
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
splitBashCommand,
|
|
21
21
|
stripEnvVars,
|
|
22
22
|
stripRedirections,
|
|
23
|
+
hasWriteRedirections,
|
|
23
24
|
getSmartPrefix,
|
|
24
25
|
DANGEROUS_COMMANDS,
|
|
25
26
|
} from "../utils/bashParser.js";
|
|
@@ -464,6 +465,9 @@ export class PermissionManager {
|
|
|
464
465
|
const parts = splitBashCommand(command);
|
|
465
466
|
|
|
466
467
|
const isDangerous = parts.some((part) => {
|
|
468
|
+
if (hasWriteRedirections(part)) {
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
467
471
|
const processedPart = stripRedirections(stripEnvVars(part));
|
|
468
472
|
const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
|
|
469
473
|
if (commandMatch) {
|
|
@@ -523,7 +527,17 @@ export class PermissionManager {
|
|
|
523
527
|
// Handle Bash command rules
|
|
524
528
|
if (toolName === BASH_TOOL_NAME) {
|
|
525
529
|
const command = String(context.toolInput?.command || "");
|
|
526
|
-
const
|
|
530
|
+
const hasWriteInPattern = hasWriteRedirections(pattern);
|
|
531
|
+
const hasWriteInCommand = hasWriteRedirections(command);
|
|
532
|
+
|
|
533
|
+
// If the command has write redirections, it must match a pattern that also has write redirections
|
|
534
|
+
if (hasWriteInCommand && !hasWriteInPattern) {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const processedPart = hasWriteInPattern
|
|
539
|
+
? stripEnvVars(command)
|
|
540
|
+
: stripRedirections(stripEnvVars(command));
|
|
527
541
|
// For Bash commands, we want '*' to match everything including slashes and spaces
|
|
528
542
|
// minimatch's default behavior for '*' is to not match across directory separators
|
|
529
543
|
// We use a regex to replace '*' with '.*' (match anything)
|
|
@@ -561,6 +575,7 @@ export class PermissionManager {
|
|
|
561
575
|
const isAllowedByRuleList = (
|
|
562
576
|
ctx: ToolPermissionContext,
|
|
563
577
|
rules: string[],
|
|
578
|
+
isDefaultRules: boolean = false,
|
|
564
579
|
) => {
|
|
565
580
|
if (ctx.toolName === BASH_TOOL_NAME && ctx.toolInput?.command) {
|
|
566
581
|
const command = String(ctx.toolInput.command);
|
|
@@ -570,53 +585,60 @@ export class PermissionManager {
|
|
|
570
585
|
const workdir = ctx.toolInput?.workdir as string | undefined;
|
|
571
586
|
|
|
572
587
|
return parts.every((part) => {
|
|
588
|
+
const hasWrite = hasWriteRedirections(part);
|
|
573
589
|
const processedPart = stripRedirections(stripEnvVars(part));
|
|
574
590
|
|
|
575
591
|
// Check for safe commands
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (cmd
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
if (workdir) {
|
|
587
|
-
// For cd and ls, check paths
|
|
588
|
-
const pathArgs =
|
|
589
|
-
(args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter(
|
|
590
|
-
(arg) => !arg.startsWith("-"),
|
|
591
|
-
) || [];
|
|
592
|
-
|
|
593
|
-
if (pathArgs.length === 0) {
|
|
594
|
-
// cd or ls without arguments operates on current dir (workdir)
|
|
592
|
+
if (!hasWrite) {
|
|
593
|
+
const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
|
|
594
|
+
if (commandMatch) {
|
|
595
|
+
const cmd = commandMatch[1];
|
|
596
|
+
const args = commandMatch[2]?.trim() || "";
|
|
597
|
+
|
|
598
|
+
if (SAFE_COMMANDS.includes(cmd)) {
|
|
599
|
+
if (cmd === "pwd" || cmd === "true" || cmd === "false") {
|
|
595
600
|
return true;
|
|
596
601
|
}
|
|
597
602
|
|
|
598
|
-
|
|
599
|
-
//
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
603
|
+
if (workdir) {
|
|
604
|
+
// For cd and ls, check paths
|
|
605
|
+
const pathArgs =
|
|
606
|
+
(args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter(
|
|
607
|
+
(arg) => !arg.startsWith("-"),
|
|
608
|
+
) || [];
|
|
609
|
+
|
|
610
|
+
if (pathArgs.length === 0) {
|
|
611
|
+
// cd or ls without arguments operates on current dir (workdir)
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const allPathsSafe = pathArgs.every((pathArg) => {
|
|
616
|
+
// Remove quotes if present
|
|
617
|
+
const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
|
|
618
|
+
const { isInside } = this.isInsideSafeZone(
|
|
619
|
+
cleanPath,
|
|
620
|
+
workdir,
|
|
621
|
+
);
|
|
622
|
+
return isInside;
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
if (allPathsSafe) {
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
610
628
|
}
|
|
611
629
|
}
|
|
612
630
|
}
|
|
613
631
|
}
|
|
614
632
|
|
|
615
633
|
// Check if this specific part is allowed by any rule
|
|
634
|
+
if (hasWrite && isDefaultRules) {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
|
|
616
638
|
// We create a temporary context with just this part of the command
|
|
617
639
|
const partContext = {
|
|
618
640
|
...ctx,
|
|
619
|
-
toolInput: { ...ctx.toolInput, command:
|
|
641
|
+
toolInput: { ...ctx.toolInput, command: part },
|
|
620
642
|
};
|
|
621
643
|
const allowedByRule = rules.some((rule) => {
|
|
622
644
|
return this.matchesRule(partContext, rule);
|
|
@@ -643,7 +665,7 @@ export class PermissionManager {
|
|
|
643
665
|
}
|
|
644
666
|
|
|
645
667
|
// Check default allowed rules
|
|
646
|
-
return isAllowedByRuleList(context, DEFAULT_ALLOWED_RULES);
|
|
668
|
+
return isAllowedByRuleList(context, DEFAULT_ALLOWED_RULES, true);
|
|
647
669
|
}
|
|
648
670
|
|
|
649
671
|
/**
|
|
@@ -659,13 +681,14 @@ export class PermissionManager {
|
|
|
659
681
|
const rules: string[] = [];
|
|
660
682
|
|
|
661
683
|
for (const part of parts) {
|
|
684
|
+
const hasWrite = hasWriteRedirections(part);
|
|
662
685
|
const processedPart = stripRedirections(stripEnvVars(part));
|
|
663
686
|
|
|
664
687
|
// Check for safe commands
|
|
665
688
|
const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
|
|
666
689
|
let isSafe = false;
|
|
667
690
|
|
|
668
|
-
if (commandMatch) {
|
|
691
|
+
if (commandMatch && !hasWrite) {
|
|
669
692
|
const cmd = commandMatch[1];
|
|
670
693
|
const args = commandMatch[2]?.trim() || "";
|
|
671
694
|
|
|
@@ -724,11 +747,11 @@ export class PermissionManager {
|
|
|
724
747
|
}
|
|
725
748
|
}
|
|
726
749
|
|
|
727
|
-
const smartPrefix = getSmartPrefix(processedPart);
|
|
750
|
+
const smartPrefix = hasWrite ? null : getSmartPrefix(processedPart);
|
|
728
751
|
if (smartPrefix) {
|
|
729
752
|
rules.push(`Bash(${smartPrefix}*)`);
|
|
730
753
|
} else {
|
|
731
|
-
rules.push(`Bash(${processedPart})`);
|
|
754
|
+
rules.push(`Bash(${hasWrite ? stripEnvVars(part) : processedPart})`);
|
|
732
755
|
}
|
|
733
756
|
}
|
|
734
757
|
}
|
|
@@ -73,7 +73,45 @@ export class PluginManager {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const marketplaceService = new MarketplaceService();
|
|
76
|
-
|
|
76
|
+
let installedRegistry = await marketplaceService.getInstalledPlugins();
|
|
77
|
+
const knownMarketplaces = await marketplaceService.listMarketplaces();
|
|
78
|
+
|
|
79
|
+
// Identify missing enabled plugins and auto-install them if marketplace is known
|
|
80
|
+
for (const pluginId of Object.keys(this.enabledPlugins)) {
|
|
81
|
+
if (this.enabledPlugins[pluginId] !== true) continue;
|
|
82
|
+
|
|
83
|
+
const [name, marketplaceName] = pluginId.split("@");
|
|
84
|
+
if (!name || !marketplaceName) continue;
|
|
85
|
+
|
|
86
|
+
const isInstalled = installedRegistry.plugins.some(
|
|
87
|
+
(p) => p.name === name && p.marketplace === marketplaceName,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (!isInstalled) {
|
|
91
|
+
const isMarketplaceKnown = knownMarketplaces.some(
|
|
92
|
+
(m) => m.name === marketplaceName,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (isMarketplaceKnown) {
|
|
96
|
+
logger?.info(`Auto-installing missing plugin: ${pluginId}`);
|
|
97
|
+
try {
|
|
98
|
+
await marketplaceService.installPlugin(pluginId);
|
|
99
|
+
} catch (installError) {
|
|
100
|
+
logger?.error(
|
|
101
|
+
`Failed to auto-install plugin ${pluginId}:`,
|
|
102
|
+
installError,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
logger?.warn(
|
|
107
|
+
`Plugin ${pluginId} is enabled but marketplace ${marketplaceName} is unknown. Skipping auto-install.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Refresh registry after potential auto-installs
|
|
114
|
+
installedRegistry = await marketplaceService.getInstalledPlugins();
|
|
77
115
|
|
|
78
116
|
for (const p of installedRegistry.plugins) {
|
|
79
117
|
const pluginId = `${p.name}@${p.marketplace}`;
|
package/src/utils/bashParser.ts
CHANGED
|
@@ -100,8 +100,13 @@ export function splitBashCommand(command: string): string[] {
|
|
|
100
100
|
|
|
101
101
|
const finalResult: string[] = [];
|
|
102
102
|
for (const part of parts) {
|
|
103
|
-
const
|
|
104
|
-
|
|
103
|
+
const envStripped = stripEnvVars(part);
|
|
104
|
+
const stripped = stripRedirections(envStripped);
|
|
105
|
+
if (
|
|
106
|
+
stripped.startsWith("(") &&
|
|
107
|
+
stripped.endsWith(")") &&
|
|
108
|
+
stripped === envStripped
|
|
109
|
+
) {
|
|
105
110
|
const inner = stripped.substring(1, stripped.length - 1).trim();
|
|
106
111
|
if (inner) {
|
|
107
112
|
finalResult.push(...splitBashCommand(inner));
|
|
@@ -286,6 +291,49 @@ export function stripRedirections(command: string): string {
|
|
|
286
291
|
return result.trim();
|
|
287
292
|
}
|
|
288
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Checks if a bash command contains any write redirections (>, >>, &>, 2>, >|).
|
|
296
|
+
*/
|
|
297
|
+
export function hasWriteRedirections(command: string): boolean {
|
|
298
|
+
let inSingleQuote = false;
|
|
299
|
+
let inDoubleQuote = false;
|
|
300
|
+
let escaped = false;
|
|
301
|
+
|
|
302
|
+
for (let i = 0; i < command.length; i++) {
|
|
303
|
+
const char = command[i];
|
|
304
|
+
|
|
305
|
+
if (escaped) {
|
|
306
|
+
escaped = false;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (char === "\\") {
|
|
311
|
+
escaped = true;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (char === "'" && !inDoubleQuote) {
|
|
316
|
+
inSingleQuote = !inSingleQuote;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (char === '"' && !inSingleQuote) {
|
|
321
|
+
inDoubleQuote = !inDoubleQuote;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (char === ">") {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
289
337
|
/**
|
|
290
338
|
* Blacklist of dangerous commands that should not be safely prefix-matched
|
|
291
339
|
* and should not have persistent permissions.
|