From 45730f61176dd22a2c6d236bed9fcf0a05a53b97 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 00:48:57 +0100 Subject: [PATCH] fix(gateway): resolve inbound assistant media refs --- src/gateway/control-ui.http.test.ts | 42 +++++++++++++++++++++++++++++ src/gateway/control-ui.ts | 14 ++++++---- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/gateway/control-ui.http.test.ts b/src/gateway/control-ui.http.test.ts index 5bbd430ee2d..8aee0c18e02 100644 --- a/src/gateway/control-ui.http.test.ts +++ b/src/gateway/control-ui.http.test.ts @@ -4,6 +4,7 @@ import type { IncomingMessage } from "node:http"; import os from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; +import { resolveStateDir } from "../config/paths.js"; import { approveDevicePairing, requestDevicePairing } from "../infra/device-pairing.js"; import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; import type { ResolvedGatewayAuth } from "./auth.js"; @@ -331,6 +332,47 @@ describe("handleControlUiHttpRequest", () => { }); }); + it("serves assistant media from canonical inbound media refs", async () => { + const stateDir = resolveStateDir(); + const id = `ui-media-ref-${Date.now()}-${Math.random().toString(36).slice(2)}.png`; + const filePath = path.join(stateDir, "media", "inbound", id); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, Buffer.from("not-a-real-png")); + + try { + const { res, handled } = await runAssistantMediaRequest({ + url: `/__openclaw__/assistant-media?source=${encodeURIComponent(`media://inbound/${id}`)}&token=test-token`, + method: "GET", + auth: { mode: "token", token: "test-token", allowTailscale: false }, + }); + expect(handled).toBe(true); + expect(res.statusCode).toBe(200); + } finally { + await fs.rm(filePath, { force: true }); + } + }); + + it("reports assistant media metadata for canonical inbound media refs", async () => { + const stateDir = resolveStateDir(); + const id = `ui-media-ref-meta-${Date.now()}-${Math.random().toString(36).slice(2)}.png`; + const filePath = path.join(stateDir, "media", "inbound", id); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, Buffer.from("not-a-real-png")); + + try { + const { res, handled, end } = await runAssistantMediaRequest({ + url: `/__openclaw__/assistant-media?meta=1&source=${encodeURIComponent(`media://inbound/${id}`)}&token=test-token`, + method: "GET", + auth: { mode: "token", token: "test-token", allowTailscale: false }, + }); + expect(handled).toBe(true); + expect(res.statusCode).toBe(200); + expect(JSON.parse(String(end.mock.calls[0]?.[0] ?? ""))).toEqual({ available: true }); + } finally { + await fs.rm(filePath, { force: true }); + } + }); + it("rejects assistant local media outside allowed preview roots", async () => { const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ui-media-blocked-")); try { diff --git a/src/gateway/control-ui.ts b/src/gateway/control-ui.ts index 435563b5237..888d0b682bb 100644 --- a/src/gateway/control-ui.ts +++ b/src/gateway/control-ui.ts @@ -15,6 +15,7 @@ import { isWithinDir } from "../infra/path-safety.js"; import { openVerifiedFileSync } from "../infra/safe-open-sync.js"; import { assertLocalMediaAllowed, getDefaultLocalRoots } from "../media/local-media-access.js"; import { getAgentScopedMediaLocalRoots } from "../media/local-roots.js"; +import { resolveMediaReferenceLocalPath } from "../media/media-reference.js"; import { detectMime } from "../media/mime.js"; import { AVATAR_MAX_BYTES } from "../shared/avatar-policy.js"; import { resolveUserPath } from "../utils.js"; @@ -401,8 +402,9 @@ async function resolveAssistantMediaAvailability( localRoots: readonly string[], ): Promise { try { - await assertLocalMediaAllowed(source, localRoots); - const opened = await openLocalFileSafely({ filePath: source }); + const localPath = await resolveMediaReferenceLocalPath(source); + await assertLocalMediaAllowed(localPath, localRoots); + const opened = await openLocalFileSafely({ filePath: localPath }); await opened.handle.close(); return { available: true }; } catch (err) { @@ -460,6 +462,7 @@ export async function handleControlUiAssistantMediaRequest( } let opened: Awaited> | null = null; + let localPath = source; let handleClosed = false; const closeOpenedHandle = async () => { if (!opened || handleClosed) { @@ -469,8 +472,9 @@ export async function handleControlUiAssistantMediaRequest( await opened.handle.close().catch(() => {}); }; try { - await assertLocalMediaAllowed(source, localRoots); - opened = await openLocalFileSafely({ filePath: source }); + localPath = await resolveMediaReferenceLocalPath(source); + await assertLocalMediaAllowed(localPath, localRoots); + opened = await openLocalFileSafely({ filePath: localPath }); const sniffLength = Math.min(opened.stat.size, 8192); const sniffBuffer = sniffLength > 0 ? Buffer.allocUnsafe(sniffLength) : undefined; const bytesRead = @@ -479,7 +483,7 @@ export async function handleControlUiAssistantMediaRequest( : 0; const mime = await detectMime({ buffer: sniffBuffer?.subarray(0, bytesRead), - filePath: source, + filePath: localPath, }); if (mime) { res.setHeader("Content-Type", mime);