@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
|
@@ -8,16 +8,17 @@ import {
|
|
|
8
8
|
} from "@mariozechner/pi-tui";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* A submenu component for editing an array of {pattern, description} objects.
|
|
11
|
+
* A submenu component for editing an array of {pattern, description, regex?} objects.
|
|
12
12
|
*
|
|
13
13
|
* List mode: navigate, delete with 'd', add with 'a', edit with 'e'/Enter.
|
|
14
|
-
* Form mode:
|
|
15
|
-
* Enter to submit, Escape to cancel.
|
|
14
|
+
* Form mode: three-field form (pattern + description + regex toggle),
|
|
15
|
+
* Tab to switch fields, Enter to submit, Escape to cancel.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
export interface PatternItem {
|
|
19
19
|
pattern: string;
|
|
20
20
|
description: string;
|
|
21
|
+
regex?: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export interface PatternEditorOptions {
|
|
@@ -26,10 +27,12 @@ export interface PatternEditorOptions {
|
|
|
26
27
|
theme: SettingsListTheme;
|
|
27
28
|
onSave: (items: PatternItem[]) => void;
|
|
28
29
|
onDone: () => void;
|
|
30
|
+
/** Context hint for the pattern field label. */
|
|
31
|
+
context?: "file" | "command";
|
|
29
32
|
maxVisible?: number;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
type Field = "pattern" | "description";
|
|
35
|
+
type Field = "pattern" | "description" | "regex";
|
|
33
36
|
|
|
34
37
|
export class PatternEditor implements Component {
|
|
35
38
|
private items: PatternItem[];
|
|
@@ -37,6 +40,7 @@ export class PatternEditor implements Component {
|
|
|
37
40
|
private theme: SettingsListTheme;
|
|
38
41
|
private onSave: (items: PatternItem[]) => void;
|
|
39
42
|
private onDone: () => void;
|
|
43
|
+
private context: "file" | "command";
|
|
40
44
|
private selectedIndex = 0;
|
|
41
45
|
private maxVisible: number;
|
|
42
46
|
private mode: "list" | "add" | "edit" = "list";
|
|
@@ -46,6 +50,7 @@ export class PatternEditor implements Component {
|
|
|
46
50
|
private patternInput: Input;
|
|
47
51
|
private descriptionInput: Input;
|
|
48
52
|
private activeField: Field = "pattern";
|
|
53
|
+
private regexEnabled = false;
|
|
49
54
|
|
|
50
55
|
constructor(options: PatternEditorOptions) {
|
|
51
56
|
this.items = [...options.items];
|
|
@@ -53,6 +58,7 @@ export class PatternEditor implements Component {
|
|
|
53
58
|
this.theme = options.theme;
|
|
54
59
|
this.onSave = options.onSave;
|
|
55
60
|
this.onDone = options.onDone;
|
|
61
|
+
this.context = options.context ?? "command";
|
|
56
62
|
this.maxVisible = options.maxVisible ?? 10;
|
|
57
63
|
|
|
58
64
|
this.patternInput = new Input();
|
|
@@ -72,7 +78,13 @@ export class PatternEditor implements Component {
|
|
|
72
78
|
return;
|
|
73
79
|
}
|
|
74
80
|
|
|
75
|
-
// If on description field
|
|
81
|
+
// If on description field, move to regex toggle
|
|
82
|
+
if (this.activeField === "description") {
|
|
83
|
+
this.activeField = "regex";
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If on regex field, submit
|
|
76
88
|
this.submitForm();
|
|
77
89
|
}
|
|
78
90
|
|
|
@@ -89,6 +101,9 @@ export class PatternEditor implements Component {
|
|
|
89
101
|
pattern,
|
|
90
102
|
description: description || pattern,
|
|
91
103
|
};
|
|
104
|
+
if (this.regexEnabled) {
|
|
105
|
+
item.regex = true;
|
|
106
|
+
}
|
|
92
107
|
|
|
93
108
|
if (this.mode === "edit") {
|
|
94
109
|
this.items[this.editIndex] = item;
|
|
@@ -105,6 +120,7 @@ export class PatternEditor implements Component {
|
|
|
105
120
|
this.mode = "list";
|
|
106
121
|
this.editIndex = -1;
|
|
107
122
|
this.activeField = "pattern";
|
|
123
|
+
this.regexEnabled = false;
|
|
108
124
|
this.patternInput.setValue("");
|
|
109
125
|
this.descriptionInput.setValue("");
|
|
110
126
|
}
|
|
@@ -118,6 +134,7 @@ export class PatternEditor implements Component {
|
|
|
118
134
|
this.activeField = "pattern";
|
|
119
135
|
this.patternInput.setValue(item.pattern);
|
|
120
136
|
this.descriptionInput.setValue(item.description);
|
|
137
|
+
this.regexEnabled = item.regex ?? false;
|
|
121
138
|
}
|
|
122
139
|
|
|
123
140
|
private deleteSelected() {
|
|
@@ -167,7 +184,8 @@ export class PatternEditor implements Component {
|
|
|
167
184
|
const prefix = isSelected ? this.theme.cursor : " ";
|
|
168
185
|
const prefixWidth = visibleWidth(prefix);
|
|
169
186
|
const maxItemWidth = width - prefixWidth - 2;
|
|
170
|
-
const
|
|
187
|
+
const regexTag = item.regex ? " [regex]" : "";
|
|
188
|
+
const display = `${item.description} (${item.pattern})${regexTag}`;
|
|
171
189
|
const text = this.theme.value(
|
|
172
190
|
truncateToWidth(display, maxItemWidth, ""),
|
|
173
191
|
isSelected,
|
|
@@ -197,13 +215,16 @@ export class PatternEditor implements Component {
|
|
|
197
215
|
|
|
198
216
|
const patternActive = this.activeField === "pattern";
|
|
199
217
|
const descActive = this.activeField === "description";
|
|
218
|
+
const regexActive = this.activeField === "regex";
|
|
200
219
|
|
|
201
220
|
// Title
|
|
202
221
|
lines.push(this.theme.hint(isEdit ? " Edit pattern:" : " New pattern:"));
|
|
203
222
|
lines.push("");
|
|
204
223
|
|
|
205
224
|
// Pattern field
|
|
206
|
-
const
|
|
225
|
+
const patternHint =
|
|
226
|
+
this.context === "file" ? "(glob or regex)" : "(substring or regex)";
|
|
227
|
+
const patternLabel = ` Pattern ${patternHint}:`;
|
|
207
228
|
lines.push(
|
|
208
229
|
patternActive
|
|
209
230
|
? this.theme.label(patternLabel, true)
|
|
@@ -222,8 +243,21 @@ export class PatternEditor implements Component {
|
|
|
222
243
|
lines.push(` ${this.descriptionInput.render(inputWidth).join("")}`);
|
|
223
244
|
lines.push("");
|
|
224
245
|
|
|
246
|
+
// Regex toggle
|
|
247
|
+
const regexLabel = " Regex:";
|
|
248
|
+
const regexValue = this.regexEnabled ? "on" : "off";
|
|
249
|
+
const regexDisplay = `${regexLabel} ${regexValue}`;
|
|
225
250
|
lines.push(
|
|
226
|
-
|
|
251
|
+
regexActive
|
|
252
|
+
? this.theme.label(regexDisplay, true)
|
|
253
|
+
: this.theme.hint(regexDisplay),
|
|
254
|
+
);
|
|
255
|
+
lines.push("");
|
|
256
|
+
|
|
257
|
+
lines.push(
|
|
258
|
+
this.theme.hint(
|
|
259
|
+
" Tab: switch field · Space: toggle regex · Enter: next/submit · Esc: cancel",
|
|
260
|
+
),
|
|
227
261
|
);
|
|
228
262
|
|
|
229
263
|
return lines;
|
|
@@ -251,6 +285,7 @@ export class PatternEditor implements Component {
|
|
|
251
285
|
} else if (data === "a" || data === "A") {
|
|
252
286
|
this.mode = "add";
|
|
253
287
|
this.activeField = "pattern";
|
|
288
|
+
this.regexEnabled = false;
|
|
254
289
|
this.patternInput.setValue("");
|
|
255
290
|
this.descriptionInput.setValue("");
|
|
256
291
|
} else if (data === "e" || data === "E" || matchesKey(data, Key.enter)) {
|
|
@@ -264,8 +299,12 @@ export class PatternEditor implements Component {
|
|
|
264
299
|
|
|
265
300
|
private handleFormInput(data: string) {
|
|
266
301
|
if (matchesKey(data, Key.tab) || matchesKey(data, Key.shift("tab"))) {
|
|
267
|
-
|
|
268
|
-
|
|
302
|
+
const fields: Field[] = ["pattern", "description", "regex"];
|
|
303
|
+
const idx = fields.indexOf(this.activeField);
|
|
304
|
+
const dir = matchesKey(data, Key.shift("tab")) ? -1 : 1;
|
|
305
|
+
this.activeField = fields[
|
|
306
|
+
(idx + dir + fields.length) % fields.length
|
|
307
|
+
] as Field;
|
|
269
308
|
return;
|
|
270
309
|
}
|
|
271
310
|
|
|
@@ -274,6 +313,18 @@ export class PatternEditor implements Component {
|
|
|
274
313
|
return;
|
|
275
314
|
}
|
|
276
315
|
|
|
316
|
+
// Regex toggle: space toggles when on regex field
|
|
317
|
+
if (this.activeField === "regex") {
|
|
318
|
+
if (data === " " || matchesKey(data, Key.enter)) {
|
|
319
|
+
this.regexEnabled = !this.regexEnabled;
|
|
320
|
+
}
|
|
321
|
+
// Enter on regex field submits if we already have a pattern
|
|
322
|
+
if (matchesKey(data, Key.enter) && this.patternInput.getValue().trim()) {
|
|
323
|
+
this.submitForm();
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
277
328
|
// Delegate to active input
|
|
278
329
|
const activeInput =
|
|
279
330
|
this.activeField === "pattern"
|
package/config.ts
CHANGED
|
@@ -1,40 +1,174 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Configuration schema for the guardrails extension.
|
|
3
|
+
*
|
|
4
|
+
* GuardrailsConfig is the user-facing schema (all fields optional).
|
|
5
|
+
* ResolvedConfig is the internal schema (all fields required, defaults applied).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A pattern with explicit matching mode.
|
|
10
|
+
* Default: glob for files, substring for commands.
|
|
11
|
+
* regex: true means full regex matching.
|
|
12
|
+
*/
|
|
13
|
+
export interface PatternConfig {
|
|
14
|
+
pattern: string;
|
|
15
|
+
regex?: boolean;
|
|
16
|
+
}
|
|
5
17
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Permission gate pattern. When regex is false (default), the pattern
|
|
20
|
+
* is matched as substring against the raw command string.
|
|
21
|
+
* When regex is true, uses full regex against the raw string.
|
|
22
|
+
*/
|
|
23
|
+
export interface DangerousPattern extends PatternConfig {
|
|
24
|
+
description: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GuardrailsConfig {
|
|
28
|
+
version?: string;
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
features?: {
|
|
31
|
+
protectEnvFiles?: boolean;
|
|
32
|
+
permissionGate?: boolean;
|
|
33
|
+
};
|
|
34
|
+
envFiles?: {
|
|
35
|
+
protectedPatterns?: PatternConfig[];
|
|
36
|
+
allowedPatterns?: PatternConfig[];
|
|
37
|
+
protectedDirectories?: PatternConfig[];
|
|
38
|
+
protectedTools?: string[];
|
|
39
|
+
onlyBlockIfExists?: boolean;
|
|
40
|
+
blockMessage?: string;
|
|
41
|
+
};
|
|
42
|
+
permissionGate?: {
|
|
43
|
+
patterns?: DangerousPattern[];
|
|
44
|
+
/** If set, replaces the default patterns entirely. */
|
|
45
|
+
customPatterns?: DangerousPattern[];
|
|
46
|
+
requireConfirmation?: boolean;
|
|
47
|
+
allowedPatterns?: PatternConfig[];
|
|
48
|
+
autoDenyPatterns?: PatternConfig[];
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ResolvedConfig {
|
|
53
|
+
version: string;
|
|
54
|
+
enabled: boolean;
|
|
55
|
+
features: {
|
|
56
|
+
protectEnvFiles: boolean;
|
|
57
|
+
permissionGate: boolean;
|
|
58
|
+
};
|
|
59
|
+
envFiles: {
|
|
60
|
+
protectedPatterns: PatternConfig[];
|
|
61
|
+
allowedPatterns: PatternConfig[];
|
|
62
|
+
protectedDirectories: PatternConfig[];
|
|
63
|
+
protectedTools: string[];
|
|
64
|
+
onlyBlockIfExists: boolean;
|
|
65
|
+
blockMessage: string;
|
|
66
|
+
};
|
|
67
|
+
permissionGate: {
|
|
68
|
+
patterns: DangerousPattern[];
|
|
69
|
+
/** When true, use hardcoded structural matchers for built-in patterns.
|
|
70
|
+
* Set to false when customPatterns replaces the defaults. */
|
|
71
|
+
useBuiltinMatchers: boolean;
|
|
72
|
+
requireConfirmation: boolean;
|
|
73
|
+
allowedPatterns: PatternConfig[];
|
|
74
|
+
autoDenyPatterns: PatternConfig[];
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
import { ConfigLoader, type Migration } from "@aliou/pi-utils-settings";
|
|
79
|
+
import {
|
|
80
|
+
backupConfig,
|
|
81
|
+
CURRENT_VERSION,
|
|
82
|
+
migrateV0,
|
|
83
|
+
needsMigration,
|
|
84
|
+
} from "./utils/migration";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Config fields removed in the toolchain extraction.
|
|
88
|
+
* Old configs containing these are auto-cleaned on first load.
|
|
89
|
+
*/
|
|
90
|
+
const REMOVED_FEATURE_KEYS = [
|
|
91
|
+
"preventBrew",
|
|
92
|
+
"preventPython",
|
|
93
|
+
"enforcePackageManager",
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
const TOOLCHAIN_MIGRATION_VERSION = "0.7.0-20260204";
|
|
97
|
+
|
|
98
|
+
function hasRemovedFields(config: GuardrailsConfig): boolean {
|
|
99
|
+
const raw = config as Record<string, unknown>;
|
|
100
|
+
const features = raw.features as Record<string, unknown> | undefined;
|
|
101
|
+
if (features) {
|
|
102
|
+
for (const key of REMOVED_FEATURE_KEYS) {
|
|
103
|
+
if (key in features) return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return "packageManager" in raw;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function stripRemovedFields(config: GuardrailsConfig): GuardrailsConfig {
|
|
110
|
+
const cleaned = structuredClone(config) as Record<string, unknown>;
|
|
111
|
+
const features = cleaned.features as Record<string, unknown> | undefined;
|
|
112
|
+
if (features) {
|
|
113
|
+
for (const key of REMOVED_FEATURE_KEYS) {
|
|
114
|
+
delete features[key];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
delete cleaned.packageManager;
|
|
118
|
+
cleaned.version = TOOLCHAIN_MIGRATION_VERSION;
|
|
119
|
+
return cleaned as GuardrailsConfig;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const migrations: Migration<GuardrailsConfig>[] = [
|
|
123
|
+
{
|
|
124
|
+
name: "v0-format-upgrade",
|
|
125
|
+
shouldRun: (config) => needsMigration(config),
|
|
126
|
+
run: async (config, filePath) => {
|
|
127
|
+
await backupConfig(filePath);
|
|
128
|
+
return migrateV0(config);
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "strip-toolchain-fields",
|
|
133
|
+
shouldRun: (config) => hasRemovedFields(config),
|
|
134
|
+
run: (config) => {
|
|
135
|
+
const version = (config as Record<string, unknown>).version as
|
|
136
|
+
| string
|
|
137
|
+
| undefined;
|
|
138
|
+
if (!version || version < TOOLCHAIN_MIGRATION_VERSION) {
|
|
139
|
+
console.error(
|
|
140
|
+
"[guardrails] preventBrew, preventPython, enforcePackageManager, and packageManager " +
|
|
141
|
+
"have been removed from guardrails and moved to @aliou/pi-toolchain. " +
|
|
142
|
+
"These fields will be stripped from your config.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return stripRemovedFields(config);
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
];
|
|
14
149
|
|
|
15
150
|
const DEFAULT_CONFIG: ResolvedConfig = {
|
|
151
|
+
version: CURRENT_VERSION,
|
|
16
152
|
enabled: true,
|
|
17
153
|
features: {
|
|
18
|
-
preventBrew: false,
|
|
19
|
-
preventPython: false,
|
|
20
154
|
protectEnvFiles: true,
|
|
21
155
|
permissionGate: true,
|
|
22
|
-
enforcePackageManager: false,
|
|
23
|
-
},
|
|
24
|
-
packageManager: {
|
|
25
|
-
selected: "npm",
|
|
26
156
|
},
|
|
27
157
|
envFiles: {
|
|
28
158
|
protectedPatterns: [
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
159
|
+
{ pattern: ".env" },
|
|
160
|
+
{ pattern: ".env.local" },
|
|
161
|
+
{ pattern: ".env.production" },
|
|
162
|
+
{ pattern: ".env.prod" },
|
|
163
|
+
{ pattern: ".dev.vars" },
|
|
34
164
|
],
|
|
35
165
|
allowedPatterns: [
|
|
36
|
-
"
|
|
37
|
-
"
|
|
166
|
+
{ pattern: "*.example.env" },
|
|
167
|
+
{ pattern: "*.sample.env" },
|
|
168
|
+
{ pattern: "*.test.env" },
|
|
169
|
+
{ pattern: ".env.example" },
|
|
170
|
+
{ pattern: ".env.sample" },
|
|
171
|
+
{ pattern: ".env.test" },
|
|
38
172
|
],
|
|
39
173
|
protectedDirectories: [],
|
|
40
174
|
protectedTools: ["read", "write", "edit", "bash", "grep", "find", "ls"],
|
|
@@ -46,131 +180,40 @@ const DEFAULT_CONFIG: ResolvedConfig = {
|
|
|
46
180
|
},
|
|
47
181
|
permissionGate: {
|
|
48
182
|
patterns: [
|
|
49
|
-
{ pattern: "rm
|
|
50
|
-
{ pattern: "
|
|
51
|
-
{ pattern: "
|
|
52
|
-
{ pattern: "
|
|
53
|
-
{ pattern: "mkfs\\.", description: "filesystem format" },
|
|
183
|
+
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
184
|
+
{ pattern: "sudo", description: "superuser command" },
|
|
185
|
+
{ pattern: "dd if=", description: "disk write operation" },
|
|
186
|
+
{ pattern: "mkfs.", description: "filesystem format" },
|
|
54
187
|
{
|
|
55
|
-
pattern: "
|
|
188
|
+
pattern: "chmod -R 777",
|
|
56
189
|
description: "insecure recursive permissions",
|
|
57
190
|
},
|
|
58
|
-
{
|
|
59
|
-
pattern: "\\bchown\\s+-R",
|
|
60
|
-
description: "recursive ownership change",
|
|
61
|
-
},
|
|
191
|
+
{ pattern: "chown -R", description: "recursive ownership change" },
|
|
62
192
|
],
|
|
193
|
+
useBuiltinMatchers: true,
|
|
63
194
|
requireConfirmation: true,
|
|
64
195
|
allowedPatterns: [],
|
|
65
196
|
autoDenyPatterns: [],
|
|
66
197
|
},
|
|
67
198
|
};
|
|
68
199
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
} catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
private mergeConfigs(): ResolvedConfig {
|
|
90
|
-
const merged = structuredClone(DEFAULT_CONFIG);
|
|
91
|
-
|
|
92
|
-
if (this.globalConfig) {
|
|
93
|
-
this.mergeInto(merged, this.globalConfig);
|
|
94
|
-
}
|
|
95
|
-
if (this.projectConfig) {
|
|
96
|
-
this.mergeInto(merged, this.projectConfig);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// customPatterns replaces entire patterns array
|
|
100
|
-
if (this.projectConfig?.permissionGate?.customPatterns) {
|
|
101
|
-
merged.permissionGate.patterns =
|
|
102
|
-
this.projectConfig.permissionGate.customPatterns;
|
|
103
|
-
} else if (this.globalConfig?.permissionGate?.customPatterns) {
|
|
104
|
-
merged.permissionGate.patterns =
|
|
105
|
-
this.globalConfig.permissionGate.customPatterns;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return merged;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private mergeInto<TTarget extends object, TSource extends object>(
|
|
112
|
-
target: TTarget,
|
|
113
|
-
source: TSource,
|
|
114
|
-
): void {
|
|
115
|
-
const t = target as Record<string, unknown>;
|
|
116
|
-
const s = source as Record<string, unknown>;
|
|
117
|
-
|
|
118
|
-
for (const key in s) {
|
|
119
|
-
if (s[key] === undefined) continue;
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
typeof s[key] === "object" &&
|
|
123
|
-
!Array.isArray(s[key]) &&
|
|
124
|
-
s[key] !== null
|
|
125
|
-
) {
|
|
126
|
-
if (!t[key]) t[key] = {};
|
|
127
|
-
this.mergeInto(t[key] as object, s[key] as object);
|
|
128
|
-
} else {
|
|
129
|
-
t[key] = s[key];
|
|
200
|
+
export const configLoader = new ConfigLoader<GuardrailsConfig, ResolvedConfig>(
|
|
201
|
+
"guardrails",
|
|
202
|
+
DEFAULT_CONFIG,
|
|
203
|
+
{
|
|
204
|
+
migrations,
|
|
205
|
+
afterMerge: (resolved, global, project) => {
|
|
206
|
+
// customPatterns replaces the entire patterns array and disables
|
|
207
|
+
// built-in structural matchers (user owns all matching).
|
|
208
|
+
if (project?.permissionGate?.customPatterns) {
|
|
209
|
+
resolved.permissionGate.patterns =
|
|
210
|
+
project.permissionGate.customPatterns;
|
|
211
|
+
resolved.permissionGate.useBuiltinMatchers = false;
|
|
212
|
+
} else if (global?.permissionGate?.customPatterns) {
|
|
213
|
+
resolved.permissionGate.patterns = global.permissionGate.customPatterns;
|
|
214
|
+
resolved.permissionGate.useBuiltinMatchers = false;
|
|
130
215
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (!this.resolved) {
|
|
136
|
-
throw new Error("Config not loaded. Call load() first.");
|
|
137
|
-
}
|
|
138
|
-
return this.resolved;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async saveGlobal(config: GuardrailsConfig): Promise<void> {
|
|
142
|
-
await this.saveConfigFile(GLOBAL_CONFIG_PATH, config);
|
|
143
|
-
await this.load();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async saveProject(config: GuardrailsConfig): Promise<void> {
|
|
147
|
-
await this.saveConfigFile(PROJECT_CONFIG_PATH, config);
|
|
148
|
-
await this.load();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
private async saveConfigFile(
|
|
152
|
-
path: string,
|
|
153
|
-
config: GuardrailsConfig,
|
|
154
|
-
): Promise<void> {
|
|
155
|
-
await mkdir(dirname(path), { recursive: true });
|
|
156
|
-
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
hasGlobalConfig(): boolean {
|
|
160
|
-
return this.globalConfig !== null;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
hasProjectConfig(): boolean {
|
|
164
|
-
return this.projectConfig !== null;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
getGlobalConfig(): GuardrailsConfig {
|
|
168
|
-
return this.globalConfig ?? {};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
getProjectConfig(): GuardrailsConfig {
|
|
172
|
-
return this.projectConfig ?? {};
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export const configLoader = new ConfigLoader();
|
|
216
|
+
return resolved;
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
);
|
package/hooks/index.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type { ResolvedConfig } from "../config
|
|
3
|
-
import { setupEnforcePackageManagerHook } from "./enforce-package-manager";
|
|
2
|
+
import type { ResolvedConfig } from "../config";
|
|
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
|
}
|