IronClaw Reborn 调度契约解析:`ironclaw_capabilities::dispatch` 的组合式运行时派发层

原创2026-09-24 19:44:071,026 阅读
文章标签:人工智能AI 应用交互助手AI Agent

IronClaw Reborn 调度契约解析:ironclaw_capabilities::dispatch 的组合式运行时派发层

导读

本文围绕 IronClaw Reborn 的调度契约文档 docs/internal/reborn/contracts/dispatcher.md 展开,深入讲解其核心组件——ironclaw_capabilities::dispatch 模块——如何将已经过授权校验的扩展能力(capability)请求路由到预先绑定的运行时适配器,并以"失败关闭"(fail-closed)的方式保证任何未授权、过期或车道不匹配的调用都无法触达后端。读完本文,你将掌握 Authorized 密封凭据的构造与消费机制、ToolResolver 预绑定解析器的组合方式、RuntimeLane 与 RuntimeKind 的映射关系、BoundCapabilityAdapter 开放扩展缝的接入方法,以及调度结果与错误分类的完整契约。


1. 定位:组合式运行时派发层

1.1 它在 Reborn 架构中的位置

ironclaw_capabilities::dispatch 是 Reborn 中**仅负责组合(composition)**的运行时派发层。它的输入输出链路非常清晰:

ToolResolver + ResourceGovernor
  -> RuntimeDispatcher::dispatch_json(Authorized)
  -> resolved BoundCapabilityAdapter
  -> normalized CapabilityDispatchResult

正如契约文档所强调的,调度器不做以下事情:不发现扩展、不解析 manifest、不实现策略、不直接打开文件、不解析密钥、不执行产品层编排。这些职责分别属于 ironclaw_extension_host、ironclaw_authorization、ironclaw_filesystem、ironclaw_secrets 等独立服务 crate。绑定(binding)的构建发生在派发之前,由解析器所有者完成,典型代表是 crates/kernel/ironclaw_host_runtime/src/services.rs 与 ironclaw_extension_host;调度器只消费一个密封的 Authorized 凭据,按 capability id 解析预绑定,并在解析出的运行时与密封车道不一致时失败关闭。

1.2 契约的演进:从独立 crate 到内核模块

契约文档的头部附注(2026-07-30,WS8)记录了一段重要的架构演进:该契约最初针对独立的 ironclaw_dispatcher crate 编写,而那个 crate 早已退化为对 ironclaw_capabilities 的 14 行 pub use 再导出,随后被删除。契约的实质内容未变,如今直接约束 ironclaw_capabilities::dispatch,其实现位于 crates/kernel/ironclaw_capabilities/src/dispatch.rs。这解释了为何能力测试被迁移到 ironclaw_capabilities/tests/runtime_dispatch_contract.rs——测试要与被钉住的代码放在一起,而不是藏在一个再导出 crate 后面。

1.3 中性端口词汇:ironclaw_host_api::dispatch

派发端口契约(port contract)定义在 ironclaw_host_api 中,调度模块只实现这个中性端口,并不拥有端口词汇表:

Authorized
CapabilityDispatchRequest
CapabilityDispatchResult
CapabilityDispatcher
DispatchError
RuntimeDispatchErrorKind

调用方通过 ironclaw_host_api 的 CapabilityDispatcher trait 访问派发,而不是直接命名具体的 RuntimeDispatcher,从而保证运行时车道(runtime lane)可替换。端口契约源码见 crates/contracts/ironclaw_host_api/src/dispatch.rs。


2. 输入:密封的 Authorized 凭据

2.1 凭据结构与端口签名

调度器接收一个已经过授权的密封 Authorized 凭据:

pub struct Authorized {
    /* private: sealed invocation + RuntimeLane + mounts + reservation + deadline */
}

pub trait CapabilityDispatcher {
    async fn dispatch_json(
        &self,
        authorized: Authorized,
    ) -> Result<CapabilityDispatchResult, DispatchError>;
}

Authorized 是安全关键的密封凭据,定义在 crates/contracts/ironclaw_host_api/src/authorized.rs。它的字段全部私有,且没有公开的字段构造器——唯一铸造途径是 Authorized::seal,而它要求一个 AuthorizationGrant;AuthorizationGrant 的唯一构造来源是 CapabilityAuthorizer::authorization_grant。这意味着,不持有 &impl CapabilityAuthorizer 就无法铸造 Authorized,未授权调用在结构上就是不可派发的。

该凭据具备四个关键性质:

  • Sealed(密封):私有字段 + grant 门槛构造,无法伪造,也无法修补成另一个 invocation;
  • Lane-bound(车道绑定):携带从描述符解析出的精确 RuntimeLane,派发只按该车道路由;
  • Single-use(单次使用):不可 Clone,dispatch_json 消费它;未派发路径必须显式调用 Authorized::abort 回滚,而不是依赖 Drop(析构函数不做异步 I/O,泄漏的凭据由租约过期回收);
  • Deadline-bounded(期限约束):凭据冻结了授权时各事实(审批/凭据租约)中最短的生命周期,过期即失败关闭。

2.2 消费与过期处理

调度器在执行时解包凭据并拒绝过期凭据。Authorized::into_parts(now) 会自行检查期限:过期时凭据原样以 Err(Box<Authorized>) 返回,调度器随即调用 authorized.abort() 取出其中持有的 ResourceReservation 并通过 DispatchReservationGuard 释放,然后返回 DispatchError::AuthorizationExpired。这一路径在 dispatch.rs 中有完整实现。

2.3 派发请求的内部形态

解包后,调度器从凭据各部分派生内部 CapabilityDispatchRequest(定义于 ironclaw_host_api/src/dispatch.rs):

pub struct CapabilityDispatchRequest {
    pub authorized_descriptor: Option<CapabilityDescriptor>,
    pub capability_id: CapabilityId,
    pub scope: ResourceScope,
    pub authenticated_actor_user_id: Option<UserId>,
    pub run_id: Option<RunId>,
    pub origin: InvocationOrigin,
    pub estimate: ResourceEstimate,
    pub mounts: Option<MountView>,
    pub resource_reservation: Option<ResourceReservation>,
    pub input: Value,
}

值得注意的细节:authenticated_actor_user_id 是直接从已授权派发请求拷贝的,不会根据 ResourceScope.user_id 重新计算——一个共享主体可能由另一个已单独认证的人类操作者执行。此外,mounts 保持 Option 形态,绝不坍缩为默认 MountView,因为 None 与空挂载的区分对文件系统解析器是语义性的(None 挂载会使 ScopedVirtual 支持的能力失败关闭,空挂载则不会)。


3. 构造方式:借用服务边界与共享句柄

调度器支持两种构造形态,分别对应请求作用域组合与脱离型后台执行:

// 请求作用域:借用服务边界
RuntimeDispatcher::new(&tool_resolver, &resource_governor)
    .with_event_sink(&event_sink)

// 脱离型后台执行:持有共享句柄
RuntimeDispatcher::from_arcs(tool_resolver, resource_governor)
    .with_event_sink_arc(event_sink)

from_arcs 形态(返回 RuntimeDispatcher<'static, G>)使调度器保持纯组合性,同时允许脱离型进程执行 capability 支撑的工作,而不会把借用的应用状态泄漏进 spawn 的任务中。实现上,ServiceHandle 枚举(Borrowed/Shared)统一了这两种持有方式,见 dispatch.rs。

一个真实的组合范例位于 crates/kernel/ironclaw_host_runtime/src/services.rs:宿主运行时把扩展快照解析器与注册表车道解析器串成 ChainToolResolver,再用 RuntimeDispatcher::from_arcs 连同资源治理器(governor)与事件接收器(event sink)一起装配:

let resolver: Arc<dyn ToolResolver> =
    match extension_tool_resolver(&self.extension_tool_resolver) {
        Some(extension_resolver) => Arc::new(ChainToolResolver::new(vec![
            extension_resolver,
            registry_resolver,
        ])),
        None => registry_resolver,
    };
let mut dispatcher = RuntimeDispatcher::from_arcs(resolver, Arc::clone(&self.governor));
if let Some(event_sink) = &self.event_sink {
    dispatcher = dispatcher.with_event_sink_arc(Arc::clone(event_sink));
}

4. 核心概念:ToolResolver 与 BoundCapabilityAdapter

4.1 ToolResolver:能运行什么的唯一权威

ToolResolver 是能运行什么的权威(authority for what can run),而运行时适配器所有者是车道如何运行的权威。它定义于 dispatch.rs:

pub trait ToolResolver: Send + Sync {
    fn resolve(&self, capability_id: &CapabilityId) -> Option<ResolvedCapability>;
}

实现是快照形态的:解析是对激活/注册时构建的绑定做一次查找,绝不是每次调用都做包/运行时类型选择。未知 id 返回 None,派发在任何适配器工作之前就失败(TOOL-2 规则)。

ResolvedCapability 携带拥有者扩展(宿主内置解析为合成的 builtin provider)、实现车道(在选择发生时已确定,仅用于派发事件与结果)以及预绑定适配器:

pub struct ResolvedCapability {
    pub provider: ExtensionId,
    pub runtime: RuntimeKind,
    pub adapter: Arc<dyn BoundCapabilityAdapter>,
}

为支持"宿主内置 + 激活扩展快照"的链式组合,dispatch 模块还提供了 ChainToolResolver(first-Some-wins 组合)。契约测试 runtime_dispatch_contract.rs 验证了链式解析的"第一个命中生效、未命中则回落"语义。

4.2 BoundCapabilityAdapter:开放扩展缝

BoundCapabilityAdapter 是派发层的开放扩展缝:

#[async_trait]
pub trait BoundCapabilityAdapter: Send + Sync {
    async fn dispatch_json(
        &self,
        request: CapabilityDispatchRequest,
    ) -> Result<RuntimeAdapterResult, DispatchError>;
}

每个运行时适配器拥有自己局部的 reserve/prepare/invoke/reconcile/release 生命周期。调度器在适配器执行前验证预备资源预留仍处于活动状态,但不拥有完整的资源治理协议,也不导入具体运行时 crate。具体的 WASM、Script、MCP 与第一方适配器都位于 dispatch 模块之外,因此 dispatch 模块对具体运行时 crate 没有常规依赖。

适配器返回 RuntimeAdapterResult(输出、展示预览、用量、收据、输出字节数),调度器在此基础上补齐稳定的身份字段,组装成 CapabilityDispatchResult。


5. 派发算法:V1 只做路由与一致性检查

V1 的 dispatch_json 执行以下七步(源码见 dispatch.rs):

1. consume the sealed `Authorized` witness and reject expired witnesses
2. derive the internal adapter request from the witness parts
3. resolve a prebound binding by capability id through `ToolResolver`
4. re-derive `RuntimeLane` from the resolved runtime and compare it to the sealed lane
5. validate any prepared `ResourceReservation` before binding execution
6. call the resolved `BoundCapabilityAdapter`, forwarding actor, mounts, run id, estimate, reservation, and input unchanged
7. return normalized result or typed failure with a stable redacted `RuntimeDispatchErrorKind`

5.1 事件序列

调度器在整个生命周期内通过事件接收器(event sink)发射可观测事件,序列固定为:dispatch_requested → runtime_selected → dispatch_succeeded/dispatch_failed。循环(loop)派发的事件还会挂上 parent_invocation_id(由 run_id 派生),用于把派发事件关联回父级运行。事件契约测试 runtime_dispatch_event_contract.rs 精确断言了这一序列与字段。

需要强调:事件发射是尽力而为(best-effort)的可观测性。配置的事件接收器故障不是派发故障,emit_event 中对 sink 的调用结果被显式忽略(let _ = sink.as_ref().emit(event).await),绝不会改变成功值或掩盖原始运行时/控制面错误。

5.2 资源预留的生命周期管理

调度器用 DispatchReservationGuard 管理预备预留:

  • 校验:适配器执行前,validate() 通过 ResourceGovernor::validate_reservation 确认预留仍有效;若预留已被撤销,返回 RuntimeDispatchErrorKind::Resource 类别的失败(测试 dispatcher_fails_closed_when_prepared_reservation_was_revoked_before_binding_dispatch 钉住了这一行为);
  • 移交:校验通过后,take() 把预留原样交给适配器,由绑定拥有 reconcile-or-release 后半程;
  • 释放:如果解析失败或校验失败发生在任何适配器接管之前,Drop 实现会通过 governor 释放预留,保证"派发前交接不泄漏预算"。

System 运行时在资源预留校验失败时没有可归因的后端,因此统一分类为 MissingRuntimeBackend 而非泛化的 provider 拒绝。


6. 运行时车道:RuntimeLane 与 RuntimeKind 的映射

6.1 两个容易混淆的概念

契约文档明确区分两个概念(见 crates/contracts/ironclaw_host_api/src/lane.rs):

  • RuntimeLane 是执行/信任边界——调度器把受管句柄交给的不可信表面,是一个封闭的四元素集合:FirstParty、Wasm、Mcp、Process。它是封闭枚举而非开放 trait:新增一个车道会变成编译错误,迫使每个 match 都面对它,这正是穷尽性安全属性所在;
  • RuntimeKind 是加载细节/分类法:Wasm/Mcp/Script/Sandbox/FirstParty/System。例如 Script 运行时执行在 Process 车道上,而 System 是宿主内部,不属于不可信车道。

6.2 映射表与 V1 派发行为

RuntimeLane::from_runtime_kind 是这两个轴相交的唯一位置(lane.rs)。契约文档给出的 V1 派发表如下:

Runtime kind Dispatch behavior
Wasm Resolved runtime must map to sealed RuntimeLane::Wasm; executes through the resolved adapter, usually composed by ironclaw_host_runtime
Script Resolved runtime must map to sealed RuntimeLane::Process; executes through the resolved adapter, usually composed by ironclaw_host_runtime
Mcp Resolved runtime must map to sealed RuntimeLane::Mcp; executes through the resolved adapter, usually composed by ironclaw_host_runtime
FirstParty Resolved runtime must map to sealed RuntimeLane::FirstParty; requires a registered host-service adapter
System Rejected as MissingRuntimeBackend before backend calls

映射细节补充:Sandbox(沙箱 shell 车道)同样映射到 Process 车道——它是租户沙箱下的 OS 进程,与 Script 共享同一执行表面。System 映射为 None——宿主内部(主密钥操作、迁移、管理工具)根本没有不可信执行车道,调用方必须把 None 理解为"宿主内部,无不可信车道",绝不能当作默认值。

V1 派发逻辑在解析出绑定后,用 RuntimeLane::from_runtime_kind(resolved.runtime) 与凭据携带的密封车道比较,不一致即返回 MissingRuntimeBackend。若 capability id 没有解析出绑定,则在适配器执行前返回 UnknownCapability。测试 runtime_dispatch_contract.rs 验证了密封车道不匹配(RuntimeLane::Process vs 解析出 Wasm)时适配器零调用、资源零占用。

6.3 特权变体的反伪造设计

RuntimeKind 与 TrustClass 的 FirstParty/System/Sandbox 变体带有 #<a href="https://link.gitcode.com/i/81833c2dd441d6ade1f402f4109685ee" target="_blank">serde(skip_deserializing)] 标记([runtime.rs):普通反序列化会拒绝这些变体,使不受信任的 manifest 无法自证特权状态;只有宿主自己写、自己读的可信持久记录才能通过 deserialize_trusted_runtime_kind 完整往返所有变体。测试 default_deserialize_still_rejects_privileged_variants 钉住了这条反伪造边界。


7. 失败关闭规则与结果形态

7.1 失败关闭清单

调度器在以下情形下在执行前失败:

  • capability ID 未注册(UnknownCapability);
  • 解析出的运行时未映射到密封车道(MissingRuntimeBackend);
  • 预备预留校验失败(RuntimeDispatchErrorKind::Resource,System 归为 MissingRuntimeBackend);
  • 选定绑定返回类型化派发失败。

这些失败不得预留资源或产生外部副作用。如第 5.2 节所述,若调用方从义务处理(obligation handling)提供了预备 ResourceReservation 而校验在适配器接管前失败,调度器会先释放该预留再返回失败。

7.2 错误分类与脱敏

运行时特定失败在跨过派发端口前被坍缩为稳定类别(Backend、ExitFailure、OutputDecode、Resource 等),原始的 backend 字符串、stderr、宿主路径与内部运行时细节字符串留在运行时 crate 内部。

错误词汇体系分为两层(ironclaw_host_api/src/dispatch.rs):

  • DispatchFailureKind:控制面级别,含 UnknownCapability、UnknownProvider、RuntimeMismatch、MissingRuntimeBackend、UnsupportedRuntime、AuthRequired,以及包一层 Runtime(RuntimeDispatchErrorKind);
  • RuntimeDispatchErrorKind:运行时级别,稳定且脱敏的 23 个类别,包括 Backend、Client、Executor、ExitFailure、Guest、InputEncode、InvalidResult、Manifest、Memory、MethodMissing、Network、NetworkDenied、OutputDecode、OutputTooLarge、PolicyDenied、Resource、SecretDenied、UndeclaredCapability、UnsupportedRunner、Unknown 等。

每个类别提供三重呈现:as_str()(稳定类别 token,用于路由/指标/审计)、human_summary()(固定宿主文案的用户可读句子,不插入任何原始内容)、event_kind()(脱敏的事件/审计 token)。DispatchError 的 Debug 输出同样脱敏:ProviderDiagnostic 渲染为 <redacted>,DispatchFailureDetail::Diagnostic 的原始文本被隐藏,而结构化输入问题(InvalidInput)保留路径与类型信息但隐藏收到值。这些行为由 dispatch.rs 中的内联单元测试 直接钉住。

7.3 成功结果形态

一次成功派发返回规范化结果:

pub struct CapabilityDispatchResult {
    pub capability_id: CapabilityId,
    pub provider: ExtensionId,
    pub runtime: RuntimeKind,
    pub output: serde_json::Value,
    pub display_preview: Option<CapabilityDisplayOutputPreview>,
    pub usage: ResourceUsage,
    pub receipt: ResourceReceipt,
}

该形态有意暴露常见宿主级事实,避免把 WASM 特定内部结构泄漏成通用契约。display_preview 是可选的、对模型隐藏的呈现侧通道,用于承载已经可净化的 UI 材料(如统一 diff);调用方必须把规范的 capability 输出保留在 output 中。CapabilityDisplayOutputPreview 中的 output_preview 字段标注为"原始、未净化内容——调用方在展示或记录前必须净化",规范的净化点是 ironclaw_composition 的投影层。


8. 非目标(Non-goals)

契约文档明确声明,当前切片不增加:

  • 授权/授权授予评估
  • 审批提示
  • 完整审计/事件投影持久化
  • 脚本文件系统挂载、工件导出、网络访问或密钥注入
  • 超出已解析绑定契约的 MCP 协议握手/生命周期管理
  • 第一方/系统能力的主机服务派发
  • 文件系统挂载选择
  • 网络或密钥注入
  • 后台 spawn / 进程生命周期
  • agent-loop 行为

这些职责属于专门的服务 crate 或后续更窄的调度组合切片。契约冻结附录(2026-04-25)进一步强调:WASM、Script、MCP 都是 V1 一等公民运行时车道,派发层仍然只是已授权路由器,不得依赖授权、审批、运行状态、内存、密钥、网络工作流、进程生命周期或具体宿主运行时组合;车道通过 RuntimeAdapter 注册。因为 Script 和 MCP 是一等车道,它们的适配器必须满足与 WASM 相同的脱敏、资源、进程、事件与网络强制契约;若某车道的必要义务无法强制,该调用在派发前失败关闭。


9. 契约测试:调用者级的行为钉扎

契约测试位于 crates/kernel/ironclaw_capabilities/tests/runtime_dispatch_contract.rs 与 runtime_dispatch_event_contract.rs,覆盖:

  • WASM 能力通过已解析适配器的派发(成功路径:输出、provider、runtime、receipt 状态 Reconciled、资源归零);
  • 未知能力在任何资源预留之前失败(UnknownCapability,绑定零调用、资源零占用);
  • 密封车道不匹配在执行前失败(MissingRuntimeBackend);
  • Script 能力通过已解析适配器的派发;
  • MCP 能力通过已解析适配器的派发;
  • 第一方与系统车道在解析器/绑定接缝处的行为;
  • 事件接收器故障在成功与失败路径上都被忽略;
  • 运行时失败细节脱敏为 RuntimeDispatchErrorKind(错误字符串不包含 secret token、私有路径);
  • 预备预留的移交与释放语义(解析失败释放、撤销后校验失败、成功时原样移交绑定);
  • CapabilityDispatcher trait object 派发(from_arcs 形态);
  • ChainToolResolver 的 first-Some-wins 与回落语义。

这些测试故意是调用者级的:它们驱动 RuntimeDispatcher::dispatch_json,而不是只测辅助函数。测试替身 ScriptedResolver(基于 HashMap 的脚本化解析器)与 RecordingBinding(模拟真实车道 legs:接管预备预留、否则新预留并 reconcile)共同构成了对调度契约的完整行为钉扎。


10. 延伸阅读

登录后查看全文
ironclaw