feat: add per-session agent sandbox
This commit is contained in:
83
src/agents/sandbox-paths.ts
Normal file
83
src/agents/sandbox-paths.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user