一. 介绍
1. 介绍
//CLI命令主要是通过clap 的 #[derive(Subcommand)]按子命令解析去实现
//首先解析然后去匹配command的枚举
let cli = Cli::parse();
let enable_non_cli = non_cli_channels_enabled(cli.cli_only);
// Handle non-agent commands first (they don't need full setup)
match &cli.command {
Some(Command::Tool(tool_cmd)) => {
init_cli_tracing();
return run_tool_command(tool_cmd.clone()).await;//当匹配到时,直接执行对应逻辑,然后结束本次运行。
}2. 涉及的类
#[command(version)]
#[command(color = ColorChoice::Auto)] // Enable auto-color for help (if the terminal supports it)
pub struct Cli {//Cli类,主要是下面的命令参数
#[command(subcommand)]
pub command: Option<Command>, ****//命令Command枚举,对应不同的子命令,最主要的是下面两个子命令:运行和首次配置
pub enum Command {
/// Run the agent (default if no subcommand given)
Run,
Onboard {
/// Skip authentication (use existing session)
#[arg(long)]
skip_auth: bool,
/// Reconfigure channels only
#[arg(long, conflicts_with_all = ["provider_only", "quick", "step"], help = "Deprecated: use --step channels")]
channels_only: bool,
/// Reconfigure LLM provider and model only
#[arg(long, conflicts_with_all = ["channels_only", "quick", "step"], help = "Deprecated: use --step provider")]
provider_only: bool,
/// Quick setup: auto-defaults everything except LLM provider and model
#[arg(long, conflicts_with_all = ["channels_only", "provider_only", "step"])]
quick: bool,
/// Run only specific setup steps (comma-separated: provider, channels, model, database, security)
#[arg(long, value_delimiter = ',', conflicts_with_all = ["channels_only", "provider_only", "quick"])]
step: Vec<String>,
},
}二. Onboard做了什么
1. 创建引导
{
let config = SetupConfig {//入参
skip_auth: *skip_auth,
channels_only: *channels_only,
provider_only: *provider_only,
quick: *quick,
steps: step.clone(),
};
let mut wizard =
SetupWizard::try_with_config_and_toml(config, cli.config.as_deref())?;//加载配置config和setting toml文件得到引导配置
wizard.run().await?;//执行初始化
}2. ownerId
本地开发不设 IRONCLAW_OWNER_ID 也不写 TOML → 就是 "default";多租户部署在环境变量或 TOML 配置文件里指定 IRONCLAW_OWNER_ID / owner_id。
主要是通过这个方法解决身份:不从db查:解决先有鸡还是先有蛋的问题(去db查都需要ownerId)
pub(crate) fn resolve_owner_id(settings: &Settings) -> Result<String, ConfigError> {
let env_owner_id = self::helpers::optional_env("IRONCLAW_OWNER_ID")?;//通过完整的 env 查找链(真实 env > runtime overlay > injected secrets),优先级最高。
let settings_owner_id = settings.owner_id.clone();//TOML 配置文件里的 owner_id 字段。
let configured_owner_id = env_owner_id.clone().or(settings_owner_id.clone());
let owner_id = configured_owner_id
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "default".to_string());//兜底 "default" — 什么都不配就是它。
if owner_id == "default"
&& (env_owner_id.is_some()
|| settings_owner_id
.as_deref()
.is_some_and(|value| !value.trim().is_empty()))
{
WARNED_EXPLICIT_DEFAULT_OWNER_ID.call_once(|| {
tracing::warn!(
"IRONCLAW_OWNER_ID resolved to the legacy 'default' scope explicitly; durable state will keep legacy owner behavior"
);
});
}
Ok(owner_id)
}三. wizard.run()
设置完会持久化到db,若已有设置,则会加载。
1. step_database()
选择并连接数据库后端——交互式让用户选 PostgreSQL 还是 libSQL(除非环境变量 DATABASE_BACKEND 已指定),收集连接信息(URL/路径),然后把 DATABASE_URL / DATABASE_BACKEND 写入 ~/.ironclaw/.env 持久化。
// Step 1: Database
print_step(1, total_steps, "Database Connection");
self.step_database().await?;
let step1_settings = self.settings.clone();
self.try_load_existing_settings().await;
self.settings.merge_from(&step1_settings);
self.persist_after_step().await;//每一步后都写回数据库2. Security
加密密钥存储
step_security 是初始化主密钥——用来加解密 DB secrets 表里存的 API key/token。提供三种存储方式:
1. OS Keychain(macOS/Linux 系统级凭据管理,推荐本地用)
2. 环境变量 SECRETS_MASTER_KEY(CI/Docker 用,密钥存 ~/.ironclaw/.env)
3. 跳过(不用 secrets 功能)
密钥查找优先级:已有的 env var > OS keychain 里已有的 > 交互生成新的。选哪种方式就把 secrets_master_key_source 设为对应值、并立即初始化 SecretsCrypto 实例,后续 step 就能用这个 key 加密凭据了。3. step_inference_provider
设置llm provider
从provider.json里加载各个产商的需要的配置,然后从环境变量或用户输入。得到llm,然后加密存储到db中(secretStore)
4. Model selection
选择模型
从 provider API 拉取可用模型列表,让用户选一个作为默认模型。流程:
1. 已有模型 → 问"保留还是改"
2. 根据刚选的 llm_backend 走不同分支:
- OpenAI / Anthropic / OpenRouter 等 → 调 provider API 实时拉模型列表(顺便起到验证 API key 有效的作用——拉不下来说明 key 错了)
- Ollama → 调本地 ollama list
- Bedrock / NEAR AI / Gemini → 用硬编码的模型清单
- 其他 → 让用户手动输入模型 ID
3. 用户上/下键选一个,写入 self.settings.selected_model
总结:setup 收集 API key(step 3),选模型时实际调 API 拉列表(step 4)——如果 key 无效,这里就会报错,所以不需要单独的"验证连接"步骤。5. Embeddings
是否启用语义搜索
6. Channel


setup_tunnel_tailscale 仅收集用户选择的隧道信息
先初始化隧道,为webhook等提供ip。
隧道(tunnel)是这样绕过这个限制的:
你的机器 隧道服务器(ngrok/Cloudflare) 外部服务
| | |
|--- 1. 主动建立长连接 (outbound) --->| |
| | |
| |<--- 2. webhook POST 过来 -----------|
|<-- 3. 通过现有连接转发给你 ---------| |
关键点是第 1 步是你主动发起的出站连接 — 这不受 NAT/防火墙限制。隧道服务器的公网 IP 是真实可路由的,外部服务打到它,它再沿着你已经建好的那条连接把数据转发回你的本地进程。 let mut discovered_channels = discover_wasm_channels(&channels_dir).await; 发现 ~/.ironclaw/channels/cap.json能力文件,已经安装的
// Build channel list from registry (if available) + bundled + discovered,
//Bundled(available_channel_names() — 随二进制分发的预编译 channel)
//Registry(load_registry_catalog() — 远端 registry 清单里的 channel)
let wasm_channel_names = build_channel_options(&discovered_channels);
得到最终channel列表让用户去选
registry:
mcp、channel、tools,存在一个清单json
"source": {
"dir": "channels-src/feishu",
"capabilities": "feishu.capabilities.json",
"crate_name": "feishu-channel"
},
"artifacts": {
"wasm32-wasip2": {
"sha256": "edddd28d478a73add695315b99485cfb00656e1682b1f45e6f3a0a90c56e5378",
"url": "https://github.com/nearai/ironclaw/releases/download/ironclaw-v0.26.0/channel-feishu-0.2.2-wasm32-wasip2.tar.gz"
}
},
对于channel两种url路径去下载安装。 // Build options list dynamically 可选的channel
let mut options: Vec<(String, bool)> = vec![
("CLI/TUI (always enabled)".to_string(), true),
(
"HTTP webhook".to_string(),
self.settings.channels.http_enabled,
),
("Signal".to_string(), self.settings.channels.signal_enabled),
]; // HTTP channel
if selected.contains(&CHANNEL_INDEX_HTTP) {
println!();
if let Some(ref ctx) = secrets {
let result = setup_http(ctx).await?;//存储一个密钥验证,防止滥用
self.settings.channels.http_enabled = result.enabled;
self.settings.channels.http_port = Some(result.port);
} else {
self.settings.channels.http_enabled = true;
self.settings.channels.http_port = Some(8080);
print_info("HTTP webhook enabled on port 8080 (set HTTP_WEBHOOK_SECRET in env)");
}
} else {//最后,根据配置的channel能力文件中的setup的值,交互式收集然后,存到db
遍历 selected_wasm_channels
│
▼
┌─────────────────────────────────────────────┐
│ discovered_by_name 有 cap_file? │
├─────────────────────────────────────────────┤
│ ├─ 无 → print "channel 不可用",跳过 │
│ │ │
│ └─ 有 │
│ │ │
│ ▼ │
│ required_secrets 非空? │
│ ├─ 空 → enabled=true, overrides=空 │
│ └─ 非空 │
│ │ │
│ ▼ │
│ setup_wasm_channel(交互式收集) │
│ │ │
│ ├─ secret_input / auto_generate │
│ ├─ save_secret 存 DB │
│ └─ 返回 WasmChannelSetupResult │
└─────────────────────────────────────────────┘
│
▼
result.enabled?
├─ 否 → 下一 channel
└─ 是
│
├─ channel_name 加入 enabled_wasm_channels
│
▼
┌─────────────────────────────────────────┐
│ merge_wasm_channel_runtime_overrides_ │
│ for_channel(已有 overrides, config) │
│ │ │
│ └─ 合并已有值 + 本次新值 │
│ │ │
│ ▼ │
│ 遍历 channel_overrides │
│ │ │
│ ▼ │
│ key = wasm_channel_runtime_override_key │
│ (channel, config_key) │
│ │ │
│ ▼ │
│ enabled_runtime_overrides[key] = value │
└─────────────────────────────────────────┘
│
▼
所有 channel 遍历完毕
│
├─ settings.wasm_channels = enabled_wasm_channels
└─ settings.wasm_channel_runtime_overrides = enabled_runtime_overrides setup 阶段:
wizard → setup_wasm_channel 收集 token → save_secret 加密存 DB
↓
enabled_wasm_channels → settings.wasm_channels(启动快照)
enabled_runtime_overrides → settings.wasm_channel_runtime_overrides
key 形如 "telegram:bot_username"
agent 启动阶段:
setup_wasm_channels()
→ 扫描 ~/.ironclaw/channels/,按 activated_channels 过滤
→ 对每个 channel 调用 register_startup_loaded_channels
→ 读 secrets(DB 解密)
→ load_wasm_channel_runtime_overrides 按 "channel:" 前缀拆出配置
→ build_runtime_config_updates 拼装 {tunnel.public_url, webhook_secret, owner_id, ...}
→ channel.update_config(config_updates) 注入到 WASM 进程
→ WASM channel 进程启动 → 调用 Telegram/Slack API 注册 webhook
(webhook URL = tunnel.public_url + "/webhook/{channel}")7. Extensions (tools)
同外部channel,基本一样。
8. Docker沙箱
第三方不可信扩展(channels/tools) → WASM 沙箱
LLM 触发的代码执行(shell/python/build) → Docker 沙箱
内置 Rust tool + 主机进程 → 无隔离(受信代码)
```
step_docker_sandbox 启动
|
v
"Enable Docker sandbox?" (confirm)
/ \
否 是
| |
v v
sandbox.enabled check_docker()
= false |
┌──────┴──────┬──────────────┐
| | |
Available NotInstalled NotRunning
| (相同分支) (相同分支)
v | |
enabled = true v v
print_success 区分 not_installed:
ensure_worker_ - true → "Docker is not installed."
image() + install_hint()
- false → "Docker is installed but not running."
+ start_hint()
| |
| v
| "Retry after install/starting?" (confirm)
| / \
| 否 是
| | |
| v v
| enabled = false retry check_docker()
| 提示之后 / \
| SANDBOX_ENABLED OK not OK
| =true / \
| v v
| enabled = true enabled = false
| ensure_worker_ "Still not available"
| image() 提示
|
v
Disabled 状态
|
v
enabled = false (静默)
|
v
sandbox.enabled 为 true?
/ \
否 是
| v
| step_claude_code_sandbox()
| |
| v
| "Enable Claude Code sandbox mode?" (confirm)
| / \
| 否 是
| | v
| | 检查 ANTHROPIC_API_KEY 或 OAuth token
| | / \
| | 有 无
| | | v
| | v "Retry after setting up credentials?"
| | enabled = true / \
| | print_success 否 是
| | | v
| | v 再次检查 has_credentials
| | claude_code / \
| | enabled = false 有 无
| | 提示 v enabled=false
| | enabled=true
| v print_success
| return Ok(())
|
v
return Ok(())9. Heartbeat
是否开
间隔
通知channel
最后收集到setting
10. .env
最后将需要存到.env的存下。