mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-26 16:06:16 +00:00
fix: enforce workspaceOnly for native prompt image autoload
This commit is contained in:
@@ -28,7 +28,7 @@ import { resolveUserPath } from "../../../utils.js";
|
||||
import { normalizeMessageChannel } from "../../../utils/message-channel.js";
|
||||
import { isReasoningTagProvider } from "../../../utils/provider-utils.js";
|
||||
import { resolveOpenClawAgentDir } from "../../agent-paths.js";
|
||||
import { resolveSessionAgentIds } from "../../agent-scope.js";
|
||||
import { resolveAgentConfig, resolveSessionAgentIds } from "../../agent-scope.js";
|
||||
import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js";
|
||||
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
||||
import { createCacheTrace } from "../../cache-trace.js";
|
||||
@@ -363,6 +363,9 @@ export async function runEmbeddedAttempt(
|
||||
config: params.config,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const effectiveFsWorkspaceOnly =
|
||||
(resolveAgentConfig(params.config ?? {}, sessionAgentId)?.tools?.fs?.workspaceOnly ??
|
||||
params.config?.tools?.fs?.workspaceOnly) === true;
|
||||
// Check if the model supports native image input
|
||||
const modelHasVision = params.model.input?.includes("image") ?? false;
|
||||
const toolsRaw = params.disableTools
|
||||
@@ -1087,6 +1090,7 @@ export async function runEmbeddedAttempt(
|
||||
historyMessages: activeSession.messages,
|
||||
maxBytes: MAX_IMAGE_BYTES,
|
||||
maxDimensionPx: resolveImageSanitizationLimits(params.config).maxDimensionPx,
|
||||
workspaceOnly: effectiveFsWorkspaceOnly,
|
||||
// Enforce sandbox path restrictions when sandbox is enabled
|
||||
sandbox:
|
||||
sandbox?.enabled && sandbox?.fsBridge
|
||||
|
||||
@@ -3,6 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createHostSandboxFsBridge } from "../../test-helpers/host-sandbox-fs-bridge.js";
|
||||
import { createUnsafeMountedSandbox } from "../../test-helpers/unsafe-mounted-sandbox.js";
|
||||
import {
|
||||
detectAndLoadPromptImages,
|
||||
detectImageReferences,
|
||||
@@ -275,4 +276,76 @@ describe("detectAndLoadPromptImages", () => {
|
||||
expect(result.images).toHaveLength(0);
|
||||
expect(result.historyImagesByIndex.size).toBe(0);
|
||||
});
|
||||
|
||||
it("blocks prompt image refs outside workspace when sandbox workspaceOnly is enabled", async () => {
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-native-image-sandbox-"));
|
||||
const sandboxRoot = path.join(stateDir, "sandbox");
|
||||
const agentRoot = path.join(stateDir, "agent");
|
||||
await fs.mkdir(sandboxRoot, { recursive: true });
|
||||
await fs.mkdir(agentRoot, { recursive: true });
|
||||
const pngB64 =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII=";
|
||||
await fs.writeFile(path.join(agentRoot, "secret.png"), Buffer.from(pngB64, "base64"));
|
||||
const sandbox = createUnsafeMountedSandbox({ sandboxRoot, agentRoot });
|
||||
const bridge = sandbox.fsBridge;
|
||||
if (!bridge) {
|
||||
throw new Error("sandbox fs bridge missing");
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await detectAndLoadPromptImages({
|
||||
prompt: "Inspect /agent/secret.png",
|
||||
workspaceDir: sandboxRoot,
|
||||
model: { input: ["text", "image"] },
|
||||
workspaceOnly: true,
|
||||
sandbox: { root: sandbox.workspaceDir, bridge },
|
||||
});
|
||||
|
||||
expect(result.detectedRefs).toHaveLength(1);
|
||||
expect(result.loadedCount).toBe(0);
|
||||
expect(result.skippedCount).toBe(1);
|
||||
expect(result.images).toHaveLength(0);
|
||||
} finally {
|
||||
await fs.rm(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("blocks history image refs outside workspace when sandbox workspaceOnly is enabled", async () => {
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-native-image-sandbox-"));
|
||||
const sandboxRoot = path.join(stateDir, "sandbox");
|
||||
const agentRoot = path.join(stateDir, "agent");
|
||||
await fs.mkdir(sandboxRoot, { recursive: true });
|
||||
await fs.mkdir(agentRoot, { recursive: true });
|
||||
const pngB64 =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII=";
|
||||
await fs.writeFile(path.join(agentRoot, "secret.png"), Buffer.from(pngB64, "base64"));
|
||||
const sandbox = createUnsafeMountedSandbox({ sandboxRoot, agentRoot });
|
||||
const bridge = sandbox.fsBridge;
|
||||
if (!bridge) {
|
||||
throw new Error("sandbox fs bridge missing");
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await detectAndLoadPromptImages({
|
||||
prompt: "No inline image in this turn.",
|
||||
workspaceDir: sandboxRoot,
|
||||
model: { input: ["text", "image"] },
|
||||
workspaceOnly: true,
|
||||
historyMessages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Previous image /agent/secret.png" }],
|
||||
},
|
||||
],
|
||||
sandbox: { root: sandbox.workspaceDir, bridge },
|
||||
});
|
||||
|
||||
expect(result.detectedRefs).toHaveLength(1);
|
||||
expect(result.loadedCount).toBe(0);
|
||||
expect(result.skippedCount).toBe(1);
|
||||
expect(result.historyImagesByIndex.size).toBe(0);
|
||||
} finally {
|
||||
await fs.rm(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ImageContent } from "@mariozechner/pi-ai";
|
||||
import { resolveUserPath } from "../../../utils.js";
|
||||
import { loadWebMedia } from "../../../web/media.js";
|
||||
import type { ImageSanitizationLimits } from "../../image-sanitization.js";
|
||||
import { assertSandboxPath } from "../../sandbox-paths.js";
|
||||
import type { SandboxFsBridge } from "../../sandbox/fs-bridge.js";
|
||||
import { sanitizeImageBlocks } from "../../tool-images.js";
|
||||
import { log } from "../logger.js";
|
||||
@@ -181,6 +182,7 @@ export async function loadImageFromRef(
|
||||
workspaceDir: string,
|
||||
options?: {
|
||||
maxBytes?: number;
|
||||
workspaceOnly?: boolean;
|
||||
sandbox?: { root: string; bridge: SandboxFsBridge };
|
||||
},
|
||||
): Promise<ImageContent | null> {
|
||||
@@ -211,6 +213,14 @@ export async function loadImageFromRef(
|
||||
} else if (!path.isAbsolute(targetPath)) {
|
||||
targetPath = path.resolve(workspaceDir, targetPath);
|
||||
}
|
||||
if (options?.workspaceOnly) {
|
||||
const root = options?.sandbox?.root ?? workspaceDir;
|
||||
await assertSandboxPath({
|
||||
filePath: targetPath,
|
||||
cwd: root,
|
||||
root,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// loadWebMedia handles local file paths (including file:// URLs)
|
||||
@@ -361,6 +371,7 @@ export async function detectAndLoadPromptImages(params: {
|
||||
historyMessages?: unknown[];
|
||||
maxBytes?: number;
|
||||
maxDimensionPx?: number;
|
||||
workspaceOnly?: boolean;
|
||||
sandbox?: { root: string; bridge: SandboxFsBridge };
|
||||
}): Promise<{
|
||||
/** Images for the current prompt (existingImages + detected in current prompt) */
|
||||
@@ -422,6 +433,7 @@ export async function detectAndLoadPromptImages(params: {
|
||||
for (const ref of allRefs) {
|
||||
const image = await loadImageFromRef(ref, params.workspaceDir, {
|
||||
maxBytes: params.maxBytes,
|
||||
workspaceOnly: params.workspaceOnly,
|
||||
sandbox: params.sandbox,
|
||||
});
|
||||
if (image) {
|
||||
|
||||
Reference in New Issue
Block a user