@aliou/pi-guardrails 0.11.1 → 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/paths}/path.test.ts +116 -0
- package/src/{utils → core/paths}/path.ts +23 -0
- package/src/core/shell/command-args.test.ts +94 -0
- package/src/core/shell/command-args.ts +226 -0
- 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 +61 -2
- package/src/{utils → shared/paths}/bash-paths.ts +9 -21
- 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 -332
- 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/shell-utils.ts → core/shell/ast.ts} +0 -0
- /package/src/{utils/glob-expander.ts → shared/glob.ts} +0 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
getSettingsTheme,
|
|
4
|
+
type Scope,
|
|
5
|
+
type SettingsTheme,
|
|
6
|
+
Wizard,
|
|
7
|
+
} from "@aliou/pi-utils-settings";
|
|
8
|
+
import {
|
|
9
|
+
type ExtensionAPI,
|
|
10
|
+
getAgentDir,
|
|
11
|
+
type Theme,
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
14
|
+
import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
15
|
+
import type { GuardrailsConfig } from "../../../../src/shared/config";
|
|
16
|
+
import { configLoader } from "../../../../src/shared/config";
|
|
17
|
+
import {
|
|
18
|
+
appendDangerousPattern,
|
|
19
|
+
appendPolicyRule,
|
|
20
|
+
COMMAND_EXAMPLES,
|
|
21
|
+
POLICY_EXAMPLES,
|
|
22
|
+
} from "../settings/examples";
|
|
23
|
+
|
|
24
|
+
type ExampleScope = Extract<Scope, "local" | "global">;
|
|
25
|
+
|
|
26
|
+
type ExamplesState = {
|
|
27
|
+
scope: ExampleScope | null;
|
|
28
|
+
policyIndexes: Set<number>;
|
|
29
|
+
commandIndexes: Set<number>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type ExamplesResult =
|
|
33
|
+
| { applied: false }
|
|
34
|
+
| { applied: true; state: ExamplesState };
|
|
35
|
+
|
|
36
|
+
const EXAMPLES_CONTENT_HEIGHT = 19;
|
|
37
|
+
const PRESET_LIST_HEADER_LINES = 3;
|
|
38
|
+
const PRESET_DESCRIPTION_LINES = 4;
|
|
39
|
+
const PRESET_LIST_HEIGHT = Math.floor(
|
|
40
|
+
(EXAMPLES_CONTENT_HEIGHT -
|
|
41
|
+
PRESET_LIST_HEADER_LINES -
|
|
42
|
+
PRESET_DESCRIPTION_LINES) /
|
|
43
|
+
2,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
function padStepLines(lines: string[]): string[] {
|
|
47
|
+
while (lines.length < EXAMPLES_CONTENT_HEIGHT) lines.push("");
|
|
48
|
+
return lines;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function wrapText(text: string, width: number, indent = " "): string[] {
|
|
52
|
+
const max = Math.max(20, width - indent.length);
|
|
53
|
+
const words = text.split(/\s+/);
|
|
54
|
+
const lines: string[] = [];
|
|
55
|
+
let current = "";
|
|
56
|
+
for (const word of words) {
|
|
57
|
+
const next = current ? `${current} ${word}` : word;
|
|
58
|
+
if (next.length > max && current) {
|
|
59
|
+
lines.push(`${indent}${current}`);
|
|
60
|
+
current = word;
|
|
61
|
+
} else {
|
|
62
|
+
current = next;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (current) lines.push(`${indent}${current}`);
|
|
66
|
+
return lines;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function scopeLabel(scope: ExampleScope): string {
|
|
70
|
+
if (scope === "local") return "Project";
|
|
71
|
+
return "System";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function scopePath(scope: ExampleScope): string {
|
|
75
|
+
if (scope === "local") return ".pi/extensions/guardrails.json";
|
|
76
|
+
return join(getAgentDir(), "extensions", "guardrails.json");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class ExamplesWelcomeStep implements Component {
|
|
80
|
+
private selectedIndex = 0;
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
private readonly theme: SettingsTheme,
|
|
84
|
+
private readonly state: ExamplesState,
|
|
85
|
+
private readonly scopes: ExampleScope[],
|
|
86
|
+
private readonly onSelect: () => void,
|
|
87
|
+
) {
|
|
88
|
+
const currentIndex = state.scope ? scopes.indexOf(state.scope) : -1;
|
|
89
|
+
this.selectedIndex = Math.max(0, currentIndex);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
invalidate() {}
|
|
93
|
+
|
|
94
|
+
render(width: number): string[] {
|
|
95
|
+
const lines = [
|
|
96
|
+
...wrapText(
|
|
97
|
+
"Example presets help you quickly add common guardrails. File policy presets add named rules for protected files, such as dotenv files, SSH keys, database files, and certificates.",
|
|
98
|
+
width,
|
|
99
|
+
),
|
|
100
|
+
"",
|
|
101
|
+
...wrapText(
|
|
102
|
+
"Dangerous command presets add command patterns that require confirmation, such as terraform destroy or npm publish.",
|
|
103
|
+
width,
|
|
104
|
+
),
|
|
105
|
+
"",
|
|
106
|
+
...wrapText(
|
|
107
|
+
"This command adds selected presets to one config scope. It does not replace existing config.",
|
|
108
|
+
width,
|
|
109
|
+
),
|
|
110
|
+
"",
|
|
111
|
+
this.theme.label(" Save examples to", true),
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
for (let index = 0; index < this.scopes.length; index++) {
|
|
115
|
+
const scope = this.scopes[index];
|
|
116
|
+
if (!scope) continue;
|
|
117
|
+
const selected = index === this.selectedIndex;
|
|
118
|
+
const prefix = selected ? this.theme.cursor : " ";
|
|
119
|
+
lines.push(
|
|
120
|
+
`${prefix}${this.theme.value(scopeLabel(scope), selected)} ${this.theme.hint(`(${scopePath(scope)})`)}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return padStepLines(lines);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
handleInput(data: string): void {
|
|
128
|
+
if (matchesKey(data, Key.up) || data === "k") {
|
|
129
|
+
this.selectedIndex =
|
|
130
|
+
(this.selectedIndex - 1 + this.scopes.length) % this.scopes.length;
|
|
131
|
+
} else if (matchesKey(data, Key.down) || data === "j") {
|
|
132
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.scopes.length;
|
|
133
|
+
} else if (matchesKey(data, Key.enter)) {
|
|
134
|
+
this.state.scope = this.scopes[this.selectedIndex] ?? null;
|
|
135
|
+
this.onSelect();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.state.scope = this.scopes[this.selectedIndex] ?? null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
type PresetListItem = { checked: boolean; label: string; description: string };
|
|
143
|
+
|
|
144
|
+
function renderPresetList(options: {
|
|
145
|
+
title: string;
|
|
146
|
+
items: PresetListItem[];
|
|
147
|
+
selectedIndex: number;
|
|
148
|
+
scrollOffset: number;
|
|
149
|
+
theme: SettingsTheme;
|
|
150
|
+
width: number;
|
|
151
|
+
}): string[] {
|
|
152
|
+
const { title, items, selectedIndex, scrollOffset, theme, width } = options;
|
|
153
|
+
const end = Math.min(items.length, scrollOffset + PRESET_LIST_HEIGHT);
|
|
154
|
+
const selectedItem = items[selectedIndex];
|
|
155
|
+
const lines = [
|
|
156
|
+
theme.label(` ${title}`, true),
|
|
157
|
+
theme.hint(
|
|
158
|
+
` ${items.filter((item) => item.checked).length} selected · Showing ${scrollOffset + 1}-${end} of ${items.length}`,
|
|
159
|
+
),
|
|
160
|
+
"",
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
for (let index = scrollOffset; index < end; index++) {
|
|
164
|
+
const item = items[index];
|
|
165
|
+
if (!item) continue;
|
|
166
|
+
const selected = index === selectedIndex;
|
|
167
|
+
const prefix = selected ? theme.cursor : " ";
|
|
168
|
+
const mark = item.checked ? "[x]" : "[ ]";
|
|
169
|
+
const labelWidth = Math.max(1, width - 6 - item.label.length);
|
|
170
|
+
lines.push(
|
|
171
|
+
`${prefix}${mark} ${theme.value(item.label, selected)}`,
|
|
172
|
+
theme.hint(` ${truncateToWidth(item.description, labelWidth)}`),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
while (lines.length < EXAMPLES_CONTENT_HEIGHT - PRESET_DESCRIPTION_LINES) {
|
|
177
|
+
lines.push("");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const descriptionLines = selectedItem
|
|
181
|
+
? wrapText(selectedItem.description, width, " ").slice(0, 2)
|
|
182
|
+
: [];
|
|
183
|
+
while (descriptionLines.length < PRESET_DESCRIPTION_LINES - 2) {
|
|
184
|
+
descriptionLines.push(" ");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
lines.push(
|
|
188
|
+
"",
|
|
189
|
+
theme.label(" Description", true),
|
|
190
|
+
...descriptionLines.map((line) => theme.hint(line)),
|
|
191
|
+
);
|
|
192
|
+
return padStepLines(lines);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
class PolicyPresetsStep implements Component {
|
|
196
|
+
private selectedIndex = 0;
|
|
197
|
+
private scrollOffset = 0;
|
|
198
|
+
|
|
199
|
+
constructor(
|
|
200
|
+
private readonly theme: SettingsTheme,
|
|
201
|
+
private readonly state: ExamplesState,
|
|
202
|
+
) {}
|
|
203
|
+
|
|
204
|
+
invalidate() {}
|
|
205
|
+
|
|
206
|
+
render(width: number): string[] {
|
|
207
|
+
return this.renderMultiSelect(
|
|
208
|
+
"File policy presets",
|
|
209
|
+
POLICY_EXAMPLES.map((example, index) => ({
|
|
210
|
+
checked: this.state.policyIndexes.has(index),
|
|
211
|
+
label: example.label,
|
|
212
|
+
description: example.description,
|
|
213
|
+
})),
|
|
214
|
+
width,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
handleInput(data: string): void {
|
|
219
|
+
if (matchesKey(data, Key.up) || data === "k") {
|
|
220
|
+
this.selectedIndex =
|
|
221
|
+
(this.selectedIndex - 1 + POLICY_EXAMPLES.length) %
|
|
222
|
+
POLICY_EXAMPLES.length;
|
|
223
|
+
this.ensureVisible(POLICY_EXAMPLES.length);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (matchesKey(data, Key.down) || data === "j") {
|
|
227
|
+
this.selectedIndex = (this.selectedIndex + 1) % POLICY_EXAMPLES.length;
|
|
228
|
+
this.ensureVisible(POLICY_EXAMPLES.length);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (data === " " || matchesKey(data, Key.enter)) {
|
|
232
|
+
if (this.state.policyIndexes.has(this.selectedIndex)) {
|
|
233
|
+
this.state.policyIndexes.delete(this.selectedIndex);
|
|
234
|
+
} else {
|
|
235
|
+
this.state.policyIndexes.add(this.selectedIndex);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private renderMultiSelect(
|
|
241
|
+
title: string,
|
|
242
|
+
items: PresetListItem[],
|
|
243
|
+
width: number,
|
|
244
|
+
): string[] {
|
|
245
|
+
return renderPresetList({
|
|
246
|
+
title,
|
|
247
|
+
items,
|
|
248
|
+
selectedIndex: this.selectedIndex,
|
|
249
|
+
scrollOffset: this.scrollOffset,
|
|
250
|
+
theme: this.theme,
|
|
251
|
+
width,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private ensureVisible(count: number): void {
|
|
256
|
+
if (this.selectedIndex < this.scrollOffset) {
|
|
257
|
+
this.scrollOffset = this.selectedIndex;
|
|
258
|
+
}
|
|
259
|
+
if (this.selectedIndex >= this.scrollOffset + PRESET_LIST_HEIGHT) {
|
|
260
|
+
this.scrollOffset = this.selectedIndex - PRESET_LIST_HEIGHT + 1;
|
|
261
|
+
}
|
|
262
|
+
this.scrollOffset = Math.max(
|
|
263
|
+
0,
|
|
264
|
+
Math.min(this.scrollOffset, Math.max(0, count - PRESET_LIST_HEIGHT)),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
class CommandPresetsStep implements Component {
|
|
270
|
+
private selectedIndex = 0;
|
|
271
|
+
private scrollOffset = 0;
|
|
272
|
+
|
|
273
|
+
constructor(
|
|
274
|
+
private readonly theme: SettingsTheme,
|
|
275
|
+
private readonly state: ExamplesState,
|
|
276
|
+
) {}
|
|
277
|
+
|
|
278
|
+
invalidate() {}
|
|
279
|
+
|
|
280
|
+
render(width: number): string[] {
|
|
281
|
+
const items = COMMAND_EXAMPLES.map((example, index) => ({
|
|
282
|
+
checked: this.state.commandIndexes.has(index),
|
|
283
|
+
label: example.label,
|
|
284
|
+
description: example.description,
|
|
285
|
+
}));
|
|
286
|
+
return renderPresetList({
|
|
287
|
+
title: "Dangerous command presets",
|
|
288
|
+
items,
|
|
289
|
+
selectedIndex: this.selectedIndex,
|
|
290
|
+
scrollOffset: this.scrollOffset,
|
|
291
|
+
theme: this.theme,
|
|
292
|
+
width,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
handleInput(data: string): void {
|
|
297
|
+
if (matchesKey(data, Key.up) || data === "k") {
|
|
298
|
+
this.selectedIndex =
|
|
299
|
+
(this.selectedIndex - 1 + COMMAND_EXAMPLES.length) %
|
|
300
|
+
COMMAND_EXAMPLES.length;
|
|
301
|
+
this.ensureVisible(COMMAND_EXAMPLES.length);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (matchesKey(data, Key.down) || data === "j") {
|
|
305
|
+
this.selectedIndex = (this.selectedIndex + 1) % COMMAND_EXAMPLES.length;
|
|
306
|
+
this.ensureVisible(COMMAND_EXAMPLES.length);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (data === " " || matchesKey(data, Key.enter)) {
|
|
310
|
+
if (this.state.commandIndexes.has(this.selectedIndex)) {
|
|
311
|
+
this.state.commandIndexes.delete(this.selectedIndex);
|
|
312
|
+
} else {
|
|
313
|
+
this.state.commandIndexes.add(this.selectedIndex);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private ensureVisible(count: number): void {
|
|
319
|
+
if (this.selectedIndex < this.scrollOffset) {
|
|
320
|
+
this.scrollOffset = this.selectedIndex;
|
|
321
|
+
}
|
|
322
|
+
if (this.selectedIndex >= this.scrollOffset + PRESET_LIST_HEIGHT) {
|
|
323
|
+
this.scrollOffset = this.selectedIndex - PRESET_LIST_HEIGHT + 1;
|
|
324
|
+
}
|
|
325
|
+
this.scrollOffset = Math.max(
|
|
326
|
+
0,
|
|
327
|
+
Math.min(this.scrollOffset, Math.max(0, count - PRESET_LIST_HEIGHT)),
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
class ExamplesReviewStep implements Component {
|
|
333
|
+
constructor(
|
|
334
|
+
private readonly theme: SettingsTheme,
|
|
335
|
+
private readonly state: ExamplesState,
|
|
336
|
+
) {}
|
|
337
|
+
|
|
338
|
+
invalidate() {}
|
|
339
|
+
|
|
340
|
+
render(width: number): string[] {
|
|
341
|
+
const selectedPolicies = [...this.state.policyIndexes]
|
|
342
|
+
.sort((a, b) => a - b)
|
|
343
|
+
.map((index) => POLICY_EXAMPLES[index])
|
|
344
|
+
.filter((item): item is (typeof POLICY_EXAMPLES)[number] =>
|
|
345
|
+
Boolean(item),
|
|
346
|
+
);
|
|
347
|
+
const selectedCommands = [...this.state.commandIndexes]
|
|
348
|
+
.sort((a, b) => a - b)
|
|
349
|
+
.map((index) => COMMAND_EXAMPLES[index])
|
|
350
|
+
.filter((item): item is (typeof COMMAND_EXAMPLES)[number] =>
|
|
351
|
+
Boolean(item),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const lines = [
|
|
355
|
+
" Review selected presets.",
|
|
356
|
+
"",
|
|
357
|
+
this.theme.hint(
|
|
358
|
+
` Scope: ${this.state.scope ? `${scopeLabel(this.state.scope)} (${scopePath(this.state.scope)})` : "not selected"}`,
|
|
359
|
+
),
|
|
360
|
+
this.theme.hint(` File policies: ${selectedPolicies.length}`),
|
|
361
|
+
...selectedPolicies.flatMap((example) =>
|
|
362
|
+
wrapText(
|
|
363
|
+
`- ${example.label}: ${example.rule.protection}`,
|
|
364
|
+
width,
|
|
365
|
+
" ",
|
|
366
|
+
),
|
|
367
|
+
),
|
|
368
|
+
this.theme.hint(` Dangerous commands: ${selectedCommands.length}`),
|
|
369
|
+
...selectedCommands.flatMap((example) =>
|
|
370
|
+
wrapText(
|
|
371
|
+
`- ${example.label}: ${example.pattern.pattern}`,
|
|
372
|
+
width,
|
|
373
|
+
" ",
|
|
374
|
+
),
|
|
375
|
+
),
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
if (
|
|
379
|
+
!this.state.scope ||
|
|
380
|
+
selectedPolicies.length + selectedCommands.length === 0
|
|
381
|
+
) {
|
|
382
|
+
lines.push(
|
|
383
|
+
"",
|
|
384
|
+
this.theme.hint(
|
|
385
|
+
" Select a scope and at least one preset before submitting.",
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return padStepLines(lines);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
handleInput(): void {}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function createExamplesWizard(
|
|
397
|
+
theme: Theme,
|
|
398
|
+
scopes: ExampleScope[],
|
|
399
|
+
done: (result: ExamplesResult) => void,
|
|
400
|
+
): Component {
|
|
401
|
+
const settingsTheme = getSettingsTheme(theme);
|
|
402
|
+
const state: ExamplesState = {
|
|
403
|
+
scope: scopes[0] ?? null,
|
|
404
|
+
policyIndexes: new Set(),
|
|
405
|
+
commandIndexes: new Set(),
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const apply = () => {
|
|
409
|
+
if (!state.scope) {
|
|
410
|
+
done({ applied: false });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (state.policyIndexes.size + state.commandIndexes.size === 0) {
|
|
414
|
+
done({ applied: false });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
done({ applied: true, state });
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
return new Wizard({
|
|
421
|
+
title: "Guardrails examples",
|
|
422
|
+
theme,
|
|
423
|
+
steps: [
|
|
424
|
+
{
|
|
425
|
+
label: "Welcome",
|
|
426
|
+
build: (ctx) => {
|
|
427
|
+
ctx.markComplete();
|
|
428
|
+
return new ExamplesWelcomeStep(settingsTheme, state, scopes, () => {
|
|
429
|
+
ctx.markComplete();
|
|
430
|
+
ctx.goNext();
|
|
431
|
+
});
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
label: "Files",
|
|
436
|
+
build: (ctx) => {
|
|
437
|
+
ctx.markComplete();
|
|
438
|
+
return new PolicyPresetsStep(settingsTheme, state);
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
label: "Commands",
|
|
443
|
+
build: (ctx) => {
|
|
444
|
+
ctx.markComplete();
|
|
445
|
+
return new CommandPresetsStep(settingsTheme, state);
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
label: "Review",
|
|
450
|
+
build: (ctx) => {
|
|
451
|
+
ctx.markComplete();
|
|
452
|
+
return new ExamplesReviewStep(settingsTheme, state);
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
onComplete: apply,
|
|
457
|
+
onCancel: () => done({ applied: false }),
|
|
458
|
+
minContentHeight: EXAMPLES_CONTENT_HEIGHT,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function getExampleScopes(): ExampleScope[] {
|
|
463
|
+
const enabled = new Set(configLoader.getEnabledScopes());
|
|
464
|
+
return (["local", "global"] as ExampleScope[]).filter((scope) =>
|
|
465
|
+
enabled.has(scope),
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function applyExample(
|
|
470
|
+
result: Extract<ExamplesResult, { applied: true }>,
|
|
471
|
+
) {
|
|
472
|
+
if (!result.state.scope) return;
|
|
473
|
+
const scope = result.state.scope;
|
|
474
|
+
const baseConfig = configLoader.getRawConfig(scope) ?? null;
|
|
475
|
+
let updated: GuardrailsConfig = structuredClone(baseConfig ?? {});
|
|
476
|
+
|
|
477
|
+
for (const index of [...result.state.policyIndexes].sort((a, b) => a - b)) {
|
|
478
|
+
const example = POLICY_EXAMPLES[index];
|
|
479
|
+
if (example) updated = appendPolicyRule(updated, example.rule);
|
|
480
|
+
}
|
|
481
|
+
for (const index of [...result.state.commandIndexes].sort((a, b) => a - b)) {
|
|
482
|
+
const example = COMMAND_EXAMPLES[index];
|
|
483
|
+
if (example) updated = appendDangerousPattern(updated, example.pattern);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
await configLoader.save(scope, updated);
|
|
487
|
+
await configLoader.load();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function registerGuardrailsExamplesCommand(pi: ExtensionAPI): void {
|
|
491
|
+
pi.registerCommand("guardrails:examples", {
|
|
492
|
+
description: "Apply guardrails example presets",
|
|
493
|
+
handler: async (_args, ctx) => {
|
|
494
|
+
if (!ctx.hasUI) return;
|
|
495
|
+
|
|
496
|
+
const scopes = getExampleScopes();
|
|
497
|
+
if (scopes.length === 0) {
|
|
498
|
+
ctx.ui.notify("[Guardrails] no config scopes available.", "error");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const result = await ctx.ui.custom<ExamplesResult>(
|
|
503
|
+
(_tui, theme: Theme, _keybindings, done) =>
|
|
504
|
+
createExamplesWizard(theme, scopes, done),
|
|
505
|
+
{ overlay: true },
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
if (!result.applied) {
|
|
509
|
+
ctx.ui.notify("[Guardrails] no examples applied.", "warning");
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
await applyExample(result);
|
|
514
|
+
ctx.ui.notify(
|
|
515
|
+
`[Guardrails] examples applied to ${result.state.scope}.`,
|
|
516
|
+
"info",
|
|
517
|
+
);
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CURRENT_VERSION,
|
|
3
|
+
type GuardrailsConfig,
|
|
4
|
+
} from "../../../../src/shared/config";
|
|
5
|
+
|
|
6
|
+
export function buildOnboardedConfig(
|
|
7
|
+
applyBuiltinDefaults: boolean,
|
|
8
|
+
pathAccessEnabled?: boolean | null,
|
|
9
|
+
): GuardrailsConfig {
|
|
10
|
+
const config: GuardrailsConfig = {
|
|
11
|
+
version: CURRENT_VERSION,
|
|
12
|
+
applyBuiltinDefaults,
|
|
13
|
+
onboarding: {
|
|
14
|
+
completed: true,
|
|
15
|
+
completedAt: new Date().toISOString(),
|
|
16
|
+
version: CURRENT_VERSION,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
if (pathAccessEnabled) {
|
|
20
|
+
config.features = { ...config.features, pathAccess: true };
|
|
21
|
+
config.pathAccess = { mode: "ask" };
|
|
22
|
+
}
|
|
23
|
+
return config;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function mergeOnboardingConfig(
|
|
27
|
+
base: GuardrailsConfig | null,
|
|
28
|
+
applyBuiltinDefaults: boolean,
|
|
29
|
+
pathAccessEnabled?: boolean | null,
|
|
30
|
+
): GuardrailsConfig {
|
|
31
|
+
const next = structuredClone(base ?? {});
|
|
32
|
+
const onboarded = buildOnboardedConfig(
|
|
33
|
+
applyBuiltinDefaults,
|
|
34
|
+
pathAccessEnabled,
|
|
35
|
+
);
|
|
36
|
+
next.applyBuiltinDefaults = onboarded.applyBuiltinDefaults;
|
|
37
|
+
next.version = onboarded.version;
|
|
38
|
+
next.onboarding = onboarded.onboarding;
|
|
39
|
+
if (onboarded.features?.pathAccess !== undefined) {
|
|
40
|
+
next.features = {
|
|
41
|
+
...next.features,
|
|
42
|
+
pathAccess: onboarded.features.pathAccess,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (onboarded.pathAccess) {
|
|
46
|
+
next.pathAccess = onboarded.pathAccess;
|
|
47
|
+
}
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isOnboardingPending(config: GuardrailsConfig | null): boolean {
|
|
52
|
+
if (!config) return true;
|
|
53
|
+
return config.onboarding?.completed !== true;
|
|
54
|
+
}
|
package/{src/commands/onboarding-command.ts → extensions/guardrails/commands/onboarding/index.ts}
RENAMED
|
@@ -1,36 +1,10 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { configLoader
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { configLoader } from "../../../../src/shared/config";
|
|
3
3
|
import {
|
|
4
|
-
buildOnboardedConfig,
|
|
5
4
|
createOnboardingWizard,
|
|
6
|
-
isOnboardingPending,
|
|
7
5
|
type OnboardingResult,
|
|
8
|
-
} from "
|
|
9
|
-
|
|
10
|
-
function mergeOnboarding(
|
|
11
|
-
base: GuardrailsConfig | null,
|
|
12
|
-
applyBuiltinDefaults: boolean,
|
|
13
|
-
pathAccessEnabled?: boolean | null,
|
|
14
|
-
): GuardrailsConfig {
|
|
15
|
-
const next = structuredClone(base ?? {});
|
|
16
|
-
const onboarded = buildOnboardedConfig(
|
|
17
|
-
applyBuiltinDefaults,
|
|
18
|
-
pathAccessEnabled,
|
|
19
|
-
);
|
|
20
|
-
next.applyBuiltinDefaults = onboarded.applyBuiltinDefaults;
|
|
21
|
-
next.version = onboarded.version;
|
|
22
|
-
next.onboarding = onboarded.onboarding;
|
|
23
|
-
if (onboarded.features?.pathAccess !== undefined) {
|
|
24
|
-
next.features = {
|
|
25
|
-
...next.features,
|
|
26
|
-
pathAccess: onboarded.features.pathAccess,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
if (onboarded.pathAccess) {
|
|
30
|
-
next.pathAccess = onboarded.pathAccess;
|
|
31
|
-
}
|
|
32
|
-
return next;
|
|
33
|
-
}
|
|
6
|
+
} from "../../components/onboarding-wizard";
|
|
7
|
+
import { isOnboardingPending, mergeOnboardingConfig } from "./config";
|
|
34
8
|
|
|
35
9
|
export function registerGuardrailsOnboardingCommand(
|
|
36
10
|
pi: ExtensionAPI,
|
|
@@ -61,7 +35,7 @@ export function registerGuardrailsOnboardingCommand(
|
|
|
61
35
|
return;
|
|
62
36
|
}
|
|
63
37
|
|
|
64
|
-
const merged =
|
|
38
|
+
const merged = mergeOnboardingConfig(
|
|
65
39
|
globalConfig,
|
|
66
40
|
result.applyBuiltinDefaults,
|
|
67
41
|
result.pathAccessEnabled,
|