feat(sandbox): allow image tool
This commit is contained in:
@@ -31,6 +31,7 @@ export function createClawdbotTools(options?: {
|
|||||||
agentProvider?: GatewayMessageProvider;
|
agentProvider?: GatewayMessageProvider;
|
||||||
agentAccountId?: string;
|
agentAccountId?: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
|
sandboxRoot?: string;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
sandboxed?: boolean;
|
sandboxed?: boolean;
|
||||||
config?: ClawdbotConfig;
|
config?: ClawdbotConfig;
|
||||||
@@ -46,6 +47,7 @@ export function createClawdbotTools(options?: {
|
|||||||
const imageTool = createImageTool({
|
const imageTool = createImageTool({
|
||||||
config: options?.config,
|
config: options?.config,
|
||||||
agentDir: options?.agentDir,
|
agentDir: options?.agentDir,
|
||||||
|
sandboxRoot: options?.sandboxRoot,
|
||||||
});
|
});
|
||||||
const memorySearchTool = createMemorySearchTool({
|
const memorySearchTool = createMemorySearchTool({
|
||||||
config: options?.config,
|
config: options?.config,
|
||||||
|
|||||||
@@ -678,6 +678,7 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
agentProvider: resolveGatewayMessageProvider(options?.messageProvider),
|
agentProvider: resolveGatewayMessageProvider(options?.messageProvider),
|
||||||
agentAccountId: options?.agentAccountId,
|
agentAccountId: options?.agentAccountId,
|
||||||
agentDir: options?.agentDir,
|
agentDir: options?.agentDir,
|
||||||
|
sandboxRoot,
|
||||||
workspaceDir: options?.workspaceDir,
|
workspaceDir: options?.workspaceDir,
|
||||||
sandboxed: !!sandbox,
|
sandboxed: !!sandbox,
|
||||||
config: options?.config,
|
config: options?.config,
|
||||||
|
|||||||
@@ -486,4 +486,22 @@ describe("Agent-specific sandbox config", () => {
|
|||||||
const sandbox = resolveSandboxConfigForAgent(cfg, "main");
|
const sandbox = resolveSandboxConfigForAgent(cfg, "main");
|
||||||
expect(sandbox.tools.allow).toContain("session_status");
|
expect(sandbox.tools.allow).toContain("session_status");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes image in default sandbox allowlist", async () => {
|
||||||
|
const { resolveSandboxConfigForAgent } = await import("./sandbox.js");
|
||||||
|
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
sandbox: {
|
||||||
|
mode: "all",
|
||||||
|
scope: "agent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const sandbox = resolveSandboxConfigForAgent(cfg, "main");
|
||||||
|
expect(sandbox.tools.allow).toContain("image");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ const DEFAULT_TOOL_ALLOW = [
|
|||||||
"write",
|
"write",
|
||||||
"edit",
|
"edit",
|
||||||
"apply_patch",
|
"apply_patch",
|
||||||
|
"image",
|
||||||
"sessions_list",
|
"sessions_list",
|
||||||
"sessions_history",
|
"sessions_history",
|
||||||
"sessions_send",
|
"sessions_send",
|
||||||
|
|||||||
@@ -104,4 +104,31 @@ describe("image tool implicit imageModel config", () => {
|
|||||||
primary: "openai/gpt-5-mini",
|
primary: "openai/gpt-5-mini",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sandboxes image paths like the read tool", async () => {
|
||||||
|
const stateDir = await fs.mkdtemp(
|
||||||
|
path.join(os.tmpdir(), "clawdbot-image-sandbox-"),
|
||||||
|
);
|
||||||
|
const agentDir = path.join(stateDir, "agent");
|
||||||
|
const sandboxRoot = path.join(stateDir, "sandbox");
|
||||||
|
await fs.mkdir(agentDir, { recursive: true });
|
||||||
|
await fs.mkdir(sandboxRoot, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(sandboxRoot, "img.png"), "fake", "utf8");
|
||||||
|
|
||||||
|
vi.stubEnv("OPENAI_API_KEY", "openai-test");
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agents: { defaults: { model: { primary: "minimax/MiniMax-M2.1" } } },
|
||||||
|
};
|
||||||
|
const tool = createImageTool({ config: cfg, agentDir, sandboxRoot });
|
||||||
|
expect(tool).not.toBeNull();
|
||||||
|
if (!tool) throw new Error("expected image tool");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
tool.execute("t1", { image: "https://example.com/a.png" }),
|
||||||
|
).rejects.toThrow(/Sandboxed image tool does not allow remote URLs/i);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
tool.execute("t2", { image: "../escape.png" }),
|
||||||
|
).rejects.toThrow(/escapes sandbox root/i);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { runWithImageModelFallback } from "../model-fallback.js";
|
|||||||
import { parseModelRef } from "../model-selection.js";
|
import { parseModelRef } from "../model-selection.js";
|
||||||
import { ensureClawdbotModelsJson } from "../models-config.js";
|
import { ensureClawdbotModelsJson } from "../models-config.js";
|
||||||
import { extractAssistantText } from "../pi-embedded-utils.js";
|
import { extractAssistantText } from "../pi-embedded-utils.js";
|
||||||
|
import { assertSandboxPath } from "../sandbox-paths.js";
|
||||||
import type { AnyAgentTool } from "./common.js";
|
import type { AnyAgentTool } from "./common.js";
|
||||||
|
|
||||||
const DEFAULT_PROMPT = "Describe the image.";
|
const DEFAULT_PROMPT = "Describe the image.";
|
||||||
@@ -296,6 +297,7 @@ async function runImagePrompt(params: {
|
|||||||
export function createImageTool(options?: {
|
export function createImageTool(options?: {
|
||||||
config?: ClawdbotConfig;
|
config?: ClawdbotConfig;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
|
sandboxRoot?: string;
|
||||||
}): AnyAgentTool | null {
|
}): AnyAgentTool | null {
|
||||||
const agentDir = options?.agentDir;
|
const agentDir = options?.agentDir;
|
||||||
if (!agentDir?.trim()) {
|
if (!agentDir?.trim()) {
|
||||||
@@ -337,9 +339,23 @@ export function createImageTool(options?: {
|
|||||||
typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined;
|
typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined;
|
||||||
const maxBytes = pickMaxBytes(options?.config, maxBytesMb);
|
const maxBytes = pickMaxBytes(options?.config, maxBytesMb);
|
||||||
|
|
||||||
const resolvedImage = imageRaw.startsWith("~")
|
const sandboxRoot = options?.sandboxRoot?.trim();
|
||||||
? resolveUserPath(imageRaw)
|
const isUrl = /^https?:\/\//i.test(imageRaw);
|
||||||
: imageRaw;
|
if (sandboxRoot && isUrl) {
|
||||||
|
throw new Error("Sandboxed image tool does not allow remote URLs.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedImage = sandboxRoot
|
||||||
|
? (
|
||||||
|
await assertSandboxPath({
|
||||||
|
filePath: imageRaw,
|
||||||
|
cwd: sandboxRoot,
|
||||||
|
root: sandboxRoot,
|
||||||
|
})
|
||||||
|
).resolved
|
||||||
|
: imageRaw.startsWith("~")
|
||||||
|
? resolveUserPath(imageRaw)
|
||||||
|
: imageRaw;
|
||||||
const media = await loadWebMedia(resolvedImage, maxBytes);
|
const media = await loadWebMedia(resolvedImage, maxBytes);
|
||||||
if (media.kind !== "image") {
|
if (media.kind !== "image") {
|
||||||
throw new Error(`Unsupported media type: ${media.kind}`);
|
throw new Error(`Unsupported media type: ${media.kind}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user