Files
pi-kit/extensions/commit-guard.ts
Aleksey Shakhmatov 7e50043984 feat(security): secret-scanner, commit-guard, llm-redaction, audit-log
- lib/secrets.ts: shared SECRET_PATTERNS + scanSecrets/redact (AWS/GitLab/GitHub/Slack tokens, private keys, JWT, generic credentials)
- lib/audit.ts: local JSONL audit (~/.config/pi-kit/audit.jsonl, 600), optional PI_KIT_AUDIT_URL POST; never logs secret values
- secret-scanner: block write/edit whose content matches a secret (complements protected-paths)
- commit-guard: enforce Conventional Commits on 'git commit -m' + scan message for secrets
- llm-redaction: redact secrets from outbound payload via before_provider_request
- audit-log: session_start + slash-command usage; wires company.json auditEndpoint into env
- config: auditEndpoint (null); package.json: npm test; test/guardrails.test.ts (16 cases, all pass)

Verified: all extensions load via pi (lib/ not loaded as extensions); npm test green.
2026-07-16 13:46:22 +03:00

61 lines
2.4 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.
/**
* Commit Guard Extension
*
* WHAT: On `git commit -m …`, validates the message against Conventional
* Commits and scans it for secrets. Invalid/secret → confirm (UI) or
* block (no UI). Non-`-m` commits (editor) are left alone.
*
* CONFIGURE: edit COMMIT_TYPES below. Secret patterns live in
* extensions/lib/secrets.ts.
*
* DISABLE: remove/rename this file from the package `extensions/` folder.
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { scanSecrets } from "./lib/secrets.ts";
import { audit } from "./lib/audit.ts";
// Conventional Commits types accepted in the header. Extend as needed.
const COMMIT_TYPES = ["feat", "fix", "chore", "docs", "refactor", "test", "perf", "build", "ci", "style", "revert"];
const HEADER_RE = new RegExp(`^(${COMMIT_TYPES.join("|")})(\\(.+\\))?!?: .+`);
/** Extract the -m / --message value from a `git commit` command, if present. */
function extractCommitMessage(command: string): string | null {
// Matches -m "msg", -m 'msg', --message=msg, -m msg (best effort).
const m =
command.match(/(?:-m|--message)[=\s]+"([^"]*)"/) ??
command.match(/(?:-m|--message)[=\s]+'([^']*)'/) ??
command.match(/(?:-m|--message)[=\s]+(\S+)/);
return m ? m[1] : null;
}
export default function commitGuardExtension(pi: ExtensionAPI) {
pi.on("tool_call", async (event, ctx) => {
if (event.toolName !== "bash") return undefined;
const command = (event.input.command as string | undefined) ?? "";
if (!/\bgit\s+commit\b/.test(command)) return undefined;
const message = extractCommitMessage(command);
if (message === null) return undefined; // editor-based commit; not our concern
const problems: string[] = [];
if (!HEADER_RE.test(message)) problems.push("не Conventional Commits");
const secretHits = scanSecrets(message);
if (secretHits.length > 0) problems.push(`секрет (${secretHits.join(", ")})`);
if (problems.length === 0) return undefined;
audit({ kind: "guardrail_block", guardrail: "commit-guard", problems, hasSecret: secretHits.length > 0 });
if (ctx.hasUI) {
const choice = await ctx.ui.select(
`⚠️ Проблема с сообщением коммита: ${problems.join("; ")}.\ормат: type(scope): описание. Всё равно закоммитить?`,
["Нет", "Да"],
);
if (choice === "Да") return undefined;
}
return { block: true, reason: `Commit message rejected: ${problems.join("; ")}` };
});
}