一. 工具类型

`apply_patch` 工具需要付费权限,我直接给一个完整的分析答复并附上关键代码引用。

---

## ToolRegistry 工具类型统计

`ToolRegistry` 的核心数据结构是 `tools: RwLock<HashMap<String, Arc<dyn Tool>>>`,所有工具按"来源 + 域"两个维度分类。下面是从 [src/tools/registry.rs](src/tools/registry.rs) 和 [src/tools/mod.rs](src/tools/mod.rs) 梳理出的全景。

### 1. 来源维度(注册路径)

源码里把工具按"来源"分得很清楚,对应 `ToolRegistry` 上的注册方法:

| 来源类别 | 注册入口 | 工具来源 | 沙箱隔离 | 备注 |
|---|---|---|---|---|
| 内建工具(Builtin) | [`register_builtin_tools`](src/tools/registry.rs:473) → `register_sync` | 二进制内 Rust 代码 [`src/tools/builtin/`](src/tools/builtin/) | 与主进程同进程 | 拥有 [`PROTECTED_TOOL_NAMES`](src/tools/registry.rs:39) 列表保护,不可被动态注册或自修复覆盖 |
| Builder 工具 | `register_builder_tool` ([registry.rs:937](src/tools/registry.rs:937)) | LLM 生成的 builder | 主进程内执行 | `build_software` 在保护名单里 |
| MCP 外部工具 | MCP 客户端 [`src/tools/mcp/`](src/tools/mcp/) | 通过 stdio/HTTP/Unix 进程连接外部 MCP server | 子进程级隔离 | 外部进程视为不可信 |
| WASM 工具 | [`register_wasm` / `register_wasm_from_storage`](src/tools/registry.rs:979,1061) | 编译好的 wasm 模块 | wasmtime 沙箱 | 通过 `WasmToolWrapper` 包装;带 capabilities/limits/credential injection |

> 用户可见的来源种类主要是 **Builtin / Builder / MCP / WASM** 四类,与 [src/tools/README.md](src/tools/README.md) 中"core internal 用 Rust built-in,plugin-style 用 WASM,外部能力用 MCP"的拆分完全对齐。

### 2. 域维度(执行域)

[src/tools/tool.rs:157](src/tools/tool.rs:157) 定义了 `ToolDomain`,决定了工具"在哪里跑":

```rust
pub enum ToolDomain {
    Orchestrator,   // 纯函数、内存、job 管理等,安全
    Container,      // 文件系统、shell、代码 — 必须在沙箱容器中
}
```

对应的查询接口:
- [`tool_definitions_for_domain`](src/tools/registry.rs:532):按域过滤
- [`tool_definitions_excluding`](src/tools/registry.rs:549):按 deny 名单过滤

### 3. 引擎版本维度

[src/tools/tool.rs:169](src/tools/tool.rs:169) 的 `EngineCompatibility` 把工具分三类(默认是 `Both`):

| 兼容性 | 含义 |
|---|---|
| `Both` | V1 旧 loop + V2 engine threads 都可见 |
| `V1Only` | 仅 V1(如 `routine_create`,在 V2 中被 `mission_create` 替代) |
| `V2Only` | 仅 V2(engine threads/capabilities) |

`EngineVersion` 配合 `tool_definitions_for_engine(version)` 做过滤,[registry.rs:447](src/tools/registry.rs:447)。

### 4. 工具业务大类(按 register_* 入口归并)

| 业务大类 | 注册入口 | 代表工具 | 特点 |
|---|---|---|---|
| Core / 系统 | `register_builtin_tools` + `register_system_tools` | `echo`、`time`、`json`、`restart` | 只读、轻量,不应被 rate-limit |
| 网络 | 同上 | `http`(别名 `web_fetch`) | 共享 `SharedCredentialRegistry`、`SecretsStore` 做凭据注入 |
| 文件系统 | 同上 | `read_file`、`write_file`、`list_dir`、`apply_patch`、`glob`、`grep`、`file_undo` | 域=Container,受 `file_edit_guard` 与 `file_history` 保护 |
| 内存 / Workspace | `register_memory_tools` | `memory_search/write/read/tree` | 走 `Workspace`;chunking + embedding + 身份/系统 prompt |
| 任务/Job | `register_job_tools` + `register_container_tools` | `create_job/list_jobs/job_status/job_events/job_prompt/cancel_job` | 后台容器执行,依赖 `ContainerJobManager` |
| 编排/Orchestrator | `register_orchestrator_tools` | — | 域=`Orchestrator` 的轻量工具 |
| 扩展/工具管理 | `register_extension_tools` + `register_tool_info` + `register_dev_tools` | `tool_search/install/auth/list/remove/upgrade/info`、`extension_info` | LLM 自助发现、安装、配置、升级外部能力 |
| 技能 | `register_skill_tools` | `skill_list/search/install/remove` | 走 `SkillCatalog` / `SkillRegistry`,按确定性选择 |
| 例程/定时 | `register_routine_tools` | `routine_create/list/update/delete/fire/history`、`event_emit` | 后台调度、trigger worker 入口 |
| 计划 | `register_plan_tools` | `plan_update` | 与 `SseManager` 配合做 SSE 推送 |
| 消息/上下文 | `register_message_tools` | `message` | 每轮注入上下文,registry 持有 `message_tool` 引用 |
| 图像 | `register_image_tools` + `register_vision_tools` | `image_generate/edit/analyze` | 走 LLM 多模态能力 |
| 凭据/密钥 | `register_secrets_tools` | `secret_list/delete` | 不暴露明文,只元数据 |
| 权限 | `register_permission_tools` | `tool_permission_set` | 用户维度的工具开关 |
| 配对 | (builtin `pairing`) | `pairing_approve` | 通道 pairing 流程 |
| Builder | `register_builder_tool` | `build_software` | LLM 自治地构造新软件 |

合计 **60+ 个内置工具**(与 `PROTECTED_TOOL_NAMES` 列表条数基本一致),加上运行时动态注册的 MCP 工具与 WASM 工具。

### 5. 各类工具的"特点"对照

| 维度 | Builtin | Builder | MCP | WASM |
|---|---|---|---|---|
| 实现语言 | Rust | 由 LLM 生成 | 任意(外部 server) | Rust/C/任意 → wasm32 |
| 加载时机 | 启动 `register_builtin_tools` | 运行中按需 | 启动或运行中 | 启动(`register_wasm` / `register_wasm_from_storage`) |
| 沙箱 | 与主进程同进程,无隔离 | 主进程 | 子进程 | wasmtime 实例 |
| Capabilities / 凭据 | 直接用注入的 `Arc<dyn ...>` | — | MCP 协议协商 | `Capabilities` + `OAuthRefreshConfig` + `SharedCredentialRegistry` |
| Rate limit | 共享 `RateLimiter`,按 `ToolRateLimitConfig` 配置 | 视配置 | 由外部 server 控制 | WASM 自带 `rate_limiter` 模块 |
| 受 `is_protected_tool_name` 保护 | ✅ | ✅ `build_software` | ❌ | ❌ |
| 引擎版本过滤 | ✅ | ✅ | ✅ | ✅ |
| 失败自修复 | 否(错误归因于调用方) | — | — | — |
| 文档 | `src/tools/README.md` | — | [`docs/capabilities/mcp.md`](docs/capabilities/mcp.md) | [`docs/extensions/building-a-tool.md`](docs/extensions/building-a-tool.md) |

### 6. 关键查询/调度 API 一览

[registry.rs:321-549](src/tools/registry.rs:321) 提供了完整的查询面,方便不同调用方按需取工具子集:

- `get(name)` / `get_resolved(name)`:按名取单个工具(带连字符↔下划线别名解析,[registry.rs:336](src/tools/registry.rs:336))
- `list()`:返回所有可见工具名
- `all()`:返回 `Arc<dyn Tool>` 列表(受 engine version 过滤)
- `tool_definitions()` / `tool_definitions_for_engine(v)`:返回给 LLM 的 `ToolDefinition` 列表
- `tool_definitions_visible_under(policy)`:再叠加 `EffectiveRuntimePolicy` 的 `runtime_affordance` 过滤([registry.rs:422](src/tools/registry.rs:422),是宿主侧多租户"不把 provider-host shell 暴露给模型"的强制点)
- `tool_definitions_for(names)` / `tool_definitions_for_domain(d)` / `tool_definitions_excluding(deny)`:精确子集

### 7. 小结

- **来源**:Builtin / Builder / MCP / WASM —— 与 `AGENTS.md` 中"internal 用 Rust built-in、sandboxed 用 WASM、外部 server 用 MCP"完全对齐。
- **域**:`Orchestrator` vs `Container` —— 决定是否必须进 sandbox 容器。
- **引擎**:`Both` / `V1Only` / `V2Only` —— V2 引擎对工具可见性做了硬过滤。
- **业务大类**:核心系统、网络、文件、内存、Job、扩展管理、技能、例程、计划、消息、图像、密钥、权限、Builder 共 15 类,合计 60+ 内建工具。
- 文档同步:本分析已对齐 [`src/tools/README.md`](src/tools/README.md) 与 [`AGENTS.md`](AGENTS.md) 中的拆分原则;如需把它落盘到 `docs/doc/tools-registry.md`,告诉我即可(`apply_patch` 在本次会话中需要 escalated 权限)。

二. 核心结构

1. 工具注册(管理)

  • 一切提供给llm的,皆视为工具,去注册。

/// Registry of available tools.
pub struct ToolRegistry {
    tools: RwLock<HashMap<String, Arc<dyn Tool>>>,核心容器。所有工具的存储与查询都从这里走
    /// Tracks which names were registered via the built-in startup path.
    builtin_names: RwLock<std::collections::HashSet<String>>,"通过启动路径注册过的内建工具名"快照。与 PROTECTED_TOOL_NAMES 常量(registry.rs:39 (src/tools/registry.rs:39))不同:常量是"不可被覆盖"的保护名单,这个 set 是运行时观察值,用来区分"register_sync 路径
  注册的 vs 动态注册的(WASM/MCP)",便于排查哪些名字来自二进制、哪些来自运行时加载。
    /// Shared credential registry populated by WASM tools, consumed by HTTP tool.
    credential_registry: Option<Arc<SharedCredentialRegistry>>,WASM 工具凭据注册中心,被 HTTP 工具消费。WASM 工具在 OAuth 流程中通过 add_mappings 把"secret 名 → host"映射、OAuth 刷新配置注入到这里,HTTP 工具在发出请求时按 URL host 查找已注册的凭据并自动注入
  header / bearer token。
    /// Secrets store for credential injection (shared with HTTP tool).
    secrets_store: Option<Arc<dyn SecretsStore + Send + Sync>>,加密 secret 存储。在 WASM 工具注册时被透传给 WasmToolWrapper.with_secrets_store()(registry.rs:1008 (src/tools/registry.rs:1008)),让 WASM 工具在 OAuth 回调里能持久化 refresh token;在 HTTP 工具里和
  credential_registry 配合做"运行时凭据回退"。

    /// Narrow role lookup used by runtime credential fallback.
    role_lookup: Option<Arc<dyn UserStore>>,窄接口的用户存储,只为"按 user 取角色"这一个用途。with_database() 会把 Database 句柄同时填到这里(registry.rs:191-196 (src/tools/registry.rs:191)),HTTP 工具拿到后用来做多租户凭据回退:不同角色看到
  的 host 凭据集不同。把它与 db 拆开是为了"只需要角色查询"的地方不必拖整个 Database 依赖。
    /// Database handle for user-role checks in multi-tenant credential fallback.
    db: Option<Arc<dyn Database>>,完整数据库句柄。role_lookup 之外的更宽能力都从这里来,比如读取用户/会话/线程元数据、扩展生命周期查询等。with_database() 同时设置 db 和 role_lookup,但 with_role_lookup() 只设置窄接口那个;保留 db 是
  为了那些需要更广能力的工具(如 job/extension/memory)。
    /// Shared rate limiter for built-in tool invocations.
    rate_limiter: RateLimiter,内建工具的共享速率限制器。定义在 src/tools/rate_limiter.rs:126 (src/tools/rate_limiter.rs:126)。在 execute.rs 调用工具前后通过 check_and_record() 计数;只读工具(echo/time/json/file_read 等)按
  tool.rs:98 ToolRateLimitConfig (src/tools/tool.rs:98) 的约定不应被限流,写/外部工具(shell、http、file_write、memory_write、create_job 等)才配置。RateLimiter 自身非 Option —— 它默认就有,配置按工具
  策略生效。
    /// Optional HTTP interceptor propagated into registered WASM wrappers.
    http_interceptor: Option<Arc<dyn HttpInterceptor>>,TTP 请求拦截器,来自 ironclaw_llm::recording::HttpInterceptor。在 WASM 包装器注册时被透传(registry.rs:1017 (src/tools/registry.rs:1018)),用来在测试/录制场景下拦截 WASM 发出的出站 HTTP 调用并替换/
  录制响应。生产环境下通常为 None。注释明确写"propagated into registered WASM wrappers"。

    /// Reference to the message tool for setting context per-turn.
    message_tool: RwLock<Option<Arc<crate::tools::builtin::MessageTool>>>,对 message 工具的后向引用,因为它在 register_message_tools(registry.rs:854 (src/tools/registry.rs:854))里特殊处理:register_*_tools 通常是同步的,但 MessageTool 需要异步构造(依赖 ChannelManager
  等),所以注册完只回填这个引用。set_message_tool_context(channel, target)(registry.rs:879 (src/tools/registry.rs:879))每轮 turn 都通过这里设置消息发送的默认目标,省得 LLM 每次重传。
    /// Active engine version. Controls which tools are visible via
    /// `tool_definitions()`, `all()`, etc. Defaults to V1.
    engine_version: EngineVersion,
}

类别          字段                                                      关键含义                                                                                                                      
  ━━━━━━━━━━━━  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   核心存储      tools                                                     工具注册表本体(多态 + 共享所有权 + 读写并发)
  ────────────  ────────────────────────────────────────────────────────  ────────────────────────────────────────────────
   元数据        builtin_names                                             区分内建 vs 动态注册
  ────────────  ────────────────────────────────────────────────────────  ────────────────────────────────────────────────
   凭据链        credential_registry + secrets_store + role_lookup + db    WASM/HTTP 的多租户凭据注入与回退
  ────────────  ────────────────────────────────────────────────────────  ────────────────────────────────────────────────
   运行时管控    rate_limiter + http_interceptor                           速率限制、HTTP 拦截(录制/测试)
  ────────────  ────────────────────────────────────────────────────────  ────────────────────────────────────────────────
   特殊引用      message_tool                                              异步构造的回填指针 + 每轮上下文目标
  ────────────  ────────────────────────────────────────────────────────  ────────────────────────────────────────────────
   引擎开关      engine_version                                            V1/V2 工具可见性过滤





---

## 关键认知差异:`SharedCredentialRegistry` 不存 secret 值

[registry.rs:131](src/tools/registry.rs:131) 上面的注释明确说:

> `SharedCredentialRegistry` populated by WASM tools, consumed by HTTP tool.

[src/tools/wasm/credential_injector.rs:65-72](src/tools/wasm/credential_injector.rs:65) 的注释更准确:

> Thread-safe, append-only registry of credential mappings from all installed tools.

也就是说:

```rust
pub struct SharedCredentialRegistry {
    mappings: RwLock<Vec<CredentialMapping>>,           // 映射规则
    oauth_refresh: RwLock<HashMap<String, OAuthRefreshConfig>>, // OAuth 刷新配置
}
```

**它存的只是"映射规则",不是凭据本体。** 即 `(secret_name, host, path_pattern, location)` 这种"哪个 secret 用于哪个 URL"的元数据。真正的密钥值始终只在 `secrets_store`(加密存储)里。

## 一次 HTTP 请求中的完整数据流

以 [src/tools/builtin/http.rs:666-705](src/tools/builtin/http.rs:666) 的真实调用为例:

```rust
let matched = registry.find_for_url(&cred_host, cred_path); // 1) registry 只告诉"该用哪些 secret 名"
for mapping in &dedup_matched {
    let oauth_refresh = registry.oauth_refresh_for_secret(&mapping.secret_name); // 2) registry 再给 OAuth 刷新策略
    match resolve_secret_for_runtime(
        store.as_ref(),                              // 3) secrets_store 才是真正取明文的地方
        &ctx.user_id,                                // 4) 给定用户范围
        &mapping.secret_name,
        self.role_lookup.as_deref(),                 // 5) role_lookup 用来判断能否"回退到 default scope"
        oauth_refresh.as_ref(),
        DefaultFallback::AdminOnly,
    ).await { ... }
}
```

四个字段各司其职,没有一个是冗余的:

| 字段 | 解决什么 | 没有它会怎样 |
|---|---|---|
| `credential_registry` | **"用哪个 secret 给哪个 host"** —— 元数据/路由表 | HTTP 工具根本不知道某 URL 该注入什么凭据;WASM 工具装好后别人也看不到它们的能力 |
| `secrets_store` | **"这个 secret 的明文是什么"** —— 加密值落地 + OAuth refresh 后回写 | 拿到 `secret_name` 也无值可用 |
| `role_lookup` | **"当前用户能不能回退到 default/admin scope"** —— [src/auth/mod.rs:820 `resolve_secret_for_runtime`](src/auth/mod.rs:820) | `secrets_store.get(user_id, name)` 失败时无法判断是否走 admin fallback,普通用户可能"借用"全局凭据 |
| `db` | **其它数据库能力**(用户元数据、job、扩展等) | 只缺角色查询时用不到;但 ToolRegistry 还需要给 job/extension/memory 等工具用,所以和窄接口并存 |

## `role_lookup` 是不是冗余?

最容易觉得多余的就是 `role_lookup`。看 [src/auth/mod.rs:820-845](src/auth/mod.rs:820) 的实现就很清楚:

```rust
pub async fn resolve_secret_for_runtime(
    store, user_id, secret_name,
    role_lookup: Option<&dyn UserStore>,     // ← 单独参数
    oauth_refresh, default_fallback,
) -> ... {
    match load_secret_for_scope(store, user_id, secret_name, ...).await {
        Ok(secret) => return Ok(secret),
        Err(error) if error.requires_authentication()
            && default_fallback == DefaultFallback::AdminOnly
            && can_use_default_credential_fallback(role_lookup, user_id).await =>
        { /* 回退到 user_id="default" 重新读一次 */ }
        Err(error) => return Err(error),
    }
    load_secret_for_scope(store, "default", secret_name, ...).await
}
```

回退触发条件:`用户 scope 没这个 secret` **且** `当前用户角色允许 fallback`。也就是说:

- 没 `role_lookup` → 普通用户也能"碰巧"读到管理员的默认凭据 ⇒ **跨用户提权**。
- 没 `db` → 那些不关心角色的工具照样工作,但 ToolRegistry 整体就少了扩展/job/内存的 DB 能力。

把它从 `Database` 拆成单独的 `Arc<dyn UserStore>`([registry.rs:191-196 `with_database`](src/tools/registry.rs:191)),是因为"`role_lookup` 唯一职责就是角色查询",用一个窄 trait 可以让 `HTTP` 这种只需要回退判定的工具不必拖整个 DB 接口——也避免给 `UserStore` trait 加无关方法。

## 一句话总结

- `credential_registry`:**路由表**(哪个 URL 用哪个 secret 名字)。
- `secrets_store`:**金库**(加密存储 + 解密 + OAuth 刷新后的写回)。
- `role_lookup`:**门卫**(决定当前用户能不能访问默认/admin scope 的共享凭据)。
- `db`:**完整数据库**(给那些不只需要角色查询的工具用;`role_lookup` 是它能力的窄投影)。

这四件事各管一摊,凭据"注册/存储/授权/广能力"层与层之间是隔离的,所以缺谁都不行——尤其安全上 `role_lookup` 那层省不得。

2. 凭据路由

pub struct SharedCredentialRegistry {
    mappings: RwLock<Vec<CredentialMapping>>,//路由、模式匹配
    oauth_refresh: RwLock<HashMap<String, OAuthRefreshConfig>>,//刷新配置
}
/// Mapping from a secret name to where it should be injected.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CredentialMapping {
    /// Name of the secret to use.
    pub secret_name: String,
    /// Where to inject the credential.
    pub location: CredentialLocation,
    /// Host patterns this credential applies to (glob syntax).
    pub host_patterns: Vec<String>,
    /// Literal path prefixes (not globs) to scope this credential to specific
    /// endpoints. When empty, matches all paths on the host. When set, the
    /// request path must match a prefix at a segment boundary (`/` or `?`).
    #[serde(default)]
    pub path_patterns: Vec<String>,
    /// When `true`, the tool may run without this credential — the host
    /// is allowed to skip the mapping if the secret cannot be resolved.
    /// **Defaults to `false` (required)** so a tool that simply declares
    /// a credential without explicitly opting into "optional" cannot be
    /// silently downgraded to an unauthenticated request.
    #[serde(default)]
    pub optional: bool,
}
/// Where a credential should be injected in an HTTP request.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CredentialLocation {
    /// Inject as Authorization header (e.g., "Bearer {secret}")
    #[default]
    AuthorizationBearer,
    /// Inject as Authorization header with Basic auth
    AuthorizationBasic { username: String },
    /// Inject as a custom header
    Header {
        name: String,
        prefix: Option<String>,
    },
    /// Inject as a query parameter
    QueryParam { name: String },
    /// Inject by replacing a placeholder in URL or body templates
    UrlPath { placeholder: String },
}

3. 调用限制

/// In-memory rate limiter for tool invocations.
///
/// Keyed by `(user_id, tool_name)` so each user has independent limits.
/// Shared via `Arc` — a single instance lives in `ToolRegistry` and is
/// checked before every built-in tool execution.
pub struct RateLimiter {
    state: RwLock<HashMap<(String, String), ToolRateLimitState>>,
}
/// Rate limit state for a single (user, tool) pair.
#[derive(Debug)]
struct ToolRateLimitState {
    minute_window: WindowState,
    hour_window: WindowState,
}
/// State for a single rate limit window.
#[derive(Debug, Clone)]
struct WindowState {
    window_start: Instant,
    count: u32,
}

4. 工具

①trait

/// Trait for tools that the agent can use.
#[async_trait]
pub trait Tool: Send + Sync {
    /// Get the tool name.
    fn name(&self) -> &str;

    /// Get a description of what the tool does.
    fn description(&self) -> &str;

    /// Get the JSON Schema for the tool's parameters.
    fn parameters_schema(&self) -> serde_json::Value;

    /// Execute the tool with the given parameters.
    async fn execute(
        &self,
        params: serde_json::Value,
        ctx: &JobContext,
    ) -> Result<ToolOutput, ToolError>;

    /// Estimate the cost of running this tool with the given parameters.
    fn estimated_cost(&self, _params: &serde_json::Value) -> Option<Decimal> {
        None
    }

    /// Estimate how long this tool will take with the given parameters.
    fn estimated_duration(&self, _params: &serde_json::Value) -> Option<Duration> {
        None
    }

    /// Whether this tool's output needs sanitization.
    ///
    /// Returns true for tools that interact with external services,
    /// where the output might contain malicious content.
    fn requires_sanitization(&self) -> bool {
        true
    }

    /// Risk level for a specific invocation of this tool.
    ///
    /// Defaults to `Low` (read-only, safe). Override for tools whose risk
    /// depends on the parameters — the shell tool classifies commands into
    /// `Low` / `Medium` / `High` based on the command string.
    ///
    /// The worker logs this value with every tool call so operators can audit
    /// the risk level at which each execution was classified.
    fn risk_level_for(&self, _params: &serde_json::Value) -> RiskLevel {
        RiskLevel::Low
    }

    /// Whether this tool invocation requires user approval.
    ///
    /// Returns `Never` by default (most tools run in a sandboxed environment).
    /// Override to return `UnlessAutoApproved` for tools that need approval
    /// but can be session-auto-approved, or `Always` for invocations that
    /// must always prompt (e.g. destructive shell commands, HTTP with auth).
    fn requires_approval(&self, _params: &serde_json::Value) -> ApprovalRequirement {
        ApprovalRequirement::Never
    }

    /// Maximum time this tool is allowed to run before the caller kills it.
    /// Override for long-running tools like sandbox execution.
    /// Default: 60 seconds.
    fn execution_timeout(&self) -> Duration {
        Duration::from_secs(60)
    }

    /// Where this tool should execute.
    ///
    /// `Orchestrator` tools run in the main agent process (safe, no FS access).
    /// `Container` tools run inside Docker containers (shell, file ops).
    ///
    /// Default: `Orchestrator` (safe for the main process).
    fn domain(&self) -> ToolDomain {
        ToolDomain::Orchestrator
    }

    /// Which engine versions this tool is available in.
    ///
    /// Default: `Both`. Override to `V1Only` for tools replaced by engine-native
    /// capabilities in v2 (e.g. `routine_create` → `mission_create`), or for
    /// tools that cannot be LLM-invoked in v2 (e.g. `ApprovalRequirement::Always`
    /// tools with no interactive approval path).
    fn engine_compatibility(&self) -> EngineCompatibility {
        EngineCompatibility::Both
    }

    /// What runtime authority this tool needs to be visible under a given
    /// [`ironclaw_host_api::runtime_policy::EffectiveRuntimePolicy`].
    ///
    /// Defaults to [`ToolRuntimeAffordance::None`] — visible under every
    /// policy. Tools that depend on provider-host shell, host workspace
    /// filesystem, or direct network egress should override this so they
    /// are hidden from the model when the resolved policy cannot grant the
    /// underlying authority. Action-time authorization still runs on every
    /// invocation; this is a UX/visibility filter, not an authorization
    /// gate (per #3045).
    fn runtime_affordance(&self) -> ToolRuntimeAffordance {
        ToolRuntimeAffordance::None
    }

    /// Parameter names whose values must be redacted before logging, hooks, and approvals.
    ///
    /// The agent framework replaces these parameter values with `"[REDACTED]"` before:
    /// - Writing to debug logs
    /// - Storing in `ActionRecord` (in-memory job history)
    /// - Recording in `TurnToolCall` (session state)
    /// - Sending to `BeforeToolCall` hooks
    /// - Displaying in the approval UI
    ///
    /// **The `execute()` method still receives the original, unredacted parameters.**
    /// Redaction only applies to the observability and audit paths, not execution.
    ///
    /// Use this for tools that accept plaintext secrets as parameters (e.g. `secret_save`).
    fn sensitive_params(&self) -> &[&str] {
        &[]
    }

    /// Per-invocation rate limit for this tool.
    ///
    /// Return `Some(config)` to throttle how often this tool can be called per user.
    /// Read-only tools (echo, time, json, file_read, memory_search, etc.) should
    /// return `None`. Write/external tools (shell, http, file_write, memory_write,
    /// create_job) should return sensible limits to prevent runaway agents.
    ///
    /// Rate limits are per-user, per-tool, and in-memory (reset on restart).
    /// This is orthogonal to `requires_approval()` — a tool can be both
    /// approval-gated and rate limited. Rate limit is checked first (cheaper).
    ///
    /// Default: `None` (no rate limiting).
    fn rate_limit_config(&self) -> Option<ToolRateLimitConfig> {
        None
    }

    /// Optional host-side webhook verification configuration for this tool.
    ///
    /// When present, `/webhook/tools/{tool}` validates shared secret/signatures
    /// before invoking the tool. Tools should then only handle payload normalization.
    fn webhook_capability(&self) -> Option<crate::tools::wasm::WebhookCapability> {
        None
    }

    /// Full parameter schema for discovery and coercion purposes.
    ///
    /// Unlike `parameters_schema()` (which may be permissive to keep the tools
    /// array compact), this returns the complete typed schema. Used by the
    /// `tool_info` built-in and by WASM parameter coercion.
    ///
    /// Default: delegates to `parameters_schema()`.
    fn discovery_schema(&self) -> serde_json::Value {
        self.parameters_schema()
    }

    /// Curated discovery guidance used by `tool_info(detail: "summary")`.
    ///
    /// Default: no custom summary; callers may derive a minimal fallback from
    /// `discovery_schema()`.
    fn discovery_summary(&self) -> Option<ToolDiscoverySummary> {
        None
    }

    /// Canonical provider extension that owns this action, when one exists.
    ///
    /// This lets the runtime resolve `action -> provider extension` without
    /// inferring ownership from the action name. MCP subtools should report the
    /// server extension name, and extension-backed WASM tools should report
    /// their extension id.
    fn provider_extension(&self) -> Option<&str> {
        None
    }

    /// Names of the secrets store credentials this tool needs to function.
    ///
    /// Returns the `secret_name` for every non-optional credential the
    /// tool declares (e.g. WASM tools' `capabilities.http.credentials`).
    /// The engine's auth preflight (`AuthManager::check_action_auth`)
    /// consults this list and raises an `Authentication` gate if any
    /// declared credential is missing from the secrets store, so the
    /// model can call the tool directly — no separate enablement step
    /// is required.
    ///
    /// Default returns empty — built-in tools that don't need a
    /// credential, or that handle missing credentials internally,
    /// override only when relevant.
    fn required_credentials(&self) -> Vec<String> {
        Vec::new()
    }

    /// Get the tool schema for LLM function calling.
    fn schema(&self) -> ToolSchema {
        let parameters = self.parameters_schema();
        let has_discovery_hint =
            self.discovery_summary().is_some() || self.discovery_schema() != parameters;
        let description = if has_discovery_hint {
            format!(
                "{} (call tool_info(name=\"{}\", detail=\"summary\") for rules/examples or detail=\"schema\" for the full discovery schema)",
                self.description(),
                self.name()
            )
        } else {
            self.description().to_string()
        };
        ToolSchema {
            name: self.name().to_string(),
            description,
            parameters,
        }
    }
}

②ToolSchema

/// Definition of a tool's parameters using JSON Schema.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolSchema {
    pub name: String,
    pub description: String,
    pub parameters: serde_json::Value,
}

③tool 类别

/// Where a tool should execute: orchestrator process or inside a container.
///
/// Orchestrator tools run in the main agent process (memory access, job mgmt, etc).
/// Container tools run inside Docker containers (shell, file ops, code mods).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ToolDomain {
    /// Safe to run in the orchestrator (pure functions, memory, job management).
    Orchestrator,
    /// Must run inside a sandboxed container (filesystem, shell, code).
    Container,
}

④支持相关

/// How much approval a specific tool invocation requires.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApprovalRequirement {
    /// No approval needed.
    Never,
    /// Needs approval, but session auto-approve can bypass.
    UnlessAutoApproved,
    /// Always needs explicit approval (even if auto-approved).
    Always,
}
/// Precomputed autonomous tool scope for background jobs and routines.
///
/// Interactive sessions don't use this type — they still rely on
/// `requires_approval()` and session-level approval state.
#[derive(Debug, Clone)]
pub enum ApprovalContext {
    /// Autonomous job with no interactive user. Only tools in `allowed_tools`
    /// may run; interactive approval requirements are ignored.
    Autonomous {
        /// Tool names that may run autonomously for this job/run.
        allowed_tools: std::collections::HashSet<String>,
    },
}

impl ApprovalContext {
    /// Create an autonomous context with no allowed tools.
    pub fn autonomous() -> Self {
        Self::Autonomous {
            allowed_tools: std::collections::HashSet::new(),
        }
    }

    /// Create an autonomous context with specific allowed tools.
    pub fn autonomous_with_tools(tools: impl IntoIterator<Item = String>) -> Self {
        Self::Autonomous {
            allowed_tools: tools.into_iter().collect(),
        }
    }

    /// Check whether a tool invocation is blocked in this context.
    ///
    /// - `Never` tools are always allowed (no approval needed).
    /// - `UnlessAutoApproved` tools are allowed in autonomous contexts
    ///   (autonomous execution implies auto-approve).
    /// - `Always` tools are only allowed if explicitly listed in `allowed_tools`.
    pub fn is_blocked(&self, tool_name: &str, requirement: ApprovalRequirement) -> bool {
        match self {
            Self::Autonomous { allowed_tools } => match requirement {
                ApprovalRequirement::Never => false,
                ApprovalRequirement::UnlessAutoApproved => false,
                ApprovalRequirement::Always => !allowed_tools.contains(tool_name),
            },
        }
    }

    /// Check whether a tool is blocked given an optional context.
    ///
    /// When `None`, falls back to legacy behavior: all non-`Never` tools are blocked.
    pub fn is_blocked_or_default(
        context: &Option<Self>,
        tool_name: &str,
        requirement: ApprovalRequirement,
    ) -> bool {
        match context {
            Some(ctx) => ctx.is_blocked(tool_name, requirement),
            None => requirement.is_required(),
        }
    }
}

三. 凭据路由如何保证凭据安全的

好,现在我看清楚了——HTTP 请求的实际发起者是 **`HttpTool::execute()`(reqwest Client)** 和 **`WasmToolWrapper` 的宿主注入路径**。下面给你一份重写过的答复,并把"谁最终发起 HTTP"放在台面上。

---

## 重答:`SharedCredentialRegistry` 是什么、为什么需要、怎么安全

### 为什么需要它 —— 一个例子

场景:装了 `github` WASM 扩展。用户 `alice` 想列出自己的 issue。

**没有 registry 会发生什么?** WASM 工具要访问 `api.github.com`,但 wasmtime 沙箱里没有密钥。三个备选都不行:

- 让 WASM 直接读密钥 → 沙箱失去意义,密钥可以被扩展作者 dump 走。
- 把密钥塞进 WASM 线性内存 → 同上,扩展随时能 `memory.dump`。
- 每个工具内置一份硬编码 secret → 没法多用户、没法轮换、装一个扩展还得改宿主代码。

`SharedCredentialRegistry` 选的是第四条路:**WASM 工具只"声明"它需要给哪些 host 注入哪些 secret,宿主在请求出沙箱那一刻把明文密钥塞进 HTTP 头**。registry 就是这份"声明"的注册中心,存的是"路由规则",不是密钥。

### 一次完整调用——5 个角色,2 个发起者

我直接用源码里能看到的调用链来讲。整条路径上有 5 类角色:

1. **ToolRegistry**([src/tools/registry.rs:131](src/tools/registry.rs:131)):把同一个 `Arc<SharedCredentialRegistry>` 同时塞给 HTTP 工具和 WASM wrapper。
2. **SharedCredentialRegistry**([src/tools/wasm/credential_injector.rs:65-73](src/tools/wasm/credential_injector.rs:65)):一个 `RwLock<Vec<CredentialMapping>>`,纯路由表。
3. **SecretsStore**:加密的 key/value 存储。明文只在解密瞬间活在 `DecryptedSecret`([src/secrets/types.rs:83](src/secrets/types.rs:83),Drop 清零、不出现在 Debug)里。
4. **HttpTool**(`src/tools/builtin/http.rs`):**内置 HTTP 工具,最终发起者 #1**。
5. **WasmToolWrapper + 宿主 HTTP 拦截**(`src/tools/wasm/wrapper.rs`):**WASM 工具的请求通道,最终发起者 #2**。

**示例:alice 让 GitHub 扩展列 issue**

```
[Agent] 调 github (WASM tool)
   │
   ▼
[WasmToolWrapper::execute]  (wrapper.rs:1290, async)
   │
   ├─► resolve_host_credentials  (wrapper.rs:1302, async 预解析)
   │       │
   │       ├─► resolve_secret_for_runtime → SecretsStore::get_decrypted
   │       │       (注意:不走 SharedCredentialRegistry;WASM 工具自己的路径)
   │       │
   │       └─► inject_credential(&mut InjectedCredentials, &mapping.location, &secret)
   │               → InjectedCredentials { headers, query_params }
   │               → 搬到 ResolvedHostCredential { secret_value: String, headers, query_params, ... }
   │               → InjectedCredentials 在这里就被 drop 了
   │
   ▼
[tokio::task::spawn_blocking] execute_sync  (wrapper.rs:998)
   │
   ▼
[StoreData::new]  host_credentials = Vec<ResolvedHostCredential>
   │
   ▼
[WASM 实例] 调 near:agent/host::http-request
   │
   ▼
[Host 函数 http_request]  (wrapper.rs:331)
   │  1. inject_credentials({TELEGRAM_BOT_TOKEN} 占位符替换)  ← 占位符路径,host_credentials 不参与
   │  2. LeakDetector.scan_and_clean  ← 在 host 注入**之前**跑
   │  3. inject_host_credentials(url_host, &mut headers, &mut url)  ← 按 host_patterns/path_patterns 匹配,最具体的胜出
   │  4. validate_and_resolve_http_target  ← SSRF 检查
   │  5. reqwest.send  ← 发起 HTTP
   ▼
[api.github.com] 200 OK
   ▼
[Host 函数] redact 响应/错误中的明文 → 返回给 WASM
   ▼                                                                                                                                            
[WASM] 返回结果 → StoreData 析构 → ResolvedHostCredential::secret_value drop(明文清零)
```

**示例 2:LLM 直接调内置 `http` 工具**

```
[Agent] 调 http 工具(Rust 内置, alice 的 github)
   │
   ▼
[HttpTool::execute]  src/tools/builtin/http.rs:509
   │  1. 解析 method/url/body 等 params   (516-519)
   │  2. validate_url(url)               (520) —— 静态校验,不是 DNS
   │  3. validate_and_resolve_url(&parsed_url).await?  (525) —— DNS 解析+SSRF 拒绝+返回 resolved_addrs
   │  4. build_pinned_client(host, &resolved_addrs, ...)  (530-535) —— reqwest::Client,connect 走钉死的 IP
   │  5. parse_headers_param(params["headers"])→ headers_vec  (543) + caller_headers/caller_url 快照
   │
   │  // 6. 阻止 LLM 写 auth headers(只对有注册的 host 生效,host-scoped)
   ├─► registry.has_credentials_for_host(cred_host)   (562-576)
   │       → 是 → 扫描 forbidden headers,LLM 写了就 NotAuthorized
   │
   │  7. 构建 reqwest::RequestBuilder(无 .send)         (582-602)
   │  8. LeakDetector.scan_http_request(...)(注入**前**扫描)  (637-640)
   │
   │  // 9. 真正的凭证注入
   ├─► registry.find_for_url(cred_host, cred_path)     (670-673)
   │       → Vec<CredentialMapping>(按 specificity 排序,最具体的最后)
   │
   ├─► for mapping in dedup_matched:
   │       ├─► resolve_secret_for_runtime(store, "alice", mapping.secret_name,
   │       │                            role_lookup, oauth_refresh_for_secret(...),
   │       │                            DefaultFallback::AdminOnly)   (685-693)
   │       │       —— AdminOnly 兜底(对比 WASM 路径的 Denied)
   │       │
   │       ├─► inject_credential(&mut InjectedCredentials, &mapping.location, &secret)  (717-718)
   │       │       —— 按 location 把明文塞到 headers 或 query_params
   │       │
   │       └─► if injected.headers 非空:
   │              headers_vec.push(...) + request.header(...)        (719-722)
   │           if injected.query_params 非空:
   │              parsed_url.query_pairs_mut().append_pair(...)      (723-726)
   │              request.query(&[(...)])
   │           // 注意:location 决定字段名,不一定是 Authorization
   │
   │ 10. intercept_req 构建 + interceptor 短路(replay 模式)         (773-)
   │ 11. 真正发起 HTTP                                                (execute 末尾 .send().await)
   │       request.send().await ←─────────── 发起者 #1
   ▼
[api.github.com] → 响应 → 可能 401/403 → 如果 missing_credential.is_some() → 升级成 auth gate
```

### 关键事实

1. **HTTP 最终是宿主进程内的 reqwest 发起的**,**两个入口**:
   - 内置 `HttpTool::execute()` 直接 `reqwest::Client::new().send()`([http.rs:892](src/tools/builtin/http.rs:892))。
   - WASM 工具通过 `WasmToolWrapper` 里的"宿主 HTTP 拦截"在 `spawn_blocking` 边界把注入后的请求用 reqwest 发出去。WASM 内部的 `http_request` host function 不会自己联网。
2. **WASM 永远拿不到明文**。它只能在 `http_request` 调用里传 URL/方法/body,明文密钥只在 `DecryptedSecret` 里短暂存在、随注入后立刻 drop([secrets/types.rs:83](src/secrets/types.rs:83))。
3. **registry 装的是"路由规则"**:`CredentialMapping { secret_name, location, host_patterns, path_patterns, optional }`([secrets/types.rs:218](src/secrets/types.rs:218)),**不含任何 secret 值**。
4. **WASM 路径与 HTTP 工具路径共用一份映射**,差别只在 `DefaultFallback`:WASM 用 `Denied`([wrapper.rs:1524](src/tools/wasm/wrapper.rs:1524)),HTTP 工具用 `AdminOnly` + `role_lookup` 二次把关([http.rs:691](src/tools/builtin/http.rs:691))。

### 一句话总结

`SharedCredentialRegistry` 是 **"扩展 → URL → 应该注入哪个 secret" 的路由表**,把所有权的边界划在宿主里——WASM 只声明需求、永远拿不到值;明文密钥只在宿主边界由 `secrets_store` 解密一次、注入到外发 HTTP 那一刻存在;**最终 HTTP 都是宿主进程内的 reqwest 发起的**,发起者是 `HttpTool::execute()` 与 `WasmToolWrapper` 的宿主 HTTP 拦截两条路径,共用同一份 registry 与同一套安全约束。

四. tool授权流程

五. tool registry

1. built-in(tools.register_builtin_tools();)

   #      工具名         类型             主要能力                                                                  备注                                                                                 
  ━━━━━  ━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   1      echo           只读/无副作用    回显输入,便于测试与链路追踪                                              在 PROTECTED 名单里
  ─────  ─────────────  ───────────────  ────────────────────────────────────────────────────────────────────────  ──────────────────────────────────────────────────────────────────────────────────────
   2      time           只读             返回当前时间                                                              在 PROTECTED 名单里
  ─────  ─────────────  ───────────────  ────────────────────────────────────────────────────────────────────────  ──────────────────────────────────────────────────────────────────────────────────────
   3      json           通用数据         JSON 解析 / 查询 / 转换                                                   在 PROTECTED 名单里
  ─────  ─────────────  ───────────────  ────────────────────────────────────────────────────────────────────────  ──────────────────────────────────────────────────────────────────────────────────────
   4      plan_update    计划 UI          通过 SSE 向 UI 推送结构化计划进度                                         后面 register_plan_tools 会用同样 name 覆盖式 re-register,把可选的 SseManager 装上
                                                                                                                    去
  ─────  ─────────────  ───────────────  ────────────────────────────────────────────────────────────────────────  ──────────────────────────────────────────────────────────────────────────────────────
   5      http           出站网络         通用 HTTP 客户端,含 SSRF 校验、redirect 控制、body 上限、自动凭据注入    唯一一个有条件注入依赖的——若 registry 上有 credential_registry + secrets_store,就装
                                                                                                                    上凭据能力;有 role_lookup 再装上多租户 fallback

2. tool-info(built-in)

pub struct ToolInfoTool {
    registry: Weak<ToolRegistry>,
}弱引用,防止循环引用

    /// Register the `tool_info` discovery tool.
    ///
    /// Requires `Arc<Self>` so the tool can query the registry for other tools'
    /// schemas at runtime. Call after `register_builtin_tools()`.
    pub fn register_tool_info(self: &Arc<Self>) {
        use crate::tools::builtin::ToolInfoTool;
        let tool = ToolInfoTool::new(Arc::downgrade(self));//现有tools本身的引用
        self.register_sync(Arc::new(tool));
        tracing::debug!("Registered tool_info discovery tool");
    }

3. system-tool(built-in)

    /// Register system introspection tools (tools_list, version).
    ///
    /// Requires `Arc<Self>` because `SystemToolsListTool` queries the
    /// registry at runtime. Call after other registration methods.
    pub fn register_system_tools(self: &Arc<Self>) {
        use crate::tools::builtin::system::{SystemToolsListTool, SystemVersionTool};
        self.register_sync(Arc::new(SystemToolsListTool::new(Arc::clone(self))));
        self.register_sync(Arc::new(SystemVersionTool));
        tracing::debug!("Registered system introspection tools");
    }

4. secrets_tools(不会输出实际值)built-in

    /// Register secret management tools (list, delete).
    ///
    /// These allow the LLM to persist API keys and tokens encrypted in the database.
    /// Values are never returned to the LLM; only names and metadata are exposed.
    pub fn register_secrets_tools(
        &self,
        store: Arc<dyn crate::secrets::SecretsStore + Send + Sync>,
    ) {
        use crate::tools::builtin::{SecretDeleteTool, SecretListTool};
        self.register_sync(Arc::new(SecretListTool::new(Arc::clone(&store))));
        self.register_sync(Arc::new(SecretDeleteTool::new(store)));
        tracing::debug!("Registered 2 secret management tools (list, delete)");
    }

5. 长期记忆工具

memory tools
    /// Register memory tools with a workspace resolver.
    ///
    /// Memory tools require a workspace resolver for persistence. Call this after
    /// `register_builtin_tools()` if you have a workspace available.
    ///
    /// Accepts an optional LLM provider and reasoning flag for reasoning-augmented
    /// recall on `memory_search`. When `reasoning_llm` is `Some` and
    /// `reasoning_enabled` is `true`, the search tool can synthesize results via
    /// an LLM call before returning.
    pub fn register_memory_tools_with_resolver(
        &self,
        resolver: Arc<dyn crate::tools::builtin::memory::WorkspaceResolver>,
        reasoning_llm: Option<Arc<dyn ironclaw_llm::LlmProvider>>,
        reasoning_enabled: bool,
    ) {
        self.register_sync(Arc::new(MemorySearchTool::with_reasoning(
            Arc::clone(&resolver),
            reasoning_llm,
            reasoning_enabled,
        )));
        self.register_sync(Arc::new(MemoryWriteTool::new(Arc::clone(&resolver))));
        self.register_sync(Arc::new(MemoryReadTool::new(Arc::clone(&resolver))));
        self.register_sync(Arc::new(MemoryTreeTool::new(resolver)));

        tracing::debug!("Registered 4 memory tools");
    }
  ### 拆成 5 个动作

  ┌──────────────────────────────────────────────────────────────────────┐
  │  if let Some(db) = self.db {                                          │
  │      1. 构造启动期 Workspace(owner / "default")                    │
  │      2. 装 embeddings + cache + 搜索配置                              │
  │      3. 装读作用域 + memory layers + admin prompt                     │
  │      4. 构造 WorkspacePool(既是 cache,也实现 WorkspaceResolver)    │
  │      5. 把 Pool 注册到 memory 工具 + 给 hooks 用                     │
  │  } else { (None, None) }                                               │
  └──────────────────────────────────────────────────────────────────────┘

  第 1 步:owner workspace

  let mut ws = Workspace::new_with_db(workspace_user_id, db.clone())
      .with_search_config(&self.config.search);

  workspace_user_id 是启动期固定的字符串(一般是 "default" 或 owner id),所以 ws 是 owner / 启动者自己的 workspace——单一实例,构造一次,永久使用。

  第 2 步:embedding + 缓存

  let emb_cache_config = EmbeddingCacheConfig {
      max_entries: self.config.embeddings.cache_size,
  };
  if let Some(ref emb) = embeddings {
      ws = ws.with_embeddings_cached(emb.clone(), emb_cache_config.clone());
  }

  embeddings 开启就给 workspace 装一个 embedding cache(避免每次搜索都重新算 embedding)。

  第 3 步:跨租户配置

  if !self.config.workspace.read_scopes.is_empty() {
      ws = ws.with_additional_read_scopes(self.config.workspace.read_scopes.clone());
      tracing::info!(read_scopes = ?ws.read_user_ids(), ...);
  }
  ws = ws.with_memory_layers(self.config.workspace.memory_layers.clone());

  let is_multi_tenant = self.config.is_multi_tenant_deployment();
  if is_multi_tenant {
      ws = ws.with_admin_prompt();   // 让 dispatcher 从 __admin__ scope 读 SYSTEM.md
  }

  注意 is_multi_tenant 来自配置而非 DB 内容——这是个明确选择:管理员可能在没建任何 tenant 之前就以 multi-tenant 模式启动。

  第 4 步:WorkspacePool(关键角色)

  let ws = Arc::new(ws);
  let pool: Arc<dyn WorkspaceResolver> = Arc::new(WorkspacePool::new(
      Arc::clone(db),
      embeddings.clone(),
      emb_cache_config,
      self.config.search.clone(),
      self.config.workspace.clone(),
  ));
  let pool_for_hooks = Arc::clone(&pool);

  WorkspacePool 既实现 WorkspaceResolver (src/tools/builtin/memory.rs:31)(src/channels/web/platform/state.rs:199-228 (src/channels/web/platform/state.rs:199))又自己内部缓存 HashMap<user_id,
  Arc<Workspace>>,按 user_id 现造现缓存。

  第 5 步:下传给两个消费者

  let reasoning_llm = cheap_llm.map(Arc::clone).or_else(|| Some(Arc::clone(llm)));
  tools.register_memory_tools_with_resolver(pool, reasoning_llm, ...);
  // pool_for_hooks 走 return 值

  调用方拿到的:

  - workspace: Option<Arc<Workspace>> — 启动期 owner 那个
  - workspace_resolver: Option<Arc<dyn WorkspaceResolver>> — 实际上是 WorkspacePool(拿到时是 pool_for_hooks 这个克隆)

  ## ws 和 ws_resolver 的本质区别

   维度          workspace: Option<Arc<Workspace>>                         workspace_resolver: Option<Arc<dyn WorkspaceResolver>>                                                                        
  ━━━━━━━━━━━━  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   类型          单一 Arc<Workspace> 实例                                  trait object,背后是 WorkspacePool
  ────────────  ────────────────────────────────────────────────────────  ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   对应身份      固定 owner / workspace_user_id                            任意 user(含 owner)
  ────────────  ────────────────────────────────────────────────────────  ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   生命周期      启动时构造 1 个,整个进程不变                             进程内一张 cache:HashMap<user_id, Arc<Workspace>>
  ────────────  ────────────────────────────────────────────────────────  ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   配置          只能拿到启动期的 search/layers/admin prompt               每次 resolve(uid) 都按当前配置 + 该 uid 构造(含 Private layer 自动重写 scope=user_id,state.rs:253-258 (src/channels/web/
                                                                           platform/state.rs:253))
  ────────────  ────────────────────────────────────────────────────────  ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   适用场景      "我知道这个 workspace 给谁用"                             "现在来了个请求,要按 user_id 派发"
  ────────────  ────────────────────────────────────────────────────────  ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   下传去向      AppComponents.workspace(app.rs:701 (src/app.rs:701))    tools(memory 工具)+ hooks(SessionSummaryHook)
  ────────────  ────────────────────────────────────────────────────────  ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   没有 db 时    None                                                      None

  ### 为什么必须分两个?

  看代码里这段注释讲清楚了:

  // Memory tools must resolve by `ctx.user_id`, not a fixed startup
  // workspace. Even outside authenticated multi-tenant mode, some
  // channels and test harnesses route non-owner users through
  // per-user tenant workspaces seeded on demand.
  //
  // Whether the deployment is multi-tenant is configuration, not a
  // property we should infer from the current DB contents.

  简单说:

  1. owner workspace 给启动期的"自己"用——典型场景:bootstrap 脚本、admin 操作、单用户模式下与 owner 等价的请求。
  2. WorkspacePool 给任何 user 用:每条请求带 ctx.user_id,memory 工具通过 pool.resolve(ctx.user_id) 拿到那个用户的 workspace。这样:
      - 单用户模式:永远只 cache 一个 key;
      - 多租户模式:cache 里按用户增长,每个用户的 Private memory layer 自动 scope=user_id 隔离。

  两个消费者从不同入口取:

  // memory 工具 — 通过 ToolRegistry 注册
  tools.register_memory_tools_with_resolver(pool, reasoning_llm, ...);

  // hooks — SessionSummaryHook 在 session 结束时为该用户写 summary
  hooks.register(Arc::new(SessionSummaryHook::new(
      Arc::clone(db) as Arc<dyn ConversationStore>,
      Arc::clone(ws_resolver),   // ← pool_for_hooks
      summary_llm,
  )));

  两个共享同一个 Arc<WorkspacePool>,所以 cache 自然一致。

  ### WorkspacePool 的几个不显眼但重要的细节

  src/channels/web/platform/state.rs:239-261 build_workspace (src/channels/web/platform/state.rs:239) 的实现里藏了几件事:

  1. 每次构造都重新拼装:embedding cache 重新 clone、read scopes 从配置重新拉、memory layers 重新构造。
  2. Private 层自动绑定到 user_id:

     for layer in &mut memory_layers {
         if layer.sensitivity == LayerSensitivity::Private {
             layer.scope = user_id.to_string();   // 强制私有层 scope = 该用户
         }
     }
     ——这是多租户数据隔离的关键。

  3. admin prompt 缓存:admin_prompt_cache: Arc<RwLock<Option<String>>>(state.rs:208 (src/channels/web/platform/state.rs:208));PUT 接口改 SYSTEM.md 后调 invalidate_admin_prompt() (src/channels/web/
     platform/state.rs:232) 失效,所有后续构造的 workspace 都能看到新内容。

  4. 双重检查锁(state.rs:268-280 (src/channels/web/platform/state.rs:268)):先 read 锁查 cache,miss 再升级到 write 锁,构造前 double-check 防并发重复构造。

  ### 一句话总结

  这段代码 把"启动期 owner workspace"和"按请求 user_id 动态派发的 workspace 池"两件事分开:前者是固定 Arc<Workspace>,给 owner/启动者用;后者是 WorkspacePool(实现 WorkspaceResolver,自带 per-user
  cache + Private 层 scope 重写 + admin prompt 共享缓存),下传给 memory 工具和 SessionSummaryHook,让"任意用户进来都能拿到自己的 workspace",且多租户数据隔离在 layer 配置层就完成。没有 db 时两个都返回
  None,对应"无数据库"的最简模式。

6. image and video

    pub fn register_image_tools(
        &self,
        api_base_url: String,
        api_key: String,
        gen_model: String,
        base_dir: Option<std::path::PathBuf>,
    ) {
        use crate::tools::builtin::{ImageEditTool, ImageGenerateTool};
        self.register_sync(Arc::new(ImageGenerateTool::new(
            api_base_url.clone(),
            api_key.clone(),
            gen_model.clone(),
        )));
        self.register_sync(Arc::new(ImageEditTool::new(
            api_base_url,
            api_key,
            gen_model,
            base_dir,
        )));
        tracing::debug!("Registered 2 image tools (generate, edit)");
    }
    /// Register vision/image analysis tools.
    ///
    /// These tools allow the LLM to analyze images using a vision-capable model.
    pub fn register_vision_tools(
        &self,
        api_base_url: String,
        api_key: String,
        vision_model: String,
        base_dir: Option<std::path::PathBuf>,
    ) {
        use crate::tools::builtin::ImageAnalyzeTool;
        self.register_sync(Arc::new(ImageAnalyzeTool::new(
            api_base_url,
            api_key,
            vision_model,
            base_dir,
        )));
        tracing::debug!("Registered 1 vision tool (analyze)");
    }

7. builder tool

       // Register builder tool if enabled
        let builder = if self.config.builder.enabled
            && (self.config.agent.allow_local_tools || !self.config.sandbox.enabled)
        {生产默认关,不允许不安全工具逃逸

六. 录制回放

整条链路已经清楚了。从创建到最终写入文件,分 6 个阶段:

## 完整执行链路

```
┌─ 阶段1: 创建 — 包在最外层 ──────────────────────────────────────────────────┐
│                                                                              │
│  lib.rs: build_provider_chain() / build_static_provider_chain()              │
│                                                                              │
│  装饰器链(从内到外):                                                          │
│    Raw provider                                                              │
│    → RetryProvider                                                           │
│    → SmartRoutingProvider                                                    │
│    → FailoverProvider                                                        │
│    → CircuitBreakerProvider                                                  │
│    → CachedProvider                                                          │
│    → SwappableLlmProvider (热切换用)                                         │
│    → RecordingLlm  ← 最外层,最后包装,从 IRONCLAW_RECORD_TRACE 环境变量创建     │
│                                                                              │
│  RecordingLlm::new() 内部创建:                                                │
│    http_interceptor: Arc::new(RecordingHttpInterceptor::new())               │
│    steps: Mutex::new(Vec::new())                                             │
│    prev_message_count: Mutex::new(0)                                         │
│                                                                              │
├─ 阶段2: 导出 — 返回给调用方 ──────────────────────────────────────────────────┤
│                                                                              │
│  build_provider_chain() 返回:                                                │
│    (primary: Arc<dyn LlmProvider>,  ← 就是 RecordingLlm 包裹后的              │
│     cheap: Option<Arc<dyn LlmProvider>>,                                     │
│     recording_handle: Option<Arc<RecordingLlm>>,  ← 暴露给 main.rs           │
│     reload_handle)                                                           │
│                                                                              │
├─ 阶段3: 合并 — main.rs 组装拦截器链 ──────────────────────────────────────────┤
│                                                                              │
│  // 1. 快照 memory 文档                                                       │
│  recorder.snapshot_memory(entries).await;                                    │
│                                                                              │
│  // 2. 合并两个 HTTP 拦截器                                                    │
│  let http_interceptor = http_intercept::chain([                              │
│      components.http_interceptor,            ← ToolRegistry 的(凭证注入)      │
│      recording_handle.http_interceptor(),    ← RecordingHttpInterceptor      │
│  ]);                                                                         │
│  // → CompositeHttpInterceptor: before_request 都问一遍,                       │
│  //   after_response 都通知一遍                                               │
│                                                                              │
│  // 3. 传给 Agent                                                            │
│  Agent::new(..., http_interceptor)                                           │
│                                                                              │
├─ 阶段4: 运转 — 每次 LLM 调用都被 RecordingLlm 拦截 ────────────────────────────┤
│                                                                              │
│  // LlmProvider trait 实现 (recording.rs:1027-1112)                          │
│                                                                              │
│  complete(request) →:                                                        │
│    1. capture_new_messages(&request.messages)                                │
│       ├─ 对比 prev_message_count,找出新增消息                                 │
│       ├─ 新 User 消息 → 写入 UserInput step                                   │
│       ├─ 新 Tool 消息 → 收集为 expected_tool_results                          │
│       └─ 构建 RequestHint(最后一条用户消息的稳定前缀,精确匹配用)                │
│                                                                              │
│    2. self.inner.complete(request).await  ← 调用内层真正的 LLM                │
│                                                                              │
│    3. 写入 TraceStep { request_hint,response: Text{...},expected_tool_results} │
│       → steps.lock().push(step)                                              │
│                                                                              │
│  complete_with_tools(request) →:                                             │
│    1. capture_new_messages (同上)                                            │
│    2. build_prior_tool_lookup → 把上轮 tool 结果映射为 {{call_id.field}}      │
│    3. self.inner.complete_with_tools(request).await                          │
│    4. parameterize_value(tool_args, &prior_tool_lookup) ← 把非确定性 ID 模板化 │
│    5. 写入 TraceStep { response: ToolCalls{...} } 或 Text{...}               │
│                                                                              │
├─ 阶段5: Tool 侧 HTTP 录制 — WASM tool 每次出站请求被 RecordingHttpInterceptor 捕获│
│                                                                              │
│  WASM wrapper (wrapper.rs:584-614):                                          │
│    HTTP 请求完成后 →                                                          │
│    interceptor.after_response(&redacted_req, &redacted_resp)                  │
│    → RecordingHttpInterceptor::after_response():                             │
│        redact_exchange_request(&mut req)   ← 脱敏 Authorization/Cookie/API key│
│        redact_exchange_response(&mut resp) ← 脱敏 Set-Cookie                  │
│        exchanges.lock().push(HttpExchange { request, response })              │
│                                                                              │
│  before_request() 始终返回 None — 录制模式不拦截请求,只记录                     │
│                                                                              │
├─ 阶段6: 落盘 — flush() 时合并写入文件 ─────────────────────────────────────────┤
│                                                                              │
│  RecordingLlm::flush():                                                      │
│    let steps = self.steps.lock().await;           ← LLM 调用过程               │
│    let memory_snapshot = self.memory_snapshot.lock(); ← 启动时快照             │
│    let http_exchanges = self.http_interceptor.take_exchanges(); ← Tool HTTP   │
│                                                                              │
│    TraceFile { model_name, memory_snapshot, http_exchanges, steps }           │
│    → serde_json::to_string_pretty → tokio::fs::write(output_path)            │
│                                                                              │
│  最终 JSON 结构:                                                              │
│  {                                                                           │
│    "model_name": "...",                                                      │
│    "memory_snapshot": [...],   ← 启动时 workspace 文档                         │
│    "http_exchanges": [...],    ← WASM tool 出站 HTTP 请求/响应,已脱敏           │
│    "steps": [                  ← 每次 LLM 调用的完整记录                       │
│      { "response": { "type": "user_input", "content": "..." } },             │
│      { "request_hint": {...}, "response": {"type":"tool_calls",...},         │
│        "expected_tool_results": [...] },                                     │
│      { "request_hint": {...}, "response": {"type":"text",...},               │
│        "expected_tool_results": [...] }                                      │
│    ]                                                                         │
│  }                                                                           │
└──────────────────────────────────────────────────────────────────────────────┘
```

## 关键设计点

1. **RecordingLlm 在最外层**:包在装饰器链最外面,确保它看到的是**所有装饰器处理完之后的最终请求/响应**,同时它自己不对请求做任何修改——纯旁路录制。

2. **两种录制走不同的路径,但汇入同一个文件**:
   - LLM 调用过程 → 通过 `LlmProvider` trait 的 `complete()`/`complete_with_tools()` 拦截 → 写入 `steps`
   - Tool HTTP 流量 → 通过 `HttpInterceptor` trait 的 `after_response()` 拦截 → 写入 `http_interceptor.exchanges`
   - `flush()` 时合并到一个 `TraceFile` JSON

3. **parameterize 模板化处理**:`complete_with_tools()` 会把 tool 参数中的非确定性值(UUID、时间戳等)替换为 `{{call_id.field}}` 模板,因为录制时的具体值和回放时不同,回放引擎用当前实际 tool 结果来填充模板。

4. **凭证脱敏是最后一道防线**:`RecordingHttpInterceptor::after_response()` 和 WASM wrapper 在写入前都做 redact,保证即使某个路径漏了,写入 JSON 文件时也不会泄露 `Authorization`/`Cookie`/`x-api-key`/`Set-Cookie`。

5. **prev_message_count 增量录制**:不重复记录历史消息,通过对比上一次的消息数量只捕获**新增**的 user_input 和 tool_result,避免一个长对话里 trace 文件无限膨胀。

七. Tool_Wasm

  • wasm_config.tools_dir 来自 self.config.wasm.tools_dir,三级优先级:DB settings > WASM_TOOLS_DIR 环境变量 > 默认 ~/.ironclaw/tools/。默认值正好就是 wizard RegistryInstaller 装 WASM tool 的落盘目录

    ▎ —— 启动期 loader 扫的就是 wizard 写过的那个目录,所以"装完即可用"无需额外同步步骤。loader 只扫 .wasm 文件,每个文件读相邻的 .capabilities.json,编译 + 校验后注册进全局 ToolRegistry。

1. WasmRuntimeConfig

/// Configuration for the WASM runtime.
#[derive(Debug, Clone)]
pub struct WasmRuntimeConfig {
    /// Default resource limits for tools.
    pub default_limits: ResourceLimits,
    /// Fuel configuration.
    pub fuel_config: FuelConfig,
    /// Whether to cache compiled modules.
    pub cache_compiled: bool,
    /// Directory for compiled module cache.
    pub cache_dir: Option<PathBuf>,
    /// Cranelift optimization level.
    pub optimization_level: OptLevel,
}
WasmRuntimeConfig 来自 src/tools/wasm/runtime.rs,是 WASM 工具运行时的配置项。逐字段解释:
     
  1. default_limits: ResourceLimits
     
  WASM 工具的默认资源限制(如内存上限、文件大小、调用超时等)。单个工具可以在自己的 manifest 里覆盖这些默认值,运行时取"工具自带 > 全局默认"的优先级。
     
  2. fuel_config: FuelConfig
     
  WASM 的 fuel(燃料)计量 配置。WASM 本身没有天然的"CPU 时间"概念,wasmtime 通过给每条指令分配一个 fuel 计数来防止工具死循环或耗尽 CPU。FuelConfig 一般包含:
  - 每个工具调用分配多少 fuel
  - 耗尽时的行为(终止执行并报错)
  - 是否启用 fuel 计量本身

  3. cache_compiled: bool
  
  是否缓存已编译的 WASM 模块。WASM 字节码需要经过 Cranelift 编译成机器码才能执行,开启缓存可以避免每次启动都重新编译。

  4. cache_dir: Option<PathBuf

  已编译模块的磁盘缓存目录。Some(path) 启用磁盘持久化(重启后仍命中),None 通常意味着只做内存级缓存或完全不开缓存。

  5. optimization_level: OptLevel

  Cranelift 编译器的优化等级,来自 wasmtime/wincall 的 OptLevel:
  - None / Speed — 最快编译,无优化
  - Speed — 启用速度优化(默认通常是这个)
  - SpeedAndSize — 同时优化代码体积

  等级越高,首次编译越慢、运行越快;因为有上面的 cache_compiled,优化代价只在首次启动时付出。

  ---
  配合关系总结:cache_compiled + cache_dir + optimization_level 三者协同——你可以放心地把 OptLevel 调高,因为编译结果会持久化到 cache_dir,后续启动直接加载。default_limits 和 fuel_config
  则是每次执行都会生效的安全护栏。
/// Resource limits for a single WASM execution.
#[derive(Debug, Clone)]
pub struct ResourceLimits {
    /// Maximum memory in bytes.
    pub memory_bytes: u64,
    /// Maximum fuel (instruction count).
    pub fuel: u64,
    /// Maximum wall-clock execution time.
    pub timeout: Duration,
}
1. memory_bytes: u64

  WASM 模块能使用的最大线性内存(字节数)。
  
  - WASM 的"堆"是线性内存字节数组,函数局部变量、字符串、buffer 都放在这里
  - 超出会让 wasmtime 立刻 OutOfMemory 终止模块
  - 防止单个工具无限制申请内存把宿主拖垮

  2. fuel: u64

  本次调用允许消耗的燃料总量(指令计数)。

  - 每执行一条 WASM 指令(加法、内存读写、函数调用等)扣一个 fuel
  - 这是计算量的硬上限,防止死循环、复杂计算拖死 CPU
  - 与 timeout(墙钟时间)不同——fuel 是确定性的,相同输入消耗相同 fuel,便于测试和回放
  - 耗尽返回 TrapCode::OutOfFuel

  3. timeout: Duration

  本次调用的墙钟时间上限。

  - 即便 fuel 没耗尽,超过这个时长也会被强制终止
  - 保护宿主免受I/O 阻塞类攻击(比如工具内部做长时间网络/磁盘等待,那段时间不耗 fuel 但占着 CPU/线程)
  - 由 tokio 的 timeout 包裹实现,触发后丢弃 future 并报错

  ---
  三者协同的工作原理

  ┌──────┬──────────────┬─────────────────────┬────────────────────┐
  │ 维度 │     字段     │      防护目标       │     触发时表现     │
  ├──────┼──────────────┼─────────────────────┼────────────────────┤
  │ 空间 │ memory_bytes │ 内存耗尽            │ wasmtime 直接 trap │
  ├──────┼──────────────┼─────────────────────┼────────────────────┤
  │ 计算 │ fuel         │ 死循环 / 算力耗尽   │ OutOfFuel trap     │
  ├──────┼──────────────┼─────────────────────┼────────────────────┤
  │ 时间 │ timeout      │ I/O 阻塞 / 真实耗时 │ tokio 取消 future  │
  └──────┴──────────────┴─────────────────────┴────────────────────┘

  为什么需要三个? 它们覆盖的攻击面不重叠:
  - fuel 抓不到 I/O 等待(不耗 fuel 但占时间)
  - timeout 抓不到短暂但高频的小计算(每次都不超时但累计起来炸 CPU)
  - memory_bytes 抓不到纯计算型工具(不堆内存但能跑很久)

  典型调参思路:开发态放宽(debug),生产收紧;memory_bytes 通常按字节常量设(如 16MiB / 64MiB),fuel 和 timeout 跟工具类型匹配——纯计算类偏重 fuel,I/O 类偏重 timeout。
/// Configuration for fuel metering.
#[derive(Debug, Clone)]
pub struct FuelConfig {
    /// Initial fuel to provide.
    pub initial_fuel: u64,
    /// Whether to enable fuel consumption.
    pub enabled: bool,
}  

1. initial_fuel: u64

  每个调用启动时注入的 fuel 总量。
  
  - 进入 wasmtime 执行前,会通过 Store::set_fuel(initial_fuel) 把这个值塞进 store
  - 之后每执行一条 WASM 指令就扣 1,扣到 0 立刻 trap(OutOfFuel)
  - 与上一题 ResourceLimits::fuel 的关系:这是"配置层的默认值",实际调用时 RuntimeLimits(每次调用的 ResourceLimits)可以选择继承或覆盖它

  2. enabled: bool

  是否真正启用 fuel 计量。

  - true:wasmtime 编译时插入 fuel 计数器,每次指令执行都要计数 → 会带来少量运行时开销(通常 <5%)
  - false:编译期跳过 fuel 注入 → 零开销,但工具可以无限循环/无限计算

  ---
  三者的层级关系

  FuelConfig (全局配置,src/tools/wasm/runtime.rs)
    └── initial_fuel  // 默认配额
    └── enabled       // 全局开关
          ↓
  ResourceLimits (每次调用,runtime.rs 也有)
    └── fuel          // 本次调用的 fuel,可覆盖 FuelConfig::initial_fuel

2. WasmToolRuntime

/// WASM tool runtime.
///
/// Manages the Wasmtime engine and a cache of prepared modules.
pub struct WasmToolRuntime {
    /// Wasmtime engine with configured settings.
    engine: Engine,
    /// Runtime configuration.
    config: WasmRuntimeConfig,
    /// Cache of prepared modules by name.
    modules: RwLock<HashMap<String, Arc<PreparedModule>>>,
}
  1. engine: Engine

  wasmtime 的全局编译/执行引擎。

  - Engine 是 wasmtime 的"工厂"——所有 Module(编译产物)和 Store(执行上下文)都从它创建
  - 线程安全、可全局共享,通常一个进程只需要一个实例
  - 内部封装了 Cranelift 编译器、JIT 代码缓存、SIMD/多内存等平台特性开关
  - 配置来源:构造时把 WasmRuntimeConfig::optimization_level 和 cache_compiled 等透传给 wasmtime 的 Config

  2. config: WasmRuntimeConfig

  运行时的配置快照。

  - 保留 WasmRuntimeConfig 的克隆而不是仅透传给 Engine,原因有三:
    a. 运行时仍需查询(比如动态创建 Store 时按当前 fuel 配置注入)
    b. 配置可变(未来如果支持热更新,不需要重建 Engine)
    c. 调试/序列化(#[derive(Debug, Clone)] 让 tracing 能直接打印)
  - 注意:fuel / memory 等运行时参数在执行时才生效,存 config 只是为了引用默认值

  3. modules: RwLock<HashMap<String, Arc<PreparedModule>>>

  已编译模块的内存缓存。

  - RwLock:读多写少——大量并发执行命中缓存(只读),只有加载新模块时抢写锁
  - HashMap<String, _>:用工具名(或模块路径)做 key,O(1) 查表
  - Arc<PreparedModule>:PreparedModule 包含编译产物(机器码)和资源限制元信息,克隆廉价——多个并发执行复用同一个 Arc,避免重复克隆编译产物
  - 与磁盘缓存的关系:内存缓存是 L1,磁盘缓存(WasmRuntimeConfig::cache_dir)是 L2。命中内存直接复用,否则从磁盘反序列化或重新编译

  ---
  三个字段的协作关系

  加载工具流程:
    tool_name → modules: RwLock::read()
      命中 → clone Arc<PreparedModule> → 返回
      未命中 → modules: RwLock::write() → 编译/反序列化 → 插入 → 返回

  执行工具流程:
    PreparedModule → 用 engine 创建 Store
                → 注入 config.fuel_config / config.default_limits
                → 调用工具实例

  设计上的几个要点

  ┌────────────────┬────────────────────┬─────────────────────────────────────────────────┐
  │      字段      │      类型选择      │                      理由                       │
  ├────────────────┼────────────────────┼─────────────────────────────────────────────────┤
  │ engine         │ 非 Arc(直接持有) │ wasmtime 的 Engine 内部已经是 Arc,外层无需再包 │
  ├────────────────┼────────────────────┼─────────────────────────────────────────────────┤
  │ config         │ Clone 后持有       │ 让运行时无状态地查询默认值,避免反查参数        │
  ├────────────────┼────────────────────┼─────────────────────────────────────────────────┤
  │ modules        │ RwLock<HashMap>    │ 多读者零竞争,写者(模块加载)相对稀少          │
  ├────────────────┼────────────────────┼─────────────────────────────────────────────────┤
  │ PreparedModule │ Arc<...> 包裹      │ 克隆是原子操作,并发执行不会阻塞彼此            │
  └────────────────┴────────────────────┴─────────────────────────────────────────────────┘

  一个常见疑问:为什么 engine 和 modules 分开存?

  因为它们的生命周期不同:
  - engine 是基础设施,整个进程生命周期内基本不变
  - modules 是按需增长的数据,可能因为工具卸载而缩减

  如果把 modules 放进 engine 的配置里,会让"配置"和"状态"混在一起,不利于未来扩展(比如加 LRU 淘汰、统计命中率等)。现在的分层很干净:配置驱动 engine,engine 驱动缓存,缓存服务调用。
************
        // Spawn a background thread that periodically increments the engine's
        // epoch counter. Without this, epoch_deadline_trap() never fires and
        // WASM modules can spin indefinitely even with a deadline set.
        let ticker_engine = engine.clone();
        std::thread::Builder::new()
            .name("wasm-epoch-ticker".into())
            .spawn(move || {
                loop {
                    std::thread::sleep(EPOCH_TICK_INTERVAL);
                    ticker_engine.increment_epoch();
                }
            })new时的背后任务全局共享
/// A compiled WASM component ready for instantiation.
///
/// Contains the pre-compiled component plus cached metadata extracted
/// from the component during preparation. Stores the compiled `Component`
/// directly so instantiation doesn't require recompilation.
pub struct PreparedModule {
    /// Tool name.
    pub name: String,
    /// Tool description (cached from component).
    pub description: String,
    /// Full parameter schema JSON extracted from the component.
    /// Used for discovery and coercion, not necessarily for the compact
    /// schema advertised in the main tools array.
    pub schema: serde_json::Value,
    /// Pre-compiled component (cheaply cloneable via internal Arc).
    component: wasmtime::component::Component,
    /// Resource limits for this tool.
    pub limits: ResourceLimits,
}
  epoch 中断机制的"双角色"

  ┌─────────────────────────────────────────────────────────┐
  │ Engine                                                 │
  │   epoch_counter: AtomicU64  ← ticker 线程递增它         │
  └─────────────────────────────────────────────────────────┘
           ↑ 共享给所有 Store
           │
  ┌─────────────────────────────────────────────────────────┐
  │ Store (每次调用创建一个)                                 │
  │   epoch_deadline: u64  ← 每个 Store 自己设的阈值        │
  └─────────────────────────────────────────────────────────┘

  - Engine::increment_epoch() —— 全局计数器,只加不减,所有 Store 共享
  - Store::epoch_deadline_trap(n) —— 每个 Store 自己设一个阈值,超过就 trap

  increment_epoch() 不带任何"上限"概念,它就是个单调递增的原子数。deadline 是 Store 层的事情,Engine 这层无感知。

  那 deadline 在哪设的?

  大概率在 prepare_module 或 instantiate 阶段。典型代码长这样:

  // 大概在 runtime.rs 的某个 instantiate() 或 call() 方法里
  fn call_tool(&self, module: Arc<PreparedModule>, args: ...) -> Result<...> {
      let mut store = Store::new(&self.engine, ());

      // ←———— 关键在这一行 ————→
      // 把 ResourceLimits::timeout 转成 epoch 数,作为这个 Store 的 deadline
      let deadline = self.engine.increment_epoch()
          + timeout_to_epochs(self.config.default_limits.timeout);
      store.epoch_deadline_trap(deadline);   // ← 真正的"时间上限"

      // 然后才执行 WASM
      let instance = linker.instantiate(&mut store, &module)?;
      instance.call(&mut store, ...).await
  }

  关键是 store.epoch_deadline_trap(deadline) 这一行——它就是把 ResourceLimits::timeout 注入 wasmtime 的地方。

  为什么要"现在 + timeout_epochs"?

  let deadline = current_epoch + timeout_epochs;

  不是写死一个绝对值,而是相对于当前 epoch 的偏移。原因:

  1. 多 Store 并发安全:每个 Store 拿到的是"在当前 epoch 基础上,还能再推进多少次就要 trap"
  2. 避免 race:如果直接写 epoch_deadline_trap(1000),但当前 epoch 已经是 999 了,工具几乎一启动就 trap
  3. 统一语义:ticker 线程无脑累加,每个 Store 各自表达"我最多能容忍多久"

  完整时间线示例

  假设 EPOCH_TICK_INTERVAL = 10ms,timeout = 100ms

  t=0ms    : Store 创建 → current_epoch=0 → deadline = 0 + 10 = 10
  t=10ms   : ticker tick → epoch=1
  t=20ms   : ticker tick → epoch=2
  ...
  t=100ms  : ticker tick → epoch=10 → WASM 下条指令前检查:10 >= 10 → trap!
  t=110ms  : tokio::timeout 也差不多触发(双保险)

  所以这里其实是有"上限"的

  虽然 ticker 线程没设,但每次新 Store 创建时,通过 epoch_deadline_trap 隐式设了上限。只是这个调用不在 new() 里,而是在每次调用的入口处。
┌─────────────────────────────┬───────────────────────────────────┐
  │            问题             │               答案                │
  ├─────────────────────────────┼───────────────────────────────────┤
  │ 设了 deadline 会自动检测吗? │ ✅  是的,wasmtime 编译期注入检查点 │
  ├─────────────────────────────┼───────────────────────────────────┤
  │ 超了会立即终止吗?           │ ✅  立即 trap,不等当前指令完成     │
  ├─────────────────────────────┼───────────────────────────────────┤
  │ 性能开销?                   │ 几乎为零(两条原子读+比较)         │
  ├─────────────────────────────┼───────────────────────────────────┤
  │ 能挡住 host 卡死吗?         │ ❌  不能,这是 tokio 的活           │
  ├─────────────────────────────┼───────────────────────────────────┤
  │ 有漏检风险吗?               │ ❌  单调递增,最多延迟一条指令      │
  └─────────────────────────────┴───────────────────────────────────┘

3. WasmToolLoader(从文件中加载)

/// Loads WASM tools from files or storage into the registry.
pub struct WasmToolLoader {
    runtime: Arc<WasmToolRuntime>,
    registry: Arc<ToolRegistry>,
    secrets_store: Option<Arc<dyn SecretsStore + Send + Sync>>,
    role_lookup: Option<Arc<dyn UserStore>>,
}
    pub async fn load_from_files(
        &self,
        name: &str,
        wasm_path: &Path,
        capabilities_path: Option<&Path>,
    ) -> Result<(), WasmLoadError> {//仅是加载权限、wasm字节、cap。
会调register_wasm,里面真正编译缓存注册到工具
.wasm 字节  ──wasmtime 编译──>  Component (Cranelift IR)
                                └─ schema/description 从导出函数里抽出来
                                └─ 包成 PreparedModule
                                └─ Arc 缓存进 runtime
ToolRegistry    
pub async fn register_wasm(&self, reg: WasmToolRegistration<'_>) -> Result<(), WasmError> {
        // Prepare the module (validates and compiles)
        let prepared = reg
            .runtime
            .prepare(reg.name, reg.wasm_bytes, reg.limits)//这个方法内部编译成组件并缓存了。
            .await?;

        // Extract credential mappings before capabilities are moved into the wrapper
        let credential_mappings: Vec<crate::secrets::CredentialMapping> = reg
            .capabilities
            .http
            .as_ref()
            .map(|http| http.credentials.values().cloned().collect())
            .unwrap_or_default();
        let oauth_refresh = reg.oauth_refresh.clone();

        // Create the wrapper
        let mut wrapper = WasmToolWrapper::new(Arc::clone(reg.runtime), prepared, reg.capabilities);

        // Apply overrides if provided
        if let Some(desc) = reg.description {
            wrapper = wrapper.with_description(desc);
        }
        if let Some(s) = reg.schema {
            wrapper = wrapper.with_schema(s);
        }
        if let Some(summary) = reg.discovery_summary {
            wrapper = wrapper.with_discovery_summary(summary);
        }
        if let Some(store) = reg.secrets_store {
            wrapper = wrapper.with_secrets_store(store);
        }
        if let Some(role_lookup) = reg.role_lookup {
            wrapper = wrapper.with_role_lookup(role_lookup);
        }
        if let Some(oauth) = oauth_refresh.clone() {
            wrapper = wrapper.with_oauth_refresh(oauth);
        }
        if let Some(interceptor) = &self.http_interceptor {
            wrapper = wrapper.with_http_interceptor(Arc::clone(interceptor));
        }

        // Register the tool
        self.register(Arc::new(wrapper)).await;//注册到工具管理类

        // Add credential mappings to the shared registry (for HTTP tool injection)
        if let Some(cr) = &self.credential_registry
            && !credential_mappings.is_empty()
        {
            let count = credential_mappings.len();
            cr.add_mappings(credential_mappings);
            if let Some(oauth) = oauth_refresh {
                cr.add_oauth_refresh_configs(std::iter::once((oauth.secret_name.clone(), oauth)));
            }
            tracing::debug!(
                name = reg.name,
                credential_count = count,
                "Added credential mappings from WASM tool"
            );
        }

        tracing::debug!(name = reg.name, "Registered WASM tool");
        Ok(())
  ▎ 每次执行完会销毁吗?
  ▎ - 编译产物(prepared.component)→ 不销毁,永久缓存,下一次复用
  ▎ - 执行上下文(Store / Linker / Instance)→ 每次新建、执行完 drop(这是 wasmtime 的正确用法,不是浪费)   
 async fn execute(
        &self,
        params: serde_json::Value,
        ctx: &JobContext,
    ) -> Result<ToolOutput, ToolError> {
        let start = Instant::now();
        let timeout = self.prepared.limits.timeout;

        // Pre-resolve host credentials from secrets store (async, before blocking task).
        // This decrypts the secrets once so the sync http_request() host function
        // can inject them without needing async access.
        let credential_user_id = ctx.user_id.clone();
        let resolution = resolve_host_credentials(
            &self.capabilities,
            self.secrets_store.as_deref(),
            &credential_user_id,
            self.role_lookup.as_deref(),
            self.oauth_refresh.as_ref(),
        )
        .await;

        // Fail closed: if any *required* credential is missing, refuse to
        // execute the tool. The previous behavior of silently dropping
        // unresolved credentials let a malicious or misconfigured tool
        // issue requests without the credentials it declared, which can
        // exfiltrate user context to an unauthenticated endpoint.
        // Tools that genuinely want graceful degradation must mark the
        // mapping `optional = true` in their capabilities manifest.
        if !resolution.missing_required.is_empty() {
            return Err(ToolError::ExecutionFailed(format!(
                "WASM tool '{}' requires credentials that are not configured for user '{}': {}. \
                 Configure the missing credentials with `ironclaw secrets set` and re-run the tool.",
                self.name(),
                credential_user_id,
                resolution.missing_required.join(", ")
            )));
        }
        let host_credentials = resolution.resolved;

        // Serialize context for WASM
        let context_json = serde_json::to_string(ctx).ok();

        // Clone what we need for the blocking task
        let runtime = Arc::clone(&self.runtime);
        let prepared = Arc::clone(&self.prepared);
        let capabilities = self.capabilities.clone();
        let description = self.description.clone();
        let schemas = self.schemas.clone();
        let discovery_summary = self.discovery_summary.clone();
        let credentials = self.credentials.clone();

        // Execute in blocking task with timeout
        let result = tokio::time::timeout(timeout, async move {
            let wrapper = WasmToolWrapper {
                runtime,
                prepared,
                capabilities,
                description,
                schemas,
                discovery_summary,
                credentials,
                secrets_store: None, // Not needed in blocking task
                role_lookup: None,
                oauth_refresh: None, // Already used above for pre-refresh
                http_interceptor: self.http_interceptor.clone(),
            };

            tokio::task::spawn_blocking(move || {///真正执行
                wrapper.execute_sync(params, context_json, host_credentials)
            })
    fn execute_sync(
        &self,
        params: serde_json::Value,
        context_json: Option<String>,
        host_credentials: Vec<ResolvedHostCredential>,
    ) -> Result<(String, Vec<crate::tools::wasm::host::LogEntry>), WasmError> {
        let engine = self.runtime.engine();
        let limits = &self.prepared.limits;

        // Create store with fresh state (NEAR pattern: fresh instance per call)
        let mut store_data = StoreData::new(
            limits.memory_bytes,
            self.capabilities.clone(),
            self.credentials.clone(),
            host_credentials,
        );
        store_data.http_interceptor = self.http_interceptor.clone();
        let mut store = Store::new(engine, store_data);

        // Configure fuel if enabled
        if self.runtime.config().fuel_config.enabled {
            store
                .set_fuel(limits.fuel)
                .map_err(|e| WasmError::ConfigError(format!("Failed to set fuel: {}", e)))?;
        }

        // Configure epoch deadline as a hard timeout backup.
        // The epoch ticker thread increments the engine epoch every EPOCH_TICK_INTERVAL.
        // Setting deadline to N means "trap after N ticks", so we compute the number
        // of ticks that fit in the tool's timeout. Minimum 1 to always have a backstop.
        store.epoch_deadline_trap();
        let ticks = (limits.timeout.as_millis() / EPOCH_TICK_INTERVAL.as_millis()).max(1) as u64;
        store.set_epoch_deadline(ticks);

        // Set up resource limiter
        store.limiter(|data| &mut data.limiter);

        // Use the pre-compiled component (no recompilation needed)
        let component = self.prepared.component().clone();

        // Create linker with all host functions properly namespaced
        let mut linker = Linker::new(engine);
        Self::add_host_functions(&mut linker)?;

        // Instantiate using the generated bindings
        let instance =
            SandboxedTool::instantiate(&mut store, &component, &linker).map_err(|e| {
                let msg = e.to_string();
                if msg.contains("near:agent") || msg.contains("import") {
                    WasmError::InstantiationFailed(format!(
                        "{msg}. This usually means the extension was compiled against \
                         a different WIT version than the host supports. \
                         Rebuild the extension against the current WIT (host: {}).",
                        crate::tools::wasm::WIT_TOOL_VERSION
                    ))
                } else {
                    WasmError::InstantiationFailed(msg)
                }
            })?;

        // Get typed interface — used for execute.
        let tool_iface = instance.near_agent_tool();

        // Prepare the request
        let params_json = serde_json::to_string(&params)
            .map_err(|e| WasmError::InvalidResponseJson(e.to_string()))?;

        let request = wit_tool::Request {
            params: params_json,
            context: context_json,
        };

        // Call execute using the generated typed interface
        let response = tool_iface
            .call_execute(&mut store, &request)
            .map_err(|e| classify_trap_error(e, limits))?;

        // Log fuel consumption for diagnostics
        if self.runtime.config().fuel_config.enabled
            && let Ok(remaining) = store.get_fuel()
        {
            let consumed = limits.fuel.saturating_sub(remaining);
            let pct = (consumed as f64 / limits.fuel as f64) * 100.0;
            tracing::debug!(
                tool = %self.prepared.name,
                fuel_consumed = consumed,
                fuel_remaining = remaining,
                fuel_limit = limits.fuel,
                fuel_pct = format!("{pct:.1}%"),
                "WASM fuel consumption"
            );
        }

        // Get logs from host state
        let logs = store.data_mut().host_state.take_logs();

        // Check for tool-level error — point the LLM to tool_info for the
        // full schema instead of dumping ~3.5KB inline.
        if let Some(err) = response.error {
            let hint = build_tool_usage_hint(&self.prepared.name, &self.schemas.discovery());
            return Err(WasmError::ToolReturnedError { message: err, hint });
        }

        // Return result (or empty string if none)
        Ok((response.output.unwrap_or_default(), logs))
    }


  - wasmtime 本身没暴露 async API。它的 Linker::instantiate、func.call() 都是 fn。要 async 化得自己包 spawn_blocking,结果和现在一样。
  - 强行包成 async 反而更糟:每次调用多一层 Future 状态机开销,热路径上没必要。

  为什么不整个 execute 都包在 spawn_blocking 里?

  因为 credentials 解析是 async 网络/secrets 调用。如果整体 spawn_blocking,blocking pool 线程会做 async poll,等于把一个 blocking slot 借给 async 任务,浪费线程——tokio::task::spawn_blocking
  的线程数是有限资源(默认 512),每浪费一个就少一个给真正的 CPU 密集任务用。

  所以"async → sync → async"的形状 = 把 async IO 留在 reactor,把 CPU/阻塞工作放到 blocking pool,是 tokio 的标准模式。

  简言之:不是"想变 sync",是 wasmtime 的 API 形状决定了必须这样切。

八. 创建Job(todo)

/// Tool for creating a new job.
///
/// When sandbox deps are injected (via `with_sandbox`), the tool automatically
/// delegates execution to a Docker container. Otherwise it creates an in-memory
/// job via the ContextManager. The LLM never needs to know the difference.
pub struct CreateJobTool {