审批与执行链
已被取代(2026-10-21):变更集审批、独立审批人、两人规则和执行链已随旧服务器退役。现在没有审批流,只有许可:所有者发布任务包并通过发布校验,管理员分配与配置;结果由 Hub 的边界校验给出,错误码只来自合约的登记表。见通用模型的 B04、T02、T03、P11;本文只作记录保留。
只有一个审批概念:变更集
一次提交 = 一个变更集 = 一次决定。变更集里可以有多个对象的资产:一个模块里的技能包、产品线清单、一条规则、一个连接器实例,独立审批人看一次、决定一次,决定绑定所有资产摘要的合集;通过后每个资产的 lifecycle 原子更新,驳回则全部 declined。
- 审批请求绑定三项:
inputRevisionId(任务输入修订)、targetIdentity(各项 sha256 的有序摘要)、actionDigest(动作与参数的摘要)。任一变化,请求失效,重新申请。 - 申请人 ≠ 审批人。平台不是审批人:记录里只写「按规则 X vN 核验」,不写 approved。
- 变更集引用的东西不重批:技能包引用的连接器、原件、网关都是已批准的资产。平台核验只检查引用的都在、都是 approved / running;没就绪就把请求挂成 blocked 并列出原因,依赖变化时重核。
- 同一资产的新变更集取代旧的:旧请求关闭为 superseded。
- 要重批的只有两种情况:字节变了(新版本),或动作变了(登记方法 ≠ 在设备上执行)。
- 审批请求与其等待输入共用有效期:默认一天,可按部署配置调整(一秒到三十天);普通澄清输入的有效期独立(默认五分钟)。延长有效期不解除修订、目标身份、动作摘要与执行出口复核。
- 每一次已授权的回应尝试都记入决定,包括被拒绝的尝试;对同一请求的重复决定是
DECISION_CONFLICT。每条决定记录保持作出时的策略与资格上下文,不随后续回应或策略修改重写。 - 待批草稿对同租户、同授权范围且持 approve 权限的人可见,供其审阅定义与摘要;这不把审批人加进 readers,不授予原件、知识内容、凭据或运行的权限。凭据引用保持私有,审批完成或停用后不再借待批可见。
- 引用的资产不是 approved / running 时,变更集保持 open 并标 blocked,原因逐项列出;批准它是
DEPENDENCY_NOT_APPROVED并附原因。依赖的生命周期变化经持久队列触发重核,重启后同样重核。 - 回应必须原样回传绑定三项;修订、目标身份、动作摘要任一不同即失效。驳回一个已被取代的旧申请,沿该申请自己的绑定回
approve:false:核对的是它的修订、申请人、摘要与当时的资产版本,不要求原申请人仍有权限。驳回使该请求 declined、其登记任务正常结束——这是治理流程的完成,不代表内容获批;新版本的批准与执行资格不受影响。 - 单项资产的登记接口(资产、技能、模块)各自自动产生单项变更集;涉及多项的变更必须走变更集的审批接口一次决定,不能逐项批准。
- 任务的待回应输入绑定当前输入修订:回应与替换二选一,不借自然语言消息隐式修改;对旧修订的回应是
INPUT_REVISION_STALE,超期是INPUT_EXPIRED;终态与 unknown 的任务不接受输入替换,新的工作用新的提交表达。输入修订换版后,旧的执行绑定与等待原因随之作废。
运行时没有审批,只有执行链
变更集批准的那一刻,人已经决定了三件事:方法(字节摘要)、它声明要碰的资源(writes)、谁能用它(范围策略)。之后每次运行,平台做的不是决定,是执行人定好的策略:
- 权限:这个人在这个范围有执行权,资源策略允许他——策略是数据,批过一次。
- 绑定:方法版本 approved、资源版本 running、原件 approved;领取时钉住 pin。
- 核验:确定性规则——命令在绑定的树里、函数契约、自毁规则、资源回报的构建 = 绑定的构建。
- 租约:资源租约 + fencing,解决的是冲突,不是授权。
- 审计与证据:每一步原样留痕。
本地 Circle 跑和 hub worker 跑走同一条链:只有 worker 碰外部系统,模型跑在哪里不影响。
领取之后、投递之前还有一道强制复核:平台重新取回当前的规则原件再核一次,结果必须与批准的摘要一致,普通查询留下的旧缓存不能放行。复核记录与当前操作和 fence 绑定持久化,被一次性消费之后才授权派发意图——派发意图只证明平台准许了这一次提交,不证明资源已经派发。
结果只有三种
| 结果 | 条件 | 平台做什么 |
|---|---|---|
| 通过 | 五步都过 | 执行,产物带 sha 交付 |
| 报错 | 脚本能 100% 证真的失败 | 返回固定错误码和事实(哪条命令、哪棵树、规则版本、两个构建值、evidenceId)。申请人改输入能修的,任务停在 waiting-input 并写 waitingReason;否则 failed |
| 未知 | 脚本确定不了 | 状态 unknown,披露观察到的一切,不重发;申请人选择核对(reconcile)、标记失败或放弃 |
审批人在这些情况里做不了任何有意义的决定:他不能让命令出现在树里,也不能让设备变成另一个构建。所以运行时没有「异常审批」,也没有「预授权」。
错误码目录
可证真失败的标识全部固定;客户端按码分支,不解析自由文本。本表与 packages/contracts/src/index.ts 的注册表逐项核对,HTTP 状态与类别以注册表为准;包可经 registerErrorCodes 扩展注册表,见表末两条注。
| 组 | 码 | HTTP · 类别 | 含义 |
|---|---|---|---|
| 受理与请求 | INVALID_PAYLOAD |
400 · validation | 请求形状或取值非法;非法取值不静默截断 |
| 受理与请求 | BODY_TOO_LARGE |
413 · validation | 请求体超过上限 |
| 受理与请求 | UNSUPPORTED_VERSION |
400 · validation | 能力包未安装,或钉住的流程版本没有运行器 |
| 受理与请求 | RESULT_TOO_LARGE |
413 · validation | 结果超过上限;不截断冒充全量 |
| 受理与请求 | SKILLPACK_CHECK_FAILED |
422 · validation | 技能声明的工具与试跑记录或当前目录不符 |
| 受理与请求 | IMPORT_NOT_DECLARED |
422 · validation | 模块出现声明外的 import |
| 受理与请求 | TARGET_NOT_FOUND |
404 · target | 目标不存在 |
| 受理与请求 | RATE_LIMITED |
429 · transient | 超过速率上限;不回显凭据或用户名 |
| 身份与权限 | UNAUTHENTICATED |
401 · permission | 凭据无效或过期 |
| 身份与权限 | FORBIDDEN_SCOPE |
403 · permission | 越出授权范围 |
| 身份与权限 | FORBIDDEN_ACTION |
403 · permission | 未持有该动作 |
| 身份与权限 | FORBIDDEN_TARGET |
403 · permission | 目标越出已认证的策略 |
| 身份与权限 | PERMISSION_DENIED |
403 · permission | 执行链的权限拒绝 |
| 身份与权限 | SELF_APPROVAL |
403 · permission | 申请人不能批准自己的请求 |
| 身份与权限 | SHARING_REQUIRED |
403 · permission | 共享或提升需要显式许可 |
| 身份与权限 | APPROVAL_REQUIRED |
409 · permission | 需要已批准且激活的登记 |
| 身份与权限 | CONNECTOR_DISABLED |
409 · permission | 连接器已停用 |
| 身份与权限 | IDENTITY_UNMAPPED |
409 · permission | 身份无法映射到授权主体(在册,暂无抛出处) |
| 身份与权限 | OBJECT_NOT_VISIBLE |
404 · permission | 对象对当前身份不可见 |
| 身份与权限 | ASSET_NOT_VISIBLE |
404 · permission | 资产对当前身份不可见 |
| 绑定与冲突 | IDEMPOTENCY_MISMATCH |
409 · conflict-stale | 同一幂等键带了不同内容 |
| 绑定与冲突 | INPUT_REVISION_STALE |
409 · conflict-stale | 回应针对旧的输入修订 |
| 绑定与冲突 | INPUT_EXPIRED |
409 · conflict-stale | 回应超过有效期 |
| 绑定与冲突 | DECISION_CONFLICT |
409 · conflict-stale | 同一请求的重复决定 |
| 绑定与冲突 | APPROVAL_STALE |
409 · conflict-stale | 审批绑定的修订或记录已变化 |
| 绑定与冲突 | SOURCE_HASH_MISMATCH |
409 · conflict-stale | 来源字节的 SHA 与登记不符 |
| 绑定与冲突 | TOOL_NAME_CONFLICT |
409 · conflict-stale | 工具名冲突 |
| 绑定与冲突 | RUNTIME_VERSION_MISMATCH |
409 · conflict-stale | 运行器的可执行文件或修订核对不符 |
| 绑定与冲突 | TARGET_IDENTITY_MISMATCH |
409 · target | 目标身份与钉住的值不符 |
| 租约 | FENCING_STALE |
409 · lease | fence 已不是当前租约 |
| 租约 | LEASE_EXPIRED |
409 · lease | 执行租约已过期 |
| 租约 | RESOURCE_BUSY |
409 · lease | 资源因 unknown 冻结,新投递被拒 |
| 来源 | SOURCE_ABSENT |
404 · source-absent | 来源不存在 |
| 来源 | SOURCE_FORBIDDEN |
403 · permission | 来源无权限;普通调用的收敛码 |
| 来源 | SOURCE_LOGIN_EXPIRED |
401 · authentication | 来源登录失效 |
| 来源 | SOURCE_UNREACHABLE |
503 · source-unreachable | 来源不可达 |
| 来源 | SOURCE_NOT_APPROVED |
409 · dependency | 来源未批准,含明文例外失效后的配置失配 |
| 执行 | COMMAND_NOT_IN_TREE |
422 · execution | 命令不在绑定的树里 |
| 执行 | DESTRUCTIVE_COMMAND |
422 · execution | 命中自毁规则 |
| 执行 | BUILD_MISMATCH |
409 · execution | 回报的构建与绑定的构建不符 |
| 执行 | GATEWAY_UNREACHABLE |
503 · execution | 网关不可达 |
| 执行 | LEASE_UNAVAILABLE |
409 · execution | 租约不可用 |
| 执行 | RUNTIME_FAILED |
502 · execution | 运行器失败或私有执行请求被拒绝 |
| 执行 | SANDBOX_UNAVAILABLE |
503 · execution | 没有受支持的隔离适配器 |
| 执行 | HOOK_FAILED |
422 · execution | Hook 处理器未返回合法判定 |
| 执行 | RECEIPT_PROCESSING_FAILED |
500 · execution | 回报处理失败,不带内部细节 |
| 依赖与证据 | DEPENDENCY_NOT_APPROVED |
409 · dependency | 引用的依赖未批准或未运行 |
| 依赖与证据 | EVIDENCE_INSUFFICIENT |
409 · semantic-insufficient | 证据不足以支撑结论 |
| 依赖与证据 | ARTIFACT_VERIFICATION_FAILED |
409 · semantic-insufficient | 制品或证据与待证声明核对不符 |
| 暂时与兜底 | UPSTREAM_TIMEOUT |
504 · transient | 上游超时;只读操作可换新键重试 |
| 暂时与兜底 | INTERNAL_ERROR |
500 · internal | 内部兜底;未注册的码回落到它的状态与类别 |
包经 registerErrorCodes 注册的码:CURSOR_EXPIRED(409 · validation)与 NOTIFICATION_STREAM_LIMIT(429 · permission)由 tasks 包注册;INVALID_EXECUTION_WORKBOOK(422 · validation)、COMMAND_VALIDATION_FAILED(422 · validation)与 GATEWAY_REFUSED(422 · execution)由执行产品包注册。
抛出但未注册的码:CONFIRMATION_REQUIRED、CONFIRMATION_STALE、MODEL_PROXY_UNAVAILABLE、MODEL_UPSTREAM_REJECTED——它们回落到 INTERNAL_ERROR 的 HTTP 状态与类别,只保留码本身。
报错与等待输入
申请人改输入能修的失败停在 waiting-input:来源未批准、网关不可用、租约不足、构建不符各写各的 waitingReason,待回应列表里有对应请求,申请人用原输入接口交新修订;修订保留私有归属与委托关系,清掉旧的执行绑定与等待原因。命令缺失、参数违规、权限拒绝这类改输入修不了的,直接 failed。
来源失败的分类
来源失败对普通调用收敛:不存在与无权限统一报 SOURCE_FORBIDDEN,持诊断权限(sources.diagnose)的身份才看到完整分类。只有确认不存在才记入不存在缓存;登录失效、权限拒绝、源不可达都不当作缺失。失败的 details 只定位调用者自己提交的引用,不泄露记录的存在性、归属或内容。
读取的超时
声明 effect=read 的操作,其上游超时、同步等待超时、回报未知或租约失联,一律 failed + UPSTREAM_TIMEOUT:retryable=true、sideEffects=none,不冻结资源,也不要求独立副作用核对;换新键或修改查询可重试。写操作维持原规则:unknown、冻结、独立核对。
未知的纪律
失去租约、worker 断开、投递后取消、超时:操作转 unknown,资源冻结,新投递报 RESOURCE_BUSY;不重发、不自动重派,旧 fence 的回报被拒绝。取消只是一个请求,本地观察到的进程退出不等于远端停止,remoteStoppingVerified:false 如实写出。核对由另一个 worker 用原身份做只读核查:终态与输入摘要都对上才 reconciled、交付并解冻;核查未见终态,只有核查操作自己 failed,原任务保持 unknown、资源保持冻结。没有远端任务身份的投递断线不能自动恢复,先在资源侧对账,不盲目重投。
派发前拒绝与远端租约
资源在租约、环境、契约校验阶段的明确拒绝——回执没有任务身份、没有已派发标记——是可证真的 failed,不冻结资源;原始理由与完整回执保留在 gatewayRefusal,有明确码用明确码,否则 GATEWAY_REFUSED;不把环境阶段的拒绝一律当作构建不符,没有阶段信息的文字不猜派发状态。带任务身份或已派发标记的回执、提交之后任何阶段的失败、没有可证阶段的错误,一律 unknown,保留可能仍活跃的远端租约。只有证明未派发或确认终态之后才释放远端租约;释放结果如实记录(remoteLeaseReleased true/false,失败保留其原因),不覆盖主错误。
生产设备要两人规则怎么办
不另造概念:资源在登记时声明 executionIsChange: true,对它的执行提交先登记为 execution-submission 草稿资产(冻结输入修订、方法摘要、资源版本、原件 id、工作簿 sha),作为变更集的一项,批准后才进入执行链。实验床默认不声明,直接走执行链。一个概念,两种声明。这样的批准只能被对应的操作消费一次,一个决定不能重派给另一个操作。
凭据与资源
- 凭据只写不读:登记或轮换时进保险箱,执行时由 worker 注入,回给客户端的一切都过脱敏。
- 模型密钥也不进 harness 进程:worker 为每次运行起一个只绑回环临时端口的模型代理,harness 的凭据文件里只有该端点和一次性 nonce;代理核对 nonce、剥掉入站凭据头、注入真实密钥、只转发到已批准 runtime-profile 里的模型端点,运行结束即关闭。代理不记请求体,事件里只有元数据;起不来或上游拒绝以固定错误码失败,绝不退回把真实密钥写进文件。出口掩码(run token、nonce、模型密钥)是最后一道,不是边界。
- 凭据模式三种:平台签发的短期令牌、外部 OAuth、静态密钥。只有一个调用方的资源(例如 hub 的 worker 调用的网关)用静态密钥,本地比对,不回调平台。
- 端点、CA、超时在平台主机的运行 profile 文件里,不在数据库,不在 Git;runtime-profile 作为资产绑定 profile 名、版本和配置摘要,依赖要求 approved / running。
- 本地取消 ≠ 远端停止;超时先查原操作和现场;确认不了就保持 unknown。
- 静态密钥模式:值有最小长度要求,登记与轮换只写不读,响应只有引用与模式;worker 在每次已核授权的调用前取当前值注入。批准且 running 的共享资源允许当前授权成员使用其绑定密钥,成员不因此看到凭据的元数据或明文。停用凭据、资源修订、角色撤销、租约或 fence 变化都触发重新核对。
- 平台签发的短期令牌(run token):只授权本次运行声明的工具、范围与任务,绑定操作与 fence;由持有效租约的 worker 续期,截止不超过该操作的期限;任务结束、取消、租约过期、旧 fence 或输入修订变化都立即失效。它经本机私有 IPC 交给子进程,只存在于内存与进程环境,不落盘;不能走会话续期或注销接口,也不能借它登记资产、提交别的任务或审批。单个 worker 同时只容纳一个运行槽,该槽同时服务这次运行的子工具调用。
- 外部 OAuth:签发方与端点只在仓库外的签发配置里,连接器配置引用它,请求不能传任意端点;远端端点必须 HTTPS(回环合成测试除外),CA 每次核验,不跟随重定向。设备码与客户端凭据两种授权成功后只返回凭据引用与到期元数据,值不进 API、事件、审计或仓库;不同范围或所有者不能互借。续期在私有单写者锁内原子替换:未轮换时保留旧的刷新令牌;回执未知或结果不完整时停止重放、标记轮换待定并要求重新登记,避免旧令牌重放吊销整族;明确的 5xx 保留原会话;既无 expires_in 也无配置默认期限时,到期时间为未知,不虚构期限。
- 明文例外:非回环且带认证的资料源默认必须 HTTPS。确需内网纯 HTTP 时,管理员在对应 runtime-profile 资产的配置里声明
allowPlaintextAuthentication: true,该版本经独立审批人决定后才生效——即使范围允许普通自决,这条例外也不能自批;在运行文件里直接加这个字段不授予例外。当前版本不再 approved/running 或配置摘要不匹配时,调用是SOURCE_NOT_APPROVED;未批准的 HTTP 认证是SOURCE_FORBIDDEN,不附带原地址或凭据。每次获准的明文认证,在派发意图、审计与取回的证据里永久标记plaintextAuthentication: true及批准它的资产版本引用;缓存重读保留标记,后续撤销不改写历史证据。 - 模型代理的边界:只绑一个回环临时端口,一次性 nonce 仅本次运行有效;目标由当前批准的运行配置钉住,Host、查询串与路径不能换目标,入站凭据头全部剥离。请求与响应各限 16 MiB,SSE 按流透传,不记体、不把模型流量存为证据;事件只记 upstreamHost、statusCode、requestBytes、responseBytes、durationMs、requestNumber。起不来或断连是
MODEL_PROXY_UNAVAILABLE,上游拒绝或超限是MODEL_UPSTREAM_REJECTED;取消、超时、unknown、本地结束都关闭代理,关闭之后迟到的事件不覆盖已观测的本地退出。 - 隔离边界:Skill、Tool、Hook 共用同一条主机隔离边界——未信进程及其后代的文件读写、网络出口与进程信息都受限,只开放本次运行目录、固定 harness 的受信代码与依赖、明确的 CA 与回环端点;不能直接或经符号链接读 vault 或运行配置。没有受支持隔离适配器的系统以
SANDBOX_UNAVAILABLE失败,不退回同用户任意执行。Hook 与 Tool 在同一 tool-host 子进程执行:十秒与 16 MiB 的返回上限、256 MiB 堆上限;Hook 按最多 200 个待核对象一批顺序执行,首个 block 即停;待核对象先按严格 schema 校验,主机命令行只按字面解析。 - 出口掩码:产物与事件对 run token、代理 nonce、模型密钥按字节替换,其余字节原样保留,记录 sanitized 并重算 SHA。掩码是最后一道,不是边界。
- worker 的排空与替换是合作式:收到替换请求的 worker 停止领取新活,已在途的领取可以做完再退出,不为赶升级期限强杀设备操作;替换只换进程,不动数据。独立部署的 worker 由它自己的进程管理器排空与替换,平台的替换请求标志不代表它已升级。
执行链在设备执行上的固定规则
执行链是通用的;它在「表格驱动设备执行」这类工作上的每一步规则如下。允许的设备方法、检查点种类、主机名集合、设备命名规则都是产品契约声明的闭集,内容在 packs,不进本标准。
核验
- 任务先核验后领取:平台用保留的原字节重新定位构建、解析完整命令路径与重载,重读工作簿全部设备命令(含初始化行),执行确定性的命令核对;missing、参数违规、不能证明的输入都是 failed。
- 关口只接受字面行:方法闭集由产品契约声明,闭集之外、输入变量、临时寄存器替换、模糊的调用与未验证的函数,一律
INVALID_EXECUTION_WORKBOOK,必须先展开成可核对的字面行;禁用的生命周期方法不放行。一次执行的分派条数有上限,设备索引再受当前设备数限制。 - 主机方法闭集来自资源声明与产品契约,主机命令按字面 shell 实参契约解释:多行整体为一个参数,单行只在引号外按逗号分隔,外层引号去掉,关键字参数只许契约声明的键。输出寄存器保留供后续检查点,输入寄存器替换禁止。闭集可由批准的 Hook 用资源声明里的允许主机集合收窄,省略或空数组沿用产品契约;运维也可用主机命令策略按主机收窄(精确命令名单、字节与超时上限、是否允许多行)——收窄不扩展,策略随运行配置钉入目标身份,改变后旧批准失效。角色没有已验证映射的主机方法明确拒绝,不按名称推断。
- 错误定位形状固定:row(从 1 起)、autoid、object、method,非字面单元格另带列;整本格式错误对应字段为 null。不回显主机命令与寄存器内容。
派发与两次环境观察
- 投递前平台强制重新取回当前命令原件并再核对一次,结果必须与批准的树和命令证明一致;随后取资源租约,做环境核对(构建、设备数、就绪),不满足就拒绝投递。不自动换构建、换测试床或初始化设备。
- 环境观察(
execution.environment.observed,observationSource 为提交前的环境自检)只证明提交前那一刻的外部核对;资源随后可以再做它自己的环境与契约核验,第一次 passed 不覆盖第二次的拒绝,第二次的原始理由原样保留。
设备归属与日志解析
- 工作簿里的设备序号按设备管理地址在配置里的顺序确定,设备名不是排序依据。有序地址的来源按优先级取:环境自检回报的有序地址字段、拓扑的绑定地址集、拓扑设备的名字规则;前两个只在实际返回时使用。第三个来源成立的条件:全部设备名符合产品契约的命名规则、序号集合与设备数精确一致,且每条日志的地址唯一落在对应设备的接口地址集合里。名称任意、序号缺失/重复/越界、地址归属不唯一,都不归属:对应设备步 unknown、unmapped=device_map_unavailable。不据任意主机名或命令文本猜设备序号;共享地址不归属任何设备,也不推进任何设备的命令游标。
- 解析按设备推进命令游标:同一条命令跨设备或在同一设备重复出现,各有自己的日志区间;拿不到命令只让对应步骤 unknown。每个检查点只由明确的日志帧与工作簿行匹配,数量、模式、方法、时效或用例身份不匹配就是 unknown/unmapped。整案结论不扩大为逐步结论:整案通过不证明每一步通过;任务 status=succeeded 不等于测试全部通过;缺失证据交付 partial。主机方法的日志按主机名匹配,与设备索引是两条独立路由。
本文是 circle-hub 的设计规范之一,按 CC BY 4.0 授权:引用、借鉴须署名 Circle Hub 与 qingshanfeihu 并注明原始出处链接。见设计规范索引。