@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.
- package/README.md +63 -94
- package/commands/settings-command.ts +278 -0
- package/{pattern-editor.ts → components/pattern-editor.ts} +61 -10
- package/config.ts +185 -142
- package/hooks/index.ts +1 -7
- package/hooks/permission-gate.ts +247 -143
- package/hooks/protect-env-files.ts +122 -45
- package/index.ts +6 -3
- package/package.json +9 -3
- package/{events.ts → utils/events.ts} +1 -6
- package/utils/glob-expander.ts +128 -0
- package/utils/matching.ts +119 -0
- package/utils/migration.ts +135 -0
- package/utils/shell-utils.ts +139 -0
- package/array-editor.ts +0 -213
- package/config-schema.ts +0 -64
- 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/settings-command.ts +0 -458
package/settings-command.ts
DELETED
|
@@ -1,458 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { getSettingsListTheme } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import { Key, matchesKey } from "@mariozechner/pi-tui";
|
|
4
|
-
import { ArrayEditor } from "./array-editor";
|
|
5
|
-
import { configLoader } from "./config";
|
|
6
|
-
import type { GuardrailsConfig, ResolvedConfig } from "./config-schema";
|
|
7
|
-
import { PatternEditor } from "./pattern-editor";
|
|
8
|
-
import { SectionedSettings, type SettingsSection } from "./sectioned-settings";
|
|
9
|
-
|
|
10
|
-
type Tab = "local" | "global";
|
|
11
|
-
|
|
12
|
-
// Typed feature UI definitions. Adding a key to ResolvedConfig.features
|
|
13
|
-
// without adding it here will cause a type error.
|
|
14
|
-
type FeatureKey = keyof ResolvedConfig["features"];
|
|
15
|
-
|
|
16
|
-
interface FeatureUiDef {
|
|
17
|
-
label: string;
|
|
18
|
-
description: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const FEATURE_UI: Record<FeatureKey, FeatureUiDef> = {
|
|
22
|
-
preventBrew: {
|
|
23
|
-
label: "Prevent Homebrew",
|
|
24
|
-
description: "Block brew commands",
|
|
25
|
-
},
|
|
26
|
-
preventPython: {
|
|
27
|
-
label: "Prevent Python",
|
|
28
|
-
description: "Block python/pip/poetry commands. Use uv instead.",
|
|
29
|
-
},
|
|
30
|
-
protectEnvFiles: {
|
|
31
|
-
label: "Protect .env files",
|
|
32
|
-
description: "Block access to .env files containing secrets",
|
|
33
|
-
},
|
|
34
|
-
permissionGate: {
|
|
35
|
-
label: "Permission gate",
|
|
36
|
-
description:
|
|
37
|
-
"Prompt for confirmation on dangerous commands (rm -rf, sudo, etc.)",
|
|
38
|
-
},
|
|
39
|
-
enforcePackageManager: {
|
|
40
|
-
label: "Enforce package manager",
|
|
41
|
-
description:
|
|
42
|
-
"Enforce using a specific Node package manager (bun, pnpm, or npm)",
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export function registerSettingsCommand(pi: ExtensionAPI): void {
|
|
47
|
-
pi.registerCommand("guardrails:settings", {
|
|
48
|
-
description: "Configure guardrails (local/global)",
|
|
49
|
-
handler: async (_args, ctx) => {
|
|
50
|
-
let activeTab: Tab = configLoader.hasProjectConfig() ? "local" : "global";
|
|
51
|
-
|
|
52
|
-
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
53
|
-
let settings: SectionedSettings | null = null;
|
|
54
|
-
const settingsTheme = getSettingsListTheme();
|
|
55
|
-
|
|
56
|
-
// --- Helpers ---
|
|
57
|
-
|
|
58
|
-
function getTabConfig(): GuardrailsConfig {
|
|
59
|
-
return activeTab === "local"
|
|
60
|
-
? configLoader.getProjectConfig()
|
|
61
|
-
: configLoader.getGlobalConfig();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function saveTabConfig(
|
|
65
|
-
tab: Tab,
|
|
66
|
-
config: GuardrailsConfig,
|
|
67
|
-
): Promise<boolean> {
|
|
68
|
-
try {
|
|
69
|
-
if (tab === "local") {
|
|
70
|
-
await configLoader.saveProject(config);
|
|
71
|
-
} else {
|
|
72
|
-
await configLoader.saveGlobal(config);
|
|
73
|
-
}
|
|
74
|
-
ctx.ui.notify(`guardrails: saved to ${tab} config`, "info");
|
|
75
|
-
return true;
|
|
76
|
-
} catch (error) {
|
|
77
|
-
ctx.ui.notify(`guardrails: failed to save: ${error}`, "error");
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function setNestedValue(
|
|
83
|
-
config: GuardrailsConfig,
|
|
84
|
-
id: string,
|
|
85
|
-
value: unknown,
|
|
86
|
-
): void {
|
|
87
|
-
const parts = id.split(".");
|
|
88
|
-
// biome-ignore lint/suspicious/noExplicitAny: dynamic config path traversal
|
|
89
|
-
let target: any = config;
|
|
90
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
91
|
-
const key = parts[i] as string;
|
|
92
|
-
if (!target[key]) target[key] = {};
|
|
93
|
-
target = target[key];
|
|
94
|
-
}
|
|
95
|
-
target[parts[parts.length - 1] as string] = value;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function getNestedValue(config: GuardrailsConfig, id: string): unknown {
|
|
99
|
-
const parts = id.split(".");
|
|
100
|
-
// biome-ignore lint/suspicious/noExplicitAny: dynamic config path traversal
|
|
101
|
-
let target: any = config;
|
|
102
|
-
for (const part of parts) {
|
|
103
|
-
if (target == null) return undefined;
|
|
104
|
-
target = target[part];
|
|
105
|
-
}
|
|
106
|
-
return target;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function formatCount(id: string): string {
|
|
110
|
-
const config = getTabConfig();
|
|
111
|
-
const resolved = configLoader.getConfig();
|
|
112
|
-
const val =
|
|
113
|
-
(getNestedValue(config, id) as unknown[] | undefined) ??
|
|
114
|
-
(getNestedValue(resolved, id) as unknown[]) ??
|
|
115
|
-
[];
|
|
116
|
-
return `${val.length} items`;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// --- Submenu factories ---
|
|
120
|
-
|
|
121
|
-
function stringArraySubmenu(
|
|
122
|
-
id: string,
|
|
123
|
-
label: string,
|
|
124
|
-
): (
|
|
125
|
-
currentValue: string,
|
|
126
|
-
done: (selectedValue?: string) => void,
|
|
127
|
-
) => ArrayEditor {
|
|
128
|
-
return (_currentValue, submenuDone) => {
|
|
129
|
-
const config = getTabConfig();
|
|
130
|
-
const resolved = configLoader.getConfig();
|
|
131
|
-
const currentItems =
|
|
132
|
-
(getNestedValue(config, id) as string[] | undefined) ??
|
|
133
|
-
(getNestedValue(resolved, id) as string[]) ??
|
|
134
|
-
[];
|
|
135
|
-
|
|
136
|
-
return new ArrayEditor({
|
|
137
|
-
label,
|
|
138
|
-
items: [...currentItems],
|
|
139
|
-
theme: settingsTheme,
|
|
140
|
-
onSave: (items) => {
|
|
141
|
-
const updated: GuardrailsConfig = structuredClone(config);
|
|
142
|
-
setNestedValue(updated, id, items);
|
|
143
|
-
void saveTabConfig(activeTab, updated).then((ok) => {
|
|
144
|
-
if (ok) tui.requestRender();
|
|
145
|
-
});
|
|
146
|
-
},
|
|
147
|
-
onDone: () => {
|
|
148
|
-
submenuDone(formatCount(id));
|
|
149
|
-
settings = buildSettings(activeTab);
|
|
150
|
-
tui.requestRender();
|
|
151
|
-
},
|
|
152
|
-
});
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function patternArraySubmenu(
|
|
157
|
-
id: string,
|
|
158
|
-
label: string,
|
|
159
|
-
): (
|
|
160
|
-
currentValue: string,
|
|
161
|
-
done: (selectedValue?: string) => void,
|
|
162
|
-
) => PatternEditor {
|
|
163
|
-
return (_currentValue, submenuDone) => {
|
|
164
|
-
const config = getTabConfig();
|
|
165
|
-
const resolved = configLoader.getConfig();
|
|
166
|
-
const currentPatterns =
|
|
167
|
-
(getNestedValue(config, id) as
|
|
168
|
-
| Array<{ pattern: string; description: string }>
|
|
169
|
-
| undefined) ??
|
|
170
|
-
(getNestedValue(resolved, id) as Array<{
|
|
171
|
-
pattern: string;
|
|
172
|
-
description: string;
|
|
173
|
-
}>) ??
|
|
174
|
-
[];
|
|
175
|
-
|
|
176
|
-
return new PatternEditor({
|
|
177
|
-
label,
|
|
178
|
-
items: [...currentPatterns],
|
|
179
|
-
theme: settingsTheme,
|
|
180
|
-
onSave: (patterns) => {
|
|
181
|
-
const updated: GuardrailsConfig = structuredClone(config);
|
|
182
|
-
setNestedValue(updated, id, patterns);
|
|
183
|
-
void saveTabConfig(activeTab, updated).then((ok) => {
|
|
184
|
-
if (ok) tui.requestRender();
|
|
185
|
-
});
|
|
186
|
-
},
|
|
187
|
-
onDone: () => {
|
|
188
|
-
submenuDone(formatCount(id));
|
|
189
|
-
settings = buildSettings(activeTab);
|
|
190
|
-
tui.requestRender();
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// --- Build sections ---
|
|
197
|
-
|
|
198
|
-
function buildSettings(tab: Tab): SectionedSettings {
|
|
199
|
-
const config =
|
|
200
|
-
tab === "local"
|
|
201
|
-
? configLoader.getProjectConfig()
|
|
202
|
-
: configLoader.getGlobalConfig();
|
|
203
|
-
const resolved = configLoader.getConfig();
|
|
204
|
-
|
|
205
|
-
const featureItems = (Object.keys(FEATURE_UI) as FeatureKey[]).map(
|
|
206
|
-
(key) => {
|
|
207
|
-
const def = FEATURE_UI[key];
|
|
208
|
-
return {
|
|
209
|
-
id: `features.${key}`,
|
|
210
|
-
label: def.label,
|
|
211
|
-
description: def.description,
|
|
212
|
-
currentValue:
|
|
213
|
-
(config.features?.[key] ?? resolved.features[key])
|
|
214
|
-
? "enabled"
|
|
215
|
-
: "disabled",
|
|
216
|
-
values: ["enabled", "disabled"],
|
|
217
|
-
};
|
|
218
|
-
},
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
const sections: SettingsSection[] = [
|
|
222
|
-
{
|
|
223
|
-
label: "Features",
|
|
224
|
-
items: featureItems,
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
label: "Env Files",
|
|
228
|
-
items: [
|
|
229
|
-
{
|
|
230
|
-
id: "envFiles.onlyBlockIfExists",
|
|
231
|
-
label: "Only block existing files",
|
|
232
|
-
description:
|
|
233
|
-
"Only block .env file access if the file exists on disk",
|
|
234
|
-
currentValue:
|
|
235
|
-
(config.envFiles?.onlyBlockIfExists ??
|
|
236
|
-
resolved.envFiles.onlyBlockIfExists)
|
|
237
|
-
? "on"
|
|
238
|
-
: "off",
|
|
239
|
-
values: ["on", "off"],
|
|
240
|
-
},
|
|
241
|
-
{
|
|
242
|
-
id: "envFiles.protectedPatterns",
|
|
243
|
-
label: "Protected patterns",
|
|
244
|
-
description:
|
|
245
|
-
"Regex patterns for files to protect (e.g. \\.env$)",
|
|
246
|
-
currentValue: formatCount("envFiles.protectedPatterns"),
|
|
247
|
-
submenu: stringArraySubmenu(
|
|
248
|
-
"envFiles.protectedPatterns",
|
|
249
|
-
"Protected Patterns",
|
|
250
|
-
),
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
id: "envFiles.allowedPatterns",
|
|
254
|
-
label: "Allowed patterns",
|
|
255
|
-
description:
|
|
256
|
-
"Regex patterns for exceptions (e.g. \\.env\\.example$)",
|
|
257
|
-
currentValue: formatCount("envFiles.allowedPatterns"),
|
|
258
|
-
submenu: stringArraySubmenu(
|
|
259
|
-
"envFiles.allowedPatterns",
|
|
260
|
-
"Allowed Patterns",
|
|
261
|
-
),
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
id: "envFiles.protectedDirectories",
|
|
265
|
-
label: "Protected directories",
|
|
266
|
-
description: "Regex patterns for directories to protect",
|
|
267
|
-
currentValue: formatCount("envFiles.protectedDirectories"),
|
|
268
|
-
submenu: stringArraySubmenu(
|
|
269
|
-
"envFiles.protectedDirectories",
|
|
270
|
-
"Protected Directories",
|
|
271
|
-
),
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
id: "envFiles.protectedTools",
|
|
275
|
-
label: "Protected tools",
|
|
276
|
-
description:
|
|
277
|
-
"Tools to intercept (read, write, edit, bash, grep, find, ls)",
|
|
278
|
-
currentValue: formatCount("envFiles.protectedTools"),
|
|
279
|
-
submenu: stringArraySubmenu(
|
|
280
|
-
"envFiles.protectedTools",
|
|
281
|
-
"Protected Tools",
|
|
282
|
-
),
|
|
283
|
-
},
|
|
284
|
-
],
|
|
285
|
-
},
|
|
286
|
-
{
|
|
287
|
-
label: "Permission Gate",
|
|
288
|
-
items: [
|
|
289
|
-
{
|
|
290
|
-
id: "permissionGate.requireConfirmation",
|
|
291
|
-
label: "Require confirmation",
|
|
292
|
-
description:
|
|
293
|
-
"Show confirmation dialog for dangerous commands (if off, just warns)",
|
|
294
|
-
currentValue:
|
|
295
|
-
(config.permissionGate?.requireConfirmation ??
|
|
296
|
-
resolved.permissionGate.requireConfirmation)
|
|
297
|
-
? "on"
|
|
298
|
-
: "off",
|
|
299
|
-
values: ["on", "off"],
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
id: "permissionGate.patterns",
|
|
303
|
-
label: "Dangerous patterns",
|
|
304
|
-
description:
|
|
305
|
-
"Command patterns that trigger the permission gate",
|
|
306
|
-
currentValue: formatCount("permissionGate.patterns"),
|
|
307
|
-
submenu: patternArraySubmenu(
|
|
308
|
-
"permissionGate.patterns",
|
|
309
|
-
"Dangerous Patterns",
|
|
310
|
-
),
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
id: "permissionGate.allowedPatterns",
|
|
314
|
-
label: "Allowed commands",
|
|
315
|
-
description:
|
|
316
|
-
"Regex patterns that bypass the permission gate entirely",
|
|
317
|
-
currentValue: formatCount("permissionGate.allowedPatterns"),
|
|
318
|
-
submenu: stringArraySubmenu(
|
|
319
|
-
"permissionGate.allowedPatterns",
|
|
320
|
-
"Allowed Commands",
|
|
321
|
-
),
|
|
322
|
-
},
|
|
323
|
-
{
|
|
324
|
-
id: "permissionGate.autoDenyPatterns",
|
|
325
|
-
label: "Auto-deny patterns",
|
|
326
|
-
description:
|
|
327
|
-
"Regex patterns that are blocked immediately without dialog",
|
|
328
|
-
currentValue: formatCount("permissionGate.autoDenyPatterns"),
|
|
329
|
-
submenu: stringArraySubmenu(
|
|
330
|
-
"permissionGate.autoDenyPatterns",
|
|
331
|
-
"Auto-Deny Patterns",
|
|
332
|
-
),
|
|
333
|
-
},
|
|
334
|
-
],
|
|
335
|
-
},
|
|
336
|
-
{
|
|
337
|
-
label: "Package Manager",
|
|
338
|
-
items: [
|
|
339
|
-
{
|
|
340
|
-
id: "packageManager.selected",
|
|
341
|
-
label: "Selected manager",
|
|
342
|
-
description:
|
|
343
|
-
"Which package manager to enforce (when feature is enabled)",
|
|
344
|
-
currentValue:
|
|
345
|
-
config.packageManager?.selected ??
|
|
346
|
-
resolved.packageManager.selected,
|
|
347
|
-
values: ["npm", "pnpm", "bun"],
|
|
348
|
-
},
|
|
349
|
-
],
|
|
350
|
-
},
|
|
351
|
-
];
|
|
352
|
-
|
|
353
|
-
return new SectionedSettings(
|
|
354
|
-
sections,
|
|
355
|
-
15,
|
|
356
|
-
settingsTheme,
|
|
357
|
-
(id, newValue) => {
|
|
358
|
-
void handleSettingChange(tab, id, newValue);
|
|
359
|
-
},
|
|
360
|
-
() => done(undefined),
|
|
361
|
-
{ enableSearch: true },
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// --- Change handler ---
|
|
366
|
-
|
|
367
|
-
async function handleSettingChange(
|
|
368
|
-
tab: Tab,
|
|
369
|
-
id: string,
|
|
370
|
-
newValue: string,
|
|
371
|
-
): Promise<void> {
|
|
372
|
-
const config =
|
|
373
|
-
tab === "local"
|
|
374
|
-
? configLoader.getProjectConfig()
|
|
375
|
-
: configLoader.getGlobalConfig();
|
|
376
|
-
const updated: GuardrailsConfig = structuredClone(config);
|
|
377
|
-
|
|
378
|
-
// Boolean toggles
|
|
379
|
-
if (
|
|
380
|
-
newValue === "enabled" ||
|
|
381
|
-
newValue === "disabled" ||
|
|
382
|
-
newValue === "on" ||
|
|
383
|
-
newValue === "off"
|
|
384
|
-
) {
|
|
385
|
-
const boolVal = newValue === "enabled" || newValue === "on";
|
|
386
|
-
setNestedValue(updated, id, boolVal);
|
|
387
|
-
|
|
388
|
-
const ok = await saveTabConfig(tab, updated);
|
|
389
|
-
if (ok) {
|
|
390
|
-
settings = buildSettings(activeTab);
|
|
391
|
-
tui.requestRender();
|
|
392
|
-
}
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Package manager selection
|
|
397
|
-
if (id === "packageManager.selected") {
|
|
398
|
-
setNestedValue(updated, id, newValue);
|
|
399
|
-
|
|
400
|
-
const ok = await saveTabConfig(tab, updated);
|
|
401
|
-
if (ok) {
|
|
402
|
-
settings = buildSettings(activeTab);
|
|
403
|
-
tui.requestRender();
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// --- Tab rendering ---
|
|
409
|
-
|
|
410
|
-
function renderTabs(): string[] {
|
|
411
|
-
const localLabel =
|
|
412
|
-
activeTab === "local"
|
|
413
|
-
? theme.bg("selectedBg", theme.fg("accent", " Local "))
|
|
414
|
-
: theme.fg("dim", " Local ");
|
|
415
|
-
const globalLabel =
|
|
416
|
-
activeTab === "global"
|
|
417
|
-
? theme.bg("selectedBg", theme.fg("accent", " Global "))
|
|
418
|
-
: theme.fg("dim", " Global ");
|
|
419
|
-
|
|
420
|
-
return ["", ` ${localLabel} ${globalLabel}`, ""];
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function handleTabSwitch(data: string): boolean {
|
|
424
|
-
if (matchesKey(data, Key.tab) || matchesKey(data, Key.shift("tab"))) {
|
|
425
|
-
activeTab = activeTab === "local" ? "global" : "local";
|
|
426
|
-
settings = buildSettings(activeTab);
|
|
427
|
-
tui.requestRender();
|
|
428
|
-
return true;
|
|
429
|
-
}
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// --- Init ---
|
|
434
|
-
|
|
435
|
-
settings = buildSettings(activeTab);
|
|
436
|
-
|
|
437
|
-
return {
|
|
438
|
-
render(width: number) {
|
|
439
|
-
const lines: string[] = [];
|
|
440
|
-
lines.push(theme.fg("accent", theme.bold("Guardrails Settings")));
|
|
441
|
-
lines.push(...renderTabs());
|
|
442
|
-
lines.push(...(settings?.render(width) ?? []));
|
|
443
|
-
return lines;
|
|
444
|
-
},
|
|
445
|
-
invalidate() {
|
|
446
|
-
settings?.invalidate?.();
|
|
447
|
-
},
|
|
448
|
-
handleInput(data: string) {
|
|
449
|
-
// Don't switch tabs when a submenu is active (it needs Tab)
|
|
450
|
-
if (!settings?.hasActiveSubmenu() && handleTabSwitch(data)) return;
|
|
451
|
-
settings?.handleInput?.(data);
|
|
452
|
-
tui.requestRender();
|
|
453
|
-
},
|
|
454
|
-
};
|
|
455
|
-
});
|
|
456
|
-
},
|
|
457
|
-
});
|
|
458
|
-
}
|