fix(gateway): ingest managed image file URLs (#103540)

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Harjoth Khara
2026-07-28 21:17:22 -07:00
committed by GitHub
parent e59e7b82cf
commit 6f4f2a782e
2 changed files with 54 additions and 30 deletions
+45 -27
View File
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
import http from "node:http";
import type { AddressInfo } from "node:net";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
createNoisyPngBuffer as createNoisyPngFixtureBuffer,
@@ -779,35 +780,52 @@ describe("createManagedOutgoingImageBlocks", () => {
).rejects.toThrow("Invalid image data URL");
});
it("rewrites local image sources into managed display blocks without leaking the source path", async () => {
const sourcePath = path.join(stateDir, "workspace", "fixtures", "dot.png");
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
await fs.writeFile(sourcePath, Buffer.from(TINY_PNG_BASE64, "base64"));
it.each([
{ name: "local paths", sourceForPath: (sourcePath: string) => sourcePath },
{
name: "file URLs",
sourceForPath: (sourcePath: string) => {
const sourceUrl = pathToFileURL(sourcePath);
sourceUrl.searchParams.set("sig", "secret");
sourceUrl.hash = "preview";
return sourceUrl.href;
},
},
])(
"rewrites $name into managed display blocks without leaking the source path",
async ({ sourceForPath }) => {
const sourcePath = path.join(stateDir, "workspace", "fixtures", "dot.png");
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
await fs.writeFile(sourcePath, Buffer.from(TINY_PNG_BASE64, "base64"));
await withEnvAsync({ OPENCLAW_STATE_DIR: stateDir }, async () => {
const blocks = await createManagedOutgoingImageBlocks({
stateDir,
sessionKey: "agent:main:main",
mediaUrls: [sourcePath],
localRoots: [path.join(stateDir, "workspace")],
await withEnvAsync({ OPENCLAW_STATE_DIR: stateDir }, async () => {
const blocks = await createManagedOutgoingImageBlocks({
stateDir,
sessionKey: "agent:main:main",
mediaUrls: [sourceForPath(sourcePath)],
localRoots: [path.join(stateDir, "workspace")],
});
expect(blocks).toHaveLength(1);
const block = requireBlock(blocks);
expect(block.type).toBe("image");
expect(block.url).toContain("/api/chat/media/outgoing/agent%3Amain%3Amain/");
expect(block.openUrl).toContain("/full");
expect(block.url).toBe(block.openUrl);
expect(block.alt).toBe("dot.png");
expect(JSON.stringify(block)).not.toContain(sourcePath);
expect(JSON.stringify(block)).not.toContain("sig=secret");
expect(JSON.stringify(block)).not.toContain("preview");
const attachmentId = requireAttachmentIdFromUrl(block.url);
const record = readManagedImageRecord(attachmentId, stateDir);
const originalPath = requireManagedOriginalPath(stateDir, attachmentId);
expect(record?.original.filename).toMatch(/\.png$/);
expect(originalPath).not.toBe(sourcePath);
expect(originalPath).toContain(path.join(stateDir, "media", "outgoing", "originals"));
});
expect(blocks).toHaveLength(1);
const block = requireBlock(blocks);
expect(block.type).toBe("image");
expect(block.url).toContain("/api/chat/media/outgoing/agent%3Amain%3Amain/");
expect(block.openUrl).toContain("/full");
expect(block.url).toBe(block.openUrl);
expect(JSON.stringify(block)).not.toContain(sourcePath);
const attachmentId = requireAttachmentIdFromUrl(block.url);
const record = readManagedImageRecord(attachmentId, stateDir);
const originalPath = requireManagedOriginalPath(stateDir, attachmentId);
expect(record?.original.filename).toMatch(/\.png$/);
expect(originalPath).not.toBe(sourcePath);
expect(originalPath).toContain(path.join(stateDir, "media", "outgoing", "originals"));
});
});
},
);
it("ingests external image URLs into managed storage instead of hotlinking them", async () => {
const imageBuffer = Buffer.from(TINY_PNG_BASE64, "base64");
+9 -3
View File
@@ -1026,8 +1026,12 @@ export async function createManagedOutgoingImageBlocks(params: {
for (const [index, mediaUrl] of mediaUrls.entries()) {
const fallbackAlt = `Generated image ${index + 1}`;
const parsedDataUrl = parseImageDataUrl(mediaUrl, fallbackAlt, limits);
const localMediaPath =
parsedDataUrl.kind === "image-data-url" ? undefined : resolveLocalMediaPath(mediaUrl);
const alt =
parsedDataUrl.kind === "image-data-url" ? fallbackAlt : deriveAltText(mediaUrl, index);
parsedDataUrl.kind === "image-data-url"
? fallbackAlt
: deriveAltText(localMediaPath ?? mediaUrl, index);
if (parsedDataUrl.kind === "non-image-data-url") {
continue;
}
@@ -1048,7 +1052,6 @@ export async function createManagedOutgoingImageBlocks(params: {
`generated-image-${index + 1}`,
)
: await (async () => {
const localMediaPath = resolveLocalMediaPath(mediaUrl);
if (localMediaPath) {
const localRoots = params.localRoots;
const localMediaOptions =
@@ -1062,8 +1065,11 @@ export async function createManagedOutgoingImageBlocks(params: {
};
await assertLocalMediaAllowed(localMediaPath, localRoots, localMediaOptions);
}
// File URLs have already been normalized for display metadata and policy checks.
// Pass that path to the store instead of treating URI syntax as a filename.
const ingestSource = localMediaPath ?? mediaUrl;
return await saveMediaSource(
mediaUrl,
ingestSource,
undefined,
"outgoing/originals",
Math.max(limits.maxBytes, MEDIA_MAX_BYTES),