Files
api_health/agent/infra/restricted_fs_backend.js
2026-09-09 17:36:45 +08:00

97 lines
3.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import path from "path";
import { FilesystemBackend } from "deepagents";
/**
* 受限文件系统后端:在 FilesystemBackend 基础上收紧 LLM 文件工具的访问范围。
*
* 相比原生非 virtualMode 模式的两个安全缺口:
* 1. 原生 resolvePath 对绝对路径原样放行(可读磁盘任意文件),此处强制锁定在 rootDir 内;
* 2. 沙箱根下的敏感文件(conf.json、.env*、logs、dataDir)默认禁止访问,
* 防止 LLM 通过 read_file/grep 读取密钥、数据库连接串及其他用户记忆。
*
* 注意:memoryAGENTS.md)与 skillsSKILL.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 专用的受限后端,自动把 dataDirconf.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 });
}