- {JSON.stringify(item.input, null, 2)}
+ {redactSecrets(JSON.stringify(item.input, null, 2))}
)}
diff --git a/apps/web/features/issues/utils/redact.test.ts b/apps/web/features/issues/utils/redact.test.ts
new file mode 100644
index 00000000..9aee9f03
--- /dev/null
+++ b/apps/web/features/issues/utils/redact.test.ts
@@ -0,0 +1,88 @@
+import { describe, it, expect } from "vitest";
+import { redactSecrets } from "./redact";
+
+describe("redactSecrets", () => {
+ it("redacts AWS access key", () => {
+ const result = redactSecrets("key: AKIAIOSFODNN7EXAMPLE");
+ expect(result).not.toContain("AKIAIOSFODNN7EXAMPLE");
+ expect(result).toContain("[REDACTED AWS KEY]");
+ });
+
+ it("redacts AWS secret key", () => {
+ const result = redactSecrets("aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY");
+ expect(result).not.toContain("wJalrXUtnFEMI");
+ });
+
+ it("redacts PEM private keys", () => {
+ const input = "-----BEGIN RSA PRIVATE KEY-----\nMIIEow...\n-----END RSA PRIVATE KEY-----";
+ const result = redactSecrets(input);
+ expect(result).not.toContain("MIIEow");
+ expect(result).toContain("[REDACTED PRIVATE KEY]");
+ });
+
+ it("redacts GitHub tokens", () => {
+ const result = redactSecrets("GITHUB_TOKEN=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn");
+ expect(result).not.toContain("ghp_");
+ });
+
+ it("redacts GitLab tokens", () => {
+ const result = redactSecrets("glpat-AbCdEfGhIjKlMnOpQrStUvWx");
+ expect(result).not.toContain("glpat-");
+ expect(result).toContain("[REDACTED GITLAB TOKEN]");
+ });
+
+ it("redacts OpenAI/Anthropic API keys", () => {
+ const result = redactSecrets("sk-proj-abc123def456ghi789jkl012mno345");
+ expect(result).not.toContain("sk-proj");
+ expect(result).toContain("[REDACTED API KEY]");
+ });
+
+ it("redacts Slack tokens", () => {
+ const result = redactSecrets("xoxb-123456789012-1234567890123-AbCdEfGhIjKl");
+ expect(result).not.toContain("xoxb-");
+ });
+
+ it("redacts JWT tokens", () => {
+ const result = redactSecrets("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c");
+ expect(result).not.toContain("eyJhbGci");
+ expect(result).toContain("[REDACTED JWT]");
+ });
+
+ it("redacts Bearer tokens", () => {
+ const result = redactSecrets("Authorization: Bearer abc123xyz.def456");
+ expect(result).toContain("Bearer [REDACTED]");
+ expect(result).not.toContain("abc123xyz");
+ });
+
+ it("redacts connection strings", () => {
+ const result = redactSecrets("postgres://admin:s3cret@db.example.com:5432/mydb");
+ expect(result).not.toContain("s3cret");
+ });
+
+ it("redacts generic credential env vars", () => {
+ for (const key of ["PASSWORD", "SECRET", "TOKEN", "DATABASE_URL", "API_KEY"]) {
+ const result = redactSecrets(`${key}=supersecretvalue123`);
+ expect(result).toContain("[REDACTED CREDENTIAL]");
+ expect(result).not.toContain("supersecretvalue123");
+ }
+ });
+
+ it("redacts multiple secrets in one string", () => {
+ const result = redactSecrets("AKIAIOSFODNN7EXAMPLE and ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn");
+ expect(result).not.toContain("AKIAIOSFODNN7EXAMPLE");
+ expect(result).not.toContain("ghp_");
+ });
+
+ it("does not alter normal text", () => {
+ const inputs = [
+ "This is a normal commit message about fixing a bug",
+ "The function returns skip-navigation as the class name",
+ "Created PR #42 for the authentication feature",
+ "Running tests in /tmp/test-workspace/project",
+ "The API endpoint /api/issues/123 was updated",
+ ];
+ for (const input of inputs) {
+ expect(redactSecrets(input)).toBe(input);
+ }
+ });
+});
diff --git a/apps/web/features/issues/utils/redact.ts b/apps/web/features/issues/utils/redact.ts
new file mode 100644
index 00000000..066c8fa6
--- /dev/null
+++ b/apps/web/features/issues/utils/redact.ts
@@ -0,0 +1,37 @@
+/**
+ * Client-side fallback for redacting sensitive information in agent output.
+ * The server performs primary redaction; this is a safety net for display.
+ */
+
+const patterns: { re: RegExp; replacement: string }[] = [
+ // AWS access key IDs
+ { re: /\bAKIA[0-9A-Z]{16}\b/g, replacement: "[REDACTED AWS KEY]" },
+ // AWS secret access keys
+ { re: /(?:aws_secret_access_key|secret_?access_?key)\s*[=:]\s*[A-Za-z0-9/+=]{40}/gi, replacement: "[REDACTED AWS SECRET]" },
+ // PEM private keys
+ { re: /-----BEGIN[A-Z\s]*PRIVATE KEY-----[\s\S]*?-----END[A-Z\s]*PRIVATE KEY-----/g, replacement: "[REDACTED PRIVATE KEY]" },
+ // GitHub tokens
+ { re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,255}\b/g, replacement: "[REDACTED GITHUB TOKEN]" },
+ // GitLab personal access tokens
+ { re: /\bglpat-[A-Za-z0-9_-]{20,}\b/g, replacement: "[REDACTED GITLAB TOKEN]" },
+ // OpenAI / Anthropic API keys
+ { re: /\bsk-[A-Za-z0-9_-]{20,}\b/g, replacement: "[REDACTED API KEY]" },
+ // Slack tokens
+ { re: /\bxox[bporas]-[A-Za-z0-9-]{10,}\b/g, replacement: "[REDACTED SLACK TOKEN]" },
+ // JWT tokens
+ { re: /\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, replacement: "[REDACTED JWT]" },
+ // Bearer tokens
+ { re: /\bBearer\s+[A-Za-z0-9\-._~+/]+=*/gi, replacement: "Bearer [REDACTED]" },
+ // Connection strings with embedded passwords
+ { re: /(?:postgres|mysql|mongodb|redis|amqp)(?:ql)?:\/\/[^:\s]+:[^@\s]+@/gi, replacement: "[REDACTED CONNECTION STRING]@" },
+ // Generic key=value secret env vars
+ { re: /(?:API_KEY|API_SECRET|SECRET_KEY|SECRET|ACCESS_TOKEN|AUTH_TOKEN|PRIVATE_KEY|DATABASE_URL|DB_PASSWORD|DB_URL|REDIS_URL|PASSWORD|TOKEN)\s*[=:]\s*\S+/gi, replacement: "[REDACTED CREDENTIAL]" },
+];
+
+export function redactSecrets(text: string): string {
+ let result = text;
+ for (const { re, replacement } of patterns) {
+ result = result.replace(re, replacement);
+ }
+ return result;
+}
diff --git a/server/internal/handler/daemon.go b/server/internal/handler/daemon.go
index b5bf102c..d91b9376 100644
--- a/server/internal/handler/daemon.go
+++ b/server/internal/handler/daemon.go
@@ -11,6 +11,7 @@ import (
"github.com/jackc/pgx/v5/pgtype"
db "github.com/multica-ai/multica/server/pkg/db/generated"
"github.com/multica-ai/multica/server/pkg/protocol"
+ "github.com/multica-ai/multica/server/pkg/redact"
)
// ---------------------------------------------------------------------------
@@ -430,6 +431,11 @@ func (h *Handler) ReportTaskMessages(w http.ResponseWriter, r *http.Request) {
}
for _, msg := range req.Messages {
+ // Redact sensitive information before persisting or broadcasting.
+ msg.Content = redact.Text(msg.Content)
+ msg.Output = redact.Text(msg.Output)
+ msg.Input = redact.InputMap(msg.Input)
+
var inputJSON []byte
if msg.Input != nil {
inputJSON, _ = json.Marshal(msg.Input)
diff --git a/server/pkg/redact/redact.go b/server/pkg/redact/redact.go
index 878b88d8..6a64ca7b 100644
--- a/server/pkg/redact/redact.go
+++ b/server/pkg/redact/redact.go
@@ -35,11 +35,37 @@ var patterns = []secretPattern{
// Slack tokens
{regexp.MustCompile(`\bxox[bporas]-[A-Za-z0-9\-]{10,}\b`), "[REDACTED SLACK TOKEN]"},
+ // GitLab personal access tokens
+ {regexp.MustCompile(`\bglpat-[A-Za-z0-9_-]{20,}\b`), "[REDACTED GITLAB TOKEN]"},
+
+ // JWT tokens (three base64url segments)
+ {regexp.MustCompile(`\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b`), "[REDACTED JWT]"},
+
// Generic "Bearer