安全设计的核心矛盾:自主性 vs 安全性

这是《写一个 Coding Agent Harness and TUI 》系列的第 7 篇。 Agent 现在能自主执行命令和编辑文件了。越自主,越强大——也越危险。 这篇不是给 agent “加上安全功能”。这篇是分析:agent 的"自主"和"安全"之间的根本矛盾, 以及怎样的安全模型能让用户不放弃自主性的同时感觉可控。


“我信任 agent,让它全自动跑”——这句话在第一次出事故之前都是成立的。

agent 是一个能执行任意 shell 命令的程序。它不问你。它自己决定。你说"帮我重构这个模块",它执行了 15 个命令——其中可能有一个 rm -rf build/ 是合理的清理,也可能有一个 rm -rf / 是幻觉参数。

绝大多数时候 agent 不会做危险的事。但安全系统的意义不在于"绝大多数时候"——在于"那一次"。 那一次 agent 误解了你的指令。那一次 LLM 产生了幻觉 tool_call。那一次一个看似无害的 cargo clean 清掉了你花了三小时下载的依赖缓存。

安全系统需要回答三个问题,每个都有隐藏的陷阱:

  1. agent 能做什么操作? 陷阱:如果你说"什么都能做",agent 可以做 rm -rf /。如果你说"什么都不能做",agent 做不了任何事——它失去了全部价值。不是"什么能做什么不能做"——是在什么级别上区分"能"和"不能"。

  2. 每次操作需要谁确认? 陷阱:如果每次操作都需要你确认,agent 在一个简单任务里可能弹 10 次确认框——你在第 3 次之后就不再看了,无脑点"允许"。“每次确认"的安全模型是"零安全"的安全模型——因为确认疲劳让确认失去了意义。

  3. 出事了怎么追溯? 陷阱:如果 agent 改了 30 个文件后你的项目不能编译了——你不知道是哪一次修改导致的。你不知道 agent 当时在想什么。你不知道这个操作经过了审批还是自动执行的。没有审计日志的 agent = 出了事只能全盘回滚——回到 agent 介入之前。

这三个问题共享同一个根源:agent 的自主性在给你价值的同时,也把决策权从你手里拿走了。安全的本质,是把一部分决策权还给你——但只还那些你真正想掌握的,不是全部都还。

〇、元问题:自主性和安全性的零和博弈——以及它为什么不是零和的

直觉:两条线

安全性 ↑
    │  ● (高安全,低自主)  ← 每步都要确认
    │         ● (低安全,高自主)  ← 全自动
    └──────────────────────→ 自主性

直觉说这两条是反比的——自主性越高,安全性越低。但这不是真的。

真正的安全性不是"多少操作被拦截”,是"多少次拦截是有效拦截"——用户在乎的被拦截了,用户不在乎的没有被骚扰。

一个 agent 在 1 小时内调了 200 次工具。其中 195 次是 read_file、grep、cargo check——这些都是不可能造成破坏的操作。3 次是 WriteFile。2 次是 bash(cargo build —release)。

如果安全模型是"每次操作都问":用户点 200 次"允许"。前 195 次是骚扰——用户不需要确认读文件。后 5 次才是有价值的确认。有效拦截率 = 2.5%。

如果安全模型是"只拦截写操作和 shell 命令":用户点 5 次"允许"。有效拦截率 = 100%。

安全性不是"拦截了多少操作"。安全性是"拦截了百分之多少的用户真正关心的操作"。 一个好安全模型 = 最大化有效拦截率。不是最大化拦截数量。

这就是三模式的核心——区分"用户真正关心的操作"和"用户不在乎的操作",让用户只在前者上做决策。

一、三模式:YOLO、Auto、Ask——不是三个开关,是一个光谱

二进制模型(开/关)

唯一参数:—dangerously-skip-permissions。开了全自动,关了每次问。

陷阱已分析:关了 = 确认疲劳 = 零安全。开了 = 全裸。

三模式

模式读文件/grep/glob写文件/编辑文件执行命令网络请求
YOLO
Auto❓询问❓询问❓询问
Ask

YOLO 和 Ask 是两个极端。Auto 是折中——但不是"折中"那么简单。

Auto 模式的设计哲学

Auto 的默认放行列表只包含不可能造成破坏的操作

  • read_file:读文件。你的文件不会因为被读而改变
  • grep:搜索文件内容。同上
  • glob:列出文件名。同上
  • git statusgit loggit diff:查看 Git 状态。永远不会修改仓库

放行这些操作的安全论证不是"它们通常安全"——是"它们不可能不安全"。 你无法用读操作破坏一个文件系统。你无法用搜索操作删除数据。

git push 不在放行列表。因为 push 不是读操作——它修改了远程仓库的状态。所以 Auto 模式下 git push 需要确认。

Auto 模式的安全边界不是"低风险 vs 高风险"——是"读操作 vs 写操作"。 读操作放行。写操作拦截。这个边界的优势是它客观可判定——不需要启发式判断"这个命令有多危险"。cargo build 是读还是写?它会写 target/ 目录——但这是编译产物,不是源文件。Auto 模式下它应该被拦截吗?这个边界取决于你怎么定义"破坏"——这是 Auto 模式留给用户的唯一判断空间。

为什么不是"半自动(Auto)一直开着就行"

不同的场景,你对"写操作"的容忍度不同。

你在一个 Docker 容器里跑 agent——容器里没有任何重要文件。写操作可以全自动(YOLO)。你在本地开发环境跑 agent——源文件在 ~/projects/。写操作需要确认(Auto)。你在生产服务器上跑 agent——任何操作都应该确认(Ask)。

三模式不是为了"给用户更多选择"——是承认用户和 agent 之间的信任关系不是全局常量,而是上下文相关的**。不同的上下文,信任级别不同。**

grok-build: agent/src/config.rs — PermissionMode 支持 per-project 覆盖

二、文件沙箱:允许列表 vs 拒绝列表——防御 vs 围堵

两种安全哲学

拒绝列表:定义一个"危险的路径"列表(/etc/passwd~/.ssh~/.aws)。不在列表里的允许访问。

允许列表:定义一个"安全的路径"列表(~/projects//tmp)。不在列表里的拒绝访问。

拒绝列表为什么对 agent 无效

拒绝列表的维护模式是被动的、后验的。你只有在 agent 访问了某个路径、造成了问题之后,才把它加入拒绝列表。

~/.aws/credentials 是明显的密钥文件——你知道要加入拒绝列表。但 ~/.config/secret-tool/keys.db 呢?~/snap/nextcloud/current/.env 呢?你的系统上有多少个你不知道的敏感文件?

拒绝列表是一个"已知危险"列表。agent 不需要访问已知危险——它访问未知危险就够了。 你不知道的危险路径是这个列表无法覆盖的。

允许列表的力量

pub fn validate_path(work_dir: &Path, target: &Path) -> Result<PathBuf, String> {
    let canonical = target.canonicalize()
        .map_err(|e| format!("无法解析路径 '{}': {}", target.display(), e))?;

    if !canonical.starts_with(work_dir) && !is_temp_path(&canonical) {
        return Err(format!(
            "路径 '{}' 在允许范围之外。agent 只能访问工作目录 ({}) 和系统临时目录。",
            target.display(), work_dir.display()
        ));
    }
    Ok(canonical)
}

默认全部拒绝。只开放显式允许的范围。 任何新路径都需要显式加入允许列表才能被 agent 访问。

拒绝列表在被攻破。允许列表在被设计。 设计时你只需要回答"agent 应该能访问哪些路径"——一个有限的、你可以自信列举的问题。拒绝列表需要你回答"agent 不应该能访问哪些路径"——一个无限的、你永远无法完整回答的问题。

三、命令沙箱:为什么用模式匹配而不是容器

三个层次的方案

层次方案安全性部署成本
1模式匹配中等
2Docker/podman需要 Docker 环境
3seccomp (Linux)极高Linux 专属 + 内核配置

为什么选模式匹配——不是因为最好,是因为最"可用"

Docker 容器和 seccomp 都在某个维度上提供了更强的隔离:文件系统隔离(容器)、网络隔离(容器 + seccomp)、系统调用过滤(seccomp)。

但它们共享一个致命缺陷:部署门槛。 不是所有用户的机器上都装了 Docker。不是所有用户都在 Linux 上(seccomp 是 Linux 专属)。

一个只有在你满足了环境条件后才能工作的安全系统,是一个在大部分用户机器上不存在的安全系统。 用户不会因为你的 agent 推荐装 Docker 而去装 Docker。他们会在 clone 了你的项目、发现需要 Docker 时关掉终端。

模式匹配的三个层次

// 层 1:拒绝已知危险模式——直接拦截
fn is_denied(cmd: &str) -> bool {
    let denied = ["rm -rf /", "rm -rf /*", "> /dev/sda", "mkfs.", "dd if=",
                  ":(){ :|:& };:", "chmod -R 777 /"];
    denied.iter().any(|p| cmd.contains(p))
}

// 层 2:需要审批的危险模式——弹确认框
fn needs_approval(cmd: &str) -> bool {
    let cautious = ["sudo ", "chmod 777", "curl | bash", "wget -O - |",
                    "git push --force", "git push -f", "docker rm -f"];
    cautious.iter().any(|p| cmd.contains(p))
}

// 层 3:其他——放行(cargo build, npm install, git status, ...)

模式匹配不能阻止一个恶意 agent 执行 python -c "import os; os.system('rm -rf ~')"——因为模式列表里没有 python模式匹配的安全性取决于你的模式列表有多完整。 这是一个合理的批评。

但反过来看:模式匹配的列表是可见的、可审计的、可修改的。 用户打开 src/safety/command_filter.rs,看到所有拦截规则。用户加一条自己关心的规则,改一行代码。容器和 seccomp 的策略配置对大多数用户是不透明的——他们不知道怎么修改 seccomp profile,怎么在 Dockerfile 里限制可执行命令。

安全性不是绝对属性——是"你理解的程度"和"你能控制的程度"的乘积。 一个你完全理解但只有 80% 覆盖率的安全系统,比一个你完全不理解但宣称 99% 覆盖率的安全系统更有实际价值。

四、审计日志:JSON Lines vs SQLite——不是存储格式,是访问模式

SQLite 在这个场景下是对的——但不必要

审计日志记录每一次工具调用:时间、工具名、参数、结果、是否通过审批。SQLite 提供了:精确查询(“找出所有被拒绝的 WriteFile 操作”)、索引、事务。

但这些都不是审计日志的访问模式。 审计日志的实际使用方式是:

  1. “出事了,回溯 agent 最近 5 分钟做了什么” → tail -100 audit.log
  2. “统计 agent 今天执行了多少次 bash” → grep -c '"tool":"bash"' audit.log
  3. “看看有没有奇怪的 sudo 操作” → grep sudo audit.log

tail + grep 就够了。 SQLite 的多余能力(索引、事务、关系查询)不是免费的——它们增加了代码依赖(rusqlitesqlx),增加了文件格式复杂度(SQLite 文件不是人类可读的),增加了调试难度(你不能用 less 看 SQLite 文件)。

JSON Lines 的附加优势

轮转简单。 mv audit.log audit.$(date +%Y%m%d).log。SQLite 的备份需要用 .backup 命令。

监控友好。 一个后台脚本 tail -f audit.log | while read line; do alert_if_suspicious "$line"; done 可以实时监控 agent 操作。SQLite 需要轮询 SELECT 或写 trigger。

人类可读。 用户打开 audit.log 能看到每一行是一个独立的 JSON 操作记录——不需要 sqlite3 CLI 工具。这对一个开源项目尤其重要——用户不需要额外的工具就能验证 agent 的操作历史。

什么时候 SQLite 是对的

跨会话记忆(第 8 篇 Memory 系统)。 记忆的访问模式是"查询所有 user_preference: 开头的 key"或"找到最近 7 天的高频关键词"。这需要索引 + 结构查询——SQLite 的价值在这里体现。审计日志的访问模式是"追加 + 偶尔全文扫描"——不需要这些能力。

五、总结:安全不是"加一层保护"——是"定义人机信任的动态边界"

层面核心问题我们的答案为什么
权限模式agent 每次操作要不要确认三模式(YOLO/ Auto/ Ask)有效拦截率 > 拦截数量
文件沙箱agent 能访问哪些文件允许列表默认拒绝 > 已知危险
命令沙箱agent 能执行什么命令模式匹配可理解 > 全覆盖
审批流危险操作谁来拍板用户(TUI 弹窗)决策权回归用户
审计日志出了事怎么追溯JSON Lines 追加tail+grep > SQLite 索引

每一个设计都在回答同一个问题:用户把决策权交给 agent 之后,怎么在需要的时候把决策权拿回来——而且只拿回来那些用户真正想掌握的,不是全部。

完整代码在 tutorial-code/part-07-security/


🏢 企业级安全方案? 本系列的安全模型是基础架构。生产环境还需要团队策略下发、审批工作流集成(飞书/钉钉/企微)。扫码联系咨询。

下一篇:扩展系统的哲学——Skills、Subagent、MCP、Memory:每个扩展都在能力与复杂度之间做交易