Files
pi-kit/extensions/permission-gate.ts
Aleksey Shakhmatov 6521d9c431 fix(permission-gate): guard release-* branch and v* tag pushes (deploy convention)
Deploy convention at M.Video: a release-* branch push deploys to stage and a
v* tag push deploys to PROD. The old guard only matched release/* (slash) and
never caught tag pushes, so a prod deploy could run unconfirmed.

- permission-gate: match main/master, release-[-/], --tags/--follow-tags, and
  whitespace-preceded v<digit> tags; false-positive-safe (space discriminator)
- create-mr.sh: refuse source branch release-* (not just release/*)
- jira-workflow doc updated to release-*

Verified with a behavioral test suite (10 cases incl. v2, false-positive checks).
2026-07-16 13:13:35 +03:00

64 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 }> = [
{ re: /\brm\s+(-\w*r\w*f|-\w*f\w*r|--recursive)/i, reason: "recursive force delete (rm -rf)" },
{ 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)" },
{ re: /\bgit\s+push\b[^\n]*\sv\d[\w.-]*/i, reason: "push a v* version tag (deploys to PROD)" },
{
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;
});
}