@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,47 +0,0 @@
1
- /**
2
- * Model resolution helper for subagents.
3
- *
4
- * Resolves a model by provider + ID from the model registry.
5
- */
6
-
7
- import type { Model } from "@mariozechner/pi-ai";
8
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
9
-
10
- /**
11
- * Find a model by provider and ID.
12
- *
13
- * @param provider - Provider name (e.g., "openrouter", "anthropic", "openai-codex")
14
- * @param modelId - Model ID (e.g., "anthropic/claude-haiku-4.5")
15
- * @param ctx - Extension context with modelRegistry
16
- * @returns The resolved model
17
- * @throws Error if model not found or API key not configured
18
- */
19
- export function resolveModel(
20
- provider: string,
21
- modelId: string,
22
- ctx: ExtensionContext,
23
- // biome-ignore lint/suspicious/noExplicitAny: Model type requires any for generic API
24
- ): Model<any> {
25
- const available = ctx.modelRegistry.getAvailable();
26
- const model = available.find(
27
- (m) => m.id === modelId && m.provider === provider,
28
- );
29
-
30
- if (model) {
31
- return model;
32
- }
33
-
34
- // Check if the model exists but the API key is missing
35
- const all = ctx.modelRegistry.getAll();
36
- const existsWithoutKey = all.some(
37
- (m) => m.id === modelId && m.provider === provider,
38
- );
39
-
40
- if (existsWithoutKey) {
41
- throw new Error(
42
- `Model "${modelId}" exists on ${provider} but no valid API key is configured.`,
43
- );
44
- }
45
-
46
- throw new Error(`Model "${modelId}" not found on provider "${provider}".`);
47
- }
package/src/lib/timing.ts DELETED
@@ -1,42 +0,0 @@
1
- /**
2
- * Shared timing utilities for tool and subagent execution.
3
- */
4
-
5
- /** Minimal shape that supports timing fields. */
6
- export interface TimedExecution {
7
- startedAt?: number;
8
- endedAt?: number;
9
- durationMs?: number;
10
- }
11
-
12
- /** Mark execution start time (epoch ms). */
13
- export function markExecutionStart<T extends TimedExecution>(
14
- target: T,
15
- startedAt = Date.now(),
16
- ): T {
17
- target.startedAt = startedAt;
18
- return target;
19
- }
20
-
21
- /** Mark execution end time and compute duration (epoch ms / ms). */
22
- export function markExecutionEnd<T extends TimedExecution>(
23
- target: T,
24
- endedAt = Date.now(),
25
- ): T {
26
- target.endedAt = endedAt;
27
- if (target.startedAt !== undefined) {
28
- target.durationMs = Math.max(0, endedAt - target.startedAt);
29
- }
30
- return target;
31
- }
32
-
33
- /** Simple wall-clock timer for a full operation (e.g., subagent call). */
34
- export function createExecutionTimer(startedAt = Date.now()): {
35
- startedAt: number;
36
- getDurationMs: (endedAt?: number) => number;
37
- } {
38
- return {
39
- startedAt,
40
- getDurationMs: (endedAt = Date.now()) => Math.max(0, endedAt - startedAt),
41
- };
42
- }
package/src/lib/types.ts DELETED
@@ -1,115 +0,0 @@
1
- import type { AgentTool, ThinkingLevel } from "@mariozechner/pi-agent-core";
2
- import type { Model } from "@mariozechner/pi-ai";
3
- import type { Skill, ToolDefinition } from "@mariozechner/pi-coding-agent";
4
-
5
- /**
6
- * Configuration for a subagent.
7
- */
8
- export interface SubagentConfig {
9
- /** Subagent name (for logging and run ID) */
10
- name: string;
11
-
12
- /** Model instance to use */
13
- // biome-ignore lint/suspicious/noExplicitAny: Model type requires any for generic API
14
- model: Model<any>;
15
-
16
- /** System prompt for the subagent */
17
- systemPrompt: string;
18
-
19
- /** Built-in tools (AgentTool[]) - e.g., from createReadOnlyTools() */
20
- tools?: AgentTool[];
21
-
22
- /** Custom tools (ToolDefinition[]) - e.g., GitHub tools */
23
- customTools?: ToolDefinition[];
24
-
25
- /** Skills to load into system prompt */
26
- skills?: Skill[];
27
-
28
- /** Thinking level. Default: "low" */
29
- thinkingLevel?: ThinkingLevel;
30
-
31
- /** Logging options */
32
- logging?: {
33
- /** Enable logging. Default: false */
34
- enabled: boolean;
35
- /** Include raw events in debug.jsonl. Default: false */
36
- debug?: boolean;
37
- };
38
- }
39
-
40
- /**
41
- * Tool call state for tracking subagent tool executions.
42
- */
43
- export interface SubagentToolCall {
44
- toolCallId: string;
45
- toolName: string;
46
- args: Record<string, unknown>;
47
- status: "running" | "done" | "error";
48
- /** Epoch ms when tool execution started */
49
- startedAt?: number;
50
- /** Epoch ms when tool execution ended */
51
- endedAt?: number;
52
- /** Duration in milliseconds (set when ended) */
53
- durationMs?: number;
54
- result?: unknown;
55
- error?: string;
56
- /** Partial result from tool updates (for progress display) */
57
- partialResult?: {
58
- content: Array<{ type: string; text?: string }>;
59
- details?: unknown;
60
- };
61
- }
62
-
63
- /**
64
- * Usage/cost information from the model response.
65
- */
66
- export interface SubagentUsage {
67
- /** Input tokens from API (if available) */
68
- inputTokens?: number;
69
- /** Output tokens from API (if available) */
70
- outputTokens?: number;
71
- /** Cache read tokens (if available) */
72
- cacheReadTokens?: number;
73
- /** Cache write tokens (if available) */
74
- cacheWriteTokens?: number;
75
- /** Estimated tokens from response length (chars/4) */
76
- estimatedTokens: number;
77
- /** LLM cost in USD (if available) */
78
- llmCost?: number;
79
- /** Tool/API cost in USD (e.g., Exa, GitHub) */
80
- toolCost?: number;
81
- /** Total cost in USD (llmCost + toolCost) */
82
- totalCost?: number;
83
- }
84
-
85
- /**
86
- * Result from executing a subagent.
87
- */
88
- export interface SubagentResult {
89
- /** Final text content from the subagent */
90
- content: string;
91
-
92
- /** Whether the subagent was aborted */
93
- aborted: boolean;
94
-
95
- /** Final tool call states */
96
- toolCalls: SubagentToolCall[];
97
-
98
- /** Total subagent execution duration in milliseconds */
99
- totalDurationMs: number;
100
-
101
- /** Error message if the subagent failed */
102
- error?: string;
103
-
104
- /** Unique run identifier */
105
- runId: string;
106
-
107
- /** Usage/cost information */
108
- usage: SubagentUsage;
109
- }
110
-
111
- /** Callback for text streaming updates */
112
- export type OnTextUpdate = (delta: string, accumulated: string) => void;
113
-
114
- /** Callback for tool execution updates */
115
- export type OnToolUpdate = (toolCalls: SubagentToolCall[]) => void;
@@ -1,32 +0,0 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
-
3
- export const GUARDRAILS_BLOCKED_EVENT = "guardrails:blocked";
4
- export const GUARDRAILS_DANGEROUS_EVENT = "guardrails:dangerous";
5
-
6
- export interface GuardrailsBlockedEvent {
7
- feature: "policies" | "permissionGate" | "pathAccess";
8
- toolName: string;
9
- input: Record<string, unknown>;
10
- reason: string;
11
- userDenied?: boolean;
12
- }
13
-
14
- export interface GuardrailsDangerousEvent {
15
- command: string;
16
- description: string;
17
- pattern: string;
18
- }
19
-
20
- export function emitBlocked(
21
- pi: ExtensionAPI,
22
- event: GuardrailsBlockedEvent,
23
- ): void {
24
- pi.events.emit(GUARDRAILS_BLOCKED_EVENT, event);
25
- }
26
-
27
- export function emitDangerous(
28
- pi: ExtensionAPI,
29
- event: GuardrailsDangerousEvent,
30
- ): void {
31
- pi.events.emit(GUARDRAILS_DANGEROUS_EVENT, event);
32
- }
@@ -1,58 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { GuardrailsConfig } from "../config";
3
- import {
4
- migrateAllowedPaths,
5
- needsAllowedPathsMigration,
6
- normalizeAllowedPaths,
7
- } from "./migration";
8
-
9
- describe("allowedPaths migration", () => {
10
- it("normalizes strings and legacy pattern objects", () => {
11
- expect(
12
- normalizeAllowedPaths([
13
- "/dev/null",
14
- { pattern: "~/Downloads/" },
15
- { pattern: " /tmp/file " },
16
- { pattern: "" },
17
- { regex: true },
18
- 42,
19
- null,
20
- "/dev/null",
21
- ]),
22
- ).toEqual(["/dev/null", "~/Downloads/", "/tmp/file"]);
23
- });
24
-
25
- it("detects legacy object-shaped allowed paths", () => {
26
- const config = {
27
- pathAccess: {
28
- allowedPaths: [{ pattern: "/dev/null" }],
29
- },
30
- } as unknown as GuardrailsConfig;
31
-
32
- expect(needsAllowedPathsMigration(config)).toBe(true);
33
- });
34
-
35
- it("does not migrate valid string allowed paths", () => {
36
- const config: GuardrailsConfig = {
37
- pathAccess: {
38
- allowedPaths: ["/dev/null"],
39
- },
40
- };
41
-
42
- expect(needsAllowedPathsMigration(config)).toBe(false);
43
- });
44
-
45
- it("converts legacy object-shaped allowed paths to strings", () => {
46
- const config = {
47
- pathAccess: {
48
- mode: "block",
49
- allowedPaths: [{ pattern: "/dev/null" }, "~/Downloads/"],
50
- },
51
- } as unknown as GuardrailsConfig;
52
-
53
- expect(migrateAllowedPaths(config).pathAccess?.allowedPaths).toEqual([
54
- "/dev/null",
55
- "~/Downloads/",
56
- ]);
57
- });
58
- });
@@ -1,340 +0,0 @@
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";
16
- import { pendingWarnings } from "./warnings";
17
-
18
- /**
19
- * Config schema version.
20
- *
21
- * Keep this independent from package.json version.
22
- * Bump only when config schema/default migration markers change.
23
- */
24
- export const CURRENT_VERSION = "0.9.0-20260327";
25
-
26
- /**
27
- * Check if a config needs migration (no version field = v0).
28
- */
29
- export function needsMigration(config: GuardrailsConfig): boolean {
30
- return config.version === undefined;
31
- }
32
-
33
- /**
34
- * Migrate a v0 config to the current format.
35
- * All string patterns become `{ pattern, regex: true }` to preserve behavior.
36
- */
37
- export function migrateV0(config: GuardrailsConfig): GuardrailsConfig {
38
- const migrated = structuredClone(config);
39
-
40
- // Migrate envFiles patterns
41
- if (migrated.envFiles) {
42
- if (migrated.envFiles.protectedPatterns) {
43
- migrated.envFiles.protectedPatterns = migrateStringArray(
44
- migrated.envFiles.protectedPatterns,
45
- );
46
- }
47
- if (migrated.envFiles.allowedPatterns) {
48
- migrated.envFiles.allowedPatterns = migrateStringArray(
49
- migrated.envFiles.allowedPatterns,
50
- );
51
- }
52
- if (migrated.envFiles.protectedDirectories) {
53
- migrated.envFiles.protectedDirectories = migrateStringArray(
54
- migrated.envFiles.protectedDirectories,
55
- );
56
- }
57
- }
58
-
59
- // Migrate permissionGate patterns
60
- if (migrated.permissionGate) {
61
- if (migrated.permissionGate.patterns) {
62
- migrated.permissionGate.patterns = migrateDangerousPatterns(
63
- migrated.permissionGate.patterns,
64
- );
65
- }
66
- if (migrated.permissionGate.customPatterns) {
67
- migrated.permissionGate.customPatterns = migrateDangerousPatterns(
68
- migrated.permissionGate.customPatterns,
69
- );
70
- }
71
- if (migrated.permissionGate.allowedPatterns) {
72
- migrated.permissionGate.allowedPatterns = migrateStringArray(
73
- migrated.permissionGate.allowedPatterns,
74
- );
75
- }
76
- if (migrated.permissionGate.autoDenyPatterns) {
77
- migrated.permissionGate.autoDenyPatterns = migrateStringArray(
78
- migrated.permissionGate.autoDenyPatterns,
79
- );
80
- }
81
- }
82
-
83
- migrated.version = CURRENT_VERSION;
84
- return migrated;
85
- }
86
-
87
- /**
88
- * Check if a config still uses deprecated envFiles/protectEnvFiles fields.
89
- */
90
- export function needsEnvFilesToPoliciesMigration(
91
- config: GuardrailsConfig,
92
- ): boolean {
93
- const raw = config as Record<string, unknown>;
94
- if (raw.envFiles !== undefined) return true;
95
-
96
- const features = raw.features as Record<string, unknown> | undefined;
97
- return features?.protectEnvFiles !== undefined;
98
- }
99
-
100
- /**
101
- * Check if config needs applyBuiltinDefaults bridge migration.
102
- * This runs only for existing config files loaded by ConfigLoader.
103
- */
104
- export function needsApplyBuiltinDefaultsMigration(
105
- config: GuardrailsConfig,
106
- ): boolean {
107
- return config.applyBuiltinDefaults === undefined;
108
- }
109
-
110
- /**
111
- * Bridge migration for defaults deprecation.
112
- * Existing config files get applyBuiltinDefaults=true to preserve behavior.
113
- */
114
- export function migrateApplyBuiltinDefaults(
115
- config: GuardrailsConfig,
116
- ): GuardrailsConfig {
117
- const migrated = structuredClone(config);
118
- migrated.applyBuiltinDefaults = true;
119
- migrated.version = CURRENT_VERSION;
120
-
121
- pendingWarnings.push(
122
- "Guardrails config was migrated. `applyBuiltinDefaults` was set to `true` to preserve current behavior.",
123
- );
124
-
125
- return migrated;
126
- }
127
-
128
- export function needsOnboardingDoneMigration(
129
- config: GuardrailsConfig,
130
- ): boolean {
131
- return (
132
- config.onboarding?.completed === undefined &&
133
- config.applyBuiltinDefaults !== undefined
134
- );
135
- }
136
-
137
- export function migrateMarkOnboardingDone(
138
- config: GuardrailsConfig,
139
- ): GuardrailsConfig {
140
- const migrated = structuredClone(config);
141
- pendingWarnings.push(
142
- "Guardrails config was migrated. Existing setup marked as onboarding-complete.",
143
- );
144
- migrated.onboarding = {
145
- ...(migrated.onboarding ?? {}),
146
- completed: true,
147
- completedAt: migrated.onboarding?.completedAt ?? new Date().toISOString(),
148
- version: migrated.onboarding?.version ?? CURRENT_VERSION,
149
- };
150
- migrated.version = CURRENT_VERSION;
151
- return migrated;
152
- }
153
-
154
- /**
155
- * Migrate allowedPaths entries accidentally written as PatternConfig objects.
156
- */
157
- export function needsAllowedPathsMigration(config: GuardrailsConfig): boolean {
158
- const raw = config as Record<string, unknown>;
159
- const pathAccess = raw.pathAccess as Record<string, unknown> | undefined;
160
- if (!Array.isArray(pathAccess?.allowedPaths)) return false;
161
- return pathAccess.allowedPaths.some((item) => typeof item !== "string");
162
- }
163
-
164
- export function normalizeAllowedPaths(items: unknown): string[] {
165
- if (!Array.isArray(items)) return [];
166
-
167
- const paths = new Set<string>();
168
- for (const item of items) {
169
- let path: string | null = null;
170
- if (typeof item === "string") {
171
- path = item;
172
- } else if (typeof item === "object" && item !== null) {
173
- const pattern = (item as Record<string, unknown>).pattern;
174
- if (typeof pattern === "string") path = pattern;
175
- }
176
-
177
- const normalized = path?.trim();
178
- if (normalized) paths.add(normalized);
179
- }
180
-
181
- return [...paths];
182
- }
183
-
184
- export function migrateAllowedPaths(
185
- config: GuardrailsConfig,
186
- ): GuardrailsConfig {
187
- const migrated = structuredClone(config) as Record<string, unknown>;
188
- const pathAccess = migrated.pathAccess as Record<string, unknown> | undefined;
189
- if (!pathAccess) return migrated as GuardrailsConfig;
190
-
191
- pathAccess.allowedPaths = normalizeAllowedPaths(pathAccess.allowedPaths);
192
- migrated.version = CURRENT_VERSION;
193
- pendingWarnings.push(
194
- "[guardrails] pathAccess.allowedPaths was migrated from pattern objects to path strings.",
195
- );
196
- return migrated as GuardrailsConfig;
197
- }
198
-
199
- /**
200
- * Migrate deprecated envFiles/protectEnvFiles fields to policies.
201
- */
202
- export function migrateEnvFilesToPolicies(
203
- config: GuardrailsConfig,
204
- ): GuardrailsConfig {
205
- const migrated = structuredClone(config);
206
- const raw = migrated as Record<string, unknown>;
207
- const features = raw.features as Record<string, unknown> | undefined;
208
- const envFiles = raw.envFiles as Record<string, unknown> | undefined;
209
-
210
- if (features?.protectEnvFiles !== undefined) {
211
- features.policies = features.protectEnvFiles;
212
- delete features.protectEnvFiles;
213
- }
214
-
215
- if (envFiles) {
216
- const rule: Record<string, unknown> = {
217
- id: "secret-files",
218
- description: "Files containing secrets (migrated from envFiles)",
219
- protection: "noAccess",
220
- };
221
-
222
- if (envFiles.protectedPatterns) {
223
- rule.patterns = envFiles.protectedPatterns;
224
- }
225
- if (envFiles.allowedPatterns) {
226
- rule.allowedPatterns = envFiles.allowedPatterns;
227
- }
228
- if (envFiles.onlyBlockIfExists !== undefined) {
229
- rule.onlyIfExists = envFiles.onlyBlockIfExists;
230
- }
231
- if (typeof envFiles.blockMessage === "string") {
232
- rule.blockMessage = envFiles.blockMessage;
233
- }
234
-
235
- if (Array.isArray(envFiles.protectedDirectories)) {
236
- const dirs = envFiles.protectedDirectories as Array<
237
- Record<string, unknown>
238
- >;
239
- const patterns = Array.isArray(rule.patterns)
240
- ? ([...rule.patterns] as Array<Record<string, unknown>>)
241
- : [];
242
-
243
- for (const dir of dirs) {
244
- const dirPattern = dir.pattern;
245
- if (typeof dirPattern !== "string" || dirPattern.trim() === "") {
246
- continue;
247
- }
248
-
249
- const normalized = dirPattern.endsWith("/**")
250
- ? dirPattern
251
- : `${dirPattern}/**`;
252
- patterns.push({ pattern: normalized, regex: dir.regex });
253
- }
254
-
255
- if (patterns.length > 0) {
256
- rule.patterns = patterns;
257
- }
258
- }
259
-
260
- if (Array.isArray(envFiles.protectedTools)) {
261
- pendingWarnings.push(
262
- "[guardrails] envFiles.protectedTools is deprecated and has no direct policies equivalent. " +
263
- "The migrated secret-files rule uses protection=noAccess.",
264
- );
265
- }
266
-
267
- if (!Array.isArray(rule.patterns) || rule.patterns.length === 0) {
268
- rule.patterns = [
269
- { pattern: ".env" },
270
- { pattern: ".env.local" },
271
- { pattern: ".env.production" },
272
- { pattern: ".env.prod" },
273
- { pattern: ".dev.vars" },
274
- ];
275
- }
276
-
277
- raw.policies = { rules: [rule] };
278
- delete raw.envFiles;
279
- }
280
-
281
- raw.version = CURRENT_VERSION;
282
- return migrated as GuardrailsConfig;
283
- }
284
-
285
- /**
286
- * Migrate a string[] or PatternConfig[] to PatternConfig[] with regex: true.
287
- * Handles mixed arrays (some already migrated, some still strings).
288
- */
289
- function migrateStringArray(
290
- items: (string | PatternConfig)[],
291
- ): PatternConfig[] {
292
- return items.map((item) => {
293
- if (typeof item === "string") {
294
- return { pattern: item, regex: true };
295
- }
296
- // Already a PatternConfig, ensure regex is set
297
- if (item.regex === undefined) {
298
- return { ...item, regex: true };
299
- }
300
- return item;
301
- });
302
- }
303
-
304
- /**
305
- * Migrate dangerous pattern arrays. Handles both legacy
306
- * `{ pattern: string, description: string }` and already-migrated formats.
307
- */
308
- function migrateDangerousPatterns(
309
- items: (DangerousPattern | { pattern: string; description: string })[],
310
- ): DangerousPattern[] {
311
- return items.map((item) => {
312
- if ("regex" in item && item.regex !== undefined) {
313
- return item as DangerousPattern;
314
- }
315
- return { ...item, regex: true };
316
- });
317
- }
318
-
319
- /**
320
- * Back up a config file before migration.
321
- * Creates `<name>.v0.json` in the same directory.
322
- * Skips if backup already exists.
323
- */
324
- export async function backupConfig(configPath: string): Promise<void> {
325
- const dir = dirname(configPath);
326
- const basename = configPath.split("/").pop() ?? "guardrails.json";
327
- const backupName = basename.replace(".json", ".v0.json");
328
- const backupPath = resolve(dir, backupName);
329
-
330
- try {
331
- await stat(backupPath);
332
- // Backup already exists, skip
333
- } catch {
334
- try {
335
- await copyFile(configPath, backupPath);
336
- } catch (err) {
337
- pendingWarnings.push(`guardrails: could not back up config: ${err}`);
338
- }
339
- }
340
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Module-level warnings queue for messages that arise before any session
3
- * context is available (config loading, migration, pattern compilation).
4
- *
5
- * Drained and reported via ctx.ui.notify in the session_start handler.
6
- */
7
- export const pendingWarnings: string[] = [];
File without changes
File without changes
File without changes
File without changes
File without changes