mirror of https://github.com/openclaw/openclaw.git
fix(media): keep local roots configuration-derived (#57770)
* fix(media): keep local roots configuration-derived Co-authored-by: Jacob Tomlinson <jtomlinson@nvidia.com> * fix(media): simplify local root lookup * fix(media): keep legacy local roots export
This commit is contained in:
parent
aff6883f93
commit
1ca4261d7e
|
|
@ -0,0 +1,37 @@
|
|||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveMediaToolLocalRoots } from "./media-tool-shared.js";
|
||||
|
||||
function normalizeHostPath(value: string): string {
|
||||
return path.normalize(path.resolve(value));
|
||||
}
|
||||
|
||||
describe("resolveMediaToolLocalRoots", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("does not widen default local roots from media sources", () => {
|
||||
const stateDir = path.join("/tmp", "openclaw-media-tool-roots-state");
|
||||
const picturesDir =
|
||||
process.platform === "win32" ? "C:\\Users\\peter\\Pictures" : "/Users/peter/Pictures";
|
||||
const moviesDir =
|
||||
process.platform === "win32" ? "C:\\Users\\peter\\Movies" : "/Users/peter/Movies";
|
||||
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
|
||||
|
||||
const roots = resolveMediaToolLocalRoots(path.join(stateDir, "workspace-agent"), undefined, [
|
||||
path.join(picturesDir, "photo.png"),
|
||||
pathToFileURL(path.join(moviesDir, "clip.mp4")).href,
|
||||
"/top-level-file.png",
|
||||
]);
|
||||
|
||||
const normalizedRoots = roots.map(normalizeHostPath);
|
||||
expect(normalizedRoots).toContain(normalizeHostPath(path.join(stateDir, "workspace-agent")));
|
||||
expect(normalizedRoots).toContain(normalizeHostPath(path.join(stateDir, "workspace")));
|
||||
expect(normalizedRoots).not.toContain(normalizeHostPath(picturesDir));
|
||||
expect(normalizedRoots).not.toContain(normalizeHostPath(moviesDir));
|
||||
expect(normalizedRoots).not.toContain(normalizeHostPath("/"));
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { type Api, type Model } from "@mariozechner/pi-ai";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { appendLocalMediaParentRoots } from "../../media/local-roots.js";
|
||||
import { getDefaultLocalRoots } from "../../media/web-media.js";
|
||||
import type { ImageModelConfig } from "./image-tool.helpers.js";
|
||||
import type { ToolModelConfig } from "./model-config.helpers.js";
|
||||
|
|
@ -56,15 +55,14 @@ function applyAgentDefaultModelConfig(
|
|||
export function resolveMediaToolLocalRoots(
|
||||
workspaceDirRaw: string | undefined,
|
||||
options?: { workspaceOnly?: boolean },
|
||||
mediaSources?: readonly string[],
|
||||
_mediaSources?: readonly string[],
|
||||
): string[] {
|
||||
const workspaceDir = normalizeWorkspaceDir(workspaceDirRaw);
|
||||
if (options?.workspaceOnly) {
|
||||
return workspaceDir ? [workspaceDir] : [];
|
||||
}
|
||||
const roots = getDefaultLocalRoots();
|
||||
const scopedRoots = workspaceDir ? Array.from(new Set([...roots, workspaceDir])) : [...roots];
|
||||
return appendLocalMediaParentRoots(scopedRoots, mediaSources);
|
||||
return workspaceDir ? Array.from(new Set([...roots, workspaceDir])) : [...roots];
|
||||
}
|
||||
|
||||
export function resolvePromptAndModelOverride(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import path from "node:path";
|
|||
import { pathToFileURL } from "node:url";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
appendLocalMediaParentRoots,
|
||||
getAgentScopedMediaLocalRoots,
|
||||
getAgentScopedMediaLocalRootsForSources,
|
||||
getDefaultMediaLocalRoots,
|
||||
|
|
@ -52,6 +51,14 @@ describe("local media roots", () => {
|
|||
expect(normalizedRoots).not.toContain(picturesRoot);
|
||||
}
|
||||
|
||||
function expectPicturesRootAbsent(roots: readonly string[], picturesRoot?: string) {
|
||||
expectPicturesRootPresence({
|
||||
roots,
|
||||
shouldContainPictures: false,
|
||||
picturesRoot,
|
||||
});
|
||||
}
|
||||
|
||||
function expectAgentMediaRootsCase(params: {
|
||||
stateDir: string;
|
||||
getRoots: () => readonly string[];
|
||||
|
|
@ -101,38 +108,12 @@ describe("local media roots", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("adds concrete parent roots for local media sources without widening to filesystem root", () => {
|
||||
const picturesDir =
|
||||
process.platform === "win32" ? "C:\\Users\\peter\\Pictures" : "/Users/peter/Pictures";
|
||||
const moviesDir =
|
||||
process.platform === "win32" ? "C:\\Users\\peter\\Movies" : "/Users/peter/Movies";
|
||||
|
||||
const roots = appendLocalMediaParentRoots(
|
||||
["/tmp/base"],
|
||||
[
|
||||
path.join(picturesDir, "photo.png"),
|
||||
pathToFileURL(path.join(moviesDir, "clip.mp4")).href,
|
||||
"https://example.com/remote.png",
|
||||
"/top-level-file.png",
|
||||
],
|
||||
);
|
||||
|
||||
expect(roots.map(normalizeHostPath)).toEqual(
|
||||
expect.arrayContaining([
|
||||
normalizeHostPath("/tmp/base"),
|
||||
normalizeHostPath(picturesDir),
|
||||
normalizeHostPath(moviesDir),
|
||||
]),
|
||||
);
|
||||
expect(roots.map(normalizeHostPath)).not.toContain(normalizeHostPath("/"));
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "widens agent media roots for concrete local sources when workspaceOnly is disabled",
|
||||
name: "does not widen agent media roots for concrete local sources when workspaceOnly is disabled",
|
||||
stateDir: path.join("/tmp", "openclaw-flexible-media-roots-state"),
|
||||
cfg: {},
|
||||
shouldContainPictures: true,
|
||||
shouldContainPictures: false,
|
||||
},
|
||||
{
|
||||
name: "does not widen agent media roots when workspaceOnly is enabled",
|
||||
|
|
@ -147,7 +128,7 @@ describe("local media roots", () => {
|
|||
shouldContainPictures: false,
|
||||
},
|
||||
{
|
||||
name: "widens media roots again when messaging-profile agents explicitly enable filesystem tools",
|
||||
name: "does not widen media roots even when messaging-profile agents explicitly enable filesystem tools",
|
||||
stateDir: path.join("/tmp", "openclaw-messaging-fs-media-roots-state"),
|
||||
cfg: {
|
||||
tools: {
|
||||
|
|
@ -155,7 +136,7 @@ describe("local media roots", () => {
|
|||
fs: { workspaceOnly: false },
|
||||
},
|
||||
},
|
||||
shouldContainPictures: true,
|
||||
shouldContainPictures: false,
|
||||
},
|
||||
] as const)("$name", ({ stateDir, cfg, shouldContainPictures }) => {
|
||||
const roots = withStateDir(stateDir, () =>
|
||||
|
|
@ -167,4 +148,33 @@ describe("local media roots", () => {
|
|||
);
|
||||
expectPicturesRootPresence({ roots, shouldContainPictures });
|
||||
});
|
||||
|
||||
it("keeps agent-scoped defaults even when mediaSources include file URLs and top-level paths", () => {
|
||||
const stateDir = path.join("/tmp", "openclaw-file-url-media-roots-state");
|
||||
const picturesDir =
|
||||
process.platform === "win32" ? "C:\\Users\\peter\\Pictures" : "/Users/peter/Pictures";
|
||||
const moviesDir =
|
||||
process.platform === "win32" ? "C:\\Users\\peter\\Movies" : "/Users/peter/Movies";
|
||||
|
||||
const roots = withStateDir(stateDir, () =>
|
||||
getAgentScopedMediaLocalRootsForSources({
|
||||
cfg: {},
|
||||
agentId: "ops",
|
||||
mediaSources: [
|
||||
path.join(picturesDir, "photo.png"),
|
||||
pathToFileURL(path.join(moviesDir, "clip.mp4")).href,
|
||||
"/top-level-file.png",
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expectNormalizedRootsContain(roots, [
|
||||
path.join(stateDir, "media"),
|
||||
path.join(stateDir, "workspace"),
|
||||
path.join(stateDir, "workspace-ops"),
|
||||
]);
|
||||
expectPicturesRootAbsent(roots, picturesDir);
|
||||
expectPicturesRootAbsent(roots, moviesDir);
|
||||
expect(roots.map(normalizeHostPath)).not.toContain(normalizeHostPath("/"));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,23 +1,14 @@
|
|||
import path from "node:path";
|
||||
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import {
|
||||
resolveEffectiveToolFsRootExpansionAllowed,
|
||||
resolveEffectiveToolFsWorkspaceOnly,
|
||||
} from "../agents/tool-fs-policy.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { safeFileURLToPath } from "../infra/local-file-access.js";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
|
||||
type BuildMediaLocalRootsOptions = {
|
||||
preferredTmpDir?: string;
|
||||
};
|
||||
|
||||
let cachedPreferredTmpDir: string | undefined;
|
||||
const HTTP_URL_RE = /^https?:\/\//i;
|
||||
const DATA_URL_RE = /^data:/i;
|
||||
const WINDOWS_DRIVE_RE = /^[A-Za-z]:[\\/]/;
|
||||
|
||||
function resolveCachedPreferredTmpDir(): string {
|
||||
if (!cachedPreferredTmpDir) {
|
||||
|
|
@ -63,60 +54,24 @@ export function getAgentScopedMediaLocalRoots(
|
|||
return roots;
|
||||
}
|
||||
|
||||
function resolveLocalMediaPath(source: string): string | undefined {
|
||||
const trimmed = source.trim();
|
||||
if (!trimmed || HTTP_URL_RE.test(trimmed) || DATA_URL_RE.test(trimmed)) {
|
||||
return undefined;
|
||||
}
|
||||
if (trimmed.startsWith("file://")) {
|
||||
try {
|
||||
return safeFileURLToPath(trimmed);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (trimmed.startsWith("~")) {
|
||||
return resolveUserPath(trimmed);
|
||||
}
|
||||
if (path.isAbsolute(trimmed) || WINDOWS_DRIVE_RE.test(trimmed)) {
|
||||
return path.resolve(trimmed);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Kept for plugin-sdk compatibility. Media sources no longer widen allowed roots.
|
||||
*/
|
||||
export function appendLocalMediaParentRoots(
|
||||
roots: readonly string[],
|
||||
mediaSources?: readonly string[],
|
||||
_mediaSources?: readonly string[],
|
||||
): string[] {
|
||||
const appended = Array.from(new Set(roots.map((root) => path.resolve(root))));
|
||||
for (const source of mediaSources ?? []) {
|
||||
const localPath = resolveLocalMediaPath(source);
|
||||
if (!localPath) {
|
||||
continue;
|
||||
}
|
||||
const parentDir = path.dirname(localPath);
|
||||
if (parentDir === path.parse(parentDir).root) {
|
||||
continue;
|
||||
}
|
||||
const normalizedParent = path.resolve(parentDir);
|
||||
if (!appended.includes(normalizedParent)) {
|
||||
appended.push(normalizedParent);
|
||||
}
|
||||
}
|
||||
return appended;
|
||||
return Array.from(new Set(roots.map((root) => path.resolve(root))));
|
||||
}
|
||||
|
||||
export function getAgentScopedMediaLocalRootsForSources(params: {
|
||||
export function getAgentScopedMediaLocalRootsForSources({
|
||||
cfg,
|
||||
agentId,
|
||||
mediaSources: _mediaSources,
|
||||
}: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId?: string;
|
||||
mediaSources?: readonly string[];
|
||||
}): readonly string[] {
|
||||
const roots = getAgentScopedMediaLocalRoots(params.cfg, params.agentId);
|
||||
if (resolveEffectiveToolFsWorkspaceOnly({ cfg: params.cfg, agentId: params.agentId })) {
|
||||
return roots;
|
||||
}
|
||||
if (!resolveEffectiveToolFsRootExpansionAllowed({ cfg: params.cfg, agentId: params.agentId })) {
|
||||
return roots;
|
||||
}
|
||||
return appendLocalMediaParentRoots(roots, params.mediaSources);
|
||||
return getAgentScopedMediaLocalRoots(cfg, agentId);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue