fix(security): OC-07 redact session history credentials and enforce webhook secret (#16928)

* Security: refresh sessions history redaction patch

* tests: align sessions_history redaction-only truncation expectation

* Changelog: credit sessions history security hardening

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Aether AI
2026-02-23 10:29:40 +11:00
committed by GitHub
parent 3efe63d1ad
commit d306fc8ef1
3 changed files with 120 additions and 11 deletions

View File

@@ -247,11 +247,13 @@ describe("sessions tools", () => {
truncated?: boolean;
droppedMessages?: boolean;
contentTruncated?: boolean;
contentRedacted?: boolean;
bytes?: number;
};
expect(details.truncated).toBe(true);
expect(details.droppedMessages).toBe(true);
expect(details.contentTruncated).toBe(true);
expect(details.contentRedacted).toBe(false);
expect(typeof details.bytes).toBe("number");
expect((details.bytes ?? 0) <= 80 * 1024).toBe(true);
expect(details.messages && details.messages.length > 0).toBe(true);
@@ -309,11 +311,13 @@ describe("sessions tools", () => {
truncated?: boolean;
droppedMessages?: boolean;
contentTruncated?: boolean;
contentRedacted?: boolean;
bytes?: number;
};
expect(details.truncated).toBe(true);
expect(details.droppedMessages).toBe(true);
expect(details.contentTruncated).toBe(false);
expect(details.contentRedacted).toBe(false);
expect(typeof details.bytes).toBe("number");
expect((details.bytes ?? 0) <= 80 * 1024).toBe(true);
expect(details.messages).toHaveLength(1);
@@ -322,6 +326,83 @@ describe("sessions tools", () => {
);
});
it("sessions_history sets contentRedacted when sensitive data is redacted", async () => {
callGatewayMock.mockReset();
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string };
if (request.method === "chat.history") {
return {
messages: [
{
role: "assistant",
content: [
{ type: "text", text: "Use sk-1234567890abcdef1234 to authenticate with the API." },
],
},
],
};
}
return {};
});
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_history");
expect(tool).toBeDefined();
if (!tool) {
throw new Error("missing sessions_history tool");
}
const result = await tool.execute("call-redact-1", { sessionKey: "main" });
const details = result.details as {
messages?: Array<Record<string, unknown>>;
truncated?: boolean;
contentTruncated?: boolean;
contentRedacted?: boolean;
};
expect(details.contentRedacted).toBe(true);
expect(details.contentTruncated).toBe(false);
expect(details.truncated).toBe(false);
const msg = details.messages?.[0] as { content?: Array<{ type?: string; text?: string }> };
const textBlock = msg?.content?.find((b) => b.type === "text");
expect(typeof textBlock?.text).toBe("string");
expect(textBlock?.text).not.toContain("sk-1234567890abcdef1234");
});
it("sessions_history sets both contentRedacted and contentTruncated independently", async () => {
callGatewayMock.mockReset();
const longPrefix = "safe text ".repeat(420);
const sensitiveText = `${longPrefix} sk-9876543210fedcba9876 end`;
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string };
if (request.method === "chat.history") {
return {
messages: [
{
role: "assistant",
content: [{ type: "text", text: sensitiveText }],
},
],
};
}
return {};
});
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_history");
expect(tool).toBeDefined();
if (!tool) {
throw new Error("missing sessions_history tool");
}
const result = await tool.execute("call-redact-2", { sessionKey: "main" });
const details = result.details as {
truncated?: boolean;
contentTruncated?: boolean;
contentRedacted?: boolean;
};
expect(details.contentRedacted).toBe(true);
expect(details.contentTruncated).toBe(true);
expect(details.truncated).toBe(true);
});
it("sessions_history resolves sessionId inputs", async () => {
const sessionId = "sess-group";
const targetKey = "agent:main:discord:channel:1457165743010611293";