@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.
Files changed (94) 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/{utils → core/shell}/command-args.test.ts +31 -20
  46. package/src/core/shell/index.ts +2 -0
  47. package/src/core/types.ts +55 -0
  48. package/src/shared/config/defaults.ts +118 -0
  49. package/src/shared/config/index.ts +17 -0
  50. package/src/shared/config/loader.ts +64 -0
  51. package/src/shared/config/migration/001-v0-format-upgrade.ts +107 -0
  52. package/src/shared/config/migration/002-strip-toolchain-fields.ts +39 -0
  53. package/src/shared/config/migration/003-strip-command-explainer-fields.ts +42 -0
  54. package/src/shared/config/migration/004-env-files-to-policies.ts +87 -0
  55. package/src/shared/config/migration/005-normalize-allowed-paths.ts +43 -0
  56. package/src/shared/config/migration/006-apply-builtin-defaults.ts +19 -0
  57. package/src/shared/config/migration/007-mark-onboarding-done.ts +25 -0
  58. package/src/shared/config/migration/index.ts +44 -0
  59. package/src/shared/config/migration/version.ts +7 -0
  60. package/src/shared/config/types.ts +141 -0
  61. package/src/shared/events.ts +100 -0
  62. package/src/shared/index.ts +6 -0
  63. package/src/shared/matching.test.ts +86 -0
  64. package/src/{utils → shared}/matching.ts +4 -4
  65. package/src/{utils → shared/paths}/bash-paths.test.ts +11 -2
  66. package/src/{utils → shared/paths}/bash-paths.ts +4 -4
  67. package/src/shared/paths/index.ts +1 -0
  68. package/src/shared/warnings.ts +17 -0
  69. package/docs/defaults.md +0 -140
  70. package/docs/examples.md +0 -170
  71. package/src/commands/onboarding.ts +0 -390
  72. package/src/commands/settings-command.ts +0 -1616
  73. package/src/config.ts +0 -392
  74. package/src/hooks/index.ts +0 -11
  75. package/src/hooks/path-access.ts +0 -395
  76. package/src/hooks/permission-gate/index.test.ts +0 -332
  77. package/src/hooks/permission-gate/index.ts +0 -595
  78. package/src/hooks/policies.ts +0 -322
  79. package/src/index.ts +0 -96
  80. package/src/lib/executor.ts +0 -280
  81. package/src/lib/index.ts +0 -16
  82. package/src/lib/model-resolver.ts +0 -47
  83. package/src/lib/timing.ts +0 -42
  84. package/src/lib/types.ts +0 -115
  85. package/src/utils/events.ts +0 -32
  86. package/src/utils/migration.test.ts +0 -58
  87. package/src/utils/migration.ts +0 -340
  88. package/src/utils/warnings.ts +0 -7
  89. /package/src/{utils/path-access.ts → core/paths/access.ts} +0 -0
  90. /package/src/{utils → core/paths}/path.test.ts +0 -0
  91. /package/src/{utils → core/paths}/path.ts +0 -0
  92. /package/src/{utils/shell-utils.ts → core/shell/ast.ts} +0 -0
  93. /package/src/{utils → core/shell}/command-args.ts +0 -0
  94. /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";
@@ -12,37 +12,45 @@ describe("classifyCommandArgs", () => {
12
12
  ]);
13
13
  });
14
14
 
15
+ it("normalizes command basenames", () => {
16
+ expect(tokens("/usr/bin/awk", ["/aaa/{print}", "./input"])).toEqual([
17
+ "./input",
18
+ ]);
19
+ });
20
+
15
21
  it("ignores awk inline program and keeps file operands", () => {
16
22
  expect(tokens("awk", ["/aaa/{print}", "./input"])).toEqual(["./input"]);
17
23
  });
18
24
 
19
- it("keeps awk -f program files", () => {
20
- expect(tokens("awk", ["-f", "./prog.awk", "./input"])).toEqual([
21
- "./prog.awk",
22
- "./input",
23
- ]);
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"]);
24
30
  });
25
31
 
26
32
  it("ignores sed inline scripts and keeps file operands", () => {
27
33
  expect(tokens("sed", ["s#/old#/new#g", "./file"])).toEqual(["./file"]);
28
34
  });
29
35
 
30
- it("keeps sed -f script files", () => {
31
- expect(tokens("sed", ["-f", "./script.sed", "./file"])).toEqual([
32
- "./script.sed",
33
- "./file",
34
- ]);
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"]);
35
42
  });
36
43
 
37
44
  it("ignores grep patterns and keeps file operands", () => {
38
45
  expect(tokens("grep", ["/api/v1", "./src"])).toEqual(["./src"]);
39
46
  });
40
47
 
41
- it("keeps grep pattern files", () => {
42
- expect(tokens("grep", ["-f", "./patterns", "./src"])).toEqual([
43
- "./patterns",
44
- "./src",
45
- ]);
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"]);
46
54
  });
47
55
 
48
56
  it("keeps find roots and ignores expression patterns", () => {
@@ -57,11 +65,14 @@ describe("classifyCommandArgs", () => {
57
65
  ]);
58
66
  });
59
67
 
60
- it("keeps jq -f filter files", () => {
61
- expect(tokens("jq", ["-f", "./filter.jq", "./data.json"])).toEqual([
62
- "./filter.jq",
63
- "./data.json",
64
- ]);
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"]);
65
76
  });
66
77
 
67
78
  it("ignores interpreter inline code", () => {
@@ -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
+ );
@@ -0,0 +1,107 @@
1
+ import { copyFile, stat } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { addPendingWarning } from "../../warnings";
4
+ import type {
5
+ DangerousPattern,
6
+ GuardrailsConfig,
7
+ PatternConfig,
8
+ } from "../types";
9
+ import { CURRENT_VERSION } from "./version";
10
+
11
+ export function shouldRun(config: GuardrailsConfig): boolean {
12
+ return config.version === undefined;
13
+ }
14
+
15
+ export async function run(
16
+ config: GuardrailsConfig,
17
+ filePath: string,
18
+ ): Promise<GuardrailsConfig> {
19
+ await backupConfig(filePath);
20
+ return migrateV0(config);
21
+ }
22
+
23
+ function migrateV0(config: GuardrailsConfig): GuardrailsConfig {
24
+ const migrated = structuredClone(config);
25
+
26
+ if (migrated.envFiles) {
27
+ if (migrated.envFiles.protectedPatterns) {
28
+ migrated.envFiles.protectedPatterns = migrateStringArray(
29
+ migrated.envFiles.protectedPatterns,
30
+ );
31
+ }
32
+ if (migrated.envFiles.allowedPatterns) {
33
+ migrated.envFiles.allowedPatterns = migrateStringArray(
34
+ migrated.envFiles.allowedPatterns,
35
+ );
36
+ }
37
+ if (migrated.envFiles.protectedDirectories) {
38
+ migrated.envFiles.protectedDirectories = migrateStringArray(
39
+ migrated.envFiles.protectedDirectories,
40
+ );
41
+ }
42
+ }
43
+
44
+ if (migrated.permissionGate) {
45
+ if (migrated.permissionGate.patterns) {
46
+ migrated.permissionGate.patterns = migrateDangerousPatterns(
47
+ migrated.permissionGate.patterns,
48
+ );
49
+ }
50
+ if (migrated.permissionGate.customPatterns) {
51
+ migrated.permissionGate.customPatterns = migrateDangerousPatterns(
52
+ migrated.permissionGate.customPatterns,
53
+ );
54
+ }
55
+ if (migrated.permissionGate.allowedPatterns) {
56
+ migrated.permissionGate.allowedPatterns = migrateStringArray(
57
+ migrated.permissionGate.allowedPatterns,
58
+ );
59
+ }
60
+ if (migrated.permissionGate.autoDenyPatterns) {
61
+ migrated.permissionGate.autoDenyPatterns = migrateStringArray(
62
+ migrated.permissionGate.autoDenyPatterns,
63
+ );
64
+ }
65
+ }
66
+
67
+ migrated.version = CURRENT_VERSION;
68
+ return migrated;
69
+ }
70
+
71
+ function migrateStringArray(
72
+ items: (string | PatternConfig)[],
73
+ ): PatternConfig[] {
74
+ return items.map((item) => {
75
+ if (typeof item === "string") return { pattern: item, regex: true };
76
+ if (item.regex === undefined) return { ...item, regex: true };
77
+ return item;
78
+ });
79
+ }
80
+
81
+ function migrateDangerousPatterns(
82
+ items: (DangerousPattern | { pattern: string; description: string })[],
83
+ ): DangerousPattern[] {
84
+ return items.map((item) => {
85
+ if ("regex" in item && item.regex !== undefined) {
86
+ return item as DangerousPattern;
87
+ }
88
+ return { ...item, regex: true };
89
+ });
90
+ }
91
+
92
+ async function backupConfig(configPath: string): Promise<void> {
93
+ const dir = dirname(configPath);
94
+ const basename = configPath.split("/").pop() ?? "guardrails.json";
95
+ const backupName = basename.replace(".json", ".v0.json");
96
+ const backupPath = resolve(dir, backupName);
97
+
98
+ try {
99
+ await stat(backupPath);
100
+ } catch {
101
+ try {
102
+ await copyFile(configPath, backupPath);
103
+ } catch (err) {
104
+ addPendingWarning(`guardrails: could not back up config: ${err}`);
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,39 @@
1
+ import { addPendingWarning } from "../../warnings";
2
+ import type { GuardrailsConfig } from "../types";
3
+ import { CURRENT_VERSION } from "./version";
4
+
5
+ const REMOVED_FEATURE_KEYS = [
6
+ "preventBrew",
7
+ "preventPython",
8
+ "enforcePackageManager",
9
+ ] as const;
10
+
11
+ export function shouldRun(config: GuardrailsConfig): boolean {
12
+ const raw = config as Record<string, unknown>;
13
+ const features = raw.features as Record<string, unknown> | undefined;
14
+ if (features) {
15
+ for (const key of REMOVED_FEATURE_KEYS) {
16
+ if (key in features) return true;
17
+ }
18
+ }
19
+ return "packageManager" in raw;
20
+ }
21
+
22
+ export function run(config: GuardrailsConfig): GuardrailsConfig {
23
+ addPendingWarning(
24
+ "[guardrails] preventBrew, preventPython, enforcePackageManager, and packageManager " +
25
+ "have been removed from guardrails and moved to @aliou/pi-toolchain. " +
26
+ "These fields will be stripped from your config.",
27
+ );
28
+
29
+ const cleaned = structuredClone(config) as Record<string, unknown>;
30
+ const features = cleaned.features as Record<string, unknown> | undefined;
31
+ if (features) {
32
+ for (const key of REMOVED_FEATURE_KEYS) {
33
+ delete features[key];
34
+ }
35
+ }
36
+ delete cleaned.packageManager;
37
+ cleaned.version = CURRENT_VERSION;
38
+ return cleaned as GuardrailsConfig;
39
+ }