@aliou/pi-guardrails 0.7.7 → 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/README.md +98 -150
- package/package.json +13 -3
- package/src/commands/settings-command.ts +916 -118
- package/src/components/pattern-editor.ts +9 -7
- package/src/config.ts +110 -45
- package/src/hooks/index.ts +2 -2
- package/src/hooks/permission-gate.ts +149 -12
- package/src/hooks/policies.ts +297 -0
- package/src/index.ts +1 -1
- package/src/lib/executor.ts +280 -0
- package/src/lib/index.ts +16 -0
- package/src/lib/model-resolver.ts +47 -0
- package/src/lib/timing.ts +42 -0
- package/src/lib/types.ts +115 -0
- package/src/utils/events.ts +1 -1
- package/src/utils/migration.ts +106 -1
- package/src/hooks/protect-env-files.ts +0 -220
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
FuzzySelector,
|
|
3
3
|
getNestedValue,
|
|
4
4
|
registerSettingsCommand,
|
|
5
|
+
SettingsDetailEditor,
|
|
6
|
+
type SettingsDetailField,
|
|
5
7
|
type SettingsSection,
|
|
8
|
+
type SettingsTheme,
|
|
6
9
|
setNestedValue,
|
|
10
|
+
Wizard,
|
|
7
11
|
} from "@aliou/pi-utils-settings";
|
|
8
12
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
type Component,
|
|
15
|
+
Input,
|
|
16
|
+
Key,
|
|
17
|
+
matchesKey,
|
|
18
|
+
type SettingItem,
|
|
19
|
+
type SettingsListTheme,
|
|
20
|
+
} from "@mariozechner/pi-tui";
|
|
10
21
|
import { PatternEditor } from "../components/pattern-editor";
|
|
11
22
|
import type {
|
|
12
23
|
DangerousPattern,
|
|
13
24
|
GuardrailsConfig,
|
|
14
25
|
PatternConfig,
|
|
26
|
+
PolicyRule,
|
|
15
27
|
ResolvedConfig,
|
|
16
28
|
} from "../config";
|
|
17
29
|
import { configLoader } from "../config";
|
|
@@ -19,9 +31,9 @@ import { configLoader } from "../config";
|
|
|
19
31
|
type FeatureKey = keyof ResolvedConfig["features"];
|
|
20
32
|
|
|
21
33
|
const FEATURE_UI: Record<FeatureKey, { label: string; description: string }> = {
|
|
22
|
-
|
|
23
|
-
label: "
|
|
24
|
-
description: "Block access
|
|
34
|
+
policies: {
|
|
35
|
+
label: "Policies",
|
|
36
|
+
description: "Block or limit file access using named policy rules",
|
|
25
37
|
},
|
|
26
38
|
permissionGate: {
|
|
27
39
|
label: "Permission gate",
|
|
@@ -30,6 +42,614 @@ const FEATURE_UI: Record<FeatureKey, { label: string; description: string }> = {
|
|
|
30
42
|
},
|
|
31
43
|
};
|
|
32
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
|
+
|
|
96
|
+
function toKebabCase(input: string): string {
|
|
97
|
+
return input
|
|
98
|
+
.trim()
|
|
99
|
+
.toLowerCase()
|
|
100
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
101
|
+
.replace(/^-+|-+$/g, "");
|
|
102
|
+
}
|
|
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
|
+
|
|
298
|
+
class AddRuleSubmenu implements Component {
|
|
299
|
+
private readonly wizard: Wizard;
|
|
300
|
+
private activeEditor: Component | null = null;
|
|
301
|
+
|
|
302
|
+
constructor(
|
|
303
|
+
theme: SettingsTheme,
|
|
304
|
+
onCreate: (draft: NewPolicyDraft) => number | null,
|
|
305
|
+
openEditor: (index: number, done: (value?: string) => void) => Component,
|
|
306
|
+
onDone: (value?: string) => void,
|
|
307
|
+
) {
|
|
308
|
+
const state: NewPolicyDraft = {
|
|
309
|
+
name: "",
|
|
310
|
+
id: "",
|
|
311
|
+
protection: "readOnly",
|
|
312
|
+
patterns: [],
|
|
313
|
+
};
|
|
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
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
invalidate(): void {
|
|
371
|
+
this.activeEditor?.invalidate?.();
|
|
372
|
+
this.wizard.invalidate?.();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
render(width: number): string[] {
|
|
376
|
+
if (this.activeEditor) {
|
|
377
|
+
return this.activeEditor.render(width);
|
|
378
|
+
}
|
|
379
|
+
return this.wizard.render(width);
|
|
380
|
+
}
|
|
381
|
+
|
|
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),
|
|
406
|
+
"",
|
|
407
|
+
this.theme.hint(" Select target scope:"),
|
|
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;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
handleInput(data: string): void {
|
|
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;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
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
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function createPolicyRuleEditor(options: {
|
|
455
|
+
index: number;
|
|
456
|
+
theme: SettingsListTheme;
|
|
457
|
+
getRule: () => PolicyRule | undefined;
|
|
458
|
+
updateRule: (updater: (rule: PolicyRule) => PolicyRule) => void;
|
|
459
|
+
deleteRule: () => void;
|
|
460
|
+
onDone: (value?: string) => void;
|
|
461
|
+
}): SettingsDetailEditor {
|
|
462
|
+
const { index, theme, getRule, updateRule, deleteRule, onDone } = options;
|
|
463
|
+
|
|
464
|
+
const fields: SettingsDetailField[] = [
|
|
465
|
+
{
|
|
466
|
+
id: "name",
|
|
467
|
+
type: "text",
|
|
468
|
+
label: "Name",
|
|
469
|
+
description: "Display name shown in settings",
|
|
470
|
+
getValue: () => getRule()?.name?.trim() || "",
|
|
471
|
+
setValue: (value) => {
|
|
472
|
+
const next = value.trim();
|
|
473
|
+
updateRule((rule) => ({ ...rule, name: next || undefined }));
|
|
474
|
+
},
|
|
475
|
+
emptyValueText: "(uses id)",
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
id: "id",
|
|
479
|
+
type: "text",
|
|
480
|
+
label: "ID",
|
|
481
|
+
description: "Stable identifier used for overrides across scopes",
|
|
482
|
+
getValue: () => getRule()?.id ?? "",
|
|
483
|
+
setValue: (value) => {
|
|
484
|
+
const next = value.trim();
|
|
485
|
+
if (!next) return;
|
|
486
|
+
updateRule((rule) => ({ ...rule, id: next }));
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
id: "description",
|
|
491
|
+
type: "text",
|
|
492
|
+
label: "Description",
|
|
493
|
+
description: "Human-readable explanation",
|
|
494
|
+
getValue: () => getRule()?.description?.trim() || "",
|
|
495
|
+
setValue: (value) => {
|
|
496
|
+
const next = value.trim();
|
|
497
|
+
updateRule((rule) => ({ ...rule, description: next || undefined }));
|
|
498
|
+
},
|
|
499
|
+
emptyValueText: "(empty)",
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
id: "protection",
|
|
503
|
+
type: "enum",
|
|
504
|
+
label: "Protection",
|
|
505
|
+
description: "noAccess | readOnly | none",
|
|
506
|
+
getValue: () => getRule()?.protection ?? "readOnly",
|
|
507
|
+
setValue: (value) => {
|
|
508
|
+
if (value !== "noAccess" && value !== "readOnly" && value !== "none") {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
updateRule((rule) => ({ ...rule, protection: value }));
|
|
512
|
+
},
|
|
513
|
+
options: ["noAccess", "readOnly", "none"],
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
id: "enabled",
|
|
517
|
+
type: "boolean",
|
|
518
|
+
label: "Enabled",
|
|
519
|
+
description: "Turn this policy on/off",
|
|
520
|
+
getValue: () => getRule()?.enabled !== false,
|
|
521
|
+
setValue: (value) => {
|
|
522
|
+
updateRule((rule) => ({ ...rule, enabled: value }));
|
|
523
|
+
},
|
|
524
|
+
trueLabel: "on",
|
|
525
|
+
falseLabel: "off",
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
id: "onlyIfExists",
|
|
529
|
+
type: "boolean",
|
|
530
|
+
label: "Only if exists",
|
|
531
|
+
description: "Only block when file exists on disk",
|
|
532
|
+
getValue: () => getRule()?.onlyIfExists !== false,
|
|
533
|
+
setValue: (value) => {
|
|
534
|
+
updateRule((rule) => ({ ...rule, onlyIfExists: value }));
|
|
535
|
+
},
|
|
536
|
+
trueLabel: "on",
|
|
537
|
+
falseLabel: "off",
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
id: "patterns",
|
|
541
|
+
type: "submenu",
|
|
542
|
+
label: "Patterns",
|
|
543
|
+
description: "Files protected by this policy",
|
|
544
|
+
getValue: () => `${getRule()?.patterns?.length ?? 0} items`,
|
|
545
|
+
submenu: (done) => {
|
|
546
|
+
const rule = getRule();
|
|
547
|
+
const items = (rule?.patterns ?? []).map((p) => ({
|
|
548
|
+
pattern: p.pattern,
|
|
549
|
+
description: p.pattern,
|
|
550
|
+
regex: p.regex,
|
|
551
|
+
}));
|
|
552
|
+
|
|
553
|
+
return new PatternEditor({
|
|
554
|
+
label: "Policy patterns",
|
|
555
|
+
items,
|
|
556
|
+
theme,
|
|
557
|
+
context: "file",
|
|
558
|
+
onSave: (newItems) => {
|
|
559
|
+
const patterns: PatternConfig[] = newItems
|
|
560
|
+
.map((p) => {
|
|
561
|
+
const pattern = p.pattern.trim();
|
|
562
|
+
if (!pattern) return null;
|
|
563
|
+
return { pattern, ...(p.regex ? { regex: true } : {}) };
|
|
564
|
+
})
|
|
565
|
+
.filter((item): item is PatternConfig => item !== null);
|
|
566
|
+
|
|
567
|
+
updateRule((current) => ({ ...current, patterns }));
|
|
568
|
+
},
|
|
569
|
+
onDone: () => done(`${getRule()?.patterns?.length ?? 0} items`),
|
|
570
|
+
});
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
id: "allowedPatterns",
|
|
575
|
+
type: "submenu",
|
|
576
|
+
label: "Allowed patterns",
|
|
577
|
+
description: "Exceptions",
|
|
578
|
+
getValue: () => `${getRule()?.allowedPatterns?.length ?? 0} items`,
|
|
579
|
+
submenu: (done) => {
|
|
580
|
+
const rule = getRule();
|
|
581
|
+
const items = (rule?.allowedPatterns ?? []).map((p) => ({
|
|
582
|
+
pattern: p.pattern,
|
|
583
|
+
description: p.pattern,
|
|
584
|
+
regex: p.regex,
|
|
585
|
+
}));
|
|
586
|
+
|
|
587
|
+
return new PatternEditor({
|
|
588
|
+
label: "Policy allowed patterns",
|
|
589
|
+
items,
|
|
590
|
+
theme,
|
|
591
|
+
context: "file",
|
|
592
|
+
onSave: (newItems) => {
|
|
593
|
+
const patterns: PatternConfig[] = newItems
|
|
594
|
+
.map((p) => {
|
|
595
|
+
const pattern = p.pattern.trim();
|
|
596
|
+
if (!pattern) return null;
|
|
597
|
+
return { pattern, ...(p.regex ? { regex: true } : {}) };
|
|
598
|
+
})
|
|
599
|
+
.filter((item): item is PatternConfig => item !== null);
|
|
600
|
+
|
|
601
|
+
updateRule((current) => ({
|
|
602
|
+
...current,
|
|
603
|
+
allowedPatterns: patterns.length > 0 ? patterns : undefined,
|
|
604
|
+
}));
|
|
605
|
+
},
|
|
606
|
+
onDone: () =>
|
|
607
|
+
done(`${getRule()?.allowedPatterns?.length ?? 0} items`),
|
|
608
|
+
});
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
id: "blockMessage",
|
|
613
|
+
type: "text",
|
|
614
|
+
label: "Block message",
|
|
615
|
+
description: "Custom block message ({file} supported)",
|
|
616
|
+
getValue: () => getRule()?.blockMessage?.trim() || "",
|
|
617
|
+
setValue: (value) => {
|
|
618
|
+
const next = value.trim();
|
|
619
|
+
updateRule((rule) => ({ ...rule, blockMessage: next || undefined }));
|
|
620
|
+
},
|
|
621
|
+
emptyValueText: "(default)",
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
id: "delete",
|
|
625
|
+
type: "action",
|
|
626
|
+
label: "Delete rule",
|
|
627
|
+
description: "Remove this rule",
|
|
628
|
+
getValue: () => "danger",
|
|
629
|
+
onConfirm: () => {
|
|
630
|
+
deleteRule();
|
|
631
|
+
},
|
|
632
|
+
confirmMessage: "Delete this rule? This cannot be undone.",
|
|
633
|
+
},
|
|
634
|
+
];
|
|
635
|
+
|
|
636
|
+
return new SettingsDetailEditor({
|
|
637
|
+
title: () => {
|
|
638
|
+
const rule = getRule();
|
|
639
|
+
const title = rule?.name?.trim() || rule?.id || `Policy ${index + 1}`;
|
|
640
|
+
return `Policy: ${title}`;
|
|
641
|
+
},
|
|
642
|
+
fields,
|
|
643
|
+
theme,
|
|
644
|
+
onDone,
|
|
645
|
+
getDoneSummary: () => {
|
|
646
|
+
const rule = getRule();
|
|
647
|
+
if (!rule) return "deleted";
|
|
648
|
+
return `${rule.protection}, ${rule.enabled === false ? "disabled" : "enabled"}`;
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
33
653
|
export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
34
654
|
registerSettingsCommand<GuardrailsConfig, ResolvedConfig>(pi, {
|
|
35
655
|
commandName: "guardrails:settings",
|
|
@@ -37,46 +657,86 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
37
657
|
configStore: configLoader,
|
|
38
658
|
buildSections: (
|
|
39
659
|
tabConfig: GuardrailsConfig | null,
|
|
40
|
-
|
|
41
|
-
{ setDraft },
|
|
660
|
+
_resolved: ResolvedConfig,
|
|
661
|
+
{ setDraft, theme },
|
|
42
662
|
): SettingsSection[] => {
|
|
43
|
-
const settingsTheme =
|
|
44
|
-
|
|
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
|
+
}
|
|
45
670
|
|
|
46
671
|
function count(id: string): string {
|
|
47
672
|
const val =
|
|
48
|
-
(getNestedValue(
|
|
49
|
-
(getNestedValue(resolved, id) as unknown[]) ??
|
|
50
|
-
[];
|
|
673
|
+
(getNestedValue(scopedConfig, id) as unknown[] | undefined) ?? [];
|
|
51
674
|
return `${val.length} items`;
|
|
52
675
|
}
|
|
53
676
|
|
|
54
677
|
function applyDraft(id: string, value: unknown): void {
|
|
55
|
-
const updated = structuredClone(
|
|
678
|
+
const updated = structuredClone(scopedConfig);
|
|
56
679
|
setNestedValue(updated, id, value);
|
|
57
|
-
|
|
680
|
+
commitDraft(updated);
|
|
58
681
|
}
|
|
59
682
|
|
|
60
|
-
|
|
683
|
+
function getPolicyRules(): PolicyRule[] {
|
|
684
|
+
return scopedConfig.policies?.rules?.map((r) => ({ ...r })) ?? [];
|
|
685
|
+
}
|
|
61
686
|
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
[];
|
|
68
|
-
let latest = [...items];
|
|
69
|
-
return new ArrayEditor({
|
|
70
|
-
label,
|
|
71
|
-
items: [...items],
|
|
72
|
-
theme: settingsTheme,
|
|
73
|
-
onSave: (newItems) => {
|
|
74
|
-
latest = newItems;
|
|
75
|
-
applyDraft(id, newItems);
|
|
76
|
-
},
|
|
77
|
-
onDone: () => submenuDone(`${latest.length} items`),
|
|
78
|
-
});
|
|
687
|
+
function setPolicyRules(rules: PolicyRule[]): void {
|
|
688
|
+
const updated = structuredClone(scopedConfig);
|
|
689
|
+
updated.policies = {
|
|
690
|
+
...(updated.policies ?? {}),
|
|
691
|
+
rules,
|
|
79
692
|
};
|
|
693
|
+
commitDraft(updated);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function updateRule(
|
|
697
|
+
index: number,
|
|
698
|
+
updater: (rule: PolicyRule) => PolicyRule,
|
|
699
|
+
): void {
|
|
700
|
+
const rules = getPolicyRules();
|
|
701
|
+
const existing = rules[index];
|
|
702
|
+
if (!existing) return;
|
|
703
|
+
rules[index] = updater(existing);
|
|
704
|
+
setPolicyRules(rules);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function deleteRule(index: number): void {
|
|
708
|
+
const rules = getPolicyRules();
|
|
709
|
+
if (!rules[index]) return;
|
|
710
|
+
rules.splice(index, 1);
|
|
711
|
+
setPolicyRules(rules);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function addRule(draft: NewPolicyDraft): number | null {
|
|
715
|
+
const normalizedName = draft.name.trim();
|
|
716
|
+
if (!normalizedName || draft.patterns.length === 0) return null;
|
|
717
|
+
|
|
718
|
+
const rules = getPolicyRules();
|
|
719
|
+
const baseId = toKebabCase(draft.id || normalizedName) || "policy";
|
|
720
|
+
const existingIds = new Set(rules.map((rule) => rule.id));
|
|
721
|
+
|
|
722
|
+
let id = baseId;
|
|
723
|
+
let i = 2;
|
|
724
|
+
while (existingIds.has(id)) {
|
|
725
|
+
id = `${baseId}-${i}`;
|
|
726
|
+
i++;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
rules.push({
|
|
730
|
+
id,
|
|
731
|
+
name: normalizedName,
|
|
732
|
+
description: "",
|
|
733
|
+
patterns: draft.patterns,
|
|
734
|
+
protection: draft.protection,
|
|
735
|
+
onlyIfExists: true,
|
|
736
|
+
enabled: true,
|
|
737
|
+
});
|
|
738
|
+
setPolicyRules(rules);
|
|
739
|
+
return rules.length - 1;
|
|
80
740
|
}
|
|
81
741
|
|
|
82
742
|
function patternSubmenu(
|
|
@@ -86,11 +746,9 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
86
746
|
) {
|
|
87
747
|
return (_val: string, submenuDone: (v?: string) => void) => {
|
|
88
748
|
const items =
|
|
89
|
-
(getNestedValue(
|
|
749
|
+
(getNestedValue(scopedConfig, id) as
|
|
90
750
|
| DangerousPattern[]
|
|
91
|
-
| undefined) ??
|
|
92
|
-
(getNestedValue(resolved, id) as DangerousPattern[]) ??
|
|
93
|
-
[];
|
|
751
|
+
| undefined) ?? [];
|
|
94
752
|
let latestCount = items.length;
|
|
95
753
|
return new PatternEditor({
|
|
96
754
|
label,
|
|
@@ -113,10 +771,7 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
113
771
|
) {
|
|
114
772
|
return (_val: string, submenuDone: (v?: string) => void) => {
|
|
115
773
|
const currentItems =
|
|
116
|
-
(getNestedValue(
|
|
117
|
-
| PatternConfig[]
|
|
118
|
-
| undefined) ??
|
|
119
|
-
(getNestedValue(resolved, id) as PatternConfig[]) ??
|
|
774
|
+
(getNestedValue(scopedConfig, id) as PatternConfig[] | undefined) ??
|
|
120
775
|
[];
|
|
121
776
|
const items = currentItems.map((p) => ({
|
|
122
777
|
pattern: p.pattern,
|
|
@@ -131,11 +786,15 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
131
786
|
context,
|
|
132
787
|
onSave: (newItems) => {
|
|
133
788
|
latestCount = newItems.length;
|
|
134
|
-
const configs: PatternConfig[] = newItems
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
789
|
+
const configs: PatternConfig[] = newItems
|
|
790
|
+
.map((p) => {
|
|
791
|
+
const pattern = p.pattern.trim();
|
|
792
|
+
if (!pattern) return null;
|
|
793
|
+
const cfg: PatternConfig = { pattern };
|
|
794
|
+
if (p.regex) cfg.regex = true;
|
|
795
|
+
return cfg;
|
|
796
|
+
})
|
|
797
|
+
.filter((item): item is PatternConfig => item !== null);
|
|
139
798
|
applyDraft(id, configs);
|
|
140
799
|
},
|
|
141
800
|
onDone: () => submenuDone(`${latestCount} items`),
|
|
@@ -143,83 +802,100 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
143
802
|
};
|
|
144
803
|
}
|
|
145
804
|
|
|
146
|
-
|
|
805
|
+
function hasExplainModelOverride(): boolean {
|
|
806
|
+
return scopedConfig.permissionGate?.explainModel !== undefined;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function getExplainModel(): string {
|
|
810
|
+
return scopedConfig.permissionGate?.explainModel?.trim() ?? "";
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function hasExplainTimeoutOverride(): boolean {
|
|
814
|
+
return scopedConfig.permissionGate?.explainTimeout !== undefined;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function getExplainTimeout(): number | null {
|
|
818
|
+
return scopedConfig.permissionGate?.explainTimeout ?? null;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const featureItems = (Object.keys(FEATURE_UI) as FeatureKey[])
|
|
822
|
+
.filter((key) => key !== "policies")
|
|
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
|
+
});
|
|
838
|
+
|
|
839
|
+
const policyRules = getPolicyRules();
|
|
147
840
|
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
841
|
+
const openPolicyEditor = (
|
|
842
|
+
index: number,
|
|
843
|
+
submenuDone: (v?: string) => void,
|
|
844
|
+
): Component =>
|
|
845
|
+
createPolicyRuleEditor({
|
|
846
|
+
index,
|
|
847
|
+
theme: settingsTheme,
|
|
848
|
+
getRule: () => getPolicyRules()[index],
|
|
849
|
+
updateRule: (updater) => updateRule(index, updater),
|
|
850
|
+
deleteRule: () => deleteRule(index),
|
|
851
|
+
onDone: submenuDone,
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
const policyItems: SettingItem[] = [
|
|
855
|
+
{
|
|
856
|
+
id: "features.policies",
|
|
857
|
+
label: " Enabled",
|
|
858
|
+
description: FEATURE_UI.policies.description,
|
|
153
859
|
currentValue:
|
|
154
|
-
|
|
155
|
-
? "
|
|
156
|
-
:
|
|
860
|
+
scopedConfig.features?.policies === undefined
|
|
861
|
+
? "(inherited)"
|
|
862
|
+
: scopedConfig.features.policies
|
|
863
|
+
? "enabled"
|
|
864
|
+
: "disabled",
|
|
157
865
|
values: ["enabled", "disabled"],
|
|
866
|
+
},
|
|
867
|
+
...policyRules.map((rule, index) => {
|
|
868
|
+
const label = rule.name?.trim() || rule.id || `Policy ${index + 1}`;
|
|
869
|
+
return {
|
|
870
|
+
id: `policies.rules.${index}`,
|
|
871
|
+
label: ` ${label}`,
|
|
872
|
+
description: rule.description?.trim() || "No description",
|
|
873
|
+
currentValue: `${rule.protection}, ${rule.enabled === false ? "disabled" : "enabled"}`,
|
|
874
|
+
submenu: (_val: string, submenuDone: (v?: string) => void) =>
|
|
875
|
+
openPolicyEditor(index, submenuDone),
|
|
876
|
+
};
|
|
158
877
|
}),
|
|
159
|
-
|
|
878
|
+
];
|
|
879
|
+
|
|
880
|
+
policyItems.push({
|
|
881
|
+
id: "policies.addRule",
|
|
882
|
+
label: " + Add policy",
|
|
883
|
+
description: "Open wizard to create policy",
|
|
884
|
+
currentValue: "",
|
|
885
|
+
submenu: (_val: string, submenuDone: (v?: string) => void) =>
|
|
886
|
+
new AddRuleSubmenu(
|
|
887
|
+
settingsTheme,
|
|
888
|
+
addRule,
|
|
889
|
+
(index, done) => openPolicyEditor(index, done),
|
|
890
|
+
submenuDone,
|
|
891
|
+
),
|
|
892
|
+
});
|
|
160
893
|
|
|
161
894
|
return [
|
|
162
895
|
{ label: "Features", items: featureItems },
|
|
163
896
|
{
|
|
164
|
-
label:
|
|
165
|
-
items:
|
|
166
|
-
{
|
|
167
|
-
id: "envFiles.onlyBlockIfExists",
|
|
168
|
-
label: "Only block existing files",
|
|
169
|
-
description:
|
|
170
|
-
"Only block .env file access if the file exists on disk",
|
|
171
|
-
currentValue:
|
|
172
|
-
(tabConfig?.envFiles?.onlyBlockIfExists ??
|
|
173
|
-
resolved.envFiles.onlyBlockIfExists)
|
|
174
|
-
? "on"
|
|
175
|
-
: "off",
|
|
176
|
-
values: ["on", "off"],
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
id: "envFiles.protectedPatterns",
|
|
180
|
-
label: "Protected patterns",
|
|
181
|
-
description: "Patterns for files to protect (e.g. .env.local)",
|
|
182
|
-
currentValue: count("envFiles.protectedPatterns"),
|
|
183
|
-
submenu: patternConfigSubmenu(
|
|
184
|
-
"envFiles.protectedPatterns",
|
|
185
|
-
"Protected Patterns",
|
|
186
|
-
"file",
|
|
187
|
-
),
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
id: "envFiles.allowedPatterns",
|
|
191
|
-
label: "Allowed patterns",
|
|
192
|
-
description: "Patterns for exceptions (e.g. .env.example)",
|
|
193
|
-
currentValue: count("envFiles.allowedPatterns"),
|
|
194
|
-
submenu: patternConfigSubmenu(
|
|
195
|
-
"envFiles.allowedPatterns",
|
|
196
|
-
"Allowed Patterns",
|
|
197
|
-
"file",
|
|
198
|
-
),
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
id: "envFiles.protectedDirectories",
|
|
202
|
-
label: "Protected directories",
|
|
203
|
-
description: "Patterns for directories to protect",
|
|
204
|
-
currentValue: count("envFiles.protectedDirectories"),
|
|
205
|
-
submenu: patternConfigSubmenu(
|
|
206
|
-
"envFiles.protectedDirectories",
|
|
207
|
-
"Protected Directories",
|
|
208
|
-
"file",
|
|
209
|
-
),
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
id: "envFiles.protectedTools",
|
|
213
|
-
label: "Protected tools",
|
|
214
|
-
description:
|
|
215
|
-
"Tools to intercept (read, write, edit, bash, grep, find, ls)",
|
|
216
|
-
currentValue: count("envFiles.protectedTools"),
|
|
217
|
-
submenu: stringArraySubmenu(
|
|
218
|
-
"envFiles.protectedTools",
|
|
219
|
-
"Protected Tools",
|
|
220
|
-
),
|
|
221
|
-
},
|
|
222
|
-
],
|
|
897
|
+
label: `Policies (${policyRules.length})`,
|
|
898
|
+
items: policyItems,
|
|
223
899
|
},
|
|
224
900
|
{
|
|
225
901
|
label: "Permission Gate",
|
|
@@ -230,10 +906,11 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
230
906
|
description:
|
|
231
907
|
"Show confirmation dialog for dangerous commands (if off, just warns)",
|
|
232
908
|
currentValue:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
909
|
+
scopedConfig.permissionGate?.requireConfirmation === undefined
|
|
910
|
+
? "(inherited)"
|
|
911
|
+
: scopedConfig.permissionGate.requireConfirmation
|
|
912
|
+
? "on"
|
|
913
|
+
: "off",
|
|
237
914
|
values: ["on", "off"],
|
|
238
915
|
},
|
|
239
916
|
{
|
|
@@ -270,9 +947,130 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
270
947
|
"command",
|
|
271
948
|
),
|
|
272
949
|
},
|
|
950
|
+
{
|
|
951
|
+
id: "permissionGate.explainCommands",
|
|
952
|
+
label: "Explain commands",
|
|
953
|
+
description:
|
|
954
|
+
"Call an LLM to explain dangerous commands in the confirmation dialog",
|
|
955
|
+
currentValue:
|
|
956
|
+
scopedConfig.permissionGate?.explainCommands === undefined
|
|
957
|
+
? "(inherited)"
|
|
958
|
+
: scopedConfig.permissionGate.explainCommands
|
|
959
|
+
? "on"
|
|
960
|
+
: "off",
|
|
961
|
+
values: ["on", "off"],
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
id: "permissionGate.explainModel",
|
|
965
|
+
label: "Explain model",
|
|
966
|
+
description: "Model spec in provider/model-id format",
|
|
967
|
+
currentValue: hasExplainModelOverride()
|
|
968
|
+
? getExplainModel() || "(not set)"
|
|
969
|
+
: "(inherited)",
|
|
970
|
+
submenu: (_val: string, submenuDone: (v?: string) => void) =>
|
|
971
|
+
new SettingsDetailEditor({
|
|
972
|
+
title: "Explain Commands: Model",
|
|
973
|
+
theme: settingsTheme,
|
|
974
|
+
onDone: submenuDone,
|
|
975
|
+
getDoneSummary: () => getExplainModel() || "(not set)",
|
|
976
|
+
fields: [
|
|
977
|
+
{
|
|
978
|
+
id: "permissionGate.explainModel",
|
|
979
|
+
type: "text",
|
|
980
|
+
label: "Model",
|
|
981
|
+
description: "Format: provider/model-id",
|
|
982
|
+
getValue: getExplainModel,
|
|
983
|
+
setValue: (value) => {
|
|
984
|
+
const model = value.trim();
|
|
985
|
+
applyDraft(
|
|
986
|
+
"permissionGate.explainModel",
|
|
987
|
+
model || undefined,
|
|
988
|
+
);
|
|
989
|
+
},
|
|
990
|
+
emptyValueText: "(not set)",
|
|
991
|
+
},
|
|
992
|
+
],
|
|
993
|
+
}),
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
id: "permissionGate.explainTimeout",
|
|
997
|
+
label: "Explain timeout",
|
|
998
|
+
description: "Timeout for LLM explanation in milliseconds",
|
|
999
|
+
currentValue: hasExplainTimeoutOverride()
|
|
1000
|
+
? `${getExplainTimeout()}ms`
|
|
1001
|
+
: "(inherited)",
|
|
1002
|
+
submenu: (_val: string, submenuDone: (v?: string) => void) =>
|
|
1003
|
+
new SettingsDetailEditor({
|
|
1004
|
+
title: "Explain Commands: Timeout",
|
|
1005
|
+
theme: settingsTheme,
|
|
1006
|
+
onDone: submenuDone,
|
|
1007
|
+
getDoneSummary: () => {
|
|
1008
|
+
const timeout = getExplainTimeout();
|
|
1009
|
+
return timeout === null ? "(not set)" : `${timeout}ms`;
|
|
1010
|
+
},
|
|
1011
|
+
fields: [
|
|
1012
|
+
{
|
|
1013
|
+
id: "permissionGate.explainTimeout",
|
|
1014
|
+
type: "text",
|
|
1015
|
+
label: "Timeout (ms)",
|
|
1016
|
+
description: "Abort explanation call after this many ms",
|
|
1017
|
+
getValue: () => {
|
|
1018
|
+
const timeout = getExplainTimeout();
|
|
1019
|
+
return timeout === null ? "" : String(timeout);
|
|
1020
|
+
},
|
|
1021
|
+
setValue: (value) => {
|
|
1022
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
1023
|
+
if (Number.isNaN(parsed) || parsed < 1) return;
|
|
1024
|
+
applyDraft("permissionGate.explainTimeout", parsed);
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
],
|
|
1028
|
+
}),
|
|
1029
|
+
},
|
|
273
1030
|
],
|
|
274
1031
|
},
|
|
275
1032
|
];
|
|
276
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
|
+
],
|
|
277
1075
|
});
|
|
278
1076
|
}
|