/** * Permission Gate Extension * * WHAT: Asks for confirmation before running dangerous `bash` commands. * In non-interactive mode (no UI) such commands are blocked by default. * Based on the official permission-gate example, with more patterns. * * CONFIGURE: Edit the DANGEROUS_PATTERNS constant below. Each entry has a * regex and a human-readable reason shown in the confirmation prompt. * * 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"; // --------------------------------------------------------------------------- // Dangerous command patterns. Add new entries here. // Matching is intentionally broad; the user confirms, so false positives are // cheap while misses can be costly. // --------------------------------------------------------------------------- const DANGEROUS_PATTERNS: Array<{ re: RegExp; reason: string }> = [ // rm: single-token forms (-rf, -fr) and --recursive. { re: /\brm\s+(-\w*r\w*f|-\w*f\w*r|--recursive)/i, reason: "recursive force delete (rm -rf)" }, // rm: separated flags (rm -r -f, rm -f -r) — one flag token with r, another with f. { re: /\brm\b(?=[^;\n]*-[^\s]*r)(?=[^;\n]*-[^\s]*f)/i, reason: "recursive force delete (rm -r -f)" }, { re: /\bgit\s+push\b[^\n]*(--force\b|--force-with-lease\b|\s-f\b)/i, reason: "force push" }, { re: /\bgit\s+push\b[^\n]*\b(main|master)\b/i, reason: "push to main/master" }, // Deploy convention: a `release-*` branch push deploys to STAGE, and a `v*` // tag push deploys to PROD. Guard both the branch and the tag pushes. { re: /\bgit\s+push\b[^\n]*\brelease[-/]\S+/i, reason: "push to a release-* branch (deploys to stage)" }, { re: /\bgit\s+push\b[^\n]*(--tags\b|--follow-tags\b)/i, reason: "push tags (a v* tag deploys to PROD)" }, // v* tag pushes: plain `v1.2.3`, and ref-style `tags/v1.2.3` / `refs/tags/v1.2.3` // which would otherwise slip past the plain-tag pattern. { re: /\bgit\s+push\b[^\n]*\sv\d[\w.-]*/i, reason: "push a v* version tag (deploys to PROD)" }, { re: /\bgit\s+push\b[^\n]*\b(?:refs\/)?tags?\/v\d[\w.-]*/i, reason: "push a v* version tag via refs/tags/ (deploys to PROD)" }, { re: /\bgit\s+reset\s+--hard\b/i, reason: "git reset --hard (destroys uncommitted changes)" }, { re: /\bgit\s+clean\b/i, reason: "git clean (removes untracked files)" }, { re: /\bkubectl\b[^\n]*(--context[=\s]?\S*prod|--namespace[=\s]?\S*prod|\bctx-prod\b)/i, reason: "kubectl against a production context/namespace", }, { re: /\bdocker\s+system\s+prune\b/i, reason: "docker system prune (removes containers/images/volumes)" }, { re: /\bsudo\b/i, reason: "elevated privileges (sudo)" }, ]; export default function permissionGateExtension(pi: ExtensionAPI) { pi.on("tool_call", async (event, ctx) => { if (event.toolName !== "bash") return undefined; const command = (event.input.command as string | undefined) ?? ""; const hit = DANGEROUS_PATTERNS.find((p) => p.re.test(command)); if (!hit) return undefined; if (!ctx.hasUI) { // Non-interactive mode: cannot ask, so block by default. return { block: true, reason: `Dangerous command blocked (${hit.reason}); no UI for confirmation` }; } const choice = await ctx.ui.select( `⚠️ Опасная команда — ${hit.reason}:\n\n ${command}\n\nВыполнить?`, ["Нет", "Да"], ); if (choice !== "Да") { return { block: true, reason: `Blocked by user (${hit.reason})` }; } return undefined; }); }