Engineering note
安全的文件与命令执行:先划边界,再赋能力
这是「Agent 工程实战」的第 11 篇。专题从一个能聊天、能调工具的 .NET Agent 出发,逐步补齐可靠性、安全、测试与发布能力。
本篇要解决的问题: 用工作区沙箱、命令白名单、超时和审计,限制 Agent 的副作用。
本章定位:本章从路径沙箱和命令 allowlist 开始建立安全边界。你会学习路径规范化、符号链接、原子写入、进程输出和超时等真实工程问题。
建议阅读方式:先通读原理,再对照当前项目源码,最后完成本章实践。安全检查必须在程序执行层完成,不能把安全寄托在 system prompt 或模型自觉上。
本章导读
本章从路径沙箱和命令 allowlist 开始建立安全边界。你会学习路径规范化、符号链接、原子写入、进程输出和超时等真实工程问题。
本章采用“源码观察 → 概念拆解 → 工程改造 → 实践验证”的顺序。示例中的接口和代码骨架用于说明设计方向,真正提交代码时应结合项目当前状态逐步落地。
11.1 路径沙箱
定义唯一根目录:
public sealed class WorkspacePathResolver
{
private readonly string _root;
public WorkspacePathResolver(string root)
=> _root = Path.GetFullPath(root);
public string Resolve(string relativePath)
{
if (Path.IsPathFullyQualified(relativePath))
throw new UnauthorizedAccessException("不允许使用绝对路径");
var full = Path.GetFullPath(Path.Combine(_root, relativePath));
if (!full.StartsWith(_root + Path.DirectorySeparatorChar,
StringComparison.OrdinalIgnoreCase)
&& !string.Equals(full, _root, StringComparison.OrdinalIgnoreCase))
throw new UnauthorizedAccessException("路径超出 workspace 范围");
return full;
}
}
真实项目还要考虑符号链接、大小限制、文件扩展名 allowlist 和并发写入。
11.2 命令 allowlist
不要把模型给出的整段字符串交给 shell。第一阶段可只允许固定程序:
var allowed = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"dotnet",
"git",
"rg"
};
更稳妥的做法是把命令封装成业务工具,例如 search_code,而不是暴露通用 execute。工具越接近业务意图,风险越容易控制。
11.3 进程工具的工程约束
需要支持:
- 标准输出和标准错误同时读取,避免管道阻塞;
- 非零退出码;
- 超时后杀死进程树;
- 输出上限,防止模型上下文被刷满;
- 工作目录限制;
- 环境变量最小化;
- 审计日志和审批记录。
11.4 本章交付物
WorkspacePathResolver;ICommandPolicy;- 默认拒绝、显式允许的命令策略;
- 文件大小、输出长度和进程超时限制;
- 安全测试:
../secret.txt、绝对路径、超大输出、长时间进程。
11.5 路径安全不是字符串前缀判断那么简单
路径安全至少包含三个阶段:规范化、边界判断、资源类型判断。
规范化用于消除 a/../b、重复分隔符和相对路径差异;边界判断确认结果仍然位于 workspace;资源类型判断确认调用者要求的是文件还是目录。
还要考虑符号链接:一个位于 workspace 内的链接可能指向 workspace 外。高安全等级的实现需要解析真实路径,或者在打开文件时使用操作系统提供的安全选项。
11.6 写入操作要避免半成品
直接写目标文件时,进程中断可能留下半截内容。可以采用临时文件加原子替换:
写入 target.tmp
刷新并关闭 target.tmp
验证文件大小或 hash
替换 target
替换前还要记录原文件是否存在,必要时保留备份。对 Agent 来说,“写成功”应该意味着文件已经完整落盘,而不是 WriteAllTextAsync 没有立即抛异常。
11.7 进程输出的两个管道
如果同时重定向 stdout 和 stderr,却只读取 stdout,stderr 缓冲区写满后,子进程可能阻塞,主程序也会一直等待。正确做法是并行读取两个流,再等待进程结束。
结果中至少应包含:
{
"exitCode": 0,
"stdout": "...",
"stderr": "",
"timedOut": false
}
11.8 练习:安全攻击用例
不要只测试正常路径。准备以下用例:workspace 外的绝对路径、多个 ..、符号链接、生成大量输出的命令、永不退出的命令、返回非零退出码的命令。每个用例都要有预期状态和日志事件。
11.9 本章产出
安全不是给工具加一句系统提示词,而是让工具执行器在程序层拒绝非法动作。完成本章后,任何模型输出都不能绕过路径、命令和资源限制。
单篇实战作业
实践:实现路径解析测试和命令策略测试;把安全用例当成回归用例提交,而不是只在本地手工尝试。
建议把作业拆成一个独立提交,并在提交说明中写清楚:改动前的行为、改动后的行为、验证命令、尚未解决的风险。安全检查必须在程序执行层完成,不能把安全寄托在 system prompt 或模型自觉上。
章节复盘
复盘问题:任何模型输出都能否穿过路径、命令、输出大小和超时限制?如果不能,继续补执行层策略。
本章的完成标准不是把所有设计一次性做完,而是能把它变成项目中的一个明确边界,并为下一章留下可验证的接口。
下一篇:第 12 篇《错误处理、超时与取消:让失败有边界》
如果你正在把 Agent 放进真实工作流,建议完成本篇的实战作业后再继续:每一步都应留下可验证的代码、测试或运行记录。