- 人工智能
- AI 应用
- 交互助手
- AI Agent
【免费下载链接】ironclaw
IronClaw is an Agent OS focused on privacy, security and extensibility
本篇技术指南聚焦 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>.jsontenant + 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_fingerprintspawn 审批走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 的修复):
- 重新读取审批请求并要求其为
Pending。 - 把审批请求标记为
Approved——该审批记录是持久化的决策权威。 - 构建并持久化精确的带指纹租约。
- 若审批状态写成功之后租约持久化失败,保持请求为
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 -> runtimedispatcher 保持 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
相关推荐
IronClaw 审批决议机制深度解析:从 RequireApproval 到能力租约的内核管线
IronClaw 审批决议机制深度解析:从 RequireApproval 到能力租约的内核管线 导读 ironclaw_approvals 是 IronCla
人工智能AI 应用交互助手AI AgentIronClaw 内核审批解析(ironclaw_approvals)深入解析:从持久化审批请求到作用域能力租约
IronClaw 内核审批解析(ironclaw_approvals)深入解析:从持久化审批请求到作用域能力租约 导读 IronClaw 是一个以隐私、安全与可
人工智能AI 应用交互助手AI AgentIronClaw Reborn 身份层契约解析:从外部登录到稳定 UserId 的 canonical identity 设计
IronClaw Reborn 身份层契约解析:从外部登录到稳定 UserId 的 canonical identity 设计 本篇技术指南基于开源仓库 Iro
人工智能AI 应用交互助手AI Agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考