@aliou/pi-guardrails 0.7.6 → 0.8.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 +98 -150
- package/package.json +19 -4
- package/src/commands/settings-command.ts +489 -91
- package/src/config.ts +111 -45
- package/src/hooks/index.ts +2 -2
- package/src/hooks/permission-gate.ts +149 -12
- package/src/hooks/policies.ts +297 -0
- package/src/index.ts +8 -1
- package/src/lib/executor.ts +280 -0
- package/src/lib/index.ts +16 -0
- package/src/lib/model-resolver.ts +47 -0
- package/src/lib/timing.ts +42 -0
- package/src/lib/types.ts +115 -0
- package/src/utils/events.ts +1 -1
- package/src/utils/matching.ts +7 -2
- package/src/utils/migration.ts +108 -2
- package/src/utils/warnings.ts +7 -0
- package/src/hooks/protect-env-files.ts +0 -220
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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;
|
package/src/utils/events.ts
CHANGED
|
@@ -4,7 +4,7 @@ export const GUARDRAILS_BLOCKED_EVENT = "guardrails:blocked";
|
|
|
4
4
|
export const GUARDRAILS_DANGEROUS_EVENT = "guardrails:dangerous";
|
|
5
5
|
|
|
6
6
|
export interface GuardrailsBlockedEvent {
|
|
7
|
-
feature: "
|
|
7
|
+
feature: "policies" | "permissionGate";
|
|
8
8
|
toolName: string;
|
|
9
9
|
input: Record<string, unknown>;
|
|
10
10
|
reason: string;
|
package/src/utils/matching.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { PatternConfig } from "../config";
|
|
12
|
+
import { pendingWarnings } from "./warnings";
|
|
12
13
|
|
|
13
14
|
export interface CompiledPattern {
|
|
14
15
|
test: (input: string) => boolean;
|
|
@@ -62,7 +63,9 @@ export function compileFilePattern(config: PatternConfig): CompiledPattern {
|
|
|
62
63
|
const re = new RegExp(config.pattern, "i");
|
|
63
64
|
return { test: (input) => re.test(input), source: config };
|
|
64
65
|
} catch {
|
|
65
|
-
|
|
66
|
+
pendingWarnings.push(
|
|
67
|
+
`Invalid regex in guardrails config: ${config.pattern}`,
|
|
68
|
+
);
|
|
66
69
|
return { test: () => false, source: config };
|
|
67
70
|
}
|
|
68
71
|
}
|
|
@@ -89,7 +92,9 @@ export function compileCommandPattern(config: PatternConfig): CompiledPattern {
|
|
|
89
92
|
const re = new RegExp(config.pattern);
|
|
90
93
|
return { test: (input) => re.test(input), source: config };
|
|
91
94
|
} catch {
|
|
92
|
-
|
|
95
|
+
pendingWarnings.push(
|
|
96
|
+
`Invalid regex in guardrails config: ${config.pattern}`,
|
|
97
|
+
);
|
|
93
98
|
return { test: () => false, source: config };
|
|
94
99
|
}
|
|
95
100
|
}
|
package/src/utils/migration.ts
CHANGED
|
@@ -13,8 +13,15 @@ import type {
|
|
|
13
13
|
GuardrailsConfig,
|
|
14
14
|
PatternConfig,
|
|
15
15
|
} from "../config";
|
|
16
|
+
import { pendingWarnings } from "./warnings";
|
|
16
17
|
|
|
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.8.0-20260228";
|
|
18
25
|
|
|
19
26
|
/**
|
|
20
27
|
* Check if a config needs migration (no version field = v0).
|
|
@@ -77,6 +84,105 @@ export function migrateV0(config: GuardrailsConfig): GuardrailsConfig {
|
|
|
77
84
|
return migrated;
|
|
78
85
|
}
|
|
79
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
|
+
* Migrate deprecated envFiles/protectEnvFiles fields to policies.
|
|
102
|
+
*/
|
|
103
|
+
export function migrateEnvFilesToPolicies(
|
|
104
|
+
config: GuardrailsConfig,
|
|
105
|
+
): GuardrailsConfig {
|
|
106
|
+
const migrated = structuredClone(config);
|
|
107
|
+
const raw = migrated as Record<string, unknown>;
|
|
108
|
+
const features = raw.features as Record<string, unknown> | undefined;
|
|
109
|
+
const envFiles = raw.envFiles as Record<string, unknown> | undefined;
|
|
110
|
+
|
|
111
|
+
if (features?.protectEnvFiles !== undefined) {
|
|
112
|
+
features.policies = features.protectEnvFiles;
|
|
113
|
+
delete features.protectEnvFiles;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (envFiles) {
|
|
117
|
+
const rule: Record<string, unknown> = {
|
|
118
|
+
id: "secret-files",
|
|
119
|
+
description: "Files containing secrets (migrated from envFiles)",
|
|
120
|
+
protection: "noAccess",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (envFiles.protectedPatterns) {
|
|
124
|
+
rule.patterns = envFiles.protectedPatterns;
|
|
125
|
+
}
|
|
126
|
+
if (envFiles.allowedPatterns) {
|
|
127
|
+
rule.allowedPatterns = envFiles.allowedPatterns;
|
|
128
|
+
}
|
|
129
|
+
if (envFiles.onlyBlockIfExists !== undefined) {
|
|
130
|
+
rule.onlyIfExists = envFiles.onlyBlockIfExists;
|
|
131
|
+
}
|
|
132
|
+
if (typeof envFiles.blockMessage === "string") {
|
|
133
|
+
rule.blockMessage = envFiles.blockMessage;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (Array.isArray(envFiles.protectedDirectories)) {
|
|
137
|
+
const dirs = envFiles.protectedDirectories as Array<
|
|
138
|
+
Record<string, unknown>
|
|
139
|
+
>;
|
|
140
|
+
const patterns = Array.isArray(rule.patterns)
|
|
141
|
+
? ([...rule.patterns] as Array<Record<string, unknown>>)
|
|
142
|
+
: [];
|
|
143
|
+
|
|
144
|
+
for (const dir of dirs) {
|
|
145
|
+
const dirPattern = dir.pattern;
|
|
146
|
+
if (typeof dirPattern !== "string" || dirPattern.trim() === "") {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const normalized = dirPattern.endsWith("/**")
|
|
151
|
+
? dirPattern
|
|
152
|
+
: `${dirPattern}/**`;
|
|
153
|
+
patterns.push({ pattern: normalized, regex: dir.regex });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (patterns.length > 0) {
|
|
157
|
+
rule.patterns = patterns;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (Array.isArray(envFiles.protectedTools)) {
|
|
162
|
+
pendingWarnings.push(
|
|
163
|
+
"[guardrails] envFiles.protectedTools is deprecated and has no direct policies equivalent. " +
|
|
164
|
+
"The migrated secret-files rule uses protection=noAccess.",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!Array.isArray(rule.patterns) || rule.patterns.length === 0) {
|
|
169
|
+
rule.patterns = [
|
|
170
|
+
{ pattern: ".env" },
|
|
171
|
+
{ pattern: ".env.local" },
|
|
172
|
+
{ pattern: ".env.production" },
|
|
173
|
+
{ pattern: ".env.prod" },
|
|
174
|
+
{ pattern: ".dev.vars" },
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
raw.policies = { rules: [rule] };
|
|
179
|
+
delete raw.envFiles;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
raw.version = CURRENT_VERSION;
|
|
183
|
+
return migrated as GuardrailsConfig;
|
|
184
|
+
}
|
|
185
|
+
|
|
80
186
|
/**
|
|
81
187
|
* Migrate a string[] or PatternConfig[] to PatternConfig[] with regex: true.
|
|
82
188
|
* Handles mixed arrays (some already migrated, some still strings).
|
|
@@ -129,7 +235,7 @@ export async function backupConfig(configPath: string): Promise<void> {
|
|
|
129
235
|
try {
|
|
130
236
|
await copyFile(configPath, backupPath);
|
|
131
237
|
} catch (err) {
|
|
132
|
-
|
|
238
|
+
pendingWarnings.push(`guardrails: could not back up config: ${err}`);
|
|
133
239
|
}
|
|
134
240
|
}
|
|
135
241
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
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[] = [];
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { stat } from "node:fs/promises";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { parse } from "@aliou/sh";
|
|
4
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
|
-
import type { ResolvedConfig } from "../config";
|
|
6
|
-
import { emitBlocked } from "../utils/events";
|
|
7
|
-
import { expandGlob, hasGlobChars } from "../utils/glob-expander";
|
|
8
|
-
import { type CompiledPattern, compileFilePatterns } from "../utils/matching";
|
|
9
|
-
import { walkCommands, wordToString } from "../utils/shell-utils";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Prevents accessing .env files unless they match an allowed pattern.
|
|
13
|
-
* Protects sensitive environment files from being accessed accidentally.
|
|
14
|
-
*
|
|
15
|
-
* Uses AST-based parsing for bash commands to extract file references,
|
|
16
|
-
* with glob expansion via `fd` when args contain shell glob characters.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
async function fileExists(filePath: string): Promise<boolean> {
|
|
20
|
-
try {
|
|
21
|
-
await stat(resolve(filePath));
|
|
22
|
-
return true;
|
|
23
|
-
} catch {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function isProtectedEnvFile(
|
|
29
|
-
filePath: string,
|
|
30
|
-
protectedPatterns: CompiledPattern[],
|
|
31
|
-
allowedPatterns: CompiledPattern[],
|
|
32
|
-
dirPatterns: CompiledPattern[],
|
|
33
|
-
onlyBlockIfExists: boolean,
|
|
34
|
-
): Promise<boolean> {
|
|
35
|
-
const isProtected = protectedPatterns.some((p) => p.test(filePath));
|
|
36
|
-
if (!isProtected) return false;
|
|
37
|
-
|
|
38
|
-
const isAllowed = allowedPatterns.some((p) => p.test(filePath));
|
|
39
|
-
if (isAllowed) return false;
|
|
40
|
-
|
|
41
|
-
// Check protected directories (if any configured)
|
|
42
|
-
if (dirPatterns.length > 0) {
|
|
43
|
-
const inProtectedDir = dirPatterns.some((p) => p.test(filePath));
|
|
44
|
-
if (inProtectedDir) {
|
|
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
|
-
}
|
|
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);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return files;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
interface ToolProtectionRule {
|
|
135
|
-
tools: string[];
|
|
136
|
-
extractTargets: (input: Record<string, unknown>) => Promise<string[]>;
|
|
137
|
-
shouldBlock: (target: string) => Promise<boolean>;
|
|
138
|
-
blockMessage: (target: string) => string;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function setupProtectEnvFilesHook(
|
|
142
|
-
pi: ExtensionAPI,
|
|
143
|
-
config: ResolvedConfig,
|
|
144
|
-
) {
|
|
145
|
-
if (!config.features.protectEnvFiles) return;
|
|
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
|
-
|
|
162
|
-
const protectionRules: ToolProtectionRule[] = [
|
|
163
|
-
{
|
|
164
|
-
tools: config.envFiles.protectedTools.filter((t) =>
|
|
165
|
-
["read", "write", "edit", "grep", "find", "ls"].includes(t),
|
|
166
|
-
),
|
|
167
|
-
extractTargets: async (input) => {
|
|
168
|
-
const path = String(input.file_path ?? input.path ?? "");
|
|
169
|
-
return path ? [path] : [];
|
|
170
|
-
},
|
|
171
|
-
shouldBlock,
|
|
172
|
-
blockMessage: (target) =>
|
|
173
|
-
config.envFiles.blockMessage.replace("{file}", target),
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
tools: config.envFiles.protectedTools.includes("bash") ? ["bash"] : [],
|
|
177
|
-
extractTargets: async (input) => {
|
|
178
|
-
const command = String(input.command ?? "");
|
|
179
|
-
return extractBashFileTargets(command);
|
|
180
|
-
},
|
|
181
|
-
shouldBlock,
|
|
182
|
-
blockMessage: (target) =>
|
|
183
|
-
`Command references protected file ${target}. ` +
|
|
184
|
-
config.envFiles.blockMessage.replace("{file}", target),
|
|
185
|
-
},
|
|
186
|
-
];
|
|
187
|
-
|
|
188
|
-
// Build lookup: tool name -> rule
|
|
189
|
-
const rulesByTool = new Map<string, ToolProtectionRule>();
|
|
190
|
-
for (const rule of protectionRules) {
|
|
191
|
-
for (const tool of rule.tools) {
|
|
192
|
-
rulesByTool.set(tool, rule);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
197
|
-
const rule = rulesByTool.get(event.toolName);
|
|
198
|
-
if (!rule) return;
|
|
199
|
-
|
|
200
|
-
const targets = await rule.extractTargets(event.input);
|
|
201
|
-
|
|
202
|
-
for (const target of targets) {
|
|
203
|
-
if (await rule.shouldBlock(target)) {
|
|
204
|
-
ctx.ui.notify(`Blocked access to protected file: ${target}`, "warning");
|
|
205
|
-
|
|
206
|
-
const reason = rule.blockMessage(target);
|
|
207
|
-
|
|
208
|
-
emitBlocked(pi, {
|
|
209
|
-
feature: "protectEnvFiles",
|
|
210
|
-
toolName: event.toolName,
|
|
211
|
-
input: event.input,
|
|
212
|
-
reason,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
return { block: true, reason };
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return;
|
|
219
|
-
});
|
|
220
|
-
}
|