SOP 语法参考
SOP 定义会从 sops_dir 下的子目录中加载;默认情况下未设置该项,因此在操作员主动选择启用之前,运行时 SOP 执行处于关闭状态。将 sops_dir 设置为某个目录即可启用该功能:相对值会解析为相对于安装根目录(即存放 config.toml 的目录)的路径,因此文档中的 shared/sops 会解析为 <install>/shared/sops,也就是 SOP 作者写入的同一目录。绝对路径或以 ~- 开头的值将按原样使用。将其重新设置为 ""(或不设置)会禁用运行时 SOP 执行;CLI 命令仍会回退到 <install>/shared/sops 以进行离线检查。
1. 目录结构
<shared>/sops/
deploy-prod/
SOP.toml
SOP.md
每个 SOP 都必须包含 SOP.toml。SOP.md 是可选的,但如果未解析任何步骤,则验证将失败。
2. 编写边界
基于文件的表示形式仍然包含一个清单文件以及 SOP.md。本页有意不枚举清单字段,也不提供手工编写的清单示例。
使用此页面了解在审查、验证或调试 SOP 时可见的语法:SOP.md 步骤项目符号、从运行时 schema 生成的触发字段摘要,以及 condition 表达式。在运行生成或已提交的 SOP 之前,请使用 zeroclaw sop validate <name> 对其进行验证。
SOP.toml 包含 SOP 的身份信息(name、description、version)、其 triggers 以及执行参数。并发准入字段规定了当触发器到达而此 SOP 的执行槽位已满时会发生什么:
| 字段 | 默认 | 效果 |
|---|---|---|
max_concurrent | 1 | 同时执行的此 SOP 运行的最大数量。停在 HITL 审批或确定性检查点的运行会释放其槽位,因此不计入此限制。 |
admission_policy | parallel | 无法立即接纳的触发器的处理方式(见下文)。 |
max_pending_approvals | 0(无限制) | 此 SOP 同时停留在 HITL 审批处的运行数上限。超过该上限后,后续触发会被延迟(背压),绝不会被静默丢弃(drop 除外)。 |
admission_policy 值(SopAdmissionPolicy,snake_case):
parallel(默认)- 最多允许max_concurrent个并发;当前无法准入的触发器将被延迟处理(在触发器的传输层上浮现,用于背压/重投递),而不会被静默丢弃。最适合独立工作(例如 PR 审批 SOP)。hold- 序列化:仅当该 SOP 没有运行中或已暂停的实例时才允许触发;其他触发请求将被推迟。适用于预审批步骤不能重叠的流水线。coalesce- 将并发触发合并到已在进行的运行中(正在进行的运行的最新状态已涵盖该触发)。drop- 传统的即发即弃模式:无法接受的触发器将被丢弃。需显式选择启用;永远不作为默认值。
延迟触发器的恢复取决于传输层——此版本中引擎内部没有持久化的待处理触发器队列(这将作为单独的后续工作跟进):
- AMQP (
durable_ack = true,仅 SOP 分发):投递会被拒绝确认(requeue = true),以便代理在有空间后重试该投递。 - AMQP 组合
sop_and_agent_loop:代理端已经消费了这条消息,因此会高调记录背压导致的 SOP 溢出并发送 ACK(不重新投递),以避免代理端重复运行。 - MQTT / cron / filesystem / channel-router(以及其他任何仅记录调度结果的无头源):不支持逐消息重新投递,因此延迟触发器在产生明显日志记录后即被丢弃(唯一的恢复方式是等待下一次计划/发布/观测到的触发器)。
[sop]
name = "deploy-prod"
description = "需要审批的生产部署"
version = "1.0.0"
max_concurrent = 1
admission_policy = "hold"
max_pending_approvals = 8
[[triggers]]
type = "manual"
审批代理组和策略存在于 ZeroClaw 主配置中,而非每个 SOP 的 SOP.toml 文件中。步骤可以在 SOP.md 中通过 - policy: prod 按名称引用已配置的策略:
[sop.approval.groups.release]
members = ["http:<paired-token-subject>", "agent:release-bot"]
[sop.approval.policies.prod]
required_group = "release"
quorum = 2
escalation_route = "oncall"
[sop.approval.groups.*] 的成员是审批身份标识,而非账户名。成员可以带源限定符(http:<subject>、ws:<subject>、agent:<alias>),表示仅在对应传输上拥有审批权限;也可以使用裸标识(ZeroClawOperator),表示携带该身份的任意来源均可审批。HTTP 和 WebSocket 审批入口使用配对令牌的 subject;当前 CLI 审批路径(zeroclaw sop approve)为匿名方式,暂不支持通过 cli:<user> 成员身份进行验证。
配对令牌主体是 bearer token 的小写 SHA-256 十六进制摘要。配对后,从规范的 gateway.paired_tokens 条目复制摘要,或者在不将该机密写入 shell 历史记录的情况下,根据 bearer token 计算摘要。轮换配对令牌会创建新的主体,因此应在同一轮换操作中更新所有引用旧摘要的审批组成员关系。
3. SOP.md 步骤格式
步骤是从 ## Steps 部分解析的。
## Steps
1. **Preflight** — Check service health and release window.
- tools: http_request
2. **Deploy** — Run deployment command.
- tools: shell
- requires_confirmation: true
- policy: prod
- input: {"type":"object","required":["version"],"properties":{"version":{"type":"string"}}}
- output: {"type":"object","required":["digest"],"properties":{"digest":{"type":"string"}}}
- next: 3
路由和审批要点可以合并到同一 SOP.md 步骤中:
## Steps
1. **Classify event** — Inspect the incoming payload.
- output: {"type":"object","required":["severity"],"properties":{"severity":{"type":"string"}}}
- when: $.steps.1.severity == "critical"
- next: 2
2. **Prepare summary** — Build the operator-facing remediation plan.
- depends_on: 1
- on_failure: retry:2
- next: 3
3. **Approval gate** — Require explicit approval before changing state.
- kind: checkpoint
- requires_confirmation: true
- next: 4
4. **Apply remediation** — Execute the approved action.
- tools: shell
- allow-tools: shell
- on_failure: goto:5
5. **Notify operator** — Send a failure notice for follow-up.
- tools: http_request
解析器行为:
## Steps部分会解析到下一个二级标题为止。- 编号项(
1.、2.、…)定义步骤顺序。 - 开头的粗体文本(
**Title**)会成为步骤标题;其余文本会成为步骤正文。 - tools:映射为suggested_tools,为该步骤提供建议使用的工具名称。- allow-tools:(或- allow_tools:)定义了显式的每步工具允许列表。- deny-tools:(或- deny_tools:)定义了显式的逐步骤工具拒绝列表。- requires_confirmation: true强制要求对该步骤进行审批。- kind:接受execute(默认值)、checkpoint/approval或capability;检查点会暂停确定性执行,而requires_confirmation: true会在任何执行模式下要求批准。- capability:用于指定kind: capability步骤使用的确定性能力。- with:为能力步骤提供结构化输入。- input:为步骤边界附加类似 JSON Schema 的输入契约。- output:会将类似 JSON Schema 的输出契约附加到步骤边界。- when:会在当前步骤完成后,针对已累积的已完成步骤输出进行求值。条件为 false 时会跳过 switch 和显式 next,转到线性后继;如果该步骤是终端步骤或没有后继,则完成执行。条件为 true 或未设置时,非空 switch 的优先级高于 next;没有 switch 时,会先使用显式 next,然后才进行终端或线性路由。- 仅当顶层
when允许路由且未声明switch端口时,- next:才会路由到显式后继;不符合条件的路由步骤会标记为skipped,使运行保持pending,而不是进行调度。 - terminal: true会完成运行,而不是继续执行下一步;当最终步骤没有线性后继步骤时,它也会完成。- depends_on:(或- depends-on:)列出非线性运行所需的前置步骤。- switch:定义用于多分支路由的有序name>condition>step端口。在顶层when为 true 或未设置时,第一个匹配的端口获胜;未匹配的 switch 会完成运行,并忽略next以及线性后继。顶层when为 false 时会跳过 switch 求值。- on_failure:(或- on-failure:)接受fail、retry:<count>或goto:<step>,并对报告的步骤失败和输出模式失败强制执行。- mode:会覆盖该步骤的 SOP 执行模式。- agent:会覆盖该步骤的父代理别名。- call:会在该值解析为计划调用时,向步骤添加 JSON 计划工具调用。- prompt:设置审批门通知模板。- policy:指定[sop.approval].policies中的审批代理策略;该策略通过必需组成员资格和法定人数来控制审批。缺少该策略时会安全失败,而不是因单次审批而放行;省略该字段则不会对该门禁实施策略。- edit:将检查点设置为在恢复前编辑指定字段。- 无法识别的子项目符号和其他非空的续行会追加到步骤正文中。
可复制的条件路由示例
这份完整的 SOP.md 会将关键警报引导至经批准的修复步骤,同时记录其他未进行修复的警报:
# Alert triage
Classify an incoming alert, remediate critical alerts, and notify the operator.
## Steps
1. **Classify alert** - Normalize the incoming alert severity.
- output: {"type":"object","required":["severity"],"properties":{"severity":{"type":"string"}}}
- when: $.steps.1.severity == "critical"
- next: 3
2. **Record routine alert** - Add the non-critical alert to the incident log.
- tools: shell
- next: 4
3. **Remediate critical alert** - Run the approved remediation command.
- tools: shell
- requires_confirmation: true
- on_failure: retry:2
- next: 4
4. **Notify operator** - Send the outcome to the operations channel.
- tools: http_request
当步骤 1 输出 {"severity":"critical"} 时,其守卫条件匹配,next 跳转到步骤 3。任何其他严重级别都会继续执行步骤 2;步骤 2 中显式的 next 会跳过修复流程,并在步骤 4 汇入关键路径。
加载器只会发现同时包含 SOP.toml 的目录,因此请将上述步骤与 <sops_dir>/alert-triage/ 中的此清单配套使用:
[sop]
name = "alert-triage"
description = "对传入的警报进行分类,修复严重警报,并通知操作员。"
[[triggers]]
type = "manual"
然后运行 zeroclaw sop validate alert-triage,该命令会报告 SOP 有效。
[sop.approval] 策略和路由投递
策略还可以将其审批带外路由到某个通道,这样审批者无需盯着发起运行的界面即可进行操作:
[sop.approval.policies.prod]
required_group = "release"
quorum = 2
# 当运行在此策略所管理的关卡处 PARKS 时发送。
request_route = "discord.ops:123456789012345678"
# 仅当该关卡随后 TIMES OUT 时发送(这是一个不同的第二条路由)。
escalation_route = "discord.oncall:987654321098765432"
两个路由均为 channel:recipient:channel 是已配置 channel 的映射键(<channel>.<alias>,或用于单例的裸 <channel>),而 recipient 是该 channel 的接收方(Discord channel id、chat id,…)。投递采用尽力而为,绝不会阻塞或清除门控 - 审批本身仍会通过经过身份验证的批准/拒绝界面返回,其主体可以满足策略的组和仲裁要求。路由仅在 daemon 中触发(channels 在此处配置);将其保留为未设置(或为空)即可仅通知发起界面,这是默认行为。
路由投递没有持久化重试队列。守护进程在异步发送完成前退出,或通道发送失败,可能导致通知丢失而不改变已暂存的门控状态。运维人员可使用 zeroclaw sop pending 检查待处理的运行,并通过经身份验证的审批界面联系符合条件的审批人。
授予频道原生审批者的审批组必须使用带频道限定的成员格式 channel:<channel-key>:<sender>,例如 channel:discord.ops:123456789012345678。未限定作用域的成员(例如 channel:123 或裸 123)不会匹配频道审批,因为发送者 ID 可能在不同平台和频道别名之间重复。
确定性检查点:审批与恢复
暂停在 kind: checkpoint 步骤上的确定性运行,可通过与审批门相同的批准/拒绝界面进行处理(zeroclaw sop pending 会列出两者,并通过 kind 加以区分)。批准后,引擎会恢复运行,并在无需代理参与的情况下执行后续的 kind: capability 步骤,直到下一次暂停或运行完成——因此,checkpoint -> capability 尾段(例如发布已批准的草稿)无需实时代理回合即可执行。拒绝后,运行将被取消。这两种处理结果都会记录在审批账本中。检查点步骤可以带有 - policy:;在检查点决策生效前,同一策略规定的必需组成员资格和法定人数要求同样适用。如果该策略指定了 request_route,守护进程会将带外检查点通知发送到该处。escalation_route 仍是有时限审批门的超时路由;检查点暂停目前不会安排检查点专用的升级超时。
以下两种检查点决议方式让审查者能够塑造草稿,而不仅仅是对其进行门禁控制(两者都像 approve/deny 一样经过账本审计):
- 编辑(修改) - 通过在检查点上添加
- edit: <field>项目符号来选择启用:审批者可以在运行恢复之前,将管道值的该字段替换为自己的文本(在 Discord 上,Edit 按钮会打开一个预填了当前值的模态框)。检查点的记录输出保存人工审批的文本;前置步骤保留模型的原始内容用于审计跟踪。账本行记录decision: amend。 - 修订 - 当检查点的前驱步骤为
llm.generate步骤时自动提供:审批者发送指导意见,引擎以该指导意见作为审阅者反馈(revision_feedback,承载于步骤的静态配置平面——不可信载荷的框架保持不变)重新运行该步骤,替换草稿,并重新呈现关卡。运行过程中每次呈现关卡都携带唯一的修订版本号(每次修订均会递增,此后每个检查点首次驻留时同样递增);提示词引用格式为<run_id>#<rev>,对已废弃提示词(旧草稿或更早关卡遗留的按钮)的应答将被拒绝。每个关卡最多修订 3 次;若重新生成草稿失败,则保留上一草稿处于可应答的驻留状态。账本记录decision: revise,并以指导意见作为原因。
注入适配器功能
两个 kind: capability 步骤通过守护进程在构建引擎时注入的适配器执行实际副作用;没有守护进程时(CLI 验证、测试),它们会像 shell.exec 一样以明确的消息安全失败:
llm.generate- 作为流水线步骤执行一次有界模型调用(无工具、无智能体循环),使用默认智能体解析得到的模型提供商。with:中声明的字段包括 -instruction(必填)、system、output_key(默认为text)、echo(复制到输出中以供下游管道传递的负载字段)。通过管道传入的事件负载会置于显式的不受信任内容框架内,并且绝不会被作为配置读取。forge.comment- 通过 git 频道的出站路径向 git-forge 的 issue/PR 发布评论(与提供商无关:GitHub / Gitea / Forgejo)。输入字段:repo(owner/repo)、number、body,以及可选的channel(git.<alias>;默认为唯一已配置的 git 频道)。
它们与检查点共同构成一个无头审查流水线:
1. **Draft** - kind: capability / capability: llm.generate
- with: { instruction = "...", output_key = "body", echo = ["repo", "number"] }
2. **Approve** - kind: checkpoint / policy: triage
3. **Post** - kind: capability / capability: forge.comment
适配器的接入位置。 真实的适配器(llm.generate 的模型提供者、forge.comment 的 git 通道,以及检查点策略的带外审批路由)仅在 daemon / channel-start 路径上注入,该路径是唯一具有已配置通道映射和在用模型的路径。独立 agent 运行和 CLI SOP 执行在构建引擎时不包含这些适配器,因此这些能力和路由在那里是故障关闭的:llm.generate / forge.comment 会报告明确的“requires an injected adapter“错误而不执行任何操作,检查点的路由通知则是仅记录日志的空操作。这与 shell.exec 的故障关闭模型相同;如需使用这些能力,请在 daemon 下运行相应流水线。
步骤契约强制执行
步骤契约是可选的。存在时,input 和 output 接受一个包含 type、required、properties 和 items 字段的紧凑 JSON 对象。支持的原始类型有 object、array、string、number、integer、boolean 和 null。
[sop] 配置控制强制执行:
| 字段 | 默认 | 效果 |
|---|---|---|
step_schema_enforce | true | 在引擎边界验证声明的步骤输入/输出模式。 |
step_scope_enforce | false | 将每步工具作用域视为强制过滤器,而不是建议性提示。 |
step_mandatory_tools | ["sop_advance", "sop_approve", "sop_status"] | 在启用作用域强制时保持生命周期工具可用。 |
max_step_visits | 256 | 停止路由运行,避免对同一步骤重复访问过多次。 |
max_step_retries | 2 | 限制步骤失败策略请求的重试次数。 |
untrusted_payload_max_bytes | 8192 | 将不受信任的触发器 topic/payload 文本限制在 UTF-8 字符边界;0 会禁用该限制。 |
untrusted_input_guard | "warn" | 针对不受信任触发器输入的 Prompt-guard 操作:warn、block 或 sanitize。 |
untrusted_guard_sensitivity | 0.7 | 用于 prompt-guard 筛选和出站脱敏的敏感度。 |
untrusted_frame_warning | true | 在不受信任内容框架中包含说明性警告文本。框架边界保持启用。 |
untrusted_outbound_redact | true | 为 SOP 内容安全消费者启用共享出站脱敏。 |
procedural_memory_enabled | false | 为提案捕获、审阅以及显式 SOP 回写注册 sop_workshop 工具。 |
Schema 强制执行采用 fail closed:无效的步骤输入会阻止该步骤启动,无效的步骤输出会通过该步骤的 on_failure 策略进行路由。路由强制执行在 LLM 和确定性运行中取代线性的 current_step + 1 推进。工具作用域强制执行会缩小当前步骤轮次可用的工具范围,并在调度时阻止超出作用域的调用。
在到达步骤上下文之前,不受信任的触发主题和负载文本会被截断、归一化、筛选并加上框架。框架始终开启;警告文本可以隐藏,但原始外部触发文本不会插入到模型上下文中。
程序记忆是可选启用的。启用后,sop_workshop 可以创建并检查已存储的 SOP 提案,将已完成的运行上下文捕获为候选流程,并将已批准的提案应用到 SOP.toml/SOP.md。写回仅通过显式的 apply 操作发生。
运行持久性
[sop] 配置还控制守护进程重启后运行状态是否保留:
| 字段 | 默认 | 效果 |
|---|---|---|
persist_runs | true | 持久化运行状态——包括停驻于 HITL 审批或确定性检查点的运行——使其在重启后依然存在。设为 false 则使用仅内存、非持久化的引擎。 |
run_store_backend | "sqlite" | 当 persist_runs 为 true 时使用的持久化后端。sqlite 会在运行状态目录下写入 runs.db。 |
persist_runs = true 是默认值,因此重启后不会丢失已暂停的 HITL 审批(若持久化后端无法打开,build_sop_engine 将回退至内存存储并输出明显的日志,故此默认值是安全的);persist_runs = false 是针对临时引擎的文档化退出选项。
4. 触发器类型
| 类型 | 字段 | 备注 |
|---|---|---|
mqtt | topic,可选的 condition | MQTT 消息到达。Live:由 MQTT 监听器传递。 |
webhook | path | 传入的 HTTP 请求。已上线:网关 /sop/* 和 SOP 优先的 /webhook 路由。 |
cron | expression | 基于时间的触发。实时:由 SOP 维护定时器(daemon / channel-start 路径)分发。 |
peripheral | board、signal,可选的 condition | 硬件信号。已定义并匹配,但没有外围监听器为其提供输入。 |
filesystem | path、可选的 condition、可选的 events | Filesystem 变更。实时:由文件系统监视器传递。 |
calendar | calendar_source,可选 calendar_ids,可选 condition | 日历事件状态。已定义并匹配,但没有轮询器实时提供它。 |
channel | channel、可选 alias、可选 condition | 配置的通道上的入站消息或 forge/platform 事件(telegram、discord、slack、Git 等)。实时:当启用该通道的 SOP dispatch 时,由通道协调器投递。Git forge 生产者会设置形如 <channel>.<alias>:<event_type> 的事件主题,并将 event_type 放入载荷中,因此编写的 condition 可以按类型过滤 forge 事件,而无需第二种触发器形状。 |
manual | 无 | 通过 sop_execute 工具发起的代理运行。不是外部 fan-in。 |
amqp | routing_key,可选 condition | AMQP 投递。Live:由 AMQP 消费者以 SOP 派发模式投递。 |
有关每个 source 的 live-versus-unwired 状态以及 transport 详情,请参见 SOP Fan-In。
5. 条件语法
触发器 condition 字段和步骤 when: 守卫使用相同的表达式语法。触发器条件针对事件负载进行求值。步骤 when: 守卫针对以下形式的已累积完成步骤输出进行求值:
{
步骤: {
"1": {
"severity": "关键"
}
}
}
对于无效条件、缺少有效负载、无法解析的 JSON 路径,以及有效负载或比较值不是数字的直接数值比较,求值均采用失败关闭策略。空条件无条件匹配。
JSON 路径表单
以 $ 开头的条件用于比较 JSON 载荷中的值:$.path.to.field <op> <value>。
| 表达式 | 有效负载 | 匹配 |
|---|---|---|
$.value > 85 | {"value":90} | 是 |
$.value >= 85 | {"value":85} | 是 |
$.temp < 25 | {"temp":20} | 是 |
$.temp <= 25 | {"temp":25} | 是 |
$.status == "critical" | {"status":"critical"} | 是 |
$.status != "error" | {"status":"ok"} | 是 |
$.count == 42 | {"count":42} | 是 |
$.data.sensor.value > 85 | {"data":{"sensor":{"value":87.3}}} | 是 |
$.readings.1 == 20 | {"readings":[10,20,30]} | 是 |
$.active == "true" | {"active":true} | 是 |
$.nonexistent > 0 | {"value":90} | 不 |
路径规则:
- 使用以点号分隔的段。数组元素使用数字段,例如
$.readings.1;不支持方括号语法。 - 缺失键、超出范围的数组索引、无效 JSON 和空负载均以失败关闭处理。
- 没有通配符、过滤器、递归下降或内置变量。
直接数值形式
没有前导 $ 的条件会将整个负载作为数字进行比较。这对于标量事件负载很有用。
| 表达式 | 有效负载 | 匹配 |
|---|---|---|
> 0 | 1 | 是 |
> 0 | 0 | 不 |
>= 5 | 6 | 是 |
< 100 | 50 | 是 |
== 42 | 42 | 是 |
!= 0 | 1 | 是 |
> 3.14 | 3.15 | 是 |
> 0 | not a number | 不 |
运算符
比较使用一个运算符。源拥有的创作目录是:
==:是!=:不等于>:大于>=:至少为<:小于<=:不超过
解析器会优先匹配最长的运算符标记。JSON 路径比较会先尝试进行数值比较。如果两侧都能解析为数字,则按数值进行比较;否则按字符串进行比较。比较值两侧的双引号会被去除,因此字符串字面量需要加引号:$.status == "critical"。直接数值条件仅支持数值:如果任一侧无法解析为数字,则不会匹配。
条件评估器将JSON布尔值转换为字符串 true 和 false,因此请将它们作为带引号的字符串进行比较,例如 $.active == "true"。
条件是单个比较。不支持 AND、OR 和 NOT 等逻辑组合运算符。
6. 验证
使用:
sh
zeroclaw sop validate
zeroclaw sop validate <name>
验证会在名称/描述为空、触发器缺失、步骤缺失以及步骤编号存在间隙时发出警告。