Commit b626269eb8ca
Changed files (2)
dots
pi
agent
extensions
filter-output
dots/pi/agent/extensions/filter-output/index.ts
@@ -0,0 +1,117 @@
+/**
+ * Filter Output Extension for Pi
+ *
+ * Redacts sensitive data from tool outputs before the LLM sees them.
+ * Prevents accidental leakage of API keys, tokens, passwords,
+ * private keys, and connection strings into LLM context.
+ *
+ * Also blocks reading of known sensitive files entirely.
+ *
+ * Source: adapted from michalvavra/agents filter-output.ts
+ */
+
+import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+
+// ── Sensitive data patterns ───────────────────────────────────
+
+const sensitivePatterns = [
+ // API keys
+ { pattern: /\b(sk-[a-zA-Z0-9]{20,})\b/g, replacement: "[OPENAI_KEY_REDACTED]" },
+ { pattern: /\b(ghp_[a-zA-Z0-9]{36,})\b/g, replacement: "[GITHUB_TOKEN_REDACTED]" },
+ { pattern: /\b(gho_[a-zA-Z0-9]{36,})\b/g, replacement: "[GITHUB_OAUTH_REDACTED]" },
+ { pattern: /\b(ghs_[a-zA-Z0-9]{36,})\b/g, replacement: "[GITHUB_SERVER_REDACTED]" },
+ { pattern: /\b(xox[baprs]-[a-zA-Z0-9-]{10,})\b/g, replacement: "[SLACK_TOKEN_REDACTED]" },
+ { pattern: /\b(AKIA[A-Z0-9]{16})\b/g, replacement: "[AWS_KEY_REDACTED]" },
+
+ // Generic key=value secrets
+ {
+ pattern: /\b(api[_-]?key|apikey)\s*[=:]\s*['"]?([a-zA-Z0-9_-]{20,})['"]?/gi,
+ replacement: "$1=[REDACTED]",
+ },
+ {
+ pattern: /\b(secret|token|password|passwd|pwd)\s*[=:]\s*['"]?([^\s'"]{8,})['"]?/gi,
+ replacement: "$1=[REDACTED]",
+ },
+
+ // Bearer tokens
+ { pattern: /\b(bearer)\s+([a-zA-Z0-9._-]{20,})\b/gi, replacement: "Bearer [REDACTED]" },
+
+ // Connection strings with embedded passwords
+ { pattern: /(mongodb(\+srv)?:\/\/[^:]+:)[^@]+(@)/gi, replacement: "$1[REDACTED]$3" },
+ { pattern: /(postgres(ql)?:\/\/[^:]+:)[^@]+(@)/gi, replacement: "$1[REDACTED]$3" },
+ { pattern: /(mysql:\/\/[^:]+:)[^@]+(@)/gi, replacement: "$1[REDACTED]$3" },
+ { pattern: /(redis:\/\/[^:]+:)[^@]+(@)/gi, replacement: "$1[REDACTED]$3" },
+
+ // Private keys
+ {
+ pattern: /-----BEGIN (RSA |EC |OPENSSH |DSA |)PRIVATE KEY-----[\s\S]*?-----END \1PRIVATE KEY-----/g,
+ replacement: "[PRIVATE_KEY_REDACTED]",
+ },
+
+ // Age keys (used by agenix)
+ { pattern: /\bAGE-SECRET-KEY-[A-Z0-9]+\b/g, replacement: "[AGE_SECRET_KEY_REDACTED]" },
+
+ // ntfy/webhook tokens in URLs
+ { pattern: /(https?:\/\/[^/]+\/)[a-zA-Z0-9_-]{20,}$/gm, replacement: "$1[TOKEN_REDACTED]" },
+];
+
+// ── Files to block reading entirely ───────────────────────────
+
+const sensitiveFiles = [
+ /\.env$/,
+ /\.env\.[^/]+$/,
+ /\.dev\.vars($|\.[^/]+$)/,
+ /secrets?\.(json|ya?ml|toml)$/i,
+ /credentials/i,
+];
+
+// ── Extension entry point ─────────────────────────────────────
+
+export default function (pi: ExtensionAPI) {
+ pi.on("tool_result", async (event, ctx) => {
+ if (event.isError) return undefined;
+
+ const textContent = event.content.find(
+ (c): c is { type: "text"; text: string } => c.type === "text",
+ );
+ if (!textContent) return undefined;
+
+ // Block reading sensitive files entirely
+ if (event.toolName === "read") {
+ const filePath = (event.input.path as string) || "";
+ // Allow .env.example files
+ if (/(^|\/)\.env\.example$/i.test(filePath)) {
+ return undefined;
+ }
+ for (const pattern of sensitiveFiles) {
+ if (pattern.test(filePath)) {
+ ctx.ui.notify(`🔒 Redacted contents of sensitive file: ${filePath}`, "info");
+ return {
+ content: [{ type: "text", text: `[Contents of ${filePath} redacted for security]` }],
+ };
+ }
+ }
+ }
+
+ // Redact sensitive patterns from all tool outputs
+ let result = textContent.text;
+ let wasModified = false;
+
+ for (const { pattern, replacement } of sensitivePatterns) {
+ // Reset lastIndex for global regexes
+ pattern.lastIndex = 0;
+ const newResult = result.replace(pattern, replacement);
+ if (newResult !== result) {
+ wasModified = true;
+ result = newResult;
+ }
+ }
+
+ if (wasModified) {
+ ctx.ui.notify("🔒 Sensitive data redacted from output", "info");
+ return { content: [{ type: "text", text: result }] };
+ }
+
+ return undefined;
+ });
+}
dots/pi/agent/extensions/filter-output/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "filter-output",
+ "version": "1.0.0",
+ "type": "module",
+ "main": "index.ts",
+ "dependencies": {
+ "@mariozechner/pi-coding-agent": "*"
+ }
+}