@aliou/pi-guardrails 0.5.4 → 0.6.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/README.md +63 -94
- package/config-schema.ts +37 -25
- package/config.ts +108 -141
- package/events.ts +1 -6
- package/glob-expander.ts +128 -0
- package/hooks/index.ts +0 -6
- package/hooks/permission-gate.ts +243 -142
- package/hooks/protect-env-files.ts +120 -43
- package/index.ts +6 -3
- package/matching.ts +119 -0
- package/migration.ts +135 -0
- package/package.json +6 -3
- package/pattern-editor.ts +61 -10
- package/settings-command.ts +247 -426
- package/shell-utils.ts +139 -0
- package/array-editor.ts +0 -213
- package/hooks/enforce-package-manager.ts +0 -96
- package/hooks/prevent-brew.ts +0 -41
- package/hooks/prevent-python.ts +0 -45
- package/sectioned-settings.ts +0 -345
package/glob-expander.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glob expansion using `fd` for env file protection.
|
|
3
|
+
*
|
|
4
|
+
* When a bash command contains shell globs referencing env files
|
|
5
|
+
* (e.g. `.env*`), we expand them against the filesystem to check
|
|
6
|
+
* if any expanded path matches a protected pattern.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
interface ExpandGlobOptions {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
maxDepth?: number;
|
|
15
|
+
maxResults?: number;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Expand a glob pattern using `fd`.
|
|
21
|
+
* Returns matching file paths, or empty array on failure.
|
|
22
|
+
*
|
|
23
|
+
* fd is available at `~/.pi/agent/bin/fd` (in pi's PATH).
|
|
24
|
+
*/
|
|
25
|
+
export async function expandGlob(
|
|
26
|
+
pattern: string,
|
|
27
|
+
options: ExpandGlobOptions = {},
|
|
28
|
+
): Promise<string[]> {
|
|
29
|
+
const {
|
|
30
|
+
cwd = process.cwd(),
|
|
31
|
+
maxDepth = 3,
|
|
32
|
+
maxResults = 50,
|
|
33
|
+
timeout = 2000,
|
|
34
|
+
} = options;
|
|
35
|
+
|
|
36
|
+
// Convert glob to fd-compatible regex.
|
|
37
|
+
// fd uses regex by default, so we convert glob chars.
|
|
38
|
+
const fdPattern = globToFdRegex(pattern);
|
|
39
|
+
|
|
40
|
+
return new Promise((res) => {
|
|
41
|
+
const args = [
|
|
42
|
+
"--type",
|
|
43
|
+
"f",
|
|
44
|
+
"--max-depth",
|
|
45
|
+
String(maxDepth),
|
|
46
|
+
"--max-results",
|
|
47
|
+
String(maxResults),
|
|
48
|
+
"--no-ignore",
|
|
49
|
+
"--hidden",
|
|
50
|
+
fdPattern,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const child = execFile("fd", args, { cwd, timeout }, (err, stdout) => {
|
|
54
|
+
if (err) {
|
|
55
|
+
res([]);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const files = stdout
|
|
60
|
+
.trim()
|
|
61
|
+
.split("\n")
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map((f) => resolve(cwd, f));
|
|
64
|
+
|
|
65
|
+
res(files);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Safety net: kill if timeout isn't handled by execFile
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
child.kill();
|
|
71
|
+
res([]);
|
|
72
|
+
}, timeout + 500);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convert a shell glob to an fd-compatible regex pattern.
|
|
78
|
+
* Handles `*`, `?`, and character classes `[...]`.
|
|
79
|
+
*/
|
|
80
|
+
function globToFdRegex(glob: string): string {
|
|
81
|
+
let regex = "";
|
|
82
|
+
let i = 0;
|
|
83
|
+
while (i < glob.length) {
|
|
84
|
+
const ch = glob[i] as string;
|
|
85
|
+
switch (ch) {
|
|
86
|
+
case "*":
|
|
87
|
+
regex += "[^/]*";
|
|
88
|
+
break;
|
|
89
|
+
case "?":
|
|
90
|
+
regex += "[^/]";
|
|
91
|
+
break;
|
|
92
|
+
case "[": {
|
|
93
|
+
// Pass character classes through
|
|
94
|
+
const end = glob.indexOf("]", i + 1);
|
|
95
|
+
if (end !== -1) {
|
|
96
|
+
regex += glob.slice(i, end + 1);
|
|
97
|
+
i = end;
|
|
98
|
+
} else {
|
|
99
|
+
regex += "\\[";
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case ".":
|
|
104
|
+
case "(":
|
|
105
|
+
case ")":
|
|
106
|
+
case "+":
|
|
107
|
+
case "^":
|
|
108
|
+
case "$":
|
|
109
|
+
case "{":
|
|
110
|
+
case "}":
|
|
111
|
+
case "|":
|
|
112
|
+
case "\\":
|
|
113
|
+
regex += `\\${ch}`;
|
|
114
|
+
break;
|
|
115
|
+
default:
|
|
116
|
+
regex += ch;
|
|
117
|
+
}
|
|
118
|
+
i++;
|
|
119
|
+
}
|
|
120
|
+
return `^${regex}$`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if a string contains shell glob characters.
|
|
125
|
+
*/
|
|
126
|
+
export function hasGlobChars(s: string): boolean {
|
|
127
|
+
return /[*?[\]]/.test(s);
|
|
128
|
+
}
|
package/hooks/index.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import type { ResolvedConfig } from "../config-schema";
|
|
3
|
-
import { setupEnforcePackageManagerHook } from "./enforce-package-manager";
|
|
4
3
|
import { setupPermissionGateHook } from "./permission-gate";
|
|
5
|
-
import { setupPreventBrewHook } from "./prevent-brew";
|
|
6
|
-
import { setupPreventPythonHook } from "./prevent-python";
|
|
7
4
|
import { setupProtectEnvFilesHook } from "./protect-env-files";
|
|
8
5
|
|
|
9
6
|
export function setupGuardrailsHooks(pi: ExtensionAPI, config: ResolvedConfig) {
|
|
10
|
-
setupPreventBrewHook(pi, config);
|
|
11
|
-
setupPreventPythonHook(pi, config);
|
|
12
7
|
setupProtectEnvFilesHook(pi, config);
|
|
13
8
|
setupPermissionGateHook(pi, config);
|
|
14
|
-
setupEnforcePackageManagerHook(pi, config);
|
|
15
9
|
}
|
package/hooks/permission-gate.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parse } from "@aliou/sh";
|
|
1
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
3
|
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
3
4
|
import {
|
|
@@ -8,66 +9,158 @@ import {
|
|
|
8
9
|
Text,
|
|
9
10
|
wrapTextWithAnsi,
|
|
10
11
|
} from "@mariozechner/pi-tui";
|
|
11
|
-
import type { ResolvedConfig } from "../config-schema";
|
|
12
|
+
import type { DangerousPattern, ResolvedConfig } from "../config-schema";
|
|
12
13
|
import { emitBlocked, emitDangerous } from "../events";
|
|
14
|
+
import { type CompiledPattern, compileCommandPatterns } from "../matching";
|
|
15
|
+
import { walkCommands, wordToString } from "../shell-utils";
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* Permission gate that prompts user confirmation for dangerous commands.
|
|
16
|
-
*
|
|
19
|
+
*
|
|
20
|
+
* Built-in dangerous patterns are matched structurally via AST parsing.
|
|
21
|
+
* User custom patterns use substring/regex matching on the raw string.
|
|
22
|
+
* Allowed/auto-deny patterns match against the raw command string.
|
|
17
23
|
*/
|
|
18
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Structural matcher for a built-in dangerous command.
|
|
27
|
+
* Returns a description if matched, undefined otherwise.
|
|
28
|
+
*/
|
|
29
|
+
type StructuralMatcher = (words: string[]) => string | undefined;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Built-in dangerous command matchers. These check the parsed command
|
|
33
|
+
* structure instead of regex against the raw string.
|
|
34
|
+
*/
|
|
35
|
+
const BUILTIN_MATCHERS: StructuralMatcher[] = [
|
|
36
|
+
// rm -rf
|
|
37
|
+
(words) => {
|
|
38
|
+
if (words[0] !== "rm") return undefined;
|
|
39
|
+
const hasRF = words.some(
|
|
40
|
+
(w) =>
|
|
41
|
+
w === "-rf" ||
|
|
42
|
+
w === "-fr" ||
|
|
43
|
+
(w.startsWith("-") && w.includes("r") && w.includes("f")),
|
|
44
|
+
);
|
|
45
|
+
return hasRF ? "recursive force delete" : undefined;
|
|
46
|
+
},
|
|
47
|
+
// sudo
|
|
48
|
+
(words) => (words[0] === "sudo" ? "superuser command" : undefined),
|
|
49
|
+
// dd if=
|
|
50
|
+
(words) => {
|
|
51
|
+
if (words[0] !== "dd") return undefined;
|
|
52
|
+
return words.some((w) => w.startsWith("if="))
|
|
53
|
+
? "disk write operation"
|
|
54
|
+
: undefined;
|
|
55
|
+
},
|
|
56
|
+
// mkfs.*
|
|
57
|
+
(words) => (words[0]?.startsWith("mkfs.") ? "filesystem format" : undefined),
|
|
58
|
+
// chmod -R 777
|
|
59
|
+
(words) => {
|
|
60
|
+
if (words[0] !== "chmod") return undefined;
|
|
61
|
+
return words.includes("-R") && words.includes("777")
|
|
62
|
+
? "insecure recursive permissions"
|
|
63
|
+
: undefined;
|
|
64
|
+
},
|
|
65
|
+
// chown -R
|
|
66
|
+
(words) => {
|
|
67
|
+
if (words[0] !== "chown") return undefined;
|
|
68
|
+
return words.includes("-R") ? "recursive ownership change" : undefined;
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
interface DangerMatch {
|
|
73
|
+
description: string;
|
|
74
|
+
pattern: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check a parsed command against built-in structural matchers.
|
|
79
|
+
*/
|
|
80
|
+
function checkBuiltinDangerous(words: string[]): DangerMatch | undefined {
|
|
81
|
+
if (words.length === 0) return undefined;
|
|
82
|
+
for (const matcher of BUILTIN_MATCHERS) {
|
|
83
|
+
const desc = matcher(words);
|
|
84
|
+
if (desc) return { description: desc, pattern: "(structural)" };
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check a command string against dangerous patterns.
|
|
91
|
+
*
|
|
92
|
+
* When useBuiltinMatchers is true (default patterns): tries structural AST
|
|
93
|
+
* matching first, falls back to substring match on parse failure.
|
|
94
|
+
*
|
|
95
|
+
* When useBuiltinMatchers is false (customPatterns replaced defaults): skips
|
|
96
|
+
* structural matchers entirely, uses compiled patterns (substring/regex)
|
|
97
|
+
* against the raw command string.
|
|
98
|
+
*/
|
|
99
|
+
function findDangerousMatch(
|
|
100
|
+
command: string,
|
|
101
|
+
compiledPatterns: CompiledPattern[],
|
|
102
|
+
useBuiltinMatchers: boolean,
|
|
103
|
+
fallbackPatterns: DangerousPattern[],
|
|
104
|
+
): DangerMatch | undefined {
|
|
105
|
+
if (useBuiltinMatchers) {
|
|
106
|
+
// Try structural matching first
|
|
107
|
+
try {
|
|
108
|
+
const { ast } = parse(command);
|
|
109
|
+
let match: DangerMatch | undefined;
|
|
110
|
+
walkCommands(ast, (cmd) => {
|
|
111
|
+
const words = (cmd.words ?? []).map(wordToString);
|
|
112
|
+
const result = checkBuiltinDangerous(words);
|
|
113
|
+
if (result) {
|
|
114
|
+
match = result;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
});
|
|
119
|
+
if (match) return match;
|
|
120
|
+
} catch {
|
|
121
|
+
// Parse failed -- fall back to substring matching on raw string
|
|
122
|
+
for (const p of fallbackPatterns) {
|
|
123
|
+
if (command.includes(p.pattern)) {
|
|
124
|
+
return { description: p.description, pattern: p.pattern };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check compiled patterns (substring/regex on raw string).
|
|
131
|
+
// When customPatterns replaces defaults, this is the only matching path.
|
|
132
|
+
for (const cp of compiledPatterns) {
|
|
133
|
+
if (cp.test(command)) {
|
|
134
|
+
const src = cp.source as DangerousPattern;
|
|
135
|
+
return { description: src.description, pattern: src.pattern };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
19
142
|
export function setupPermissionGateHook(
|
|
20
143
|
pi: ExtensionAPI,
|
|
21
144
|
config: ResolvedConfig,
|
|
22
145
|
) {
|
|
23
146
|
if (!config.features.permissionGate) return;
|
|
24
147
|
|
|
25
|
-
// Compile patterns
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} catch {
|
|
35
|
-
console.error(
|
|
36
|
-
`Invalid regex in guardrails permission-gate config: ${p.pattern}`,
|
|
37
|
-
);
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
})
|
|
41
|
-
.filter(
|
|
42
|
-
(p): p is { pattern: RegExp; description: string; rawPattern: string } =>
|
|
43
|
-
p !== null,
|
|
44
|
-
);
|
|
148
|
+
// Compile all configured patterns for substring/regex matching.
|
|
149
|
+
// When useBuiltinMatchers is true (defaults), these act as a supplement
|
|
150
|
+
// to the structural matchers. When false (customPatterns), these are the
|
|
151
|
+
// only matching path.
|
|
152
|
+
const compiledPatterns = compileCommandPatterns(
|
|
153
|
+
config.permissionGate.patterns,
|
|
154
|
+
);
|
|
155
|
+
const { useBuiltinMatchers } = config.permissionGate;
|
|
156
|
+
const fallbackPatterns = config.permissionGate.patterns;
|
|
45
157
|
|
|
46
|
-
const allowedPatterns =
|
|
47
|
-
.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`Invalid regex in guardrails allowedPatterns config: ${p}`,
|
|
53
|
-
);
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
.filter((r): r is RegExp => r !== null);
|
|
58
|
-
|
|
59
|
-
const autoDenyPatterns = config.permissionGate.autoDenyPatterns
|
|
60
|
-
.map((p) => {
|
|
61
|
-
try {
|
|
62
|
-
return new RegExp(p);
|
|
63
|
-
} catch {
|
|
64
|
-
console.error(
|
|
65
|
-
`Invalid regex in guardrails autoDenyPatterns config: ${p}`,
|
|
66
|
-
);
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
.filter((r): r is RegExp => r !== null);
|
|
158
|
+
const allowedPatterns = compileCommandPatterns(
|
|
159
|
+
config.permissionGate.allowedPatterns,
|
|
160
|
+
);
|
|
161
|
+
const autoDenyPatterns = compileCommandPatterns(
|
|
162
|
+
config.permissionGate.autoDenyPatterns,
|
|
163
|
+
);
|
|
71
164
|
|
|
72
165
|
pi.on("tool_call", async (event, ctx) => {
|
|
73
166
|
if (event.toolName !== "bash") return;
|
|
@@ -98,104 +191,112 @@ export function setupPermissionGateHook(
|
|
|
98
191
|
}
|
|
99
192
|
}
|
|
100
193
|
|
|
101
|
-
// Check dangerous patterns
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
// Check dangerous patterns (structural + compiled)
|
|
195
|
+
const match = findDangerousMatch(
|
|
196
|
+
command,
|
|
197
|
+
compiledPatterns,
|
|
198
|
+
useBuiltinMatchers,
|
|
199
|
+
fallbackPatterns,
|
|
200
|
+
);
|
|
201
|
+
if (!match) return;
|
|
202
|
+
|
|
203
|
+
const { description, pattern: rawPattern } = match;
|
|
204
|
+
|
|
205
|
+
// Emit dangerous event (presenter will play sound)
|
|
206
|
+
emitDangerous(pi, { command, description, pattern: rawPattern });
|
|
207
|
+
|
|
208
|
+
if (config.permissionGate.requireConfirmation) {
|
|
209
|
+
// In print/RPC mode, block by default (safe fallback)
|
|
210
|
+
if (!ctx.hasUI) {
|
|
211
|
+
const reason = `Dangerous command blocked (no UI to confirm): ${description}`;
|
|
212
|
+
emitBlocked(pi, {
|
|
213
|
+
feature: "permissionGate",
|
|
214
|
+
toolName: "bash",
|
|
215
|
+
input: event.input,
|
|
216
|
+
reason,
|
|
217
|
+
});
|
|
218
|
+
return { block: true, reason };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const proceed = await ctx.ui.custom<boolean>((_tui, theme, _kb, done) => {
|
|
222
|
+
const container = new Container();
|
|
223
|
+
const redBorder = (s: string) => theme.fg("error", s);
|
|
224
|
+
|
|
225
|
+
container.addChild(new DynamicBorder(redBorder));
|
|
226
|
+
container.addChild(
|
|
227
|
+
new Text(
|
|
228
|
+
theme.fg("error", theme.bold("Dangerous Command Detected")),
|
|
229
|
+
1,
|
|
230
|
+
0,
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
container.addChild(new Spacer(1));
|
|
234
|
+
container.addChild(
|
|
235
|
+
new Text(
|
|
236
|
+
theme.fg("warning", `This command contains ${description}:`),
|
|
237
|
+
1,
|
|
238
|
+
0,
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
container.addChild(new Spacer(1));
|
|
242
|
+
container.addChild(
|
|
243
|
+
new DynamicBorder((s: string) => theme.fg("muted", s)),
|
|
244
|
+
);
|
|
245
|
+
const commandText = new Text("", 1, 0);
|
|
246
|
+
container.addChild(commandText);
|
|
247
|
+
container.addChild(
|
|
248
|
+
new DynamicBorder((s: string) => theme.fg("muted", s)),
|
|
249
|
+
);
|
|
250
|
+
container.addChild(new Spacer(1));
|
|
251
|
+
container.addChild(
|
|
252
|
+
new Text(theme.fg("text", "Allow execution?"), 1, 0),
|
|
253
|
+
);
|
|
254
|
+
container.addChild(new Spacer(1));
|
|
255
|
+
container.addChild(
|
|
256
|
+
new Text(theme.fg("dim", "y/enter: allow • n/esc: deny"), 1, 0),
|
|
257
|
+
);
|
|
258
|
+
container.addChild(new DynamicBorder(redBorder));
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
render: (width: number) => {
|
|
262
|
+
const wrappedCommand = wrapTextWithAnsi(
|
|
263
|
+
theme.fg("text", command),
|
|
264
|
+
width - 4,
|
|
265
|
+
).join("\n");
|
|
266
|
+
commandText.setText(wrappedCommand);
|
|
267
|
+
return container.render(width);
|
|
268
|
+
},
|
|
269
|
+
invalidate: () => container.invalidate(),
|
|
270
|
+
handleInput: (data: string) => {
|
|
271
|
+
if (matchesKey(data, Key.enter) || data === "y" || data === "Y") {
|
|
272
|
+
done(true);
|
|
273
|
+
} else if (
|
|
274
|
+
matchesKey(data, Key.escape) ||
|
|
275
|
+
data === "n" ||
|
|
276
|
+
data === "N"
|
|
277
|
+
) {
|
|
278
|
+
done(false);
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (!proceed) {
|
|
285
|
+
emitBlocked(pi, {
|
|
286
|
+
feature: "permissionGate",
|
|
287
|
+
toolName: "bash",
|
|
288
|
+
input: event.input,
|
|
289
|
+
reason: "User denied dangerous command",
|
|
290
|
+
userDenied: true,
|
|
291
|
+
});
|
|
195
292
|
|
|
196
|
-
|
|
293
|
+
return { block: true, reason: "User denied dangerous command" };
|
|
197
294
|
}
|
|
295
|
+
} else {
|
|
296
|
+
// No confirmation required - just notify and allow
|
|
297
|
+
ctx.ui.notify(`Dangerous command detected: ${description}`, "warning");
|
|
198
298
|
}
|
|
299
|
+
|
|
199
300
|
return;
|
|
200
301
|
});
|
|
201
302
|
}
|