@czottmann/pi-automode 1.16.0 → 1.17.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.
@@ -331,6 +331,10 @@ export function createPiAutomode(options: PiAutomodeOptions = {}) {
331
331
  };
332
332
  }
333
333
 
334
+ function blockedToolReason(reason: string): string {
335
+ return `[pi-automode] Action blocked; the tool did not run. ${reason} Do not claim success, rely on effects from this call, or attempt an equivalent workaround. Report the block to the user before continuing with dependent work. Independent work can continue.`;
336
+ }
337
+
334
338
  function block(
335
339
  ctx: ExtensionContext,
336
340
  denial: DenialRecord,
@@ -364,7 +368,10 @@ export function createPiAutomode(options: PiAutomodeOptions = {}) {
364
368
  "warning",
365
369
  );
366
370
  }
367
- return { block: true, reason: `[pi-automode] ${denial.reason}` };
371
+ return {
372
+ block: true,
373
+ reason: blockedToolReason(denial.reason),
374
+ };
368
375
  }
369
376
 
370
377
  function allow(
@@ -440,11 +447,38 @@ export function createPiAutomode(options: PiAutomodeOptions = {}) {
440
447
  // 7. classifier for every remaining action, fail-closed on setup/parse errors.
441
448
  const cfg = effectiveConfig();
442
449
  if (!cfg.enabled) return undefined;
443
- if (ctx.signal?.aborted) return { block: true, reason: "Cancelled" };
450
+
451
+ const input = event.input as Record<string, unknown>;
452
+ const summary = actionSummary(event.toolName, input);
453
+ const logCtx: LogCtx = {
454
+ logger: createLogger({
455
+ enabled: cfg.log.enabled,
456
+ classifierIo: cfg.log.classifierIo,
457
+ sessionFile: ctx.sessionManager.getSessionFile?.(),
458
+ sessionDir: ctx.sessionManager.getSessionDir?.() ?? "",
459
+ sessionCwd: ctx.cwd,
460
+ sessionId: ctx.sessionManager.getSessionId?.() ?? "unknown",
461
+ logRoot: options.logRoot,
462
+ now: now(),
463
+ }),
464
+ decisionId: newDecisionId(),
465
+ classifierModel: cfg.classifierModel,
466
+ reasoning: classifierReasoningForConfig(cfg.classifierReasoningLevel),
467
+ };
468
+
469
+ if (ctx.signal?.aborted) {
470
+ state.checkedActions += 1;
471
+ return block(ctx, {
472
+ timestamp: Date.now(),
473
+ toolName: event.toolName,
474
+ reason: "Cancelled",
475
+ action: summary,
476
+ kind: "setup",
477
+ }, logCtx);
478
+ }
444
479
 
445
480
  const isOwnedInspection = event.toolName === INSPECT_TOOL &&
446
481
  ownsInspectionTool();
447
- const input = event.input as Record<string, unknown>;
448
482
  let bashAnalysis: BashAnalysis | undefined;
449
483
  if (event.toolName === "bash") {
450
484
  const source = typeof input.command === "string" ? input.command : "";
@@ -466,23 +500,7 @@ export function createPiAutomode(options: PiAutomodeOptions = {}) {
466
500
  };
467
501
  }
468
502
  }
469
- const summary = actionSummary(event.toolName, input);
470
503
  if (!isOwnedInspection) state.checkedActions += 1;
471
- const logCtx: LogCtx = {
472
- logger: createLogger({
473
- enabled: cfg.log.enabled,
474
- classifierIo: cfg.log.classifierIo,
475
- sessionFile: ctx.sessionManager.getSessionFile?.(),
476
- sessionDir: ctx.sessionManager.getSessionDir?.() ?? "",
477
- sessionCwd: ctx.cwd,
478
- sessionId: ctx.sessionManager.getSessionId?.() ?? "unknown",
479
- logRoot: options.logRoot,
480
- now: now(),
481
- }),
482
- decisionId: newDecisionId(),
483
- classifierModel: cfg.classifierModel,
484
- reasoning: classifierReasoningForConfig(cfg.classifierReasoningLevel),
485
- };
486
504
 
487
505
  for (const pattern of cfg.permissionDeny) {
488
506
  if (
@@ -74,6 +74,16 @@ export function parseToolPattern(value: unknown): ToolPattern | undefined {
74
74
  return pattern;
75
75
  }
76
76
 
77
+ /** Return whether a parsed permission entry is malformed. */
78
+ export function isMalformedToolPattern(pattern: ToolPattern): boolean {
79
+ if (!pattern.toolName) return true;
80
+ if (pattern.argumentPattern === undefined) return false;
81
+ if (pattern.argumentPattern.trim() === "") return true;
82
+ return pattern.toolName === "bash" &&
83
+ (bashPatternAnalyses.get(pattern)?.errors.length ??
84
+ analyzeBash(pattern.argumentPattern).errors.length) > 0;
85
+ }
86
+
77
87
  function literalPrefixTable(value: string): number[] {
78
88
  const table = new Array<number>(value.length).fill(0);
79
89
  let prefixLength = 0;
@@ -491,6 +501,93 @@ function redirectListsMatch(
491
501
  });
492
502
  }
493
503
 
504
+ function containsUnquotedBracketExpression(text: string, start: number): boolean {
505
+ let index = start + 1;
506
+ if (text[index] === "!" || text[index] === "^") index += 1;
507
+ let hasMember = false;
508
+ if (text[index] === "]") {
509
+ hasMember = true;
510
+ index += 1;
511
+ }
512
+ let quote: "'" | '"' | "$'" | undefined;
513
+ for (; index < text.length; index += 1) {
514
+ const character = text[index];
515
+ if (quote) {
516
+ if (quote === "$'") {
517
+ if (character === "\\") {
518
+ if (index + 1 < text.length) {
519
+ hasMember = true;
520
+ index += 1;
521
+ }
522
+ } else if (character === "'") quote = undefined;
523
+ else hasMember = true;
524
+ } else {
525
+ if (character === quote) quote = undefined;
526
+ else if (quote === '"' && character === "\\") index += 1;
527
+ else hasMember = true;
528
+ }
529
+ continue;
530
+ }
531
+ if (character === "\\") {
532
+ if (index + 1 < text.length) {
533
+ hasMember = true;
534
+ index += 1;
535
+ }
536
+ continue;
537
+ }
538
+ if (character === "$" && text[index + 1] === "'") {
539
+ quote = "$'";
540
+ index += 1;
541
+ continue;
542
+ }
543
+ if (character === "'" || character === '"') {
544
+ quote = character;
545
+ continue;
546
+ }
547
+ if (character === "]") return hasMember;
548
+ hasMember = true;
549
+ }
550
+ return false;
551
+ }
552
+
553
+ function hasUnquotedPathnameExpansion(text: string): boolean {
554
+ let quote: "'" | '"' | "$'" | undefined;
555
+ for (let index = 0; index < text.length; index += 1) {
556
+ const character = text[index];
557
+ if (quote) {
558
+ if (quote === "$'") {
559
+ if (character === "\\") index += 1;
560
+ else if (character === "'") quote = undefined;
561
+ } else {
562
+ if (character === quote) quote = undefined;
563
+ else if (quote === '"' && character === "\\") index += 1;
564
+ }
565
+ continue;
566
+ }
567
+ if (character === "\\") {
568
+ index += 1;
569
+ continue;
570
+ }
571
+ if (character === "$" && text[index + 1] === "'") {
572
+ quote = "$'";
573
+ index += 1;
574
+ continue;
575
+ }
576
+ if (character === "'" || character === '"') {
577
+ quote = character;
578
+ continue;
579
+ }
580
+ if (character === "*" || character === "?") return true;
581
+ if (
582
+ character === "[" &&
583
+ containsUnquotedBracketExpression(text, index)
584
+ ) {
585
+ return true;
586
+ }
587
+ }
588
+ return false;
589
+ }
590
+
494
591
  function commandMatchesAllowPattern(
495
592
  patternCommand: BashCommandAnalysis,
496
593
  inputCommand: BashCommandAnalysis,
@@ -560,7 +657,10 @@ export function matchesAllowedToolPatterns(
560
657
  }
561
658
  if (
562
659
  bashAnalysis.commands.some((command) =>
563
- command.dynamicName || command.dynamicShellScript
660
+ command.dynamic ||
661
+ command.dynamicName ||
662
+ command.dynamicShellScript ||
663
+ command.argTexts.some(hasUnquotedPathnameExpansion)
564
664
  )
565
665
  ) {
566
666
  return false;
@@ -20,7 +20,7 @@ export function statusLine(
20
20
  const classifier = state.classifierAllowed > 0 || state.classifierDenied > 0
21
21
  ? ` ca:${state.classifierAllowed} cd:${state.classifierDenied}`
22
22
  : "";
23
- return `AM${circle} a:${allowed} d:${state.blockedActions}${classifier}`;
23
+ return `AM ${circle} a:${allowed} d:${state.blockedActions}${classifier}`;
24
24
  }
25
25
 
26
26
  export function statusText(
@@ -154,6 +154,10 @@ export type ClassifierIoAttempt = {
154
154
  response?: {
155
155
  stopReason?: string;
156
156
  text: string;
157
+ toolCalls?: Array<{
158
+ name: string;
159
+ arguments: Record<string, unknown>;
160
+ }>;
157
161
  model: string;
158
162
  timestamp: number;
159
163
  usage: AssistantMessage["usage"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@czottmann/pi-automode",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Claude Code-style auto mode guardrail for pi.",
5
5
  "repository": {
6
6
  "url": "https://github.com/czottmann/pi-automode"
@@ -47,9 +47,9 @@
47
47
  "typebox": "*"
48
48
  },
49
49
  "devDependencies": {
50
- "@earendil-works/pi-ai": "^0.84.1",
51
- "@earendil-works/pi-coding-agent": "^0.84.1",
52
- "@earendil-works/pi-tui": "^0.84.1",
50
+ "@earendil-works/pi-ai": "^0.86.0",
51
+ "@earendil-works/pi-coding-agent": "^0.86.0",
52
+ "@earendil-works/pi-tui": "^0.86.0",
53
53
  "@types/node": "^24.0.0",
54
54
  "tsx": "^4.22.4",
55
55
  "typebox": "^1.3.10",