@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
@@ -1,3 +1,6 @@
1
+ import { parse } from "@aliou/sh";
2
+ import { walkCommands, wordToString } from "../shell/ast";
3
+
1
4
  /**
2
5
  * Dangerous command matchers for the permission gate.
3
6
  *
@@ -8,6 +11,29 @@
8
11
 
9
12
  export type StructuralMatcher = (words: string[]) => string | undefined;
10
13
 
14
+ export interface CommandPattern {
15
+ pattern: string;
16
+ description?: string;
17
+ regex?: boolean;
18
+ }
19
+
20
+ export interface CompiledCommandPattern {
21
+ test: (input: string) => boolean;
22
+ source: CommandPattern;
23
+ }
24
+
25
+ export interface DangerousCommandMatch {
26
+ description: string;
27
+ pattern: string;
28
+ }
29
+
30
+ export interface DangerousCommandCheckOptions {
31
+ command: string;
32
+ patterns: readonly CompiledCommandPattern[];
33
+ useBuiltinMatchers: boolean;
34
+ fallbackPatterns: readonly CommandPattern[];
35
+ }
36
+
11
37
  /**
12
38
  * Helper to check if any word starts with a given prefix.
13
39
  */
@@ -332,7 +358,7 @@ export const BUILTIN_KEYWORD_PATTERNS = new Set([
332
358
  */
333
359
  export function matchDangerousCommand(
334
360
  words: string[],
335
- ): { description: string; pattern: string } | undefined {
361
+ ): DangerousCommandMatch | undefined {
336
362
  for (const matcher of BUILTIN_MATCHERS) {
337
363
  const description = matcher(words);
338
364
  if (description) {
@@ -343,3 +369,95 @@ export function matchDangerousCommand(
343
369
  }
344
370
  return undefined;
345
371
  }
372
+
373
+ export function compileCommandPattern(
374
+ config: CommandPattern,
375
+ ): CompiledCommandPattern {
376
+ if (config.regex) {
377
+ try {
378
+ const re = new RegExp(config.pattern);
379
+ return { test: (input) => re.test(input), source: config };
380
+ } catch {
381
+ return { test: () => false, source: config };
382
+ }
383
+ }
384
+
385
+ return {
386
+ test: (input) => input.includes(config.pattern),
387
+ source: config,
388
+ };
389
+ }
390
+
391
+ export function compileCommandPatterns(
392
+ configs: readonly CommandPattern[],
393
+ ): CompiledCommandPattern[] {
394
+ return configs.map(compileCommandPattern);
395
+ }
396
+
397
+ function matchBuiltinDangerous(
398
+ words: string[],
399
+ ): DangerousCommandMatch | undefined {
400
+ if (words.length === 0) return undefined;
401
+ for (const matcher of BUILTIN_MATCHERS) {
402
+ const description = matcher(words);
403
+ if (description) return { description, pattern: "(structural)" };
404
+ }
405
+ return undefined;
406
+ }
407
+
408
+ export function checkDangerousCommand({
409
+ command,
410
+ patterns,
411
+ useBuiltinMatchers,
412
+ fallbackPatterns,
413
+ }: DangerousCommandCheckOptions): DangerousCommandMatch | undefined {
414
+ let parsedSuccessfully = false;
415
+
416
+ if (useBuiltinMatchers) {
417
+ try {
418
+ const { ast } = parse(command);
419
+ parsedSuccessfully = true;
420
+ let match: DangerousCommandMatch | undefined;
421
+ walkCommands(ast, (cmd) => {
422
+ const words = (cmd.words ?? []).map(wordToString);
423
+ const result = matchBuiltinDangerous(words);
424
+ if (result) {
425
+ match = result;
426
+ return true;
427
+ }
428
+ return false;
429
+ });
430
+ if (match) return match;
431
+ } catch {
432
+ for (const pattern of fallbackPatterns) {
433
+ if (command.includes(pattern.pattern)) {
434
+ return {
435
+ description: pattern.description ?? pattern.pattern,
436
+ pattern: pattern.pattern,
437
+ };
438
+ }
439
+ }
440
+ }
441
+ }
442
+
443
+ for (const compiled of patterns) {
444
+ const source = compiled.source;
445
+ if (
446
+ useBuiltinMatchers &&
447
+ parsedSuccessfully &&
448
+ !source.regex &&
449
+ BUILTIN_KEYWORD_PATTERNS.has(source.pattern)
450
+ ) {
451
+ continue;
452
+ }
453
+
454
+ if (compiled.test(command)) {
455
+ return {
456
+ description: source.description ?? source.pattern,
457
+ pattern: source.pattern,
458
+ };
459
+ }
460
+ }
461
+
462
+ return undefined;
463
+ }
@@ -0,0 +1,15 @@
1
+ export type {
2
+ CommandPattern,
3
+ CompiledCommandPattern,
4
+ DangerousCommandCheckOptions,
5
+ DangerousCommandMatch,
6
+ StructuralMatcher,
7
+ } from "./dangerous";
8
+ export {
9
+ BUILTIN_KEYWORD_PATTERNS,
10
+ BUILTIN_MATCHERS,
11
+ checkDangerousCommand,
12
+ compileCommandPattern,
13
+ compileCommandPatterns,
14
+ matchDangerousCommand,
15
+ } from "./dangerous";
@@ -0,0 +1,13 @@
1
+ export { checkAction, resolveDecision } from "./check";
2
+ export * from "./commands";
3
+ export * from "./paths";
4
+ export * from "./shell";
5
+ export type {
6
+ Action,
7
+ Decision,
8
+ Grant,
9
+ PermissionState,
10
+ Rule,
11
+ RuleResult,
12
+ Safety,
13
+ } from "./types";
@@ -1,9 +1,5 @@
1
1
  import { assert, describe, expect, it } from "vitest";
2
- import {
3
- checkPathAccess,
4
- isPathAllowed,
5
- type PathAccessState,
6
- } from "./path-access";
2
+ import { checkPathAccess, isPathAllowed, type PathAccessState } from "./access";
7
3
 
8
4
  describe("isPathAllowed", () => {
9
5
  describe("when allowedPaths is empty", () => {
@@ -0,0 +1,14 @@
1
+ export {
2
+ checkPathAccess,
3
+ isPathAllowed,
4
+ type PathAccessState,
5
+ type PathDecision,
6
+ } from "./access";
7
+ export {
8
+ expandHomePath,
9
+ isWithinBoundary,
10
+ maybePathLike,
11
+ normalizeForDisplay,
12
+ resolveFromCwd,
13
+ toStorageForm,
14
+ } from "./path";
@@ -0,0 +1,142 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { classifyCommandArgs } from "./command-args";
3
+
4
+ const tokens = (command: string, args: string[]) =>
5
+ classifyCommandArgs(command, args).map((arg) => arg.token);
6
+
7
+ describe("classifyCommandArgs", () => {
8
+ it("keeps unknown command arguments unchanged", () => {
9
+ expect(tokens("cat", ["/etc/hosts", "./file"])).toEqual([
10
+ "/etc/hosts",
11
+ "./file",
12
+ ]);
13
+ });
14
+
15
+ it("normalizes command basenames", () => {
16
+ expect(tokens("/usr/bin/awk", ["/aaa/{print}", "./input"])).toEqual([
17
+ "./input",
18
+ ]);
19
+ });
20
+
21
+ it("ignores awk inline program and keeps file operands", () => {
22
+ expect(tokens("awk", ["/aaa/{print}", "./input"])).toEqual(["./input"]);
23
+ });
24
+
25
+ it.each([
26
+ ["-f as separate option", ["-f", "./prog.awk", "./input"]],
27
+ ["-f as joined option", ["-f./prog.awk", "./input"]],
28
+ ])("keeps awk program files with %s", (_label, args) => {
29
+ expect(tokens("awk", args)).toEqual(["./prog.awk", "./input"]);
30
+ });
31
+
32
+ it("ignores sed inline scripts and keeps file operands", () => {
33
+ expect(tokens("sed", ["s#/old#/new#g", "./file"])).toEqual(["./file"]);
34
+ });
35
+
36
+ it.each([
37
+ ["-f as separate option", ["-f", "./script.sed", "./file"]],
38
+ ["--file as long option", ["--file", "./script.sed", "./file"]],
39
+ ["-f as joined option", ["-f./script.sed", "./file"]],
40
+ ])("keeps sed script files with %s", (_label, args) => {
41
+ expect(tokens("sed", args)).toEqual(["./script.sed", "./file"]);
42
+ });
43
+
44
+ it("ignores grep patterns and keeps file operands", () => {
45
+ expect(tokens("grep", ["/api/v1", "./src"])).toEqual(["./src"]);
46
+ });
47
+
48
+ it.each([
49
+ ["-f as separate option", ["-f", "./patterns", "./src"]],
50
+ ["--file as long option", ["--file", "./patterns", "./src"]],
51
+ ["-f as joined option", ["-f./patterns", "./src"]],
52
+ ])("keeps grep pattern files with %s", (_label, args) => {
53
+ expect(tokens("grep", args)).toEqual(["./patterns", "./src"]);
54
+ });
55
+
56
+ it("keeps find roots and ignores expression patterns", () => {
57
+ expect(tokens("find", ["./src", "-regex", ".*/test/.*"])).toEqual([
58
+ "./src",
59
+ ]);
60
+ });
61
+
62
+ it("ignores jq filters and keeps file operands", () => {
63
+ expect(tokens("jq", ['.path | test("^/tmp/")', "./data.json"])).toEqual([
64
+ "./data.json",
65
+ ]);
66
+ });
67
+
68
+ it.each([
69
+ ["-f as separate option", ["-f", "./filter.jq", "./data.json"]],
70
+ [
71
+ "--from-file as long option",
72
+ ["--from-file", "./filter.jq", "./data.json"],
73
+ ],
74
+ ])("keeps jq filter files with %s", (_label, args) => {
75
+ expect(tokens("jq", args)).toEqual(["./filter.jq", "./data.json"]);
76
+ });
77
+
78
+ it("ignores interpreter inline code", () => {
79
+ expect(tokens("python3", ["-c", 'open("/etc/passwd")'])).toEqual([]);
80
+ });
81
+
82
+ it("keeps interpreter script operands", () => {
83
+ expect(tokens("python3", ["./script.py", "./data.json"])).toEqual([
84
+ "./script.py",
85
+ "./data.json",
86
+ ]);
87
+ });
88
+
89
+ it("ignores delimiter args", () => {
90
+ expect(tokens("cut", ["-d", "/", "./file"])).toEqual(["./file"]);
91
+ expect(tokens("sort", ["-t", "/", "./file"])).toEqual(["./file"]);
92
+ expect(tokens("tr", ["/", ":"])).toEqual([]);
93
+ });
94
+
95
+ describe("go subcommand", () => {
96
+ it("skips Go package wildcard patterns", () => {
97
+ expect(tokens("go", ["test", "./..."])).toEqual([]);
98
+ });
99
+
100
+ it("keeps go run .go file operands", () => {
101
+ expect(tokens("go", ["run", "main.go"])).toEqual(["main.go"]);
102
+ });
103
+
104
+ it("skips non-.go positionals for go run", () => {
105
+ expect(tokens("go", ["run", "-exec", "/bin/env", "main.go"])).toEqual([
106
+ "main.go",
107
+ ]);
108
+ });
109
+
110
+ it("skips package patterns for build/vet/list", () => {
111
+ expect(tokens("go", ["build", "./..."])).toEqual([]);
112
+ expect(tokens("go", ["vet", "./pkg/..."])).toEqual([]);
113
+ expect(tokens("go", ["list", "./..."])).toEqual([]);
114
+ });
115
+
116
+ it("keeps file-valued flags", () => {
117
+ expect(tokens("go", ["build", "-modfile", "./go.mod", "./..."])).toEqual([
118
+ "./go.mod",
119
+ ]);
120
+ });
121
+
122
+ it("keeps -o flag value for go build", () => {
123
+ expect(tokens("go", ["build", "-o", "./bin/app", "./..."])).toEqual([
124
+ "./bin/app",
125
+ ]);
126
+ });
127
+
128
+ it("handles -C global flag before subcommand", () => {
129
+ expect(tokens("go", ["-C", "/tmp", "test", "./..."])).toEqual([]);
130
+ });
131
+
132
+ it("handles -C joined form before subcommand", () => {
133
+ expect(tokens("go", ["-C=/tmp", "test", "./..."])).toEqual([]);
134
+ });
135
+
136
+ it("keeps go run .go file operands with -C", () => {
137
+ expect(tokens("go", ["-C", "/tmp", "run", "main.go"])).toEqual([
138
+ "main.go",
139
+ ]);
140
+ });
141
+ });
142
+ });
@@ -32,6 +32,7 @@ export function classifyCommandArgs(
32
32
  ) {
33
33
  return classifyInterpreterArgs(cmd, args);
34
34
  }
35
+ if (cmd === "go") return classifyGoArgs(args);
35
36
  if (cmd === "cut")
36
37
  return skipOptionValues(args, new Set(["-d", "--delimiter"]));
37
38
  if (cmd === "sort")
@@ -224,3 +225,73 @@ function skipOptionValues(
224
225
  }
225
226
  return out;
226
227
  }
228
+
229
+ /**
230
+ * Classify `go` subcommand arguments.
231
+ *
232
+ * Go commands take package patterns, not file paths, as positional args
233
+ * for most subcommands. Package patterns like `./...`, `pkg/...`,
234
+ * or `github.com/user/repo/...` use Go's `...` wildcard and are not
235
+ * filesystem paths.
236
+ *
237
+ * `go run` is an exception: it takes .go file operands, emitted with
238
+ * `forcePath` since bare filenames like `main.go` don't pass
239
+ * `maybePathLike`.
240
+ *
241
+ * File-valued flags (e.g. `-o`, `-modfile`, `-overlay`) are kept
242
+ * so that policy checks can still gate them.
243
+ *
244
+ * Global flags like `-C dir` are handled before subcommand detection
245
+ * so their values aren't mistaken for the subcommand.
246
+ */
247
+ function classifyGoArgs(args: string[]): ClassifiedArg[] {
248
+ const out: ClassifiedArg[] = [];
249
+ const fileFlags = new Set(["-o", "-modfile", "-overlay"]);
250
+
251
+ // Global flags that consume a value and must be skipped before
252
+ // subcommand detection. E.g. `go -C /tmp test ./...`
253
+ const globalFlagsWithValues = new Set(["-C"]);
254
+
255
+ let subcommand: string | undefined;
256
+ for (let i = 0; i < args.length; i++) {
257
+ const arg = args[i] as string;
258
+
259
+ // Handle file-valued flags
260
+ if (fileFlags.has(arg)) {
261
+ if (args[i + 1]) out.push({ token: args[++i] as string });
262
+ continue;
263
+ }
264
+
265
+ // Handle joined file flags like -o=./bin/app
266
+ if (arg.startsWith("-o=")) {
267
+ out.push({ token: arg.slice(3) });
268
+ continue;
269
+ }
270
+
271
+ // Skip global flags with values before subcommand detection
272
+ if (!subcommand && globalFlagsWithValues.has(arg)) {
273
+ i++; // skip value
274
+ continue;
275
+ }
276
+ if (!subcommand && arg.startsWith("-C=")) {
277
+ // joined form -C=/tmp
278
+ continue;
279
+ }
280
+
281
+ if (isOption(arg)) continue;
282
+
283
+ // First non-flag positional is the subcommand
284
+ if (!subcommand) {
285
+ subcommand = arg;
286
+ continue;
287
+ }
288
+
289
+ // `go run` takes .go file operands; emit with forcePath since
290
+ // bare filenames like main.go don't pass maybePathLike.
291
+ // All other subcommands take package patterns; skip them.
292
+ if (subcommand === "run" && arg.endsWith(".go")) {
293
+ out.push({ token: arg, forcePath: true });
294
+ }
295
+ }
296
+ return out;
297
+ }
@@ -0,0 +1,2 @@
1
+ export { walkCommands, wordToString } from "./ast";
2
+ export { type ClassifiedArg, classifyCommandArgs } from "./command-args";
@@ -0,0 +1,55 @@
1
+ export type Action =
2
+ | {
3
+ kind: "file";
4
+ path: string;
5
+ origin?: string;
6
+ }
7
+ | {
8
+ kind: "command";
9
+ command: string;
10
+ origin?: string;
11
+ };
12
+
13
+ export type RuleResult<TMeta = null> =
14
+ | {
15
+ kind: "pass";
16
+ }
17
+ | {
18
+ kind: "match";
19
+ reason: string;
20
+ metadata: TMeta;
21
+ };
22
+
23
+ export type Safety<TMeta = null> =
24
+ | {
25
+ kind: "safe";
26
+ }
27
+ | {
28
+ kind: "dangerous";
29
+ action: Action;
30
+ key: string;
31
+ reason: string;
32
+ metadata: TMeta;
33
+ };
34
+
35
+ export type Rule<TMeta = null> = {
36
+ key: string;
37
+ check: (action: Action) => RuleResult<TMeta> | Promise<RuleResult<TMeta>>;
38
+ };
39
+
40
+ export type PermissionState = "granted" | "prompt" | "denied";
41
+
42
+ export type Grant = "once" | "always" | "never";
43
+
44
+ export type Decision<TMeta = null> =
45
+ | {
46
+ kind: "allow";
47
+ }
48
+ | {
49
+ kind: "deny";
50
+ reason: string;
51
+ }
52
+ | {
53
+ kind: "prompt";
54
+ risk: Safety<TMeta> & { kind: "dangerous" };
55
+ };
@@ -0,0 +1,118 @@
1
+ import { CURRENT_VERSION } from "./migration";
2
+ import type { ResolvedConfig } from "./types";
3
+
4
+ export const DEFAULT_CONFIG: ResolvedConfig = {
5
+ version: CURRENT_VERSION,
6
+ enabled: true,
7
+ applyBuiltinDefaults: true,
8
+ features: {
9
+ policies: true,
10
+ permissionGate: true,
11
+ pathAccess: false,
12
+ },
13
+ pathAccess: {
14
+ mode: "ask",
15
+ allowedPaths: [],
16
+ },
17
+ policies: {
18
+ rules: [
19
+ {
20
+ id: "secret-files",
21
+ description: "Files containing secrets",
22
+ patterns: [
23
+ { pattern: ".env" },
24
+ { pattern: ".env.local" },
25
+ { pattern: ".env.production" },
26
+ { pattern: ".env.prod" },
27
+ { pattern: ".dev.vars" },
28
+ ],
29
+ allowedPatterns: [
30
+ { pattern: "*.example.env" },
31
+ { pattern: "*.sample.env" },
32
+ { pattern: "*.test.env" },
33
+ { pattern: ".env.example" },
34
+ { pattern: ".env.sample" },
35
+ { pattern: ".env.test" },
36
+ ],
37
+ protection: "noAccess",
38
+ onlyIfExists: true,
39
+ blockMessage:
40
+ "Accessing {file} is not allowed. This file contains secrets. " +
41
+ "Explain to the user why you want to access this file, and if changes are needed ask the user to make them.",
42
+ },
43
+ {
44
+ id: "home-ssh",
45
+ description: "SSH directory and keys",
46
+ enabled: false,
47
+ patterns: [
48
+ { pattern: "~/.ssh/**" },
49
+ { pattern: "~/.ssh/*_rsa" },
50
+ { pattern: "~/.ssh/*_ed25519" },
51
+ { pattern: "~/.ssh/*.pem" },
52
+ ],
53
+ allowedPatterns: [{ pattern: "~/.ssh/*.pub" }],
54
+ protection: "noAccess",
55
+ onlyIfExists: true,
56
+ blockMessage:
57
+ "Accessing {file} is not allowed. This file is part of your SSH configuration and may contain private keys or sensitive host information.",
58
+ },
59
+ {
60
+ id: "home-config",
61
+ description: "Sensitive user configuration directories",
62
+ enabled: false,
63
+ patterns: [
64
+ { pattern: "~/.config/gh/**" },
65
+ { pattern: "~/.config/gcloud/**" },
66
+ { pattern: "~/.config/op/**" },
67
+ { pattern: "~/.config/sops/**" },
68
+ ],
69
+ protection: "noAccess",
70
+ onlyIfExists: true,
71
+ blockMessage:
72
+ "Accessing {file} is not allowed. This file is in a sensitive user configuration directory and may contain credentials or tokens.",
73
+ },
74
+ {
75
+ id: "home-gpg",
76
+ description: "GPG keys and configuration",
77
+ enabled: false,
78
+ patterns: [
79
+ { pattern: "~/.gnupg/**" },
80
+ { pattern: "~/*.gpg" },
81
+ { pattern: "~/.gpg-agent.conf" },
82
+ ],
83
+ protection: "noAccess",
84
+ onlyIfExists: true,
85
+ blockMessage:
86
+ "Accessing {file} is not allowed. This file is part of your GPG configuration and may contain private keys or trust settings.",
87
+ },
88
+ ],
89
+ },
90
+ permissionGate: {
91
+ patterns: [
92
+ { pattern: "rm -rf", description: "recursive force delete" },
93
+ { pattern: "sudo", description: "superuser command" },
94
+ { pattern: "dd of=", description: "disk write operation" },
95
+ { pattern: "mkfs.", description: "filesystem format" },
96
+ {
97
+ pattern: "chmod -R 777",
98
+ description: "insecure recursive permissions",
99
+ },
100
+ { pattern: "chown -R", description: "recursive ownership change" },
101
+ { pattern: "doas", description: "privileged command execution" },
102
+ { pattern: "pkexec", description: "privileged command execution" },
103
+ { pattern: "shred", description: "secure file overwrite" },
104
+ { pattern: "wipefs", description: "filesystem signature wipe" },
105
+ { pattern: "blkdiscard", description: "block device discard" },
106
+ { pattern: "fdisk", description: "disk partitioning" },
107
+ { pattern: "parted", description: "disk partitioning" },
108
+ {
109
+ pattern: "docker run --privileged",
110
+ description: "container with privileged mode",
111
+ },
112
+ ],
113
+ useBuiltinMatchers: true,
114
+ requireConfirmation: true,
115
+ allowedPatterns: [],
116
+ autoDenyPatterns: [],
117
+ },
118
+ };
@@ -0,0 +1,17 @@
1
+ export { DEFAULT_CONFIG } from "./defaults";
2
+ export { configLoader } from "./loader";
3
+ export {
4
+ CURRENT_VERSION,
5
+ globalConfigMigrations,
6
+ migrations,
7
+ } from "./migration";
8
+ export type {
9
+ DangerousPattern,
10
+ GuardrailsConfig,
11
+ PathAccessConfig,
12
+ PathAccessMode,
13
+ PatternConfig,
14
+ PolicyRule,
15
+ Protection,
16
+ ResolvedConfig,
17
+ } from "./types";
@@ -0,0 +1,64 @@
1
+ import { buildSchemaUrl, ConfigLoader } from "@aliou/pi-utils-settings";
2
+ import pkg from "../../../package.json" with { type: "json" };
3
+ import { DEFAULT_CONFIG } from "./defaults";
4
+ import { migrations } from "./migration";
5
+ import type { GuardrailsConfig, PolicyRule, ResolvedConfig } from "./types";
6
+
7
+ export const configLoader = new ConfigLoader<GuardrailsConfig, ResolvedConfig>(
8
+ "guardrails",
9
+ DEFAULT_CONFIG,
10
+ {
11
+ scopes: ["global", "local", "memory"],
12
+ migrations,
13
+ schemaUrl: buildSchemaUrl(pkg.name, pkg.version),
14
+ afterMerge: (resolved, global, local, memory) => {
15
+ const ruleMap = new Map<string, PolicyRule>();
16
+
17
+ if (resolved.applyBuiltinDefaults) {
18
+ for (const rule of DEFAULT_CONFIG.policies.rules) {
19
+ ruleMap.set(rule.id, rule);
20
+ }
21
+ }
22
+ if (global?.policies?.rules) {
23
+ for (const rule of global.policies.rules) {
24
+ ruleMap.set(rule.id, rule);
25
+ }
26
+ }
27
+ if (local?.policies?.rules) {
28
+ for (const rule of local.policies.rules) {
29
+ ruleMap.set(rule.id, rule);
30
+ }
31
+ }
32
+ if (memory?.policies?.rules) {
33
+ for (const rule of memory.policies.rules) {
34
+ ruleMap.set(rule.id, rule);
35
+ }
36
+ }
37
+ resolved.policies.rules = [...ruleMap.values()];
38
+
39
+ const customPatterns =
40
+ memory?.permissionGate?.customPatterns ??
41
+ local?.permissionGate?.customPatterns ??
42
+ global?.permissionGate?.customPatterns;
43
+ if (customPatterns) {
44
+ resolved.permissionGate.patterns = customPatterns;
45
+ resolved.permissionGate.useBuiltinMatchers = false;
46
+ }
47
+
48
+ const mergedPaths = new Set<string>();
49
+ for (const paths of [
50
+ global?.pathAccess?.allowedPaths,
51
+ local?.pathAccess?.allowedPaths,
52
+ memory?.pathAccess?.allowedPaths,
53
+ ]) {
54
+ for (const path of paths ?? []) {
55
+ const trimmed = path.trim();
56
+ if (trimmed) mergedPaths.add(trimmed);
57
+ }
58
+ }
59
+ resolved.pathAccess.allowedPaths = [...mergedPaths];
60
+
61
+ return resolved;
62
+ },
63
+ },
64
+ );