Agent 可靠性工程实战(二):把工具调用从“模型想做”变成“宿主允许做”

Agent 可靠性工程实战(二):把工具调用从“模型想做”变成“宿主允许做”
本篇直接复用上一篇的Ledger.append()、load_events()与runs/demo-001/events.jsonl。上一篇最后一条model_proposed事件就是本篇授权器的输入本篇新增tools.json和policy.py输出tool_allowed或tool_denied事件。模型只描述意图真正的文件路径解析与权限判断由宿主完成。一、工具声明不是权限把read_file的 JSON Schema 发给模型只代表模型知道怎样构造参数不代表它获得了读取任意文件的能力。如果宿主直接执行模型给出的路径../../.ssh/id_rsa、符号链接和绝对路径都可能越出工作区。安全边界必须位于模型无法修改的确定性代码里。tools.json用数据描述每个工具的操作类别、允许根目录和单次输出上限。策略默认拒绝名称不存在、参数缺失、路径越界或操作类别不匹配都会产生明确原因码。原因码比自然语言稳定后续统计和回放不会因翻译变化而失效。{schema_version:1,tools:{read_file:{operation:read,roots:[workspace/src,workspace/tests],max_bytes:65536},run_tests:{operation:execute,commands:[[python,-m,unittest]]}}}运行输出loaded_tools2 schema_version1下面的路径判断先做词法拒绝再解析真实路径最后用relative_to验证所属关系。只用字符串前缀会把/work/app-copy误判成/work/app的子目录只删除..又挡不住指向外部的符号链接。from__future__importannotationsimportjsonfromdataclassesimportdataclassfrompathlibimportPathfromtypingimportAnyfromledgerimportLedger,load_eventsdataclass(frozenTrue)classDecision:allowed:boolreason:strnormalized:dict[str,Any]definside(path:Path,root:Path)-bool:try:path.relative_to(root)returnTrueexceptValueError:returnFalsedefauthorize(call:dict[str,Any],manifest:dict,project:Path)-Decision:specmanifest[tools].get(call.get(tool))ifspecisNone:returnDecision(False,unknown_tool,{})ifspec[operation]read:rawcall.get(path)ifnotisinstance(raw,str)orPath(raw).is_absolute():returnDecision(False,invalid_path,{})candidate(project/raw).resolve()roots[(project/root).resolve()forrootinspec[roots]]ifnotany(inside(candidate,root)forrootinroots):returnDecision(False,outside_roots,{path:str(candidate)})returnDecision(True,allowed,{path:str(candidate),max_bytes:spec[max_bytes]})returnDecision(False,unsupported_operation,{})defmain()-int:events_pathPath(runs/demo-001/events.jsonl)proposedload_events(events_path)[-1][payload]manifestjson.loads(Path(tools.json).read_text(encodingutf-8))decisionauthorize(proposed,manifest,Path.cwd())Ledger(events_path,demo-001).append(tool_allowedifdecision.allowedelsetool_denied,{reason:decision.reason,**decision.normalized},)print(fallowed{decision.allowed}reason{decision.reason})return0if__name____main__:raiseSystemExit(main())运行输出allowedTrue reasonallowed二、为什么规范化必须发生在执行前权限判断若针对原始字符串执行时再解析路径两阶段看到的可能不是同一个对象。攻击者可以让符号链接在判断后指向另一个位置这叫检查与使用之间的竞态。对高风险写操作仅靠resolve()仍不够需要使用支持目录文件描述符的系统调用、打开后核验 inode或把执行放进只挂载允许目录的隔离环境。本系列先把读工具限制在项目内并把规范化后的绝对路径写入授权事件。执行器只能消费授权事件里的路径不能重新使用模型原参数。记忆点是裁决输入可以来自模型执行输入必须来自裁决结果。两者看似重复实际切断了“审的是 A做的是 B”。三、拒绝也是正常控制流工具被拒绝不应抛出含糊的内部异常也不该自动把权限扩大后重试。宿主把outside_roots连同允许根目录反馈给下一轮模型可以选择合规路径。连续拒绝会消耗预算防止模型无限探测目录。不存在的文件与无权访问也应使用不同原因码前者可能通过选择其他文件恢复后者需要用户明确授权。输出上限同样属于权限。读取一个数百兆日志会挤掉上下文并增加费用即使路径合法也可能造成拒绝服务。执行器读取max_bytes 1个字节超限时返回摘要与截断标志而不是默默截断后假装得到完整文件。四、怎样验收策略没有形同虚设至少覆盖正常相对路径、绝对路径、..、相似目录名前缀和指向外部的符号链接。测试不能只断言布尔值还要检查账本追加的原因码与规范化路径。tools.json自身必须由人工控制不能放在 Agent 可写目录否则模型只需给自己增加一个shell工具。本篇输出仍是runs/demo-001/events.jsonl现在多了一条带规范化路径的授权事件。下一篇会读取run_started.payload.goal与这条授权事件生成contract.json把原始目标、允许修改范围和检查命令做成指纹防止重试过程中目标悄悄漂移。参考来源Python 文档pathlib — Object-oriented filesystem pathsOWASPPath Traversal 觉得有用就点个赞 收藏方便回头查阅有疑问直接在评论区留言我看到都会回。 本文属于《Agent可靠性工程实战》系列持续更新关注不迷路。 文章里的代码都能直接跑。想要可直接 clone 的完整工程 配套部署脚本 / 踩坑清单评论一声或发邮件到cj2664qq.com我免费发你。如果你正好在做类似系统、或有工程化难题想找人做也欢迎邮件聊一句——我按实际情况评估能落地的就接单或出方案。评论和邮件都能直接找到我不用跳别的平台。