




















@@ -13,6 +13,8 @@ import { guardSessionManager } from "./session-tool-result-guard-wrapper.js";
13131414const EMPTY_PLUGIN_SCHEMA = { type: "object", additionalProperties: false, properties: {} };
1515const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
16+const originalConfigPath = process.env.OPENCLAW_CONFIG_PATH;
17+let tempDirs: string[] = [];
16181719function writeTempPlugin(params: { dir: string; id: string; body: string }): string {
1820const pluginDir = path.join(params.dir, params.id);
@@ -111,6 +113,15 @@ afterEach(() => {
111113} else {
112114process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir;
113115}
116+if (originalConfigPath === undefined) {
117+delete process.env.OPENCLAW_CONFIG_PATH;
118+} else {
119+process.env.OPENCLAW_CONFIG_PATH = originalConfigPath;
120+}
121+for (const dir of tempDirs) {
122+fs.rmSync(dir, { force: true, recursive: true });
123+}
124+tempDirs = [];
114125});
115126116127describe("tool_result_persist hook", () => {
@@ -128,6 +139,156 @@ describe("tool_result_persist hook", () => {
128139expect(toolResult.details.originalDetailsBytesAtLeast).toBeGreaterThan(8_192);
129140});
130141142+it("redacts small toolResult details before persistence", () => {
143+const tokenValue = "abcdefghijklmnopqrstuvwx1234567890";
144+const bearerValue = "bearerdiagnosticvalue1234567890";
145+const adjacentLongGithubToken = `ghp_${"a".repeat(5_000)}`;
146+const sm = guardSessionManager(SessionManager.inMemory(), {
147+agentId: "main",
148+sessionKey: "main",
149+});
150+const appendMessage = sm.appendMessage.bind(sm) as unknown as (message: AgentMessage) => void;
151+appendMessage({
152+role: "assistant",
153+content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
154+} as AgentMessage);
155+appendMessage({
156+role: "toolResult",
157+toolCallId: "call_1",
158+isError: false,
159+content: [{ type: "text", text: "visible output stays small" }],
160+details: {
161+status: "completed",
162+token: tokenValue,
163+GITHUB_TOKEN: tokenValue,
164+github_token: tokenValue,
165+openai_api_key: tokenValue,
166+card_number: 4242424242424242,
167+cvc: 123,
168+authToken: [tokenValue],
169+aggregated: `GITHUB_TOKEN=${tokenValue}`,
170+adjacentLongGithubToken: `${"x".repeat(1_000)}${adjacentLongGithubToken} z`,
171+nested: {
172+apiKey: { value: bearerValue },
173+stdout: `Authorization: Bearer ${bearerValue}`,
174+items: [`curl --token ${tokenValue} https://example.test`],
175+},
176+},
177+} as any);
178+179+const toolResult = requirePersistedToolResult(sm);
180+const serialized = JSON.stringify(toolResult.details);
181+expect(toolResult.content[0]?.text).toBe("visible output stays small");
182+expect(serialized).toContain("GITHUB_TOKEN=");
183+expect(serialized).toContain("Bearer");
184+expect(serialized).toContain("…");
185+expect(serialized).not.toContain(tokenValue);
186+expect(serialized).not.toContain(bearerValue);
187+expect(serialized).not.toContain(adjacentLongGithubToken);
188+expect(serialized).not.toContain("a".repeat(100));
189+expect(serialized).not.toContain("4242424242424242");
190+});
191+192+it("applies in-memory redaction config to persisted details", () => {
193+const customSecret = "customsecret=abcdef1234567890ghij";
194+const sm = guardSessionManager(SessionManager.inMemory(), {
195+agentId: "main",
196+sessionKey: "main",
197+config: {
198+logging: {
199+redactPatterns: [String.raw`customsecret=([^\s]+)`],
200+},
201+},
202+});
203+const appendMessage = sm.appendMessage.bind(sm) as unknown as (message: AgentMessage) => void;
204+appendMessage({
205+role: "assistant",
206+content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
207+} as AgentMessage);
208+appendMessage({
209+role: "toolResult",
210+toolCallId: "call_1",
211+isError: false,
212+content: [{ type: "text", text: customSecret }],
213+details: {
214+diagnostic: customSecret,
215+},
216+} as any);
217+218+const toolResult = requirePersistedToolResult(sm);
219+const serialized = JSON.stringify(toolResult);
220+expect(serialized).toContain("customsecret=abcdef…ghij");
221+expect(serialized).not.toContain(customSecret);
222+});
223+224+it("keeps sensitive parent keys when custom value patterns match the key probe", () => {
225+const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-redact-config-"));
226+tempDirs.push(tempDir);
227+process.env.OPENCLAW_CONFIG_PATH = path.join(tempDir, "openclaw.json");
228+fs.writeFileSync(
229+process.env.OPENCLAW_CONFIG_PATH,
230+JSON.stringify({ logging: { redactPatterns: ["/[a-z0-9]{30,}/g"] } }),
231+"utf-8",
232+);
233+const sm = guardSessionManager(SessionManager.inMemory(), {
234+agentId: "main",
235+sessionKey: "main",
236+});
237+const appendMessage = sm.appendMessage.bind(sm) as unknown as (message: AgentMessage) => void;
238+appendMessage({
239+role: "assistant",
240+content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
241+} as AgentMessage);
242+appendMessage({
243+role: "toolResult",
244+toolCallId: "call_1",
245+isError: false,
246+content: [{ type: "text", text: "visible output stays small" }],
247+details: {
248+token: { value: "shortsecret" },
249+},
250+} as any);
251+252+const toolResult = requirePersistedToolResult(sm);
253+const serialized = JSON.stringify(toolResult.details);
254+expect(serialized).toContain("***");
255+expect(serialized).not.toContain("shortsecret");
256+});
257+258+it("redacts secret-bearing keys and too-deep detail branches before persistence", () => {
259+const tokenValue = "abcdefghijklmnopqrstuvwx1234567890";
260+const sm = guardSessionManager(SessionManager.inMemory(), {
261+agentId: "main",
262+sessionKey: "main",
263+});
264+let deepDetails: Record<string, unknown> = { token: tokenValue };
265+for (let index = 0; index < 10; index += 1) {
266+deepDetails = { child: deepDetails };
267+}
268+const appendMessage = sm.appendMessage.bind(sm) as unknown as (message: AgentMessage) => void;
269+appendMessage({
270+role: "assistant",
271+content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
272+} as AgentMessage);
273+appendMessage({
274+role: "toolResult",
275+toolCallId: "call_1",
276+isError: false,
277+content: [{ type: "text", text: "visible output stays small" }],
278+details: {
279+[`https://example.test/callback?token=${tokenValue}`]: "ok",
280+ deepDetails,
281+},
282+} as any);
283+284+const toolResult = requirePersistedToolResult(sm);
285+const serialized = JSON.stringify(toolResult.details);
286+expect(serialized).toContain("token=");
287+expect(serialized).toContain("***");
288+expect(serialized).toContain("max depth exceeded");
289+expect(serialized).not.toContain(tokenValue);
290+});
291+131292it("caps oversized toolResult details before persistence", () => {
132293const sm = guardSessionManager(SessionManager.inMemory(), {
133294agentId: "main",
@@ -165,6 +326,165 @@ describe("tool_result_persist hook", () => {
165326expectPersistedToolResultDetailsCapped(sm);
166327});
167328329+it("redacts summarized oversized toolResult details before persistence", () => {
330+const tokenValue = "abcdefghijklmnopqrstuvwx1234567890";
331+const boundaryGhToken = "ghp_1234567890abcdefghij1234567890abcdef";
332+const leadingTailToken = "a".repeat(5_000);
333+const omittedTailToken = "b".repeat(5_000);
334+const sm = guardSessionManager(SessionManager.inMemory(), {
335+agentId: "main",
336+sessionKey: "main",
337+});
338+const appendMessage = sm.appendMessage.bind(sm) as unknown as (message: AgentMessage) => void;
339+appendMessage({
340+role: "assistant",
341+content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
342+} as AgentMessage);
343+appendMessage({
344+role: "toolResult",
345+toolCallId: "call_1",
346+isError: false,
347+content: [{ type: "text", text: "visible output stays small" }],
348+details: {
349+status: { state: "completed", token: tokenValue },
350+sessionId: "exec-1",
351+[`https://example.test/callback?token=${tokenValue}`]: "ok",
352+aggregated: "x".repeat(120_000),
353+tail: `GITHUB_TOKEN=${tokenValue} ${"x".repeat(
354+ 1_940,
355+ )} ${boundaryGhToken} GITHUB_TOKEN=${leadingTailToken} {"token":"${omittedTailToken}"}`,
356+sessions: [
357+{
358+sessionId: "proc-1",
359+status: { state: "completed", token: tokenValue },
360+command: `${"x".repeat(490)} --token ${tokenValue} ${"y".repeat(6_000)}`,
361+aggregated: "a".repeat(80_000),
362+tail: "z".repeat(8_000),
363+},
364+],
365+},
366+} as any);
367+368+const toolResult = requirePersistedToolResult(sm);
369+const serialized = JSON.stringify(toolResult.details);
370+expect(toolResult.content[0]?.text).toBe("visible output stays small");
371+expect(toolResult.details.persistedDetailsTruncated).toBe(true);
372+expect(serialized).toContain("token=***");
373+expect(serialized).toContain("partial secret span omitted");
374+expect(serialized).toContain("boundary overlap omitted");
375+expect(serialized).not.toContain(tokenValue);
376+expect(serialized).not.toContain(boundaryGhToken.slice(0, 12));
377+expect(serialized).not.toContain("a".repeat(100));
378+expect(serialized).not.toContain("b".repeat(100));
379+});
380+381+it("redacts retained structured fields in fallback oversized details summaries", () => {
382+const tokenValue = "fallback-token-abcdefghijklmnopqrstuv";
383+const sm = guardSessionManager(SessionManager.inMemory(), {
384+agentId: "main",
385+sessionKey: "main",
386+});
387+const appendMessage = sm.appendMessage.bind(sm) as unknown as (message: AgentMessage) => void;
388+appendMessage({
389+role: "assistant",
390+content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
391+} as AgentMessage);
392+appendMessage({
393+role: "toolResult",
394+toolCallId: "call_1",
395+isError: false,
396+content: [{ type: "text", text: "visible output stays small" }],
397+details: {
398+status: { state: "completed", token: tokenValue },
399+sessionId: "exec-1",
400+cwd: "/tmp/".concat("workspace/".repeat(400)),
401+name: "oversized fallback command ".repeat(200),
402+fullOutputPath: "/tmp/".concat("output/".repeat(400)),
403+aggregated: "x".repeat(120_000),
404+tail: "tail ".repeat(800),
405+sessions: Array.from({ length: 10 }, (_, i) => ({
406+sessionId: `proc-${i}`,
407+status: "completed",
408+command: `node script-${i}.js ${"x".repeat(6_000)}`,
409+})),
410+},
411+} as any);
412+413+const toolResult = requirePersistedToolResult(sm);
414+const details = toolResult.details;
415+const serialized = JSON.stringify(details);
416+expect(details.persistedDetailsTruncated).toBe(true);
417+expect(details.finalDetailsTruncated).toBe(true);
418+expect(details.status?.token).toBe("***");
419+expect(serialized).not.toContain(tokenValue);
420+});
421+422+it("does not persist lookahead text after redaction shrinks an oversized detail prefix", () => {
423+const tokenValue = "abcdefghijklmnopqrstuvwx1234567890";
424+const postBoundarySecret = "UNREDACTED_AFTER_LIMIT_SECRET";
425+const shrinkPrefix = `${Array.from({ length: 20 }, () => `GITHUB_TOKEN=${tokenValue}`).join(
426+ " ",
427+ )} `;
428+const tail = `${shrinkPrefix}${"x".repeat(
429+ 2_300 - shrinkPrefix.length,
430+ )}${postBoundarySecret}${"z".repeat(5_000)}`;
431+const sm = guardSessionManager(SessionManager.inMemory(), {
432+agentId: "main",
433+sessionKey: "main",
434+});
435+const appendMessage = sm.appendMessage.bind(sm) as unknown as (message: AgentMessage) => void;
436+appendMessage({
437+role: "assistant",
438+content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
439+} as AgentMessage);
440+appendMessage({
441+role: "toolResult",
442+toolCallId: "call_1",
443+isError: false,
444+content: [{ type: "text", text: "visible output stays small" }],
445+details: {
446+status: "completed",
447+aggregated: "x".repeat(120_000),
448+ tail,
449+},
450+} as any);
451+452+const toolResult = requirePersistedToolResult(sm);
453+const serialized = JSON.stringify(toolResult.details);
454+expect(toolResult.details.persistedDetailsTruncated).toBe(true);
455+expect(serialized).toContain("partial secret span omitted");
456+expect(serialized).not.toContain(tokenValue);
457+expect(serialized).not.toContain(postBoundarySecret);
458+});
459+460+it("fails closed for partially scanned oversized structured secret values", () => {
461+const longSecret = "r".repeat(10_000);
462+const sm = guardSessionManager(SessionManager.inMemory(), {
463+agentId: "main",
464+sessionKey: "main",
465+});
466+const appendMessage = sm.appendMessage.bind(sm) as unknown as (message: AgentMessage) => void;
467+appendMessage({
468+role: "assistant",
469+content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
470+} as AgentMessage);
471+appendMessage({
472+role: "toolResult",
473+toolCallId: "call_1",
474+isError: false,
475+content: [{ type: "text", text: "visible output stays small" }],
476+details: {
477+status: "completed",
478+tail: `${"x".repeat(1_000)}{"token":"${longSecret}${"z".repeat(1_000)}`,
479+},
480+} as any);
481+482+const toolResult = requirePersistedToolResult(sm);
483+const serialized = JSON.stringify(toolResult.details);
484+expect(serialized).toContain("partial secret span omitted");
485+expect(serialized).not.toContain("r".repeat(100));
486+});
487+168488it("caps oversized toolResult details without serializing the original payload", () => {
169489const sm = guardSessionManager(SessionManager.inMemory(), {
170490agentId: "main",
@@ -419,6 +739,28 @@ describe("tool_result_persist hook", () => {
419739appendToolCallAndResult(sm);
420740expectPersistedToolResultDetailsCapped(sm);
421741});
742+743+it("reapplies the details cap after redaction expands hook details", () => {
744+const deepItems = Array.from({ length: 2_000 }, () => ({}));
745+const hookDetails = { a: { b: { c: { d: { e: { f: { g: deepItems } } } } } } };
746+initializeTempPlugin({
747+tmpPrefix: "openclaw-toolpersist-details-redaction-expand-",
748+id: "persist-details-redaction-expand",
749+body: `export default { id: "persist-details-redaction-expand", register(api) {
750+ api.on("tool_result_persist", (event) => {
751+ return { message: { ...event.message, details: ${JSON.stringify(hookDetails)} } };
752+ }, { priority: 10 });
753+} };`,
754+});
755+756+const sm = guardSessionManager(SessionManager.inMemory(), {
757+agentId: "main",
758+sessionKey: "main",
759+});
760+761+appendToolCallAndResult(sm);
762+expectPersistedToolResultDetailsCapped(sm);
763+});
422764});
423765424766describe("before_message_write hook", () => {
@@ -479,4 +821,4 @@ describe("before_message_write hook", () => {
479821appendToolCallAndResult(sm);
480822expectPersistedToolResultTextCapped(sm);
481823});
482-});
824+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。