Agent 的安全边界:从可见性到执行控制
echo-agent 前身为 2025 年 11 月启动的个人助理项目 fubot,最初面向长期陪伴型个人智能体,围绕认知记忆、上下文延续、用户偏好沉淀、任务闭环与持续自我优化展开。随着真实场景迭代,项目逐步形成多入口接入、统一事件模型、消息总线、Agent Loop、多模型抽象、工具调用、MCP 接入、任务调度、权限审批、运行轨迹、长期记忆和受控自演进等能力。目前已支持微信、QQ、CLI、Gateway、Webhook、Cron 等入口,服务用户超过 20 万、累计下载超过 50 万,是面向长期运行、记忆增强和可持续成长智能体的开源 Agent Runtime。

你让 Agent “帮我修复测试失败”。
它读了日志,准备运行测试,又判断依赖可能缺失,于是生成了一条安装命令。命令看起来合理,但里面可能包含网络访问、脚本执行、文件覆盖,甚至会改动用户目录里的配置。
这时真正的问题不是模型有没有“安全意识”,而是系统能不能回答:这次行动会使用哪些工具、访问哪些路径、触发哪些副作用、是否需要审批、最后在哪里执行。
这就是 Agent 安全边界要解决的问题。
问题入口
如果只靠提示词告诉模型“不要做危险操作”,系统仍然是不可靠的。
模型可能判断错,把破坏性命令当成清理缓存;也可能被网页、文档、工具输出里的注入文本诱导;更关键的是,模型不知道真实运行环境。它不知道当前执行器是否联网、路径是否敏感、通道是否可信,也不知道用户是否已经授权这类副作用。
提示词负责行为引导,安全策略负责硬边界。
对工程系统来说,安全不能停在“模型应该怎么想”,而要落到“代码允许什么发生”。Agent 的每次行动,都应该先经过系统层判断,再进入执行器。
为了不停留在抽象层面,下面以 echo-agent 的实现为例。它的安全策略不是一个总开关,而是一组从工具可见性到执行控制的分层边界。
工具可见性
第一层边界不是拦截危险动作,而是减少模型一开始能看到的动作空间。
工具越多,模型可选择的行动越多,误调用和注入攻击后的影响范围也越大。一个只需要读文件、改代码、跑测试的任务,不应该默认暴露技能安装、定时任务、外部消息发送、任意网络请求这类能力。
echo-agent 在工具发现后,会根据 ToolsConfig 做过滤:
tools = filter_tools_by_policy(config, tools)
它支持 minimal、messaging、coding、full 四类 profile。minimal 只保留最小交互和读取能力;messaging 增加消息、记忆和媒体能力;coding 增加文件写入、补丁、任务和工作流能力;full 尽量暴露全部工具,但仍受安全 profile 和 deny 约束。

仅按工具名过滤还不够。工具名会扩展,风险属性却相对稳定。echo-agent 因此引入 capabilities,把工具映射到 fs.read、fs.write、process.exec、code.exec、network.outbound、skill.install 等能力。
这解决了自定义工具和 MCP 动态工具的问题。一个工具即使不在内置名单里,只要声明了 network.outbound,在网络策略为 deny 时就应该被识别和限制。
def tool_capabilities(tool_or_name):
declared = getattr(tool_or_name, "capabilities", None)
if declared:
return frozenset(declared)
name = tool_name(tool_or_name)
if name.startswith("mcp_"):
return frozenset({"mcp.call"})
return TOOL_CAPABILITIES.get(name, frozenset())
这层边界的原则很简单:不是把所有能力交给模型,再期待它谨慎使用,而是让模型只看到当前任务所需的能力。
风险信号
工具可见性只能减少入口,不能替代执行前判断。因为同一个工具在不同参数下风险不同,同一个命令在不同环境里后果也不同。
echo-agent 用 risk_classifier.py 把工具调用分为四类:
| 风险级别 | 典型工具 | 策略含义 |
|---|---|---|
read_only | 读文件、列目录、搜索、网页读取 | 通常可自动执行,但仍受路径和网络策略约束 |
write | 写文件、补丁、记忆、消息、图片生成 | 有副作用,需要更强上下文判断 |
exec | shell 命令、代码执行、后台进程 | 进入执行控制和审批路径 |
dangerous | 定时任务、技能安装、技能管理 | 默认更保守,常需显式授权 |
分类入口的优先级也很克制:先查静态风险表,再读取工具声明的 risk_level,未知工具默认视为 write。未知不等于安全,未知意味着系统没有足够信息自动放行。
网络策略是另一条关键风险信号。书稿里的默认值是:
network_policy: Literal["allow", "deny", "restricted"] = "deny"
网络动作可能从很多路径发生:web_fetch、shell 里的 curl / wget / ssh,Python 里的 requests,Node 里的 http,甚至通过 MCP 工具转发出去。因此只禁用 web 工具是不够的,工具暴露、runtime guard 和 executor 层都要重复检查。
会调用工具只说明有行动接口;能否托付给 Agent,要看工具调用是否进入闭环、是否受权限约束、是否能被追踪和复盘。
执行前 Guard
真正接近执行边界的是 runtime guard。
echo-agent 用 GuardDecision 表达执行前判断结果:allow、ask、deny。这不是一个布尔值,因为安全决策不是简单的“能不能”。有些动作可以自动执行,有些动作需要审批,有些动作必须硬阻断。

GuardDecision 还带有 reason、pattern_key 和 approval_action。这让系统不仅知道结果,还知道为什么命中规则。比如 network_denied、allowlist_miss、recursive_delete 都可以成为后续审批、审计和测试的稳定标识。
@dataclass(frozen=True) class GuardDecision: action: GuardAction = "allow" reason: str = "" pattern_key: str = "" approval_action: str = "" @property def needs_approval(self) -> bool: return self.action == "ask"
Shell guard 分两类规则。硬阻断包括根目录破坏性删除、块设备写入、文件系统格式化、系统关机、敏感账号文件和根凭证路径。这些操作即使模型或用户在普通任务里提出,也不应通过常规工具执行。
需要审批的模式包括递归删除、chmod 777、root ownership 修改、服务控制、大范围 kill、curl | sh、inline interpreter、疑似凭证文件写入、破坏性 SQL。这些动作不一定永远禁止,但不能静默执行。
代码执行 guard 关注敏感系统文件访问、网络访问、动态执行和进程启动。代码比普通 shell 更危险的一点在于,它可以动态拼接命令、读取文件、创建子进程,再把行为隐藏在语言运行时内部。
路径策略则处理文件系统边界。echo-agent 当前不是简单的 restrict-to-workspace,而是 default-allow 加显式 denylist:允许 Agent 操作用户目录,比如 Desktop、Documents,同时保护凭证、系统文件、设备文件和内部缓存。
这个取舍很工程化。只允许 workspace 内写入更容易推理,但会让个人 CLI 场景很受限;默认允许用户目录更实用,但必须坚决阻断 ~/.ssh、~/.aws、~/.kube、/etc/passwd、/etc/shadow、/dev/zero、/proc/self/fd/0 这类路径。必要时,还可以用 safe_write_root 把写入范围收紧到指定目录。
统一入口
这些规则最终要在工具调用前汇合。书稿中的统一静态 guard 入口是 evaluate_tool_call。
它会读取执行策略和网络策略,再根据工具类型进入不同检查:exec 走 shell command guard;execute_code 走 code execution guard;process start 按 shell 命令评估;网络策略为 deny 时,web_fetch 和 web_search 会被拒绝;其他工具先默认 allow,后续再进入风险分类和审批策略。
可以把它理解成下面这段最小伪代码:
def evaluate_tool_call(config, tool_name, arguments):
if tool_name == "exec":
if not config.tools.exec.enabled:
return GuardDecision("deny", "exec tool is disabled")
return evaluate_shell_command(config, arguments["command"])
if tool_name == "execute_code":
if not config.tools.code_exec.enabled:
return GuardDecision("deny", "code execution is disabled")
return evaluate_code_execution(config, arguments["code"])
if config.execution.network_policy == "deny":
if tool_name in {"web_fetch", "web_search"}:
return GuardDecision("deny", "network access is denied")
return GuardDecision("allow")
这段逻辑的重点是时序:工具调用先进入策略判断,再进入真实执行。不要让模型直接把字符串交给 shell,也不要让每个工具各自随手写一套安全判断。
纵深防御
Agent 安全不能依赖单点防线。提示词可能被绕过,schema 可能覆盖不了全部语义,静态扫描可能漏掉组合风险,审批也可能被误点。因此生产级 Agent 必须采用纵深防御。

| 层次 | 作用 |
|---|---|
| 工具可见性 | 先减少模型能看到的能力 |
| Schema 约束 | 让参数结构化,避免任意文本表达副作用 |
| 静态 Guard | 在执行前扫描命令、代码、路径和网络 |
| 风险分类 | 把 read-only、write、exec、dangerous 分流 |
| ApprovalGate | 综合上下文、权限、allowlist 和审批状态 |
| 执行器隔离 | 限制 cwd、网络、凭证和运行环境 |
| 审计可观测 | 记录决策、原因、审批和执行结果 |
每一层都假设上一层可能失败。模型遵守提示词很好,但不能只靠提示词;path policy 有用,但不能替代审批;审批能降低风险,但不能覆盖硬阻断。
Prompt Injection 也要放在这个框架里理解。它不是“模型被骗了”这么简单,而是不可信内容试图改变指令层级。网页、知识库、工具输出和记忆片段只能作为数据,不能绕过工具审批、路径策略和执行边界。
生产可用性
判断一个 Agent 的安全边界是否接近生产级,不能只看“有没有安全提示词”。更硬的检查项应该是这些:
| 检查项 | 可验证标准 |
|---|---|
| 工具可见性 | 是否按 profile、channel、security profile 过滤工具 |
| 能力声明 | 自定义工具是否声明 fs.write、process.exec、network.outbound |
| 风险分类 | 是否区分 read-only、write、exec、dangerous |
| 网络控制 | web 工具、shell、代码执行、executor 是否都尊重 network policy |
| 路径策略 | 是否阻断凭证目录、系统文件、设备文件和内部缓存 |
| 审批信号 | 是否能输出 allow、ask、deny,而不是只返回 true/false |
| 审计字段 | 是否记录 reason、pattern_key、审批来源和执行结果 |
| 回归测试 | 是否覆盖 network deny、allowlist miss、代码联网、敏感路径和 safe_write_root |
这些检查项背后的共同原则是:安全不是阻止 Agent 行动,而是定义哪些行动在什么条件下可接受。
读取当前项目文件通常可以接受;写入用户明确要求修改的工作区文件,在可回滚或可审查条件下可以接受;执行测试命令,在 cwd、网络和输出都受控时可以接受。删除文件、安装依赖、访问外网、创建定时任务、发送外部消息,则需要更强确认。访问凭证、系统目录、设备文件和内部缓存,应默认拒绝。
安全策略如果只会拒绝,会让 Agent 变得僵硬;如果过度宽松,又无法托付。成熟的做法是给出可解释降级:网络被禁用时使用本地资料,敏感路径写入被拒绝时改写到安全目录,危险删除被阻断时先生成删除计划,外部消息不能自动发送时先生成草稿。
小结
Agent 的安全边界不是一句“请谨慎执行”,而是一条从可见性到执行控制的链路。
工具可见性先缩小行动空间,capabilities 让系统按能力而不只按名称识别风险,network policy 在多层重复生效,risk classifier 把工具调用分流,runtime guard 在执行前判断 allow、ask、deny,path policy 保护敏感资产,executor isolation 把最终行动限制在受控环境中。
这一章只讲 ApprovalGate 之前的基础层。下一篇会继续往前走:当这些风险信号都已经产生,系统如何把它们汇总成一次真正可执行、可审批、可审计的决策。
(全篇完)
本文为 echo-agent 设计笔记系列第 15 篇。项目源码已开源至 GitHub。如果你对工业级 Agent 的工程落地感兴趣,欢迎加入技术交流群参与日常讨论。下一篇我们将探讨 《给 Agent 加一个 Approval Gate》,敬请期待。
更多推荐


所有评论(0)