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.
@@ -1 +1 @@
1
- {"version":3,"file":"permissionManager.d.ts","sourceRoot":"","sources":["../../src/managers/permissionManager.ts"],"names":[],"mappings":"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"}
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 processedPart = stripRedirections(stripEnvVars(command));
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
- const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
435
- if (commandMatch) {
436
- const cmd = commandMatch[1];
437
- const args = commandMatch[2]?.trim() || "";
438
- if (SAFE_COMMANDS.includes(cmd)) {
439
- if (cmd === "pwd" || cmd === "true" || cmd === "false") {
440
- return true;
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
- const allPathsSafe = pathArgs.every((pathArg) => {
450
- // Remove quotes if present
451
- const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
452
- const { isInside } = this.isInsideSafeZone(cleanPath, workdir);
453
- return isInside;
454
- });
455
- if (allPathsSafe) {
456
- return true;
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: processedPart },
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;IAyBlC;;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"}
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
- const installedRegistry = await marketplaceService.getInstalledPlugins();
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,CA8G1D;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;;;GAGG;AACH,eAAO,MAAM,kBAAkB,UAa9B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuL7D"}
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"}
@@ -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 stripped = stripRedirections(stripEnvVars(part));
102
- if (stripped.startsWith("(") && stripped.endsWith(")")) {
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.2",
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 processedPart = stripRedirections(stripEnvVars(command));
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
- const commandMatch = processedPart.match(/^(\w+)(\s+.*)?$/);
577
- if (commandMatch) {
578
- const cmd = commandMatch[1];
579
- const args = commandMatch[2]?.trim() || "";
580
-
581
- if (SAFE_COMMANDS.includes(cmd)) {
582
- if (cmd === "pwd" || cmd === "true" || cmd === "false") {
583
- return true;
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
- const allPathsSafe = pathArgs.every((pathArg) => {
599
- // Remove quotes if present
600
- const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
601
- const { isInside } = this.isInsideSafeZone(
602
- cleanPath,
603
- workdir,
604
- );
605
- return isInside;
606
- });
607
-
608
- if (allPathsSafe) {
609
- return true;
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: processedPart },
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
- const installedRegistry = await marketplaceService.getInstalledPlugins();
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}`;
@@ -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 stripped = stripRedirections(stripEnvVars(part));
104
- if (stripped.startsWith("(") && stripped.endsWith(")")) {
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.