From ee3d8c18905c6f9a4b8be72065c8a6df0396c0fb Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 11 Mar 2026 20:56:47 -0500 Subject: [PATCH] Redact home paths in transcript views Co-Authored-By: Paperclip --- packages/adapter-utils/src/index.ts | 6 ++ packages/adapter-utils/src/log-redaction.ts | 81 +++++++++++++++++++ .../codex-local/src/ui/parse-stdout.ts | 61 ++++++++------ .../src/__tests__/codex-local-adapter.test.ts | 4 +- ui/src/adapters/transcript.ts | 9 ++- ui/src/pages/AgentDetail.tsx | 29 ++++--- 6 files changed, 146 insertions(+), 44 deletions(-) create mode 100644 packages/adapter-utils/src/log-redaction.ts diff --git a/packages/adapter-utils/src/index.ts b/packages/adapter-utils/src/index.ts index 89f03fb4..56579022 100644 --- a/packages/adapter-utils/src/index.ts +++ b/packages/adapter-utils/src/index.ts @@ -22,3 +22,9 @@ export type { CLIAdapterModule, CreateConfigValues, } from "./types.js"; +export { + REDACTED_HOME_PATH_USER, + redactHomePathUserSegments, + redactHomePathUserSegmentsInValue, + redactTranscriptEntryPaths, +} from "./log-redaction.js"; diff --git a/packages/adapter-utils/src/log-redaction.ts b/packages/adapter-utils/src/log-redaction.ts new file mode 100644 index 00000000..037e279e --- /dev/null +++ b/packages/adapter-utils/src/log-redaction.ts @@ -0,0 +1,81 @@ +import type { TranscriptEntry } from "./types.js"; + +export const REDACTED_HOME_PATH_USER = "[]"; + +const HOME_PATH_PATTERNS = [ + { + regex: /\/Users\/[^/\\\s]+/g, + replace: `/Users/${REDACTED_HOME_PATH_USER}`, + }, + { + regex: /\/home\/[^/\\\s]+/g, + replace: `/home/${REDACTED_HOME_PATH_USER}`, + }, + { + regex: /([A-Za-z]:\\Users\\)[^\\/\s]+/g, + replace: `$1${REDACTED_HOME_PATH_USER}`, + }, +] as const; + +function isPlainObject(value: unknown): value is Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) return false; + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} + +export function redactHomePathUserSegments(text: string): string { + let result = text; + for (const pattern of HOME_PATH_PATTERNS) { + result = result.replace(pattern.regex, pattern.replace); + } + return result; +} + +export function redactHomePathUserSegmentsInValue(value: T): T { + if (typeof value === "string") { + return redactHomePathUserSegments(value) as T; + } + if (Array.isArray(value)) { + return value.map((entry) => redactHomePathUserSegmentsInValue(entry)) as T; + } + if (!isPlainObject(value)) { + return value; + } + + const redacted: Record = {}; + for (const [key, entry] of Object.entries(value)) { + redacted[key] = redactHomePathUserSegmentsInValue(entry); + } + return redacted as T; +} + +export function redactTranscriptEntryPaths(entry: TranscriptEntry): TranscriptEntry { + switch (entry.kind) { + case "assistant": + case "thinking": + case "user": + case "stderr": + case "system": + case "stdout": + return { ...entry, text: redactHomePathUserSegments(entry.text) }; + case "tool_call": + return { ...entry, name: redactHomePathUserSegments(entry.name), input: redactHomePathUserSegmentsInValue(entry.input) }; + case "tool_result": + return { ...entry, content: redactHomePathUserSegments(entry.content) }; + case "init": + return { + ...entry, + model: redactHomePathUserSegments(entry.model), + sessionId: redactHomePathUserSegments(entry.sessionId), + }; + case "result": + return { + ...entry, + text: redactHomePathUserSegments(entry.text), + subtype: redactHomePathUserSegments(entry.subtype), + errors: entry.errors.map((error) => redactHomePathUserSegments(error)), + }; + default: + return entry; + } +} diff --git a/packages/adapters/codex-local/src/ui/parse-stdout.ts b/packages/adapters/codex-local/src/ui/parse-stdout.ts index 7f4028a0..c3151b05 100644 --- a/packages/adapters/codex-local/src/ui/parse-stdout.ts +++ b/packages/adapters/codex-local/src/ui/parse-stdout.ts @@ -1,4 +1,8 @@ -import type { TranscriptEntry } from "@paperclipai/adapter-utils"; +import { + redactHomePathUserSegments, + redactHomePathUserSegmentsInValue, + type TranscriptEntry, +} from "@paperclipai/adapter-utils"; function safeJsonParse(text: string): unknown { try { @@ -39,12 +43,12 @@ function errorText(value: unknown): string { } function stringifyUnknown(value: unknown): string { - if (typeof value === "string") return value; + if (typeof value === "string") return redactHomePathUserSegments(value); if (value === null || value === undefined) return ""; try { - return JSON.stringify(value, null, 2); + return JSON.stringify(redactHomePathUserSegmentsInValue(value), null, 2); } catch { - return String(value); + return redactHomePathUserSegments(String(value)); } } @@ -57,7 +61,8 @@ function parseCommandExecutionItem( const command = asString(item.command); const status = asString(item.status); const exitCode = typeof item.exit_code === "number" && Number.isFinite(item.exit_code) ? item.exit_code : null; - const output = asString(item.aggregated_output).replace(/\s+$/, ""); + const safeCommand = redactHomePathUserSegments(command); + const output = redactHomePathUserSegments(asString(item.aggregated_output)).replace(/\s+$/, ""); if (phase === "started") { return [{ @@ -67,13 +72,13 @@ function parseCommandExecutionItem( toolUseId: id || command || "command_execution", input: { id, - command, + command: safeCommand, }, }]; } const lines: string[] = []; - if (command) lines.push(`command: ${command}`); + if (safeCommand) lines.push(`command: ${safeCommand}`); if (status) lines.push(`status: ${status}`); if (exitCode !== null) lines.push(`exit_code: ${exitCode}`); if (output) { @@ -104,7 +109,7 @@ function parseFileChangeItem(item: Record, ts: string): Transcr .filter((change): change is Record => Boolean(change)) .map((change) => { const kind = asString(change.kind, "update"); - const path = asString(change.path, "unknown"); + const path = redactHomePathUserSegments(asString(change.path, "unknown")); return `${kind} ${path}`; }); @@ -126,13 +131,13 @@ function parseCodexItem( if (itemType === "agent_message") { const text = asString(item.text); - if (text) return [{ kind: "assistant", ts, text }]; + if (text) return [{ kind: "assistant", ts, text: redactHomePathUserSegments(text) }]; return []; } if (itemType === "reasoning") { const text = asString(item.text); - if (text) return [{ kind: "thinking", ts, text }]; + if (text) return [{ kind: "thinking", ts, text: redactHomePathUserSegments(text) }]; return [{ kind: "system", ts, text: phase === "started" ? "reasoning started" : "reasoning completed" }]; } @@ -148,9 +153,9 @@ function parseCodexItem( return [{ kind: "tool_call", ts, - name: asString(item.name, "unknown"), + name: redactHomePathUserSegments(asString(item.name, "unknown")), toolUseId: asString(item.id), - input: item.input ?? {}, + input: redactHomePathUserSegmentsInValue(item.input ?? {}), }]; } @@ -162,24 +167,28 @@ function parseCodexItem( asString(item.result) || stringifyUnknown(item.content ?? item.output ?? item.result); const isError = item.is_error === true || asString(item.status) === "error"; - return [{ kind: "tool_result", ts, toolUseId, content, isError }]; + return [{ kind: "tool_result", ts, toolUseId, content: redactHomePathUserSegments(content), isError }]; } if (itemType === "error" && phase === "completed") { const text = errorText(item.message ?? item.error ?? item); - return [{ kind: "stderr", ts, text: text || "error" }]; + return [{ kind: "stderr", ts, text: redactHomePathUserSegments(text || "error") }]; } const id = asString(item.id); const status = asString(item.status); const meta = [id ? `id=${id}` : "", status ? `status=${status}` : ""].filter(Boolean).join(" "); - return [{ kind: "system", ts, text: `item ${phase}: ${itemType || "unknown"}${meta ? ` (${meta})` : ""}` }]; + return [{ + kind: "system", + ts, + text: redactHomePathUserSegments(`item ${phase}: ${itemType || "unknown"}${meta ? ` (${meta})` : ""}`), + }]; } export function parseCodexStdoutLine(line: string, ts: string): TranscriptEntry[] { const parsed = asRecord(safeJsonParse(line)); if (!parsed) { - return [{ kind: "stdout", ts, text: line }]; + return [{ kind: "stdout", ts, text: redactHomePathUserSegments(line) }]; } const type = asString(parsed.type); @@ -189,8 +198,8 @@ export function parseCodexStdoutLine(line: string, ts: string): TranscriptEntry[ return [{ kind: "init", ts, - model: asString(parsed.model, "codex"), - sessionId: threadId, + model: redactHomePathUserSegments(asString(parsed.model, "codex")), + sessionId: redactHomePathUserSegments(threadId), }]; } @@ -212,15 +221,15 @@ export function parseCodexStdoutLine(line: string, ts: string): TranscriptEntry[ return [{ kind: "result", ts, - text: asString(parsed.result), + text: redactHomePathUserSegments(asString(parsed.result)), inputTokens, outputTokens, cachedTokens, costUsd: asNumber(parsed.total_cost_usd), - subtype: asString(parsed.subtype), + subtype: redactHomePathUserSegments(asString(parsed.subtype)), isError: parsed.is_error === true, errors: Array.isArray(parsed.errors) - ? parsed.errors.map(errorText).filter(Boolean) + ? parsed.errors.map(errorText).map(redactHomePathUserSegments).filter(Boolean) : [], }]; } @@ -234,21 +243,21 @@ export function parseCodexStdoutLine(line: string, ts: string): TranscriptEntry[ return [{ kind: "result", ts, - text: asString(parsed.result), + text: redactHomePathUserSegments(asString(parsed.result)), inputTokens, outputTokens, cachedTokens, costUsd: asNumber(parsed.total_cost_usd), - subtype: asString(parsed.subtype, "turn.failed"), + subtype: redactHomePathUserSegments(asString(parsed.subtype, "turn.failed")), isError: true, - errors: message ? [message] : [], + errors: message ? [redactHomePathUserSegments(message)] : [], }]; } if (type === "error") { const message = errorText(parsed.message ?? parsed.error ?? parsed); - return [{ kind: "stderr", ts, text: message || line }]; + return [{ kind: "stderr", ts, text: redactHomePathUserSegments(message || line) }]; } - return [{ kind: "stdout", ts, text: line }]; + return [{ kind: "stdout", ts, text: redactHomePathUserSegments(line) }]; } diff --git a/server/src/__tests__/codex-local-adapter.test.ts b/server/src/__tests__/codex-local-adapter.test.ts index 07733399..18479e43 100644 --- a/server/src/__tests__/codex-local-adapter.test.ts +++ b/server/src/__tests__/codex-local-adapter.test.ts @@ -107,7 +107,7 @@ describe("codex_local ui stdout parser", () => { item: { id: "item_52", type: "file_change", - changes: [{ path: "/home/user/project/ui/src/pages/AgentDetail.tsx", kind: "update" }], + changes: [{ path: "/Users/paperclipuser/project/ui/src/pages/AgentDetail.tsx", kind: "update" }], status: "completed", }, }), @@ -117,7 +117,7 @@ describe("codex_local ui stdout parser", () => { { kind: "system", ts, - text: "file changes: update /home/user/project/ui/src/pages/AgentDetail.tsx", + text: "file changes: update /Users/[]/project/ui/src/pages/AgentDetail.tsx", }, ]); }); diff --git a/ui/src/adapters/transcript.ts b/ui/src/adapters/transcript.ts index 394fd999..545c94f4 100644 --- a/ui/src/adapters/transcript.ts +++ b/ui/src/adapters/transcript.ts @@ -1,3 +1,4 @@ +import { redactHomePathUserSegments, redactTranscriptEntryPaths } from "@paperclipai/adapter-utils"; import type { TranscriptEntry, StdoutLineParser } from "./types"; export type RunLogChunk = { ts: string; stream: "stdout" | "stderr" | "system"; chunk: string }; @@ -26,11 +27,11 @@ export function buildTranscript(chunks: RunLogChunk[], parser: StdoutLineParser) for (const chunk of chunks) { if (chunk.stream === "stderr") { - entries.push({ kind: "stderr", ts: chunk.ts, text: chunk.chunk }); + entries.push({ kind: "stderr", ts: chunk.ts, text: redactHomePathUserSegments(chunk.chunk) }); continue; } if (chunk.stream === "system") { - entries.push({ kind: "system", ts: chunk.ts, text: chunk.chunk }); + entries.push({ kind: "system", ts: chunk.ts, text: redactHomePathUserSegments(chunk.chunk) }); continue; } @@ -40,14 +41,14 @@ export function buildTranscript(chunks: RunLogChunk[], parser: StdoutLineParser) for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; - appendTranscriptEntries(entries, parser(trimmed, chunk.ts)); + appendTranscriptEntries(entries, parser(trimmed, chunk.ts).map(redactTranscriptEntryPaths)); } } const trailing = stdoutBuffer.trim(); if (trailing) { const ts = chunks.length > 0 ? chunks[chunks.length - 1]!.ts : new Date().toISOString(); - appendTranscriptEntries(entries, parser(trailing, ts)); + appendTranscriptEntries(entries, parser(trailing, ts).map(redactTranscriptEntryPaths)); } return entries; diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index b0eeb89f..30921807 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -59,6 +59,7 @@ import { Input } from "@/components/ui/input"; import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker"; import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView"; import { isUuidLike, type Agent, type HeartbeatRun, type HeartbeatRunEvent, type AgentRuntimeState, type LiveEvent } from "@paperclipai/shared"; +import { redactHomePathUserSegments, redactHomePathUserSegmentsInValue } from "@paperclipai/adapter-utils"; import { agentRouteRef } from "../lib/utils"; const runStatusIcons: Record = { @@ -92,11 +93,11 @@ function redactEnvValue(key: string, value: unknown): string { } if (shouldRedactSecretValue(key, value)) return REDACTED_ENV_VALUE; if (value === null || value === undefined) return ""; - if (typeof value === "string") return value; + if (typeof value === "string") return redactHomePathUserSegments(value); try { - return JSON.stringify(value); + return JSON.stringify(redactHomePathUserSegmentsInValue(value)); } catch { - return String(value); + return redactHomePathUserSegments(String(value)); } } @@ -2023,7 +2024,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin const adapterInvokePayload = useMemo(() => { const evt = events.find((e) => e.eventType === "adapter.invoke"); - return asRecord(evt?.payload ?? null); + return redactHomePathUserSegmentsInValue(asRecord(evt?.payload ?? null)); }, [events]); const adapter = useMemo(() => getUIAdapter(adapterType), [adapterType]); @@ -2096,8 +2097,8 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
Prompt
                 {typeof adapterInvokePayload.prompt === "string"
-                  ? adapterInvokePayload.prompt
-                  : JSON.stringify(adapterInvokePayload.prompt, null, 2)}
+                  ? redactHomePathUserSegments(adapterInvokePayload.prompt)
+                  : JSON.stringify(redactHomePathUserSegmentsInValue(adapterInvokePayload.prompt), null, 2)}
               
)} @@ -2105,7 +2106,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
Context
-                {JSON.stringify(adapterInvokePayload.context, null, 2)}
+                {JSON.stringify(redactHomePathUserSegmentsInValue(adapterInvokePayload.context), null, 2)}
               
)} @@ -2189,14 +2190,14 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin {run.error && (
Error: - {run.error} + {redactHomePathUserSegments(run.error)}
)} {run.stderrExcerpt && run.stderrExcerpt.trim() && (
stderr excerpt
-                {run.stderrExcerpt}
+                {redactHomePathUserSegments(run.stderrExcerpt)}
               
)} @@ -2204,7 +2205,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
adapter result JSON
-                {JSON.stringify(run.resultJson, null, 2)}
+                {JSON.stringify(redactHomePathUserSegmentsInValue(run.resultJson), null, 2)}
               
)} @@ -2212,7 +2213,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
stdout excerpt
-                {run.stdoutExcerpt}
+                {redactHomePathUserSegments(run.stdoutExcerpt)}
               
)} @@ -2238,7 +2239,11 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin {evt.stream ? `[${evt.stream}]` : ""} - {evt.message ?? (evt.payload ? JSON.stringify(evt.payload) : "")} + {evt.message + ? redactHomePathUserSegments(evt.message) + : evt.payload + ? JSON.stringify(redactHomePathUserSegmentsInValue(evt.payload)) + : ""} );