Files
pi-kit/extensions/protected-paths.ts
Aleksey Shakhmatov 640e1da3c6 fix(extensions): avoid premature block-comment close; mark package as ESM
- protected-paths.ts: reword comment that contained a globstar+slash sequence which closed the /** */ block early (caused a ParseError at load)
- package.json: add "type": "module" (extensions are ESM)

Verified locally: pi install ./, all 5 commands + 4 skills load via RPC get_commands, /kit-config prints values+source, dynamic config change reflected, protected-paths + permission-gate pass a behavioral test suite.
2026-07-16 11:19:12 +03:00

93 lines
2.9 KiB
TypeScript

/**
* Protected Paths Extension
*
* WHAT: Blocks `write` and `edit` tool calls that target sensitive files
* (secrets, keys, VCS internals, production config). Based on the
* official protected-paths example, extended with glob matching.
*
* CONFIGURE: Edit the PROTECTED_PATHS constant below. Patterns are globs:
* `*` matches within one path segment,
* `**` matches across segments,
* `?` matches a single character.
* A leading globstar + slash means "in any directory".
*
* DISABLE: Remove/rename this file from the package `extensions/` folder,
* or exclude it via the package manifest / settings.
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
// ---------------------------------------------------------------------------
// Protected path patterns. Add new entries here to widen protection.
// Keep this list conservative — it blocks writes agent-wide.
// ---------------------------------------------------------------------------
const PROTECTED_PATHS: string[] = [
".env*", // .env, .env.local, .env.production, ...
"**/secrets/**", // anything under a secrets/ directory
"**/*.pem", // TLS/cert private material
"**/*.key", // private keys
".git/**", // VCS internals
// Production environment config files (adjust to your conventions):
"**/*.prod.yml",
"**/*.prod.yaml",
"**/*.production.yml",
"**/*.production.yaml",
"**/values-prod.yaml", // Helm prod values
"**/config/prod*", // config/prod.json, config/production.*, ...
];
/**
* Compile a glob pattern into a RegExp that matches a path suffix, so the
* pattern matches regardless of whether the path is absolute or relative.
*/
function globToRegExp(glob: string): RegExp {
let g = glob;
if (g.startsWith("**/")) {
g = g.slice(3); // the "(^|/)" prefix already means "in any directory"
}
let re = "";
for (let i = 0; i < g.length; i++) {
const c = g[i];
if (c === "*") {
if (g[i + 1] === "*") {
re += ".*";
i++;
} else {
re += "[^/]*";
}
} else if (c === "?") {
re += "[^/]";
} else if ("\\^$+.()|{}[]".includes(c)) {
re += `\\${c}`;
} else {
re += c;
}
}
return new RegExp(`(^|/)${re}$`);
}
const COMPILED = PROTECTED_PATHS.map((p) => ({ glob: p, re: globToRegExp(p) }));
export default function protectedPathsExtension(pi: ExtensionAPI) {
pi.on("tool_call", async (event, ctx) => {
if (event.toolName !== "write" && event.toolName !== "edit") {
return undefined;
}
const raw = event.input.path as string | undefined;
if (!raw) return undefined;
const path = raw.replace(/\\/g, "/"); // normalize Windows separators
const hit = COMPILED.find((c) => c.re.test(path));
if (hit) {
if (ctx.hasUI) {
ctx.ui.notify(`Blocked write to protected path: ${raw} (matches "${hit.glob}")`, "warning");
}
return { block: true, reason: `Path "${raw}" is protected (pattern "${hit.glob}")` };
}
return undefined;
});
}