第25章 Omnigent 内置策略:安全与成本控制护栏
Builtin Policies Omnigent 内置了用于常见护栏策略的规则,分为两大类别: 安全与成本控制。当要求添加策略时,你的 Omnigent 实例可按名称应用其中任意一项;或者在 YAML 的 handler 字段中通过完整路径引用它们。 所有内置策略均位于 omnigent.policies.builtins 目录下。
Safety Policy | What it does | Parameters ask_on_os_tools | 在执行任何文件或 shell 操作前进行询问 | 无 block_skills | 阻止特定技能加载 | blocked(字符串数组,必填) block_working_dir_changes | 阻止更改工作目录的 shell 命令 | block_cd(布尔值)、block_worktree(布尔值)、allowed_dirs(字符串数组)、action("deny" 或 "ask") cel_policy | 使用 CEL(Common Expression Language,一种安全且非图灵完备的表达语言)编写自定义策略逻辑。表达式返回一个映射,包含 result 键("DENY"、"ASK" 或 "ALLOW")以及可选的 reason 和 state_updates 键——参见 自定义策略 | expression(CEL 表达式字符串)、reason(默认的拒绝/询问消息) deny_pii_in_llm_request | 扫描出站消息中的 PII(个人身份信息)并将其阻止或标记 | pii_types(字符串数组)、action("DENY" 或 "ASK") detect_thrashing | 通过在一个滚动窗口内跟踪工具结果来检测代理是否反复失败。当连续错误超过阈值或窗口错误率过高时触发。错误检测基于启发式规则,无需服务器端 LLM | consecutive_threshold(整数,默认 5;0 表示禁用)、window(整数,默认 10)、window_error_rate(0.0–1.0 之间的数值,默认 0.8;0 表示禁用)、action("ASK" 或 "DENY",默认 "ASK") detect_loop | 当代理陷入重试同一工具调用的循环时进行询问。对每个(工具、参数)对进行哈希,如果相同的调用在最近的滑动窗口内重复达到阈值次数,则将其路由至人工审批。这能捕捉到 max_tool_calls_per_session 无法发现的 Token 浪费模式,因为该策略仅统计总调用次数 | window(整数,默认 10,最小 1)、threshold(整数,默认 3,最小 1) enforce_sandbox | 在代理启动时强制应用沙箱配置 | sandbox_type、allow_network、write_paths(字符串条目,或用于一次性写时复制写入的 {path, copy_on_write} 对象)、read_paths gcalendar_policy | 控制 Google 日历。默认只读 | 无 gdrive_policy | 控制 Google 云盘、Docs、Sheets 和 Slides 的访问权限。默认情况下,写入操作仅限于代理创建的文件。可选择强制应用 Bell-LaPadula“禁止向下写入”原则:一旦会话读取了机密文件,其写入操作将局限于该机密集合 | read_all、allow_create、write_files、read_files、comment_files、confidential_files(字符串数组)、write_down_action("DENY" 或 "ASK",默认 "DENY") github_policy | 控制 MCP 工具和 shell 命令对 GitHub 的读写访问权限。不可逆的破坏性操作(如删除)即使在允许的仓库中默认也被拒绝,除非设置了 allow_destructive。git push force 标记(--force、-f、--force-with-lease、--force-if-includes)、包含 f 的捆绑短标记(例如 -uf)以及 +refspec 强制推送前缀,无论仓库/分支允许列表如何,默认均被拒绝,除非将 deny_force_push 设为 false | read_all、write_repos、write_branches、allow_destructive(布尔值,默认 false)、deny_force_push(布尔值,默认 true) gmail_policy | 控制 Gmail。默认允许读取和草稿,禁止发送 | allow_read、allow_send、allow_drafts intent_based_authorization | 记录第一条用户消息作为会话意图,然后在任何与该意图无明显关联的工具调用前进行询问。需要 llm: 配置块;若不可用则按通过处理 | 无 max_tool_calls_per_session | 当达到总工具调用限制时拒绝执行 | limit(整数,默认 100) prompt_policy | 使用 LLM 评估策略决策。策略将事件上下文发送给模型,并将响应解释为 ALLOW/ASK/DENY。适用于无法用静态规则表达的细节决策 | prompt(评估模型的系统指令) risk_score_policy | 从工具调用和敏感数据标签中累积风险评分。一旦评分超过阈值,将受保护工具升级为 ASK 或 DENY | threshold(整数)、tool_points(映射工具名称到分数的对象)、sensitive_labels(映射标签到分数的对象)、guarded_tools(字符串数组)、escalate_action("ASK" 或 "DENY") blast_radius | 将 shell 命令分类为安全(ALLOW)、可恢复但存在风险(risky_action)或灾难性(始终 DENY,例如 rm -rf / 或强制推送)。覆盖 Omnigent(sys_os_shell)、Claude/Codex(Bash)、Cursor、Pi、Hermes 和 Goose 中的 shell 工具 | gate_pushes(布尔值,默认 true)、risky_action(默认 "ASK" 或 "DENY")、deny_reason spawn_bounds | 限制每轮次中子代理分发数量,以防止失控的扇出 | max_dispatches_per_turn(整数,默认 5)、dispatch_tools headless_subagent_purpose_guard | 要求每次子代理分发都声明目的(实现、审查、探索、搜索) | allowed_purposes(字符串数组)、deny_reason worktree_guard | 阻止在 worker 的 git worktree 之外写入文件,以防止跨分支污染 | allowed_root(字符串,默认 .worktrees)、deny_reason read_only_os | 拒绝所有涉及文件变更的工具,使得仅报告型代理可以读取和运行 shell,但永远无法更改代码 | deny_reason
Cost Control Policy | What it does | Parameters cost_budget | 跟踪每个会话的累计 LLM 支出。在软阈值时询问,在硬性限制处阻止昂贵模型 | max_cost_usd(必填)、ask_thresholds_usd、expensive_models detect_task_switch | 使用服务器级 LLM 检测用户何时开始新的无关任务,并提示开启新会话 | min_turns(整数)、history_window(整数)、action("ASK" 或 "DENY",默认 "ASK")、classification_prompt(字符串) deny_trivial_to_expensive_model | 将消息分类为琐碎或复杂。将琐碎任务从昂贵模型中路由出去 | expensive_models(字符串数组,必填)、classification_prompt(字符串) subagent_cost_budget | 基于其自身子树 LLM 支出限制子代理。在硬性限制处阻止昂贵模型,在软阈值时询问。通过 sys_session_send 的 cost_budget 参数挂载到子项 | max_cost_usd、ask_thresholds_usd、expensive_models user_daily_cost_budget | 与 cost_budget 相同,但在所有会话中按用户每日强制执行 | max_cost_usd(必填)、ask_thresholds_usd、expensive_models user_period_cost_budget | 与 user_daily_cost_budget 相同,但针对可配置的 UTC 时间段——天、周、月、季度或年。汇总用户当前时间段内所有会话的每日支出;对于当前时间段内的软检查点批准,按用户记忆 | period(必填)、max_cost_usd(必填)、ask_thresholds_usd、expensive_models
Usage examples Confine confidential Google Drive data gdrive_policy 可以在其访问控制之上叠加 Bell-LaPadula 经典的“禁止向下写入”规则。你预先使用 confidential_files(文件 ID 或 Google URL)声明一组机密文档。该规则仅在会话读取其中某个文件后生效:从那时起,其写入操作将被限制在机密集合内。对任何其他文件的写入——或创建——都会将机密数据移动到保护较少的地方,因此会被阻止。
# Omnigent 配置(policies 块) policies: confidential_containment: type: function handler: omnigent.policies.builtins.google.gdrive_policy factory_params: # 机密隔间:文件 ID 或 Google URL。 confidential_files: - "1ConfidentialStrategyDocID" # 允许创建新文件,这样创建操作可以被视为受限制的向下写入, # 而不是被基础访问规则直接阻止。 allow_create: true # 在向下写入时拒绝(硬性阻止)或询问(需要人工审批)。 write_down_action: DENY
由于机密集合是明确声明的,而不是从单文档分类标签中推断出来的,该规则适用于任何 Google 云盘租户。
Catch a stuck agent detect_thrashing 监控 tool_result 事件,并标记反复失败的代理——重试同一错误命令、在权限错误上循环,或以其他方式没有进展。与 detect_task_switch 不同,它不需要服务器 LLM:错误检测是基于启发式的(如 Error:、Traceback (most recent call last)、Permission denied 等常见错误前缀,以及携带 "error" 键的 JSON 负载)。
它在会话状态中维护一个最近工具结果(错误或成功)的滚动窗口,并在满足以下任一条件时触发:
- 最近 consecutive_threshold 个结果均为错误,或 - 最近 window 个结果内的错误率达到或超过 window_error_rate。
检测到后,它会返回配置的操作并附带一条消息,告知用户代理似乎卡住了。触发时窗口不会重置——卡住的代理往往持续失败,因此策略会持续触发,直到用户干预或代理自然恢复(成功的结果会将旧错误推出窗口)。
# Omnigent 配置(policies 块) policies: catch_stuck_agent: type: function handler: omnigent.policies.builtins.context.detect_thrashing factory_params: consecutive_threshold: 5 # 连续 5 次错误后触发(0 表示禁用) window: 10 # 用于比率检查的滚动窗口 window_error_rate: 0.8 # 窗口内错误率 ≥80% 时触发(0 表示禁用) action: ASK # ASK 升级给用户;DENY 阻止下一次结果
两个检查相互独立——设置 consecutive_threshold: 0 以完全依赖窗口错误率,或设置 window_error_rate: 0 以完全依赖连续错误运行。
Track custom state from a CEL expression cel_policy 针对每个策略事件评估 CEL(Common Expression Language)表达式。表达式必须返回一个包含 result 键("DENY"、"ASK" 或 "ALLOW")的映射,并可添加可选的 reason。非映射返回(或缺少有效 result 的映射)表示弃权,传递到下一个策略。由于 CEL 是非图灵完备且无副作用的,表达式本身无法更改任何内容。
为了让 CEL 策略在各轮次间累积其自身的会话状态,返回的映射还可包含 state_updates 键。它必须是一个映射列表,每个映射的形状为: { "key": string, "action": "set" | "increment" | "delete" | "append", "value": any }
action 值的操作如下,value 在 set、increment 和 append 时是必需的,在 delete 时被忽略: Action | Effect set | 在 key 处存储 value,替换任何现有值。 increment | 将 value 加到存储在 key 的数字上。 append | 将 value 附加到存储在 key 的列表上。 delete | 从会话状态中删除 key(value 被忽略)。
例如,此策略允许所有工具调用,同时计数并在会话状态中记录最后一个工具名称:
# Omnigent 配置(policies 块) policies: count_tool_calls: type: function handler: omnigent.policies.builtins.cel.cel_policy factory_params: expression: | event.type == "tool_call" ? { "result": "ALLOW", "state_updates": [ {"key": "call_count", "action": "increment", "value": 1}, {"key": "last_tool", "action": "set", "value": event.data.name} ] } : {"result": "ALLOW"}
以这种方式写入的状态是共享的会话状态,因此后续策略(或同一 CEL 表达式的后续评估)可以读取它并据此分支,就像有状态内置策略那样。
Make spend visible before you restrict it 先仅使用软阈值。工程师可以看到他们正在运行昂贵的会话,并决定任务是否值得。没有人会被阻止;习惯会自行改变。
# Omnigent 配置(policies 块) policies: session_visibility: type: function handler: omnigent.policies.builtins.cost.cost_budget factory_params: ask_thresholds_usd: [1.0, 5.0] max_cost_usd: 999.0 # 实际上没有硬性上限 daily_visibility: type: function handler: omnigent.policies.builtins.cost.user_daily_cost_budget factory_params: ask_thresholds_usd: [10.0, 25.0] max_cost_usd: 999.0
这能让你在决定在哪里添加护栏之前,获得有关支出集中在何处的真实数据。
Server-wide team policy 团队部署的合理起点:按用户每日可见性为 25 美元,50 美元软检查。工程师仍可使用任何模型处理任何任务——只是在每天花费超过 25 美元之前会被询问。
# config.yaml policies: daily_budget: type: function function: path: omnigent.policies.builtins.cost.user_daily_cost_budget arguments: ask_thresholds_usd: [25.0, 50.0] max_cost_usd: 100.0 omnigent server -c config.yaml
每日上限的存在是为了揭示真正失控的会话,而不是惩罚生产性使用。
Per-user monthly (or weekly, quarterly, yearly) budget 每日上限无法捕捉那些每天都刚好低于该上限的用户。 user_period_cost_budget 在更长的窗口上应用与 user_daily_cost_budget 相同的门控:它汇总所有者当前 UTC 时间段内所有会话的支出,并基于总门控。
# config.yaml policies: monthly_budget: type: function function: path: omnigent.policies.builtins.cost.user_period_cost_budget arguments: period: month # day | week | month | quarter | year ask_thresholds_usd: [250.0, 400.0] max_cost_usd: 500.0 # 省略(或传 [])以获得真正的硬性停止,在限制处阻止所有模型; # 命名层级以获得降级门控,仅阻止这些。 expensive_models: [opus]
时间段的计算方式(全部为 UTC): period | Window day | 当前日历日 week | ISO 周——从周一到当前天 month | 当月1日到今天 quarter | 日历季度的第一天起 year | 当年1月1日起
行为与每日策略匹配:
- 软检查点(ask_thresholds_usd)在时间段支出首次超过每个值时询问。批准按用户记录,因此同一检查点在本时间段内不会再次提示该用户——包括来自不同会话的情况。拒绝仅阻止该轮次或工具调用。 - 硬性限制(max_cost_usd)一旦时间段支出达到该值,即拒绝该轮次(请求阶段)或每个工具调用。如果设置了 expensive_models,则仅阻止命名的层级,用户可以 /model 降级到更便宜的模型;如果没有设置,则阻止所有模型。
如果时间段支出为 0.0 或会话没有所有者(单用户模式),策略将弃权,因此绝不会因缺失数据而触发。
ask_thresholds_usd 的值必须每个都大于 0 且小于 max_cost_usd,并且 max_cost_usd 必须大于 0。
每个策略集配置一个 user_period_cost_budget 策略。不要将其与 user_daily_cost_budget 组合;若要限制每日支出,请使用 period: day。不支持堆叠多个时间段策略(例如每周 + 每月)。
Keep sessions focused 使用此策略保持会话集中,避免在过时上下文上浪费 Token。
在每个用户请求时,它分类最新消息是继续当前任务还是开始新任务,并在检测到任务切换时返回配置的操作。它需要服务器 llm: 配置,如果 LLM 客户端不可用则按通过处理。
# Omnigent 配置(policies 块) policies: keep_context_lean: type: function handler: omnigent.policies.builtins.context.detect_task_switch factory_params: min_turns: 2 history_window: 4 action: ASK