mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-21 05:32:53 +00:00
fix(media): resolve relative MEDIA paths against agent workspace (#58624)
* fix(media): resolve relative MEDIA: paths against agent workspace dir * fix(agents): remove stale ollama compat import * fix(media): preserve workspace dir in outbound access
This commit is contained in:
@@ -2,7 +2,6 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import {
|
||||
isOllamaCompatProvider,
|
||||
resolveOllamaBaseUrlForRun,
|
||||
resolveOllamaCompatNumCtxEnabled,
|
||||
shouldInjectOllamaCompatNumCtx,
|
||||
wrapOllamaCompatNumCtx,
|
||||
|
||||
@@ -3,6 +3,8 @@ export type OutboundMediaReadFile = (filePath: string) => Promise<Buffer>;
|
||||
export type OutboundMediaAccess = {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: OutboundMediaReadFile;
|
||||
/** Agent workspace directory for resolving relative MEDIA: paths. */
|
||||
workspaceDir?: string;
|
||||
};
|
||||
|
||||
export type OutboundMediaLoadParams = {
|
||||
@@ -11,6 +13,8 @@ export type OutboundMediaLoadParams = {
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: OutboundMediaReadFile;
|
||||
optimizeImages?: boolean;
|
||||
/** Agent workspace directory for resolving relative MEDIA: paths. */
|
||||
workspaceDir?: string;
|
||||
};
|
||||
|
||||
export type OutboundMediaLoadOptions = {
|
||||
@@ -19,6 +23,8 @@ export type OutboundMediaLoadOptions = {
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
hostReadCapability?: boolean;
|
||||
optimizeImages?: boolean;
|
||||
/** Agent workspace directory for resolving relative MEDIA: paths. */
|
||||
workspaceDir?: string;
|
||||
};
|
||||
|
||||
export function resolveOutboundMediaLocalRoots(
|
||||
@@ -38,12 +44,14 @@ export function resolveOutboundMediaAccess(
|
||||
params.mediaAccess?.localRoots ?? params.mediaLocalRoots,
|
||||
);
|
||||
const readFile = params.mediaAccess?.readFile ?? params.mediaReadFile;
|
||||
if (!localRoots && !readFile) {
|
||||
const workspaceDir = params.mediaAccess?.workspaceDir;
|
||||
if (!localRoots && !readFile && !workspaceDir) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...(localRoots ? { localRoots } : {}),
|
||||
...(readFile ? { readFile } : {}),
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,6 +59,7 @@ export function buildOutboundMediaLoadOptions(
|
||||
params: OutboundMediaLoadParams = {},
|
||||
): OutboundMediaLoadOptions {
|
||||
const mediaAccess = resolveOutboundMediaAccess(params);
|
||||
const workspaceDir = mediaAccess?.workspaceDir ?? params.workspaceDir;
|
||||
if (mediaAccess?.readFile) {
|
||||
return {
|
||||
...(params.maxBytes !== undefined ? { maxBytes: params.maxBytes } : {}),
|
||||
@@ -58,6 +67,7 @@ export function buildOutboundMediaLoadOptions(
|
||||
readFile: mediaAccess.readFile,
|
||||
hostReadCapability: true,
|
||||
...(params.optimizeImages !== undefined ? { optimizeImages: params.optimizeImages } : {}),
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
};
|
||||
}
|
||||
const localRoots = mediaAccess?.localRoots;
|
||||
@@ -65,5 +75,6 @@ export function buildOutboundMediaLoadOptions(
|
||||
...(params.maxBytes !== undefined ? { maxBytes: params.maxBytes } : {}),
|
||||
...(localRoots ? { localRoots } : {}),
|
||||
...(params.optimizeImages !== undefined ? { optimizeImages: params.optimizeImages } : {}),
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
24
src/media/read-capability.test.ts
Normal file
24
src/media/read-capability.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveAgentScopedOutboundMediaAccess } from "./read-capability.js";
|
||||
|
||||
describe("resolveAgentScopedOutboundMediaAccess", () => {
|
||||
it("preserves caller-provided workspaceDir from mediaAccess", () => {
|
||||
const result = resolveAgentScopedOutboundMediaAccess({
|
||||
cfg: {} as OpenClawConfig,
|
||||
mediaAccess: { workspaceDir: "/tmp/media-workspace" },
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ workspaceDir: "/tmp/media-workspace" });
|
||||
});
|
||||
|
||||
it("prefers explicit workspaceDir over mediaAccess.workspaceDir", () => {
|
||||
const result = resolveAgentScopedOutboundMediaAccess({
|
||||
cfg: {} as OpenClawConfig,
|
||||
workspaceDir: "/tmp/explicit-workspace",
|
||||
mediaAccess: { workspaceDir: "/tmp/media-workspace" },
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ workspaceDir: "/tmp/explicit-workspace" });
|
||||
});
|
||||
});
|
||||
@@ -45,16 +45,21 @@ export function resolveAgentScopedOutboundMediaAccess(params: {
|
||||
agentId: params.agentId,
|
||||
mediaSources: params.mediaSources,
|
||||
});
|
||||
const resolvedWorkspaceDir =
|
||||
params.workspaceDir ??
|
||||
params.mediaAccess?.workspaceDir ??
|
||||
(params.agentId ? resolveAgentWorkspaceDir(params.cfg, params.agentId) : undefined);
|
||||
const readFile =
|
||||
params.mediaAccess?.readFile ??
|
||||
params.mediaReadFile ??
|
||||
createAgentScopedHostMediaReadFile({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
workspaceDir: params.workspaceDir,
|
||||
workspaceDir: resolvedWorkspaceDir,
|
||||
});
|
||||
return {
|
||||
...(localRoots?.length ? { localRoots } : {}),
|
||||
...(readFile ? { readFile } : {}),
|
||||
...(resolvedWorkspaceDir ? { workspaceDir: resolvedWorkspaceDir } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -133,4 +133,42 @@ describe("loadWebMedia", () => {
|
||||
] as const)("$name", async (testCase) => {
|
||||
await expectRejectedWebMediaWithoutFilesystemAccess(testCase);
|
||||
});
|
||||
|
||||
describe("workspaceDir relative path resolution", () => {
|
||||
it("resolves a bare filename against workspaceDir", async () => {
|
||||
const result = await loadWebMedia("tiny.png", {
|
||||
...createLocalWebMediaOptions(),
|
||||
workspaceDir: fixtureRoot,
|
||||
});
|
||||
expect(result.kind).toBe("image");
|
||||
expect(result.buffer.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("resolves a dot-relative path against workspaceDir", async () => {
|
||||
const result = await loadWebMedia("./tiny.png", {
|
||||
...createLocalWebMediaOptions(),
|
||||
workspaceDir: fixtureRoot,
|
||||
});
|
||||
expect(result.kind).toBe("image");
|
||||
expect(result.buffer.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("resolves a MEDIA:-prefixed relative path against workspaceDir", async () => {
|
||||
const result = await loadWebMedia("MEDIA:tiny.png", {
|
||||
...createLocalWebMediaOptions(),
|
||||
workspaceDir: fixtureRoot,
|
||||
});
|
||||
expect(result.kind).toBe("image");
|
||||
expect(result.buffer.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("leaves absolute paths unchanged when workspaceDir is set", async () => {
|
||||
const result = await loadWebMedia(tinyPngFile, {
|
||||
...createLocalWebMediaOptions(),
|
||||
workspaceDir: "/some/other/dir",
|
||||
});
|
||||
expect(result.kind).toBe("image");
|
||||
expect(result.buffer.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,8 @@ type WebMediaOptions = {
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
/** Host-local fs-policy read piggyback; rejects plaintext-like document sends. */
|
||||
hostReadCapability?: boolean;
|
||||
/** Agent workspace directory for resolving relative MEDIA: paths. */
|
||||
workspaceDir?: string;
|
||||
};
|
||||
|
||||
function resolveWebMediaOptions(params: {
|
||||
@@ -221,6 +223,7 @@ async function loadWebMediaInternal(
|
||||
sandboxValidated = false,
|
||||
readFile: readFileOverride,
|
||||
hostReadCapability = false,
|
||||
workspaceDir,
|
||||
} = options;
|
||||
// Strip MEDIA: prefix used by agent tools (e.g. TTS) to tag media paths.
|
||||
// Be lenient: LLM output may add extra whitespace (e.g. " MEDIA : /tmp/x.png").
|
||||
@@ -321,6 +324,13 @@ async function loadWebMediaInternal(
|
||||
if (mediaUrl.startsWith("~")) {
|
||||
mediaUrl = resolveUserPath(mediaUrl);
|
||||
}
|
||||
|
||||
// Resolve relative MEDIA: paths (e.g. "poker_profit.png", "./subdir/file.png")
|
||||
// against the agent workspace directory so bare filenames written by agents
|
||||
// are found on disk and pass the local-roots allowlist check.
|
||||
if (workspaceDir && !path.isAbsolute(mediaUrl)) {
|
||||
mediaUrl = path.resolve(workspaceDir, mediaUrl);
|
||||
}
|
||||
try {
|
||||
assertNoWindowsNetworkPath(mediaUrl, "Local media path");
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user