import path from "path"; import { FilesystemBackend } from "deepagents"; /** * 受限文件系统后端:在 FilesystemBackend 基础上收紧 LLM 文件工具的访问范围。 * * 相比原生非 virtualMode 模式的两个安全缺口: * 1. 原生 resolvePath 对绝对路径原样放行(可读磁盘任意文件),此处强制锁定在 rootDir 内; * 2. 沙箱根下的敏感文件(conf.json、.env*、logs、dataDir)默认禁止访问, * 防止 LLM 通过 read_file/grep 读取密钥、数据库连接串及其他用户记忆。 * * 注意:memory(AGENTS.md)与 skills(SKILL.md)也经由 backend 加载, * 均不在 deny 列表中,不受影响。 * * 注:1.11+ 的 createDeepAgent permissions 机制对真实磁盘后端不适用—— * 非虚拟模式下 ls/glob/grep 结果是 Windows 绝对路径,而权限规则强制以 "/" 开头, * 永远匹配不上(grep 会泄露 conf.json 内容)。敏感路径拦截必须保持在后端层实现。 */ export class RestrictedFilesystemBackend extends FilesystemBackend { constructor(options = {}) { super(options); this.denyPaths = (options.deny ?? ["conf.json", "logs"]).map(d => String(d).toLowerCase().replace(/\/+$/, "")); } // 判断已解析的绝对路径是否命中 deny 列表(目录条目连同其子路径一并命中) _isDenied(fullPath) { const rel = path.relative(this.cwd, fullPath); if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return false; const norm = rel.split(path.sep).join("/").toLowerCase(); // 根目录下的 .env / .env.local 等 if (norm === ".env" || norm.startsWith(".env.")) return true; return this.denyPaths.some(d => norm === d || norm.startsWith(d + "/")); } /** * 覆写路径解析:一律锁定在 rootDir 内。 * - "/" 或空串视为沙箱根(LLM 工具以 / 为根) * - 相对路径按 rootDir 解析 * - 绝对路径仅接受位于 rootDir 内的(非 virtualMode 的 ls/glob 返回的是绝对路径,需支持回传) * - 禁止 .. 与 ~ 穿越 * - 命中 deny 列表直接抛错 */ resolvePath(key) { const k = String(key); if (k.includes("..") || k.startsWith("~")) { throw new Error(`Path traversal not allowed: ${key}`); } let full; if (/^\/+$/.test(k)) { full = this.cwd; } else if (path.isAbsolute(k)) { full = path.resolve(k); } else { full = path.resolve(this.cwd, k); } const rel = path.relative(this.cwd, full); if (rel.startsWith("..") || path.isAbsolute(rel)) { throw new Error(`Path outside sandbox root: ${key}`); } if (this._isDenied(full)) { throw new Error(`Access denied: ${key}`); } return full; } // ls/glob/grep 需过滤结果,避免泄露敏感文件的存在性与内容 async ls(dirPath) { const result = await super.ls(dirPath); return { files: (result.files ?? []).filter(f => !this._isDenied(f.path)) }; } async glob(pattern, searchPath = "/") { const result = await super.glob(pattern, searchPath); return { files: (result.files ?? []).filter(f => !this._isDenied(f.path)) }; } async grep(pattern, dirPath = "/", globFilter = null, maxCount = null) { const result = await super.grep(pattern, dirPath, globFilter, maxCount); return { ...result, matches: (result.matches ?? []).filter(m => !this._isDenied(m.path)) }; } } /** * 创建 bot 专用的受限后端,自动把 dataDir(conf.json agent.dataDir)加入 deny 列表。 * dataDir 位于沙箱根之外时无需 deny(沙箱本身不可达)。 */ export function createRestrictedBackend({ rootDir, dataDir }) { const deny = ["conf.json", "logs"]; if (dataDir) { const rel = path.relative(path.resolve(rootDir), path.resolve(dataDir)); if (rel && !rel.startsWith("..") && !path.isAbsolute(rel)) { deny.push(rel.split(path.sep).join("/")); } } return new RestrictedFilesystemBackend({ rootDir, deny }); }