diff --git a/src/agents/tools/media-tool-shared.test.ts b/src/agents/tools/media-tool-shared.test.ts new file mode 100644 index 00000000000..7a69c5eb286 --- /dev/null +++ b/src/agents/tools/media-tool-shared.test.ts @@ -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("/")); + }); +}); diff --git a/src/agents/tools/media-tool-shared.ts b/src/agents/tools/media-tool-shared.ts index 777499bce9e..6b65c595b5e 100644 --- a/src/agents/tools/media-tool-shared.ts +++ b/src/agents/tools/media-tool-shared.ts @@ -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( diff --git a/src/media/local-roots.test.ts b/src/media/local-roots.test.ts index 27d7e144d2a..46d4f4281c8 100644 --- a/src/media/local-roots.test.ts +++ b/src/media/local-roots.test.ts @@ -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("/")); + }); }); diff --git a/src/media/local-roots.ts b/src/media/local-roots.ts index acb3aab3cf6..1b5e1bbe993 100644 --- a/src/media/local-roots.ts +++ b/src/media/local-roots.ts @@ -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); }