- 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.
81 lines
4.1 KiB
TypeScript
81 lines
4.1 KiB
TypeScript
/**
|
|
* Behavioral tests for the guardrail extensions. No test framework — run with:
|
|
* node --experimental-strip-types test/guardrails.test.ts
|
|
* (or `npm test`). Exits non-zero on any failure.
|
|
*
|
|
* Extensions only `import type` the Pi API, so they run here with node's
|
|
* type-stripping and mocked pi/ctx objects.
|
|
*/
|
|
|
|
process.env.PI_KIT_AUDIT_OFF = "1"; // don't touch the real audit log during tests
|
|
|
|
import protectedPaths from "../extensions/protected-paths.ts";
|
|
import permissionGate from "../extensions/permission-gate.ts";
|
|
import secretScanner from "../extensions/secret-scanner.ts";
|
|
import commitGuard from "../extensions/commit-guard.ts";
|
|
import llmRedaction from "../extensions/llm-redaction.ts";
|
|
import { redact, scanSecrets } from "../extensions/lib/secrets.ts";
|
|
|
|
let fails = 0;
|
|
function check(name: string, cond: boolean) {
|
|
console.log(`${cond ? "PASS" : "FAIL"} ${name}`);
|
|
if (!cond) fails++;
|
|
}
|
|
|
|
/** Capture the handler an extension registers for a given event. */
|
|
function grab(ext: (pi: any) => void, evName: string): any {
|
|
let handler: any;
|
|
ext({ on: (ev: string, fn: any) => { if (ev === evName) handler = fn; }, registerCommand() {} });
|
|
return handler;
|
|
}
|
|
|
|
const noUI: any = { hasUI: false };
|
|
const AKIA = "AKIA" + "ABCDEFGHIJKLMNOP";
|
|
const GLPAT = "glpat-" + "aBcDeF1234567890ghXY12"; // 22 chars after prefix (>=20)
|
|
const PRIVKEY = "-----BEGIN RSA PRIVATE KEY-----";
|
|
const JWT =
|
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U";
|
|
|
|
async function run() {
|
|
// --- protected-paths ---
|
|
const pp = grab(protectedPaths, "tool_call");
|
|
check("protected: block .env", !!(await pp({ toolName: "write", input: { path: ".env" } }, noUI))?.block);
|
|
check("protected: allow src/app.go", !(await pp({ toolName: "write", input: { path: "src/app.go" } }, noUI))?.block);
|
|
|
|
// --- permission-gate ---
|
|
const pg = grab(permissionGate, "tool_call");
|
|
check("perm: block push v* tag", !!(await pg({ toolName: "bash", input: { command: "git push origin v1.2.3" } }, noUI))?.block);
|
|
check("perm: allow ls", !(await pg({ toolName: "bash", input: { command: "ls -la" } }, noUI))?.block);
|
|
|
|
// --- secret-scanner ---
|
|
const ss = grab(secretScanner, "tool_call");
|
|
check("secret: block write AKIA", !!(await ss({ toolName: "write", input: { path: "cfg.txt", content: `key=${AKIA}` } }, noUI))?.block);
|
|
check("secret: block edit private key", !!(await ss({ toolName: "edit", input: { path: "id_rsa", new_string: PRIVKEY } }, noUI))?.block);
|
|
check("secret: allow clean content", !(await ss({ toolName: "write", input: { path: "a.go", content: "package main" } }, noUI))?.block);
|
|
check("secret: ignore non-write tool", (await ss({ toolName: "bash", input: { command: "echo hi" } }, noUI)) === undefined);
|
|
|
|
// --- commit-guard ---
|
|
const cg = grab(commitGuard, "tool_call");
|
|
check("commit: block non-conventional", !!(await cg({ toolName: "bash", input: { command: 'git commit -m "fixed stuff"' } }, noUI))?.block);
|
|
check("commit: allow conventional", !(await cg({ toolName: "bash", input: { command: 'git commit -m "feat(auth): add login"' } }, noUI))?.block);
|
|
check("commit: block secret in message", !!(await cg({ toolName: "bash", input: { command: `git commit -m "chore: token ${GLPAT}"` } }, noUI))?.block);
|
|
check("commit: ignore editor commit", (await cg({ toolName: "bash", input: { command: "git commit" } }, noUI)) === undefined);
|
|
|
|
// --- llm-redaction ---
|
|
const lr = grab(llmRedaction, "before_provider_request");
|
|
const payload = { model: "x", messages: [{ role: "user", content: `here is my token ${GLPAT}` }] };
|
|
const out: any = await lr({ payload });
|
|
const redacted = JSON.stringify(out?.messages ?? []);
|
|
check("redact: token removed from payload", !redacted.includes(GLPAT) && redacted.includes("REDACTED"));
|
|
check("redact: no-op when clean", (await lr({ payload: { messages: [{ role: "user", content: "hello" }] } })) === undefined);
|
|
|
|
// --- lib units ---
|
|
check("scanSecrets finds jwt", scanSecrets(JWT).length > 0);
|
|
check("redact keeps clean text", redact("just normal text") === "just normal text");
|
|
|
|
console.log(fails === 0 ? "\nALL PASS" : `\n${fails} FAILED`);
|
|
process.exit(fails === 0 ? 0 : 1);
|
|
}
|
|
|
|
run();
|