Files
clawdbot/src/agents/sandbox-paths.ts
2026-01-03 21:41:58 +01:00

84 lines
2.2 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
function normalizeUnicodeSpaces(str: string): string {
return str.replace(UNICODE_SPACES, " ");
}
function expandPath(filePath: string): string {
const normalized = normalizeUnicodeSpaces(filePath);
if (normalized === "~") {
return os.homedir();
}
if (normalized.startsWith("~/")) {
return os.homedir() + normalized.slice(1);
}
return normalized;
}
function resolveToCwd(filePath: string, cwd: string): string {
const expanded = expandPath(filePath);
if (path.isAbsolute(expanded)) return expanded;
return path.resolve(cwd, expanded);
}
export function resolveSandboxPath(params: {
filePath: string;
cwd: string;
root: string;
}): { resolved: string; relative: string } {
const resolved = resolveToCwd(params.filePath, params.cwd);
const rootResolved = path.resolve(params.root);
const relative = path.relative(rootResolved, resolved);
if (!relative || relative === "") {
return { resolved, relative: "" };
}
if (relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error(
`Path escapes sandbox root (${shortPath(rootResolved)}): ${params.filePath}`,
);
}
return { resolved, relative };
}
export async function assertSandboxPath(params: {
filePath: string;
cwd: string;
root: string;
}) {
const resolved = resolveSandboxPath(params);
await assertNoSymlink(resolved.relative, path.resolve(params.root));
return resolved;
}
async function assertNoSymlink(relative: string, root: string) {
if (!relative) return;
const parts = relative.split(path.sep).filter(Boolean);
let current = root;
for (const part of parts) {
current = path.join(current, part);
try {
const stat = await fs.lstat(current);
if (stat.isSymbolicLink()) {
throw new Error(`Symlink not allowed in sandbox path: ${current}`);
}
} catch (err) {
const anyErr = err as { code?: string };
if (anyErr.code === "ENOENT") {
return;
}
throw err;
}
}
}
function shortPath(value: string) {
if (value.startsWith(os.homedir())) {
return `~${value.slice(os.homedir().length)}`;
}
return value;
}