feat(extensions): protected-paths, permission-gate, company-context
- protected-paths: glob-based block of write/edit to secrets/keys/.git/prod configs - permission-gate: confirm dangerous bash (rm -rf, force push, push to protected branch, prod kubectl, docker prune, sudo) - company-context: loads config/company.json (+ optional remoteConfigUrl w/ 2.5s timeout), injects corporate context via before_agent_start, registers /kit-config - config path resolved via import.meta.url, not cwd; text template in company-context.md
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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 `**/` 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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user