ZeroClaw 工具共享状态所有权契约(ADR-004)深度解析:多客户端环境下的 Handle 模式、ClientId 隔离与配置重载语义
2026/9/19 23:24:26 网站建设 项目流程

ZeroClaw 工具共享状态所有权契约(ADR-004)深度解析:多客户端环境下的 Handle 模式、ClientId 隔离与配置重载语义

【免费下载链接】zeroclawFast, small, and fully autonomous AI personal assistant infrastructure, any OS, any platform — deploy anywhere, swap anything 🦀项目地址: https://gitcode.com/gh_mirrors/ze/zeroclaw

导读:ZeroClaw 是一个单守护进程(daemon)同时服务多个客户端的自治 AI 助手基础设施,其工具(Tool)系统天然需要持有跨请求存活的共享状态。ADR-004 为"工具能否拥有长期共享状态、身份由谁分配、校验在何时运行、哪些状态必须按客户端隔离、配置重载如何使缓存失效"这五个问题确立了正式契约。读完本文,你将掌握 ZeroClaw 工具系统的Arc<RwLock<T>>Handle 模式、ClientId身份抽象、四阶段生命周期,以及配置变更驱动的重校验语义,并能在新增工具时直接套用这套规范避免多租户数据泄漏。

背景:单守护进程多客户端架构下的状态管理难题

ZeroClaw 的工具在一个多客户端环境中执行:一个守护进程同时为多个连接的客户端提供服务。这意味着任何工具持有的共享状态,都可能被不同客户端并发访问。在 ADR-004 之前,仓库中已经自然生长出三个典型的"长期共享状态"实现,它们遵循了相似但未成文的模式:

共享状态句柄代码位置内部类型
DelegateParentToolsHandlesrc/tools/mod.rsVec<Arc<dyn Tool>>
ChannelMapHandlesrc/tools/reaction.rsHashMap<String, Arc<dyn Channel>>
CanvasStoresrc/tools/canvas.rsHashMap<String, CanvasEntry>

这三个实现暴露了四个未解决的问题:

  • 缺少按客户端隔离DelegateParentToolsHandle持有委托代理的父工具列表,但没有任何 per-client 命名空间;ChannelMapHandle是全局共享的通道映射;CanvasStore的 canvas ID 是纯字符串,同样没有客户端命名空间。
  • 客户端身份仅依赖 IP:当前网关层通过parse_client_ip/forwarded_client_ip/client_key_from_request从请求头或对端地址推导客户端标识(见 src/gateway/mod.rs),这在多客户端共享 NAT 地址或经过代理转发时无法可靠区分不同客户端。
  • 安全策略粒度不匹配SecurityPolicy是按 agent 而不是按客户端作用域的(见 src/security/policy.rs),客户端隔离与 agent 级策略是正交的两个维度。
  • 工作区切换是全局的WorkspaceManager提供了一定隔离,但工作区切换作用于全局,无法覆盖需要 per-client 状态的场景。

随着工具面不断扩张、并发客户端增多,ADR-004 将这些松散的模式固化为一份明确契约,覆盖**所有权(ownership)、身份(identity)、隔离(isolation)、生命周期(lifecycle)与重载行为(reload)**五个方面,防止新工具引入客户端间数据泄漏、配置重载后的过期状态或不一致的初始化时序。

决策一:所有权 —— 工具可以持有共享状态,但必须遵循 Handle 模式

结论:可以。工具可以(MAY)拥有长期存活的共享状态,前提是遵循既定的handle 模式:用Arc<RwLock<T>>(或Arc<parking_lot::RwLock<T>>)包裹状态,并暴露一个可克隆的句柄类型。

这一模式在仓库中已有三个独立实现作为先例。例如ChannelMapHandle在 src/tools/poll.rs、src/tools/reaction.rs 和 src/tools/ask_user.rs 中被一致地定义为:

pub type ChannelMapHandle = Arc<RwLock<HashMap<String, Arc<dyn Channel>>>>;

CanvasStore则把锁内层更进一步封装成结构体,隐藏Arc<RwLock<HashMap<String, CanvasEntry>>>的实现细节,同时通过#[derive(Clone)]让句柄可以廉价克隆共享(src/tools/canvas.rs):

/// Shared canvas store — holds all active canvases. /// /// Thread-safe and cheaply cloneable (wraps `Arc`). #[derive(Clone)] pub struct CanvasStore { inner: Arc<RwLock<HashMap<String, CanvasEntry>>>, }

ADR-004 为需要共享状态的工具规定了三条 MUST:

  1. 定义具名句柄类型别名,例如pub type FooHandle = Arc<RwLock<T>>
  2. 在构造时接收句柄,而不是在工具内部创建全局状态。这一点在all_tools_with_runtime()中体现得非常典型:PollTool通过PollTool::new(security.clone(), Arc::clone(&channel_map_handle))注入共享句柄,且注释明确说明"使用晚期绑定的 channel map handle"(src/tools/mod.rs);ReactionToolAskUserTool同样在构造后通过channel_map_handle()获取句柄,稍后由start_channels填充(src/tools/mod.rs);
  3. 在句柄类型的 doc 注释中记录并发契约。例如DelegateParentToolsHandle的注释写明"调用方可以在构造后推送额外工具(如 MCP 包装器)"(src/tools/mod.rs)。

严禁(MUST NOT)使用静态可变状态(lazy_static!、带内部可变性的OnceCell)存放 per-request 或 per-client 数据。静态可变状态无法被测试替换、无法被注入、也无法在配置重载时重新构建,与"构造时注入句柄"的契约直接冲突。

决策二:身份 —— ClientId 由守护进程分配,工具不得自造身份键

守护进程(daemon)应当(SHOULD)提供客户端身份,工具不得自行构造客户端身份键。

当前实现用原始 IP 作为客户端键:client_key_from_requesttrust_forwarded_headers时优先解析X-Forwarded-For/X-Real-IP头,否则回退到peer_addr的 IP,最后兜底为"unknown"(src/gateway/mod.rs)。这个方案的缺陷正是 ADR 指出的两点:多个客户端共享 NAT 地址时无法区分;经过代理的连接中转发头可被伪造。

ADR-004 提出引入一个新的ClientId类型,要求满足Clone + Eq + Hash + Send + Sync,由网关层在连接建立时生成。其契约包括四条:

  • 由网关层在连接建立时生成
  • 对工具不透明——工具不得解析或从值中推导含义;
  • 在单个客户端会话生命周期内保持稳定
  • 通过执行上下文(execution context)传递,而非全局存储

ClientId会作为工具执行上下文的一部分,传给那些需要 per-client 状态命名空间的工具;不需要按客户端隔离的工具(例如启动后不可变的工具注册表)可以直接忽略它。该抽象的价值在于:把工具与传输层细节解耦,未来无论引入 token、证书还是其他身份机制,工具代码都无需变动(见"影响评估"中的可演化性)。

决策三:生命周期 —— 校验只在注册期和配置变更时运行

ADR-004 将工具生命周期划分为四个明确阶段,消除了"校验究竟何时运行"的歧义:

  1. 构造(Construction):工具携带句柄和配置被实例化。此阶段不得进行任何 I/O 或校验。在 all_tools_with_runtime() 中可以看到,所有工具都是通过XxxTool::new(...)形式同步构造后统一放入tool_arcs向量的。
  2. 注册(Registration):工具通过all_tools_with_runtime()注册进工具注册表。此时工具可以(MAY)执行一次性启动校验——例如检查必需凭据是否存在、验证外部服务连通性。仓库中已存在这类"注册期快速失败"的实例:Microsoft 365 工具在client_credentials认证流程下若client_secret为空,会直接输出错误日志拒绝注册(src/tools/mod.rs)。
  3. 执行(Execution):工具处理单个请求。不得(MUST NOT)在执行阶段调用中进行阻塞式校验;校验结果应缓存在句柄状态中,执行时走快速路径(fast path)检查。
  4. 关闭(Shutdown):守护进程停止。持有开放资源的工具应当(SHOULD)通过Drop或显式 shutdown 方法优雅清理。

配置变更信号触发时,工具需要回到注册期语义重新执行校验(详见决策五)。

决策四:隔离 —— 哪些状态必须按客户端隔离?

状态被划分为两类,隔离要求截然不同。

必须按客户端隔离(MUST be isolated per client):

  • 安全敏感状态:凭据、API 密钥、配额、限流计数器、per-client 授权决策;
  • 用户专属会话数据:对话上下文、用户偏好、工作区作用域内的文件路径。

隔离机制:持有 per-client 状态的工具必须ClientId作为内部 map 的键。Handle 模式天然支持这一点——只需在RwLock内部使用HashMap<ClientId, T>即可,例如:

pub type ClientScopedHandle = Arc<RwLock<HashMap<ClientId, ClientState>>>;

可以跨客户端共享(MAY be shared),但需要命名空间前缀:

  • 广播/展示类状态:canvas 帧(CanvasStore)、通知通道(ChannelMapHandle);
  • 只读参考数据:工具注册表、静态配置、模型元数据。

当共享状态使用字符串键(如 canvas ID、通道名)时,工具应当(SHOULD)支持可选的命名空间前缀(例如{client_id}:{canvas_name}格式),以便在需要时提供 per-client 隔离,同时不强制广播场景也必须隔离。

工具不得(MUST NOT)将 per-client 密钥存入共享(非隔离)状态结构。

CanvasStore为例,其现状是纯字符串键的HashMap<String, CanvasEntry>(src/tools/canvas.rs),每个CanvasEntry内部包含当前帧、最多 50 帧的历史记录和容量为 64 的broadcast::Sender。canvas 帧内容通过 WebSocket 广播给所有订阅客户端——这正是"广播/展示状态可跨客户端共享"的典型场景,但若要支持多客户端隔离渲染,就需要按 ADR 建议改为{client_id}:{canvas_id}的键结构。

决策五:重载语义 —— 配置变更如何使缓存失效?

通过哈希比较检测到的配置变更,必须使缓存的校验状态失效。

重载契约如下:

  • 守护进程在启动时和每次配置重载事件后,对工具相关的配置片段计算哈希;
  • 当哈希变化时,守护进程向受影响的工具发出信号,要求其重新运行注册期校验;
  • 工具在收到信号后必须(MUST)将缓存的校验结果视为过期,并在下一次执行前重新校验。

ADR-004 给出了具体的失效范围映射表:

配置变更失效范围
凭据/密钥轮换每工具校验缓存;per-client 凭据状态
工具启用/禁用通过all_tools_with_runtime()全量重建工具注册表
安全策略变更重新推导SecurityPolicy;per-agent 策略状态
工作区目录变更WorkspaceManager状态;依赖文件路径的工具状态
Provider 配置变更依赖 Provider 的工具重新校验连通性

同时,工具可以(MAY)在配置重载期间保留非安全性的共享状态(如 canvas 内容、通道订阅),除非该重载明确影响到这些状态的有效性。

仓库中已存在"配置变更检测"的工程先例:通道层通过config_file_stamp(记录文件的修改时间modified与长度len,见 src/channels/mod.rs)比对配置文件是否变化,变化后重新解析并解密密钥、应用环境变量覆盖(load_runtime_defaults_from_config_file,src/channels/mod.rs)。这与 ADR 中"启动时与重载后各计算一次指纹、变化即触发重校验"的思路一致——差异在于 ADR 将指纹从文件元数据升级为"工具相关配置片段的哈希",粒度更精细。SecurityPolicyfrom_config(src/security/policy.rs)每次都会基于AutonomyConfigworkspace_dir重建完整策略(含新的ActionTracker),正是"安全策略变更 → 重新推导SecurityPolicy"这条规则的落地实现。

影响评估:契约带来的收益与代价

正面影响(Positive)

  • 一致性:所有新工具遵循同一 Handle 模式,共享状态可被发现、可被审计;
  • 安全性:安全敏感状态的 per-client 隔离防止多租户场景下的数据泄漏;
  • 清晰性:显式的生命周期阶段消除了"校验何时运行"的歧义;
  • 可演化性ClientId抽象将工具与传输层细节解耦,为未来支持 token、证书等身份机制铺路。

负面影响(Negative)

  • 迁移成本:现有工具(CanvasStoreReactionTool)可能需要重构以接受ClientId并为其状态加命名空间;
  • 复杂度:原本是简单单例的工具,即使当前只有一个客户端,也必须考虑多客户端语义;
  • 性能:per-client 键控在每次访问时增加一次哈希查找,但与 I/O 成本相比可忽略。

中性影响(Neutral)

  • 工具注册表在启动后保持不可变——本 ADR不改变这一不变式;
  • SecurityPolicy仍按 agent 作用域存在——本 ADR 明确记录:客户端隔离与 agent 级策略是正交的,不应混为一谈。

给工具开发者的落地清单

结合 ADR-004 与仓库现状,新增需要共享状态的工具时应依次回答五个问题:

  1. 状态是共享还是客户端私有?共享状态 → 定义具名FooHandle = Arc<RwLock<T>>;客户端私有 →HashMap<ClientId, T>作为锁内结构。
  2. 句柄从哪里来?all_tools_with_runtime()中通过构造函数注入,或先创建空句柄再延迟填充(参考PollTool/ReactionTool的晚期绑定模式,src/tools/mod.rs)。
  3. 校验何时做?构造阶段零 I/O;一次性校验放在注册期;执行期只做缓存的快速路径检查。
  4. 字符串键是否可能冲突?支持{client_id}:{name}命名空间前缀;绝不在共享结构中存放 per-client 密钥。
  5. 配置重载后什么会过期?遵循失效范围表:密钥轮换清空凭据缓存、工具开关全量重建注册表、策略/工作区/Provider 变更分别触发对应重校验。

这套契约的完整规范原文见 docs/architecture/adr-004-tool-shared-state-ownership.md,相关的工具特质定义见 src/tools/traits.rs(Tooltrait 的name/description/parameters_schema/execute接口),共享状态实现的三个样本分别位于 src/tools/mod.rs、src/tools/reaction.rs 与 src/tools/canvas.rs。

【免费下载链接】zeroclawFast, small, and fully autonomous AI personal assistant infrastructure, any OS, any platform — deploy anywhere, swap anything 🦀项目地址: https://gitcode.com/gh_mirrors/ze/zeroclaw

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

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

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

立即咨询