
结论先给主流框架的拦截点LangGraph middleware、SK filter、CrewAI hook解决的是能不能拦住没解决拦截规则本身会不会被改。生产环境要在框架之上自建四层不可变授权对象frozen capability、版本化审批规则、stop-only人工通道、以及一套验证真的不可变的对抗测试清单。文末给一个百行级的Python最小实现可直接挂到LangGraph上。0. 为什么写这篇前两篇把一个问题拆成了两半《我审了DeepSeek开源的超级Agent原型》dsh-superagent的Mandate是真不可变——93处引用全是读运行时无写入路径。《我翻了5个主流Agent框架的授权模型DeepSeek那个不可变授权设计是孤例》LangGraph、AutoGen、CrewAI、Assistants API、Semantic Kernel全部没有不可变授权对象授权本质是运行时可变的进程内配置。那结论就只剩一个问题dsh的Mandate深度耦合在它自己的DSH运行时里你总不能为了一个授权模型换掉整个技术栈。怎么在现有框架上自己造一个不可变授权层这篇就是答案。不是理论是可落地的分层方案最小实现。1. 先定威胁模型你到底在防什么授权层的设计完全取决于威胁模型。生产环境Agent的权限风险就三类按出现频率排威胁攻击路径现有框架能防吗T1prompt注入诱导扩权恶意网页/邮件内容诱导Agent顺便执行超出原任务的操作⚠️ 框架拦截点能拦工具调用但拦不住Agent决定去做什么T2授权配置被运行时篡改同进程代码插件、依赖、业务代码改掉策略dict/hook/filter❌ 全部防不了——配置本来就是可变的T3审批规则被降级攻击者不改授权本身改什么时候需要审批的规则❌ CrewAI的hook能被clear_all一键清空SK的filter运行时增删看清楚**T2和T3防的不是LLM是你自己进程里的代码。**这是大多数团队没想明白的——他们以为授权层防的是Agent乱来其实更硬的威胁是授权层本身被改。dsh-superagent的Mandate恰好同时防住三个scope创建时冻结防T1的扩权结果落不了地、运行时无写入路径防T2、没有自主Watchdog/审批规则不可降级防T3。2. 分层方案在现有框架上叠四层不要指望一个机制解决所有问题。生产环境的授权层应该是四层叠加每层防一个面┌─────────────────────────────────────────┐ │ L4 人工通道stop-only │ 防T3人只能停不能改 ├─────────────────────────────────────────┤ │ L3 版本化审批规则rule_version绑定 │ 防T3规则变更新授权 ├─────────────────────────────────────────┤ │ L2 不可变授权对象Mandate │ 防T1T2创建后冻结 ├─────────────────────────────────────────┤ │ L1 框架拦截点middleware/filter/hook │ 框架自带执行前拦截 └─────────────────────────────────────────┘L1框架拦截点——用现有的别重造你用什么框架就用什么拦截点这一层框架已经给了LangGraphHumanInTheLoopMiddleware的interrupt_onSemantic KernelIFunctionInvocationFilter不调next就不执行CrewAIbefore_tool_callhook这一层的职责只有一个在工具执行前拿到一个判断点。判断什么是上面三层的事。L2不可变授权对象——本篇的核心这是5个框架都没有的。设计原则照抄dsh-superagent的三条创建即冻结授权对象用不可变数据结构Python用dataclass(frozenTrue)字段包括strength、scope、rule_version、actor、issued_at。只有两种命运被使用只读或被收回标记revoked不修改任何字段。防篡改签名对象创建时用HMAC签名校验时验签——防止有人绕过类型系统直接构造一个假授权。第三条比dsh更进一步。dsh的Mandate不可变靠的是代码里没有写入路径的纪律在Python这种什么都能改的语言里光靠frozenTrue不够——object.__setattr__能绕过。加签名才是硬保证。L3版本化审批规则——规则变更新授权把什么时候需要人工审批的规则做成版本化的规则对象同样frozen带rule_version字段Mandate的basis字段绑定创建时的rule_version规则要改发新版本旧Mandate在revalidate时发现版本不匹配→自动失效走重新授权这就是dsh的revalidate机制policy版本变了旧授权不能偷偷升级只能作废重来。规则没有修改只有替换。L4stop-only人工通道人工介入只给一个动作stop。不给修改scope“提升strength”临时放行这些选项。原因很简单每一个人能改授权的入口都是社工攻击的入口。dsh的设计里人唯一能做的是停止一个Ticket设置ended.byhuman要新权限就创建新Ticket。麻烦是麻烦但这就是代价。3. 最小实现不到100行Python实现可直接挂LangGraph。核心是两个frozen dataclass加一个验签importhmac,hashlib,time,osfromdataclassesimportdataclass,fieldfromtypingimportOptional _SECRETos.environ[MANDATE_HMAC_KEY].encode()def_sign(payload:str)-str:returnhmac.new(_SECRET,payload.encode(),hashlib.sha256).hexdigest()dataclass(frozenTrue)classMandate:不可变授权对象。创建后只能读不能改要改只能revoke后重建。mandate_id:stractor:str# 谁授的权人类用户标识scope:str# 授权范围自然语言描述accept时冻结strength:str# ask_first | auto_low_riskrule_version:str# 绑定的审批规则版本issued_at:floatrevoked:boolFalsesig:strdef__post_init__(self):ifnotself.sig:# 创建时签名此后任何字段篡改都会导致验签失败payloadf{self.mandate_id}|{self.actor}|{self.scope}|{self.strength}|{self.rule_version}|{self.issued_at}|{self.revoked}object.__setattr__(self,sig,_sign(payload))defverify(self)-bool:payloadf{self.mandate_id}|{self.actor}|{self.scope}|{self.strength}|{self.rule_version}|{self.issued_at}|{self.revoked}returnhmac.compare_digest(self.sig,_sign(payload))dataclass(frozenTrue)classApprovalRule:版本化审批规则。要改规则发新版本旧的全部失效。version:strrequire_approval_for:frozenset# 需要审批的工具名集合sig:strdef__post_init__(self):ifnotself.sig:payloadf{self.version}|{sorted(self.require_approval_for)}object.__setattr__(self,sig,_sign(payload))classMandateStore:授权存储。只有三个操作签发、收回、校验。没有修改。def__init__(self):self._store:dict[str,Mandate]{}defissue(self,m:Mandate)-None:ifnotm.verify():raiseValueError(mandate signature invalid at issue time)self._store[m.mandate_id]mdefrevoke(self,mandate_id:str)-None:oldself._store[mandate_id]# 收回也是创建新对象除了revoked标志其他字段原样重新签名newMandate(mandate_idold.mandate_id,actorold.actor,scopeold.scope,strengthold.strength,rule_versionold.rule_version,issued_atold.issued_at,revokedTrue,)self._store[mandate_id]newdefcheck(self,mandate_id:str,rule:ApprovalRule,tool:str)-str:返回 allow / need_approval / denied。这是执行前唯一的判断入口。mself._store.get(mandate_id)ifmisNoneorm.revokedornotm.verify():returndeniedifm.rule_version!rule.version:returndenied# 规则已升级旧授权自动失效iftoolinrule.require_approval_for:returnneed_approvalreturnallow三个设计决策值得说为什么revoke也是重建对象因为frozenTrue在Python里可以被object.__setattr__绕过。与其赌没人这么干不如让收回本身也走创建新对象重新签名的路径——MandateStore外部根本拿不到可变引用拿到的永远是verify()之后的判断结果。为什么scope还是自由文本这是从dsh审计里学到的教训结构化scope比如枚举操作集合更安全但表达力差自由文本表达力强但依赖LLM的自律。最小实现选自由文本生产环境建议换成结构化操作集合比如frozenset({git:read,git:write:src/**})把判断从prompt层提到代码层。为什么check返回三态而不是布尔need_approval是显式状态交给L4的人工通道处理而不是让调用方自己猜False是拒绝还是待审批。4. 挂到框架上挂LangGraph只需要一个自定义middleware在interrupt_on之前先过MandateStore.check下面是示意代码实际接口签名按各框架的middleware规范实现defmandate_middleware(store:MandateStore,rule:ApprovalRule,mandate_id:str):defbefore_tool_call(tool_name:str,tool_args:dict)-Optional[str]:verdictstore.check(mandate_id,rule,tool_name)ifverdictdenied:returnfDENIED: mandate{mandate_id}revoked, expired, or rule upgradedifverdictneed_approval:returnNone# 交给框架的interrupt走人工审批returnNone# allow继续执行returnbefore_tool_call挂Semantic Kernel就是实现一个IFunctionInvocationFilter在next(context)之前调checkdenied时不调next。挂CrewAI就是一个before_tool_callhook返回False。模式完全一样框架给拦截点你的授权层给判断两边解耦。5. 怎么验证你的授权层真的不可变写完不算完得测。这是从dsh审计的方法论直接搬过来的——把不可变当成可证伪的命题来攻击。五个测试少过一个都不算完成测试操作预期结果T1 直接改字段m.strength adminFrozenInstanceErrorT2 绕过frozenobject.__setattr__(m, strength, admin)改成功了但verify()返回Falsecheck拒绝T3 伪造授权不知道密钥构造新Mandateverify()失败issue时报错T4 规则降级用旧rule_version的Mandate配新规则check返回deniedT5 revoke后复活revoke后拿旧Mandate对象继续用check返回denied存储里的已换T2是最关键的——它验证的是即使攻击者在你进程里执行了任意代码授权判断结果仍然不可篡改。这就是HMAC签名比dsh的代码纪律更进一步的地方。6. 什么不该抄dshdsh-superagent的Mandate设计值得抄三条不可变对象、版本化规则、stop-only但有两条不要照抄不要抄它的靠纪律保证不可变。dsh是TypeScript没用readonly靠的是全仓没有写入路径——这在code review可控的原型里成立在你的生产代码库里不成立。Python必须用frozenTrue签名双保险。不要抄它的scope自由文本然后靠prompt约束。dsh的mandateInstructions里写do not expand scope是prompt层防护不是代码硬约束我上一篇审计里明确说了这个层次差。你的生产实现应该把scope做成结构化操作集合让越权判断发生在check()的代码里而不是LLM的自觉里。7. 一句话总结主流框架给你的是拦截点不是授权。生产环境的Agent授权要自己造四层框架拦截点管拦得住不可变Mandate管改不了版本化规则管降不了级stop-only人工通道管社工进不来。上面那个100行的实现是最小骨架scope结构化和持久化存储Mandate应该落盘而不是只在内存是下一步。前两篇解决了看清楚这篇解决造出来。下一篇做运行时实测把这个授权层挂到真实Agent上用prompt注入打一遍看它拦不拦得住。附本系列三篇篇内容链接①dsh-superagent静态审计Mandate不可变、Subscription不扩权、Watchdog不存在链接②5个主流框架授权模型对比全部没有不可变授权对象链接③生产环境Agent授权分层方案本篇—