diff --git a/src/gateway/openresponses-http.test.ts b/src/gateway/openresponses-http.test.ts index ac8bf0efb31..3f6cb43917d 100644 --- a/src/gateway/openresponses-http.test.ts +++ b/src/gateway/openresponses-http.test.ts @@ -90,6 +90,67 @@ async function ensureResponseConsumed(res: Response) { } } +const WEATHER_TOOL = [ + { + type: "function", + function: { name: "get_weather", description: "Get weather" }, + }, +] as const; + +function buildUrlInputMessage(params: { + kind: "input_file" | "input_image"; + url: string; + text?: string; +}) { + return [ + { + type: "message", + role: "user", + content: [ + { type: "input_text", text: params.text ?? "read this" }, + { + type: params.kind, + source: { type: "url", url: params.url }, + }, + ], + }, + ]; +} + +function buildResponsesUrlPolicyConfig(maxUrlParts: number) { + return { + gateway: { + http: { + endpoints: { + responses: { + enabled: true, + maxUrlParts, + files: { + allowUrl: true, + urlAllowlist: ["cdn.example.com", "*.assets.example.com"], + }, + images: { + allowUrl: true, + urlAllowlist: ["images.example.com"], + }, + }, + }, + }, + }, + }; +} + +async function expectInvalidRequest( + res: Response, + messagePattern: RegExp, +): Promise<{ type?: string; message?: string } | undefined> { + expect(res.status).toBe(400); + const json = (await res.json()) as { error?: { type?: string; message?: string } }; + expect(json.error?.type).toBe("invalid_request_error"); + expect(json.error?.message ?? "").toMatch(messagePattern); + return json.error; +} + describe("OpenResponses HTTP API (e2e)", () => { it("rejects when disabled (default + config)", { timeout: 15_000 }, async () => { const port = await getFreePort(); @@ -324,12 +385,7 @@ describe("OpenResponses HTTP API (e2e)", () => { const resToolNone = await postResponses(port, { model: "openclaw", input: "hi", - tools: [ - { - type: "function", - function: { name: "get_weather", description: "Get weather" }, - }, - ], + tools: WEATHER_TOOL, tool_choice: "none", }); expect(resToolNone.status).toBe(200); @@ -367,12 +423,7 @@ describe("OpenResponses HTTP API (e2e)", () => { const resUnknownTool = await postResponses(port, { model: "openclaw", input: "hi", - tools: [ - { - type: "function", - function: { name: "get_weather", description: "Get weather" }, - }, - ], + tools: WEATHER_TOOL, tool_choice: { type: "function", function: { name: "unknown_tool" } }, }); expect(resUnknownTool.status).toBe(400); @@ -536,100 +587,35 @@ describe("OpenResponses HTTP API (e2e)", () => { const blockedPrivate = await postResponses(port, { model: "openclaw", - input: [ - { - type: "message", - role: "user", - content: [ - { type: "input_text", text: "read this" }, - { - type: "input_file", - source: { type: "url", url: "http://127.0.0.1:6379/info" }, - }, - ], - }, - ], + input: buildUrlInputMessage({ + kind: "input_file", + url: "http://127.0.0.1:6379/info", + }), }); - expect(blockedPrivate.status).toBe(400); - const blockedPrivateJson = (await blockedPrivate.json()) as { - error?: { type?: string; message?: string }; - }; - expect(blockedPrivateJson.error?.type).toBe("invalid_request_error"); - expect(blockedPrivateJson.error?.message ?? "").toMatch( - /invalid request|private|internal|blocked/i, - ); + await expectInvalidRequest(blockedPrivate, /invalid request|private|internal|blocked/i); const blockedMetadata = await postResponses(port, { model: "openclaw", - input: [ - { - type: "message", - role: "user", - content: [ - { type: "input_text", text: "read this" }, - { - type: "input_image", - source: { type: "url", url: "http://metadata.google.internal/computeMetadata/v1" }, - }, - ], - }, - ], + input: buildUrlInputMessage({ + kind: "input_image", + url: "http://metadata.google.internal/computeMetadata/v1", + }), }); - expect(blockedMetadata.status).toBe(400); - const blockedMetadataJson = (await blockedMetadata.json()) as { - error?: { type?: string; message?: string }; - }; - expect(blockedMetadataJson.error?.type).toBe("invalid_request_error"); - expect(blockedMetadataJson.error?.message ?? "").toMatch( - /invalid request|blocked|metadata|internal/i, - ); + await expectInvalidRequest(blockedMetadata, /invalid request|blocked|metadata|internal/i); const blockedScheme = await postResponses(port, { model: "openclaw", - input: [ - { - type: "message", - role: "user", - content: [ - { type: "input_text", text: "read this" }, - { - type: "input_file", - source: { type: "url", url: "file:///etc/passwd" }, - }, - ], - }, - ], + input: buildUrlInputMessage({ + kind: "input_file", + url: "file:///etc/passwd", + }), }); - expect(blockedScheme.status).toBe(400); - const blockedSchemeJson = (await blockedScheme.json()) as { - error?: { type?: string; message?: string }; - }; - expect(blockedSchemeJson.error?.type).toBe("invalid_request_error"); - expect(blockedSchemeJson.error?.message ?? "").toMatch(/invalid request|http or https/i); + await expectInvalidRequest(blockedScheme, /invalid request|http or https/i); expect(agentCommand).not.toHaveBeenCalled(); }); it("enforces URL allowlist and URL part cap for responses inputs", async () => { - const allowlistConfig = { - gateway: { - http: { - endpoints: { - responses: { - enabled: true, - maxUrlParts: 1, - files: { - allowUrl: true, - urlAllowlist: ["cdn.example.com", "*.assets.example.com"], - }, - images: { - allowUrl: true, - urlAllowlist: ["images.example.com"], - }, - }, - }, - }, - }, - }; + const allowlistConfig = buildResponsesUrlPolicyConfig(1); await writeGatewayConfig(allowlistConfig); const allowlistPort = await getFreePort(); @@ -639,52 +625,18 @@ describe("OpenResponses HTTP API (e2e)", () => { const allowlistBlocked = await postResponses(allowlistPort, { model: "openclaw", - input: [ - { - type: "message", - role: "user", - content: [ - { type: "input_text", text: "fetch this" }, - { - type: "input_file", - source: { type: "url", url: "https://evil.example.org/secret.txt" }, - }, - ], - }, - ], + input: buildUrlInputMessage({ + kind: "input_file", + text: "fetch this", + url: "https://evil.example.org/secret.txt", + }), }); - expect(allowlistBlocked.status).toBe(400); - const allowlistBlockedJson = (await allowlistBlocked.json()) as { - error?: { type?: string; message?: string }; - }; - expect(allowlistBlockedJson.error?.type).toBe("invalid_request_error"); - expect(allowlistBlockedJson.error?.message ?? "").toMatch( - /invalid request|allowlist|blocked/i, - ); + await expectInvalidRequest(allowlistBlocked, /invalid request|allowlist|blocked/i); } finally { await allowlistServer.close({ reason: "responses allowlist hardening test done" }); } - const capConfig = { - gateway: { - http: { - endpoints: { - responses: { - enabled: true, - maxUrlParts: 0, - files: { - allowUrl: true, - urlAllowlist: ["cdn.example.com", "*.assets.example.com"], - }, - images: { - allowUrl: true, - urlAllowlist: ["images.example.com"], - }, - }, - }, - }, - }, - }; + const capConfig = buildResponsesUrlPolicyConfig(0); await writeGatewayConfig(capConfig); const capPort = await getFreePort(); @@ -693,26 +645,14 @@ describe("OpenResponses HTTP API (e2e)", () => { agentCommand.mockClear(); const maxUrlBlocked = await postResponses(capPort, { model: "openclaw", - input: [ - { - type: "message", - role: "user", - content: [ - { type: "input_text", text: "fetch this" }, - { - type: "input_file", - source: { type: "url", url: "https://cdn.example.com/file-1.txt" }, - }, - ], - }, - ], + input: buildUrlInputMessage({ + kind: "input_file", + text: "fetch this", + url: "https://cdn.example.com/file-1.txt", + }), }); - expect(maxUrlBlocked.status).toBe(400); - const maxUrlBlockedJson = (await maxUrlBlocked.json()) as { - error?: { type?: string; message?: string }; - }; - expect(maxUrlBlockedJson.error?.type).toBe("invalid_request_error"); - expect(maxUrlBlockedJson.error?.message ?? "").toMatch( + await expectInvalidRequest( + maxUrlBlocked, /invalid request|Too many URL-based input sources/i, ); expect(agentCommand).not.toHaveBeenCalled();