☰
IronClaw Reborn 审批解析契约(V1):从 RequireApproval 到一次性能力租约的完整闭环
2026/9/25 17:42:37 网站建设 项目流程
  • 人工智能
  • AI 应用
  • 交互助手
  • AI Agent

【免费下载链接】ironclaw

IronClaw is an Agent OS focused on privacy, security and extensibility

项目地址:https://gitcode.com/gh_mirrors/iro/ironclaw
点击查看免费下载

本篇技术指南聚焦 IronClaw(Agent OS)内核的审批解析契约切片:ironclaw_approvals如何把授权器返回的RequireApproval待决请求,通过ApprovalResolver解析为带调用指纹(invocation fingerprint)的一次性CapabilityLease或有记录的拒绝(Deny),再经LeaseBackedAuthorizer与CapabilityHost::resume_json/auth_resume_json完成"批准一次、只放行一次、禁止输入重放"的安全闭环。读者将掌握 V1 审批状态机、指纹防重放、租约生命周期(Active → Claimed → Dispatching → Consumed)以及 fail-closed 恢复路径等可直接落地的实现细节。


1. 契约定位与整体工作流

ironclaw_approvals是主机(host)控制平面服务,负责把持久化的审批请求解析为有边界的授权租约。它不做用户提示、不渲染 UI、不执行能力、不预留资源、也不调度运行时工作——审批通知与审批解析、租约签发是分离的:ApprovalResolver只把已存储的待决审批解析成精确作用域的租约,不负责选择投递目标、渲染提示界面或发送通知。

契约文档(docs/internal/reborn/contracts/approvals.md)给出的端到端流程为:

CapabilityHost -> Authorization returns RequireApproval -> ApprovalRequestStore saves Pending request under tenant/user/agent scope -> ProcessJournalStore suspends the invocation on an approval gate ApprovalResolver -> reads Pending ApprovalRecord under the same tenant/user/agent scope -> approve: marks Approved as the durable decision, then issues a scoped CapabilityLease carrying the invocation fingerprint -> deny: marks Denied and issues no lease -> optionally emits metadata-only AuditEnvelope::approval_resolved records LeaseBackedAuthorizer -> combines ExecutionContext.grants with active non-fingerprinted scoped leases -> returns Allow/Deny before CapabilityHost dispatches runtime work CapabilityHost::resume_json -> reloads the approved record and matching fingerprinted lease -> compares the replayed invocation fingerprint -> claims the lease before runtime dispatch -> dispatches and consumes the claimed lease on success CapabilityHost::auth_resume_json -> validates run record is BlockedAuth -> when approval_request_id is Some: locates matching fingerprinted lease (Active on first arrival, Claimed after a prior auth bounce claimed it) -> transitions lease to Dispatching via begin_dispatch_claimed (single-winner CAS) -> dispatches; on success consumes the Dispatching lease -> on non-terminal BlockedAuth re-bounce: reverts Dispatching → Claimed via abort_dispatch_claimed so the next auth_resume_json call can reuse the same lease without a second approval

从源码结构看,这一职责划分体现在 crate 依赖边界上:ironclaw_approvals依赖ironclaw_authorization(签发租约)、ironclaw_trust、ironclaw_runtime_policy、ironclaw_event_log(仅元数据审计)与ironclaw_filesystem(持久化),并被ironclaw_capabilities、ironclaw_host_runtime、ironclaw_assistant、ironclaw_composition等 8 个消费者引用(见 crates/kernel/ironclaw_approvals/README.md)。依赖方向是"膜(membrane)依赖本 crate,绝不反向",ironclaw_capabilities、ironclaw_host_runtime、ironclaw_processes等 crate 被显式禁止出现在本 crate 的依赖列表中。

2. 审批请求状态机与作用域隔离

审批记录与模型可见的 gate 记录都存放在ironclaw_approvalscrate 中。V1 状态模型为:

pub enum ApprovalStatus { Pending, Approved, Denied, Expired, }

在 approval_store.rs 的实际实现中,ApprovalStatus还额外包含一个Discarded变体:它是discard_pending回滚路径写入的墓碑(tombstone),用于防止同一条request_id被save_pending复用;get与records_for_scope都会过滤掉Discarded记录。Expired在状态机中保留为显式过期位,而租约层面的过期由GrantConstraints.expires_at在授权、claim、consume 时统一强制。

ApprovalRequestStorePort提供作用域化的解析方法:

async fn approve(scope, request_id) -> Result<ApprovalRecord, RunStateError>; async fn deny(scope, request_id) -> Result<ApprovalRecord, RunStateError>;

所有操作都是 tenant/user/agent 作用域的。用错误的 tenant/user/agent 解析某个请求会返回 unknown request 错误,且不得泄露其他 tenant/user/agent 是否存在匹配的 UUID。ApprovalRequestStore::read_versioned在反序列化后调用same_scope_owner二次校验(approval_store.rs),update_status的 CAS 闭包在每次重试重读后也强制作用域归属校验,形成纵深防御:错误作用域的读取一律表现为Ok(None)。

存储路径布局(挂载别名/approvals、/gate-records下):

/approvals[/agents/<agent>][/projects/<project>][/missions/<mission>][/threads/<thread>]/<request_id>.json /gate-records[/agents/<agent>][/projects/<project>][/missions/<mission>][/threads/<thread>]/<gate_ref>.json

tenant + user 身份不写进路径——它们位于调用方MountView的每租户MountAlias重写中,由ScopedFilesystem在每次操作前强制 ACL。每个文件写入都带RecordKind标签(approval_record/gate_record),与其它类型化记录共用同一 fail-closed CAS 门。

GateRecordStore是模型可见 gate 内容的持久化载体:Resolution控制平面只携带不透明的GateRef,loop 渲染待决 gate 所需的内容(认证凭据要求、依赖运行的分阶段结果句柄、脱敏摘要)都存在GateRecord中。因为 gate 阻塞于一个 turn、在之后的 turn恢复,该记录必须跨 turn 存活。GateRecordStore是 write-once 语义(重复保存返回GateRecordAlreadyExists),且有意不提供删除方法——宿主拥有的、模型可见的内容不做硬删除。

3. 调用指纹(Invocation Fingerprint):禁止输入重放

审批记录可以携带InvocationFingerprint:

pub struct InvocationFingerprint(String);

CapabilityHost在 dispatch 审批时,从以下要素计算指纹(approval.rs 的InvocationFingerprint::for_dispatch/for_spawn/for_action实现):

version kind = dispatch ResourceScope, including invocation_id CapabilityId ResourceEstimate canonical JSON input with object keys sorted recursively

指纹的序列化载荷结构为{ version, kind, scope, capability, estimate, input },其中input先经过canonical_json_v1递归排序对象键规范化(深度上限 64 层,超出 fail closed;数组保持顺序,标量原样返回),再整体做 SHA-256,最终以sha256:<lower-hex>形式存储。存储的是摘要而非原始 JSON 输入,因此 resume 路径在 dispatch 前比对摘要,一份输入获批的审批无法被另一份输入重放。

作者化(authorizer)侧的 fail-closed 规则:若作者化返回Decision::RequireApproval且未携带指纹,CapabilityHost会补上计算出的指纹;若作者化返回了不同的指纹,CapabilityHost在保存审批请求之前就关闭(fail closed)。Decision枚举定义在 decision.rs:Allow { obligations }、Deny { reason }、RequireApproval { request }三态,审批正是第三条路径的入口。

4. 能力租约(Capability Lease)与生命周期状态机

已批准的 dispatch 请求在ironclaw_authorization中签发CapabilityLease(ironclaw_authorization/src/lib.rs):

pub struct CapabilityLease { pub scope: ResourceScope, pub grant: CapabilityGrant, pub status: CapabilityLeaseStatus, }

租约包装一个普通CapabilityGrant,因此既有授权的权威形状(authority shape)保持不变:

capability principal/grantee allowed effects mount/network/secret/resource constraints expiry max invocations

租约新增宿主管理的生命周期状态:

pub enum CapabilityLeaseStatus { Active, Claimed, Dispatching, Consumed, Revoked, }

Dispatching是auth_resume_json期间由begin_dispatch_claimed设置的瞬态,用于对并发复用Claimed租约实施 single-winner。第二个并发auth_resume_json若发现租约已在Dispatching,将收到InactiveLease——与并发Active租约claim()竞态中败者路径完全一致。abort_dispatch_claimed在非终止性 auth 再弹回(re-bounce)时把Dispatching还原为Claimed,使下一次尝试无需二次审批即可复用同一租约。

CapabilityLeaseStorePort新增两个操作:

  • begin_dispatch_claimed(scope, lease_id, fingerprint)— 原子地把Claimed、指纹匹配、未过期的租约迁移到Dispatching。
  • abort_dispatch_claimed(scope, lease_id)— 把Dispatching还原为Claimed;已处于Claimed时为 no-op 安全。

consume与revoke都接受Claimed与Dispatching作为合法源状态(终止结果可以从两者之一消费或撤销)。

4.1 存储与作用域语义

V1 提供内存与文件系统两种租约存储,支持精确的 tenant/user/agent/invocation 作用域查找、claim、消费与撤销。文件系统租约持久化在:

/engine/tenants/{tenant_id}/users/{user_id}/agents/{agent_id-or-_none}/capability-leases/{invocation_id}/{lease_id}.json

租约的查找、claim、消费、撤销不是按全局 ID 进行的:作者化器询问的是当前ExecutionContext.resource_scope可见的未过期活动租约。本切片把已签发的审批租约视为一次性调用租约:租约只授权具有与已批准请求相同 invocation ID 的上下文;更广泛的可复用审批作用域属于后续策略切片。

租约保存审批请求的指纹,使 resume 可以验证重放的调用请求与已批准的请求一致。带指纹的审批租约不会转换成普通 grant 供普通invoke_json使用;它们只能被resume_json或auth_resume_json使用——这两个路径会比对指纹并在 dispatch 前精确 claim 该租约。

4.2 Claim 与消费的并发安全

Claim 强制要求租约处于活动、未过期、未耗尽状态,且指纹与重放请求相等。已 claim 的租约对普通授权隐藏,因此第二个并发 resume 无法用同一张一次性审批租约再次 dispatch。CapabilityLeaseStore的底层用按 owner 的进程内互斥锁 + 版本化 CAS(update_lease_cas,最多 3 次重试,见 ironclaw_authorization/src/lib.rs 的CAS_RETRY_ATTEMPTS)保证多进程并发下的安全:VersionMismatch触发重读重写,预算耗尽返回CasExhausted(调用方可按瞬时错误高层重试)。

租约消费强制GrantConstraints.max_invocations:

Some(n > 1) -> decrement and remain Active Some(1) -> decrement to Some(0) and mark Consumed Some(0) -> reject as exhausted None -> no invocation-count decrement

带指纹的一次性租约在消费时直接把剩余次数清零并标记Consumed(Consumed状态拒绝再次消费,返回ExhaustedLease),从根本上杜绝一次性授权被消费两次;无指纹的多调用租约则递减计数。过期(expires_at)在授权、claim、消费三个阶段都会被强制执行(ensure_not_expired_or_exhausted与grant_is_active共用Utc::now()判断)。

4.3 租约的防御性写入

每次租约与租约索引写入都会投影tenant_id索引键(authorization_by_tenant精确索引,best-effort 声明):路径前缀作用域是主隔离边界,索引投影是"双保险"——即使出现路径重写 bug,也会在查询时暴露为不匹配而非静默跨租户泄露。字节型后端(如DiskFilesystem)不支持版本化 CAS 与索引投影时,写入会降级为CasExpectation::Any+ 丢弃投影,改由按 owner 互斥锁承担排序安全不变量。

5. ApprovalResolver:解析器实现与 fail-closed 排序

ApprovalResolver只解析Pending记录;对已批准、已拒绝或已过期的记录执行 approve/deny 会失败且不改动该记录。它把待决的 dispatch 或 spawn 审批转变为租约(lib.rs):

let lease = resolver .approve_dispatch( &scope, approval_request_id, LeaseApproval { issued_by, allowed_effects, expires_at, max_invocations, }, ) .await?;

注意LeaseApproval在源码中的实际形状是{ issued_by: Principal, constraints: GrantConstraints }——constraints是解析器盖章到 resume-only 租约上的最终衰减授权形状。由于ApprovalRequest不携带发起能力描述符的完整 grant 约束,调用方必须从向审批人展示的同源描述符/请求中推导这些值,而不是在 UI 层放宽它们(lib.rs 的文档注释)。

5.1 dispatch 与 spawn 的租约构造

dispatch 审批的租约 grant 使用:

grant.capability = capability from Action::Dispatch grant.grantee = ApprovalRequest.requested_by grant.issued_by = LeaseApproval.issued_by grant.constraints.allowed_effects = LeaseApproval.allowed_effects grant.constraints.expires_at = LeaseApproval.expires_at grant.constraints.max_invocations = LeaseApproval.max_invocations lease.invocation_fingerprint = ApprovalRequest.invocation_fingerprint

spawn 审批走approve_spawn:应用同样的租约字段,但要求ApprovalRequest.action = Action::SpawnCapability,并以该能力 id 作为grant.capability。capability_for_action辅助函数对(Dispatch, Action::Dispatch)与(Spawn, Action::SpawnCapability)做精确匹配,其它组合返回UnsupportedAction错误。产品/WebUI 审批交互服务只有在 spawn 租约签发后才恢复停靠在审批 gate 上的 turn;若重试在 turn 仍停靠同一 gate 时观察到已批准请求,会先调用retry_lease_issue_for_spawn再恢复——使"解析器成功但协调器失败"的窗口可恢复。

拒绝只迁移审批记录并记录解析 actor:

resolver .deny( &scope, approval_request_id, DenyApproval { denied_by: Principal::User(scope.user_id.clone()), }, ) .await?;

被拒绝的请求不签发任何租约。

5.2 Fail-closed 解析排序与恢复窗口

审批解析围绕租约持久化采用 fail-closed 排序(源码中标注为审计发现 F2 的修复):

  1. 重新读取审批请求并要求其为Pending。
  2. 把审批请求标记为Approved——该审批记录是持久化的决策权威。
  3. 构建并持久化精确的带指纹租约。
  4. 若审批状态写成功之后租约持久化失败,保持请求为Approved并返回租约错误。

这样杜绝了"租约已经存活、而审批请求仍以Pending呈现给用户可操作"的窗口。由于解析器横跨两个独立存储,审批可能先于租约写成功变成Approved;调用方通过针对已批准请求、以相同租约条款调用retry_lease_issue_for_dispatch或retry_lease_issue_for_spawn来恢复该窗口。重试路径对审批记录幂等(不翻转状态),但底层租约存储每次调用生成新的CapabilityGrantId,因此部分成功后重试的调用方若不能接受重复租约,需用leases_for_scope比对去重。

approval_resolution_contract.rs(tests/approval_resolution_contract.rs)对这套语义有完整契约覆盖:

  • 租约写失败时请求保持Approved(不回滚到Pending);
  • 审批状态写失败时不产生孤儿租约(短路的顺序保证);
  • 并发approve_dispatch对同一请求是 first-write-wins:赢家恰好一个、败者收到NotPending(Approved)、租约存储中恰好一张租约;
  • 用错误租户解析请求 fail closed,双方作用域的租约存储均为空;
  • 审批记录缺少指纹时解析失败,无租约签发、状态不变;
  • 带指纹的租约不会变成普通invoke_json的授权(LeaseBackedAuthorizer::authorize_dispatch返回MissingGrant)。

5.3 元数据审计(best-effort)

配置了AuditSink时,审批解析可以发出 best-effort 审计记录:

let resolver = ApprovalResolver::new(&approvals, &leases).with_audit_sink(&audit);

成功的 approve 与 deny 迁移都会发出AuditEnvelope::approval_resolved(...)记录,包含AuditStage::ApprovalResolved、原始审批 correlation ID、审批请求 ID、摘要化 action 与DecisionSummary { kind: "approved" | "denied", actor: Some(resolver principal), ... }。

审计记录不包含审批理由、重放输入、调用指纹、租约 ID、租约内容、原始宿主路径或密钥值。同一脱敏契约适用于经JsonlAuditSink持久化到scoped_audit_log_path(&scope, "approval-audit.jsonl")的租户/用户/代理作用域虚拟路径。审计 sink 失败被忽略,绝不影响审批解析结果(测试approval_audit_event_sink_failure_does_not_change_resolution_outcome专门验证这一点)。

6. 授权集成:LeaseBackedAuthorizer 与 dispatcher 边界

LeaseBackedAuthorizer评估请求局部 grant 与活动的无指纹租约:

ExecutionContext.grants + CapabilityLeaseStore.active_grants_for_context(context) -> normal grant matching rules -> Decision::Allow | Decision::Deny

带指纹的审批租约被有意排除在active_grants_for_context之外(active_grants_for_context过滤掉invocation_fingerprint.is_some()的租约)。resume 期间,CapabilityHost先验证已批准的指纹并 claim 租约,再把已 claim 的租约 grant 作为请求局部授权传给重放的 dispatch。这把审批租约挡在"普通invoke_json的环境内授权"之外。

这保住了 dispatcher 边界:

caller -> CapabilityHost -> authorizer -> CapabilityDispatcher -> RuntimeDispatcher -> runtime

dispatcher 保持 auth-blind 与 state-blind:它从不解析审批、也从不检查租约。LeaseBackedAuthorizer的 grant 匹配规则(ironclaw_authorization/src/lib.rs 的authorize_from_grants_with_authority_ceiling)与GrantAuthorizer完全一致:按 capability 精确匹配、principal 与上下文匹配、grant 活跃、效果覆盖、资源估算在 ceiling 内,全部通过才Allow并附带义务(obligations)。

7. 当前边界(V1 有意保持的收窄)

契约文档明确列出了本切片的意图边界:

  • V1 租约仅限精确调用(exact-invocation):持久化审批以独立的 Reborn 审批策略记录表示,而不是放宽的 V1 租约。
  • 尚无跨审批状态更新与租约签发的单存储 ACID 事务:Approved而无租约的请求通过针对持久化决策重试租约签发来恢复。
  • 尚无 dispatch 与一次性 spawn 之外的审批支持。
  • 持久化审批策略的作用域:覆盖当前 Reborn sandbox 作用域(tenant_id、user_id、可选agent_id、可选project_id);thread_id从不参与作用域,因此在某一线程授予的"始终允许"适用于该用户相同 agent(及存在时的 project)的全部线程,且与渠道无关——WebUI 授予对解析为同一(user, agent)的 Slack 消息同样生效。预 DB 测试遗留的线程作用域本地审批策略文件不迁移、也不经兼容回退读取;切换到该作用域形态时需清空本地审批状态。
  • 持久化审批按 manifest 策略 fail closed:默认只允许default_permission为allow的能力做持久化复用;ask与deny保持一次性审批 gate,且在注入任何已存策略作为授权前会重新检查(permission_mode_allows_persistent_approval只放行Allow | Ask,见 policy.rs)。
  • revoke 仅在策略存储层暴露,供未来产品集成使用;本切片不定义面向用户的 revoke 流程。

在持久化/面向用户的审批恢复 UI 上线之前,宿主应重新评估审批记录、租约写入与 run-state 迁移是否应共享单一事务持久化边界。

8. 契约冻结附录:V1 租约作用域(2026-04-25)

V1 审批只解析为精确调用租约。一份有效的审批租约绑定:

tenant_id user_id project_id, if present agent_id, if present invocation_id capability_id invocation fingerprint expiry/status

它授权恰好一次重放被阻塞的调用。它不为未来调用授予可复用的作用域权限。可复用的作用域审批可能由 V2 设计,但绝不能由 V1 审批记录暗示存在。

9. 测试与验证路径

本契约的工程证据集中在两个 crate 的契约测试套件中:

  • crates/kernel/ironclaw_approvals/tests/:approval_resolution_contract.rs(解析语义、F2 排序、并发 first-write-wins、审计)、approval_store_contract.rs/approval_store_resolution_contract.rs(存储 CAS 与作用域)、gate_record_store_contract.rs(gate 记录 write-once 与错误作用域读取)、boundary_contract.rs(依赖边界)。
  • crates/kernel/ironclaw_authorization/tests/capability_lease_contract.rs:租约 issue/claim/consume/revoke 与begin_dispatch_claimed/abort_dispatch_claimed的状态迁移契约。

本地运行:

cargo test -p ironclaw_approvals cargo test -p ironclaw_authorization cargo test -p ironclaw_architecture_tests # 依赖/API 变更后

10. 小结

V1 审批解析契约把"人类或策略的同意"翻译成膜(membrane)可以执行的最小授权单位:一张绑定精确调用指纹、只允许一次重放、生命周期由宿主 CAS 控制的CapabilityLease。审批记录是决策权威(先 Approve 后签发租约,失败可重试恢复),审计只保留脱敏元数据,dispatcher 保持 auth-blind——四个约束共同保证:同意一次、放行一次、输入不可换、事后可追溯。

  • 人工智能
  • AI 应用
  • 交互助手
  • AI Agent

【免费下载链接】ironclaw

IronClaw is an Agent OS focused on privacy, security and extensibility

项目地址:https://gitcode.com/gh_mirrors/iro/ironclaw
点击查看免费下载

相关推荐

上一篇:theHarvester 入门指南:域与组织 OSINT 侦察的完整上手路径
下一篇:给 MCP 服务加上安全认证:Spring AI 对接鉴权 MCP 与 OAuth2/Nginx 认证网关实战

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询