ML-157: implementation and configuration
This commit is contained in:
@@ -0,0 +1,104 @@
|
|||||||
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { normalize, resolve } from "node:path";
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
blocked_paths: string[];
|
||||||
|
blocked_commands: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadConfig(cwd: string): Config {
|
||||||
|
const raw = readFileSync(resolve(cwd, ".pi/sensitive-paths.json"), "utf-8");
|
||||||
|
return JSON.parse(raw) as Config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert a glob-like pattern to a case-insensitive regex. */
|
||||||
|
function patternToRegex(pattern: string): RegExp {
|
||||||
|
let escaped = pattern
|
||||||
|
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // Escape regex specials except *
|
||||||
|
.replace(/\*/g, ".*"); // * → .*
|
||||||
|
return new RegExp(escaped, "i");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (pi: ExtensionAPI) {
|
||||||
|
let config: Config;
|
||||||
|
try {
|
||||||
|
config = loadConfig(process.cwd());
|
||||||
|
} catch {
|
||||||
|
return; // Config missing or invalid — fail open
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathRegexes = config.blocked_paths.map(patternToRegex);
|
||||||
|
const commandRegexes = config.blocked_commands.map((r) => new RegExp(r, "i"));
|
||||||
|
|
||||||
|
// Proactively tell the agent which paths are off-limits so it doesn't try in the first place
|
||||||
|
pi.on("before_agent_start", async (event, _ctx) => {
|
||||||
|
if (config.blocked_paths.length === 0) return;
|
||||||
|
const pathList = config.blocked_paths.map((p) => ` - \`${p}\``).join('\n');
|
||||||
|
return {
|
||||||
|
systemPrompt:
|
||||||
|
event.systemPrompt +
|
||||||
|
`\n\n## Sensitive File Guard\nThese path patterns are protected and all tool access is permanently blocked:\n${pathList}\n\nIf a tool call is blocked, DO NOT retry with alternate tools (bash, find, ls, grep, etc.). Instead, tell the user: "That path is protected by sensitive-file-guard."`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("tool_call", (event, ctx) => {
|
||||||
|
// --- Path-based tools ---
|
||||||
|
if (
|
||||||
|
isToolCallEventType("read", event) ||
|
||||||
|
isToolCallEventType("grep", event) ||
|
||||||
|
isToolCallEventType("write", event) ||
|
||||||
|
isToolCallEventType("edit", event) ||
|
||||||
|
isToolCallEventType("find", event) ||
|
||||||
|
isToolCallEventType("ls", event)
|
||||||
|
) {
|
||||||
|
const path = event.input.path;
|
||||||
|
if (typeof path !== "string") return;
|
||||||
|
|
||||||
|
const resolved = normalize(resolve(ctx.cwd, path));
|
||||||
|
const match = pathRegexes.find((r) => r.test(resolved));
|
||||||
|
if (match) {
|
||||||
|
const reason =
|
||||||
|
`🚫 ACCESS DENIED by sensitive-file-guard: paths matching "${match}" are permanently protected. ` +
|
||||||
|
`DO NOT retry with other tools (bash, find, ls, grep, etc.). ` +
|
||||||
|
`Tell the user the file is protected and STOP.`;
|
||||||
|
if (ctx.hasUI) ctx.ui.notify(`Blocked sensitive path: ${path}`, "warning");
|
||||||
|
return { block: true, reason };
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Bash tool ---
|
||||||
|
if (isToolCallEventType("bash", event)) {
|
||||||
|
const command = event.input.command;
|
||||||
|
if (typeof command !== "string") return;
|
||||||
|
|
||||||
|
// Check for blocked path fragments in the command text
|
||||||
|
const pathHit = config.blocked_paths.find((p) => {
|
||||||
|
// Rough substring match: does the pattern text (with glob stripped)
|
||||||
|
// appear in the command?
|
||||||
|
const literal = p.replace(/[*?]/g, "");
|
||||||
|
return literal.length > 0 && command.toLowerCase().includes(literal.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pathHit) {
|
||||||
|
const reason =
|
||||||
|
`🚫 ACCESS DENIED by sensitive-file-guard: commands targeting "${pathHit}" are permanently blocked. ` +
|
||||||
|
`DO NOT retry with alternative commands. Tell the user the path is protected and STOP.`;
|
||||||
|
if (ctx.hasUI) ctx.ui.notify(`Blocked sensitive path in bash: ${pathHit}`, "warning");
|
||||||
|
return { block: true, reason };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for blocked commands (env, printenv, set)
|
||||||
|
const cmdHit = commandRegexes.find((r) => r.test(command));
|
||||||
|
if (cmdHit) {
|
||||||
|
const reason =
|
||||||
|
`🚫 ACCESS DENIED by sensitive-file-guard: this command matches a blocked pattern. ` +
|
||||||
|
`DO NOT retry. Tell the user the command is blocked and STOP.`;
|
||||||
|
if (ctx.hasUI) ctx.ui.notify(`Blocked sensitive command`, "warning");
|
||||||
|
return { block: true, reason };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Blocked paths for pi. Glob-like patterns, matched case-insensitively against absolute paths.",
|
||||||
|
"blocked_paths": [
|
||||||
|
"mise.local.toml",
|
||||||
|
"*.pem",
|
||||||
|
"*.key",
|
||||||
|
"*.key.pub",
|
||||||
|
"*secret*",
|
||||||
|
"*credential*",
|
||||||
|
"*credentials*",
|
||||||
|
".ssh/",
|
||||||
|
".aws/",
|
||||||
|
".gnupg/",
|
||||||
|
"~/.pi/agent/sessions/"
|
||||||
|
],
|
||||||
|
"blocked_commands": [
|
||||||
|
"printenv",
|
||||||
|
"\\benv\\b",
|
||||||
|
"\\bset\\b"
|
||||||
|
]
|
||||||
|
}
|
||||||
+42
-9
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
id: ML-157
|
id: ML-157
|
||||||
title: Prevent pi from accessing sensitive files
|
title: Prevent pi from accessing sensitive files
|
||||||
status: To Do
|
status: Done
|
||||||
assignee: []
|
assignee: []
|
||||||
created_date: '2026-05-03 13:30'
|
created_date: '2026-05-03 13:30'
|
||||||
updated_date: '2026-05-03 13:44'
|
updated_date: '2026-05-03 14:50'
|
||||||
labels: []
|
labels: []
|
||||||
dependencies: []
|
dependencies: []
|
||||||
references:
|
references:
|
||||||
@@ -19,13 +19,13 @@ To prevent the pi harness from accidentally reading and sending sensitive data t
|
|||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
<!-- AC:BEGIN -->
|
<!-- AC:BEGIN -->
|
||||||
- [ ] #1 Pi cannot read `.env` files via the `read` tool — access is blocked with a notification in interactive mode
|
- [x] #1 Pi cannot read `.env` files via the `read` tool — access is blocked with a notification in interactive mode
|
||||||
- [ ] #2 Pi cannot read files matching `*secret*` or `*credential*` patterns via the `read` tool
|
- [x] #2 Pi cannot read files matching `*secret*` or `*credential*` patterns via the `read` tool
|
||||||
- [ ] #3 Pi cannot `cat .env` or `grep` inside `.ssh/` or `.aws/` via the `bash` tool
|
- [x] #3 Pi cannot `cat .env` or `grep` inside `.ssh/` or `.aws/` via the `bash` tool
|
||||||
- [ ] #4 Non-interactive mode (`pi -p`) reports blocked sensitive file access as an error rather than silently failing
|
- [x] #4 Non-interactive mode (`pi -p`) reports blocked sensitive file access as an error rather than silently failing
|
||||||
- [ ] #5 Normal file access (source code, test files, config examples like `.env.example`) is unaffected
|
- [x] #5 Normal file access (source code, test files, config examples like `.env.example`) is unaffected
|
||||||
- [ ] #6 The set of blocked paths is declared in `.pi/sensitive-paths.json` — adding/removing patterns does not require code changes
|
- [x] #6 The set of blocked paths is declared in `.pi/sensitive-paths.json` — adding/removing patterns does not require code changes
|
||||||
- [ ] #7 The extension loads correctly at pi startup and chains with existing extensions (MCP adapter) without conflicts
|
- [x] #7 The extension loads correctly at pi startup and chains with existing extensions (MCP adapter) without conflicts
|
||||||
<!-- AC:END -->
|
<!-- AC:END -->
|
||||||
|
|
||||||
## Implementation Plan
|
## Implementation Plan
|
||||||
@@ -330,3 +330,36 @@ If future patterns grow to hundreds of entries, a trie-based matcher could be co
|
|||||||
|
|
||||||
**No other documentation files need updates.** This is a pi-level concern, not an application architecture concern.
|
**No other documentation files need updates.** This is a pi-level concern, not an application architecture concern.
|
||||||
<!-- SECTION:PLAN:END -->
|
<!-- SECTION:PLAN:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
### Files Created
|
||||||
|
|
||||||
|
1. **`.pi/sensitive-paths.json`** — Declarative config of blocked path patterns and blocked commands. Primary target: `mise.local.toml` (contains secrets). Also covers common sensitive file patterns (`*.pem`, `*.key`, `*secret*`, `.ssh/`, `.aws/`, etc.) and env-dumping commands (`printenv`, `env`, `set`).
|
||||||
|
|
||||||
|
2. **`.pi/extensions/000-sensitive-file-guard.ts`** — Pi extension that intercepts `tool_call` events before execution. Named with `000-` prefix to load first among project-local extensions, ensuring the guard inspects original tool arguments before any other extension mutates them.
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
- **Path-based tools** (`read`, `grep`, `write`, `edit`, `find`, `ls`): Resolves the input path to an absolute path (handling `..` traversal via `normalize`), then checks against compiled case-insensitive regexes from `blocked_paths`.
|
||||||
|
- **Bash tool**: Substring scan of the raw command text for blocked path fragments + regex check against `blocked_commands`.
|
||||||
|
- **Blocking**: Returns `{ block: true, reason }` — blocks BEFORE any file I/O occurs. Shows `ctx.ui.notify()` warning in interactive mode. Returns error reason in non-interactive/print mode.
|
||||||
|
- **Fail-open**: If config file is missing or invalid, the extension silently does nothing.
|
||||||
|
|
||||||
|
### Key Limitation
|
||||||
|
- Does NOT resolve symlinks (would require `realpathSync` per tool call). Users must not symlink sensitive directories into the project tree.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Created two files:
|
||||||
|
|
||||||
|
1. **`.pi/sensitive-paths.json`** — Declarative config blocking `mise.local.toml` and other sensitive file patterns
|
||||||
|
2. **`.pi/extensions/000-sensitive-file-guard.ts`** — Extension that intercepts `tool_call` events before any file I/O occurs, blocks matching paths/commands, notifies in interactive mode, and reports error reason in non-interactive mode.
|
||||||
|
|
||||||
|
The guard covers all built-in file-access tools (read, grep, write, edit, find, ls, bash) and loads first (000- prefix) among project-local extensions to inspect original tool arguments.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
Reference in New Issue
Block a user