- 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.
93 lines
2.9 KiB
TypeScript
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;
|
|
});
|
|
}
|