@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.
- package/CHANGELOG.md +21 -1
- package/README.md +11 -26
- package/docs/GLOSSARY.md +1 -1
- package/docs/automode-classifier-flow.md +45 -23
- package/docs/configuration.md +35 -0
- package/docs/diagnostics.md +7 -1
- package/docs/observability-logging.md +2 -1
- package/docs/permission-recipes.md +243 -0
- package/extensions/auto-mode/classifier.ts +223 -163
- package/extensions/auto-mode/config.ts +16 -3
- package/extensions/auto-mode/constants.ts +4 -3
- package/extensions/auto-mode/extension.ts +37 -19
- package/extensions/auto-mode/permissions.ts +101 -1
- package/extensions/auto-mode/state.ts +1 -1
- package/extensions/auto-mode/types.ts +4 -0
- package/package.json +4 -4
|
@@ -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 {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
51
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
52
|
-
"@earendil-works/pi-tui": "^0.
|
|
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",
|