@aliou/pi-guardrails 0.11.2 → 0.12.1

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.
Files changed (95) hide show
  1. package/README.md +72 -167
  2. package/extensions/guardrails/commands/examples/index.ts +520 -0
  3. package/extensions/guardrails/commands/onboarding/config.ts +54 -0
  4. package/{src/commands/onboarding-command.ts → extensions/guardrails/commands/onboarding/index.ts} +5 -31
  5. package/extensions/guardrails/commands/settings/add-rule-wizard.ts +267 -0
  6. package/extensions/guardrails/commands/settings/examples.ts +399 -0
  7. package/extensions/guardrails/commands/settings/index.ts +596 -0
  8. package/extensions/guardrails/commands/settings/path-list-editor.ts +158 -0
  9. package/extensions/guardrails/commands/settings/scope-picker-submenu.ts +69 -0
  10. package/extensions/guardrails/commands/settings/utils.ts +108 -0
  11. package/extensions/guardrails/components/onboarding-choice-step.ts +140 -0
  12. package/extensions/guardrails/components/onboarding-finish-step.ts +50 -0
  13. package/extensions/guardrails/components/onboarding-intro-step.ts +30 -0
  14. package/extensions/guardrails/components/onboarding-types.ts +10 -0
  15. package/extensions/guardrails/components/onboarding-wizard.ts +116 -0
  16. package/{src → extensions/guardrails}/components/pattern-editor.ts +11 -10
  17. package/extensions/guardrails/index.ts +106 -0
  18. package/extensions/guardrails/rules.test.ts +107 -0
  19. package/extensions/guardrails/rules.ts +119 -0
  20. package/extensions/guardrails/targets.test.ts +44 -0
  21. package/extensions/guardrails/targets.ts +66 -0
  22. package/extensions/path-access/grants.test.ts +47 -0
  23. package/extensions/path-access/grants.ts +68 -0
  24. package/extensions/path-access/index.ts +143 -0
  25. package/extensions/path-access/prompt.ts +196 -0
  26. package/extensions/path-access/rules.test.ts +46 -0
  27. package/extensions/path-access/rules.ts +37 -0
  28. package/extensions/path-access/targets.test.ts +40 -0
  29. package/extensions/path-access/targets.ts +19 -0
  30. package/extensions/permission-gate/grants.ts +21 -0
  31. package/extensions/permission-gate/index.ts +122 -0
  32. package/extensions/permission-gate/prompt.ts +222 -0
  33. package/extensions/permission-gate/rules.test.ts +132 -0
  34. package/extensions/permission-gate/rules.ts +72 -0
  35. package/package.json +18 -20
  36. package/schema.json +286 -0
  37. package/src/core/check.test.ts +169 -0
  38. package/src/core/check.ts +38 -0
  39. package/src/{hooks/permission-gate/dangerous-commands.test.ts → core/commands/dangerous.test.ts} +134 -2
  40. package/src/{hooks/permission-gate/dangerous-commands.ts → core/commands/dangerous.ts} +119 -1
  41. package/src/core/commands/index.ts +15 -0
  42. package/src/core/index.ts +13 -0
  43. package/src/{utils/path-access.test.ts → core/paths/access.test.ts} +1 -5
  44. package/src/core/paths/index.ts +14 -0
  45. package/src/core/shell/command-args.test.ts +142 -0
  46. package/src/{utils → core/shell}/command-args.ts +71 -0
  47. package/src/core/shell/index.ts +2 -0
  48. package/src/core/types.ts +55 -0
  49. package/src/shared/config/defaults.ts +118 -0
  50. package/src/shared/config/index.ts +17 -0
  51. package/src/shared/config/loader.ts +64 -0
  52. package/src/shared/config/migration/001-v0-format-upgrade.ts +107 -0
  53. package/src/shared/config/migration/002-strip-toolchain-fields.ts +39 -0
  54. package/src/shared/config/migration/003-strip-command-explainer-fields.ts +42 -0
  55. package/src/shared/config/migration/004-env-files-to-policies.ts +87 -0
  56. package/src/shared/config/migration/005-normalize-allowed-paths.ts +43 -0
  57. package/src/shared/config/migration/006-apply-builtin-defaults.ts +19 -0
  58. package/src/shared/config/migration/007-mark-onboarding-done.ts +25 -0
  59. package/src/shared/config/migration/index.ts +44 -0
  60. package/src/shared/config/migration/version.ts +7 -0
  61. package/src/shared/config/types.ts +141 -0
  62. package/src/shared/events.ts +100 -0
  63. package/src/shared/index.ts +6 -0
  64. package/src/shared/matching.test.ts +86 -0
  65. package/src/{utils → shared}/matching.ts +4 -4
  66. package/src/{utils → shared/paths}/bash-paths.test.ts +32 -2
  67. package/src/{utils → shared/paths}/bash-paths.ts +4 -4
  68. package/src/shared/paths/index.ts +1 -0
  69. package/src/shared/warnings.ts +17 -0
  70. package/docs/defaults.md +0 -140
  71. package/docs/examples.md +0 -170
  72. package/src/commands/onboarding.ts +0 -390
  73. package/src/commands/settings-command.ts +0 -1616
  74. package/src/config.ts +0 -392
  75. package/src/hooks/index.ts +0 -11
  76. package/src/hooks/path-access.ts +0 -395
  77. package/src/hooks/permission-gate/index.test.ts +0 -332
  78. package/src/hooks/permission-gate/index.ts +0 -595
  79. package/src/hooks/policies.ts +0 -322
  80. package/src/index.ts +0 -96
  81. package/src/lib/executor.ts +0 -280
  82. package/src/lib/index.ts +0 -16
  83. package/src/lib/model-resolver.ts +0 -47
  84. package/src/lib/timing.ts +0 -42
  85. package/src/lib/types.ts +0 -115
  86. package/src/utils/command-args.test.ts +0 -83
  87. package/src/utils/events.ts +0 -32
  88. package/src/utils/migration.test.ts +0 -58
  89. package/src/utils/migration.ts +0 -340
  90. package/src/utils/warnings.ts +0 -7
  91. /package/src/{utils/path-access.ts → core/paths/access.ts} +0 -0
  92. /package/src/{utils → core/paths}/path.test.ts +0 -0
  93. /package/src/{utils → core/paths}/path.ts +0 -0
  94. /package/src/{utils/shell-utils.ts → core/shell/ast.ts} +0 -0
  95. /package/src/{utils/glob-expander.ts → shared/glob.ts} +0 -0
@@ -0,0 +1,267 @@
1
+ import {
2
+ FuzzySelector,
3
+ type SettingsTheme,
4
+ Wizard,
5
+ } from "@aliou/pi-utils-settings";
6
+ import {
7
+ type Component,
8
+ Input,
9
+ type SettingsListTheme,
10
+ } from "@earendil-works/pi-tui";
11
+ import type { PatternConfig } from "../../../../src/shared/config";
12
+ import { PatternEditor } from "../../components/pattern-editor";
13
+ import { type NewPolicyRuleDraft, toKebabCase } from "./utils";
14
+
15
+ type NewPolicyDraft = NewPolicyRuleDraft;
16
+
17
+ class PolicyNameStep implements Component {
18
+ private readonly input = new Input();
19
+
20
+ constructor(
21
+ private readonly theme: SettingsListTheme,
22
+ private readonly state: NewPolicyDraft,
23
+ private readonly onComplete: () => void,
24
+ ) {
25
+ this.input.setValue(state.name);
26
+ this.input.onSubmit = () => {
27
+ const name = this.input.getValue().trim();
28
+ if (!name) return;
29
+ this.state.name = name;
30
+ if (!this.state.id) {
31
+ this.state.id = toKebabCase(name) || "policy";
32
+ }
33
+ this.onComplete();
34
+ };
35
+ }
36
+
37
+ invalidate() {}
38
+
39
+ render(width: number): string[] {
40
+ return [
41
+ this.theme.hint(" Step 1: Policy name"),
42
+ "",
43
+ ...this.input.render(Math.max(1, width - 2)).map((line) => ` ${line}`),
44
+ "",
45
+ this.theme.hint(" Example: Secret files"),
46
+ this.theme.hint(" Enter to continue"),
47
+ ];
48
+ }
49
+
50
+ handleInput(data: string): void {
51
+ this.input.handleInput(data);
52
+ }
53
+ }
54
+
55
+ class PolicyProtectionStep implements Component {
56
+ private readonly selector: FuzzySelector;
57
+
58
+ constructor(
59
+ theme: SettingsListTheme,
60
+ state: NewPolicyDraft,
61
+ onComplete: () => void,
62
+ ) {
63
+ this.selector = new FuzzySelector({
64
+ label: "Protection",
65
+ items: ["noAccess", "readOnly", "none"],
66
+ currentValue: state.protection,
67
+ theme,
68
+ onSelect: (value) => {
69
+ if (value === "noAccess" || value === "readOnly" || value === "none") {
70
+ state.protection = value;
71
+ onComplete();
72
+ }
73
+ },
74
+ onDone: () => {
75
+ // Esc is handled by Wizard.
76
+ },
77
+ });
78
+ }
79
+
80
+ invalidate(): void {
81
+ this.selector.invalidate?.();
82
+ }
83
+
84
+ render(width: number): string[] {
85
+ return this.selector.render(width);
86
+ }
87
+
88
+ handleInput(data: string): void {
89
+ this.selector.handleInput(data);
90
+ }
91
+ }
92
+
93
+ class PolicyPatternsStep implements Component {
94
+ private readonly editor: PatternEditor;
95
+
96
+ constructor(
97
+ theme: SettingsListTheme,
98
+ state: NewPolicyDraft,
99
+ onComplete: () => void,
100
+ ) {
101
+ this.editor = new PatternEditor({
102
+ label: "Policy patterns",
103
+ context: "file",
104
+ theme,
105
+ items: state.patterns.map((p) => ({
106
+ pattern: p.pattern,
107
+ description: p.pattern,
108
+ regex: p.regex,
109
+ })),
110
+ onSave: (items) => {
111
+ state.patterns = items
112
+ .map((item) => {
113
+ const pattern = item.pattern.trim();
114
+ if (!pattern) return null;
115
+ return {
116
+ pattern,
117
+ ...(item.regex ? { regex: true } : {}),
118
+ };
119
+ })
120
+ .filter((item): item is PatternConfig => item !== null);
121
+ },
122
+ onDone: () => {
123
+ if (state.patterns.length > 0) {
124
+ onComplete();
125
+ }
126
+ },
127
+ });
128
+ }
129
+
130
+ invalidate(): void {
131
+ this.editor.invalidate?.();
132
+ }
133
+
134
+ render(width: number): string[] {
135
+ return this.editor.render(width);
136
+ }
137
+
138
+ handleInput(data: string): void {
139
+ this.editor.handleInput(data);
140
+ }
141
+ }
142
+
143
+ class PolicyReviewStep implements Component {
144
+ constructor(
145
+ private readonly theme: SettingsListTheme,
146
+ private readonly state: NewPolicyDraft,
147
+ ) {}
148
+
149
+ invalidate() {}
150
+
151
+ render(_width: number): string[] {
152
+ const patternPreview =
153
+ this.state.patterns.length > 0
154
+ ? this.state.patterns
155
+ .slice(0, 3)
156
+ .map((p) => `${p.pattern}${p.regex ? " [regex]" : ""}`)
157
+ .join(", ")
158
+ : "(none)";
159
+
160
+ return [
161
+ this.theme.hint(" Review"),
162
+ "",
163
+ this.theme.hint(` Name: ${this.state.name || "(empty)"}`),
164
+ this.theme.hint(` ID: ${this.state.id || "(auto)"}`),
165
+ this.theme.hint(` Protection: ${this.state.protection}`),
166
+ this.theme.hint(` Patterns: ${this.state.patterns.length}`),
167
+ this.theme.hint(` ${patternPreview}`),
168
+ "",
169
+ this.theme.hint(" Ctrl+S: create + open editor · Esc: cancel"),
170
+ ];
171
+ }
172
+
173
+ handleInput(_data: string): void {}
174
+ }
175
+
176
+ export class AddRuleSubmenu implements Component {
177
+ private readonly wizard: Wizard;
178
+ private activeEditor: Component | null = null;
179
+
180
+ constructor(
181
+ theme: SettingsTheme,
182
+ onCreate: (draft: NewPolicyDraft) => number | null,
183
+ openEditor: (index: number, done: (value?: string) => void) => Component,
184
+ onDone: (value?: string) => void,
185
+ ) {
186
+ const state: NewPolicyDraft = {
187
+ name: "",
188
+ id: "",
189
+ protection: "readOnly",
190
+ patterns: [],
191
+ };
192
+
193
+ this.wizard = new Wizard({
194
+ title: "Add policy",
195
+ theme,
196
+ steps: [
197
+ {
198
+ label: "Name",
199
+ build: (ctx) =>
200
+ new PolicyNameStep(theme, state, () => {
201
+ ctx.markComplete();
202
+ ctx.goNext();
203
+ }),
204
+ },
205
+ {
206
+ label: "Protection",
207
+ build: (ctx) =>
208
+ new PolicyProtectionStep(theme, state, () => {
209
+ ctx.markComplete();
210
+ ctx.goNext();
211
+ }),
212
+ },
213
+ {
214
+ label: "Patterns",
215
+ build: (ctx) =>
216
+ new PolicyPatternsStep(theme, state, () => {
217
+ if (state.patterns.length === 0) {
218
+ ctx.markIncomplete();
219
+ return;
220
+ }
221
+ ctx.markComplete();
222
+ ctx.goNext();
223
+ }),
224
+ },
225
+ {
226
+ label: "Review",
227
+ build: (ctx) => {
228
+ ctx.markComplete();
229
+ return new PolicyReviewStep(theme, state);
230
+ },
231
+ },
232
+ ],
233
+ onComplete: () => {
234
+ if (!state.name.trim() || state.patterns.length === 0) return;
235
+ const index = onCreate(state);
236
+ if (index === null) return;
237
+ this.activeEditor = openEditor(index, (value) => {
238
+ this.activeEditor = null;
239
+ onDone(value);
240
+ });
241
+ },
242
+ onCancel: () => onDone(),
243
+ hintSuffix: "complete steps · Ctrl+S create",
244
+ minContentHeight: 12,
245
+ });
246
+ }
247
+
248
+ invalidate(): void {
249
+ this.activeEditor?.invalidate?.();
250
+ this.wizard.invalidate?.();
251
+ }
252
+
253
+ render(width: number): string[] {
254
+ if (this.activeEditor) {
255
+ return this.activeEditor.render(width);
256
+ }
257
+ return this.wizard.render(width);
258
+ }
259
+
260
+ handleInput(data: string): void {
261
+ if (this.activeEditor) {
262
+ this.activeEditor.handleInput?.(data);
263
+ return;
264
+ }
265
+ this.wizard.handleInput(data);
266
+ }
267
+ }
@@ -0,0 +1,399 @@
1
+ import type {
2
+ DangerousPattern,
3
+ GuardrailsConfig,
4
+ PolicyRule,
5
+ } from "../../../../src/shared/config";
6
+ import { toKebabCase } from "./utils";
7
+
8
+ export const POLICY_EXAMPLES: Array<{
9
+ label: string;
10
+ description: string;
11
+ rule: PolicyRule;
12
+ }> = [
13
+ {
14
+ label: "Secrets (.env)",
15
+ description:
16
+ "Blocks common dotenv files that usually contain secrets, while allowing sample and example env files.",
17
+ rule: {
18
+ id: "example-secret-env-files",
19
+ name: "Secret env files",
20
+ description: "Block .env files and variants",
21
+ patterns: [{ pattern: ".env" }, { pattern: ".env.*" }],
22
+ allowedPatterns: [
23
+ { pattern: ".env.example" },
24
+ { pattern: "*.sample.env" },
25
+ ],
26
+ protection: "noAccess",
27
+ onlyIfExists: true,
28
+ enabled: true,
29
+ },
30
+ },
31
+ {
32
+ label: "Logs (*.log)",
33
+ description:
34
+ "Makes log and output files read-only so the agent can inspect them without accidentally rewriting them.",
35
+ rule: {
36
+ id: "example-log-files",
37
+ name: "Log files",
38
+ description: "Treat log files as read-only",
39
+ patterns: [{ pattern: "*.log" }, { pattern: "*.out" }],
40
+ protection: "readOnly",
41
+ onlyIfExists: true,
42
+ enabled: true,
43
+ },
44
+ },
45
+ {
46
+ label: "Regex env",
47
+ description:
48
+ "Shows how to use regex patterns to protect .env and .env.* files with a precise exception for .env.example.",
49
+ rule: {
50
+ id: "example-regex-env",
51
+ name: "Regex env files",
52
+ description: "Regex example for env files",
53
+ patterns: [{ pattern: "^\\.env(\\..+)?$", regex: true }],
54
+ allowedPatterns: [{ pattern: "^\\.env\\.example$", regex: true }],
55
+ protection: "noAccess",
56
+ onlyIfExists: true,
57
+ enabled: true,
58
+ },
59
+ },
60
+ {
61
+ label: "SSH keys",
62
+ description:
63
+ "Blocks common SSH private key formats while allowing public key files.",
64
+ rule: {
65
+ id: "example-ssh-keys",
66
+ name: "SSH keys",
67
+ description: "Block SSH private key files",
68
+ patterns: [
69
+ { pattern: "*.pem" },
70
+ { pattern: "*_rsa" },
71
+ { pattern: "*_ed25519" },
72
+ ],
73
+ allowedPatterns: [{ pattern: "*.pub" }],
74
+ protection: "noAccess",
75
+ onlyIfExists: true,
76
+ enabled: true,
77
+ },
78
+ },
79
+ {
80
+ label: "AWS credentials",
81
+ description:
82
+ "Blocks AWS CLI credential and config files that may contain access keys, profiles, and account details.",
83
+ rule: {
84
+ id: "example-aws-credentials",
85
+ name: "AWS credentials",
86
+ description: "Block AWS credentials and config files",
87
+ patterns: [{ pattern: ".aws/credentials" }, { pattern: ".aws/config" }],
88
+ protection: "noAccess",
89
+ onlyIfExists: true,
90
+ enabled: true,
91
+ },
92
+ },
93
+ {
94
+ label: "Database files",
95
+ description:
96
+ "Makes SQLite and database files read-only to avoid accidental data changes.",
97
+ rule: {
98
+ id: "example-database-files",
99
+ name: "Database files",
100
+ description: "Protect database files from modification",
101
+ patterns: [
102
+ { pattern: "*.db" },
103
+ { pattern: "*.sqlite" },
104
+ { pattern: "*.sqlite3" },
105
+ ],
106
+ protection: "readOnly",
107
+ onlyIfExists: true,
108
+ enabled: true,
109
+ },
110
+ },
111
+ {
112
+ label: "Kubernetes secrets",
113
+ description:
114
+ "Blocks kubeconfig-style files that can contain cluster credentials and sensitive Kubernetes access details.",
115
+ rule: {
116
+ id: "example-k8s-secrets",
117
+ name: "Kubernetes secrets",
118
+ description: "Block kubectl config and secrets",
119
+ patterns: [{ pattern: ".kube/config" }, { pattern: "*kubeconfig*" }],
120
+ protection: "noAccess",
121
+ onlyIfExists: true,
122
+ enabled: true,
123
+ },
124
+ },
125
+ {
126
+ label: "Certificates",
127
+ description:
128
+ "Blocks certificate and private key files while allowing certificate signing requests.",
129
+ rule: {
130
+ id: "example-certificates",
131
+ name: "Certificates",
132
+ description: "Block certificate and key files",
133
+ patterns: [
134
+ { pattern: "*.crt" },
135
+ { pattern: "*.key" },
136
+ { pattern: "*.p12" },
137
+ ],
138
+ allowedPatterns: [{ pattern: "*.csr" }],
139
+ protection: "noAccess",
140
+ onlyIfExists: true,
141
+ enabled: true,
142
+ },
143
+ },
144
+ ];
145
+
146
+ export const COMMAND_EXAMPLES: Array<{
147
+ label: string;
148
+ description: string;
149
+ pattern: DangerousPattern;
150
+ }> = [
151
+ {
152
+ label: "Homebrew",
153
+ description:
154
+ "Prompts before Homebrew commands, useful on machines where package installs should go through Nix.",
155
+ pattern: { pattern: "brew", description: "Homebrew package manager" },
156
+ },
157
+ {
158
+ label: "Docker secrets",
159
+ description:
160
+ "Prompts before docker inspect because container metadata can expose environment variables and mounted secrets.",
161
+ pattern: {
162
+ pattern: "docker inspect",
163
+ description: "Docker inspect (may expose env vars)",
164
+ },
165
+ },
166
+ {
167
+ label: "Terraform apply",
168
+ description: "Prompts before Terraform applies infrastructure changes.",
169
+ pattern: {
170
+ pattern: "terraform apply",
171
+ description: "Terraform infrastructure changes",
172
+ },
173
+ },
174
+ {
175
+ label: "Terraform destroy",
176
+ description: "Prompts before Terraform destroys infrastructure resources.",
177
+ pattern: {
178
+ pattern: "terraform destroy",
179
+ description: "Terraform infrastructure destruction",
180
+ },
181
+ },
182
+ {
183
+ label: "kubectl delete",
184
+ description: "Prompts before deleting Kubernetes resources.",
185
+ pattern: {
186
+ pattern: "kubectl delete",
187
+ description: "Kubernetes resource deletion",
188
+ },
189
+ },
190
+ {
191
+ label: "docker system prune",
192
+ description:
193
+ "Prompts before Docker cleanup commands that can remove images, containers, volumes, or build cache.",
194
+ pattern: {
195
+ pattern: "docker system prune",
196
+ description: "Docker system cleanup",
197
+ },
198
+ },
199
+ {
200
+ label: "git push --force",
201
+ description: "Prompts before force-pushing Git history.",
202
+ pattern: { pattern: "git push --force", description: "Git force push" },
203
+ },
204
+ {
205
+ label: "npm publish",
206
+ description: "Prompts before publishing npm packages.",
207
+ pattern: { pattern: "npm publish", description: "NPM package publishing" },
208
+ },
209
+ {
210
+ label: "yarn publish",
211
+ description: "Prompts before publishing Yarn packages.",
212
+ pattern: {
213
+ pattern: "yarn publish",
214
+ description: "Yarn package publishing",
215
+ },
216
+ },
217
+ {
218
+ label: "pnpm publish",
219
+ description: "Prompts before publishing pnpm packages.",
220
+ pattern: {
221
+ pattern: "pnpm publish",
222
+ description: "PNPM package publishing",
223
+ },
224
+ },
225
+ {
226
+ label: "drop database",
227
+ description: "Prompts before SQL statements that drop an entire database.",
228
+ pattern: { pattern: "DROP DATABASE", description: "SQL database drop" },
229
+ },
230
+ {
231
+ label: "drop table",
232
+ description: "Prompts before SQL statements that drop tables.",
233
+ pattern: { pattern: "DROP TABLE", description: "SQL table drop" },
234
+ },
235
+ {
236
+ label: "dbt run",
237
+ description:
238
+ "Prompts before running dbt models that may transform warehouse data.",
239
+ pattern: {
240
+ pattern: "dbt run",
241
+ description: "dbt model execution",
242
+ },
243
+ },
244
+ {
245
+ label: "dbt seed",
246
+ description: "Prompts before loading dbt seed data into a warehouse.",
247
+ pattern: {
248
+ pattern: "dbt seed",
249
+ description: "dbt seed data loading",
250
+ },
251
+ },
252
+ {
253
+ label: "aws s3 rm",
254
+ description: "Prompts before deleting AWS S3 objects.",
255
+ pattern: {
256
+ pattern: "aws s3 rm",
257
+ description: "AWS S3 object deletion",
258
+ },
259
+ },
260
+ {
261
+ label: "aws iam",
262
+ description:
263
+ "Prompts before AWS IAM commands that may change identities or permissions.",
264
+ pattern: {
265
+ pattern: "aws iam",
266
+ description: "AWS IAM permission changes",
267
+ },
268
+ },
269
+ {
270
+ label: "aws ec2 terminate",
271
+ description: "Prompts before terminating AWS EC2 instances.",
272
+ pattern: {
273
+ pattern: "aws ec2 terminate-instances",
274
+ description: "AWS EC2 instance termination",
275
+ },
276
+ },
277
+ {
278
+ label: "kubectl apply",
279
+ description: "Prompts before applying Kubernetes resource changes.",
280
+ pattern: {
281
+ pattern: "kubectl apply",
282
+ description: "Kubernetes resource application",
283
+ },
284
+ },
285
+ {
286
+ label: "kubectl scale",
287
+ description: "Prompts before scaling Kubernetes workloads.",
288
+ pattern: {
289
+ pattern: "kubectl scale",
290
+ description: "Kubernetes scaling operation",
291
+ },
292
+ },
293
+ {
294
+ label: "docker rm",
295
+ description: "Prompts before removing Docker containers.",
296
+ pattern: {
297
+ pattern: "docker rm",
298
+ description: "Docker container removal",
299
+ },
300
+ },
301
+ {
302
+ label: "docker rmi",
303
+ description: "Prompts before removing Docker images.",
304
+ pattern: {
305
+ pattern: "docker rmi",
306
+ description: "Docker image removal",
307
+ },
308
+ },
309
+ {
310
+ label: "docker compose down",
311
+ description: "Prompts before tearing down Docker Compose services.",
312
+ pattern: {
313
+ pattern: "docker compose down",
314
+ description: "Docker Compose service teardown",
315
+ },
316
+ },
317
+ {
318
+ label: "terraform import",
319
+ description:
320
+ "Prompts before importing existing infrastructure into Terraform state.",
321
+ pattern: {
322
+ pattern: "terraform import",
323
+ description: "Terraform resource import",
324
+ },
325
+ },
326
+ {
327
+ label: "gcloud compute delete",
328
+ description: "Prompts before deleting Google Cloud compute instances.",
329
+ pattern: {
330
+ pattern: "gcloud compute instances delete",
331
+ description: "GCP compute instance deletion",
332
+ },
333
+ },
334
+ {
335
+ label: "gcloud iam",
336
+ description:
337
+ "Prompts before Google Cloud IAM commands that may change permissions.",
338
+ pattern: {
339
+ pattern: "gcloud iam",
340
+ description: "GCP IAM permission changes",
341
+ },
342
+ },
343
+ {
344
+ label: "gcloud sql delete",
345
+ description: "Prompts before deleting Google Cloud SQL instances.",
346
+ pattern: {
347
+ pattern: "gcloud sql instances delete",
348
+ description: "GCP Cloud SQL instance deletion",
349
+ },
350
+ },
351
+ ];
352
+
353
+ export function appendPolicyRule(
354
+ config: GuardrailsConfig | null,
355
+ example: PolicyRule,
356
+ ): GuardrailsConfig {
357
+ const next = structuredClone(config ?? {}) as GuardrailsConfig;
358
+ const currentRules = next.policies?.rules ?? [];
359
+
360
+ const existingIds = new Set(currentRules.map((rule) => rule.id));
361
+ const baseId =
362
+ toKebabCase(example.id || example.name || "example") || "example";
363
+ let id = baseId;
364
+ let i = 2;
365
+ while (existingIds.has(id)) {
366
+ id = `${baseId}-${i}`;
367
+ i++;
368
+ }
369
+
370
+ const rule = structuredClone(example);
371
+ rule.id = id;
372
+
373
+ next.policies = {
374
+ ...(next.policies ?? {}),
375
+ rules: [...currentRules, rule],
376
+ };
377
+
378
+ return next;
379
+ }
380
+
381
+ export function appendDangerousPattern(
382
+ config: GuardrailsConfig | null,
383
+ pattern: DangerousPattern,
384
+ ): GuardrailsConfig {
385
+ const next = structuredClone(config ?? {}) as GuardrailsConfig;
386
+ const currentPatterns = next.permissionGate?.patterns ?? [];
387
+
388
+ const existingPatterns = new Set(currentPatterns.map((p) => p.pattern));
389
+ if (existingPatterns.has(pattern.pattern)) {
390
+ return next;
391
+ }
392
+
393
+ next.permissionGate = {
394
+ ...(next.permissionGate ?? {}),
395
+ patterns: [...currentPatterns, structuredClone(pattern)],
396
+ };
397
+
398
+ return next;
399
+ }