diff --git a/src/agents/apply-patch.ts b/src/agents/apply-patch.ts index 23ecdd5517..f6b29d4242 100644 --- a/src/agents/apply-patch.ts +++ b/src/agents/apply-patch.ts @@ -3,8 +3,8 @@ import { Type } from "@sinclair/typebox"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { applyUpdateHunk } from "./apply-patch-update.js"; import type { SandboxFsBridge } from "./sandbox/fs-bridge.js"; +import { applyUpdateHunk } from "./apply-patch-update.js"; const BEGIN_PATCH_MARKER = "*** Begin Patch"; const END_PATCH_MARKER = "*** End Patch"; @@ -242,7 +242,9 @@ function resolvePatchFileOps(options: ApplyPatchOptions): PatchFileOps { async function ensureDir(filePath: string, ops: PatchFileOps) { const parent = path.dirname(filePath); - if (!parent || parent === ".") return; + if (!parent || parent === ".") { + return; + } await ops.mkdirp(parent); } diff --git a/src/agents/openclaw-tools.ts b/src/agents/openclaw-tools.ts index 33a625d9bf..2be40ead3c 100644 --- a/src/agents/openclaw-tools.ts +++ b/src/agents/openclaw-tools.ts @@ -1,5 +1,6 @@ import type { OpenClawConfig } from "../config/config.js"; import type { GatewayMessageChannel } from "../utils/message-channel.js"; +import type { SandboxFsBridge } from "./sandbox/fs-bridge.js"; import type { AnyAgentTool } from "./tools/common.js"; import { resolvePluginTools } from "../plugins/tools.js"; import { resolveSessionAgentId } from "./agent-scope.js"; @@ -17,7 +18,7 @@ import { createSessionsListTool } from "./tools/sessions-list-tool.js"; import { createSessionsSendTool } from "./tools/sessions-send-tool.js"; import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js"; import { createTtsTool } from "./tools/tts-tool.js"; -import type { SandboxFsBridge } from "./sandbox/fs-bridge.js"; +import { createWebFetchTool, createWebSearchTool } from "./tools/web-tools.js"; export function createOpenClawTools(options?: { sandboxBrowserBridgeUrl?: string; diff --git a/src/agents/pi-embedded-runner/run/images.ts b/src/agents/pi-embedded-runner/run/images.ts index 0403fa646f..2511728ca7 100644 --- a/src/agents/pi-embedded-runner/run/images.ts +++ b/src/agents/pi-embedded-runner/run/images.ts @@ -1,11 +1,9 @@ +import type { ImageContent } from "@mariozechner/pi-ai"; import path from "node:path"; import { fileURLToPath } from "node:url"; - -import type { ImageContent } from "@mariozechner/pi-ai"; - +import type { SandboxFsBridge } from "../../sandbox/fs-bridge.js"; import { sanitizeImageBlocks } from "../../tool-images.js"; import { log } from "../logger.js"; -import type { SandboxFsBridge } from "../../sandbox/fs-bridge.js"; /** * Common image file extensions for detection. diff --git a/src/agents/pi-tools.read.ts b/src/agents/pi-tools.read.ts index 3c2c640ad8..30ca5fec3e 100644 --- a/src/agents/pi-tools.read.ts +++ b/src/agents/pi-tools.read.ts @@ -1,10 +1,10 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import { createEditTool, createReadTool, createWriteTool } from "@mariozechner/pi-coding-agent"; import type { AnyAgentTool } from "./pi-tools.types.js"; +import type { SandboxFsBridge } from "./sandbox/fs-bridge.js"; import { detectMime } from "../media/mime.js"; import { assertSandboxPath } from "./sandbox-paths.js"; import { sanitizeToolResultImages } from "./tool-images.js"; -import type { SandboxFsBridge } from "./sandbox/fs-bridge.js"; // NOTE(steipete): Upstream read now does file-magic MIME detection; we keep the wrapper // to normalize payloads and sanitize oversized images before they hit providers. @@ -278,7 +278,7 @@ export function createSandboxedReadTool(params: SandboxToolParams) { const base = createReadTool(params.root, { operations: createSandboxReadOperations(params), }) as unknown as AnyAgentTool; - return wrapSandboxPathGuard(createMoltbotReadTool(base), params.root); + return wrapSandboxPathGuard(createOpenClawReadTool(base), params.root); } export function createSandboxedWriteTool(params: SandboxToolParams) { diff --git a/src/agents/pi-tools.workspace-paths.e2e.test.ts b/src/agents/pi-tools.workspace-paths.e2e.test.ts index 3f89fcb69d..ea53e691ac 100644 --- a/src/agents/pi-tools.workspace-paths.e2e.test.ts +++ b/src/agents/pi-tools.workspace-paths.e2e.test.ts @@ -3,9 +3,6 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import { createOpenClawCodingTools } from "./pi-tools.js"; - -import { describe, expect, it } from "vitest"; -import { createMoltbotCodingTools } from "./pi-tools.js"; import { createHostSandboxFsBridge } from "./test-helpers/host-sandbox-fs-bridge.js"; vi.mock("../infra/shell-env.js", async (importOriginal) => { diff --git a/src/agents/sandbox/context.ts b/src/agents/sandbox/context.ts index 2d3251e373..b82c3bcc83 100644 --- a/src/agents/sandbox/context.ts +++ b/src/agents/sandbox/context.ts @@ -9,11 +9,11 @@ import { DEFAULT_AGENT_WORKSPACE_DIR } from "../workspace.js"; import { ensureSandboxBrowser } from "./browser.js"; import { resolveSandboxConfigForAgent } from "./config.js"; import { ensureSandboxContainer } from "./docker.js"; +import { createSandboxFsBridge } from "./fs-bridge.js"; import { maybePruneSandboxes } from "./prune.js"; import { resolveSandboxRuntimeStatus } from "./runtime-status.js"; import { resolveSandboxScopeKey, resolveSandboxWorkspaceDir } from "./shared.js"; import { ensureSandboxWorkspace } from "./workspace.js"; -import { createSandboxFsBridge } from "./fs-bridge.js"; export async function resolveSandboxContext(params: { config?: OpenClawConfig; diff --git a/src/agents/sandbox/docker.ts b/src/agents/sandbox/docker.ts index 4125b94631..5223e6edcf 100644 --- a/src/agents/sandbox/docker.ts +++ b/src/agents/sandbox/docker.ts @@ -38,7 +38,9 @@ export function execDockerRaw( const signal = opts?.signal; const handleAbort = () => { - if (aborted) return; + if (aborted) { + return; + } aborted = true; child.kill("SIGTERM"); }; @@ -58,12 +60,16 @@ export function execDockerRaw( }); child.on("error", (error) => { - if (signal) signal.removeEventListener("abort", handleAbort); + if (signal) { + signal.removeEventListener("abort", handleAbort); + } reject(error); }); child.on("close", (code) => { - if (signal) signal.removeEventListener("abort", handleAbort); + if (signal) { + signal.removeEventListener("abort", handleAbort); + } const stdout = Buffer.concat(stdoutChunks); const stderr = Buffer.concat(stderrChunks); if (aborted || signal?.aborted) { @@ -98,7 +104,6 @@ export function execDockerRaw( }); } -import { defaultRuntime } from "../../runtime.js"; import { formatCliCommand } from "../../cli/command-format.js"; import { defaultRuntime } from "../../runtime.js"; import { computeSandboxConfigHash } from "./config-hash.js"; @@ -281,9 +286,7 @@ export function buildSandboxCreateArgs(params: { if (typeof params.cfg.cpus === "number" && params.cfg.cpus > 0) { args.push("--cpus", String(params.cfg.cpus)); } - for (const [name, value] of Object.entries(params.cfg.ulimits ?? {}) as Array< - [string, string | number | { soft?: number; hard?: number }] - >) { + for (const [name, value] of Object.entries(params.cfg.ulimits ?? {})) { const formatted = formatUlimitValue(name, value); if (formatted) { args.push("--ulimit", formatted); diff --git a/src/agents/sandbox/fs-bridge.test.ts b/src/agents/sandbox/fs-bridge.test.ts index 521bb0d060..c956bfd6a4 100644 --- a/src/agents/sandbox/fs-bridge.test.ts +++ b/src/agents/sandbox/fs-bridge.test.ts @@ -4,9 +4,9 @@ vi.mock("./docker.js", () => ({ execDockerRaw: vi.fn(), })); +import type { SandboxContext } from "./types.js"; import { execDockerRaw } from "./docker.js"; import { createSandboxFsBridge } from "./fs-bridge.js"; -import type { SandboxContext } from "./types.js"; const mockedExecDockerRaw = vi.mocked(execDockerRaw); diff --git a/src/agents/sandbox/fs-bridge.ts b/src/agents/sandbox/fs-bridge.ts index e5ba40d1c3..e7d0d12a16 100644 --- a/src/agents/sandbox/fs-bridge.ts +++ b/src/agents/sandbox/fs-bridge.ts @@ -1,7 +1,6 @@ import path from "node:path"; - -import { resolveSandboxPath } from "../sandbox-paths.js"; import type { SandboxContext, SandboxWorkspaceAccess } from "./types.js"; +import { resolveSandboxPath } from "../sandbox-paths.js"; import { execDockerRaw, type ExecDockerRawResult } from "./docker.js"; type RunCommandOptions = { @@ -244,9 +243,15 @@ function resolveSandboxFsPath(params: { } function coerceStatType(typeRaw?: string): "file" | "directory" | "other" { - if (!typeRaw) return "other"; + if (!typeRaw) { + return "other"; + } const normalized = typeRaw.trim().toLowerCase(); - if (normalized.includes("directory")) return "directory"; - if (normalized.includes("file")) return "file"; + if (normalized.includes("directory")) { + return "directory"; + } + if (normalized.includes("file")) { + return "file"; + } return "other"; } diff --git a/src/agents/test-helpers/host-sandbox-fs-bridge.ts b/src/agents/test-helpers/host-sandbox-fs-bridge.ts index 57827e689d..4f3dc6bd8c 100644 --- a/src/agents/test-helpers/host-sandbox-fs-bridge.ts +++ b/src/agents/test-helpers/host-sandbox-fs-bridge.ts @@ -1,8 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; - -import { resolveSandboxPath } from "../sandbox-paths.js"; import type { SandboxFsBridge, SandboxFsStat, SandboxResolvedPath } from "../sandbox/fs-bridge.js"; +import { resolveSandboxPath } from "../sandbox-paths.js"; export function createHostSandboxFsBridge(rootDir: string): SandboxFsBridge { const root = path.resolve(rootDir); @@ -65,7 +64,9 @@ export function createHostSandboxFsBridge(rootDir: string): SandboxFsBridge { mtimeMs: stats.mtimeMs, } satisfies SandboxFsStat; } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return null; + } throw error; } }, diff --git a/src/agents/tools/image-tool.e2e.test.ts b/src/agents/tools/image-tool.e2e.test.ts index 152b8fd177..2a9a181533 100644 --- a/src/agents/tools/image-tool.e2e.test.ts +++ b/src/agents/tools/image-tool.e2e.test.ts @@ -3,8 +3,8 @@ import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; -import { __testing, createImageTool, resolveImageModelConfigForTool } from "./image-tool.js"; import { createHostSandboxFsBridge } from "../test-helpers/host-sandbox-fs-bridge.js"; +import { __testing, createImageTool, resolveImageModelConfigForTool } from "./image-tool.js"; async function writeAuthProfiles(agentDir: string, profiles: unknown) { await fs.mkdir(agentDir, { recursive: true }); diff --git a/src/agents/tools/image-tool.ts b/src/agents/tools/image-tool.ts index 48417fad65..9b08a0d19e 100644 --- a/src/agents/tools/image-tool.ts +++ b/src/agents/tools/image-tool.ts @@ -1,5 +1,8 @@ +import { type Api, type Context, complete, type Model } from "@mariozechner/pi-ai"; +import { Type } from "@sinclair/typebox"; import path from "node:path"; import type { OpenClawConfig } from "../../config/config.js"; +import type { SandboxFsBridge } from "../sandbox/fs-bridge.js"; import type { AnyAgentTool } from "./common.js"; import { resolveUserPath } from "../../utils.js"; import { loadWebMedia } from "../../web/media.js"; @@ -9,9 +12,8 @@ import { minimaxUnderstandImage } from "../minimax-vlm.js"; import { getApiKeyForModel, requireApiKey, resolveEnvApiKey } from "../model-auth.js"; import { runWithImageModelFallback } from "../model-fallback.js"; import { resolveConfiguredModelRef } from "../model-selection.js"; -import { ensureMoltbotModelsJson } from "../models-config.js"; -import type { AnyAgentTool } from "./common.js"; -import type { SandboxFsBridge } from "../sandbox/fs-bridge.js"; +import { ensureOpenClawModelsJson } from "../models-config.js"; +import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js"; import { coerceImageAssistantText, coerceImageModelConfig, @@ -207,7 +209,9 @@ async function resolveSandboxedImagePath(params: { filePath: candidateRel, cwd: params.sandbox.root, }); - if (!stat) throw err; + if (!stat) { + throw err; + } } catch { throw err; } @@ -398,8 +402,12 @@ export function createImageTool(options?: { } const resolvedImage = (() => { - if (sandboxConfig) return imageRaw; - if (imageRaw.startsWith("~")) return resolveUserPath(imageRaw); + if (sandboxConfig) { + return imageRaw; + } + if (imageRaw.startsWith("~")) { + return resolveUserPath(imageRaw); + } return imageRaw; })(); const resolvedPathInfo: { resolved: string; rewrittenFrom?: string } = isDataUrl diff --git a/src/web/media.test.ts b/src/web/media.test.ts index 6612dd2ed4..d1f6d4e40c 100644 --- a/src/web/media.test.ts +++ b/src/web/media.test.ts @@ -100,9 +100,9 @@ describe("web media loading", () => { const { buffer, file } = await createLargeTestJpeg(); const cap = Math.max(1, Math.floor(buffer.length * 0.8)); - await expect( - loadWebMedia(file, { maxBytes: cap, optimizeImages: false }), - ).rejects.toThrow(/Media exceeds/i); + await expect(loadWebMedia(file, { maxBytes: cap, optimizeImages: false })).rejects.toThrow( + /Media exceeds/i, + ); }); it("sniffs mime before extension when loading local files", async () => { diff --git a/src/web/media.ts b/src/web/media.ts index d084a90959..f7507223a3 100644 --- a/src/web/media.ts +++ b/src/web/media.ts @@ -25,6 +25,9 @@ export type WebMediaResult = { type WebMediaOptions = { maxBytes?: number; optimizeImages?: boolean; + ssrfPolicy?: SsrFPolicy; + /** Allowed root directories for local path reads. "any" skips the check (caller already validated). */ + localRoots?: string[] | "any"; readFile?: (filePath: string) => Promise; }; @@ -163,7 +166,13 @@ async function loadWebMediaInternal( mediaUrl: string, options: WebMediaOptions = {}, ): Promise { - const { maxBytes, optimizeImages = true, readFile: readFileOverride } = options; + const { + maxBytes, + optimizeImages = true, + ssrfPolicy, + localRoots, + readFile: readFileOverride, + } = options; // Use fileURLToPath for proper handling of file:// URLs (handles file://localhost/path, etc.) if (mediaUrl.startsWith("file://")) { try { @@ -285,35 +294,39 @@ async function loadWebMediaInternal( export async function loadWebMedia( mediaUrl: string, - options?: number | WebMediaOptions, + maxBytesOrOptions?: number | WebMediaOptions, + options?: { ssrfPolicy?: SsrFPolicy; localRoots?: string[] | "any" }, ): Promise { - if (typeof options === "number" || options === undefined) { + if (typeof maxBytesOrOptions === "number" || maxBytesOrOptions === undefined) { return await loadWebMediaInternal(mediaUrl, { - maxBytes: options, + maxBytes: maxBytesOrOptions, optimizeImages: true, + ssrfPolicy: options?.ssrfPolicy, + localRoots: options?.localRoots, }); } return await loadWebMediaInternal(mediaUrl, { - ...options, - optimizeImages: options.optimizeImages ?? true, + ...maxBytesOrOptions, + optimizeImages: maxBytesOrOptions.optimizeImages ?? true, }); } export async function loadWebMediaRaw( mediaUrl: string, - options?: number | WebMediaOptions, + maxBytesOrOptions?: number | WebMediaOptions, + options?: { ssrfPolicy?: SsrFPolicy; localRoots?: string[] | "any" }, ): Promise { - if (typeof options === "number" || options === undefined) { + if (typeof maxBytesOrOptions === "number" || maxBytesOrOptions === undefined) { return await loadWebMediaInternal(mediaUrl, { - maxBytes: options, + maxBytes: maxBytesOrOptions, optimizeImages: false, + ssrfPolicy: options?.ssrfPolicy, + localRoots: options?.localRoots, }); } return await loadWebMediaInternal(mediaUrl, { - ...options, + ...maxBytesOrOptions, optimizeImages: false, - ssrfPolicy: options?.ssrfPolicy, - localRoots: options?.localRoots, }); }