@aliou/pi-guardrails 0.5.4 → 0.6.1

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,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,161 @@ import {
8
9
  Text,
9
10
  wrapTextWithAnsi,
10
11
  } from "@mariozechner/pi-tui";
11
- import type { ResolvedConfig } from "../config-schema";
12
- import { emitBlocked, emitDangerous } from "../events";
12
+ import type { DangerousPattern, ResolvedConfig } from "../config";
13
+ import { emitBlocked, emitDangerous } from "../utils/events";
14
+ import {
15
+ type CompiledPattern,
16
+ compileCommandPatterns,
17
+ } from "../utils/matching";
18
+ import { walkCommands, wordToString } from "../utils/shell-utils";
13
19
 
14
20
  /**
15
21
  * Permission gate that prompts user confirmation for dangerous commands.
16
- * Patterns, confirmation behavior, allow/deny lists are all configurable.
22
+ *
23
+ * Built-in dangerous patterns are matched structurally via AST parsing.
24
+ * User custom patterns use substring/regex matching on the raw string.
25
+ * Allowed/auto-deny patterns match against the raw command string.
17
26
  */
18
27
 
28
+ /**
29
+ * Structural matcher for a built-in dangerous command.
30
+ * Returns a description if matched, undefined otherwise.
31
+ */
32
+ type StructuralMatcher = (words: string[]) => string | undefined;
33
+
34
+ /**
35
+ * Built-in dangerous command matchers. These check the parsed command
36
+ * structure instead of regex against the raw string.
37
+ */
38
+ const BUILTIN_MATCHERS: StructuralMatcher[] = [
39
+ // rm -rf
40
+ (words) => {
41
+ if (words[0] !== "rm") return undefined;
42
+ const hasRF = words.some(
43
+ (w) =>
44
+ w === "-rf" ||
45
+ w === "-fr" ||
46
+ (w.startsWith("-") && w.includes("r") && w.includes("f")),
47
+ );
48
+ return hasRF ? "recursive force delete" : undefined;
49
+ },
50
+ // sudo
51
+ (words) => (words[0] === "sudo" ? "superuser command" : undefined),
52
+ // dd if=
53
+ (words) => {
54
+ if (words[0] !== "dd") return undefined;
55
+ return words.some((w) => w.startsWith("if="))
56
+ ? "disk write operation"
57
+ : undefined;
58
+ },
59
+ // mkfs.*
60
+ (words) => (words[0]?.startsWith("mkfs.") ? "filesystem format" : undefined),
61
+ // chmod -R 777
62
+ (words) => {
63
+ if (words[0] !== "chmod") return undefined;
64
+ return words.includes("-R") && words.includes("777")
65
+ ? "insecure recursive permissions"
66
+ : undefined;
67
+ },
68
+ // chown -R
69
+ (words) => {
70
+ if (words[0] !== "chown") return undefined;
71
+ return words.includes("-R") ? "recursive ownership change" : undefined;
72
+ },
73
+ ];
74
+
75
+ interface DangerMatch {
76
+ description: string;
77
+ pattern: string;
78
+ }
79
+
80
+ /**
81
+ * Check a parsed command against built-in structural matchers.
82
+ */
83
+ function checkBuiltinDangerous(words: string[]): DangerMatch | undefined {
84
+ if (words.length === 0) return undefined;
85
+ for (const matcher of BUILTIN_MATCHERS) {
86
+ const desc = matcher(words);
87
+ if (desc) return { description: desc, pattern: "(structural)" };
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ /**
93
+ * Check a command string against dangerous patterns.
94
+ *
95
+ * When useBuiltinMatchers is true (default patterns): tries structural AST
96
+ * matching first, falls back to substring match on parse failure.
97
+ *
98
+ * When useBuiltinMatchers is false (customPatterns replaced defaults): skips
99
+ * structural matchers entirely, uses compiled patterns (substring/regex)
100
+ * against the raw command string.
101
+ */
102
+ function findDangerousMatch(
103
+ command: string,
104
+ compiledPatterns: CompiledPattern[],
105
+ useBuiltinMatchers: boolean,
106
+ fallbackPatterns: DangerousPattern[],
107
+ ): DangerMatch | undefined {
108
+ if (useBuiltinMatchers) {
109
+ // Try structural matching first
110
+ try {
111
+ const { ast } = parse(command);
112
+ let match: DangerMatch | undefined;
113
+ walkCommands(ast, (cmd) => {
114
+ const words = (cmd.words ?? []).map(wordToString);
115
+ const result = checkBuiltinDangerous(words);
116
+ if (result) {
117
+ match = result;
118
+ return true;
119
+ }
120
+ return false;
121
+ });
122
+ if (match) return match;
123
+ } catch {
124
+ // Parse failed -- fall back to substring matching on raw string
125
+ for (const p of fallbackPatterns) {
126
+ if (command.includes(p.pattern)) {
127
+ return { description: p.description, pattern: p.pattern };
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ // Check compiled patterns (substring/regex on raw string).
134
+ // When customPatterns replaces defaults, this is the only matching path.
135
+ for (const cp of compiledPatterns) {
136
+ if (cp.test(command)) {
137
+ const src = cp.source as DangerousPattern;
138
+ return { description: src.description, pattern: src.pattern };
139
+ }
140
+ }
141
+
142
+ return undefined;
143
+ }
144
+
19
145
  export function setupPermissionGateHook(
20
146
  pi: ExtensionAPI,
21
147
  config: ResolvedConfig,
22
148
  ) {
23
149
  if (!config.features.permissionGate) return;
24
150
 
25
- // Compile patterns, skipping invalid regex
26
- const dangerousPatterns = config.permissionGate.patterns
27
- .map((p) => {
28
- try {
29
- return {
30
- pattern: new RegExp(p.pattern),
31
- description: p.description,
32
- rawPattern: p.pattern,
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
- );
151
+ // Compile all configured patterns for substring/regex matching.
152
+ // When useBuiltinMatchers is true (defaults), these act as a supplement
153
+ // to the structural matchers. When false (customPatterns), these are the
154
+ // only matching path.
155
+ const compiledPatterns = compileCommandPatterns(
156
+ config.permissionGate.patterns,
157
+ );
158
+ const { useBuiltinMatchers } = config.permissionGate;
159
+ const fallbackPatterns = config.permissionGate.patterns;
45
160
 
46
- const allowedPatterns = config.permissionGate.allowedPatterns
47
- .map((p) => {
48
- try {
49
- return new RegExp(p);
50
- } catch {
51
- console.error(
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);
161
+ const allowedPatterns = compileCommandPatterns(
162
+ config.permissionGate.allowedPatterns,
163
+ );
164
+ const autoDenyPatterns = compileCommandPatterns(
165
+ config.permissionGate.autoDenyPatterns,
166
+ );
71
167
 
72
168
  pi.on("tool_call", async (event, ctx) => {
73
169
  if (event.toolName !== "bash") return;
@@ -98,104 +194,112 @@ export function setupPermissionGateHook(
98
194
  }
99
195
  }
100
196
 
101
- // Check dangerous patterns
102
- for (const { pattern, description, rawPattern } of dangerousPatterns) {
103
- if (pattern.test(command)) {
104
- // Emit dangerous event (presenter will play sound)
105
- emitDangerous(pi, { command, description, pattern: rawPattern });
106
-
107
- if (config.permissionGate.requireConfirmation) {
108
- const proceed = await ctx.ui.custom<boolean>(
109
- (_tui, theme, _kb, done) => {
110
- const container = new Container();
111
- const redBorder = (s: string) => theme.fg("error", s);
112
-
113
- container.addChild(new DynamicBorder(redBorder));
114
- container.addChild(
115
- new Text(
116
- theme.fg("error", theme.bold("Dangerous Command Detected")),
117
- 1,
118
- 0,
119
- ),
120
- );
121
- container.addChild(new Spacer(1));
122
- container.addChild(
123
- new Text(
124
- theme.fg("warning", `This command contains ${description}:`),
125
- 1,
126
- 0,
127
- ),
128
- );
129
- container.addChild(new Spacer(1));
130
- container.addChild(
131
- new DynamicBorder((s: string) => theme.fg("muted", s)),
132
- );
133
- const commandText = new Text("", 1, 0);
134
- container.addChild(commandText);
135
- container.addChild(
136
- new DynamicBorder((s: string) => theme.fg("muted", s)),
137
- );
138
- container.addChild(new Spacer(1));
139
- container.addChild(
140
- new Text(theme.fg("text", "Allow execution?"), 1, 0),
141
- );
142
- container.addChild(new Spacer(1));
143
- container.addChild(
144
- new Text(theme.fg("dim", "y/enter: allow • n/esc: deny"), 1, 0),
145
- );
146
- container.addChild(new DynamicBorder(redBorder));
147
-
148
- return {
149
- render: (width: number) => {
150
- const wrappedCommand = wrapTextWithAnsi(
151
- theme.fg("text", command),
152
- width - 4,
153
- ).join("\n");
154
- commandText.setText(wrappedCommand);
155
- return container.render(width);
156
- },
157
- invalidate: () => container.invalidate(),
158
- handleInput: (data: string) => {
159
- if (
160
- matchesKey(data, Key.enter) ||
161
- data === "y" ||
162
- data === "Y"
163
- ) {
164
- done(true);
165
- } else if (
166
- matchesKey(data, Key.escape) ||
167
- data === "n" ||
168
- data === "N"
169
- ) {
170
- done(false);
171
- }
172
- },
173
- };
174
- },
175
- );
176
-
177
- if (!proceed) {
178
- emitBlocked(pi, {
179
- feature: "permissionGate",
180
- toolName: "bash",
181
- input: event.input,
182
- reason: "User denied dangerous command",
183
- userDenied: true,
184
- });
185
-
186
- return { block: true, reason: "User denied dangerous command" };
187
- }
188
- } else {
189
- // No confirmation required - just notify and allow
190
- ctx.ui.notify(
191
- `Dangerous command detected: ${description}`,
192
- "warning",
193
- );
194
- }
197
+ // Check dangerous patterns (structural + compiled)
198
+ const match = findDangerousMatch(
199
+ command,
200
+ compiledPatterns,
201
+ useBuiltinMatchers,
202
+ fallbackPatterns,
203
+ );
204
+ if (!match) return;
205
+
206
+ const { description, pattern: rawPattern } = match;
195
207
 
196
- break;
208
+ // Emit dangerous event (presenter will play sound)
209
+ emitDangerous(pi, { command, description, pattern: rawPattern });
210
+
211
+ if (config.permissionGate.requireConfirmation) {
212
+ // In print/RPC mode, block by default (safe fallback)
213
+ if (!ctx.hasUI) {
214
+ const reason = `Dangerous command blocked (no UI to confirm): ${description}`;
215
+ emitBlocked(pi, {
216
+ feature: "permissionGate",
217
+ toolName: "bash",
218
+ input: event.input,
219
+ reason,
220
+ });
221
+ return { block: true, reason };
197
222
  }
223
+
224
+ const proceed = await ctx.ui.custom<boolean>((_tui, theme, _kb, done) => {
225
+ const container = new Container();
226
+ const redBorder = (s: string) => theme.fg("error", s);
227
+
228
+ container.addChild(new DynamicBorder(redBorder));
229
+ container.addChild(
230
+ new Text(
231
+ theme.fg("error", theme.bold("Dangerous Command Detected")),
232
+ 1,
233
+ 0,
234
+ ),
235
+ );
236
+ container.addChild(new Spacer(1));
237
+ container.addChild(
238
+ new Text(
239
+ theme.fg("warning", `This command contains ${description}:`),
240
+ 1,
241
+ 0,
242
+ ),
243
+ );
244
+ container.addChild(new Spacer(1));
245
+ container.addChild(
246
+ new DynamicBorder((s: string) => theme.fg("muted", s)),
247
+ );
248
+ const commandText = new Text("", 1, 0);
249
+ container.addChild(commandText);
250
+ container.addChild(
251
+ new DynamicBorder((s: string) => theme.fg("muted", s)),
252
+ );
253
+ container.addChild(new Spacer(1));
254
+ container.addChild(
255
+ new Text(theme.fg("text", "Allow execution?"), 1, 0),
256
+ );
257
+ container.addChild(new Spacer(1));
258
+ container.addChild(
259
+ new Text(theme.fg("dim", "y/enter: allow • n/esc: deny"), 1, 0),
260
+ );
261
+ container.addChild(new DynamicBorder(redBorder));
262
+
263
+ return {
264
+ render: (width: number) => {
265
+ const wrappedCommand = wrapTextWithAnsi(
266
+ theme.fg("text", command),
267
+ width - 4,
268
+ ).join("\n");
269
+ commandText.setText(wrappedCommand);
270
+ return container.render(width);
271
+ },
272
+ invalidate: () => container.invalidate(),
273
+ handleInput: (data: string) => {
274
+ if (matchesKey(data, Key.enter) || data === "y" || data === "Y") {
275
+ done(true);
276
+ } else if (
277
+ matchesKey(data, Key.escape) ||
278
+ data === "n" ||
279
+ data === "N"
280
+ ) {
281
+ done(false);
282
+ }
283
+ },
284
+ };
285
+ });
286
+
287
+ if (!proceed) {
288
+ emitBlocked(pi, {
289
+ feature: "permissionGate",
290
+ toolName: "bash",
291
+ input: event.input,
292
+ reason: "User denied dangerous command",
293
+ userDenied: true,
294
+ });
295
+
296
+ return { block: true, reason: "User denied dangerous command" };
297
+ }
298
+ } else {
299
+ // No confirmation required - just notify and allow
300
+ ctx.ui.notify(`Dangerous command detected: ${description}`, "warning");
198
301
  }
302
+
199
303
  return;
200
304
  });
201
305
  }
@@ -1,14 +1,19 @@
1
1
  import { stat } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
+ import { parse } from "@aliou/sh";
3
4
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
- import type { ResolvedConfig } from "../config-schema";
5
- import { emitBlocked } from "../events";
5
+ import type { ResolvedConfig } from "../config";
6
+ import { emitBlocked } from "../utils/events";
7
+ import { expandGlob, hasGlobChars } from "../utils/glob-expander";
8
+ import { type CompiledPattern, compileFilePatterns } from "../utils/matching";
9
+ import { walkCommands, wordToString } from "../utils/shell-utils";
6
10
 
7
11
  /**
8
12
  * Prevents accessing .env files unless they match an allowed pattern.
9
13
  * Protects sensitive environment files from being accessed accidentally.
10
14
  *
11
- * Covers configurable set of tools (default: read, write, edit, bash, grep, find, ls).
15
+ * Uses AST-based parsing for bash commands to extract file references,
16
+ * with glob expansion via `fd` when args contain shell glob characters.
12
17
  */
13
18
 
14
19
  async function fileExists(filePath: string): Promise<boolean> {
@@ -20,48 +25,115 @@ async function fileExists(filePath: string): Promise<boolean> {
20
25
  }
21
26
  }
22
27
 
23
- function compilePatterns(patterns: string[]): RegExp[] {
24
- return patterns
25
- .map((p) => {
26
- try {
27
- return new RegExp(p, "i");
28
- } catch {
29
- console.error(`Invalid regex pattern in guardrails config: ${p}`);
30
- return null;
31
- }
32
- })
33
- .filter((r): r is RegExp => r !== null);
34
- }
35
-
36
28
  async function isProtectedEnvFile(
37
29
  filePath: string,
38
- config: ResolvedConfig,
30
+ protectedPatterns: CompiledPattern[],
31
+ allowedPatterns: CompiledPattern[],
32
+ dirPatterns: CompiledPattern[],
33
+ onlyBlockIfExists: boolean,
39
34
  ): Promise<boolean> {
40
- const protectedRegexes = compilePatterns(config.envFiles.protectedPatterns);
41
- const isProtected = protectedRegexes.some((r) => r.test(filePath));
35
+ const isProtected = protectedPatterns.some((p) => p.test(filePath));
42
36
  if (!isProtected) return false;
43
37
 
44
- const allowedRegexes = compilePatterns(config.envFiles.allowedPatterns);
45
- const isAllowed = allowedRegexes.some((r) => r.test(filePath));
38
+ const isAllowed = allowedPatterns.some((p) => p.test(filePath));
46
39
  if (isAllowed) return false;
47
40
 
48
41
  // Check protected directories (if any configured)
49
- if (config.envFiles.protectedDirectories.length > 0) {
50
- const dirRegexes = compilePatterns(config.envFiles.protectedDirectories);
51
- const inProtectedDir = dirRegexes.some((r) => r.test(filePath));
42
+ if (dirPatterns.length > 0) {
43
+ const inProtectedDir = dirPatterns.some((p) => p.test(filePath));
52
44
  if (inProtectedDir) {
53
- return config.envFiles.onlyBlockIfExists
54
- ? await fileExists(filePath)
55
- : true;
45
+ return onlyBlockIfExists ? await fileExists(filePath) : true;
46
+ }
47
+ }
48
+
49
+ return onlyBlockIfExists ? await fileExists(filePath) : true;
50
+ }
51
+
52
+ /**
53
+ * Extract file references from a bash command using AST parsing.
54
+ * Falls back to regex extraction on parse failure.
55
+ */
56
+ async function extractBashFileTargets(command: string): Promise<string[]> {
57
+ try {
58
+ const { ast } = parse(command);
59
+ const files: string[] = [];
60
+
61
+ walkCommands(ast, (cmd) => {
62
+ const words = (cmd.words ?? []).map(wordToString);
63
+ // Skip command name (words[0]), check args for env file references
64
+ for (let i = 1; i < words.length; i++) {
65
+ const arg = words[i] as string;
66
+ if (isEnvLikeReference(arg)) {
67
+ files.push(arg);
68
+ }
69
+ }
70
+
71
+ // Also check redirect targets
72
+ for (const redir of cmd.redirects ?? []) {
73
+ const target = wordToString(redir.target);
74
+ if (isEnvLikeReference(target)) {
75
+ files.push(target);
76
+ }
77
+ }
78
+ return false;
79
+ });
80
+
81
+ // Expand globs
82
+ const expanded: string[] = [];
83
+ for (const file of files) {
84
+ if (hasGlobChars(file)) {
85
+ const matches = await expandGlob(file);
86
+ if (matches.length > 0) {
87
+ expanded.push(...matches);
88
+ } else {
89
+ // Expansion returned nothing -- could be fd not found or no matches.
90
+ // Keep original as-is so pattern matching can still catch it.
91
+ expanded.push(file);
92
+ }
93
+ } else {
94
+ expanded.push(file);
95
+ }
56
96
  }
97
+
98
+ return expanded;
99
+ } catch {
100
+ // Fallback: regex extraction from raw string
101
+ return extractEnvFilesRegex(command);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Check if a string looks like an env file reference.
107
+ * Matches anything containing ".env" as a path component.
108
+ */
109
+ function isEnvLikeReference(arg: string): boolean {
110
+ // Must contain ".env" somewhere
111
+ if (!arg.includes(".env") && !arg.includes(".dev.vars")) return false;
112
+ // Skip flags
113
+ if (arg.startsWith("-") && !arg.startsWith("-/") && !arg.startsWith("-."))
114
+ return false;
115
+ return true;
116
+ }
117
+
118
+ /**
119
+ * Fallback regex extraction for env file references in bash commands.
120
+ */
121
+ function extractEnvFilesRegex(command: string): string[] {
122
+ const files: string[] = [];
123
+ const envFileRegex =
124
+ /(?:^|\s|[<>|;&"'`])([^\s<>|;&"'`]*\.env[^\s<>|;&"'`]*)(?:\s|$|[<>|;&"'`])/gi;
125
+
126
+ for (const match of command.matchAll(envFileRegex)) {
127
+ const file = match[1];
128
+ if (file) files.push(file);
57
129
  }
58
130
 
59
- return config.envFiles.onlyBlockIfExists ? await fileExists(filePath) : true;
131
+ return files;
60
132
  }
61
133
 
62
134
  interface ToolProtectionRule {
63
135
  tools: string[];
64
- extractTargets: (input: Record<string, unknown>) => string[];
136
+ extractTargets: (input: Record<string, unknown>) => Promise<string[]>;
65
137
  shouldBlock: (target: string) => Promise<boolean>;
66
138
  blockMessage: (target: string) => string;
67
139
  }
@@ -72,36 +144,41 @@ export function setupProtectEnvFilesHook(
72
144
  ) {
73
145
  if (!config.features.protectEnvFiles) return;
74
146
 
147
+ const protectedPatterns = compileFilePatterns(
148
+ config.envFiles.protectedPatterns,
149
+ );
150
+ const allowedPatterns = compileFilePatterns(config.envFiles.allowedPatterns);
151
+ const dirPatterns = compileFilePatterns(config.envFiles.protectedDirectories);
152
+
153
+ const shouldBlock = (target: string) =>
154
+ isProtectedEnvFile(
155
+ target,
156
+ protectedPatterns,
157
+ allowedPatterns,
158
+ dirPatterns,
159
+ config.envFiles.onlyBlockIfExists,
160
+ );
161
+
75
162
  const protectionRules: ToolProtectionRule[] = [
76
163
  {
77
164
  tools: config.envFiles.protectedTools.filter((t) =>
78
165
  ["read", "write", "edit", "grep", "find", "ls"].includes(t),
79
166
  ),
80
- extractTargets: (input) => {
167
+ extractTargets: async (input) => {
81
168
  const path = String(input.file_path ?? input.path ?? "");
82
169
  return path ? [path] : [];
83
170
  },
84
- shouldBlock: (target) => isProtectedEnvFile(target, config),
171
+ shouldBlock,
85
172
  blockMessage: (target) =>
86
173
  config.envFiles.blockMessage.replace("{file}", target),
87
174
  },
88
175
  {
89
176
  tools: config.envFiles.protectedTools.includes("bash") ? ["bash"] : [],
90
- extractTargets: (input) => {
177
+ extractTargets: async (input) => {
91
178
  const command = String(input.command ?? "");
92
- const files: string[] = [];
93
-
94
- const envFileRegex =
95
- /(?:^|\s|[<>|;&"'`])([^\s<>|;&"'`]*\.env[^\s<>|;&"'`]*)(?:\s|$|[<>|;&"'`])/gi;
96
-
97
- for (const match of command.matchAll(envFileRegex)) {
98
- const file = match[1];
99
- if (file) files.push(file);
100
- }
101
-
102
- return files;
179
+ return extractBashFileTargets(command);
103
180
  },
104
- shouldBlock: (target) => isProtectedEnvFile(target, config),
181
+ shouldBlock,
105
182
  blockMessage: (target) =>
106
183
  `Command references protected file ${target}. ` +
107
184
  config.envFiles.blockMessage.replace("{file}", target),
@@ -120,7 +197,7 @@ export function setupProtectEnvFilesHook(
120
197
  const rule = rulesByTool.get(event.toolName);
121
198
  if (!rule) return;
122
199
 
123
- const targets = rule.extractTargets(event.input);
200
+ const targets = await rule.extractTargets(event.input);
124
201
 
125
202
  for (const target of targets) {
126
203
  if (await rule.shouldBlock(target)) {
package/index.ts CHANGED
@@ -1,16 +1,19 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { registerGuardrailsSettings } from "./commands/settings-command";
2
3
  import { configLoader } from "./config";
3
4
  import { setupGuardrailsHooks } from "./hooks";
4
- import { registerSettingsCommand } from "./settings-command";
5
5
 
6
6
  /**
7
7
  * Guardrails Extension
8
8
  *
9
9
  * Security hooks to prevent potentially dangerous operations:
10
- * - prevent-brew: Blocks Homebrew commands (project uses Nix)
11
10
  * - protect-env-files: Prevents access to .env files (except .example/.sample/.test)
12
11
  * - permission-gate: Prompts for confirmation on dangerous commands
13
12
  *
13
+ * Toolchain features (preventBrew, preventPython, enforcePackageManager,
14
+ * packageManager) have been moved to @aliou/pi-toolchain. Old configs
15
+ * containing these fields are auto-migrated on first load.
16
+ *
14
17
  * Configuration:
15
18
  * - Global: ~/.pi/agent/extensions/guardrails.json
16
19
  * - Project: .pi/extensions/guardrails.json
@@ -23,5 +26,5 @@ export default async function (pi: ExtensionAPI) {
23
26
  if (!config.enabled) return;
24
27
 
25
28
  setupGuardrailsHooks(pi, config);
26
- registerSettingsCommand(pi);
29
+ registerGuardrailsSettings(pi);
27
30
  }