diff --git a/scripts/anthropic-prompt-probe.ts b/scripts/anthropic-prompt-probe.ts index dc6782e2a781..6fa2a08161f3 100644 --- a/scripts/anthropic-prompt-probe.ts +++ b/scripts/anthropic-prompt-probe.ts @@ -67,6 +67,7 @@ const CAPTURE_PROXY_MAX_BODY_BYTES = parseStrictIntegerOption({ min: 1, raw: process.env.OPENCLAW_PROMPT_CAPTURE_MAX_BODY_BYTES, }); +const GATEWAY_LOG_TAIL_BYTES = 256 * 1024; const SETUP_TOKEN_RAW = process.env.OPENCLAW_LIVE_SETUP_TOKEN?.trim() ?? ""; const SETUP_TOKEN_VALUE = process.env.OPENCLAW_LIVE_SETUP_TOKEN_VALUE?.trim() ?? ""; const SETUP_TOKEN_PROFILE = process.env.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE?.trim() ?? ""; @@ -626,9 +627,29 @@ async function waitForGatewayReady(url: string, token: string): Promise { throw new Error(lastError); } -async function readLogTail(logPath: string): Promise { - const raw = await fs.readFile(logPath, "utf8").catch(() => ""); - return redactForDevToolLog(raw.split(/\r?\n/).slice(-40).join("\n").trim()); +async function readLogTail(logPath: string, maxBytes = GATEWAY_LOG_TAIL_BYTES): Promise { + if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) { + throw new Error("maxBytes must be a positive integer"); + } + const logFile = await fs.open(logPath, "r").catch(() => undefined); + if (!logFile) { + return ""; + } + try { + const stat = await logFile.stat(); + if (stat.size <= 0) { + return ""; + } + const length = Math.min(stat.size, maxBytes); + const position = Math.max(0, stat.size - length); + const buffer = Buffer.allocUnsafe(length); + const { bytesRead } = await logFile.read(buffer, 0, length, position); + const raw = buffer.subarray(0, bytesRead).toString("utf8"); + const lineAlignedRaw = position > 0 ? raw.replace(/^[^\n]*(?:\r?\n|$)/u, "") : raw; + return redactForDevToolLog(lineAlignedRaw.split(/\r?\n/).slice(-40).join("\n").trim()); + } finally { + await logFile.close(); + } } async function runGatewayPrompt(prompt: string): Promise { @@ -815,6 +836,7 @@ export const testing = { cleanupPromptProbeTmpDir, matchesExtraUsage400, promptProbeTmpResult, + readLogTail, readRequestBody, resolveAnthropicUpstreamUrl, stopGatewayPromptChild, diff --git a/test/scripts/dev-tooling-safety.test.ts b/test/scripts/dev-tooling-safety.test.ts index 75d7d71ff953..5dadd861ced8 100644 --- a/test/scripts/dev-tooling-safety.test.ts +++ b/test/scripts/dev-tooling-safety.test.ts @@ -411,6 +411,41 @@ describe("script-specific dev tooling hardening", () => { expect(destroy).toHaveBeenCalled(); }); + it("reads only the bounded Anthropic prompt probe gateway log tail", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-prompt-probe-log-")); + tempDirs.push(tempRoot); + const logPath = path.join(tempRoot, "gateway.log"); + const token = "sk-test1234567890abcdefghijklmnop"; // pragma: allowlist secret + await fs.writeFile( + logPath, + [ + `DO_NOT_PRINT_OLD_GATEWAY_LOG OPENAI_API_KEY=${token}`, + "x".repeat(256), + `recent gateway tail Authorization: Bearer ${token}`, + ].join("\n"), + "utf8", + ); + + const tail = await promptProbeTesting.readLogTail(logPath, 128); + + expect(tail).toContain("recent gateway tail"); + expect(tail).not.toContain("DO_NOT_PRINT_OLD_GATEWAY_LOG"); + expect(tail).not.toContain(token); + }); + + it("drops partial Anthropic prompt probe log lines before redaction", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-prompt-probe-log-")); + tempDirs.push(tempRoot); + const logPath = path.join(tempRoot, "gateway.log"); + const token = `sk-test${"a".repeat(80)}`; // pragma: allowlist secret + await fs.writeFile(logPath, `Authorization: Bearer ${token}\nrecent gateway tail`, "utf8"); + + const tail = await promptProbeTesting.readLogTail(logPath, "recent gateway tail".length + 24); + + expect(tail).toBe("recent gateway tail"); + expect(tail).not.toContain(token.slice(-16)); + }); + it("cleans Anthropic prompt probe temp dirs unless explicitly kept", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-prompt-probe-test-")); const keepRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-prompt-probe-test-"));