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).
64 lines
2.8 KiB
TypeScript
64 lines
2.8 KiB
TypeScript
/**
|
||
* 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;
|
||
});
|
||
}
|