mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix: redact trajectory exports consistently (#89354)
* fix trajectory export redaction * fix trajectory export top-level redaction * fix trajectory export key redaction * fix trajectory export structural key redaction
This commit is contained in:
@@ -272,6 +272,172 @@ describe("exportTrajectoryBundle", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("redacts broad secret patterns from every exported bundle file", async () => {
|
||||
const tmpDir = makeTempDir();
|
||||
const sessionFile = path.join(tmpDir, "session.jsonl");
|
||||
const runtimeFile = path.join(tmpDir, "session.trajectory.jsonl");
|
||||
const outputDir = path.join(tmpDir, "bundle");
|
||||
const rawSecrets = [
|
||||
"sk-exported-session-secret",
|
||||
"ghp_123456789012345678901234",
|
||||
"xoxb-1234567890-abcdefghijkl",
|
||||
"ya29.exported-access-token-with-enough-length",
|
||||
"ADMIN_PASSWORD=plain-text-password",
|
||||
"sk-top-level-export-secret",
|
||||
];
|
||||
const header = {
|
||||
type: "session",
|
||||
version: 3,
|
||||
id: "session-1",
|
||||
timestamp: "2026-04-01T05:46:39.000Z",
|
||||
cwd: tmpDir,
|
||||
};
|
||||
const userEntry = {
|
||||
type: "message",
|
||||
id: "entry-user",
|
||||
parentId: null,
|
||||
timestamp: "2026-04-01T05:46:40.000Z",
|
||||
message: userMessage(`user pasted ${rawSecrets[0]} keep-visible-marker`),
|
||||
};
|
||||
const assistantEntry = {
|
||||
type: "message",
|
||||
id: "entry-assistant",
|
||||
parentId: "entry-user",
|
||||
timestamp: "2026-04-01T05:46:41.000Z",
|
||||
message: assistantMessage([
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call_1",
|
||||
name: "read",
|
||||
arguments: {
|
||||
[rawSecrets[5]]: "secret-looking tool argument key",
|
||||
command: `curl -H 'Authorization: Bearer ${rawSecrets[1]}'`,
|
||||
},
|
||||
},
|
||||
]),
|
||||
};
|
||||
const compactionEntry = {
|
||||
type: "compaction",
|
||||
id: "entry-compaction",
|
||||
parentId: "entry-assistant",
|
||||
timestamp: "2026-04-01T05:46:42.000Z",
|
||||
summary: `compaction summary saw ${rawSecrets[2]}`,
|
||||
firstKeptEntryId: "entry-assistant",
|
||||
tokensBefore: 1024,
|
||||
details: { note: rawSecrets[3] },
|
||||
};
|
||||
const branchSummaryEntry = {
|
||||
type: "branch_summary",
|
||||
id: "entry-branch-summary",
|
||||
parentId: "entry-compaction",
|
||||
timestamp: "2026-04-01T05:46:43.000Z",
|
||||
fromId: "entry-assistant",
|
||||
summary: `branch summary saw ${rawSecrets[4]}`,
|
||||
details: { token: rawSecrets[0] },
|
||||
};
|
||||
fs.writeFileSync(
|
||||
sessionFile,
|
||||
`${[header, userEntry, assistantEntry, compactionEntry, branchSummaryEntry]
|
||||
.map((entry) => JSON.stringify(entry))
|
||||
.join("\n")}\n`,
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
runtimeFile,
|
||||
[
|
||||
{
|
||||
traceSchema: "openclaw-trajectory",
|
||||
schemaVersion: 1,
|
||||
traceId: "session-1",
|
||||
source: "runtime",
|
||||
type: "context.compiled",
|
||||
ts: "2026-04-22T08:00:00.000Z",
|
||||
seq: 1,
|
||||
sourceSeq: 1,
|
||||
sessionId: "session-1",
|
||||
apiKey: rawSecrets[5],
|
||||
data: {
|
||||
systemPrompt: `system includes ${rawSecrets[1]}`,
|
||||
tools: [{ name: "danger", description: `tool mentions ${rawSecrets[2]}` }],
|
||||
},
|
||||
},
|
||||
{
|
||||
traceSchema: "openclaw-trajectory",
|
||||
schemaVersion: 1,
|
||||
traceId: "session-1",
|
||||
source: "runtime",
|
||||
type: "trace.metadata",
|
||||
ts: "2026-04-22T08:00:01.000Z",
|
||||
seq: 2,
|
||||
sourceSeq: 2,
|
||||
sessionId: "session-1",
|
||||
data: {
|
||||
harness: { type: "openclaw", token: rawSecrets[3] },
|
||||
metadata: {
|
||||
[`https://example.test/callback?token=${rawSecrets[1]}`]:
|
||||
"secret-looking metadata key",
|
||||
},
|
||||
prompting: {
|
||||
skillsPrompt: `skills ${rawSecrets[4]}`,
|
||||
userPromptPrefixText: `prefix ${rawSecrets[0]}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
traceSchema: "openclaw-trajectory",
|
||||
schemaVersion: 1,
|
||||
traceId: "session-1",
|
||||
source: "runtime",
|
||||
type: "prompt.submitted",
|
||||
ts: "2026-04-22T08:00:02.000Z",
|
||||
seq: 3,
|
||||
sourceSeq: 3,
|
||||
sessionId: "session-1",
|
||||
data: { prompt: `submitted ${rawSecrets[1]}` },
|
||||
},
|
||||
{
|
||||
traceSchema: "openclaw-trajectory",
|
||||
schemaVersion: 1,
|
||||
traceId: "session-1",
|
||||
source: "runtime",
|
||||
type: "trace.artifacts",
|
||||
ts: "2026-04-22T08:00:03.000Z",
|
||||
seq: 4,
|
||||
sourceSeq: 4,
|
||||
sessionId: "session-1",
|
||||
runId: rawSecrets[5],
|
||||
data: {
|
||||
assistantTexts: [`assistant ${rawSecrets[2]}`],
|
||||
finalPromptText: `final ${rawSecrets[3]}`,
|
||||
},
|
||||
},
|
||||
]
|
||||
.map((entry) => JSON.stringify(entry))
|
||||
.join("\n") + "\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const bundle = await exportTrajectoryBundle({
|
||||
outputDir,
|
||||
sessionFile,
|
||||
sessionId: "session-1",
|
||||
sessionKey: rawSecrets[5],
|
||||
workspaceDir: tmpDir,
|
||||
runtimeFile,
|
||||
});
|
||||
|
||||
const exportedBundleText = fs
|
||||
.readdirSync(outputDir)
|
||||
.map((file) => fs.readFileSync(path.join(outputDir, file), "utf8"))
|
||||
.join("\n");
|
||||
expect(exportedBundleText).toContain("keep-visible-marker");
|
||||
for (const secret of rawSecrets) {
|
||||
expect(exportedBundleText).not.toContain(secret);
|
||||
}
|
||||
expect(JSON.stringify(bundle.events)).not.toContain(rawSecrets[5]);
|
||||
expect(JSON.stringify(bundle.manifest)).not.toContain(rawSecrets[5]);
|
||||
});
|
||||
|
||||
it("rejects oversized runtime trajectory files", async () => {
|
||||
const tmpDir = makeTempDir();
|
||||
const sessionFile = path.join(tmpDir, "session.jsonl");
|
||||
|
||||
+84
-20
@@ -18,6 +18,7 @@ import {
|
||||
redactSupportString,
|
||||
type SupportRedactionContext,
|
||||
} from "../logging/diagnostic-support-redaction.js";
|
||||
import { redactSecrets, redactToolPayloadText } from "../logging/redact.js";
|
||||
import { safeJsonStringify } from "../utils/safe-json.js";
|
||||
import { TRAJECTORY_RUNTIME_FILE_MAX_BYTES, safeTrajectorySessionFileName } from "./paths.js";
|
||||
import { isRegularNonSymlinkFile, resolveTrajectoryRuntimeFile } from "./runtime-file.js";
|
||||
@@ -389,6 +390,10 @@ function extractAssistantToolCalls(
|
||||
});
|
||||
}
|
||||
|
||||
function sanitizeTrajectoryExportValue<T>(value: T): T {
|
||||
return redactSecrets(sanitizeDiagnosticPayload(value)) as T;
|
||||
}
|
||||
|
||||
function buildTranscriptEvents(params: {
|
||||
entries: SessionEntry[];
|
||||
sessionId: string;
|
||||
@@ -525,6 +530,15 @@ function trajectoryJsonlFile(
|
||||
return jsonlSupportBundleFile(pathName, lines);
|
||||
}
|
||||
|
||||
function redactTrajectoryBundleFileContent(
|
||||
file: DiagnosticSupportBundleFile,
|
||||
): DiagnosticSupportBundleFile {
|
||||
return {
|
||||
...file,
|
||||
content: redactToolPayloadText(file.content),
|
||||
};
|
||||
}
|
||||
|
||||
function buildTrajectoryExportRedaction(params: {
|
||||
workspaceDir: string;
|
||||
}): TrajectoryExportRedaction {
|
||||
@@ -585,19 +599,55 @@ function redactLocalPathValues(value: unknown, redaction: TrajectoryExportRedact
|
||||
return next;
|
||||
}
|
||||
|
||||
function uniqueRedactedObjectKey(key: string, usedKeys: Set<string>): string {
|
||||
if (!usedKeys.has(key)) {
|
||||
usedKeys.add(key);
|
||||
return key;
|
||||
}
|
||||
let index = 2;
|
||||
while (usedKeys.has(`${key}#${index}`)) {
|
||||
index += 1;
|
||||
}
|
||||
const unique = `${key}#${index}`;
|
||||
usedKeys.add(unique);
|
||||
return unique;
|
||||
}
|
||||
|
||||
function redactTrajectoryExportObjectKeys(
|
||||
value: unknown,
|
||||
redaction: TrajectoryExportRedaction,
|
||||
): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => redactTrajectoryExportObjectKeys(entry, redaction));
|
||||
}
|
||||
if (!value || typeof value !== "object") {
|
||||
return value;
|
||||
}
|
||||
const usedKeys = new Set<string>();
|
||||
const next: Record<string, unknown> = {};
|
||||
for (const [key, entry] of Object.entries(value)) {
|
||||
const redactedKey = redactToolPayloadText(maybeRedactPathString(key, redaction));
|
||||
next[uniqueRedactedObjectKey(redactedKey, usedKeys)] = redactTrajectoryExportObjectKeys(
|
||||
entry,
|
||||
redaction,
|
||||
);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function redactTrajectoryExportValue(
|
||||
value: unknown,
|
||||
redaction: TrajectoryExportRedaction,
|
||||
): unknown {
|
||||
const redactedValue = sanitizeTrajectoryExportValue(redactLocalPathValues(value, redaction));
|
||||
return redactTrajectoryExportObjectKeys(redactedValue, redaction);
|
||||
}
|
||||
|
||||
function redactEventForExport(
|
||||
event: TrajectoryEvent,
|
||||
redaction: TrajectoryExportRedaction,
|
||||
): TrajectoryEvent {
|
||||
return {
|
||||
...event,
|
||||
workspaceDir: event.workspaceDir
|
||||
? maybeRedactPathString(event.workspaceDir, redaction)
|
||||
: undefined,
|
||||
data: event.data
|
||||
? (redactLocalPathValues(event.data, redaction) as Record<string, unknown>)
|
||||
: undefined,
|
||||
};
|
||||
return redactTrajectoryExportValue(event, redaction) as TrajectoryEvent;
|
||||
}
|
||||
|
||||
function resolveRuntimeContext(runtimeEvents: TrajectoryEvent[]): RuntimeTrajectoryContext {
|
||||
@@ -967,19 +1017,25 @@ export async function exportTrajectoryBundle(params: BuildTrajectoryBundleParams
|
||||
});
|
||||
if (metadataCapture) {
|
||||
files.push(
|
||||
jsonSupportBundleFile("metadata.json", redactLocalPathValues(metadataCapture, redaction)),
|
||||
jsonSupportBundleFile(
|
||||
"metadata.json",
|
||||
redactTrajectoryExportValue(metadataCapture, redaction),
|
||||
),
|
||||
);
|
||||
supplementalFiles.push("metadata.json");
|
||||
}
|
||||
if (artifactsCapture) {
|
||||
files.push(
|
||||
jsonSupportBundleFile("artifacts.json", redactLocalPathValues(artifactsCapture, redaction)),
|
||||
jsonSupportBundleFile(
|
||||
"artifacts.json",
|
||||
redactTrajectoryExportValue(artifactsCapture, redaction),
|
||||
),
|
||||
);
|
||||
supplementalFiles.push("artifacts.json");
|
||||
}
|
||||
if (promptsCapture) {
|
||||
files.push(
|
||||
jsonSupportBundleFile("prompts.json", redactLocalPathValues(promptsCapture, redaction)),
|
||||
jsonSupportBundleFile("prompts.json", redactTrajectoryExportValue(promptsCapture, redaction)),
|
||||
);
|
||||
supplementalFiles.push("prompts.json");
|
||||
}
|
||||
@@ -991,12 +1047,12 @@ export async function exportTrajectoryBundle(params: BuildTrajectoryBundleParams
|
||||
files.push(
|
||||
jsonSupportBundleFile(
|
||||
"session-branch.json",
|
||||
redactLocalPathValues(
|
||||
sanitizeDiagnosticPayload({
|
||||
redactTrajectoryExportValue(
|
||||
{
|
||||
header,
|
||||
leafId,
|
||||
entries: branchEntries,
|
||||
}),
|
||||
},
|
||||
redaction,
|
||||
),
|
||||
),
|
||||
@@ -1005,7 +1061,7 @@ export async function exportTrajectoryBundle(params: BuildTrajectoryBundleParams
|
||||
files.push(
|
||||
textSupportBundleFile(
|
||||
"system-prompt.txt",
|
||||
redactLocalPathValues(bundleRuntimeContext.systemPrompt, redaction) as string,
|
||||
redactTrajectoryExportValue(bundleRuntimeContext.systemPrompt, redaction) as string,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1013,21 +1069,29 @@ export async function exportTrajectoryBundle(params: BuildTrajectoryBundleParams
|
||||
files.push(
|
||||
jsonSupportBundleFile(
|
||||
"tools.json",
|
||||
redactLocalPathValues(bundleRuntimeContext.tools, redaction),
|
||||
redactTrajectoryExportValue(bundleRuntimeContext.tools, redaction),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const contents: DiagnosticSupportBundleContent[] = [...supportBundleContents(files)];
|
||||
const redactedFiles = files.map(redactTrajectoryBundleFileContent);
|
||||
const contents: DiagnosticSupportBundleContent[] = [...supportBundleContents(redactedFiles)];
|
||||
manifest.contents = contents;
|
||||
const redactedManifest = redactTrajectoryExportValue(
|
||||
manifest,
|
||||
redaction,
|
||||
) as TrajectoryBundleManifest;
|
||||
const manifestFile = redactTrajectoryBundleFileContent(
|
||||
jsonSupportBundleFile("manifest.json", redactedManifest),
|
||||
);
|
||||
|
||||
await writeSupportBundleDirectory({
|
||||
outputDir: params.outputDir,
|
||||
files: [jsonSupportBundleFile("manifest.json", manifest), ...files],
|
||||
files: [manifestFile, ...redactedFiles],
|
||||
});
|
||||
|
||||
return {
|
||||
manifest,
|
||||
manifest: redactedManifest,
|
||||
outputDir: params.outputDir,
|
||||
events,
|
||||
header,
|
||||
|
||||
Reference in New Issue
Block a user