mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 11:24:58 +00:00
Sandbox: honor bind mounts in file tools
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import path from "node:path";
|
||||
import type { SandboxContext, SandboxWorkspaceAccess } from "./types.js";
|
||||
import { resolveSandboxPath } from "../sandbox-paths.js";
|
||||
import { execDockerRaw, type ExecDockerRawResult } from "./docker.js";
|
||||
import {
|
||||
buildSandboxFsMounts,
|
||||
resolveSandboxFsPathWithMounts,
|
||||
type SandboxResolvedFsPath,
|
||||
} from "./fs-paths.js";
|
||||
|
||||
type RunCommandOptions = {
|
||||
args?: string[];
|
||||
@@ -55,17 +58,20 @@ export function createSandboxFsBridge(params: { sandbox: SandboxContext }): Sand
|
||||
|
||||
class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
private readonly sandbox: SandboxContext;
|
||||
private readonly mounts: ReturnType<typeof buildSandboxFsMounts>;
|
||||
|
||||
constructor(sandbox: SandboxContext) {
|
||||
this.sandbox = sandbox;
|
||||
this.mounts = buildSandboxFsMounts(sandbox);
|
||||
}
|
||||
|
||||
resolvePath(params: { filePath: string; cwd?: string }): SandboxResolvedPath {
|
||||
return resolveSandboxFsPath({
|
||||
sandbox: this.sandbox,
|
||||
filePath: params.filePath,
|
||||
cwd: params.cwd,
|
||||
});
|
||||
const target = this.resolveResolvedPath(params);
|
||||
return {
|
||||
hostPath: target.hostPath,
|
||||
relativePath: target.relativePath,
|
||||
containerPath: target.containerPath,
|
||||
};
|
||||
}
|
||||
|
||||
async readFile(params: {
|
||||
@@ -73,7 +79,7 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
cwd?: string;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<Buffer> {
|
||||
const target = this.resolvePath(params);
|
||||
const target = this.resolveResolvedPath(params);
|
||||
const result = await this.runCommand('set -eu; cat -- "$1"', {
|
||||
args: [target.containerPath],
|
||||
signal: params.signal,
|
||||
@@ -89,8 +95,8 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
mkdir?: boolean;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<void> {
|
||||
this.ensureWriteAccess("write files");
|
||||
const target = this.resolvePath(params);
|
||||
const target = this.resolveResolvedPath(params);
|
||||
this.ensureWriteAccess(target, "write files");
|
||||
const buffer = Buffer.isBuffer(params.data)
|
||||
? params.data
|
||||
: Buffer.from(params.data, params.encoding ?? "utf8");
|
||||
@@ -106,8 +112,8 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
}
|
||||
|
||||
async mkdirp(params: { filePath: string; cwd?: string; signal?: AbortSignal }): Promise<void> {
|
||||
this.ensureWriteAccess("create directories");
|
||||
const target = this.resolvePath(params);
|
||||
const target = this.resolveResolvedPath(params);
|
||||
this.ensureWriteAccess(target, "create directories");
|
||||
await this.runCommand('set -eu; mkdir -p -- "$1"', {
|
||||
args: [target.containerPath],
|
||||
signal: params.signal,
|
||||
@@ -121,8 +127,8 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
force?: boolean;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<void> {
|
||||
this.ensureWriteAccess("remove files");
|
||||
const target = this.resolvePath(params);
|
||||
const target = this.resolveResolvedPath(params);
|
||||
this.ensureWriteAccess(target, "remove files");
|
||||
const flags = [params.force === false ? "" : "-f", params.recursive ? "-r" : ""].filter(
|
||||
Boolean,
|
||||
);
|
||||
@@ -139,9 +145,10 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
cwd?: string;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<void> {
|
||||
this.ensureWriteAccess("rename files");
|
||||
const from = this.resolvePath({ filePath: params.from, cwd: params.cwd });
|
||||
const to = this.resolvePath({ filePath: params.to, cwd: params.cwd });
|
||||
const from = this.resolveResolvedPath({ filePath: params.from, cwd: params.cwd });
|
||||
const to = this.resolveResolvedPath({ filePath: params.to, cwd: params.cwd });
|
||||
this.ensureWriteAccess(from, "rename files");
|
||||
this.ensureWriteAccess(to, "rename files");
|
||||
await this.runCommand(
|
||||
'set -eu; dir=$(dirname -- "$2"); if [ "$dir" != "." ]; then mkdir -p -- "$dir"; fi; mv -- "$1" "$2"',
|
||||
{
|
||||
@@ -156,7 +163,7 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
cwd?: string;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<SandboxFsStat | null> {
|
||||
const target = this.resolvePath(params);
|
||||
const target = this.resolveResolvedPath(params);
|
||||
const result = await this.runCommand('set -eu; stat -c "%F|%s|%Y" -- "$1"', {
|
||||
args: [target.containerPath],
|
||||
signal: params.signal,
|
||||
@@ -204,44 +211,27 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
});
|
||||
}
|
||||
|
||||
private ensureWriteAccess(action: string) {
|
||||
if (!allowsWrites(this.sandbox.workspaceAccess)) {
|
||||
throw new Error(
|
||||
`Sandbox workspace (${this.sandbox.workspaceAccess}) does not allow ${action}.`,
|
||||
);
|
||||
private ensureWriteAccess(target: SandboxResolvedFsPath, action: string) {
|
||||
if (!allowsWrites(this.sandbox.workspaceAccess) || !target.writable) {
|
||||
throw new Error(`Sandbox path is read-only; cannot ${action}: ${target.containerPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveResolvedPath(params: { filePath: string; cwd?: string }): SandboxResolvedFsPath {
|
||||
return resolveSandboxFsPathWithMounts({
|
||||
filePath: params.filePath,
|
||||
cwd: params.cwd ?? this.sandbox.workspaceDir,
|
||||
defaultWorkspaceRoot: this.sandbox.workspaceDir,
|
||||
defaultContainerRoot: this.sandbox.containerWorkdir,
|
||||
mounts: this.mounts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function allowsWrites(access: SandboxWorkspaceAccess): boolean {
|
||||
return access === "rw";
|
||||
}
|
||||
|
||||
function resolveSandboxFsPath(params: {
|
||||
sandbox: SandboxContext;
|
||||
filePath: string;
|
||||
cwd?: string;
|
||||
}): SandboxResolvedPath {
|
||||
const root = params.sandbox.workspaceDir;
|
||||
const cwd = params.cwd ?? root;
|
||||
const { resolved, relative } = resolveSandboxPath({
|
||||
filePath: params.filePath,
|
||||
cwd,
|
||||
root,
|
||||
});
|
||||
const normalizedRelative = relative
|
||||
? relative.split(path.sep).filter(Boolean).join(path.posix.sep)
|
||||
: "";
|
||||
const containerPath = normalizedRelative
|
||||
? path.posix.join(params.sandbox.containerWorkdir, normalizedRelative)
|
||||
: params.sandbox.containerWorkdir;
|
||||
return {
|
||||
hostPath: resolved,
|
||||
relativePath: normalizedRelative,
|
||||
containerPath,
|
||||
};
|
||||
}
|
||||
|
||||
function coerceStatType(typeRaw?: string): "file" | "directory" | "other" {
|
||||
if (!typeRaw) {
|
||||
return "other";
|
||||
|
||||
Reference in New Issue
Block a user