refactor(agents): centralize sandbox media and fs policy helpers

This commit is contained in:
Peter Steinberger
2026-02-24 02:30:45 +00:00
parent 207ec7cfae
commit ce02ad9643
8 changed files with 178 additions and 198 deletions

View File

@@ -6,13 +6,8 @@ import type { OpenClawConfig } from "../../config/config.js";
import type { ModelDefinitionConfig } from "../../config/types.models.js";
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
import { createOpenClawCodingTools } from "../pi-tools.js";
import type { SandboxContext } from "../sandbox.js";
import type { SandboxFsBridge, SandboxResolvedPath } from "../sandbox/fs-bridge.js";
import {
createHostSandboxFsBridge,
createSandboxFsBridgeFromResolver,
} from "../test-helpers/host-sandbox-fs-bridge.js";
import { createPiToolsSandboxContext } from "../test-helpers/pi-tools-sandbox-context.js";
import { createHostSandboxFsBridge } from "../test-helpers/host-sandbox-fs-bridge.js";
import { createUnsafeMountedSandbox } from "../test-helpers/unsafe-mounted-sandbox.js";
import { __testing, createImageTool, resolveImageModelConfigForTool } from "./image-tool.js";
async function writeAuthProfiles(agentDir: string, profiles: unknown) {
@@ -52,58 +47,6 @@ async function withTempWorkspacePng(
}
}
function createUnsafeMountedBridge(params: {
root: string;
agentHostRoot: string;
workspaceContainerRoot?: string;
}): SandboxFsBridge {
const root = path.resolve(params.root);
const agentHostRoot = path.resolve(params.agentHostRoot);
const workspaceContainerRoot = params.workspaceContainerRoot ?? "/workspace";
const resolvePath = (filePath: string, cwd?: string): SandboxResolvedPath => {
const hostPath =
filePath === "/agent" || filePath === "/agent/" || filePath.startsWith("/agent/")
? path.join(
agentHostRoot,
filePath === "/agent" || filePath === "/agent/" ? "" : filePath.slice("/agent/".length),
)
: path.isAbsolute(filePath)
? filePath
: path.resolve(cwd ?? root, filePath);
const relFromRoot = path.relative(root, hostPath);
const relativePath =
relFromRoot && !relFromRoot.startsWith("..") && !path.isAbsolute(relFromRoot)
? relFromRoot.split(path.sep).filter(Boolean).join(path.posix.sep)
: filePath.replace(/\\/g, "/");
const containerPath = filePath.startsWith("/")
? filePath.replace(/\\/g, "/")
: relativePath
? path.posix.join(workspaceContainerRoot, relativePath)
: workspaceContainerRoot;
return { hostPath, relativePath, containerPath };
};
return createSandboxFsBridgeFromResolver(resolvePath);
}
function createSandbox(params: {
sandboxRoot: string;
agentRoot: string;
fsBridge: SandboxFsBridge;
}): SandboxContext {
return createPiToolsSandboxContext({
workspaceDir: params.sandboxRoot,
agentWorkspaceDir: params.agentRoot,
workspaceAccess: "rw",
fsBridge: params.fsBridge,
tools: { allow: [], deny: [] },
});
}
function stubMinimaxOkFetch() {
const fetch = vi.fn().mockResolvedValue({
ok: true,
@@ -569,8 +512,7 @@ describe("image tool implicit imageModel config", () => {
await fs.mkdir(sandboxRoot, { recursive: true });
await fs.writeFile(path.join(agentDir, "secret.png"), Buffer.from(ONE_PIXEL_PNG_B64, "base64"));
const bridge = createUnsafeMountedBridge({ root: sandboxRoot, agentHostRoot: agentDir });
const sandbox = createSandbox({ sandboxRoot, agentRoot: agentDir, fsBridge: bridge });
const sandbox = createUnsafeMountedSandbox({ sandboxRoot, agentRoot: agentDir });
const fetch = stubMinimaxOkFetch();
const cfg: OpenClawConfig = {
...createMinimaxImageConfig(),

View File

@@ -1,4 +1,3 @@
import path from "node:path";
import { type Api, type Context, complete, type Model } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import type { OpenClawConfig } from "../../config/config.js";
@@ -12,8 +11,12 @@ import { runWithImageModelFallback } from "../model-fallback.js";
import { resolveConfiguredModelRef } from "../model-selection.js";
import { ensureOpenClawModelsJson } from "../models-config.js";
import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
import { assertSandboxPath } from "../sandbox-paths.js";
import {
resolveSandboxedBridgeMediaPath,
type SandboxedBridgeMediaPathConfig,
} from "../sandbox-media-paths.js";
import type { SandboxFsBridge } from "../sandbox/fs-bridge.js";
import type { ToolFsPolicy } from "../tool-fs-policy.js";
import { normalizeWorkspaceDir } from "../workspace-dir.js";
import type { AnyAgentTool } from "./common.js";
import {
@@ -208,57 +211,8 @@ function buildImageContext(
type ImageSandboxConfig = {
root: string;
bridge: SandboxFsBridge;
workspaceOnly?: boolean;
};
async function resolveSandboxedImagePath(params: {
sandbox: ImageSandboxConfig;
imagePath: string;
}): Promise<{ resolved: string; rewrittenFrom?: string }> {
const normalize = (p: string) => (p.startsWith("file://") ? p.slice("file://".length) : p);
const filePath = normalize(params.imagePath);
try {
const resolved = params.sandbox.bridge.resolvePath({
filePath,
cwd: params.sandbox.root,
});
if (params.sandbox.workspaceOnly) {
await assertSandboxPath({
filePath: resolved.hostPath,
cwd: params.sandbox.root,
root: params.sandbox.root,
});
}
return { resolved: resolved.hostPath };
} catch (err) {
const name = path.basename(filePath);
const candidateRel = path.join("media", "inbound", name);
try {
const stat = await params.sandbox.bridge.stat({
filePath: candidateRel,
cwd: params.sandbox.root,
});
if (!stat) {
throw err;
}
} catch {
throw err;
}
const out = params.sandbox.bridge.resolvePath({
filePath: candidateRel,
cwd: params.sandbox.root,
});
if (params.sandbox.workspaceOnly) {
await assertSandboxPath({
filePath: out.hostPath,
cwd: params.sandbox.root,
root: params.sandbox.root,
});
}
return { resolved: out.hostPath, rewrittenFrom: filePath };
}
}
async function runImagePrompt(params: {
cfg?: OpenClawConfig;
agentDir: string;
@@ -352,7 +306,7 @@ export function createImageTool(options?: {
agentDir?: string;
workspaceDir?: string;
sandbox?: ImageSandboxConfig;
workspaceOnly?: boolean;
fsPolicy?: ToolFsPolicy;
/** If true, the model has native vision capability and images in the prompt are auto-injected */
modelHasVision?: boolean;
}): AnyAgentTool | null {
@@ -459,12 +413,12 @@ export function createImageTool(options?: {
const maxBytesMb = typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined;
const maxBytes = pickMaxBytes(options?.config, maxBytesMb);
const sandboxConfig =
const sandboxConfig: SandboxedBridgeMediaPathConfig | null =
options?.sandbox && options?.sandbox.root.trim()
? {
root: options.sandbox.root.trim(),
bridge: options.sandbox.bridge,
workspaceOnly: options.workspaceOnly === true,
workspaceOnly: options.fsPolicy?.workspaceOnly === true,
}
: null;
@@ -524,9 +478,10 @@ export function createImageTool(options?: {
const resolvedPathInfo: { resolved: string; rewrittenFrom?: string } = isDataUrl
? { resolved: "" }
: sandboxConfig
? await resolveSandboxedImagePath({
? await resolveSandboxedBridgeMediaPath({
sandbox: sandboxConfig,
imagePath: resolvedImage,
mediaPath: resolvedImage,
inboundFallbackDir: "media/inbound",
})
: {
resolved: resolvedImage.startsWith("file://")