@aliou/pi-guardrails 0.7.7 → 0.8.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 +489 -91
- 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,25 @@
|
|
|
1
1
|
import {
|
|
2
|
-
ArrayEditor,
|
|
3
2
|
getNestedValue,
|
|
4
3
|
registerSettingsCommand,
|
|
4
|
+
SettingsDetailEditor,
|
|
5
|
+
type SettingsDetailField,
|
|
5
6
|
type SettingsSection,
|
|
6
7
|
setNestedValue,
|
|
7
8
|
} from "@aliou/pi-utils-settings";
|
|
8
9
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
10
|
import { getSettingsListTheme } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import {
|
|
12
|
+
type Component,
|
|
13
|
+
Input,
|
|
14
|
+
type SettingItem,
|
|
15
|
+
type SettingsListTheme,
|
|
16
|
+
} from "@mariozechner/pi-tui";
|
|
10
17
|
import { PatternEditor } from "../components/pattern-editor";
|
|
11
18
|
import type {
|
|
12
19
|
DangerousPattern,
|
|
13
20
|
GuardrailsConfig,
|
|
14
21
|
PatternConfig,
|
|
22
|
+
PolicyRule,
|
|
15
23
|
ResolvedConfig,
|
|
16
24
|
} from "../config";
|
|
17
25
|
import { configLoader } from "../config";
|
|
@@ -19,9 +27,9 @@ import { configLoader } from "../config";
|
|
|
19
27
|
type FeatureKey = keyof ResolvedConfig["features"];
|
|
20
28
|
|
|
21
29
|
const FEATURE_UI: Record<FeatureKey, { label: string; description: string }> = {
|
|
22
|
-
|
|
23
|
-
label: "
|
|
24
|
-
description: "Block access
|
|
30
|
+
policies: {
|
|
31
|
+
label: "Policies",
|
|
32
|
+
description: "Block or limit file access using named policy rules",
|
|
25
33
|
},
|
|
26
34
|
permissionGate: {
|
|
27
35
|
label: "Permission gate",
|
|
@@ -30,6 +38,277 @@ const FEATURE_UI: Record<FeatureKey, { label: string; description: string }> = {
|
|
|
30
38
|
},
|
|
31
39
|
};
|
|
32
40
|
|
|
41
|
+
function toKebabCase(input: string): string {
|
|
42
|
+
return input
|
|
43
|
+
.trim()
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
46
|
+
.replace(/^-+|-+$/g, "");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class AddRuleSubmenu implements Component {
|
|
50
|
+
private readonly onCreate: (name: string) => number | null;
|
|
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();
|
|
58
|
+
private activeEditor: Component | null = null;
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
theme: SettingsListTheme,
|
|
62
|
+
onCreate: (name: string) => number | null,
|
|
63
|
+
openEditor: (index: number, done: (value?: string) => void) => Component,
|
|
64
|
+
onDone: (value?: string) => void,
|
|
65
|
+
) {
|
|
66
|
+
this.theme = theme;
|
|
67
|
+
this.onCreate = onCreate;
|
|
68
|
+
this.openEditor = openEditor;
|
|
69
|
+
this.onDone = onDone;
|
|
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
|
+
});
|
|
80
|
+
};
|
|
81
|
+
this.nameInput.onEscape = () => this.onDone();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
invalidate() {
|
|
85
|
+
this.activeEditor?.invalidate?.();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
render(width: number): string[] {
|
|
89
|
+
if (this.activeEditor) {
|
|
90
|
+
return this.activeEditor.render(width);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return [
|
|
94
|
+
this.theme.label("+ Add policy", true),
|
|
95
|
+
"",
|
|
96
|
+
this.theme.hint(" Enter policy name:"),
|
|
97
|
+
...this.nameInput.render(width - 2).map((line) => ` ${line}`),
|
|
98
|
+
"",
|
|
99
|
+
this.theme.hint(" Enter: create + edit · Esc: back"),
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
handleInput(data: string): void {
|
|
104
|
+
if (this.activeEditor) {
|
|
105
|
+
this.activeEditor.handleInput?.(data);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.nameInput.handleInput(data);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function createPolicyRuleEditor(options: {
|
|
114
|
+
index: number;
|
|
115
|
+
theme: SettingsListTheme;
|
|
116
|
+
getRule: () => PolicyRule | undefined;
|
|
117
|
+
updateRule: (updater: (rule: PolicyRule) => PolicyRule) => void;
|
|
118
|
+
deleteRule: () => void;
|
|
119
|
+
onDone: (value?: string) => void;
|
|
120
|
+
}): SettingsDetailEditor {
|
|
121
|
+
const { index, theme, getRule, updateRule, deleteRule, onDone } = options;
|
|
122
|
+
|
|
123
|
+
const fields: SettingsDetailField[] = [
|
|
124
|
+
{
|
|
125
|
+
id: "name",
|
|
126
|
+
type: "text",
|
|
127
|
+
label: "Name",
|
|
128
|
+
description: "Display name shown in settings",
|
|
129
|
+
getValue: () => getRule()?.name?.trim() || "",
|
|
130
|
+
setValue: (value) => {
|
|
131
|
+
const next = value.trim();
|
|
132
|
+
updateRule((rule) => ({ ...rule, name: next || undefined }));
|
|
133
|
+
},
|
|
134
|
+
emptyValueText: "(uses id)",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "id",
|
|
138
|
+
type: "text",
|
|
139
|
+
label: "ID",
|
|
140
|
+
description: "Stable identifier used for overrides across scopes",
|
|
141
|
+
getValue: () => getRule()?.id ?? "",
|
|
142
|
+
setValue: (value) => {
|
|
143
|
+
const next = value.trim();
|
|
144
|
+
if (!next) return;
|
|
145
|
+
updateRule((rule) => ({ ...rule, id: next }));
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "description",
|
|
150
|
+
type: "text",
|
|
151
|
+
label: "Description",
|
|
152
|
+
description: "Human-readable explanation",
|
|
153
|
+
getValue: () => getRule()?.description?.trim() || "",
|
|
154
|
+
setValue: (value) => {
|
|
155
|
+
const next = value.trim();
|
|
156
|
+
updateRule((rule) => ({ ...rule, description: next || undefined }));
|
|
157
|
+
},
|
|
158
|
+
emptyValueText: "(empty)",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "protection",
|
|
162
|
+
type: "enum",
|
|
163
|
+
label: "Protection",
|
|
164
|
+
description: "noAccess | readOnly | none",
|
|
165
|
+
getValue: () => getRule()?.protection ?? "readOnly",
|
|
166
|
+
setValue: (value) => {
|
|
167
|
+
if (value !== "noAccess" && value !== "readOnly" && value !== "none") {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
updateRule((rule) => ({ ...rule, protection: value }));
|
|
171
|
+
},
|
|
172
|
+
options: ["noAccess", "readOnly", "none"],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "enabled",
|
|
176
|
+
type: "boolean",
|
|
177
|
+
label: "Enabled",
|
|
178
|
+
description: "Turn this policy on/off",
|
|
179
|
+
getValue: () => getRule()?.enabled !== false,
|
|
180
|
+
setValue: (value) => {
|
|
181
|
+
updateRule((rule) => ({ ...rule, enabled: value }));
|
|
182
|
+
},
|
|
183
|
+
trueLabel: "on",
|
|
184
|
+
falseLabel: "off",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: "onlyIfExists",
|
|
188
|
+
type: "boolean",
|
|
189
|
+
label: "Only if exists",
|
|
190
|
+
description: "Only block when file exists on disk",
|
|
191
|
+
getValue: () => getRule()?.onlyIfExists !== false,
|
|
192
|
+
setValue: (value) => {
|
|
193
|
+
updateRule((rule) => ({ ...rule, onlyIfExists: value }));
|
|
194
|
+
},
|
|
195
|
+
trueLabel: "on",
|
|
196
|
+
falseLabel: "off",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "patterns",
|
|
200
|
+
type: "submenu",
|
|
201
|
+
label: "Patterns",
|
|
202
|
+
description: "Files protected by this policy",
|
|
203
|
+
getValue: () => `${getRule()?.patterns?.length ?? 0} items`,
|
|
204
|
+
submenu: (done) => {
|
|
205
|
+
const rule = getRule();
|
|
206
|
+
const items = (rule?.patterns ?? []).map((p) => ({
|
|
207
|
+
pattern: p.pattern,
|
|
208
|
+
description: p.pattern,
|
|
209
|
+
regex: p.regex,
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
return new PatternEditor({
|
|
213
|
+
label: "Policy patterns",
|
|
214
|
+
items,
|
|
215
|
+
theme,
|
|
216
|
+
context: "file",
|
|
217
|
+
onSave: (newItems) => {
|
|
218
|
+
const patterns: PatternConfig[] = newItems
|
|
219
|
+
.map((p) => {
|
|
220
|
+
const pattern = p.pattern.trim();
|
|
221
|
+
if (!pattern) return null;
|
|
222
|
+
return { pattern, ...(p.regex ? { regex: true } : {}) };
|
|
223
|
+
})
|
|
224
|
+
.filter((item): item is PatternConfig => item !== null);
|
|
225
|
+
|
|
226
|
+
updateRule((current) => ({ ...current, patterns }));
|
|
227
|
+
},
|
|
228
|
+
onDone: () => done(`${getRule()?.patterns?.length ?? 0} items`),
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: "allowedPatterns",
|
|
234
|
+
type: "submenu",
|
|
235
|
+
label: "Allowed patterns",
|
|
236
|
+
description: "Exceptions",
|
|
237
|
+
getValue: () => `${getRule()?.allowedPatterns?.length ?? 0} items`,
|
|
238
|
+
submenu: (done) => {
|
|
239
|
+
const rule = getRule();
|
|
240
|
+
const items = (rule?.allowedPatterns ?? []).map((p) => ({
|
|
241
|
+
pattern: p.pattern,
|
|
242
|
+
description: p.pattern,
|
|
243
|
+
regex: p.regex,
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
return new PatternEditor({
|
|
247
|
+
label: "Policy allowed patterns",
|
|
248
|
+
items,
|
|
249
|
+
theme,
|
|
250
|
+
context: "file",
|
|
251
|
+
onSave: (newItems) => {
|
|
252
|
+
const patterns: PatternConfig[] = newItems
|
|
253
|
+
.map((p) => {
|
|
254
|
+
const pattern = p.pattern.trim();
|
|
255
|
+
if (!pattern) return null;
|
|
256
|
+
return { pattern, ...(p.regex ? { regex: true } : {}) };
|
|
257
|
+
})
|
|
258
|
+
.filter((item): item is PatternConfig => item !== null);
|
|
259
|
+
|
|
260
|
+
updateRule((current) => ({
|
|
261
|
+
...current,
|
|
262
|
+
allowedPatterns: patterns.length > 0 ? patterns : undefined,
|
|
263
|
+
}));
|
|
264
|
+
},
|
|
265
|
+
onDone: () =>
|
|
266
|
+
done(`${getRule()?.allowedPatterns?.length ?? 0} items`),
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "blockMessage",
|
|
272
|
+
type: "text",
|
|
273
|
+
label: "Block message",
|
|
274
|
+
description: "Custom block message ({file} supported)",
|
|
275
|
+
getValue: () => getRule()?.blockMessage?.trim() || "",
|
|
276
|
+
setValue: (value) => {
|
|
277
|
+
const next = value.trim();
|
|
278
|
+
updateRule((rule) => ({ ...rule, blockMessage: next || undefined }));
|
|
279
|
+
},
|
|
280
|
+
emptyValueText: "(default)",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
id: "delete",
|
|
284
|
+
type: "action",
|
|
285
|
+
label: "Delete rule",
|
|
286
|
+
description: "Remove this rule",
|
|
287
|
+
getValue: () => "danger",
|
|
288
|
+
onConfirm: () => {
|
|
289
|
+
deleteRule();
|
|
290
|
+
},
|
|
291
|
+
confirmMessage: "Delete this rule? This cannot be undone.",
|
|
292
|
+
},
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
return new SettingsDetailEditor({
|
|
296
|
+
title: () => {
|
|
297
|
+
const rule = getRule();
|
|
298
|
+
const title = rule?.name?.trim() || rule?.id || `Policy ${index + 1}`;
|
|
299
|
+
return `Policy: ${title}`;
|
|
300
|
+
},
|
|
301
|
+
fields,
|
|
302
|
+
theme,
|
|
303
|
+
onDone,
|
|
304
|
+
getDoneSummary: () => {
|
|
305
|
+
const rule = getRule();
|
|
306
|
+
if (!rule) return "deleted";
|
|
307
|
+
return `${rule.protection}, ${rule.enabled === false ? "disabled" : "enabled"}`;
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
33
312
|
export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
34
313
|
registerSettingsCommand<GuardrailsConfig, ResolvedConfig>(pi, {
|
|
35
314
|
commandName: "guardrails:settings",
|
|
@@ -41,7 +320,6 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
41
320
|
{ setDraft },
|
|
42
321
|
): SettingsSection[] => {
|
|
43
322
|
const settingsTheme = getSettingsListTheme();
|
|
44
|
-
// --- Helpers ---
|
|
45
323
|
|
|
46
324
|
function count(id: string): string {
|
|
47
325
|
const val =
|
|
@@ -57,26 +335,66 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
57
335
|
setDraft(updated);
|
|
58
336
|
}
|
|
59
337
|
|
|
60
|
-
|
|
338
|
+
function getPolicyRules(): PolicyRule[] {
|
|
339
|
+
return (
|
|
340
|
+
tabConfig?.policies?.rules?.map((r) => ({ ...r })) ??
|
|
341
|
+
resolved.policies.rules.map((r) => ({ ...r }))
|
|
342
|
+
);
|
|
343
|
+
}
|
|
61
344
|
|
|
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
|
-
});
|
|
345
|
+
function setPolicyRules(rules: PolicyRule[]): void {
|
|
346
|
+
const updated = structuredClone(tabConfig ?? {}) as GuardrailsConfig;
|
|
347
|
+
updated.policies = {
|
|
348
|
+
...(updated.policies ?? {}),
|
|
349
|
+
rules,
|
|
79
350
|
};
|
|
351
|
+
setDraft(updated);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function updateRule(
|
|
355
|
+
index: number,
|
|
356
|
+
updater: (rule: PolicyRule) => PolicyRule,
|
|
357
|
+
): void {
|
|
358
|
+
const rules = getPolicyRules();
|
|
359
|
+
const existing = rules[index];
|
|
360
|
+
if (!existing) return;
|
|
361
|
+
rules[index] = updater(existing);
|
|
362
|
+
setPolicyRules(rules);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function deleteRule(index: number): void {
|
|
366
|
+
const rules = getPolicyRules();
|
|
367
|
+
if (!rules[index]) return;
|
|
368
|
+
rules.splice(index, 1);
|
|
369
|
+
setPolicyRules(rules);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function addRule(name: string): number | null {
|
|
373
|
+
const normalizedName = name.trim();
|
|
374
|
+
if (!normalizedName) return null;
|
|
375
|
+
|
|
376
|
+
const rules = getPolicyRules();
|
|
377
|
+
const baseId = toKebabCase(normalizedName) || "policy";
|
|
378
|
+
const existingIds = new Set(rules.map((rule) => rule.id));
|
|
379
|
+
|
|
380
|
+
let id = baseId;
|
|
381
|
+
let i = 2;
|
|
382
|
+
while (existingIds.has(id)) {
|
|
383
|
+
id = `${baseId}-${i}`;
|
|
384
|
+
i++;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
rules.push({
|
|
388
|
+
id,
|
|
389
|
+
name: normalizedName,
|
|
390
|
+
description: "",
|
|
391
|
+
patterns: [{ pattern: "" }],
|
|
392
|
+
protection: "readOnly",
|
|
393
|
+
onlyIfExists: true,
|
|
394
|
+
enabled: true,
|
|
395
|
+
});
|
|
396
|
+
setPolicyRules(rules);
|
|
397
|
+
return rules.length - 1;
|
|
80
398
|
}
|
|
81
399
|
|
|
82
400
|
function patternSubmenu(
|
|
@@ -131,11 +449,15 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
131
449
|
context,
|
|
132
450
|
onSave: (newItems) => {
|
|
133
451
|
latestCount = newItems.length;
|
|
134
|
-
const configs: PatternConfig[] = newItems
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
452
|
+
const configs: PatternConfig[] = newItems
|
|
453
|
+
.map((p) => {
|
|
454
|
+
const pattern = p.pattern.trim();
|
|
455
|
+
if (!pattern) return null;
|
|
456
|
+
const cfg: PatternConfig = { pattern };
|
|
457
|
+
if (p.regex) cfg.regex = true;
|
|
458
|
+
return cfg;
|
|
459
|
+
})
|
|
460
|
+
.filter((item): item is PatternConfig => item !== null);
|
|
139
461
|
applyDraft(id, configs);
|
|
140
462
|
},
|
|
141
463
|
onDone: () => submenuDone(`${latestCount} items`),
|
|
@@ -143,10 +465,22 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
143
465
|
};
|
|
144
466
|
}
|
|
145
467
|
|
|
146
|
-
|
|
468
|
+
function getExplainModel(): string {
|
|
469
|
+
const model = tabConfig?.permissionGate?.explainModel;
|
|
470
|
+
if (model !== undefined) return model;
|
|
471
|
+
return resolved.permissionGate.explainModel ?? "";
|
|
472
|
+
}
|
|
147
473
|
|
|
148
|
-
|
|
149
|
-
|
|
474
|
+
function getExplainTimeout(): number {
|
|
475
|
+
return (
|
|
476
|
+
tabConfig?.permissionGate?.explainTimeout ??
|
|
477
|
+
resolved.permissionGate.explainTimeout
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const featureItems = (Object.keys(FEATURE_UI) as FeatureKey[])
|
|
482
|
+
.filter((key) => key !== "policies")
|
|
483
|
+
.map((key) => ({
|
|
150
484
|
id: `features.${key}`,
|
|
151
485
|
label: FEATURE_UI[key].label,
|
|
152
486
|
description: FEATURE_UI[key].description,
|
|
@@ -155,71 +489,66 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
155
489
|
? "enabled"
|
|
156
490
|
: "disabled",
|
|
157
491
|
values: ["enabled", "disabled"],
|
|
492
|
+
}));
|
|
493
|
+
|
|
494
|
+
const policyRules = getPolicyRules();
|
|
495
|
+
|
|
496
|
+
const openPolicyEditor = (
|
|
497
|
+
index: number,
|
|
498
|
+
submenuDone: (v?: string) => void,
|
|
499
|
+
): Component =>
|
|
500
|
+
createPolicyRuleEditor({
|
|
501
|
+
index,
|
|
502
|
+
theme: settingsTheme,
|
|
503
|
+
getRule: () => getPolicyRules()[index],
|
|
504
|
+
updateRule: (updater) => updateRule(index, updater),
|
|
505
|
+
deleteRule: () => deleteRule(index),
|
|
506
|
+
onDone: submenuDone,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const policyItems: SettingItem[] = [
|
|
510
|
+
{
|
|
511
|
+
id: "features.policies",
|
|
512
|
+
label: " Enabled",
|
|
513
|
+
description: FEATURE_UI.policies.description,
|
|
514
|
+
currentValue:
|
|
515
|
+
(tabConfig?.features?.policies ?? resolved.features.policies)
|
|
516
|
+
? "enabled"
|
|
517
|
+
: "disabled",
|
|
518
|
+
values: ["enabled", "disabled"],
|
|
519
|
+
},
|
|
520
|
+
...policyRules.map((rule, index) => {
|
|
521
|
+
const label = rule.name?.trim() || rule.id || `Policy ${index + 1}`;
|
|
522
|
+
return {
|
|
523
|
+
id: `policies.rules.${index}`,
|
|
524
|
+
label: ` ${label}`,
|
|
525
|
+
description: rule.description?.trim() || "No description",
|
|
526
|
+
currentValue: `${rule.protection}, ${rule.enabled === false ? "disabled" : "enabled"}`,
|
|
527
|
+
submenu: (_val: string, submenuDone: (v?: string) => void) =>
|
|
528
|
+
openPolicyEditor(index, submenuDone),
|
|
529
|
+
};
|
|
158
530
|
}),
|
|
159
|
-
|
|
531
|
+
];
|
|
532
|
+
|
|
533
|
+
policyItems.push({
|
|
534
|
+
id: "policies.addRule",
|
|
535
|
+
label: " + Add policy",
|
|
536
|
+
description: "Create policy, then open editor",
|
|
537
|
+
currentValue: "",
|
|
538
|
+
submenu: (_val: string, submenuDone: (v?: string) => void) =>
|
|
539
|
+
new AddRuleSubmenu(
|
|
540
|
+
settingsTheme,
|
|
541
|
+
addRule,
|
|
542
|
+
(index, done) => openPolicyEditor(index, done),
|
|
543
|
+
submenuDone,
|
|
544
|
+
),
|
|
545
|
+
});
|
|
160
546
|
|
|
161
547
|
return [
|
|
162
548
|
{ label: "Features", items: featureItems },
|
|
163
549
|
{
|
|
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
|
-
],
|
|
550
|
+
label: `Policies (${policyRules.length})`,
|
|
551
|
+
items: policyItems,
|
|
223
552
|
},
|
|
224
553
|
{
|
|
225
554
|
label: "Permission Gate",
|
|
@@ -270,6 +599,75 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
270
599
|
"command",
|
|
271
600
|
),
|
|
272
601
|
},
|
|
602
|
+
{
|
|
603
|
+
id: "permissionGate.explainCommands",
|
|
604
|
+
label: "Explain commands",
|
|
605
|
+
description:
|
|
606
|
+
"Call an LLM to explain dangerous commands in the confirmation dialog",
|
|
607
|
+
currentValue:
|
|
608
|
+
(tabConfig?.permissionGate?.explainCommands ??
|
|
609
|
+
resolved.permissionGate.explainCommands)
|
|
610
|
+
? "on"
|
|
611
|
+
: "off",
|
|
612
|
+
values: ["on", "off"],
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
id: "permissionGate.explainModel",
|
|
616
|
+
label: "Explain model",
|
|
617
|
+
description: "Model spec in provider/model-id format",
|
|
618
|
+
currentValue: getExplainModel() || "(not set)",
|
|
619
|
+
submenu: (_val: string, submenuDone: (v?: string) => void) =>
|
|
620
|
+
new SettingsDetailEditor({
|
|
621
|
+
title: "Explain Commands: Model",
|
|
622
|
+
theme: settingsTheme,
|
|
623
|
+
onDone: submenuDone,
|
|
624
|
+
getDoneSummary: () => getExplainModel() || "(not set)",
|
|
625
|
+
fields: [
|
|
626
|
+
{
|
|
627
|
+
id: "permissionGate.explainModel",
|
|
628
|
+
type: "text",
|
|
629
|
+
label: "Model",
|
|
630
|
+
description: "Format: provider/model-id",
|
|
631
|
+
getValue: getExplainModel,
|
|
632
|
+
setValue: (value) => {
|
|
633
|
+
const model = value.trim();
|
|
634
|
+
applyDraft(
|
|
635
|
+
"permissionGate.explainModel",
|
|
636
|
+
model || undefined,
|
|
637
|
+
);
|
|
638
|
+
},
|
|
639
|
+
emptyValueText: "(not set)",
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
}),
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
id: "permissionGate.explainTimeout",
|
|
646
|
+
label: "Explain timeout",
|
|
647
|
+
description: "Timeout for LLM explanation in milliseconds",
|
|
648
|
+
currentValue: `${getExplainTimeout()}ms`,
|
|
649
|
+
submenu: (_val: string, submenuDone: (v?: string) => void) =>
|
|
650
|
+
new SettingsDetailEditor({
|
|
651
|
+
title: "Explain Commands: Timeout",
|
|
652
|
+
theme: settingsTheme,
|
|
653
|
+
onDone: submenuDone,
|
|
654
|
+
getDoneSummary: () => `${getExplainTimeout()}ms`,
|
|
655
|
+
fields: [
|
|
656
|
+
{
|
|
657
|
+
id: "permissionGate.explainTimeout",
|
|
658
|
+
type: "text",
|
|
659
|
+
label: "Timeout (ms)",
|
|
660
|
+
description: "Abort explanation call after this many ms",
|
|
661
|
+
getValue: () => String(getExplainTimeout()),
|
|
662
|
+
setValue: (value) => {
|
|
663
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
664
|
+
if (Number.isNaN(parsed) || parsed < 1) return;
|
|
665
|
+
applyDraft("permissionGate.explainTimeout", parsed);
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
],
|
|
669
|
+
}),
|
|
670
|
+
},
|
|
273
671
|
],
|
|
274
672
|
},
|
|
275
673
|
];
|