openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
651 lines (650 loc) • 27.1 kB
JavaScript
import { c as normalizeOptionalLowercaseString } from "./string-coerce-1VuiLaaR.mjs";
import { t as FsSafeError } from "./fs-safe-BUXBSN-4.mjs";
import { a as openRootFile, s as readFileDescriptorBounded } from "./boundary-file-read-CCth2Hwg.mjs";
import { t as resolveIdentityPathViaExistingAncestorSync } from "./boundary-path-CAv_plX7.mjs";
import { r as isMissingPathError } from "./errno-FR4CRyya.mjs";
import { r as isPathInside } from "./path-guards-CH7noY0Z.mjs";
import "./errors-O33D01XW.mjs";
import { i as resolveSandboxHostPathViaExistingAncestor, r as normalizeSandboxHostPath, t as getSandboxHostPathPolicyKey } from "./host-paths-C5NtLpi_.mjs";
import { t as SANDBOX_FILE_IDENTITY } from "./file-mutation-identity-C_-O-u-P.mjs";
import { n as normalizeContainerPathCore, r as relativePathEscapesContainerRoot, t as isPathInsideContainerRoot } from "./path-utils-Drbu0ZHc.mjs";
import { o as resolveSandboxTmpfsMounts } from "./workspace-mounts-foAiMu-T.mjs";
import { a as resolveSandboxFsPathWithMounts, i as resolveSandboxFsMount, t as buildSandboxFsMounts } from "./fs-paths-CvBLe4BA.mjs";
import { c as parseDirectoryEntries, i as PINNED_MUTATION_ACTION_LABELS, n as parseSandboxStatMtimeMs, p as runDockerSandboxShellCommand, r as parseSandboxStatSize, s as buildPinnedMutationPlan } from "./fs-bridge-stat-parse-BcjuGmYw.mjs";
import fs from "node:fs";
import path from "node:path";
import { promisify } from "node:util";
import { GUEST_FILESYSTEM_CREATE_EXISTS_EXIT_CODE } from "@openclaw/fs-safe/guest";
//#region src/agents/sandbox/fs-bridge-path-safety.ts
function sandboxBoundaryError(action, containerPath, error) {
if (error instanceof Error && !(error instanceof FsSafeError && error.code === "not-file")) return error;
return new Error(`Sandbox boundary checks failed; cannot ${action}: ${containerPath}`, { cause: error });
}
var SandboxFsPathGuard = class {
constructor(params) {
this.mountsByContainer = params.mountsByContainer.map((mount) => ({
hostRoot: mount.hostRoot,
containerRoot: mount.containerRoot,
writable: mount.writable,
source: mount.source,
canonicalHostRoot: resolveSandboxHostPathViaExistingAncestor(mount.hostRoot)
}));
this.runCommand = params.runCommand;
this.containerOnlyMounts = params.containerOnlyMounts ?? [];
}
async assertPathChecks(checks) {
for (const check of checks) await this.assertPathSafety(check.target, check.options);
}
async assertPathSafety(target, options) {
const allowedType = options.allowedType === "file-or-directory" ? this.pathIsExistingDirectory(target.hostPath) ? "directory" : "file" : options.allowedType;
const guarded = await this.openBoundaryWithinRequiredMount(target, options.action, {
aliasPolicy: options.aliasPolicy,
allowedType
});
await this.assertGuardedPathSafety(target, {
...options,
allowedType
}, guarded);
}
async openReadableFile(target, signal, expectedPolicyPath, allowedType) {
const resolved = await this.resolveCanonicalReadTarget(target, "read files", signal);
const opened = await this.openBoundaryWithinRequiredMount(resolved.target, "read files", { allowedType: allowedType === "file-or-directory" ? this.pathIsExistingDirectory(resolved.target.hostPath) ? "directory" : "file" : allowedType });
if (!opened.ok) throw sandboxBoundaryError("read files", target.containerPath, opened.error);
try {
this.assertExpectedPolicyPath(resolved.target, opened.path, expectedPolicyPath);
if (getSandboxHostPathPolicyKey(resolved.canonicalHostPath) !== getSandboxHostPathPolicyKey(opened.path)) throw new Error(`Sandbox path is hidden by another mount: ${target.containerPath}. Use the mounted container path directly.`);
} catch (error) {
fs.closeSync(opened.fd);
throw error;
}
return {
...opened,
containerPath: resolved.target.containerPath
};
}
async resolveCanonicalEndpoint(containerPath, options) {
const canonicalPath = await this.resolveCanonicalContainerPath({
containerPath,
allowFinalSymlinkForUnlink: false,
resolveIdentity: options?.identity,
signal: options?.signal
});
const mount = resolveSandboxFsMount(this.mountsByContainer, canonicalPath, this.containerOnlyMounts, { containerOnlyAsUnmapped: options?.identity });
const relative = mount ? path.posix.relative(mount.containerRoot, canonicalPath).split("/") : [];
return {
containerPath: canonicalPath,
mount,
hostPath: mount ? path.resolve(mount.hostRoot, ...relative) : void 0,
canonicalHostPath: mount ? path.resolve(mount.canonicalHostRoot, ...relative) : void 0
};
}
async resolveFileIdentity(target, signal) {
const resolved = await this.resolveCanonicalEndpoint(target.containerPath, {
identity: true,
signal
});
return resolved.canonicalHostPath ?? `container:${resolved.containerPath}`;
}
async resolveReadPolicyPath(target, signal) {
const resolved = await this.resolveCanonicalReadTarget(target, "authorize file reads", signal);
return this.policyPathForCanonicalHostIdentity(resolved.target, resolveIdentityPathViaExistingAncestorSync(resolved.canonicalHostPath));
}
policyPathForCanonicalHostIdentity(target, observedPath) {
const canonicalHostPath = normalizeSandboxHostPath(observedPath);
const requestedMount = this.resolveMountByContainerPath(target.containerPath);
const selected = requestedMount && isPathInside(requestedMount.canonicalHostRoot, canonicalHostPath) ? requestedMount : this.mountsByContainer.toSorted((a, b) => b.canonicalHostRoot.length - a.canonicalHostRoot.length).find((mount) => isPathInside(mount.canonicalHostRoot, canonicalHostPath));
if (!selected) throw new Error(`Sandbox path escapes allowed mounts: ${target.containerPath}`);
const relativeHost = path.relative(selected.canonicalHostRoot, canonicalHostPath);
return normalizeContainerPathCore(relativeHost ? path.posix.join(selected.containerRoot, ...relativeHost.split(path.sep)) : selected.containerRoot);
}
async resolveCanonicalReadTarget(target, action, signal) {
const resolved = await this.resolveCanonicalEndpoint(target.containerPath, { signal });
if (!resolved.mount || !resolved.hostPath || !resolved.canonicalHostPath) throw new Error(`Sandbox path escapes allowed mounts; cannot ${action}: ${resolved.containerPath}`);
return {
target: {
...target,
containerPath: resolved.containerPath,
hostPath: resolved.hostPath,
writable: resolved.mount.writable
},
canonicalHostPath: resolved.canonicalHostPath
};
}
resolveRequiredMount(containerPath, action) {
const lexicalMount = this.resolveMountByContainerPath(containerPath);
if (!lexicalMount) throw new Error(`Sandbox path escapes allowed mounts; cannot ${action}: ${containerPath}`);
return lexicalMount;
}
finalizePinnedEntry(params) {
const relativeParentPath = path.posix.relative(params.mount.containerRoot, params.parentPath);
if (relativePathEscapesContainerRoot(relativeParentPath)) throw new Error(`Sandbox path escapes allowed mounts; cannot ${params.action}: ${params.targetPath}`);
return {
mountRootPath: params.mount.containerRoot,
relativeParentPath: relativeParentPath === "." ? "" : relativeParentPath,
basename: params.basename
};
}
async assertGuardedPathSafety(target, options, guarded) {
if (!guarded.ok) {
if (guarded.reason !== "path") {
if (!(guarded.reason === "io" && options.allowedType === "directory" && this.pathIsExistingDirectory(target.hostPath))) throw sandboxBoundaryError(options.action, target.containerPath, guarded.error);
}
} else fs.closeSync(guarded.fd);
const canonicalContainerPath = await this.resolveCanonicalContainerPath({
containerPath: target.containerPath,
allowFinalSymlinkForUnlink: options.aliasPolicy?.allowFinalSymlinkForUnlink === true
});
const canonicalMount = this.resolveRequiredMount(canonicalContainerPath, options.action);
if (options.requireWritable === "subtree" && this.containerOnlyMounts.some((mask) => isPathInsideContainerRoot(canonicalContainerPath, mask))) throw new Error(`Sandbox subtree contains a container-only mount; cannot ${options.action}: ${target.containerPath}. Use exec to access this mount.`);
if (options.requireWritable && (!canonicalMount.writable || options.requireWritable === "subtree" && this.mountsByContainer.some((mount) => !mount.writable && isPathInsideContainerRoot(canonicalContainerPath, mount.containerRoot)))) throw new Error(`Sandbox path is read-only; cannot ${options.action}: ${target.containerPath}`);
}
async openBoundaryWithinRequiredMount(target, action, options) {
const lexicalMount = this.resolveRequiredMount(target.containerPath, action);
return openRootFile({
absolutePath: target.hostPath,
rootPath: lexicalMount.hostRoot,
boundaryLabel: "sandbox mount root",
rejectSymlinks: false,
aliasPolicy: options?.aliasPolicy,
allowedType: options?.allowedType
});
}
resolvePinnedEntry(target, action) {
const basename = path.posix.basename(target.containerPath);
if (!basename || basename === "." || basename === "/") throw new Error(`Invalid sandbox entry target: ${target.containerPath}`);
const parentPath = normalizeContainerPathCore(path.posix.dirname(target.containerPath));
const mount = this.resolveRequiredMount(parentPath, action);
return this.finalizePinnedEntry({
mount,
parentPath,
basename,
targetPath: target.containerPath,
action
});
}
async resolveAnchoredSandboxEntry(target, action) {
const basename = path.posix.basename(target.containerPath);
if (!basename || basename === "." || basename === "/") throw new Error(`Invalid sandbox entry target: ${target.containerPath}`);
const parentPath = normalizeContainerPathCore(path.posix.dirname(target.containerPath));
const canonicalParentPath = await this.resolveCanonicalContainerPath({
containerPath: parentPath,
allowFinalSymlinkForUnlink: false
});
this.resolveRequiredMount(canonicalParentPath, action);
return {
canonicalParentPath,
basename
};
}
async resolveAnchoredPinnedEntry(target, action) {
const anchoredTarget = await this.resolveAnchoredSandboxEntry(target, action);
const mount = this.resolveRequiredMount(anchoredTarget.canonicalParentPath, action);
return this.finalizePinnedEntry({
mount,
parentPath: anchoredTarget.canonicalParentPath,
basename: anchoredTarget.basename,
targetPath: target.containerPath,
action
});
}
/**
* Resolves the canonical mutation destination so callers can authorize the
* real target before pinning. File-backed actions resolve the canonical
* parent and re-attach the requested basename; directory actions
* (`options.directory`) canonicalize the directory itself, which an
* existing alias may rename, and tolerate the mount root.
*/
async resolveCanonicalMutationTarget(target, action, options) {
if (options?.directory) {
const canonicalPath = await this.resolveCanonicalContainerPath({
containerPath: target.containerPath,
allowFinalSymlinkForUnlink: false
});
this.resolveRequiredMount(canonicalPath, action);
return canonicalPath;
}
const anchoredTarget = await this.resolveAnchoredSandboxEntry(target, action);
this.resolveRequiredMount(anchoredTarget.canonicalParentPath, action);
return anchoredTarget.canonicalParentPath === "/" ? `/${anchoredTarget.basename}` : `${anchoredTarget.canonicalParentPath}/${anchoredTarget.basename}`;
}
async resolveAnchoredPinnedDirectoryEntry(target, action) {
const containerPath = await this.resolveCanonicalContainerPath({
containerPath: target.containerPath,
allowFinalSymlinkForUnlink: false
});
return this.resolvePinnedDirectoryEntry({
...target,
containerPath
}, action);
}
resolvePinnedDirectoryEntry(target, action) {
const mount = this.resolveRequiredMount(target.containerPath, action);
const relativePath = path.posix.relative(mount.containerRoot, target.containerPath);
if (relativePathEscapesContainerRoot(relativePath)) throw new Error(`Sandbox path escapes allowed mounts; cannot ${action}: ${target.containerPath}`);
return {
mountRootPath: mount.containerRoot,
relativePath: relativePath === "." ? "" : relativePath
};
}
pathIsExistingDirectory(hostPath) {
try {
return fs.statSync(hostPath).isDirectory();
} catch {
return false;
}
}
assertExpectedPolicyPath(target, observedPolicyPath, expectedPolicyPath) {
if (expectedPolicyPath !== void 0 && this.policyPathForCanonicalHostIdentity(target, observedPolicyPath) !== normalizeContainerPathCore(expectedPolicyPath)) throw new Error(`Sandbox file changed after authorization: ${target.containerPath}`);
}
resolveMountByContainerPath(containerPath) {
const normalized = normalizeContainerPathCore(containerPath);
return resolveSandboxFsMount(this.mountsByContainer, normalized, this.containerOnlyMounts);
}
async resolveCanonicalContainerPath(params) {
const script = [
"set -eu",
"target=\"$1\"",
"allow_final=\"$2\"",
"identity=\"$3\"",
"suffix=\"\"",
"read_canonical() {",
" canonical=$(readlink -n -f -- \"$cursor\" && printf .) || return",
" canonical=${canonical%.}",
"}",
"probe=\"$target\"",
"if [ \"$allow_final\" = \"1\" ] && [ -L \"$target\" ]; then probe=${target%/*}; probe=${probe:-/}; fi",
"cursor=\"$probe\"",
"while [ ! -e \"$cursor\" ] && [ ! -L \"$cursor\" ]; do",
" parent=${cursor%/*}; parent=${parent:-/}",
" if [ \"$parent\" = \"$cursor\" ]; then break; fi",
" base=${cursor##*/}",
" suffix=\"/$base$suffix\"",
" cursor=\"$parent\"",
"done",
"if [ \"$identity\" = \"1\" ]; then",
" while ! read_canonical; do",
" parent=${cursor%/*}; parent=${parent:-/}",
" if [ \"$parent\" = \"$cursor\" ]; then canonical=\"$target\"; suffix=\"\"; break; fi",
" base=${cursor##*/}",
" suffix=\"/$base$suffix\"",
" cursor=\"$parent\"",
" done",
"else",
" read_canonical",
"fi",
"printf \"%s%s\\n\" \"$canonical\" \"$suffix\""
].join("\n");
const canonical = (await this.runCommand(script, {
args: [
params.containerPath,
params.allowFinalSymlinkForUnlink ? "1" : "0",
params.resolveIdentity ? "1" : "0"
],
signal: params.signal
})).stdout.toString("utf8").replace(/\n$/, "");
if (!canonical.startsWith("/")) throw new Error(`Failed to resolve canonical sandbox path: ${params.containerPath}`);
return normalizeContainerPathCore(canonical);
}
};
//#endregion
//#region src/agents/sandbox/fs-bridge-shell-command-plans.ts
/** Builds a stat command that anchors the path at its canonical parent before reading metadata. */
function buildStatPlan(target, anchoredTarget) {
return {
checks: [{
target,
options: { action: "stat files" }
}],
script: "set -eu\ncd -- \"$1\"\nLC_ALL=C stat -c \"%F|%s|%y\" -- \"$2\"",
args: [anchoredTarget.canonicalParentPath, anchoredTarget.basename],
allowFailure: true
};
}
//#endregion
//#region src/agents/sandbox/fs-bridge.ts
const readFileAsync = promisify(fs.readFile);
function createSandboxFsBridge(params) {
return new SandboxFsBridgeImpl(params.sandbox, params.containerOnlyMounts);
}
var SandboxFsBridgeImpl = class {
constructor(sandbox, containerOnlyMounts) {
this.sandbox = sandbox;
this.mounts = buildSandboxFsMounts(sandbox);
this.containerOnlyMounts = containerOnlyMounts ?? resolveSandboxTmpfsMounts(sandbox.docker.tmpfs).map((mount) => mount.containerPath);
const mountsByContainer = this.mounts.toSorted((a, b) => b.containerRoot.length - a.containerRoot.length);
this.pathGuard = new SandboxFsPathGuard({
mountsByContainer,
containerOnlyMounts: this.containerOnlyMounts,
runCommand: (script, options) => this.runCommand(script, options)
});
}
resolvePath(params) {
const target = this.resolveResolvedPath(params);
return {
hostPath: target.hostPath,
relativePath: target.relativePath,
containerPath: target.containerPath
};
}
get pathMappings() {
return this.mounts;
}
async [SANDBOX_FILE_IDENTITY](params) {
return this.pathGuard.resolveFileIdentity(this.resolveResolvedPath(params), params.signal);
}
async resolveReadPolicyPath(params) {
return await this.pathGuard.resolveReadPolicyPath(this.resolveResolvedPath(params), params.signal);
}
async resolvePinnedMutationTarget(params) {
const target = this.resolveResolvedPath(params);
const canonicalPath = await this.pathGuard.resolveCanonicalMutationTarget(target, PINNED_MUTATION_ACTION_LABELS[params.action], { directory: params.action === "mkdir" });
return {
policyPath: canonicalPath,
pinnedPath: canonicalPath
};
}
async readFile(params) {
return (await this.readFileWithSource(params)).data;
}
async readFileWithSource(params) {
const target = this.resolveResolvedPath(params);
return this.readPinnedFile(target, params.maxBytes, params.signal, params.expectedPolicyPath);
}
async readDirectory(params) {
const target = this.resolveResolvedPath(params);
const result = await this.runCheckedCommand({
...buildPinnedMutationPlan({
kind: "readdir",
check: {
target,
options: {
action: "list directories",
allowedType: "directory"
}
},
pinned: await this.pathGuard.resolveAnchoredPinnedDirectoryEntry(target, "list directories")
}),
signal: params.signal
});
return parseDirectoryEntries(result.stdout.toString("utf8"));
}
async copyFile(params) {
const source = this.resolveResolvedPath({
filePath: params.sourcePath,
cwd: params.cwd
});
const destination = this.resolveResolvedPath({
filePath: params.destinationPath,
cwd: params.cwd
});
this.ensureWriteAccess(destination, "copy files");
const sourceCheck = {
target: source,
options: {
action: "copy files",
allowedType: "file"
}
};
const destinationCheck = {
target: destination,
options: {
action: "copy files",
requireWritable: true
}
};
await this.runCheckedCommand({
...buildPinnedMutationPlan({
kind: "copy",
sourceCheck,
destinationCheck,
source: await this.pathGuard.resolveAnchoredPinnedEntry(source, "copy files"),
destination: await this.resolveMutationPin(destination, params.pinnedPath, "copy files"),
mkdir: params.mkdir !== false
}),
signal: params.signal
});
}
async writeFile(params) {
await this.writeFileContents(params, "write");
}
async createFileExclusive(params) {
const { result, containerPath } = await this.writeFileContents(params, "create");
if (result.code === GUEST_FILESYSTEM_CREATE_EXISTS_EXIT_CODE) return "exists";
if (result.code !== 0) throw new Error(`sandbox create failed for ${containerPath}: ${result.stderr.toString("utf8").trim()}`);
return "created";
}
async writeFileContents(params, kind) {
const action = PINNED_MUTATION_ACTION_LABELS[kind];
const target = this.resolveResolvedPath(params);
this.ensureWriteAccess(target, action);
const check = {
target,
options: {
action,
requireWritable: true
}
};
await this.pathGuard.assertPathSafety(target, check.options);
const buffer = Buffer.isBuffer(params.data) ? params.data : Buffer.from(params.data, params.encoding ?? "utf8");
const pinned = await this.resolveMutationPin(target, params.pinnedPath, action);
return {
result: await this.runCheckedCommand({
...buildPinnedMutationPlan({
kind,
check,
pinned,
mkdir: params.mkdir !== false
}),
allowFailure: kind === "create" ? true : void 0,
stdin: buffer,
signal: params.signal
}),
containerPath: target.containerPath
};
}
async mkdirp(params) {
const target = this.resolveResolvedPath(params);
this.ensureWriteAccess(target, "create directories");
const mkdirCheck = {
target,
options: {
action: "create directories",
requireWritable: true,
allowedType: "directory"
}
};
await this.runCheckedCommand({
...buildPinnedMutationPlan({
kind: "mkdirp",
check: mkdirCheck,
pinned: this.pathGuard.resolvePinnedDirectoryEntry(params.pinnedPath === void 0 ? target : this.authorizedPinnedTarget(target, params.pinnedPath, "create directories", { directory: true }), "create directories")
}),
signal: params.signal
});
}
async remove(params) {
const target = this.resolveResolvedPath(params);
this.ensureWriteAccess(target, "remove files");
const removeCheck = {
target,
options: {
action: "remove files",
requireWritable: params.recursive ? "subtree" : true,
allowedType: "file-or-directory"
}
};
await this.runCheckedCommand({
...buildPinnedMutationPlan({
kind: "remove",
check: removeCheck,
pinned: this.pathGuard.resolvePinnedEntry(params.pinnedPath === void 0 ? target : this.authorizedPinnedTarget(target, params.pinnedPath, "remove files"), "remove files"),
recursive: params.recursive,
force: params.force
}),
signal: params.signal
});
}
async rename(params) {
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");
const fromCheck = {
target: from,
options: {
action: "rename files",
requireWritable: "subtree",
allowedType: "file-or-directory"
}
};
const toCheck = {
target: to,
options: {
action: "rename files",
requireWritable: "subtree",
allowedType: "file-or-directory"
}
};
await this.runCheckedCommand({
...buildPinnedMutationPlan({
kind: "rename",
sourceCheck: fromCheck,
destinationCheck: toCheck,
source: this.pathGuard.resolvePinnedEntry(from, "rename files"),
destination: this.pathGuard.resolvePinnedEntry(to, "rename files")
}),
signal: params.signal
});
}
async stat(params) {
const target = this.resolveResolvedPath(params);
if (params.expectedPolicyPath !== void 0) {
let opened;
try {
opened = await this.pathGuard.openReadableFile(target, params.signal, params.expectedPolicyPath, "file-or-directory");
} catch (error) {
if (isMissingPathError(error)) return null;
throw error;
}
try {
return {
type: opened.stat.isDirectory() ? "directory" : "file",
size: parseSandboxStatSize(String(opened.stat.size)),
mtimeMs: Math.trunc(opened.stat.mtimeMs)
};
} finally {
fs.closeSync(opened.fd);
}
}
const resolved = await this.pathGuard.resolveCanonicalReadTarget(target, "stat files", params.signal);
const anchoredTarget = await this.pathGuard.resolveAnchoredSandboxEntry(target, "stat files");
const result = await this.runCheckedCommand({
...buildStatPlan(resolved.target, anchoredTarget),
signal: params.signal
});
if (result.code !== 0) {
const stderr = result.stderr.toString("utf8");
if (stderr.includes("No such file or directory")) return null;
const message = stderr.trim() || `stat failed with code ${result.code}`;
throw new Error(`stat failed for ${target.containerPath}: ${message}`);
}
const [typeRaw, sizeRaw, mtimeRaw] = result.stdout.toString("utf8").trim().split("|");
return {
type: coerceStatType(typeRaw),
size: parseSandboxStatSize(sizeRaw),
mtimeMs: parseSandboxStatMtimeMs(mtimeRaw)
};
}
async runCommand(script, options = {}) {
const backend = this.sandbox.backend;
const command = {
script,
...options
};
if (backend) return await backend.runShellCommand(command);
return await runDockerSandboxShellCommand({
containerName: this.sandbox.containerName,
...command
});
}
async readPinnedFile(target, maxBytes, signal, expectedPolicyPath) {
const opened = await this.pathGuard.openReadableFile(target, signal, expectedPolicyPath);
try {
let data;
if (maxBytes === void 0) data = await readFileAsync(opened.fd);
else {
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new RangeError("maxBytes must be a non-negative safe integer");
const initialStat = fs.fstatSync(opened.fd);
if (!initialStat.isFile()) throw new Error(`Sandbox read requires a regular file: ${target.containerPath}`);
if (initialStat.size > maxBytes) throw new RangeError(`File exceeds ${maxBytes} bytes`);
data = await readFileDescriptorBounded(opened.fd, maxBytes);
const finalStat = fs.fstatSync(opened.fd);
if (!finalStat.isFile() || finalStat.size > maxBytes) throw new RangeError(`File exceeds ${maxBytes} bytes`);
}
const source = resolveSandboxFsMount(this.mounts, opened.containerPath, this.containerOnlyMounts);
return {
data,
canonicalPath: opened.containerPath,
...source?.source === "workspace" ? { workspaceRelativePath: path.posix.relative(source.containerRoot, opened.containerPath) } : {}
};
} finally {
fs.closeSync(opened.fd);
}
}
async runCheckedCommand(plan) {
await this.pathGuard.assertPathChecks(plan.checks);
if (plan.recheckBeforeCommand) await this.pathGuard.assertPathChecks(plan.checks);
return await this.runCommand(plan.script, {
args: plan.args,
stdin: plan.stdin,
allowFailure: plan.allowFailure,
signal: plan.signal
});
}
ensureWriteAccess(target, action) {
if (this.sandbox.workspaceAccess === "ro" || !target.writable) throw new Error(`Sandbox path is read-only; cannot ${action}: ${target.containerPath}`);
}
resolveResolvedPath(params) {
return resolveSandboxFsPathWithMounts({
filePath: params.filePath,
cwd: params.cwd ?? this.sandbox.workspaceDir,
defaultWorkspaceRoot: this.sandbox.workspaceDir,
defaultContainerRoot: this.sandbox.containerWorkdir,
mounts: this.mounts,
containerOnlyMounts: this.containerOnlyMounts
});
}
/**
* Pins a mutation for a destination the caller already authorized via
* resolvePinnedMutationTarget. The canonical path is pinned lexically so no
* sandbox-writable path component is resolved again after authorization;
* the no-follow pinned walk then fails closed on any post-authorization
* swap instead of redirecting the mutation.
*/
async resolveMutationPin(target, pinnedPath, action) {
if (pinnedPath === void 0) return await this.pathGuard.resolveAnchoredPinnedEntry(target, action);
return this.pathGuard.resolvePinnedEntry(this.authorizedPinnedTarget(target, pinnedPath, action), action);
}
authorizedPinnedTarget(target, pinnedPath, action, options) {
const canonicalPath = normalizeContainerPathCore(pinnedPath);
if (!options?.directory && path.posix.basename(canonicalPath) !== path.posix.basename(target.containerPath)) throw new Error(`Pinned sandbox destination does not match the requested path; cannot ${action}: ${target.containerPath}`);
return {
...target,
containerPath: canonicalPath
};
}
};
function coerceStatType(typeRaw) {
const normalized = normalizeOptionalLowercaseString(typeRaw) ?? "";
if (normalized.includes("directory")) return "directory";
if (normalized.includes("file")) return "file";
return "other";
}
//#endregion
export { createSandboxFsBridge as t };