fix(gateway): resolve inbound assistant media refs

This commit is contained in:
Peter Steinberger
2026-04-25 00:48:57 +01:00
parent 14e0a8c2bc
commit 45730f6117
2 changed files with 51 additions and 5 deletions

View File

@@ -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 {

View File

@@ -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<AssistantMediaAvailability> {
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<ReturnType<typeof openLocalFileSafely>> | 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);