@aliou/pi-guardrails 0.8.0 → 0.9.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-guardrails",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"README.md"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@aliou/pi-utils-settings": "^0.
|
|
32
|
+
"@aliou/pi-utils-settings": "^0.8.0",
|
|
33
33
|
"@aliou/sh": "^0.1.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
|
+
FuzzySelector,
|
|
2
3
|
getNestedValue,
|
|
3
4
|
registerSettingsCommand,
|
|
4
5
|
SettingsDetailEditor,
|
|
5
6
|
type SettingsDetailField,
|
|
6
7
|
type SettingsSection,
|
|
8
|
+
type SettingsTheme,
|
|
7
9
|
setNestedValue,
|
|
10
|
+
Wizard,
|
|
8
11
|
} from "@aliou/pi-utils-settings";
|
|
9
12
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
-
import { getSettingsListTheme } from "@mariozechner/pi-coding-agent";
|
|
11
13
|
import {
|
|
12
14
|
type Component,
|
|
13
15
|
Input,
|
|
16
|
+
Key,
|
|
17
|
+
matchesKey,
|
|
14
18
|
type SettingItem,
|
|
15
19
|
type SettingsListTheme,
|
|
16
20
|
} from "@mariozechner/pi-tui";
|
|
@@ -38,6 +42,57 @@ const FEATURE_UI: Record<FeatureKey, { label: string; description: string }> = {
|
|
|
38
42
|
},
|
|
39
43
|
};
|
|
40
44
|
|
|
45
|
+
const POLICY_EXAMPLES: Array<{
|
|
46
|
+
label: string;
|
|
47
|
+
description: string;
|
|
48
|
+
rule: PolicyRule;
|
|
49
|
+
}> = [
|
|
50
|
+
{
|
|
51
|
+
label: "Secrets (.env)",
|
|
52
|
+
description: "Block dotenv-like files (glob)",
|
|
53
|
+
rule: {
|
|
54
|
+
id: "example-secret-env-files",
|
|
55
|
+
name: "Secret env files",
|
|
56
|
+
description: "Block .env files and variants",
|
|
57
|
+
patterns: [{ pattern: ".env" }, { pattern: ".env.*" }],
|
|
58
|
+
allowedPatterns: [
|
|
59
|
+
{ pattern: ".env.example" },
|
|
60
|
+
{ pattern: "*.sample.env" },
|
|
61
|
+
],
|
|
62
|
+
protection: "noAccess",
|
|
63
|
+
onlyIfExists: true,
|
|
64
|
+
enabled: true,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
label: "Logs (*.log)",
|
|
69
|
+
description: "Mark log files read-only (glob)",
|
|
70
|
+
rule: {
|
|
71
|
+
id: "example-log-files",
|
|
72
|
+
name: "Log files",
|
|
73
|
+
description: "Treat log files as read-only",
|
|
74
|
+
patterns: [{ pattern: "*.log" }, { pattern: "*.out" }],
|
|
75
|
+
protection: "readOnly",
|
|
76
|
+
onlyIfExists: true,
|
|
77
|
+
enabled: true,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
label: "Regex env",
|
|
82
|
+
description: "Regex match for .env and .env.*",
|
|
83
|
+
rule: {
|
|
84
|
+
id: "example-regex-env",
|
|
85
|
+
name: "Regex env files",
|
|
86
|
+
description: "Regex example for env files",
|
|
87
|
+
patterns: [{ pattern: "^\\.env(\\..+)?$", regex: true }],
|
|
88
|
+
allowedPatterns: [{ pattern: "^\\.env\\.example$", regex: true }],
|
|
89
|
+
protection: "noAccess",
|
|
90
|
+
onlyIfExists: true,
|
|
91
|
+
enabled: true,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
41
96
|
function toKebabCase(input: string): string {
|
|
42
97
|
return input
|
|
43
98
|
.trim()
|
|
@@ -46,67 +101,353 @@ function toKebabCase(input: string): string {
|
|
|
46
101
|
.replace(/^-+|-+$/g, "");
|
|
47
102
|
}
|
|
48
103
|
|
|
104
|
+
function appendPolicyRule(
|
|
105
|
+
config: GuardrailsConfig | null,
|
|
106
|
+
example: PolicyRule,
|
|
107
|
+
): GuardrailsConfig {
|
|
108
|
+
const next = structuredClone(config ?? {}) as GuardrailsConfig;
|
|
109
|
+
const currentRules = next.policies?.rules ?? [];
|
|
110
|
+
|
|
111
|
+
const existingIds = new Set(currentRules.map((rule) => rule.id));
|
|
112
|
+
const baseId =
|
|
113
|
+
toKebabCase(example.id || example.name || "example") || "example";
|
|
114
|
+
let id = baseId;
|
|
115
|
+
let i = 2;
|
|
116
|
+
while (existingIds.has(id)) {
|
|
117
|
+
id = `${baseId}-${i}`;
|
|
118
|
+
i++;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const rule = structuredClone(example);
|
|
122
|
+
rule.id = id;
|
|
123
|
+
|
|
124
|
+
next.policies = {
|
|
125
|
+
...(next.policies ?? {}),
|
|
126
|
+
rules: [...currentRules, rule],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return next;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface NewPolicyDraft {
|
|
133
|
+
name: string;
|
|
134
|
+
id: string;
|
|
135
|
+
protection: PolicyRule["protection"];
|
|
136
|
+
patterns: PatternConfig[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class PolicyNameStep implements Component {
|
|
140
|
+
private readonly input = new Input();
|
|
141
|
+
|
|
142
|
+
constructor(
|
|
143
|
+
private readonly theme: SettingsListTheme,
|
|
144
|
+
private readonly state: NewPolicyDraft,
|
|
145
|
+
private readonly onComplete: () => void,
|
|
146
|
+
) {
|
|
147
|
+
this.input.setValue(state.name);
|
|
148
|
+
this.input.onSubmit = () => {
|
|
149
|
+
const name = this.input.getValue().trim();
|
|
150
|
+
if (!name) return;
|
|
151
|
+
this.state.name = name;
|
|
152
|
+
if (!this.state.id) {
|
|
153
|
+
this.state.id = toKebabCase(name) || "policy";
|
|
154
|
+
}
|
|
155
|
+
this.onComplete();
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
invalidate() {}
|
|
160
|
+
|
|
161
|
+
render(width: number): string[] {
|
|
162
|
+
return [
|
|
163
|
+
this.theme.hint(" Step 1: Policy name"),
|
|
164
|
+
"",
|
|
165
|
+
...this.input.render(Math.max(1, width - 2)).map((line) => ` ${line}`),
|
|
166
|
+
"",
|
|
167
|
+
this.theme.hint(" Example: Secret files"),
|
|
168
|
+
this.theme.hint(" Enter to continue"),
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
handleInput(data: string): void {
|
|
173
|
+
this.input.handleInput(data);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
class PolicyProtectionStep implements Component {
|
|
178
|
+
private readonly selector: FuzzySelector;
|
|
179
|
+
|
|
180
|
+
constructor(
|
|
181
|
+
theme: SettingsListTheme,
|
|
182
|
+
state: NewPolicyDraft,
|
|
183
|
+
onComplete: () => void,
|
|
184
|
+
) {
|
|
185
|
+
this.selector = new FuzzySelector({
|
|
186
|
+
label: "Protection",
|
|
187
|
+
items: ["noAccess", "readOnly", "none"],
|
|
188
|
+
currentValue: state.protection,
|
|
189
|
+
theme,
|
|
190
|
+
onSelect: (value) => {
|
|
191
|
+
if (value === "noAccess" || value === "readOnly" || value === "none") {
|
|
192
|
+
state.protection = value;
|
|
193
|
+
onComplete();
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
onDone: () => {
|
|
197
|
+
// Esc is handled by Wizard.
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
invalidate(): void {
|
|
203
|
+
this.selector.invalidate?.();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
render(width: number): string[] {
|
|
207
|
+
return this.selector.render(width);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
handleInput(data: string): void {
|
|
211
|
+
this.selector.handleInput(data);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
class PolicyPatternsStep implements Component {
|
|
216
|
+
private readonly editor: PatternEditor;
|
|
217
|
+
|
|
218
|
+
constructor(
|
|
219
|
+
theme: SettingsListTheme,
|
|
220
|
+
state: NewPolicyDraft,
|
|
221
|
+
onComplete: () => void,
|
|
222
|
+
) {
|
|
223
|
+
this.editor = new PatternEditor({
|
|
224
|
+
label: "Policy patterns",
|
|
225
|
+
context: "file",
|
|
226
|
+
theme,
|
|
227
|
+
items: state.patterns.map((p) => ({
|
|
228
|
+
pattern: p.pattern,
|
|
229
|
+
description: p.pattern,
|
|
230
|
+
regex: p.regex,
|
|
231
|
+
})),
|
|
232
|
+
onSave: (items) => {
|
|
233
|
+
state.patterns = items
|
|
234
|
+
.map((item) => {
|
|
235
|
+
const pattern = item.pattern.trim();
|
|
236
|
+
if (!pattern) return null;
|
|
237
|
+
return {
|
|
238
|
+
pattern,
|
|
239
|
+
...(item.regex ? { regex: true } : {}),
|
|
240
|
+
};
|
|
241
|
+
})
|
|
242
|
+
.filter((item): item is PatternConfig => item !== null);
|
|
243
|
+
},
|
|
244
|
+
onDone: () => {
|
|
245
|
+
if (state.patterns.length > 0) {
|
|
246
|
+
onComplete();
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
invalidate(): void {
|
|
253
|
+
this.editor.invalidate?.();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
render(width: number): string[] {
|
|
257
|
+
return this.editor.render(width);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
handleInput(data: string): void {
|
|
261
|
+
this.editor.handleInput(data);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
class PolicyReviewStep implements Component {
|
|
266
|
+
constructor(
|
|
267
|
+
private readonly theme: SettingsListTheme,
|
|
268
|
+
private readonly state: NewPolicyDraft,
|
|
269
|
+
) {}
|
|
270
|
+
|
|
271
|
+
invalidate() {}
|
|
272
|
+
|
|
273
|
+
render(_width: number): string[] {
|
|
274
|
+
const patternPreview =
|
|
275
|
+
this.state.patterns.length > 0
|
|
276
|
+
? this.state.patterns
|
|
277
|
+
.slice(0, 3)
|
|
278
|
+
.map((p) => `${p.pattern}${p.regex ? " [regex]" : ""}`)
|
|
279
|
+
.join(", ")
|
|
280
|
+
: "(none)";
|
|
281
|
+
|
|
282
|
+
return [
|
|
283
|
+
this.theme.hint(" Review"),
|
|
284
|
+
"",
|
|
285
|
+
this.theme.hint(` Name: ${this.state.name || "(empty)"}`),
|
|
286
|
+
this.theme.hint(` ID: ${this.state.id || "(auto)"}`),
|
|
287
|
+
this.theme.hint(` Protection: ${this.state.protection}`),
|
|
288
|
+
this.theme.hint(` Patterns: ${this.state.patterns.length}`),
|
|
289
|
+
this.theme.hint(` ${patternPreview}`),
|
|
290
|
+
"",
|
|
291
|
+
this.theme.hint(" Ctrl+S: create + open editor · Esc: cancel"),
|
|
292
|
+
];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
handleInput(_data: string): void {}
|
|
296
|
+
}
|
|
297
|
+
|
|
49
298
|
class AddRuleSubmenu implements Component {
|
|
50
|
-
private readonly
|
|
51
|
-
private readonly openEditor: (
|
|
52
|
-
index: number,
|
|
53
|
-
done: (value?: string) => void,
|
|
54
|
-
) => Component;
|
|
55
|
-
private readonly onDone: (value?: string) => void;
|
|
56
|
-
private readonly theme: SettingsListTheme;
|
|
57
|
-
private readonly nameInput = new Input();
|
|
299
|
+
private readonly wizard: Wizard;
|
|
58
300
|
private activeEditor: Component | null = null;
|
|
59
301
|
|
|
60
302
|
constructor(
|
|
61
|
-
theme:
|
|
62
|
-
onCreate: (
|
|
303
|
+
theme: SettingsTheme,
|
|
304
|
+
onCreate: (draft: NewPolicyDraft) => number | null,
|
|
63
305
|
openEditor: (index: number, done: (value?: string) => void) => Component,
|
|
64
306
|
onDone: (value?: string) => void,
|
|
65
307
|
) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.nameInput.onSubmit = () => {
|
|
72
|
-
const name = this.nameInput.getValue().trim();
|
|
73
|
-
if (!name) return;
|
|
74
|
-
const index = this.onCreate(name);
|
|
75
|
-
if (index === null) return;
|
|
76
|
-
this.activeEditor = this.openEditor(index, (value) => {
|
|
77
|
-
this.activeEditor = null;
|
|
78
|
-
this.onDone(value);
|
|
79
|
-
});
|
|
308
|
+
const state: NewPolicyDraft = {
|
|
309
|
+
name: "",
|
|
310
|
+
id: "",
|
|
311
|
+
protection: "readOnly",
|
|
312
|
+
patterns: [],
|
|
80
313
|
};
|
|
81
|
-
|
|
314
|
+
|
|
315
|
+
this.wizard = new Wizard({
|
|
316
|
+
title: "Add policy",
|
|
317
|
+
theme,
|
|
318
|
+
steps: [
|
|
319
|
+
{
|
|
320
|
+
label: "Name",
|
|
321
|
+
build: (ctx) =>
|
|
322
|
+
new PolicyNameStep(theme, state, () => {
|
|
323
|
+
ctx.markComplete();
|
|
324
|
+
ctx.goNext();
|
|
325
|
+
}),
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
label: "Protection",
|
|
329
|
+
build: (ctx) =>
|
|
330
|
+
new PolicyProtectionStep(theme, state, () => {
|
|
331
|
+
ctx.markComplete();
|
|
332
|
+
ctx.goNext();
|
|
333
|
+
}),
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
label: "Patterns",
|
|
337
|
+
build: (ctx) =>
|
|
338
|
+
new PolicyPatternsStep(theme, state, () => {
|
|
339
|
+
if (state.patterns.length === 0) {
|
|
340
|
+
ctx.markIncomplete();
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
ctx.markComplete();
|
|
344
|
+
ctx.goNext();
|
|
345
|
+
}),
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
label: "Review",
|
|
349
|
+
build: (ctx) => {
|
|
350
|
+
ctx.markComplete();
|
|
351
|
+
return new PolicyReviewStep(theme, state);
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
onComplete: () => {
|
|
356
|
+
if (!state.name.trim() || state.patterns.length === 0) return;
|
|
357
|
+
const index = onCreate(state);
|
|
358
|
+
if (index === null) return;
|
|
359
|
+
this.activeEditor = openEditor(index, (value) => {
|
|
360
|
+
this.activeEditor = null;
|
|
361
|
+
onDone(value);
|
|
362
|
+
});
|
|
363
|
+
},
|
|
364
|
+
onCancel: () => onDone(),
|
|
365
|
+
hintSuffix: "complete steps · Ctrl+S create",
|
|
366
|
+
minContentHeight: 12,
|
|
367
|
+
});
|
|
82
368
|
}
|
|
83
369
|
|
|
84
|
-
invalidate() {
|
|
370
|
+
invalidate(): void {
|
|
85
371
|
this.activeEditor?.invalidate?.();
|
|
372
|
+
this.wizard.invalidate?.();
|
|
86
373
|
}
|
|
87
374
|
|
|
88
375
|
render(width: number): string[] {
|
|
89
376
|
if (this.activeEditor) {
|
|
90
377
|
return this.activeEditor.render(width);
|
|
91
378
|
}
|
|
379
|
+
return this.wizard.render(width);
|
|
380
|
+
}
|
|
92
381
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
382
|
+
handleInput(data: string): void {
|
|
383
|
+
if (this.activeEditor) {
|
|
384
|
+
this.activeEditor.handleInput?.(data);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this.wizard.handleInput(data);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
class ScopePickerSubmenu implements Component {
|
|
392
|
+
private selectedIndex = 0;
|
|
393
|
+
|
|
394
|
+
constructor(
|
|
395
|
+
private readonly theme: SettingsListTheme,
|
|
396
|
+
private readonly scopes: Array<"global" | "local" | "memory">,
|
|
397
|
+
private readonly onSelect: (scope: "global" | "local" | "memory") => void,
|
|
398
|
+
private readonly onDone: (value?: string) => void,
|
|
399
|
+
) {}
|
|
400
|
+
|
|
401
|
+
invalidate() {}
|
|
402
|
+
|
|
403
|
+
render(_width: number): string[] {
|
|
404
|
+
const lines: string[] = [
|
|
405
|
+
this.theme.label(" Add example to scope", true),
|
|
98
406
|
"",
|
|
99
|
-
this.theme.hint("
|
|
407
|
+
this.theme.hint(" Select target scope:"),
|
|
100
408
|
];
|
|
409
|
+
|
|
410
|
+
for (let i = 0; i < this.scopes.length; i++) {
|
|
411
|
+
const scope = this.scopes[i];
|
|
412
|
+
if (!scope) continue;
|
|
413
|
+
const isSelected = i === this.selectedIndex;
|
|
414
|
+
const prefix = isSelected ? this.theme.cursor : " ";
|
|
415
|
+
lines.push(`${prefix}${this.theme.value(scope, isSelected)}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
lines.push("");
|
|
419
|
+
lines.push(this.theme.hint(" Enter: apply · Esc: back"));
|
|
420
|
+
return lines;
|
|
101
421
|
}
|
|
102
422
|
|
|
103
423
|
handleInput(data: string): void {
|
|
104
|
-
if (
|
|
105
|
-
this.
|
|
424
|
+
if (matchesKey(data, Key.up) || data === "k") {
|
|
425
|
+
this.selectedIndex =
|
|
426
|
+
this.selectedIndex === 0
|
|
427
|
+
? this.scopes.length - 1
|
|
428
|
+
: this.selectedIndex - 1;
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (matchesKey(data, Key.down) || data === "j") {
|
|
433
|
+
this.selectedIndex =
|
|
434
|
+
this.selectedIndex === this.scopes.length - 1
|
|
435
|
+
? 0
|
|
436
|
+
: this.selectedIndex + 1;
|
|
106
437
|
return;
|
|
107
438
|
}
|
|
108
439
|
|
|
109
|
-
|
|
440
|
+
if (matchesKey(data, Key.enter)) {
|
|
441
|
+
const scope = this.scopes[this.selectedIndex];
|
|
442
|
+
if (!scope) return;
|
|
443
|
+
this.onSelect(scope);
|
|
444
|
+
this.onDone(`applied to ${scope}`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (matchesKey(data, Key.escape)) {
|
|
449
|
+
this.onDone();
|
|
450
|
+
}
|
|
110
451
|
}
|
|
111
452
|
}
|
|
112
453
|
|
|
@@ -316,39 +657,40 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
316
657
|
configStore: configLoader,
|
|
317
658
|
buildSections: (
|
|
318
659
|
tabConfig: GuardrailsConfig | null,
|
|
319
|
-
|
|
320
|
-
{ setDraft },
|
|
660
|
+
_resolved: ResolvedConfig,
|
|
661
|
+
{ setDraft, theme },
|
|
321
662
|
): SettingsSection[] => {
|
|
322
|
-
const settingsTheme =
|
|
663
|
+
const settingsTheme = theme;
|
|
664
|
+
let scopedConfig = structuredClone(tabConfig ?? {}) as GuardrailsConfig;
|
|
665
|
+
|
|
666
|
+
function commitDraft(next: GuardrailsConfig): void {
|
|
667
|
+
scopedConfig = next;
|
|
668
|
+
setDraft(structuredClone(next));
|
|
669
|
+
}
|
|
323
670
|
|
|
324
671
|
function count(id: string): string {
|
|
325
672
|
const val =
|
|
326
|
-
(getNestedValue(
|
|
327
|
-
(getNestedValue(resolved, id) as unknown[]) ??
|
|
328
|
-
[];
|
|
673
|
+
(getNestedValue(scopedConfig, id) as unknown[] | undefined) ?? [];
|
|
329
674
|
return `${val.length} items`;
|
|
330
675
|
}
|
|
331
676
|
|
|
332
677
|
function applyDraft(id: string, value: unknown): void {
|
|
333
|
-
const updated = structuredClone(
|
|
678
|
+
const updated = structuredClone(scopedConfig);
|
|
334
679
|
setNestedValue(updated, id, value);
|
|
335
|
-
|
|
680
|
+
commitDraft(updated);
|
|
336
681
|
}
|
|
337
682
|
|
|
338
683
|
function getPolicyRules(): PolicyRule[] {
|
|
339
|
-
return (
|
|
340
|
-
tabConfig?.policies?.rules?.map((r) => ({ ...r })) ??
|
|
341
|
-
resolved.policies.rules.map((r) => ({ ...r }))
|
|
342
|
-
);
|
|
684
|
+
return scopedConfig.policies?.rules?.map((r) => ({ ...r })) ?? [];
|
|
343
685
|
}
|
|
344
686
|
|
|
345
687
|
function setPolicyRules(rules: PolicyRule[]): void {
|
|
346
|
-
const updated = structuredClone(
|
|
688
|
+
const updated = structuredClone(scopedConfig);
|
|
347
689
|
updated.policies = {
|
|
348
690
|
...(updated.policies ?? {}),
|
|
349
691
|
rules,
|
|
350
692
|
};
|
|
351
|
-
|
|
693
|
+
commitDraft(updated);
|
|
352
694
|
}
|
|
353
695
|
|
|
354
696
|
function updateRule(
|
|
@@ -369,12 +711,12 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
369
711
|
setPolicyRules(rules);
|
|
370
712
|
}
|
|
371
713
|
|
|
372
|
-
function addRule(
|
|
373
|
-
const normalizedName = name.trim();
|
|
374
|
-
if (!normalizedName) return null;
|
|
714
|
+
function addRule(draft: NewPolicyDraft): number | null {
|
|
715
|
+
const normalizedName = draft.name.trim();
|
|
716
|
+
if (!normalizedName || draft.patterns.length === 0) return null;
|
|
375
717
|
|
|
376
718
|
const rules = getPolicyRules();
|
|
377
|
-
const baseId = toKebabCase(normalizedName) || "policy";
|
|
719
|
+
const baseId = toKebabCase(draft.id || normalizedName) || "policy";
|
|
378
720
|
const existingIds = new Set(rules.map((rule) => rule.id));
|
|
379
721
|
|
|
380
722
|
let id = baseId;
|
|
@@ -388,8 +730,8 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
388
730
|
id,
|
|
389
731
|
name: normalizedName,
|
|
390
732
|
description: "",
|
|
391
|
-
patterns:
|
|
392
|
-
protection:
|
|
733
|
+
patterns: draft.patterns,
|
|
734
|
+
protection: draft.protection,
|
|
393
735
|
onlyIfExists: true,
|
|
394
736
|
enabled: true,
|
|
395
737
|
});
|
|
@@ -404,11 +746,9 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
404
746
|
) {
|
|
405
747
|
return (_val: string, submenuDone: (v?: string) => void) => {
|
|
406
748
|
const items =
|
|
407
|
-
(getNestedValue(
|
|
749
|
+
(getNestedValue(scopedConfig, id) as
|
|
408
750
|
| DangerousPattern[]
|
|
409
|
-
| undefined) ??
|
|
410
|
-
(getNestedValue(resolved, id) as DangerousPattern[]) ??
|
|
411
|
-
[];
|
|
751
|
+
| undefined) ?? [];
|
|
412
752
|
let latestCount = items.length;
|
|
413
753
|
return new PatternEditor({
|
|
414
754
|
label,
|
|
@@ -431,10 +771,7 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
431
771
|
) {
|
|
432
772
|
return (_val: string, submenuDone: (v?: string) => void) => {
|
|
433
773
|
const currentItems =
|
|
434
|
-
(getNestedValue(
|
|
435
|
-
| PatternConfig[]
|
|
436
|
-
| undefined) ??
|
|
437
|
-
(getNestedValue(resolved, id) as PatternConfig[]) ??
|
|
774
|
+
(getNestedValue(scopedConfig, id) as PatternConfig[] | undefined) ??
|
|
438
775
|
[];
|
|
439
776
|
const items = currentItems.map((p) => ({
|
|
440
777
|
pattern: p.pattern,
|
|
@@ -465,31 +802,39 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
465
802
|
};
|
|
466
803
|
}
|
|
467
804
|
|
|
805
|
+
function hasExplainModelOverride(): boolean {
|
|
806
|
+
return scopedConfig.permissionGate?.explainModel !== undefined;
|
|
807
|
+
}
|
|
808
|
+
|
|
468
809
|
function getExplainModel(): string {
|
|
469
|
-
|
|
470
|
-
if (model !== undefined) return model;
|
|
471
|
-
return resolved.permissionGate.explainModel ?? "";
|
|
810
|
+
return scopedConfig.permissionGate?.explainModel?.trim() ?? "";
|
|
472
811
|
}
|
|
473
812
|
|
|
474
|
-
function
|
|
475
|
-
return
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
813
|
+
function hasExplainTimeoutOverride(): boolean {
|
|
814
|
+
return scopedConfig.permissionGate?.explainTimeout !== undefined;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function getExplainTimeout(): number | null {
|
|
818
|
+
return scopedConfig.permissionGate?.explainTimeout ?? null;
|
|
479
819
|
}
|
|
480
820
|
|
|
481
821
|
const featureItems = (Object.keys(FEATURE_UI) as FeatureKey[])
|
|
482
822
|
.filter((key) => key !== "policies")
|
|
483
|
-
.map((key) =>
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
823
|
+
.map((key) => {
|
|
824
|
+
const scopedValue = scopedConfig.features?.[key];
|
|
825
|
+
return {
|
|
826
|
+
id: `features.${key}`,
|
|
827
|
+
label: FEATURE_UI[key].label,
|
|
828
|
+
description: FEATURE_UI[key].description,
|
|
829
|
+
currentValue:
|
|
830
|
+
scopedValue === undefined
|
|
831
|
+
? "(inherited)"
|
|
832
|
+
: scopedValue
|
|
833
|
+
? "enabled"
|
|
834
|
+
: "disabled",
|
|
835
|
+
values: ["enabled", "disabled"],
|
|
836
|
+
};
|
|
837
|
+
});
|
|
493
838
|
|
|
494
839
|
const policyRules = getPolicyRules();
|
|
495
840
|
|
|
@@ -512,9 +857,11 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
512
857
|
label: " Enabled",
|
|
513
858
|
description: FEATURE_UI.policies.description,
|
|
514
859
|
currentValue:
|
|
515
|
-
|
|
516
|
-
? "
|
|
517
|
-
:
|
|
860
|
+
scopedConfig.features?.policies === undefined
|
|
861
|
+
? "(inherited)"
|
|
862
|
+
: scopedConfig.features.policies
|
|
863
|
+
? "enabled"
|
|
864
|
+
: "disabled",
|
|
518
865
|
values: ["enabled", "disabled"],
|
|
519
866
|
},
|
|
520
867
|
...policyRules.map((rule, index) => {
|
|
@@ -533,7 +880,7 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
533
880
|
policyItems.push({
|
|
534
881
|
id: "policies.addRule",
|
|
535
882
|
label: " + Add policy",
|
|
536
|
-
description: "
|
|
883
|
+
description: "Open wizard to create policy",
|
|
537
884
|
currentValue: "",
|
|
538
885
|
submenu: (_val: string, submenuDone: (v?: string) => void) =>
|
|
539
886
|
new AddRuleSubmenu(
|
|
@@ -559,10 +906,11 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
559
906
|
description:
|
|
560
907
|
"Show confirmation dialog for dangerous commands (if off, just warns)",
|
|
561
908
|
currentValue:
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
909
|
+
scopedConfig.permissionGate?.requireConfirmation === undefined
|
|
910
|
+
? "(inherited)"
|
|
911
|
+
: scopedConfig.permissionGate.requireConfirmation
|
|
912
|
+
? "on"
|
|
913
|
+
: "off",
|
|
566
914
|
values: ["on", "off"],
|
|
567
915
|
},
|
|
568
916
|
{
|
|
@@ -605,17 +953,20 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
605
953
|
description:
|
|
606
954
|
"Call an LLM to explain dangerous commands in the confirmation dialog",
|
|
607
955
|
currentValue:
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
956
|
+
scopedConfig.permissionGate?.explainCommands === undefined
|
|
957
|
+
? "(inherited)"
|
|
958
|
+
: scopedConfig.permissionGate.explainCommands
|
|
959
|
+
? "on"
|
|
960
|
+
: "off",
|
|
612
961
|
values: ["on", "off"],
|
|
613
962
|
},
|
|
614
963
|
{
|
|
615
964
|
id: "permissionGate.explainModel",
|
|
616
965
|
label: "Explain model",
|
|
617
966
|
description: "Model spec in provider/model-id format",
|
|
618
|
-
currentValue:
|
|
967
|
+
currentValue: hasExplainModelOverride()
|
|
968
|
+
? getExplainModel() || "(not set)"
|
|
969
|
+
: "(inherited)",
|
|
619
970
|
submenu: (_val: string, submenuDone: (v?: string) => void) =>
|
|
620
971
|
new SettingsDetailEditor({
|
|
621
972
|
title: "Explain Commands: Model",
|
|
@@ -645,20 +996,28 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
645
996
|
id: "permissionGate.explainTimeout",
|
|
646
997
|
label: "Explain timeout",
|
|
647
998
|
description: "Timeout for LLM explanation in milliseconds",
|
|
648
|
-
currentValue:
|
|
999
|
+
currentValue: hasExplainTimeoutOverride()
|
|
1000
|
+
? `${getExplainTimeout()}ms`
|
|
1001
|
+
: "(inherited)",
|
|
649
1002
|
submenu: (_val: string, submenuDone: (v?: string) => void) =>
|
|
650
1003
|
new SettingsDetailEditor({
|
|
651
1004
|
title: "Explain Commands: Timeout",
|
|
652
1005
|
theme: settingsTheme,
|
|
653
1006
|
onDone: submenuDone,
|
|
654
|
-
getDoneSummary: () =>
|
|
1007
|
+
getDoneSummary: () => {
|
|
1008
|
+
const timeout = getExplainTimeout();
|
|
1009
|
+
return timeout === null ? "(not set)" : `${timeout}ms`;
|
|
1010
|
+
},
|
|
655
1011
|
fields: [
|
|
656
1012
|
{
|
|
657
1013
|
id: "permissionGate.explainTimeout",
|
|
658
1014
|
type: "text",
|
|
659
1015
|
label: "Timeout (ms)",
|
|
660
1016
|
description: "Abort explanation call after this many ms",
|
|
661
|
-
getValue: () =>
|
|
1017
|
+
getValue: () => {
|
|
1018
|
+
const timeout = getExplainTimeout();
|
|
1019
|
+
return timeout === null ? "" : String(timeout);
|
|
1020
|
+
},
|
|
662
1021
|
setValue: (value) => {
|
|
663
1022
|
const parsed = Number.parseInt(value.trim(), 10);
|
|
664
1023
|
if (Number.isNaN(parsed) || parsed < 1) return;
|
|
@@ -672,5 +1031,46 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
672
1031
|
},
|
|
673
1032
|
];
|
|
674
1033
|
},
|
|
1034
|
+
extraTabs: [
|
|
1035
|
+
{
|
|
1036
|
+
id: "examples",
|
|
1037
|
+
label: "Examples",
|
|
1038
|
+
buildSections: ({
|
|
1039
|
+
enabledScopes,
|
|
1040
|
+
getDraftForScope,
|
|
1041
|
+
getRawForScope,
|
|
1042
|
+
setDraftForScope,
|
|
1043
|
+
theme,
|
|
1044
|
+
}): SettingsSection[] => {
|
|
1045
|
+
const items: SettingItem[] = POLICY_EXAMPLES.map((example) => ({
|
|
1046
|
+
id: `examples.${example.rule.id}`,
|
|
1047
|
+
label: ` ${example.label}`,
|
|
1048
|
+
description: example.description,
|
|
1049
|
+
currentValue: "apply",
|
|
1050
|
+
submenu: (_val: string, submenuDone: (v?: string) => void) =>
|
|
1051
|
+
new ScopePickerSubmenu(
|
|
1052
|
+
theme,
|
|
1053
|
+
enabledScopes,
|
|
1054
|
+
(targetScope) => {
|
|
1055
|
+
const baseConfig =
|
|
1056
|
+
getDraftForScope(targetScope) ??
|
|
1057
|
+
getRawForScope(targetScope) ??
|
|
1058
|
+
null;
|
|
1059
|
+
const updated = appendPolicyRule(baseConfig, example.rule);
|
|
1060
|
+
setDraftForScope(targetScope, updated);
|
|
1061
|
+
},
|
|
1062
|
+
submenuDone,
|
|
1063
|
+
),
|
|
1064
|
+
}));
|
|
1065
|
+
|
|
1066
|
+
return [
|
|
1067
|
+
{
|
|
1068
|
+
label: "Policy presets",
|
|
1069
|
+
items,
|
|
1070
|
+
},
|
|
1071
|
+
];
|
|
1072
|
+
},
|
|
1073
|
+
},
|
|
1074
|
+
],
|
|
675
1075
|
});
|
|
676
1076
|
}
|
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
*
|
|
13
13
|
* List mode: navigate, delete with 'd', add with 'a', edit with 'e'/Enter.
|
|
14
14
|
* Form mode: three-field form (pattern + description + regex toggle),
|
|
15
|
-
* Tab to switch fields,
|
|
15
|
+
* Tab to switch fields, Ctrl+R to toggle regex, Enter to submit,
|
|
16
|
+
* Escape to cancel.
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
export interface PatternItem {
|
|
@@ -256,7 +257,7 @@ export class PatternEditor implements Component {
|
|
|
256
257
|
|
|
257
258
|
lines.push(
|
|
258
259
|
this.theme.hint(
|
|
259
|
-
" Tab: switch field ·
|
|
260
|
+
" Tab: switch field · Ctrl+R: toggle regex · Enter: next/submit · Esc: cancel",
|
|
260
261
|
),
|
|
261
262
|
);
|
|
262
263
|
|
|
@@ -308,17 +309,18 @@ export class PatternEditor implements Component {
|
|
|
308
309
|
return;
|
|
309
310
|
}
|
|
310
311
|
|
|
312
|
+
if (matchesKey(data, Key.ctrl("r"))) {
|
|
313
|
+
this.regexEnabled = !this.regexEnabled;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
311
317
|
if (matchesKey(data, Key.escape)) {
|
|
312
318
|
this.cancelForm();
|
|
313
319
|
return;
|
|
314
320
|
}
|
|
315
321
|
|
|
316
|
-
// Regex toggle: space toggles when on regex field
|
|
317
322
|
if (this.activeField === "regex") {
|
|
318
|
-
|
|
319
|
-
this.regexEnabled = !this.regexEnabled;
|
|
320
|
-
}
|
|
321
|
-
// Enter on regex field submits if we already have a pattern
|
|
323
|
+
// Enter on regex field submits if we already have a pattern.
|
|
322
324
|
if (matchesKey(data, Key.enter) && this.patternInput.getValue().trim()) {
|
|
323
325
|
this.submitForm();
|
|
324
326
|
}
|