Redact home paths in transcript views
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -22,3 +22,9 @@ export type {
|
||||
CLIAdapterModule,
|
||||
CreateConfigValues,
|
||||
} from "./types.js";
|
||||
export {
|
||||
REDACTED_HOME_PATH_USER,
|
||||
redactHomePathUserSegments,
|
||||
redactHomePathUserSegmentsInValue,
|
||||
redactTranscriptEntryPaths,
|
||||
} from "./log-redaction.js";
|
||||
|
||||
81
packages/adapter-utils/src/log-redaction.ts
Normal file
81
packages/adapter-utils/src/log-redaction.ts
Normal file
@@ -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<string, unknown> {
|
||||
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<T>(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<string, unknown> = {};
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<string, unknown>, ts: string): Transcr
|
||||
.filter((change): change is Record<string, unknown> => 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) }];
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string, { icon: typeof CheckCircle2; color: string }> = {
|
||||
@@ -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
|
||||
<div className="text-xs text-muted-foreground mb-1">Prompt</div>
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap">
|
||||
{typeof adapterInvokePayload.prompt === "string"
|
||||
? adapterInvokePayload.prompt
|
||||
: JSON.stringify(adapterInvokePayload.prompt, null, 2)}
|
||||
? redactHomePathUserSegments(adapterInvokePayload.prompt)
|
||||
: JSON.stringify(redactHomePathUserSegmentsInValue(adapterInvokePayload.prompt), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
@@ -2105,7 +2106,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">Context</div>
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap">
|
||||
{JSON.stringify(adapterInvokePayload.context, null, 2)}
|
||||
{JSON.stringify(redactHomePathUserSegmentsInValue(adapterInvokePayload.context), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
@@ -2189,14 +2190,14 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
||||
{run.error && (
|
||||
<div className="text-xs text-red-600 dark:text-red-200">
|
||||
<span className="text-red-700 dark:text-red-300">Error: </span>
|
||||
{run.error}
|
||||
{redactHomePathUserSegments(run.error)}
|
||||
</div>
|
||||
)}
|
||||
{run.stderrExcerpt && run.stderrExcerpt.trim() && (
|
||||
<div>
|
||||
<div className="text-xs text-red-700 dark:text-red-300 mb-1">stderr excerpt</div>
|
||||
<pre className="bg-red-50 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-red-800 dark:text-red-100">
|
||||
{run.stderrExcerpt}
|
||||
{redactHomePathUserSegments(run.stderrExcerpt)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
@@ -2204,7 +2205,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
||||
<div>
|
||||
<div className="text-xs text-red-700 dark:text-red-300 mb-1">adapter result JSON</div>
|
||||
<pre className="bg-red-50 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-red-800 dark:text-red-100">
|
||||
{JSON.stringify(run.resultJson, null, 2)}
|
||||
{JSON.stringify(redactHomePathUserSegmentsInValue(run.resultJson), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
@@ -2212,7 +2213,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
||||
<div>
|
||||
<div className="text-xs text-red-700 dark:text-red-300 mb-1">stdout excerpt</div>
|
||||
<pre className="bg-red-50 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-red-800 dark:text-red-100">
|
||||
{run.stdoutExcerpt}
|
||||
{redactHomePathUserSegments(run.stdoutExcerpt)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
@@ -2238,7 +2239,11 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
||||
{evt.stream ? `[${evt.stream}]` : ""}
|
||||
</span>
|
||||
<span className={cn("break-all", color)}>
|
||||
{evt.message ?? (evt.payload ? JSON.stringify(evt.payload) : "")}
|
||||
{evt.message
|
||||
? redactHomePathUserSegments(evt.message)
|
||||
: evt.payload
|
||||
? JSON.stringify(redactHomePathUserSegmentsInValue(evt.payload))
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user