Files
clawdbot/src/agents/sandbox-paths.ts
Peter Steinberger c379191f80 chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
2026-01-14 15:02:19 +00:00

77 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;
}