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.
This commit is contained in:
+2
-1
@@ -8,5 +8,6 @@
|
|||||||
"не пушить в main",
|
"не пушить в main",
|
||||||
"MR обязателен"
|
"MR обязателен"
|
||||||
],
|
],
|
||||||
"remoteConfigUrl": null
|
"remoteConfigUrl": null,
|
||||||
|
"auditEndpoint": null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Audit Log Extension
|
||||||
|
*
|
||||||
|
* WHAT: Records session starts and slash-command usage to the local audit log
|
||||||
|
* (~/.config/pi-kit/audit.jsonl). Guardrail blocks are recorded by the
|
||||||
|
* guardrail extensions themselves via the shared audit() helper. If an
|
||||||
|
* audit endpoint is configured it is forwarded best-effort (see below).
|
||||||
|
*
|
||||||
|
* CONFIGURE: set `auditEndpoint` in config/company.json, or the PI_KIT_AUDIT_URL
|
||||||
|
* env var, to forward events. Turn everything off with PI_KIT_AUDIT_OFF=1.
|
||||||
|
*
|
||||||
|
* DISABLE: remove/rename this file from the package `extensions/` folder.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||||
|
import { audit } from "./lib/audit.ts";
|
||||||
|
|
||||||
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const CONFIG_PATH = path.join(HERE, "..", "config", "company.json");
|
||||||
|
|
||||||
|
/** Wire company.json `auditEndpoint` into the env var read by audit() (unless already set). */
|
||||||
|
function initEndpointFromConfig(): void {
|
||||||
|
if (process.env.PI_KIT_AUDIT_URL) return;
|
||||||
|
try {
|
||||||
|
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
||||||
|
if (typeof cfg.auditEndpoint === "string" && cfg.auditEndpoint) {
|
||||||
|
process.env.PI_KIT_AUDIT_URL = cfg.auditEndpoint;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// No config / unreadable — local-only auditing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function auditLogExtension(pi: ExtensionAPI) {
|
||||||
|
initEndpointFromConfig();
|
||||||
|
|
||||||
|
pi.on("session_start", async (event) => {
|
||||||
|
audit({ kind: "session_start", reason: (event as { reason?: string }).reason });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Record slash-command usage (name only, no arguments — args may contain secrets).
|
||||||
|
pi.on("input", async (event) => {
|
||||||
|
const text = (event as { text?: string }).text ?? "";
|
||||||
|
const m = text.match(/^\/([a-z0-9:_-]+)/i);
|
||||||
|
if (m) audit({ kind: "command", command: m[1] });
|
||||||
|
return { action: "continue" };
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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("; ")}.\nФормат: type(scope): описание. Всё равно закоммитить?`,
|
||||||
|
["Нет", "Да"],
|
||||||
|
);
|
||||||
|
if (choice === "Да") return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { block: true, reason: `Commit message rejected: ${problems.join("; ")}` };
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Shared local audit log used by the guardrail extensions.
|
||||||
|
*
|
||||||
|
* WHAT: appends JSONL events to ~/.config/pi-kit/audit.jsonl (chmod 600).
|
||||||
|
* Never logs secret values — only pattern names, paths, command shape.
|
||||||
|
* If PI_KIT_AUDIT_URL is set (or company.json auditEndpoint, wired by
|
||||||
|
* audit-log.ts into that env var), each event is also POSTed best-effort.
|
||||||
|
*
|
||||||
|
* DISABLE: remove the audit-log.ts extension, or set PI_KIT_AUDIT_OFF=1.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import * as path from "node:path";
|
||||||
|
|
||||||
|
const DIR = path.join(os.homedir(), ".config", "pi-kit");
|
||||||
|
const FILE = path.join(DIR, "audit.jsonl");
|
||||||
|
const POST_TIMEOUT_MS = 1500;
|
||||||
|
|
||||||
|
export interface AuditEntry {
|
||||||
|
kind: string; // e.g. "guardrail_block", "command", "session_start"
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Append an audit entry locally and optionally forward it to an endpoint. */
|
||||||
|
export function audit(entry: AuditEntry): void {
|
||||||
|
if (process.env.PI_KIT_AUDIT_OFF === "1") return;
|
||||||
|
const line = JSON.stringify({ t: new Date().toISOString(), ...entry });
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(DIR, { recursive: true, mode: 0o700 });
|
||||||
|
fs.appendFileSync(FILE, `${line}\n`, { mode: 0o600 });
|
||||||
|
} catch {
|
||||||
|
// Auditing must never break the agent; swallow local write errors.
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = process.env.PI_KIT_AUDIT_URL;
|
||||||
|
if (url) {
|
||||||
|
// Best-effort, fire-and-forget. Never awaited; failures are ignored.
|
||||||
|
fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: line,
|
||||||
|
signal: AbortSignal.timeout(POST_TIMEOUT_MS),
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Shared secret detection/redaction used by secret-scanner, commit-guard and
|
||||||
|
* llm-redaction extensions.
|
||||||
|
*
|
||||||
|
* CONFIGURE: add entries to SECRET_PATTERNS below. Each pattern has a `name`
|
||||||
|
* (shown in warnings/audit, never the secret itself) and a `re` (no global
|
||||||
|
* flag — the global flag is added on demand for redaction).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface SecretPattern {
|
||||||
|
name: string;
|
||||||
|
re: RegExp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patterns are deliberately conservative to limit false positives. The user
|
||||||
|
// still confirms interactively, so a rare false positive is cheap.
|
||||||
|
export const SECRET_PATTERNS: SecretPattern[] = [
|
||||||
|
{ name: "aws-access-key-id", re: /\bAKIA[0-9A-Z]{16}\b/ },
|
||||||
|
{ name: "gitlab-pat", re: /\bglpat-[0-9A-Za-z_-]{20,}\b/ },
|
||||||
|
{ name: "github-pat", re: /\bghp_[0-9A-Za-z]{36}\b/ },
|
||||||
|
{ name: "github-fine-grained-pat", re: /\bgithub_pat_[0-9A-Za-z_]{22,}\b/ },
|
||||||
|
{ name: "slack-token", re: /\bxox[baprs]-[0-9A-Za-z-]{10,}\b/ },
|
||||||
|
{ name: "private-key-block", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
|
||||||
|
{ name: "jwt", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/ },
|
||||||
|
// Generic "secret: value" / "token=value" assignments with a non-trivial value.
|
||||||
|
{ name: "generic-credential", re: /\b(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*["']?[^\s"']{8,}["']?/i },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Return the names of secret patterns found in `text` (empty if none). */
|
||||||
|
export function scanSecrets(text: string): string[] {
|
||||||
|
if (!text) return [];
|
||||||
|
const hits: string[] = [];
|
||||||
|
for (const p of SECRET_PATTERNS) {
|
||||||
|
// Use a non-stateful copy (no global flag) to avoid lastIndex surprises.
|
||||||
|
if (p.re.test(text)) hits.push(p.name);
|
||||||
|
}
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Replace every secret occurrence with a labelled placeholder. */
|
||||||
|
export function redact(text: string): string {
|
||||||
|
if (!text) return text;
|
||||||
|
let out = text;
|
||||||
|
for (const p of SECRET_PATTERNS) {
|
||||||
|
const g = new RegExp(p.re.source, p.re.flags.includes("g") ? p.re.flags : `${p.re.flags}g`);
|
||||||
|
out = out.replace(g, `«REDACTED:${p.name}»`);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* LLM Redaction Extension
|
||||||
|
*
|
||||||
|
* WHAT: Before each provider request, redacts secrets from the outbound
|
||||||
|
* payload so tokens/keys never leave the machine in a prompt. Uses the
|
||||||
|
* `before_provider_request` hook and returns a modified payload.
|
||||||
|
*
|
||||||
|
* CONFIGURE: 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 { redact } from "./lib/secrets.ts";
|
||||||
|
import { audit } from "./lib/audit.ts";
|
||||||
|
|
||||||
|
/** Recursively redact secrets in every string within a JSON-like value. */
|
||||||
|
function redactDeep(value: unknown, counter: { n: number }): unknown {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const cleaned = redact(value);
|
||||||
|
if (cleaned !== value) counter.n++;
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) return value.map((v) => redactDeep(v, counter));
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
||||||
|
out[k] = redactDeep(v, counter);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function llmRedactionExtension(pi: ExtensionAPI) {
|
||||||
|
pi.on("before_provider_request", async (event) => {
|
||||||
|
const payload = event.payload;
|
||||||
|
if (!payload) return undefined;
|
||||||
|
|
||||||
|
const counter = { n: 0 };
|
||||||
|
// Only rewrite the message content to avoid disturbing payload metadata.
|
||||||
|
const messages = (payload as { messages?: unknown }).messages;
|
||||||
|
if (messages === undefined) return undefined;
|
||||||
|
|
||||||
|
const redactedMessages = redactDeep(messages, counter);
|
||||||
|
if (counter.n === 0) return undefined;
|
||||||
|
|
||||||
|
audit({ kind: "redaction", occurrences: counter.n });
|
||||||
|
return { ...payload, messages: redactedMessages };
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Secret Scanner Extension
|
||||||
|
*
|
||||||
|
* WHAT: Blocks `write`/`edit` tool calls whose content looks like a secret
|
||||||
|
* (API keys, tokens, private keys, credentials). Complements
|
||||||
|
* protected-paths (which blocks by path) by scanning content.
|
||||||
|
*
|
||||||
|
* CONFIGURE: edit SECRET_PATTERNS 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";
|
||||||
|
|
||||||
|
export default function secretScannerExtension(pi: ExtensionAPI) {
|
||||||
|
pi.on("tool_call", async (event, ctx) => {
|
||||||
|
if (event.toolName !== "write" && event.toolName !== "edit") return undefined;
|
||||||
|
|
||||||
|
// Scan every string field of the input except the path (content, new_string,
|
||||||
|
// etc.) so we don't depend on a specific tool's field naming.
|
||||||
|
const input = event.input as Record<string, unknown>;
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const [key, value] of Object.entries(input)) {
|
||||||
|
if (key === "path") continue;
|
||||||
|
if (typeof value === "string") parts.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hits = scanSecrets(parts.join("\n"));
|
||||||
|
if (hits.length === 0) return undefined;
|
||||||
|
|
||||||
|
const path = typeof input.path === "string" ? input.path : "(unknown)";
|
||||||
|
audit({ kind: "guardrail_block", guardrail: "secret-scanner", tool: event.toolName, path, patterns: hits });
|
||||||
|
|
||||||
|
if (ctx.hasUI) {
|
||||||
|
const choice = await ctx.ui.select(
|
||||||
|
`⚠️ Похоже на секрет (${hits.join(", ")}) в записи → ${path}\nВсё равно записать?`,
|
||||||
|
["Нет", "Да"],
|
||||||
|
);
|
||||||
|
if (choice === "Да") return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { block: true, reason: `Possible secret in content (${hits.join(", ")})` };
|
||||||
|
});
|
||||||
|
}
|
||||||
+2
-1
@@ -27,7 +27,8 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "node --experimental-strip-types test/guardrails.test.ts"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@earendil-works/pi-coding-agent": "*",
|
"@earendil-works/pi-coding-agent": "*",
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* 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();
|
||||||
Reference in New Issue
Block a user