尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深入解析 long-horizon-harness 的 policy 技能:用 JSONL 策略文件守护 Agent 工具调用

深入解析 long-horizon-harness 的 policy 技能:用 JSONL 策略文件守护 Agent 工具调用 深入解析 long-horizon-harness 的 policy 技能用 JSONL 策略文件守护 Agent 工具调用【免费下载链接】adk-samplesA collection of sample agents built with Agent Development Kit (ADK)项目地址: https://gitcode.com/GitHub_Trending/ad/adk-samples导读本文以adk-samples仓库中core/python/long-horizon-harness项目的内置policy技能文档为骨架系统讲解 Long Horizon Agent 如何通过「默认种子 用户覆盖层」两级 JSONL 策略文件.lha/policies.jsonl来拦截破坏性或敏感的工具调用。读完本文你将掌握策略规则的完整字段语义、argv 结构化命令分类器deny/ask/None三级判决的原理、/yolo审批模式与/grant一次性授权的边界并能独立完成「查看生效策略、新增规则、删除规则」的完整实操流程。policy 技能是什么每次工具调用前的策略守门人在long-horizon-harness中policies_guard是一个注册在before_tool_callback链路上的回调每一次工具调用都会先经过它由一个 JSONL 格式的策略文件决定该调用是否被允许。这个技能core/python/long-horizon-harness/horizon/builtin_skills/policy/SKILL.md存在的意义就是让 Agent 能够查看当前工作区生效的策略、并指导用户如何增删规则——但永远不能自己修改策略文件。这套设计有一个明确的安全前提Agent 不能自我编辑自己的防护配置。策略文件所在目录.lha/本身就是被硬性拒绝的破坏性路径模式无论是write/edit写入还是bash里的sed -i、cp/mv、rm、chmod指向.lha/都会命中种子规则中的destructive_commands_regex。详细动机记录在 security-model.md。两级策略体系默认种子与用户覆盖层策略规则存放于两个层级二者追加合并后生效默认种子read-only随 Agent 发布种子文件位于 default_policies.jsonl打包在horizon/guardrails/目录内由policies.py中的_read_default_seed()固定加载。它提供针对灾难性操作与凭据读取的基线拦截例如{canonical_tool_name: terminal, destructive_commands: {command: [dd if, of/dev/sd, of/dev/nvme, of/dev/hd, mkfs., mkfs , :(){:|:};:, nc -l, ncat -l, socat , /dev/sd, /dev/nvme, /dev/hd, /dev/tcp/, /dev/udp/, shred , wipefs , cat ~/.ssh/, cat ~/.aws/, cat ~/.config/gcloud/, cat ~/.kube/, cat /etc/shadow, cat /etc/sudoers]}, destructive_commands_regex: {command: [(?|\\btee\\b(?:\\s-\\S)*)\\s*\\S*\\.lha/, \\bsed\\b[^|;]*-i[^|;]*\\.lha/, \\b(cp|mv)\\b[^|;]*\\.lha/, \\b(rm|truncate|shred|unlink)\\b[^|;]*(?:^|[\\s/\])\\.lha(?:$|[/\\s\;|]), \\bchmod\\b[^|;]*(?:^|[\\s/\])\\.lha(?:$|[/\\s\;|])]}} {canonical_tool_name: write_file, destructive_paths: [/etc/sudoers, /etc/shadow, /etc/passwd], destructive_path_patterns: [*/.ssh/*, */.aws/*, */.config/gcloud/*, */.kube/*, .lha/*, */.lha/*]} {canonical_tool_name: patch, destructive_paths: [/etc/sudoers, /etc/shadow, /etc/passwd], destructive_path_patterns: [*/.ssh/*, */.aws/*, */.config/gcloud/*, */.kube/*, .lha/*, */.lha/*]}可以看出种子规则覆盖了四类风险原始块设备写入dd if、of/dev/sd*、 /dev/sd*等字面量子串危险系统工具mkfs、shred、wipefs、fork bomb:(){:|:};:反向 shell 监听原语nc -l、ncat -l、socat、/dev/tcp/、/dev/udp/凭据直读cat ~/.ssh/、cat ~/.aws/、cat ~/.kube/、cat /etc/shadow、cat /etc/sudoers等。值得注意的演进旧版种子中针对rm -rf、chmod -R的脆弱的子串/正则规则已被移除这两类操作现在交由 argv 结构化解析器 command_safety.py 分类处理详见下文。种子文件只保留「字面灾难命令 凭据读取」减少误伤、提高可解释性。用户覆盖层per-workspace可热加载用户覆盖层位于工作区根目录下的.lha/policies.jsonl。关键行为追加语义load_policies()policies.py返回list(seed) list(user)覆盖层规则只会叠加更多限制不能削弱种子mtime 热加载本地后端每次工具调用都检查文件的st_mtime命中缓存则直接返回否则重新解析——编辑文件后下一次工具调用即生效无需重启会话双后端支持load_policies_for_env()区分本地后端宿主文件、按 mtime 热加载与沙箱后端覆盖层存在沙箱内部通过env.read_file读取、按沙箱身份缓存。关键实现见 _overlay.py沙箱缓存必须用cache_identity()沙箱名而非裸working_dir做 key否则共享/workspace路径的多个租户会互相读到对方的覆盖层。硬性写保护.lha/*与*/.lha/*同时是write_file/patch的destructive_path_patterns并被bash的destructive_commands_regex覆盖追加、sed -i、cp/mv、rm、chmod等命令形态。因此 Agent只能读、不能写.lha/policies.jsonl——这是设计使然。何时使用这个技能policy技能适合以下两种用户请求场景用户询问当前生效的策略是什么用户想封锁某个命令或路径模式或放宽删除某条覆盖层规则——两种情况都由 Agent 查看当前文件、把精确的改动方案交给用户手动执行Agent 自己不动手。此外要特别区分如果用户想对刚刚被拦截的一次调用做一次性放行应引导用户自己输入/grant command。/grant是仅面向用户的斜杠命令Agent 无法代为调用。底层机制见 policy_grants.py授权记录写入session.state[_policy_grants]policies_guard在求值规则之前先查匹配的 grant命中即短路放行grant 采用「签名子集」匹配模型例如对{command: rm -rf build/}的授权不会放行rm -rf /保持授权尽量狭窄。文件格式一行一条规则的 JSONL DSL.lha/policies.jsonl的格式约定每行一个 JSON 对象空行与#注释行被跳过解析器见 _jsonl.py 的iter_jsonl_objects每条规则必须包含canonical_tool_name字段它决定了该规则作用于哪个工具bash、terminal、write_file、patch等。加载时若缺少该字段规则会被静默丢弃policies.py一条规则可以携带以下一个或多个字段各字段独立求值互不依赖字段类型效果destructivetrue无条件拦截该工具。destructive_commands{arg: [substring, ...]}当字符串参数包含任一子串时拦截大小写不敏感。destructive_commands_regex{arg: [regex, ...]}当任一正则命中字符串参数时拦截大小写不敏感。子串匹配过于粗糙时需要锚点、交替、词边界使用。destructive_paths[prefix, ...]当路径形参数path、file_path、target_path以任一前缀开头时拦截。destructive_path_patterns[fnmatch-glob, ...]当路径形参数命中任一 fnmatch glob 时拦截。适合*/.ssh/*这类无法用单一前缀表达的 per-user 路径。正则安全校验ReDoS 防护destructive_commands_regex中的租户编写的正则覆盖层 grant 规则在编译前会经过 _regex_safety.py 的启发式校验safe_regex()拒绝长度超过 1000 字符的模式并检测嵌套量词形态如(…)、(.*)*、([a-z])。Python 的re没有匹配超时这条防线能防止恶意/有缺陷的正则拖垮请求。被判定不安全的模式会被跳过并告警而不是编译执行。注意信任的内置种子规则不受此限制只有用户自写的 overlay/grant 模式会被门控。解析失败的行为无法解析的 JSONL 行跳过并打 warningfail closed该行不产生任何拦截效果但也不会让其余规则失效无效正则编译时报错被捕获并跳过policies.py 中对re.error的处理覆盖层文件缺失视为空read_overlay_text对FileNotFoundError返回。规则求值原理源码视角policies_guardpolicies.py的执行顺序grant 短路若会话状态中存在匹配(tool_name, args)的 per-session 授权find_matching_grant直接放行逐条求值_evaluate(rule, tool_name, args)对每条规则按字段类型分别匹配——子串pat.lower() in value.lower()大小写不敏感正则re.search(pat, value, re.IGNORECASE)路径前缀仅对_PATH_ARG_NAMES {path, file_path, target_path}中的参数做startswith路径 globfnmatch.fnmatchcase(value, pat)合成复查process(actionwrite)的data载荷与process(actionspawn)的command会伪装成bash参数重新跑一遍 bash 规则——否则这些 shell 执行路径会因为canonical_tool_name永不为bash而绕过种子里dd if、fork bomb、cat ~/.ssh/等拦截argv 分类提取 shell 命令交给command_safety.classify()判级见下节拦截返回统一返回{error: ..., confirmation_required: True, matched_rule: ...}短路口字典_block()并在错误信息中提示用户可输入/grant command做会话级放行。argv 结构化分类deny / ask / None 三级判决在覆盖层/种子规则运行之前command_safety.py先用标准库shlex把 shell 命令词法切分成 argv token引号与操作符感知再对 token 结构进行分析而非对原始字符串做模式匹配。它返回一个粗粒度判决deny灾难性任何地方都硬拦截——例如rm -rf /、rm -rf /etc、rm -rf $HOME、rm -rf /*等对系统根/用户主目录的递归强制删除ask有风险——根 Agent会看到一个交互式审批卡子代理/无头链则视为硬拒绝通过policies_guard的ask_is_denyTrue参数保证无人值守场景不回归None无意见——交给常规权限流程决定。从 command_safety.py 的实现看分类器覆盖了相当精细的结构化场景危险目标判定rm_DANGEROUS_RM_TARGETS列出了一组系统/用户根目标/、~、$HOME、/etc、/usr、/bin、/sbin、/lib、/boot、/sys、/proc、/dev、/var、/root、/home、/Users并配套_DANGEROUS_RM_PREFIXES前缀表与_HOME_CONTAINERS处理/home/bob会清空整个用户而/home/bob/build不会所以只有「一层子目录」形态才被判定为用户根。只有当rm同时具备递归-r/--recursive与强制-f/--force时才进入危险判定命中根目标返回deny命中./、*等当前目录/glob 形态返回ask。git 危险动词_git_verdictgit push --force/-f/--force-with-lease→ask重写远端历史git push --delete/-d/--mirror、refspec强制推送 →askgit reset --hard→askgit clean -f→askgit filter-branch/filter-repo→ask。实现上通过_GIT_VALUE_FLAGS-C、-c、--git-dir等跳过会消费下一个 token 的全局选项值避免把路径名push误判为子命令。云/基础设施 CLI 的不可逆删除_CLOUD_DELETE按token 精确匹配而非子串避免把 SQL 里的DELETE或名为delete的资源误伤CLI危险动词bqrmgclouddelete、rmgsutilrmkubectldeleteterraformdestroydockerrm、rmi、prunegwsdelete其他ask场景find携带副作用动作-delete、-exec、-execdir、-ok、-fls、-fprint等mv到/dev/null销毁数据管道进解释器| sh、| bash、| python、| perl、| ruby、| node、| phpsudo/su或经sudo/doas提权后的命令_effective()会剥离环境变量前缀、解包sudo/doas/env及透明启动器nice/nohup/timeout/xargs等检查真正的二进制对系统/用户根做递归chmod/chown。另外classify()对无法解析的命令采取保守策略直接返回(ask, command could not be parsed)——宁可多问一次也不放行。审批模式/yolo与四按钮审批卡策略拦截属于硬拒绝层Layer C在它之下还有一层软询问层Layer D——permission_guardpermission_guard.py负责判断「这次调用是否值得确认」。当一个调用通过了硬拒绝层但被ask判级时根 Agent 会看到一个交互式四按钮审批卡Yes, once仅此一次Yes — allow command this session本会话放行写入 session grantAlways allow command持久化到.lha/permissions.jsonlDecline拒绝。/yolo命令可在default与yolo模式间切换。YOLO 模式只自动批准 Layer-D 的交互式ask判决它不会绕过 exfil guardLayer A或 Layer-C 硬拒绝规则灾难命令 凭据读取 种子覆盖层。适合在信任当前会话、希望减少「危险但不灾难」操作弹窗时使用。审批模式是会话级状态session state 中的approval_mode键不会跨会话持久化。无头/子代理链路的行为差异permission_guard中的set_headless_mode()标记无头上下文——无头 routine 运行中shell 命令的ask会自动放行因为它运行在隔离的lhart-沙箱内且有 exfil/policy 前置拦截而非 shell 工具的ask则fail closed自动拒绝返回headless_denied因为非 shell 操作没有沙箱爆炸半径保护。工具收窄强制overlay/grant 无法授予空泛的 allow权限规则.lha/permissions.jsonl或审批卡授予可以通过commandPrefix、commandRegex或argsPattern来收窄空泛的allow规则。关键约束permission_rules.py 的_is_blanket_allow与_stamp来源为overlay或grant、且目标是bash/process、却不携带任何收窄字段的规则在加载时被直接拒绝——你不能从覆盖层或会话审批中授予「永远允许 bash」这种空泛权限只有默认种子可以携带它。这防止了意外的过度授权对应 security-model.md 威胁模型中的 H10。与权限层的集成边界需求正确做法调用被策略拦截想解释原因Agent 说明{error: ..., confirmation_required: True}对应的具体规则并告知用户被拦的原因一次性放行某条被拦的命令用户自己输入/grant command会话级policies_guard在求值前查询 grant强制弹出确认而非硬拦截某条命令在.lha/permissions.jsonl中加ask_user规则永久封锁某条命令在.lha/permissions.jsonl中加deny规则权限层细节见 permission-model.md。注意权限覆盖层与策略覆盖层共享同一.lha/写保护——它们都只能由用户直接编辑Agent 无法代写。整个守护链在 security-model.md 中被描述为Layer Aexfil guard→ Layer Cpolicies_guard command_safety→ Layer Dpermission_guard的顺序执行第一个提出异议的层就是错误信息里出现的那一层调用必须先通过硬拒绝地板软询问层永远不会重新打开安全防护已拦截的调用。实战工作流列出当前生效的规则read(.lha/policies.jsonl)如果文件不存在说明用户还没有任何覆盖层规则——当前只有种子生效。种子位于工作区之外无法通过你的工具读取此时应依据本文「默认种子」一节的内容描述其覆盖范围而不是尝试去读它。用户想新增一条规则先读取当前覆盖层确保建议的追加位置正确然后把精确的 JSON 行交给用户自己添加你不能写.lha/policies.jsonlexisting read(.lha/policies.jsonl) # 可能不存在 → 视为 new_rule {canonical_tool_name: bash, destructive_commands: {command: [rm -rf node_modules]}}然后告诉用户把这一行加到.lha/policies.jsonl文件不存在就先创建每行一个 JSON 对象后跟json.dumps(new_rule)。保存后下一次工具调用即生效mtime 热加载。用户想删除一条规则读取覆盖层定位要删除的行以覆盖层文件为准的 0 起始行号忽略空行/注释行告诉用户要删除的行号与内容。删除前务必与用户确认目标——覆盖层规则通常是为了堵住某个缺口而加的删错了会重新暴露风险。完整实操示例阻止npm publish用户需求「从终端封锁npm publish。」读取.lha/policies.jsonl不存在则视为空构造规则{canonical_tool_name: bash, destructive_commands: {command: [npm publish]}}告诉用户我无法自己编辑.lha/policies.jsonl——请把这一行添加进去文件不存在就先创建后跟上面的 JSON保存后下一次npm publish尝试就会被拦截。由于destructive_commands是子串匹配大小写不敏感这条规则同时会拦截npm publish --tag beta、npx npm publish等含npm publish子串的命令如果你希望精确到整条命令可以改用destructive_commands_regex加^npm publish(\s|$)之类的锚定正则注意正则需通过安全校验。注意事项与排障要点种子只读且够不着默认种子不可编辑且位于工作区根之外无法通过工具读取描述其覆盖范围请以上文种子内容为准JSONL 解析失败 fail closed坏行被跳过并告警——让用户保存新规则后用read读回文件确认能正常解析覆盖层追加不覆盖新规则只会叠加限制不会削弱种子若想放宽某条覆盖层规则删除对应行即可种子无法被覆盖层放宽沙箱后端差异部署在沙箱后端时覆盖层文件存在于沙箱内而非宿主机读取走 env 接口并按沙箱身份缓存_overlay.py本地开发则按 mtime 热加载行为验证仓库的单元测试如 test_policies_guard.py、test_policies_default_seed.py、test_process_spawn_guardrails.py、test_regex_safety.py覆盖了规则加载、种子内容、spawn 合成复查与正则安全校验等路径是理解各边界行为的可靠参考。总结policy技能把「破坏性工具调用防护」做成了数据驱动、可审计、Agent 不可自改的体系默认种子守住灾难底线.lha/policies.jsonl覆盖层允许用户按工作区追加限制argv 结构化分类器替代脆弱的子串匹配/grant与/yolo分别提供一次性豁免与会话级宽松而权限层Layer D的收窄强制确保任何授权都无法变成空泛的「永远允许 bash」。理解了这四层种子、覆盖层、argv 分类、权限层的交互边界你就能安全、精准地为 Long Horizon Agent 配置出符合自身风险的命令防线。【免费下载链接】adk-samplesA collection of sample agents built with Agent Development Kit (ADK)项目地址: https://gitcode.com/GitHub_Trending/ad/adk-samples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表