1. 为什么我要给 Agent 加一道权限门禁
给 Agent 加权限门禁这件事,最初并不是我主动想做的。起因很简单:我手上跑着一个基于 DeepSeek Harness 的自动化工作流 Agent,日常负责帮我处理一批本地文件的整理、归档和部分脚本调用。某天它在执行一个批量重命名任务时,因为路径匹配规则写得太宽,差点把一整个工作目录下的配置文件全改了。虽然最后靠备份救了回来,但那次之后我就意识到一个问题——Agent 的能力越强,它闯祸的上限就越高。
这就是权限门禁存在的意义。所谓权限门禁,本质上是在 Agent 真正执行某个动作之前,插入一层拦截和校验机制,判断这个动作是否被允许、是否需要人工确认、是否超出了预设的边界。它和传统软件的权限系统不太一样,传统权限系统管的是"谁能访问什么资源",而 Agent 权限门禁管的是"这个自主决策的实体,在当前上下文里,能不能执行这个具体操作"。后者要复杂得多,因为 Agent 的行为是动态生成的,你没法像配置静态路由表那样把所有情况都枚举出来。
我选择的是dsh-permission-rules这套规则方案,配合 DeepSeek Harness 的工作流插件体系来落地。选它的理由很直接:它足够轻,规则用声明式的方式写,不需要我改 Agent 的核心逻辑;它和 Harness 的会话日志能打通,每次拦截都有记录可查;而且它的规则粒度可以做到很细,从文件路径到命令关键字都能覆盖。但真正上手之后,我发现事情远没有想象中顺利,前后踩了四个坑,每一个都让我重新理解了"给 Agent 做权限控制"这件事的难度。
这篇文章适合两类人看:一类是正在用 DeepSeek Harness 或者类似 Agent 框架做自动化、并且开始担心安全边界的人;另一类是想了解 Agent 权限门禁到底该怎么设计、有哪些坑必须提前避开的开发者。我会把四个坑的来龙去脉、排查过程、最终解法都摊开讲,尽量让你少走我走过的弯路。全文涉及的核心概念包括dsh-permission-rules、Agent 权限门禁、DeepSeek Harness 工作流插件、会话日志这几个关键词,它们会贯穿始终。
2. 权限门禁的整体设计与方案选型思路
2.1 为什么不在 Agent 内部硬编码判断
最开始我想的方案特别朴素:直接在 Agent 执行动作的那个函数里加一堆if-else,判断路径是不是在允许列表里、命令是不是危险命令。写了两天我就放弃了,原因有三个。
第一,规则和业务逻辑耦合太深。Agent 的工作流本身就在不断迭代,今天加个新任务,明天改个执行顺序,每次改动都要回头去翻那段权限判断代码,生怕漏了某个分支。第二,规则无法复用。我有好几个 Agent 实例,跑在不同的项目上,如果每个都硬编码一套判断,维护成本直接爆炸。第三,没有审计能力。硬编码的判断最多打个日志,但日志格式混乱,想追溯"某次操作为什么被放行"几乎不可能。
所以正确的思路是把权限判断从 Agent 的执行逻辑里抽出来,做成一个独立的、声明式的规则层。Agent 在执行动作前,把"我要做什么"这个意图交给规则层,规则层返回"允许/拒绝/需要确认",Agent 再根据结果决定下一步。这就是dsh-permission-rules的核心设计哲学——关注点分离。
2.2 dsh-permission-rules 的规则模型长什么样
它的规则模型其实不复杂,一条规则大致包含几个要素:匹配目标(比如文件路径、命令、工具名)、匹配模式(支持通配符和正则)、动作(allow、deny、ask)、优先级。多条规则之间按优先级从高到低匹配,命中第一条就返回结果。
我用一个简化后的例子说明,实际配置里字段名可能略有差异:
rules: - name: block-system-paths target: file_path pattern: "^/(etc|sys|boot)/.*" action: deny priority: 100 - name: ask-for-delete target: command pattern: ".*(rm|del|rmdir).*" action: ask priority: 90 - name: allow-workspace target: file_path pattern: "^/home/user/workspace/.*" action: allow priority: 50这个模型的好处是,规则可以独立于 Agent 代码存在,改规则不用动 Agent,改 Agent 不用动规则。而且每条规则有名字,会话日志里能直接看到是哪条规则触发的拦截,排查问题时一目了然。
2.3 和 DeepSeek Harness 工作流插件的结合点
DeepSeek Harness 的工作流插件体系提供了一个很关键的扩展点:动作执行前的钩子。Agent 在真正调用工具或执行命令之前,会经过这个钩子,钩子里可以拿到当前动作的完整上下文——包括目标路径、命令内容、当前会话 ID、历史操作记录等。dsh-permission-rules就是挂在这个钩子上工作的。
这个结合方式的好处是,权限判断发生在动作真正执行之前,属于"事前拦截",而不是"事后补救"。我见过一些方案是等命令执行完了再检查日志,那种方式对于删除、覆盖这类不可逆操作来说毫无意义。事前拦截才能真的挡住风险。
另外,Harness 的会话日志会把每次权限判断的结果记录下来,包括规则名、动作、上下文摘要。这一点在后面排查问题时帮了我大忙,第四个坑就是靠会话日志才定位到的。
2.4 方案选型时我放弃的几个替代思路
在定方案之前,我也调研过其他几种做法,简单说说为什么没选。
一种是基于容器隔离,把 Agent 关进一个受限的容器里,用容器的文件系统权限来兜底。这个思路本身没问题,但它的粒度太粗,容器内的操作要么全放行要么全拒绝,没法做到"这个目录允许写、那个目录只读、删除操作要确认"这种细粒度控制。而且容器方案对命令级别的拦截基本无能为力。
另一种是基于大模型自己判断,让 Agent 在执行前先问一遍模型"这个操作安全吗"。这个方案听起来很智能,但实测下来极不稳定,模型对边界的判断经常飘忽,同一个操作换个说法结论就变了。权限这种事必须确定性,不能交给概率。
还有一种是纯人工确认,每个动作都弹窗让人点。这个方案安全是安全,但 Agent 的自动化价值就没了,我一天要点几百次确认,还不如自己手动干。所以最终还是要走规则引擎这条路,在自动化和安全之间找平衡。
3. 四个坑的完整复盘与排查过程
3.1 第一个坑:规则优先级写反,导致拦截形同虚设
第一个坑出现在规则刚上线的时候。我配了一条"允许工作目录下所有操作"的规则,优先级设成了最高,然后又配了几条"禁止危险命令"的规则,优先级设得比较低。我的直觉是"允许"应该优先,毕竟工作目录是我自己的地盘。结果上线第一天,Agent 在工作目录里执行了一个rm -rf把临时文件目录清空了,我本来是想让它弹确认的,结果直接被放行了。
排查的时候我才反应过来,规则匹配是"命中第一条就返回",优先级高的先匹配。我把 allow 规则设成最高优先级,意味着只要路径在工作目录下,后面的 deny 和 ask 规则根本没机会执行。这就像门禁系统里,你给某个人发了万能卡,那所有针对他的限制规则都失效了。
正确的做法是让限制性规则优先级高于放行规则。deny 最高,ask 次之,allow 最低。这样即使一个操作同时满足多条规则,也会优先命中限制最严的那条。我调整后的优先级顺序是这样的:
| 规则类型 | 优先级范围 | 说明 |
|---|---|---|
| deny | 90-100 | 绝对禁止,最高优先级 |
| ask | 70-89 | 需要人工确认 |
| allow | 10-69 | 放行,最低优先级 |
注意:优先级数值的具体范围不是固定的,关键是保证 deny > ask > allow 这个相对关系。有些规则引擎用数值大的优先,有些用数值小的优先,一定要先确认清楚再配。
这个坑给我的教训是,权限系统的默认姿态应该是"拒绝",而不是"允许"。放行规则永远是兜底的、优先级最低的,限制规则才是第一道防线。
3.2 第二个坑:路径匹配被符号链接绕过
第二个坑更隐蔽。我配了一条规则,禁止 Agent 访问/etc目录,模式写的是^/etc/.*。测试的时候直接访问/etc/passwd确实被拦了,我就以为没问题了。结果某天 Agent 通过一个软链接,从工作目录里绕进了/etc,规则完全没触发。
原因在于,规则匹配的是 Agent 传入的路径字符串,而不是路径解析后的真实路径。Agent 传进来的是/home/user/workspace/link-to-etc/passwd,这个字符串不匹配^/etc/.*,所以被放行了,但实际写入的位置是/etc/passwd。
解决办法是在规则匹配之前,先对路径做一次规范化处理,把符号链接解析掉,得到真实路径再匹配。具体做法是在钩子里调用路径解析函数,把realpath的结果传给规则引擎。我用的是 Python 的os.path.realpath,效果很直接:
import os def normalize_path(raw_path): # 解析符号链接,得到真实路径 real = os.path.realpath(raw_path) # 统一分隔符,避免 Windows/Linux 混用 return real.replace("\\", "/")改完之后,软链接绕过的路子就被堵死了。这个坑提醒我,任何涉及路径的权限判断,都必须基于规范化后的真实路径,否则各种链接、相对路径、大小写变体都能成为绕过手段。
3.3 第三个坑:命令匹配用正则,被参数顺序和别名坑了
第三个坑是关于命令拦截的。我配了一条规则,禁止执行rm -rf,正则写的是rm\s+-rf。测试的时候rm -rf /tmp/test确实被拦了。但后来 Agent 执行了rm -fr /tmp/test,参数顺序换了一下,正则没匹配上,直接放行了。还有一次它用了\rm这种带反斜杠的写法,也绕过了。
这个问题的根源是,用正则去匹配命令字符串,永远会有漏网之鱼。命令的写法太灵活了,参数顺序、别名、转义、管道组合,每一种都能构造出正则覆盖不到的形式。你不可能穷举所有变体。
我的解法是分两层。第一层还是正则,但把常见的危险命令模式写得更宽一些,比如rm后面跟任意包含-r或-f的参数组合都拦。第二层是命令解析,把命令字符串按 shell 语法拆成 token,拿到真正的命令名和参数列表,再基于结构化数据做判断。这样rm -fr和rm -rf解析出来是一样的。
import shlex def parse_command(cmd_str): try: tokens = shlex.split(cmd_str) except ValueError: # 解析失败,保守起见直接拦截 return None if not tokens: return None return { "command": tokens[0].lstrip("\\"), "args": tokens[1:] }解析出结构化数据后,判断"是不是 rm 命令"就看command == "rm",判断"有没有递归删除"就看 args 里有没有-r或-R。这样比正则可靠得多。当然,shell 的复杂性远不止这些,管道、子命令、变量替换都可能藏东西,所以我的策略是解析失败就拦截,宁可误伤不可放过。
3.4 第四个坑:会话日志缺失上下文,问题无法复现
第四个坑是最让我头疼的。有一段时间,Agent 偶尔会执行一些我没授权的操作,但频率很低,一天可能就一两次。我想排查,去看会话日志,发现日志里只记了"某条规则被触发"或者"某次操作被放行",但没有记录当时的完整上下文——比如 Agent 当时在做什么任务、传进来的原始参数是什么、匹配的是哪条规则。没有这些信息,我根本没法复现问题。
这个坑的本质是,日志的详细程度决定了你排查问题的能力上限。日志记少了,出了问题就是黑盒;记多了,又怕性能和数据量扛不住。我最后的做法是分级记录:正常放行的操作只记摘要(时间、规则名、动作类型),被拦截或需要确认的操作记完整上下文(原始参数、规范化后的路径、匹配的规则、会话 ID、任务描述)。这样既控制了日志量,又保证了出问题时能查到细节。
另外我还加了一个会话回放的能力,把一次会话里所有的权限判断按时间顺序串起来,能看到 Agent 的操作序列。这个在排查"为什么某次操作被放行"时特别有用,因为很多时候问题不在单条规则,而在于规则之间的组合效果。
提示:会话日志的字段设计要提前想清楚,尤其是"原始输入"和"规范化结果"这两个字段一定要分开记。我一开始只记了规范化后的结果,结果排查时发现根本不知道 Agent 原始传的是什么,又得回头改日志格式重跑。
4. 权限门禁的实操落地与关键配置
4.1 环境准备与插件挂载
落地这套方案,第一步是把dsh-permission-rules挂到 DeepSeek Harness 的工作流插件体系里。我用的环境是 Linux,Harness 版本是 0.1.5 之后的稳定版。挂载的核心是找到 Harness 提供的动作执行前钩子,把规则引擎的入口函数注册进去。
具体来说,Harness 的插件配置里有一个pre_action_hooks的字段,可以指定一个或多个钩子函数。我把权限检查函数注册进去,它接收动作上下文,返回一个决策结果。配置大概长这样:
plugins: - name: permission-gate type: pre_action_hook entry: permission_gate.check config: rules_file: /home/user/.dsh/permission_rules.yaml log_level: info fail_mode: deny这里有个关键配置fail_mode,我设成了deny。意思是如果规则引擎本身出错了——比如规则文件读不到、解析失败、超时——那就默认拒绝所有操作。这个设置很重要,权限系统必须"故障安全",不能因为自己挂了就把门敞开。
4.2 规则文件的组织与分层
规则文件我按用途分成了几个文件,而不是全塞在一个大文件里。这样改起来清晰,也方便不同项目复用。我的组织方式是这样的:
base.yaml:基础规则,所有项目通用,比如禁止系统路径、禁止危险命令project-xxx.yaml:项目专属规则,比如某个项目允许访问特定的数据目录override.yaml:临时覆盖规则,用于调试或特殊情况
加载的时候按 base → project → override 的顺序合并,后面的可以覆盖前面的同名规则。这样既保证了基础安全底线,又给了项目灵活性。
规则命名我也有个约定,用动作-目标-场景的格式,比如deny-system-path、ask-delete-file、allow-workspace-write。名字起好了,看日志的时候一眼就知道是哪条规则在起作用。
4.3 路径规则的具体写法与参数计算
路径规则是权限门禁里用得最多的一类。写路径规则有几个要点。
第一,用锚定匹配,不要用包含匹配。^/etc/.*是锚定在开头的,/etc/这种包含匹配会误伤/home/user/etc-backup/这种路径。第二,注意路径分隔符和大小写。Linux 下路径区分大小写,Windows 下不区分,跨平台的话要统一处理。第三,对目录本身和目录下内容要分别考虑。^/etc$匹配/etc本身,^/etc/.*匹配/etc下的内容,两个都要写才能覆盖完整。
我实际用的一组路径规则是这样的:
rules: - name: deny-system-dirs target: file_path pattern: "^/(etc|sys|boot|proc|dev)(/.*)?$" action: deny priority: 100 - name: deny-home-config target: file_path pattern: "^/home/[^/]+/\\.(ssh|gnupg|aws)(/.*)?$" action: deny priority: 95 - name: ask-outside-workspace target: file_path pattern: "^(?!/home/user/workspace).*$" action: ask priority: 80 - name: allow-workspace target: file_path pattern: "^/home/user/workspace(/.*)?$" action: allow priority: 50这组规则的效果是:系统目录和敏感配置目录绝对禁止,工作目录外的操作需要确认,工作目录内放行。优先级从高到低,逻辑很清晰。
4.4 命令规则的结构化判断
命令规则我前面提到要用解析而不是纯正则,这里展开说说具体怎么落地。解析之后,我主要判断几个维度:命令名是否在黑名单里、参数里是否包含危险标志、是否涉及重定向或管道。
黑名单命令我列了一个表,按危险程度分级:
| 命令 | 危险级别 | 处理动作 | 说明 |
|---|---|---|---|
| rm | 高 | ask | 删除操作,需确认 |
| dd | 高 | deny | 磁盘写入,直接禁止 |
| mkfs | 高 | deny | 格式化,直接禁止 |
| chmod | 中 | ask | 权限修改,需确认 |
| chown | 中 | ask | 属主修改,需确认 |
| curl | 中 | ask | 网络请求,需确认 |
| git | 低 | allow | 版本控制,放行 |
对于rm这种需要确认的命令,我还会进一步看参数。如果参数里有-r或-R,说明是递归删除,风险更高,确认信息里要特别标注。如果目标路径是根目录或者家目录,直接 deny 不给确认机会。
DANGEROUS_COMMANDS = { "rm": "ask", "dd": "deny", "mkfs": "deny", "chmod": "ask", "chown": "ask", } def check_command(cmd_str): parsed = parse_command(cmd_str) if parsed is None: return "deny" # 解析失败,保守拦截 cmd = parsed["command"] if cmd in DANGEROUS_COMMANDS: action = DANGEROUS_COMMANDS[cmd] # rm 带递归参数时升级为更严格的处理 if cmd == "rm" and any(a in ("-r", "-R", "--recursive") for a in parsed["args"]): return "ask" return action return "allow"4.5 会话日志的字段设计与查询
会话日志这块,我最终定的字段结构是这样的:
{ "timestamp": "2025-01-15T10:23:45Z", "session_id": "sess_abc123", "task": "整理下载目录", "action_type": "file_write", "raw_input": "/home/user/workspace/../etc/passwd", "normalized_path": "/etc/passwd", "matched_rule": "deny-system-dirs", "decision": "deny", "reason": "path matches system directory pattern" }关键字段是raw_input和normalized_path分开记,matched_rule记清楚是哪条规则做的决定,task记当时 Agent 在做什么任务。有了这些,排查问题时可以按 session_id 把一次会话的所有判断串起来看,也可以按 matched_rule 统计哪条规则触发最频繁。
查询的时候我主要用两个维度:按时间范围查异常决策,按 session_id 查完整会话。前者用来发现规律,后者用来复现具体问题。
5. 常见问题速查与避坑经验
5.1 权限门禁常见问题速查表
实际跑下来,我整理了一份常见问题速查表,遇到问题先查这个,能省不少时间:
| 问题现象 | 可能原因 | 排查方向 | 解决方法 |
|---|---|---|---|
| 规则不生效,操作被放行 | 优先级配反 | 检查 allow 规则优先级是否高于 deny | 调整优先级,deny > ask > allow |
| 软链接绕过拦截 | 未规范化路径 | 检查是否用了 realpath | 匹配前先解析真实路径 |
| 命令变体绕过 | 纯正则匹配 | 检查是否有参数顺序变体 | 改用命令解析 + 结构化判断 |
| 日志查不到上下文 | 日志字段缺失 | 检查是否记录了原始输入 | 补全 raw_input 和 matched_rule |
| 规则引擎报错后放行 | fail_mode 配置错误 | 检查 fail_mode 值 | 设为 deny,故障时默认拒绝 |
| 规则冲突,行为不确定 | 优先级相同 | 检查是否有同优先级规则 | 给每条规则唯一优先级 |
| 性能下降明显 | 规则过多或正则复杂 | 检查规则数量和正则复杂度 | 精简规则,避免回溯正则 |
5.2 我踩过的那些"想当然"的坑
除了前面四个大坑,还有一些小的"想当然"也让我吃过亏,一并说说。
一个是以为规则文件改了会立即生效。实际上有些实现是启动时加载一次,改了要重启才生效。我有次改完规则测试没生效,以为规则写错了,折腾半天才发现是没重启。后来我在规则加载逻辑里加了文件监听,改了自动重载,省心不少。
另一个是以为 deny 了就绝对安全。其实 deny 只是拦住了 Agent 通过这个钩子的操作,如果 Agent 有别的路径绕过钩子——比如直接调用底层 API——那 deny 也没用。所以权限门禁必须确保所有动作都经过钩子,不能有旁路。我在 Harness 配置里检查了一遍,确认没有其他执行入口才放心。
还有一个是以为规则越细越好。我一开始把规则写得特别细,每个具体路径、每个具体命令都单独一条。结果规则文件几百行,维护起来痛苦,而且规则之间的优先级关系变得很复杂,容易出冲突。后来我改成分层 + 通配的方式,基础规则用通配覆盖大类,特殊规则才单独写,规则数量降到了几十条,清晰多了。
5.3 给 Agent 做权限门禁的三条核心原则
踩完这些坑,我总结出三条原则,算是这套方案的精髓。
第一条,默认拒绝,显式放行。权限系统的默认姿态必须是拒绝,任何没有被明确允许的操作都应该被拦住。放行规则是例外,不是常态。这样即使规则有遗漏,也是漏在"该放行的被拦了"这一侧,而不是"该拦的放行了"这一侧。前者影响效率,后者影响安全,孰轻孰重很清楚。
第二条,基于结构化数据判断,不基于字符串。路径要规范化,命令要解析,任何基于原始字符串的匹配都可能被绕过。字符串是给人看的,结构化数据才是给程序判断的。这一步多花的功夫,在安全上是值得的。
第三条,日志要能复现问题。日志不是记给自己看的流水账,是出问题时用来复现和定位的证据。所以日志里必须包含足够的上下文——原始输入、规范化结果、匹配规则、决策理由、会话信息。缺了任何一项,排查时都可能卡住。
5.4 后续可以继续优化的方向
这套方案目前跑得还算稳,但我知道还有优化空间。一个是规则的动态调整,现在改规则要手动编辑文件,未来想做成根据 Agent 的历史行为自动推荐规则调整。另一个是决策的可解释性,现在拦截了只告诉你是哪条规则,未来想给出更自然的解释,让用户更容易理解为什么被拦。还有一个是多 Agent 场景下的规则共享,现在每个 Agent 实例的规则是独立的,未来想做成中心化的规则管理,多个 Agent 共享一套规则和日志。
不过这些都是后话,当前这套方案已经能挡住绝大多数风险了。如果你也在给 Agent 做权限控制,我的建议是先把基础规则跑起来,别追求一步到位。规则是在实际运行中慢慢打磨出来的,你不可能一开始就想到所有情况。先上线,再根据日志和实际拦截情况迭代,这样比闭门造车配一堆规则要靠谱得多。
最后分享一个我自己的小习惯:每次 Agent 出了新的"意外操作",我都会把它转化成一条新规则,然后记在一个"规则演进日志"里。时间长了,这份日志本身就是一份很好的经验积累,能看出 Agent 的行为模式和你自己的判断边界是怎么一步步清晰的。这个习惯坚持下来,比任何现成的规则模板都有价值。