mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 07:38:12 +00:00
fix(agents): sanitize oversized middleware inputs
This commit is contained in:
@@ -174,6 +174,54 @@ describe("createAgentToolResultMiddlewareRunner", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("truncates oversized incoming text before a no-op middleware", async () => {
|
||||
let observedText = "";
|
||||
const runner = createAgentToolResultMiddlewareRunner({ runtime: "openclaw" }, [
|
||||
(event) => {
|
||||
const content = event.result.content[0];
|
||||
observedText = content?.type === "text" ? content.text : "";
|
||||
return undefined;
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await runner.applyToolResultMiddleware({
|
||||
toolCallId: "call-1",
|
||||
toolName: "gateway",
|
||||
args: { action: "config.get" },
|
||||
result: {
|
||||
content: [{ type: "text", text: "x".repeat(100_001) }],
|
||||
details: { ok: true },
|
||||
},
|
||||
});
|
||||
|
||||
expect(observedText).toHaveLength(100_000);
|
||||
expect(result.details).toEqual({ ok: true });
|
||||
expect(result.content).toEqual([{ type: "text", text: "x".repeat(100_000) }]);
|
||||
});
|
||||
|
||||
it("fails closed when middleware returns oversized top-level text", async () => {
|
||||
const runner = createAgentToolResultMiddlewareRunner({ runtime: "openclaw" }, [
|
||||
() => ({
|
||||
result: {
|
||||
content: [{ type: "text", text: "x".repeat(100_001) }],
|
||||
details: { ok: true },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const result = await runner.applyToolResultMiddleware({
|
||||
toolCallId: "call-1",
|
||||
toolName: "gateway",
|
||||
args: { action: "config.get" },
|
||||
result: {
|
||||
content: [{ type: "text", text: "raw" }],
|
||||
details: { ok: true },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.details).toEqual({ status: "error", middlewareError: true });
|
||||
});
|
||||
|
||||
it("sanitizes incoming details before failing closed on uncoercible content", async () => {
|
||||
const details: Record<string, unknown> = {
|
||||
ok: true,
|
||||
|
||||
@@ -24,6 +24,10 @@ const NESTED_TOOL_RESULT_BLOCK_TYPES = new Set(["toolresult", "tool_result"]);
|
||||
|
||||
type MiddlewareContentBlock = OpenClawAgentToolResult["content"][number];
|
||||
type MiddlewareContentCoerceState = { depth: number; seen: Set<object> };
|
||||
type MiddlewareToolResultCoerceOptions = {
|
||||
sanitizeContent?: boolean;
|
||||
sanitizeDetails?: boolean;
|
||||
};
|
||||
|
||||
function isValidMiddlewareContentBlock(value: unknown): boolean {
|
||||
if (!isRecord(value) || typeof value.type !== "string") {
|
||||
@@ -158,6 +162,7 @@ function stringifyMiddlewareTextPayload(value: unknown): string | undefined {
|
||||
function coerceMiddlewareText(
|
||||
value: unknown,
|
||||
state: MiddlewareContentCoerceState = createMiddlewareContentCoerceState(),
|
||||
options: MiddlewareToolResultCoerceOptions = {},
|
||||
): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
@@ -173,14 +178,14 @@ function coerceMiddlewareText(
|
||||
return undefined;
|
||||
}
|
||||
for (const key of ["text", "output", "result", "message"]) {
|
||||
const text = coerceMiddlewareText(value[key], nextState);
|
||||
const text = coerceMiddlewareText(value[key], nextState, options);
|
||||
if (text !== undefined) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
const content = value.content;
|
||||
if (Array.isArray(content)) {
|
||||
const chunks = coerceMiddlewareContentArray(content, nextState)
|
||||
const chunks = coerceMiddlewareContentArray(content, nextState, options)
|
||||
.filter(
|
||||
(block): block is Extract<MiddlewareContentBlock, { type: "text" }> =>
|
||||
block.type === "text",
|
||||
@@ -224,6 +229,7 @@ function appendMiddlewareContentBlock(
|
||||
function coerceMiddlewareContentArray(
|
||||
content: unknown[],
|
||||
state: MiddlewareContentCoerceState,
|
||||
options: MiddlewareToolResultCoerceOptions = {},
|
||||
): MiddlewareContentBlock[] {
|
||||
const blocks: MiddlewareContentBlock[] = [];
|
||||
let inspectedBlocks = 0;
|
||||
@@ -235,7 +241,7 @@ function coerceMiddlewareContentArray(
|
||||
) {
|
||||
break;
|
||||
}
|
||||
const coercedBlocks = coerceMiddlewareContentBlocks(entry, state);
|
||||
const coercedBlocks = coerceMiddlewareContentBlocks(entry, state, options);
|
||||
if (coercedBlocks.length > 0) {
|
||||
for (const block of coercedBlocks) {
|
||||
appendMiddlewareContentBlock(blocks, block);
|
||||
@@ -245,7 +251,7 @@ function coerceMiddlewareContentArray(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const text = coerceMiddlewareText(entry, state);
|
||||
const text = coerceMiddlewareText(entry, state, options);
|
||||
if (text) {
|
||||
appendMiddlewareContentBlock(blocks, {
|
||||
type: "text",
|
||||
@@ -259,10 +265,22 @@ function coerceMiddlewareContentArray(
|
||||
function coerceMiddlewareContentBlocks(
|
||||
value: unknown,
|
||||
state: MiddlewareContentCoerceState = createMiddlewareContentCoerceState(),
|
||||
options: MiddlewareToolResultCoerceOptions = {},
|
||||
): MiddlewareContentBlock[] {
|
||||
if (isValidMiddlewareContentBlock(value)) {
|
||||
return [value as MiddlewareContentBlock];
|
||||
}
|
||||
// Tool emitters can produce legitimate transcript text larger than the
|
||||
// middleware cap. Normalize that only before the first handler; handlers
|
||||
// remain fail-closed if they return an oversized replacement.
|
||||
if (
|
||||
options.sanitizeContent === true &&
|
||||
isRecord(value) &&
|
||||
value.type === "text" &&
|
||||
typeof value.text === "string"
|
||||
) {
|
||||
return [{ type: "text", text: truncateUtf16Safe(value.text, MAX_MIDDLEWARE_TEXT_CHARS) }];
|
||||
}
|
||||
if (!isRecord(value) || typeof value.type !== "string") {
|
||||
return [];
|
||||
}
|
||||
@@ -273,9 +291,10 @@ function coerceMiddlewareContentBlocks(
|
||||
const content = value.content;
|
||||
if (Array.isArray(content) && content.length > 0) {
|
||||
const nextState = descendMiddlewareContentCoerceState(value, state);
|
||||
return nextState ? coerceMiddlewareContentArray(content, nextState) : [];
|
||||
return nextState ? coerceMiddlewareContentArray(content, nextState, options) : [];
|
||||
}
|
||||
const text = coerceMiddlewareText(content, state) ?? coerceMiddlewareText(value, state);
|
||||
const text =
|
||||
coerceMiddlewareText(content, state, options) ?? coerceMiddlewareText(value, state, options);
|
||||
if (!text) {
|
||||
return [];
|
||||
}
|
||||
@@ -289,7 +308,7 @@ function coerceMiddlewareContentBlocks(
|
||||
|
||||
function coerceMiddlewareToolResult(
|
||||
value: unknown,
|
||||
options: { sanitizeDetails?: boolean } = {},
|
||||
options: MiddlewareToolResultCoerceOptions = {},
|
||||
): OpenClawAgentToolResult | undefined {
|
||||
if (isValidMiddlewareToolResult(value)) {
|
||||
return value;
|
||||
@@ -305,7 +324,7 @@ function coerceMiddlewareToolResult(
|
||||
if (inspectedBlocks > MAX_MIDDLEWARE_CONTENT_BLOCKS) {
|
||||
break;
|
||||
}
|
||||
for (const coerced of coerceMiddlewareContentBlocks(block, state)) {
|
||||
for (const coerced of coerceMiddlewareContentBlocks(block, state, options)) {
|
||||
content.push(coerced);
|
||||
if (content.length >= MAX_MIDDLEWARE_CONTENT_BLOCKS) {
|
||||
break;
|
||||
@@ -379,7 +398,10 @@ function sanitizeMiddlewareDetailsValue(value: unknown): unknown {
|
||||
* subsequent middleware-side mutations are still validated strictly.
|
||||
*/
|
||||
function sanitizeToolResultForMiddleware(result: OpenClawAgentToolResult): OpenClawAgentToolResult {
|
||||
const coerced = coerceMiddlewareToolResult(result, { sanitizeDetails: true });
|
||||
const coerced = coerceMiddlewareToolResult(result, {
|
||||
sanitizeContent: true,
|
||||
sanitizeDetails: true,
|
||||
});
|
||||
if (coerced) {
|
||||
return coerced;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user