fix: resolve main-rebase fallout for sandbox fs bridge

This commit is contained in:
Peter Steinberger
2026-02-13 16:25:41 +01:00
parent 22c289100d
commit 795ec6aa2f
14 changed files with 79 additions and 51 deletions

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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) => {

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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";
}

View File

@@ -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;
}
},

View File

@@ -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 });

View File

@@ -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

View File

@@ -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 () => {

View File

@@ -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<Buffer>;
};
@@ -163,7 +166,13 @@ async function loadWebMediaInternal(
mediaUrl: string,
options: WebMediaOptions = {},
): Promise<WebMediaResult> {
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<WebMediaResult> {
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<WebMediaResult> {
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,
});
}