
书名:《Agent Runtime 工程化:生产级 AI Agent 架构、运行时与工程实践》
作者:陈堂会
平台:极客日志(https://zeeklog.com)
X:@Megick_com
联系邮箱:[email protected]
章节:第七章 权限与安全边界
第七章 权限与安全边界
能力越大,边界越要清楚。本章谈权限和安全。
Agent 的能力来自工具,风险也来自工具。一个不会调用工具的模型,最多说错话;一个能执行命令、写文件、联网、访问数据库的 Agent,可能造成真实损失。Runtime 的安全目标不是把 Agent 绑住,而是让它在明确边界内做事;越过边界时,停下来请人确认,或者直接拒绝。
安全不是最后加的一层'确认弹窗'。它应该贯穿工具定义、输入校验、执行策略、上下文注入、审计记录和恢复执行。很多事故不是因为系统没有任何防护,而是防护散落在 UI、工具函数和 prompt 里,彼此没有统一语义。

图 7-1:工具风险和权限矩阵。风险来自工具能力、输入参数、资源范围和执行环境的组合,而不是工具名称本身。
7.1 权限系统先回答三件事
设计 Agent Runtime 的权限系统,先回答三件事。
第一,谁要做事?通常是某个 run、某个 step、某个 tool call。权限记录不能只写'Agent 请求执行命令',要有 runId、stepId、toolCallId。
第二,要做什么?包括工具名、参数、资源、预期副作用。run_command("npm install") 和 run_command("ls src") 不能因为工具名相同就被归为一类。
第三,谁批准或拒绝?交互模式下是用户,非交互模式下是 policy engine,企业环境里还可能是组织策略或管理员预授权。
可以定义统一决策对象:
type PermissionDecision = {
callId: string;
toolName: string;
risk: RiskAssessment;
decision: "allow" | "ask" | "deny";
reason: string;
decidedBy: "policy" | "user" | "system";
expiresAt?: string;
};
type RiskAssessment = {
level: "read" | "write" | "network" | "install" | "destructive";
resources: string[];
sideEffects: string[];
confidence: "high" | "medium" | "low";
reasons: string[];
};
confidence 很有用。命令分析器无法确定行为时,应该提高风险等级,而不是乐观放行。
7.2 安全从 workspace 开始
最基本的边界是 workspace。读写文件工具必须把路径解析到工作区内,禁止 ../ 逃逸,谨慎处理符号链接,避免访问用户主目录、SSH key、浏览器 profile、系统 credential 等敏感路径。
路径检查不要只做字符串前缀。应先解析绝对路径,再判断相对关系:
import fs from "node:fs/promises";
import path from "node:path";
async function resolveWorkspacePath(root: string, inputPath: string) {
const rootReal = await fs.realpath(root);
const resolved = path.resolve(rootReal, inputPath);
const relative = path.relative(rootReal, resolved);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error("Path escapes workspace");
}
return resolved;
}
如果工具会跟随符号链接读取真实文件,还要检查 realpath 后的位置是否仍在 workspace 内。对写入工具,则建议默认禁止写入符号链接目标,除非用户明确允许。
workspace 边界还包括忽略目录。.git、node_modules、构建产物、大型缓存、密钥文件、数据库文件,默认不应进入模型上下文。需要访问时,应由工具策略显式允许。
7.3 Shell Policy 要保守、可解释
Shell 是 Agent Runtime 中最危险也最有用的工具。它能快速读项目、跑测试、生成文件,也能删除数据、泄露密钥、安装恶意依赖。
建议把命令分级:
| 等级 | 示例 | 策略 |
|---|---|---|
| safe read | pwd, ls, rg, cat, sed, wc | 可自动执行,限制路径和输出 |
| write | touch, mv, cp, apply_patch | 需要 diff 或 preview |
| network | curl, wget, package manager | 需要域名、方法和用途说明 |
| install | npm install, pip install | 需要包名、版本、lockfile 说明 |
| destructive | rm, git reset --hard, drop table | 默认拒绝或强确认 |
命令风险不能只看第一个词。python script.py 可能读文件,也可能删库;node -e 可能只是打印,也可能联网;npm test 通常是测试,但测试脚本可以做任何事。工程上先做保守分类,再逐步扩大自动允许范围。
一个命令分类器可以先返回结构化原因:
type CommandRisk = {
level: "safe_read" | "write" | "network" | "install" | "destructive";
reasons: string[];
requiresApproval: boolean;
};
function classifyCommand(command: string): CommandRisk {
const text = command.trim();
const reasons: string[] = [];
if (/\b(rm|unlink|mkfs|shutdown|reboot)\b/.test(text)) {
reasons.push("contains destructive command");
return { level: "destructive", reasons, requiresApproval: true };
}
if (/\b(curl|wget|ssh|scp|nc)\b/.test(text)) {
reasons.push("contains network command");
return { level: "network", reasons, requiresApproval: true };
}
if (/\b(npm|pnpm|yarn|pip|brew)\s+(install|add)\b/.test(text)) {
reasons.push("installs dependencies");
return { level: "install", reasons, requiresApproval: true };
}
if (/[>|;&]/.test(text)) {
reasons.push("contains shell operator");
return { level: "write", reasons, requiresApproval: true };
}
if (/^(pwd|ls|find|rg|cat|sed|wc)\b/.test(text)) {
reasons.push("known read-only command");
return { level: "safe_read", reasons, requiresApproval: false };
}
reasons.push("unknown command");
return { level: "write", reasons, requiresApproval: true };
}
这不是完美安全沙箱。它是策略入口。真正的执行还应配合工作目录限制、环境变量过滤、超时、输出上限、进程树清理和审计日志。
7.4 Secret Scanning 不只扫文件
工具结果、日志、文件片段进入模型上下文前,应经过 secret scanning。至少检查:
- API key;
- OAuth token;
- 私钥;
.env;- cloud credential;
- 数据库连接串;
- 内部域名和个人信息。
不要只扫描 read_file。rg 搜索结果、测试日志、错误栈、配置打印、命令 stdout/stderr 都可能带出 secret。
发现 secret 后,不要把原文注入模型。可以替换成 [REDACTED_SECRET],并在 trace metadata 中记录'已脱敏'。
type RedactionResult = {
text: string;
findings: {
kind: "api_key" | "private_key" | "token" | "database_url" | "unknown";
location: string;
replacement: string;
}[];
};
如果任务必须处理 secret,比如修改部署配置,应请求用户明确授权,并尽量让处理发生在本地工具中,而不是把 secret 发送给模型。
7.5 供应链风险从安装命令开始
coding agent 很容易倾向'缺包就装'。但安装依赖是供应链入口。Runtime 至少应要求:
- 展示包名和版本;
- 识别 package manager;
- 优先使用 lockfile;
- 对陌生包提高审批等级;
- 对 install scripts 保持警惕;
- 必要时使用
--ignore-scripts; - 记录 registry、lockfile diff 和执行环境。
这不是过度谨慎。Agent 自动安装依赖后,项目安全边界就从你的代码扩展到第三方包及其安装脚本。
审批 preview 应该具体:
{
"toolName": "run_command",
"command": "pnpm add zod",
"riskLevel": "install",
"preview": {
"packageManager": "pnpm",
"packages": ["zod"],
"registry": "default",
"lockfileMayChange": true,
"installScripts": "unknown"
}
}
用户看到这份 preview,才知道自己批准的不是'跑一个命令',而是'改变依赖图'。
7.6 MCP 工具的信任问题
MCP 让工具接入变容易,也让工具信任问题变复杂。一个 Server 暴露的工具描述不应被 runtime 无条件信任。规范中的工具行为注解适合帮助 UI 和模型理解工具,但不应替代本地安全策略。
MCP 工具审批建议按 Server 信任级别分层:
| Server 类型 | 默认策略 |
|---|---|
| 官方可信、本地只读 | 可自动执行只读工具 |
| 团队内部、有审计 | 按工具风险等级审批 |
| 第三方远程 | 默认需要确认 |
| 未知来源 | 只允许 discovery,调用需显式授权 |
同时,MCP 的 tools/list 结果进入模型上下文前也要做筛选。不要把几十个不相关工具全部暴露给模型,否则既浪费 token,也增加误调用概率。
MCP Server 断开或工具列表变化时,也应写入 trace。权限系统必须知道'这次调用基于哪个工具列表、哪个 schema、哪个 Server 信任等级'。
7.7 审批记录是系统事实
用户审批不能只当 UI 事件。对 runtime 来说,它是一条系统事实,每次都应写入 audit log:
{
"runId": "run_123",
"stepId": "step_7",
"toolCallId": "call_9",
"toolName": "run_command",
"riskLevel": "destructive",
"preview": "rm -rf build",
"decision": "denied",
"actor": "user",
"timestamp": "2026-08-12T12:00:00+08:00",
"expiresAt": null
}
恢复执行时,这些记录尤其重要。已经被拒绝的危险工具,不能因为进程重启就再次自动执行。已经批准的一次性操作,也不应无限期复用授权。
审批记录建议分两种:
| 类型 | 生命周期 | 示例 |
|---|---|---|
| one-shot approval | 只对当前 tool call 有效 | 批准一次 apply_patch |
| scoped approval | 对限定范围和时间有效 | 本 run 内允许 read_file 读取 src/** |
不要默认把用户一次批准扩展成永久授权。授权过宽,会让后续 resume 和自动重试变危险。
7.8 非交互模式必须有 policy
CI、后台任务、定时任务不能等待用户点按钮。非交互模式必须有明确 policy,而不是默认全放行。
示例:
{
"mode": "non_interactive",
"workspace": {
"read": ["src/**", "package.json", "README.md"],
"write": []
},
"tools": {
"list_files": "auto",
"read_file": "auto",
"search_code": "auto",
"run_command.safe_read": "auto",
"run_command.write": "deny",
"run_command.network": "deny",
"run_command.install": "deny",
"run_command.destructive": "deny"
}
}
非交互模式的策略要进入 run metadata。否则同一个任务在本地和 CI 跑出不同结果时,很难解释差异。
7.9 沙箱不是单一开关
沙箱有很多层,不要把它想成一个布尔值。
| 层级 | 作用 |
|---|---|
| 路径白名单 | 控制文件读写范围 |
| 命令策略 | 控制 shell 能力 |
| 环境变量过滤 | 避免 secret 被子进程读取 |
| 网络策略 | 控制外部访问 |
| 进程隔离 | 限制不可信代码 |
| 容器或 VM | 隔离文件系统和系统调用 |
| 审计日志 | 事后追踪和恢复依据 |
入门项目可以先实现路径白名单、命令策略和审计日志。生产系统再逐步引入容器、网络隔离、资源配额和组织策略。
关键是不要在 UI 上写'sandbox enabled'就以为安全。你要能说清楚:它隔离了什么,没有隔离什么,失败时怎样处理。
7.10 本章的实现路径
第一步,统一 RiskAssessment 和 PermissionDecision 类型。所有工具调用都先生成风险评估,再执行。
第二步,给文件工具加 workspace 解析、realpath 检查、忽略目录和 secret redaction。
第三步,给 shell 工具加命令分类、工作目录限制、环境变量过滤、超时和输出上限。
第四步,给写入、联网、安装、破坏性操作生成 preview。交互模式询问用户,非交互模式走 policy。
第五步,把所有审批、拒绝、policy 自动裁决写入 audit log,并让 checkpoint 引用这些记录。
第六步,为 MCP Server 增加信任等级和工具暴露筛选。
7.11 验收标准
完成本章后,你应该能解释为什么 Agent 不能直接执行任意 shell;能为 MCP 工具设计权限审批策略;能区分 schema 校验、权限策略和用户确认;能保证恢复执行后不会重复越权。
建议验收用例:
| 用例 | 期望结果 |
|---|---|
| 读取 workspace 外路径 | 拒绝,返回 PERMISSION_DENIED |
读取 .env | 默认拒绝或脱敏后返回 |
执行 ls src | 自动允许,限制输出长度 |
执行 curl https://example.com | 标记 network,要求确认 |
执行 npm install left-pad | 标记 install,展示依赖和 lockfile 风险 |
执行 git reset --hard | 默认拒绝或强确认 |
| 用户拒绝高风险工具后 resume | 不自动重放该工具 |
安全边界做得好,用户才敢把 Agent 放进真实工程目录。它不是'少做事',而是让每一次做事都有依据、边界和后路。
系列文章目录
- 《Agent Runtime 工程化》完整目录
- 《Agent Runtime 工程化》第一章 Agent Runtime 全景
- 《Agent Runtime 工程化》第二章 TypeScript / Node Runtime 基础
- 《Agent Runtime 工程化》第三章 最小 Agent Loop
- 《Agent Runtime 工程化》第四章 工具系统设计
- 《Agent Runtime 工程化》第五章 上下文工程
- 《Agent Runtime 工程化》第六章 MCP 与 Provider 抽象
- 《Agent Runtime 工程化》第七章 权限与安全边界
- 《Agent Runtime 工程化》第八章 Checkpoint、Resume、Rollback
- 《Agent Runtime 工程化》第九章 可观测与 Trace
- 《Agent Runtime 工程化》第十章 Eval Harness 与 CI 回归
- 《Agent Runtime 工程化》第十一章 Agent Runtime 进阶学习路线
- 《Agent Runtime 工程化》第十二章 hermes-agent 产品级实战
- 《Agent Runtime 工程化》附录
