@aliou/pi-guardrails 0.5.3 → 0.6.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 +63 -94
- package/config-schema.ts +37 -25
- package/config.ts +108 -141
- package/events.ts +1 -6
- package/glob-expander.ts +128 -0
- package/hooks/index.ts +0 -6
- package/hooks/permission-gate.ts +243 -142
- package/hooks/protect-env-files.ts +120 -43
- package/index.ts +6 -3
- package/matching.ts +119 -0
- package/migration.ts +135 -0
- package/package.json +7 -4
- package/pattern-editor.ts +61 -10
- package/settings-command.ts +247 -426
- package/shell-utils.ts +139 -0
- package/array-editor.ts +0 -213
- package/hooks/enforce-package-manager.ts +0 -96
- package/hooks/prevent-brew.ts +0 -41
- package/hooks/prevent-python.ts +0 -45
- package/sectioned-settings.ts +0 -345
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
+
import { parse } from "@aliou/sh";
|
|
3
4
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
5
|
import type { ResolvedConfig } from "../config-schema";
|
|
5
6
|
import { emitBlocked } from "../events";
|
|
7
|
+
import { expandGlob, hasGlobChars } from "../glob-expander";
|
|
8
|
+
import { type CompiledPattern, compileFilePatterns } from "../matching";
|
|
9
|
+
import { walkCommands, wordToString } from "../shell-utils";
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* Prevents accessing .env files unless they match an allowed pattern.
|
|
9
13
|
* Protects sensitive environment files from being accessed accidentally.
|
|
10
14
|
*
|
|
11
|
-
*
|
|
15
|
+
* Uses AST-based parsing for bash commands to extract file references,
|
|
16
|
+
* with glob expansion via `fd` when args contain shell glob characters.
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
19
|
async function fileExists(filePath: string): Promise<boolean> {
|
|
@@ -20,48 +25,115 @@ async function fileExists(filePath: string): Promise<boolean> {
|
|
|
20
25
|
}
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
function compilePatterns(patterns: string[]): RegExp[] {
|
|
24
|
-
return patterns
|
|
25
|
-
.map((p) => {
|
|
26
|
-
try {
|
|
27
|
-
return new RegExp(p, "i");
|
|
28
|
-
} catch {
|
|
29
|
-
console.error(`Invalid regex pattern in guardrails config: ${p}`);
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
.filter((r): r is RegExp => r !== null);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
28
|
async function isProtectedEnvFile(
|
|
37
29
|
filePath: string,
|
|
38
|
-
|
|
30
|
+
protectedPatterns: CompiledPattern[],
|
|
31
|
+
allowedPatterns: CompiledPattern[],
|
|
32
|
+
dirPatterns: CompiledPattern[],
|
|
33
|
+
onlyBlockIfExists: boolean,
|
|
39
34
|
): Promise<boolean> {
|
|
40
|
-
const
|
|
41
|
-
const isProtected = protectedRegexes.some((r) => r.test(filePath));
|
|
35
|
+
const isProtected = protectedPatterns.some((p) => p.test(filePath));
|
|
42
36
|
if (!isProtected) return false;
|
|
43
37
|
|
|
44
|
-
const
|
|
45
|
-
const isAllowed = allowedRegexes.some((r) => r.test(filePath));
|
|
38
|
+
const isAllowed = allowedPatterns.some((p) => p.test(filePath));
|
|
46
39
|
if (isAllowed) return false;
|
|
47
40
|
|
|
48
41
|
// Check protected directories (if any configured)
|
|
49
|
-
if (
|
|
50
|
-
const
|
|
51
|
-
const inProtectedDir = dirRegexes.some((r) => r.test(filePath));
|
|
42
|
+
if (dirPatterns.length > 0) {
|
|
43
|
+
const inProtectedDir = dirPatterns.some((p) => p.test(filePath));
|
|
52
44
|
if (inProtectedDir) {
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
|
|
45
|
+
return onlyBlockIfExists ? await fileExists(filePath) : true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return onlyBlockIfExists ? await fileExists(filePath) : true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extract file references from a bash command using AST parsing.
|
|
54
|
+
* Falls back to regex extraction on parse failure.
|
|
55
|
+
*/
|
|
56
|
+
async function extractBashFileTargets(command: string): Promise<string[]> {
|
|
57
|
+
try {
|
|
58
|
+
const { ast } = parse(command);
|
|
59
|
+
const files: string[] = [];
|
|
60
|
+
|
|
61
|
+
walkCommands(ast, (cmd) => {
|
|
62
|
+
const words = (cmd.words ?? []).map(wordToString);
|
|
63
|
+
// Skip command name (words[0]), check args for env file references
|
|
64
|
+
for (let i = 1; i < words.length; i++) {
|
|
65
|
+
const arg = words[i] as string;
|
|
66
|
+
if (isEnvLikeReference(arg)) {
|
|
67
|
+
files.push(arg);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Also check redirect targets
|
|
72
|
+
for (const redir of cmd.redirects ?? []) {
|
|
73
|
+
const target = wordToString(redir.target);
|
|
74
|
+
if (isEnvLikeReference(target)) {
|
|
75
|
+
files.push(target);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Expand globs
|
|
82
|
+
const expanded: string[] = [];
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
if (hasGlobChars(file)) {
|
|
85
|
+
const matches = await expandGlob(file);
|
|
86
|
+
if (matches.length > 0) {
|
|
87
|
+
expanded.push(...matches);
|
|
88
|
+
} else {
|
|
89
|
+
// Expansion returned nothing -- could be fd not found or no matches.
|
|
90
|
+
// Keep original as-is so pattern matching can still catch it.
|
|
91
|
+
expanded.push(file);
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
expanded.push(file);
|
|
95
|
+
}
|
|
56
96
|
}
|
|
97
|
+
|
|
98
|
+
return expanded;
|
|
99
|
+
} catch {
|
|
100
|
+
// Fallback: regex extraction from raw string
|
|
101
|
+
return extractEnvFilesRegex(command);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if a string looks like an env file reference.
|
|
107
|
+
* Matches anything containing ".env" as a path component.
|
|
108
|
+
*/
|
|
109
|
+
function isEnvLikeReference(arg: string): boolean {
|
|
110
|
+
// Must contain ".env" somewhere
|
|
111
|
+
if (!arg.includes(".env") && !arg.includes(".dev.vars")) return false;
|
|
112
|
+
// Skip flags
|
|
113
|
+
if (arg.startsWith("-") && !arg.startsWith("-/") && !arg.startsWith("-."))
|
|
114
|
+
return false;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Fallback regex extraction for env file references in bash commands.
|
|
120
|
+
*/
|
|
121
|
+
function extractEnvFilesRegex(command: string): string[] {
|
|
122
|
+
const files: string[] = [];
|
|
123
|
+
const envFileRegex =
|
|
124
|
+
/(?:^|\s|[<>|;&"'`])([^\s<>|;&"'`]*\.env[^\s<>|;&"'`]*)(?:\s|$|[<>|;&"'`])/gi;
|
|
125
|
+
|
|
126
|
+
for (const match of command.matchAll(envFileRegex)) {
|
|
127
|
+
const file = match[1];
|
|
128
|
+
if (file) files.push(file);
|
|
57
129
|
}
|
|
58
130
|
|
|
59
|
-
return
|
|
131
|
+
return files;
|
|
60
132
|
}
|
|
61
133
|
|
|
62
134
|
interface ToolProtectionRule {
|
|
63
135
|
tools: string[];
|
|
64
|
-
extractTargets: (input: Record<string, unknown>) => string[]
|
|
136
|
+
extractTargets: (input: Record<string, unknown>) => Promise<string[]>;
|
|
65
137
|
shouldBlock: (target: string) => Promise<boolean>;
|
|
66
138
|
blockMessage: (target: string) => string;
|
|
67
139
|
}
|
|
@@ -72,36 +144,41 @@ export function setupProtectEnvFilesHook(
|
|
|
72
144
|
) {
|
|
73
145
|
if (!config.features.protectEnvFiles) return;
|
|
74
146
|
|
|
147
|
+
const protectedPatterns = compileFilePatterns(
|
|
148
|
+
config.envFiles.protectedPatterns,
|
|
149
|
+
);
|
|
150
|
+
const allowedPatterns = compileFilePatterns(config.envFiles.allowedPatterns);
|
|
151
|
+
const dirPatterns = compileFilePatterns(config.envFiles.protectedDirectories);
|
|
152
|
+
|
|
153
|
+
const shouldBlock = (target: string) =>
|
|
154
|
+
isProtectedEnvFile(
|
|
155
|
+
target,
|
|
156
|
+
protectedPatterns,
|
|
157
|
+
allowedPatterns,
|
|
158
|
+
dirPatterns,
|
|
159
|
+
config.envFiles.onlyBlockIfExists,
|
|
160
|
+
);
|
|
161
|
+
|
|
75
162
|
const protectionRules: ToolProtectionRule[] = [
|
|
76
163
|
{
|
|
77
164
|
tools: config.envFiles.protectedTools.filter((t) =>
|
|
78
165
|
["read", "write", "edit", "grep", "find", "ls"].includes(t),
|
|
79
166
|
),
|
|
80
|
-
extractTargets: (input) => {
|
|
167
|
+
extractTargets: async (input) => {
|
|
81
168
|
const path = String(input.file_path ?? input.path ?? "");
|
|
82
169
|
return path ? [path] : [];
|
|
83
170
|
},
|
|
84
|
-
shouldBlock
|
|
171
|
+
shouldBlock,
|
|
85
172
|
blockMessage: (target) =>
|
|
86
173
|
config.envFiles.blockMessage.replace("{file}", target),
|
|
87
174
|
},
|
|
88
175
|
{
|
|
89
176
|
tools: config.envFiles.protectedTools.includes("bash") ? ["bash"] : [],
|
|
90
|
-
extractTargets: (input) => {
|
|
177
|
+
extractTargets: async (input) => {
|
|
91
178
|
const command = String(input.command ?? "");
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const envFileRegex =
|
|
95
|
-
/(?:^|\s|[<>|;&"'`])([^\s<>|;&"'`]*\.env[^\s<>|;&"'`]*)(?:\s|$|[<>|;&"'`])/gi;
|
|
96
|
-
|
|
97
|
-
for (const match of command.matchAll(envFileRegex)) {
|
|
98
|
-
const file = match[1];
|
|
99
|
-
if (file) files.push(file);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return files;
|
|
179
|
+
return extractBashFileTargets(command);
|
|
103
180
|
},
|
|
104
|
-
shouldBlock
|
|
181
|
+
shouldBlock,
|
|
105
182
|
blockMessage: (target) =>
|
|
106
183
|
`Command references protected file ${target}. ` +
|
|
107
184
|
config.envFiles.blockMessage.replace("{file}", target),
|
|
@@ -120,7 +197,7 @@ export function setupProtectEnvFilesHook(
|
|
|
120
197
|
const rule = rulesByTool.get(event.toolName);
|
|
121
198
|
if (!rule) return;
|
|
122
199
|
|
|
123
|
-
const targets = rule.extractTargets(event.input);
|
|
200
|
+
const targets = await rule.extractTargets(event.input);
|
|
124
201
|
|
|
125
202
|
for (const target of targets) {
|
|
126
203
|
if (await rule.shouldBlock(target)) {
|
package/index.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { configLoader } from "./config";
|
|
3
3
|
import { setupGuardrailsHooks } from "./hooks";
|
|
4
|
-
import {
|
|
4
|
+
import { registerGuardrailsSettings } from "./settings-command";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Guardrails Extension
|
|
8
8
|
*
|
|
9
9
|
* Security hooks to prevent potentially dangerous operations:
|
|
10
|
-
* - prevent-brew: Blocks Homebrew commands (project uses Nix)
|
|
11
10
|
* - protect-env-files: Prevents access to .env files (except .example/.sample/.test)
|
|
12
11
|
* - permission-gate: Prompts for confirmation on dangerous commands
|
|
13
12
|
*
|
|
13
|
+
* Toolchain features (preventBrew, preventPython, enforcePackageManager,
|
|
14
|
+
* packageManager) have been moved to @aliou/pi-toolchain. Old configs
|
|
15
|
+
* containing these fields are auto-migrated on first load.
|
|
16
|
+
*
|
|
14
17
|
* Configuration:
|
|
15
18
|
* - Global: ~/.pi/agent/extensions/guardrails.json
|
|
16
19
|
* - Project: .pi/extensions/guardrails.json
|
|
@@ -23,5 +26,5 @@ export default async function (pi: ExtensionAPI) {
|
|
|
23
26
|
if (!config.enabled) return;
|
|
24
27
|
|
|
25
28
|
setupGuardrailsHooks(pi, config);
|
|
26
|
-
|
|
29
|
+
registerGuardrailsSettings(pi);
|
|
27
30
|
}
|
package/matching.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern compilation for guardrails matching.
|
|
3
|
+
*
|
|
4
|
+
* Two contexts with different default semantics:
|
|
5
|
+
* - File context: default is glob matching against filename.
|
|
6
|
+
* - Command context: default is substring matching against raw command string.
|
|
7
|
+
*
|
|
8
|
+
* Both support `regex: true` for full regex matching.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { PatternConfig } from "./config-schema";
|
|
12
|
+
|
|
13
|
+
export interface CompiledPattern {
|
|
14
|
+
test: (input: string) => boolean;
|
|
15
|
+
source: PatternConfig;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Convert a glob pattern to a regex.
|
|
20
|
+
* `*` matches any non-`/` chars, `?` matches a single char.
|
|
21
|
+
* The rest is escaped.
|
|
22
|
+
*/
|
|
23
|
+
export function globToRegex(glob: string): RegExp {
|
|
24
|
+
let regex = "";
|
|
25
|
+
for (const ch of glob) {
|
|
26
|
+
switch (ch) {
|
|
27
|
+
case "*":
|
|
28
|
+
regex += "[^/]*";
|
|
29
|
+
break;
|
|
30
|
+
case "?":
|
|
31
|
+
regex += "[^/]";
|
|
32
|
+
break;
|
|
33
|
+
case ".":
|
|
34
|
+
case "(":
|
|
35
|
+
case ")":
|
|
36
|
+
case "+":
|
|
37
|
+
case "^":
|
|
38
|
+
case "$":
|
|
39
|
+
case "{":
|
|
40
|
+
case "}":
|
|
41
|
+
case "|":
|
|
42
|
+
case "\\":
|
|
43
|
+
case "[":
|
|
44
|
+
case "]":
|
|
45
|
+
regex += `\\${ch}`;
|
|
46
|
+
break;
|
|
47
|
+
default:
|
|
48
|
+
regex += ch;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return new RegExp(`^${regex}$`, "i");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compile a single pattern for file-context matching.
|
|
56
|
+
* Default: glob against the basename of the path.
|
|
57
|
+
* regex: true -> full regex (case-insensitive) against the full path.
|
|
58
|
+
*/
|
|
59
|
+
export function compileFilePattern(config: PatternConfig): CompiledPattern {
|
|
60
|
+
if (config.regex) {
|
|
61
|
+
try {
|
|
62
|
+
const re = new RegExp(config.pattern, "i");
|
|
63
|
+
return { test: (input) => re.test(input), source: config };
|
|
64
|
+
} catch {
|
|
65
|
+
console.error(`Invalid regex in guardrails config: ${config.pattern}`);
|
|
66
|
+
return { test: () => false, source: config };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const re = globToRegex(config.pattern);
|
|
71
|
+
return {
|
|
72
|
+
test: (input) => {
|
|
73
|
+
// Match against basename
|
|
74
|
+
const basename = input.split("/").pop() ?? input;
|
|
75
|
+
return re.test(basename);
|
|
76
|
+
},
|
|
77
|
+
source: config,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Compile a single pattern for command-context matching.
|
|
83
|
+
* Default: substring match against raw command string.
|
|
84
|
+
* regex: true -> full regex against raw command string.
|
|
85
|
+
*/
|
|
86
|
+
export function compileCommandPattern(config: PatternConfig): CompiledPattern {
|
|
87
|
+
if (config.regex) {
|
|
88
|
+
try {
|
|
89
|
+
const re = new RegExp(config.pattern);
|
|
90
|
+
return { test: (input) => re.test(input), source: config };
|
|
91
|
+
} catch {
|
|
92
|
+
console.error(`Invalid regex in guardrails config: ${config.pattern}`);
|
|
93
|
+
return { test: () => false, source: config };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
test: (input) => input.includes(config.pattern),
|
|
99
|
+
source: config,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Compile an array of patterns for file-context matching.
|
|
105
|
+
*/
|
|
106
|
+
export function compileFilePatterns(
|
|
107
|
+
configs: PatternConfig[],
|
|
108
|
+
): CompiledPattern[] {
|
|
109
|
+
return configs.map(compileFilePattern);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Compile an array of patterns for command-context matching.
|
|
114
|
+
*/
|
|
115
|
+
export function compileCommandPatterns(
|
|
116
|
+
configs: PatternConfig[],
|
|
117
|
+
): CompiledPattern[] {
|
|
118
|
+
return configs.map(compileCommandPattern);
|
|
119
|
+
}
|
package/migration.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config migration from v0 (no version field) to current format.
|
|
3
|
+
*
|
|
4
|
+
* v0 configs store patterns as plain strings (regex). The migration
|
|
5
|
+
* converts them to PatternConfig objects with `regex: true` to preserve
|
|
6
|
+
* existing behavior.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { copyFile, stat } from "node:fs/promises";
|
|
10
|
+
import { dirname, resolve } from "node:path";
|
|
11
|
+
import type {
|
|
12
|
+
DangerousPattern,
|
|
13
|
+
GuardrailsConfig,
|
|
14
|
+
PatternConfig,
|
|
15
|
+
} from "./config-schema";
|
|
16
|
+
|
|
17
|
+
export const CURRENT_VERSION = "0.6.0-20260204";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if a config needs migration (no version field = v0).
|
|
21
|
+
*/
|
|
22
|
+
export function needsMigration(config: GuardrailsConfig): boolean {
|
|
23
|
+
return config.version === undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Migrate a v0 config to the current format.
|
|
28
|
+
* All string patterns become `{ pattern, regex: true }` to preserve behavior.
|
|
29
|
+
*/
|
|
30
|
+
export function migrateV0(config: GuardrailsConfig): GuardrailsConfig {
|
|
31
|
+
const migrated = structuredClone(config);
|
|
32
|
+
|
|
33
|
+
// Migrate envFiles patterns
|
|
34
|
+
if (migrated.envFiles) {
|
|
35
|
+
if (migrated.envFiles.protectedPatterns) {
|
|
36
|
+
migrated.envFiles.protectedPatterns = migrateStringArray(
|
|
37
|
+
migrated.envFiles.protectedPatterns,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (migrated.envFiles.allowedPatterns) {
|
|
41
|
+
migrated.envFiles.allowedPatterns = migrateStringArray(
|
|
42
|
+
migrated.envFiles.allowedPatterns,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (migrated.envFiles.protectedDirectories) {
|
|
46
|
+
migrated.envFiles.protectedDirectories = migrateStringArray(
|
|
47
|
+
migrated.envFiles.protectedDirectories,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Migrate permissionGate patterns
|
|
53
|
+
if (migrated.permissionGate) {
|
|
54
|
+
if (migrated.permissionGate.patterns) {
|
|
55
|
+
migrated.permissionGate.patterns = migrateDangerousPatterns(
|
|
56
|
+
migrated.permissionGate.patterns,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (migrated.permissionGate.customPatterns) {
|
|
60
|
+
migrated.permissionGate.customPatterns = migrateDangerousPatterns(
|
|
61
|
+
migrated.permissionGate.customPatterns,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (migrated.permissionGate.allowedPatterns) {
|
|
65
|
+
migrated.permissionGate.allowedPatterns = migrateStringArray(
|
|
66
|
+
migrated.permissionGate.allowedPatterns,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (migrated.permissionGate.autoDenyPatterns) {
|
|
70
|
+
migrated.permissionGate.autoDenyPatterns = migrateStringArray(
|
|
71
|
+
migrated.permissionGate.autoDenyPatterns,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
migrated.version = CURRENT_VERSION;
|
|
77
|
+
return migrated;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Migrate a string[] or PatternConfig[] to PatternConfig[] with regex: true.
|
|
82
|
+
* Handles mixed arrays (some already migrated, some still strings).
|
|
83
|
+
*/
|
|
84
|
+
function migrateStringArray(
|
|
85
|
+
items: (string | PatternConfig)[],
|
|
86
|
+
): PatternConfig[] {
|
|
87
|
+
return items.map((item) => {
|
|
88
|
+
if (typeof item === "string") {
|
|
89
|
+
return { pattern: item, regex: true };
|
|
90
|
+
}
|
|
91
|
+
// Already a PatternConfig, ensure regex is set
|
|
92
|
+
if (item.regex === undefined) {
|
|
93
|
+
return { ...item, regex: true };
|
|
94
|
+
}
|
|
95
|
+
return item;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Migrate dangerous pattern arrays. Handles both legacy
|
|
101
|
+
* `{ pattern: string, description: string }` and already-migrated formats.
|
|
102
|
+
*/
|
|
103
|
+
function migrateDangerousPatterns(
|
|
104
|
+
items: (DangerousPattern | { pattern: string; description: string })[],
|
|
105
|
+
): DangerousPattern[] {
|
|
106
|
+
return items.map((item) => {
|
|
107
|
+
if ("regex" in item && item.regex !== undefined) {
|
|
108
|
+
return item as DangerousPattern;
|
|
109
|
+
}
|
|
110
|
+
return { ...item, regex: true };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Back up a config file before migration.
|
|
116
|
+
* Creates `<name>.v0.json` in the same directory.
|
|
117
|
+
* Skips if backup already exists.
|
|
118
|
+
*/
|
|
119
|
+
export async function backupConfig(configPath: string): Promise<void> {
|
|
120
|
+
const dir = dirname(configPath);
|
|
121
|
+
const basename = configPath.split("/").pop() ?? "guardrails.json";
|
|
122
|
+
const backupName = basename.replace(".json", ".v0.json");
|
|
123
|
+
const backupPath = resolve(dir, backupName);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await stat(backupPath);
|
|
127
|
+
// Backup already exists, skip
|
|
128
|
+
} catch {
|
|
129
|
+
try {
|
|
130
|
+
await copyFile(configPath, backupPath);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.warn(`guardrails: could not back up config: ${err}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-guardrails",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"keywords": [
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"extensions": [
|
|
19
19
|
"./index.ts"
|
|
20
20
|
],
|
|
21
|
-
"video": "https://assets.aliou.me/pi-extensions/
|
|
21
|
+
"video": "https://assets.aliou.me/pi-extensions/demos/pi-guardrails.mp4"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public"
|
|
@@ -28,8 +28,11 @@
|
|
|
28
28
|
"hooks",
|
|
29
29
|
"README.md"
|
|
30
30
|
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@aliou/pi-utils-settings": "^0.1.0",
|
|
33
|
+
"@aliou/sh": "github:aliou/sh#v0.1.0"
|
|
34
|
+
},
|
|
31
35
|
"peerDependencies": {
|
|
32
|
-
"@mariozechner/pi-coding-agent": "0.51.0"
|
|
33
|
-
"@mariozechner/pi-tui": "0.51.0"
|
|
36
|
+
"@mariozechner/pi-coding-agent": ">=0.51.0"
|
|
34
37
|
}
|
|
35
38
|
}
|