@aliou/pi-guardrails 0.11.2 → 0.12.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 +72 -167
- package/extensions/guardrails/commands/examples/index.ts +520 -0
- package/extensions/guardrails/commands/onboarding/config.ts +54 -0
- package/{src/commands/onboarding-command.ts → extensions/guardrails/commands/onboarding/index.ts} +5 -31
- package/extensions/guardrails/commands/settings/add-rule-wizard.ts +267 -0
- package/extensions/guardrails/commands/settings/examples.ts +399 -0
- package/extensions/guardrails/commands/settings/index.ts +596 -0
- package/extensions/guardrails/commands/settings/path-list-editor.ts +158 -0
- package/extensions/guardrails/commands/settings/scope-picker-submenu.ts +69 -0
- package/extensions/guardrails/commands/settings/utils.ts +108 -0
- package/extensions/guardrails/components/onboarding-choice-step.ts +140 -0
- package/extensions/guardrails/components/onboarding-finish-step.ts +50 -0
- package/extensions/guardrails/components/onboarding-intro-step.ts +30 -0
- package/extensions/guardrails/components/onboarding-types.ts +10 -0
- package/extensions/guardrails/components/onboarding-wizard.ts +116 -0
- package/{src → extensions/guardrails}/components/pattern-editor.ts +11 -10
- package/extensions/guardrails/index.ts +106 -0
- package/extensions/guardrails/rules.test.ts +107 -0
- package/extensions/guardrails/rules.ts +119 -0
- package/extensions/guardrails/targets.test.ts +44 -0
- package/extensions/guardrails/targets.ts +66 -0
- package/extensions/path-access/grants.test.ts +47 -0
- package/extensions/path-access/grants.ts +68 -0
- package/extensions/path-access/index.ts +143 -0
- package/extensions/path-access/prompt.ts +196 -0
- package/extensions/path-access/rules.test.ts +46 -0
- package/extensions/path-access/rules.ts +37 -0
- package/extensions/path-access/targets.test.ts +40 -0
- package/extensions/path-access/targets.ts +19 -0
- package/extensions/permission-gate/grants.ts +21 -0
- package/extensions/permission-gate/index.ts +122 -0
- package/extensions/permission-gate/prompt.ts +222 -0
- package/extensions/permission-gate/rules.test.ts +132 -0
- package/extensions/permission-gate/rules.ts +72 -0
- package/package.json +18 -20
- package/schema.json +286 -0
- package/src/core/check.test.ts +169 -0
- package/src/core/check.ts +38 -0
- package/src/{hooks/permission-gate/dangerous-commands.test.ts → core/commands/dangerous.test.ts} +134 -2
- package/src/{hooks/permission-gate/dangerous-commands.ts → core/commands/dangerous.ts} +119 -1
- package/src/core/commands/index.ts +15 -0
- package/src/core/index.ts +13 -0
- package/src/{utils/path-access.test.ts → core/paths/access.test.ts} +1 -5
- package/src/core/paths/index.ts +14 -0
- package/src/{utils → core/shell}/command-args.test.ts +31 -20
- package/src/core/shell/index.ts +2 -0
- package/src/core/types.ts +55 -0
- package/src/shared/config/defaults.ts +118 -0
- package/src/shared/config/index.ts +17 -0
- package/src/shared/config/loader.ts +64 -0
- package/src/shared/config/migration/001-v0-format-upgrade.ts +107 -0
- package/src/shared/config/migration/002-strip-toolchain-fields.ts +39 -0
- package/src/shared/config/migration/003-strip-command-explainer-fields.ts +42 -0
- package/src/shared/config/migration/004-env-files-to-policies.ts +87 -0
- package/src/shared/config/migration/005-normalize-allowed-paths.ts +43 -0
- package/src/shared/config/migration/006-apply-builtin-defaults.ts +19 -0
- package/src/shared/config/migration/007-mark-onboarding-done.ts +25 -0
- package/src/shared/config/migration/index.ts +44 -0
- package/src/shared/config/migration/version.ts +7 -0
- package/src/shared/config/types.ts +141 -0
- package/src/shared/events.ts +100 -0
- package/src/shared/index.ts +6 -0
- package/src/shared/matching.test.ts +86 -0
- package/src/{utils → shared}/matching.ts +4 -4
- package/src/{utils → shared/paths}/bash-paths.test.ts +11 -2
- package/src/{utils → shared/paths}/bash-paths.ts +4 -4
- package/src/shared/paths/index.ts +1 -0
- package/src/shared/warnings.ts +17 -0
- package/docs/defaults.md +0 -140
- package/docs/examples.md +0 -170
- package/src/commands/onboarding.ts +0 -390
- package/src/commands/settings-command.ts +0 -1616
- package/src/config.ts +0 -392
- package/src/hooks/index.ts +0 -11
- package/src/hooks/path-access.ts +0 -395
- package/src/hooks/permission-gate/index.test.ts +0 -332
- package/src/hooks/permission-gate/index.ts +0 -595
- package/src/hooks/policies.ts +0 -322
- package/src/index.ts +0 -96
- package/src/lib/executor.ts +0 -280
- package/src/lib/index.ts +0 -16
- package/src/lib/model-resolver.ts +0 -47
- package/src/lib/timing.ts +0 -42
- package/src/lib/types.ts +0 -115
- package/src/utils/events.ts +0 -32
- package/src/utils/migration.test.ts +0 -58
- package/src/utils/migration.ts +0 -340
- package/src/utils/warnings.ts +0 -7
- /package/src/{utils/path-access.ts → core/paths/access.ts} +0 -0
- /package/src/{utils → core/paths}/path.test.ts +0 -0
- /package/src/{utils → core/paths}/path.ts +0 -0
- /package/src/{utils/shell-utils.ts → core/shell/ast.ts} +0 -0
- /package/src/{utils → core/shell}/command-args.ts +0 -0
- /package/src/{utils/glob-expander.ts → shared/glob.ts} +0 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ConfigStore,
|
|
3
|
+
getNestedValue,
|
|
4
|
+
registerSettingsCommand,
|
|
5
|
+
type Scope,
|
|
6
|
+
SettingsDetailEditor,
|
|
7
|
+
type SettingsDetailField,
|
|
8
|
+
type SettingsSection,
|
|
9
|
+
} from "@aliou/pi-utils-settings";
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import type {
|
|
12
|
+
Component,
|
|
13
|
+
SettingItem,
|
|
14
|
+
SettingsListTheme,
|
|
15
|
+
} from "@earendil-works/pi-tui";
|
|
16
|
+
import type {
|
|
17
|
+
DangerousPattern,
|
|
18
|
+
GuardrailsConfig,
|
|
19
|
+
PatternConfig,
|
|
20
|
+
PolicyRule,
|
|
21
|
+
ResolvedConfig,
|
|
22
|
+
} from "../../../../src/shared/config";
|
|
23
|
+
import { configLoader } from "../../../../src/shared/config";
|
|
24
|
+
import type { GuardrailsFeatureId } from "../../../../src/shared/events";
|
|
25
|
+
import { PatternEditor } from "../../components/pattern-editor";
|
|
26
|
+
import { AddRuleSubmenu } from "./add-rule-wizard";
|
|
27
|
+
import { PathListEditor } from "./path-list-editor";
|
|
28
|
+
import {
|
|
29
|
+
addPolicyRuleDraft,
|
|
30
|
+
countItems,
|
|
31
|
+
deletePolicyRule,
|
|
32
|
+
getPolicyRules as getPolicyRulesFromConfig,
|
|
33
|
+
type NewPolicyRuleDraft,
|
|
34
|
+
setConfigValue,
|
|
35
|
+
updatePolicyRule,
|
|
36
|
+
} from "./utils";
|
|
37
|
+
|
|
38
|
+
const FEATURE_UI: Record<
|
|
39
|
+
GuardrailsFeatureId,
|
|
40
|
+
{ label: string; description: string }
|
|
41
|
+
> = {
|
|
42
|
+
policies: {
|
|
43
|
+
label: "Policies",
|
|
44
|
+
description: "Block or limit file access using named policy rules",
|
|
45
|
+
},
|
|
46
|
+
permissionGate: {
|
|
47
|
+
label: "Permission gate",
|
|
48
|
+
description:
|
|
49
|
+
"Prompt for confirmation on dangerous commands (rm -rf, sudo, etc.)",
|
|
50
|
+
},
|
|
51
|
+
pathAccess: {
|
|
52
|
+
label: "Path access",
|
|
53
|
+
description: "Restrict tool access to the current working directory",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function createPolicyRuleEditor(options: {
|
|
58
|
+
index: number;
|
|
59
|
+
theme: SettingsListTheme;
|
|
60
|
+
getRule: () => PolicyRule | undefined;
|
|
61
|
+
updateRule: (updater: (rule: PolicyRule) => PolicyRule) => void;
|
|
62
|
+
deleteRule: () => void;
|
|
63
|
+
onDone: (value?: string) => void;
|
|
64
|
+
}): SettingsDetailEditor {
|
|
65
|
+
const { index, theme, getRule, updateRule, deleteRule, onDone } = options;
|
|
66
|
+
|
|
67
|
+
const fields: SettingsDetailField[] = [
|
|
68
|
+
{
|
|
69
|
+
id: "name",
|
|
70
|
+
type: "text",
|
|
71
|
+
label: "Name",
|
|
72
|
+
description: "Display name shown in settings",
|
|
73
|
+
getValue: () => getRule()?.name?.trim() || "",
|
|
74
|
+
setValue: (value) => {
|
|
75
|
+
const next = value.trim();
|
|
76
|
+
updateRule((rule) => ({ ...rule, name: next || undefined }));
|
|
77
|
+
},
|
|
78
|
+
emptyValueText: "(uses id)",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "id",
|
|
82
|
+
type: "text",
|
|
83
|
+
label: "ID",
|
|
84
|
+
description: "Stable identifier used for overrides across scopes",
|
|
85
|
+
getValue: () => getRule()?.id ?? "",
|
|
86
|
+
setValue: (value) => {
|
|
87
|
+
const next = value.trim();
|
|
88
|
+
if (!next) return;
|
|
89
|
+
updateRule((rule) => ({ ...rule, id: next }));
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "description",
|
|
94
|
+
type: "text",
|
|
95
|
+
label: "Description",
|
|
96
|
+
description: "Human-readable explanation",
|
|
97
|
+
getValue: () => getRule()?.description?.trim() || "",
|
|
98
|
+
setValue: (value) => {
|
|
99
|
+
const next = value.trim();
|
|
100
|
+
updateRule((rule) => ({ ...rule, description: next || undefined }));
|
|
101
|
+
},
|
|
102
|
+
emptyValueText: "(empty)",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "protection",
|
|
106
|
+
type: "enum",
|
|
107
|
+
label: "Protection",
|
|
108
|
+
description: "noAccess | readOnly | none",
|
|
109
|
+
getValue: () => getRule()?.protection ?? "readOnly",
|
|
110
|
+
setValue: (value) => {
|
|
111
|
+
if (value !== "noAccess" && value !== "readOnly" && value !== "none") {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
updateRule((rule) => ({ ...rule, protection: value }));
|
|
115
|
+
},
|
|
116
|
+
options: ["noAccess", "readOnly", "none"],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "enabled",
|
|
120
|
+
type: "boolean",
|
|
121
|
+
label: "Enabled",
|
|
122
|
+
description: "Turn this policy on/off",
|
|
123
|
+
getValue: () => getRule()?.enabled !== false,
|
|
124
|
+
setValue: (value) => {
|
|
125
|
+
updateRule((rule) => ({ ...rule, enabled: value }));
|
|
126
|
+
},
|
|
127
|
+
trueLabel: "on",
|
|
128
|
+
falseLabel: "off",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "onlyIfExists",
|
|
132
|
+
type: "boolean",
|
|
133
|
+
label: "Only if exists",
|
|
134
|
+
description: "Only block when file exists on disk",
|
|
135
|
+
getValue: () => getRule()?.onlyIfExists !== false,
|
|
136
|
+
setValue: (value) => {
|
|
137
|
+
updateRule((rule) => ({ ...rule, onlyIfExists: value }));
|
|
138
|
+
},
|
|
139
|
+
trueLabel: "on",
|
|
140
|
+
falseLabel: "off",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: "patterns",
|
|
144
|
+
type: "submenu",
|
|
145
|
+
label: "Patterns",
|
|
146
|
+
description: "Files protected by this policy",
|
|
147
|
+
getValue: () => `${getRule()?.patterns?.length ?? 0} items`,
|
|
148
|
+
submenu: (done) => {
|
|
149
|
+
const rule = getRule();
|
|
150
|
+
const items = (rule?.patterns ?? []).map((p) => ({
|
|
151
|
+
pattern: p.pattern,
|
|
152
|
+
description: p.pattern,
|
|
153
|
+
regex: p.regex,
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
return new PatternEditor({
|
|
157
|
+
label: "Policy patterns",
|
|
158
|
+
items,
|
|
159
|
+
theme,
|
|
160
|
+
context: "file",
|
|
161
|
+
onSave: (newItems) => {
|
|
162
|
+
const patterns: PatternConfig[] = newItems
|
|
163
|
+
.map((p) => {
|
|
164
|
+
const pattern = p.pattern.trim();
|
|
165
|
+
if (!pattern) return null;
|
|
166
|
+
return { pattern, ...(p.regex ? { regex: true } : {}) };
|
|
167
|
+
})
|
|
168
|
+
.filter((item): item is PatternConfig => item !== null);
|
|
169
|
+
|
|
170
|
+
updateRule((current) => ({ ...current, patterns }));
|
|
171
|
+
},
|
|
172
|
+
onDone: () => done(`${getRule()?.patterns?.length ?? 0} items`),
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: "allowedPatterns",
|
|
178
|
+
type: "submenu",
|
|
179
|
+
label: "Allowed patterns",
|
|
180
|
+
description: "Exceptions",
|
|
181
|
+
getValue: () => `${getRule()?.allowedPatterns?.length ?? 0} items`,
|
|
182
|
+
submenu: (done) => {
|
|
183
|
+
const rule = getRule();
|
|
184
|
+
const items = (rule?.allowedPatterns ?? []).map((p) => ({
|
|
185
|
+
pattern: p.pattern,
|
|
186
|
+
description: p.pattern,
|
|
187
|
+
regex: p.regex,
|
|
188
|
+
}));
|
|
189
|
+
|
|
190
|
+
return new PatternEditor({
|
|
191
|
+
label: "Policy allowed patterns",
|
|
192
|
+
items,
|
|
193
|
+
theme,
|
|
194
|
+
context: "file",
|
|
195
|
+
onSave: (newItems) => {
|
|
196
|
+
const patterns: PatternConfig[] = newItems
|
|
197
|
+
.map((p) => {
|
|
198
|
+
const pattern = p.pattern.trim();
|
|
199
|
+
if (!pattern) return null;
|
|
200
|
+
return { pattern, ...(p.regex ? { regex: true } : {}) };
|
|
201
|
+
})
|
|
202
|
+
.filter((item): item is PatternConfig => item !== null);
|
|
203
|
+
|
|
204
|
+
updateRule((current) => ({
|
|
205
|
+
...current,
|
|
206
|
+
allowedPatterns: patterns.length > 0 ? patterns : undefined,
|
|
207
|
+
}));
|
|
208
|
+
},
|
|
209
|
+
onDone: () =>
|
|
210
|
+
done(`${getRule()?.allowedPatterns?.length ?? 0} items`),
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: "blockMessage",
|
|
216
|
+
type: "text",
|
|
217
|
+
label: "Block message",
|
|
218
|
+
description: "Custom block message ({file} supported)",
|
|
219
|
+
getValue: () => getRule()?.blockMessage?.trim() || "",
|
|
220
|
+
setValue: (value) => {
|
|
221
|
+
const next = value.trim();
|
|
222
|
+
updateRule((rule) => ({ ...rule, blockMessage: next || undefined }));
|
|
223
|
+
},
|
|
224
|
+
emptyValueText: "(default)",
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "delete",
|
|
228
|
+
type: "action",
|
|
229
|
+
label: "Delete rule",
|
|
230
|
+
description: "Remove this rule",
|
|
231
|
+
getValue: () => "danger",
|
|
232
|
+
onConfirm: () => {
|
|
233
|
+
deleteRule();
|
|
234
|
+
},
|
|
235
|
+
confirmMessage: "Delete this rule? This cannot be undone.",
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
return new SettingsDetailEditor({
|
|
240
|
+
title: () => {
|
|
241
|
+
const rule = getRule();
|
|
242
|
+
const title = rule?.name?.trim() || rule?.id || `Policy ${index + 1}`;
|
|
243
|
+
return `Policy: ${title}`;
|
|
244
|
+
},
|
|
245
|
+
fields,
|
|
246
|
+
theme,
|
|
247
|
+
onDone,
|
|
248
|
+
getDoneSummary: () => {
|
|
249
|
+
const rule = getRule();
|
|
250
|
+
if (!rule) return "deleted";
|
|
251
|
+
return `${rule.protection}, ${rule.enabled === false ? "disabled" : "enabled"}`;
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface RegisterGuardrailsSettingsOptions {
|
|
257
|
+
getLoadedFeatures?: () => ReadonlySet<GuardrailsFeatureId>;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function createSettingsConfigStore(): ConfigStore<
|
|
261
|
+
GuardrailsConfig,
|
|
262
|
+
ResolvedConfig
|
|
263
|
+
> {
|
|
264
|
+
return {
|
|
265
|
+
save: (scope, config) => configLoader.save(scope, config),
|
|
266
|
+
getConfig: () => configLoader.getConfig(),
|
|
267
|
+
getRawConfig: (scope) => configLoader.getRawConfig(scope),
|
|
268
|
+
hasScope: (scope) => configLoader.hasScope(scope),
|
|
269
|
+
hasConfig: (scope) => configLoader.hasConfig(scope),
|
|
270
|
+
getEnabledScopes: () => {
|
|
271
|
+
const enabled = new Set(configLoader.getEnabledScopes());
|
|
272
|
+
return (["memory", "local", "global"] as Scope[]).filter((scope) =>
|
|
273
|
+
enabled.has(scope),
|
|
274
|
+
);
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function registerGuardrailsSettings(
|
|
280
|
+
pi: ExtensionAPI,
|
|
281
|
+
options: RegisterGuardrailsSettingsOptions = {},
|
|
282
|
+
): void {
|
|
283
|
+
registerSettingsCommand<GuardrailsConfig, ResolvedConfig>(pi, {
|
|
284
|
+
commandName: "guardrails:settings",
|
|
285
|
+
title: "Guardrails Settings",
|
|
286
|
+
configStore: createSettingsConfigStore(),
|
|
287
|
+
buildSections: (
|
|
288
|
+
tabConfig: GuardrailsConfig | null,
|
|
289
|
+
resolved: ResolvedConfig,
|
|
290
|
+
{ setDraft, theme, scope },
|
|
291
|
+
): SettingsSection[] => {
|
|
292
|
+
const settingsTheme = theme;
|
|
293
|
+
let scopedConfig = structuredClone(tabConfig ?? {}) as GuardrailsConfig;
|
|
294
|
+
|
|
295
|
+
function commitDraft(next: GuardrailsConfig): void {
|
|
296
|
+
scopedConfig = next;
|
|
297
|
+
setDraft(structuredClone(next));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function count(id: string): string {
|
|
301
|
+
return countItems(scopedConfig, id);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function applyDraft(id: string, value: unknown): void {
|
|
305
|
+
commitDraft(setConfigValue(scopedConfig, id, value));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function getPolicyRules(): PolicyRule[] {
|
|
309
|
+
return getPolicyRulesFromConfig(scopedConfig);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function updateRule(
|
|
313
|
+
index: number,
|
|
314
|
+
updater: (rule: PolicyRule) => PolicyRule,
|
|
315
|
+
): void {
|
|
316
|
+
commitDraft(updatePolicyRule(scopedConfig, index, updater));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function deleteRule(index: number): void {
|
|
320
|
+
commitDraft(deletePolicyRule(scopedConfig, index));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function addRule(draft: NewPolicyRuleDraft): number | null {
|
|
324
|
+
const result = addPolicyRuleDraft(scopedConfig, draft);
|
|
325
|
+
commitDraft(result.config);
|
|
326
|
+
return result.index;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function patternSubmenu(
|
|
330
|
+
id: string,
|
|
331
|
+
label: string,
|
|
332
|
+
context?: "file" | "command",
|
|
333
|
+
) {
|
|
334
|
+
return (_val: string, submenuDone: (v?: string) => void) => {
|
|
335
|
+
const items =
|
|
336
|
+
(getNestedValue(scopedConfig, id) as
|
|
337
|
+
| DangerousPattern[]
|
|
338
|
+
| undefined) ?? [];
|
|
339
|
+
let latestCount = items.length;
|
|
340
|
+
return new PatternEditor({
|
|
341
|
+
label,
|
|
342
|
+
items: [...items],
|
|
343
|
+
theme: settingsTheme,
|
|
344
|
+
context,
|
|
345
|
+
onSave: (newItems) => {
|
|
346
|
+
latestCount = newItems.length;
|
|
347
|
+
applyDraft(id, newItems);
|
|
348
|
+
},
|
|
349
|
+
onDone: () => submenuDone(`${latestCount} items`),
|
|
350
|
+
});
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function pathListSubmenu(id: string, label: string) {
|
|
355
|
+
return (_val: string, submenuDone: (v?: string) => void) => {
|
|
356
|
+
const value = getNestedValue(scopedConfig, id);
|
|
357
|
+
const items = Array.isArray(value)
|
|
358
|
+
? value.filter((path): path is string => typeof path === "string")
|
|
359
|
+
: [];
|
|
360
|
+
let latestCount = items.length;
|
|
361
|
+
return new PathListEditor({
|
|
362
|
+
label,
|
|
363
|
+
items,
|
|
364
|
+
theme: settingsTheme,
|
|
365
|
+
onSave: (newItems) => {
|
|
366
|
+
latestCount = newItems.length;
|
|
367
|
+
applyDraft(id, newItems);
|
|
368
|
+
},
|
|
369
|
+
onDone: () => submenuDone(`${latestCount} items`),
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function patternConfigSubmenu(
|
|
375
|
+
id: string,
|
|
376
|
+
label: string,
|
|
377
|
+
context?: "file" | "command",
|
|
378
|
+
) {
|
|
379
|
+
return (_val: string, submenuDone: (v?: string) => void) => {
|
|
380
|
+
const currentItems =
|
|
381
|
+
(getNestedValue(scopedConfig, id) as PatternConfig[] | undefined) ??
|
|
382
|
+
[];
|
|
383
|
+
const items = currentItems.map((p) => ({
|
|
384
|
+
pattern: p.pattern,
|
|
385
|
+
description: p.description?.trim() || p.pattern,
|
|
386
|
+
regex: p.regex,
|
|
387
|
+
}));
|
|
388
|
+
let latestCount = items.length;
|
|
389
|
+
return new PatternEditor({
|
|
390
|
+
label,
|
|
391
|
+
items,
|
|
392
|
+
theme: settingsTheme,
|
|
393
|
+
context,
|
|
394
|
+
onSave: (newItems) => {
|
|
395
|
+
latestCount = newItems.length;
|
|
396
|
+
const configs: PatternConfig[] = newItems
|
|
397
|
+
.map((p) => {
|
|
398
|
+
const pattern = p.pattern.trim();
|
|
399
|
+
if (!pattern) return null;
|
|
400
|
+
const cfg: PatternConfig = { pattern };
|
|
401
|
+
const description = p.description.trim();
|
|
402
|
+
if (description && description !== pattern) {
|
|
403
|
+
cfg.description = description;
|
|
404
|
+
}
|
|
405
|
+
if (p.regex) cfg.regex = true;
|
|
406
|
+
return cfg;
|
|
407
|
+
})
|
|
408
|
+
.filter((item): item is PatternConfig => item !== null);
|
|
409
|
+
applyDraft(id, configs);
|
|
410
|
+
},
|
|
411
|
+
onDone: () => submenuDone(`${latestCount} items`),
|
|
412
|
+
});
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const loadedFeatures = options.getLoadedFeatures?.();
|
|
417
|
+
const featureItems: SettingItem[] = (
|
|
418
|
+
Object.keys(FEATURE_UI) as GuardrailsFeatureId[]
|
|
419
|
+
)
|
|
420
|
+
.filter((key) => key !== "policies")
|
|
421
|
+
.map((key): SettingItem => {
|
|
422
|
+
const scopedValue = scopedConfig.features?.[key];
|
|
423
|
+
const effectiveValue = resolved.features[key];
|
|
424
|
+
const loaded = loadedFeatures?.has(key) ?? true;
|
|
425
|
+
return {
|
|
426
|
+
id: `features.${key}`,
|
|
427
|
+
label: FEATURE_UI[key].label,
|
|
428
|
+
description: loaded
|
|
429
|
+
? FEATURE_UI[key].description
|
|
430
|
+
: `${FEATURE_UI[key].description} (Not loaded by Pi)`,
|
|
431
|
+
currentValue: loaded
|
|
432
|
+
? scopedValue === undefined
|
|
433
|
+
? `inherited: ${effectiveValue ? "enabled" : "disabled"}`
|
|
434
|
+
: scopedValue
|
|
435
|
+
? "enabled"
|
|
436
|
+
: "disabled"
|
|
437
|
+
: "unavailable",
|
|
438
|
+
values: loaded ? ["enabled", "disabled"] : [],
|
|
439
|
+
};
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (scope === "global") {
|
|
443
|
+
featureItems.push({
|
|
444
|
+
id: "onboarding.run",
|
|
445
|
+
label: "Onboarding status",
|
|
446
|
+
description: "Use /guardrails:onboarding to run onboarding",
|
|
447
|
+
currentValue:
|
|
448
|
+
scopedConfig.onboarding?.completed === true
|
|
449
|
+
? "completed"
|
|
450
|
+
: "pending",
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const policyRules = getPolicyRules();
|
|
455
|
+
|
|
456
|
+
const openPolicyEditor = (
|
|
457
|
+
index: number,
|
|
458
|
+
submenuDone: (v?: string) => void,
|
|
459
|
+
): Component =>
|
|
460
|
+
createPolicyRuleEditor({
|
|
461
|
+
index,
|
|
462
|
+
theme: settingsTheme,
|
|
463
|
+
getRule: () => getPolicyRules()[index],
|
|
464
|
+
updateRule: (updater) => updateRule(index, updater),
|
|
465
|
+
deleteRule: () => deleteRule(index),
|
|
466
|
+
onDone: submenuDone,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const policyItems: SettingItem[] = [
|
|
470
|
+
{
|
|
471
|
+
id: "features.policies",
|
|
472
|
+
label: " Enabled",
|
|
473
|
+
description: FEATURE_UI.policies.description,
|
|
474
|
+
currentValue:
|
|
475
|
+
scopedConfig.features?.policies === undefined
|
|
476
|
+
? `inherited: ${resolved.features.policies ? "enabled" : "disabled"}`
|
|
477
|
+
: scopedConfig.features.policies
|
|
478
|
+
? "enabled"
|
|
479
|
+
: "disabled",
|
|
480
|
+
values: ["enabled", "disabled"],
|
|
481
|
+
},
|
|
482
|
+
...policyRules.map((rule, index) => {
|
|
483
|
+
const label = rule.name?.trim() || rule.id || `Policy ${index + 1}`;
|
|
484
|
+
return {
|
|
485
|
+
id: `policies.rules.${index}`,
|
|
486
|
+
label: ` ${label}`,
|
|
487
|
+
description: rule.description?.trim() || "No description",
|
|
488
|
+
currentValue: `${rule.protection}, ${rule.enabled === false ? "disabled" : "enabled"}`,
|
|
489
|
+
submenu: (_val: string, submenuDone: (v?: string) => void) =>
|
|
490
|
+
openPolicyEditor(index, submenuDone),
|
|
491
|
+
};
|
|
492
|
+
}),
|
|
493
|
+
];
|
|
494
|
+
|
|
495
|
+
policyItems.push({
|
|
496
|
+
id: "policies.addRule",
|
|
497
|
+
label: " + Add policy",
|
|
498
|
+
description: "Open wizard to create policy",
|
|
499
|
+
currentValue: "",
|
|
500
|
+
submenu: (_val: string, submenuDone: (v?: string) => void) =>
|
|
501
|
+
new AddRuleSubmenu(
|
|
502
|
+
settingsTheme,
|
|
503
|
+
addRule,
|
|
504
|
+
(index, done) => openPolicyEditor(index, done),
|
|
505
|
+
submenuDone,
|
|
506
|
+
),
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
return [
|
|
510
|
+
{ label: "Features", items: featureItems },
|
|
511
|
+
{
|
|
512
|
+
label: `Policies (${policyRules.length})`,
|
|
513
|
+
items: policyItems,
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
label: "Path Access",
|
|
517
|
+
items: [
|
|
518
|
+
{
|
|
519
|
+
id: "pathAccess.mode",
|
|
520
|
+
label: "Mode",
|
|
521
|
+
description:
|
|
522
|
+
"allow: no restrictions, ask: prompt for outside paths, block: deny all outside paths",
|
|
523
|
+
currentValue:
|
|
524
|
+
scopedConfig.pathAccess?.mode ??
|
|
525
|
+
`inherited: ${resolved.pathAccess.mode}`,
|
|
526
|
+
values: ["allow", "ask", "block"],
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
id: "pathAccess.allowedPaths",
|
|
530
|
+
label: "Allowed paths",
|
|
531
|
+
description:
|
|
532
|
+
"Paths always allowed (trailing / for directories). Supports ~/",
|
|
533
|
+
currentValue: count("pathAccess.allowedPaths"),
|
|
534
|
+
submenu: pathListSubmenu(
|
|
535
|
+
"pathAccess.allowedPaths",
|
|
536
|
+
"Allowed Paths",
|
|
537
|
+
),
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
label: "Permission Gate",
|
|
543
|
+
items: [
|
|
544
|
+
{
|
|
545
|
+
id: "permissionGate.requireConfirmation",
|
|
546
|
+
label: "Require confirmation",
|
|
547
|
+
description:
|
|
548
|
+
"Show confirmation dialog for dangerous commands (if off, just warns)",
|
|
549
|
+
currentValue:
|
|
550
|
+
scopedConfig.permissionGate?.requireConfirmation === undefined
|
|
551
|
+
? `inherited: ${resolved.permissionGate.requireConfirmation ? "on" : "off"}`
|
|
552
|
+
: scopedConfig.permissionGate.requireConfirmation
|
|
553
|
+
? "on"
|
|
554
|
+
: "off",
|
|
555
|
+
values: ["on", "off"],
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
id: "permissionGate.patterns",
|
|
559
|
+
label: "Dangerous patterns",
|
|
560
|
+
description: "Command patterns that trigger the permission gate",
|
|
561
|
+
currentValue: count("permissionGate.patterns"),
|
|
562
|
+
submenu: patternSubmenu(
|
|
563
|
+
"permissionGate.patterns",
|
|
564
|
+
"Dangerous Patterns",
|
|
565
|
+
"command",
|
|
566
|
+
),
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
id: "permissionGate.allowedPatterns",
|
|
570
|
+
label: "Allowed commands",
|
|
571
|
+
description: "Patterns that bypass the permission gate entirely",
|
|
572
|
+
currentValue: count("permissionGate.allowedPatterns"),
|
|
573
|
+
submenu: patternConfigSubmenu(
|
|
574
|
+
"permissionGate.allowedPatterns",
|
|
575
|
+
"Allowed Commands",
|
|
576
|
+
"command",
|
|
577
|
+
),
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
id: "permissionGate.autoDenyPatterns",
|
|
581
|
+
label: "Auto-deny patterns",
|
|
582
|
+
description:
|
|
583
|
+
"Patterns that block commands immediately without dialog",
|
|
584
|
+
currentValue: count("permissionGate.autoDenyPatterns"),
|
|
585
|
+
submenu: patternConfigSubmenu(
|
|
586
|
+
"permissionGate.autoDenyPatterns",
|
|
587
|
+
"Auto-Deny Patterns",
|
|
588
|
+
"command",
|
|
589
|
+
),
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
},
|
|
593
|
+
];
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
}
|