Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ OpenAI、Claude、Gemini、およびその他の互換形式の間で変換で
対応クライアントでは、利用可能なモデルカタログの同期、デフォルトモデルの選択、管理設定を適用する前の
元設定のバックアップ、以前の設定への復元、利用可能なデスクトップまたは CLI エントリーポイントの起動ができます。

Claude Desktop では、Cowork タスクと Claude Code セッションの両方が使用するツールネットワークの
許可リスト `coworkEgressAllowedHosts` もエージェント画面で管理できます。推奨初期値は
`localhost`、`127.0.0.1`、`api.anthropic.com`、`github.com`、`*.github.com`、
`*.githubusercontent.com`、`gitlab.com`、`*.gitlab.com` で、連携を再適用しても既存のカスタム項目は
保持されます。1 行に 1 件、`localhost`、`host[:port]`、または `*.host[:port]` を入力してください。
互換性のため単独の `*` も使用できますが、すべてのホストを許可してネットワークサンドボックスの制限を
無効にするため、既定値には使用しません。

## その他の機能

- コア設定、API Key、リモート管理用認証情報、ルーティング戦略の管理。
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ For supported clients, the application can synchronize the available model catal
default model, back up the original configuration before applying managed settings, and restore the
previous configuration.

For Claude Desktop, the Agents page also manages `coworkEgressAllowedHosts`, the tool-network
allowlist used by both Cowork tasks and Claude Code sessions. The recommended defaults are
`localhost`, `127.0.0.1`, `api.anthropic.com`, `github.com`, `*.github.com`,
`*.githubusercontent.com`, `gitlab.com`, and `*.gitlab.com`; existing custom entries are preserved
when the integration is reapplied. Add one entry per line using `localhost`, `host[:port]`, or
`*.host[:port]`. A bare `*` is supported for compatibility, but it allows every host and disables the
network sandbox restriction, so it is never used as the default.

## Additional Capabilities

- Manage core settings, API keys, remote management credentials, and routing strategy.
Expand Down
7 changes: 7 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ API 接入页面按照协议或 Provider 管理上游 API 凭证和服务地址
对于受支持的客户端,软件可以同步可用模型目录、选择默认模型、在应用托管配置前备份原始配置,
以及恢复之前的配置。

对于 Claude Desktop,智能体页面还会管理 `coworkEgressAllowedHosts`,它是 Cowork 任务和
Claude Code 会话共同使用的工具网络白名单。推荐默认值为 `localhost`、`127.0.0.1`、
`api.anthropic.com`、`github.com`、`*.github.com`、`*.githubusercontent.com`、
`gitlab.com` 和 `*.gitlab.com`;重新应用集成时会保留现有的自定义条目。每行填写一个
`localhost`、`host[:port]` 或 `*.host[:port]`。为兼容 Claude Desktop 也支持单独的 `*`,
但它会允许访问所有主机并关闭网络沙箱限制,因此不会作为默认值。

## 其他功能

- 管理内核配置、API Key、远程管理凭证和路由策略。
Expand Down
32 changes: 32 additions & 0 deletions src-tauri/src/agents/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,18 @@ pub(crate) fn resolve_claude_desktop_model_mappings(
}))
}

pub(crate) fn resolve_claude_desktop_egress_allowed_hosts(
client: AgentClient,
requested: Option<Vec<String>>,
) -> Result<Option<Vec<String>>, String> {
if client != AgentClient::ClaudeDesktop {
return Ok(None);
}
requested
.map(|hosts| normalize_claude_desktop_egress_allowed_hosts(&hosts))
.transpose()
}

pub(crate) fn resolve_claude_code_model_mappings(
client: AgentClient,
models: &[AgentModelOption],
Expand Down Expand Up @@ -801,6 +813,7 @@ pub(crate) fn prepare_codex_agent_models(
}

#[tauri::command]
#[allow(clippy::too_many_arguments)]
pub(crate) async fn apply_agent_config(
app: tauri::AppHandle,
gui_config_state: tauri::State<'_, GuiConfigState>,
Expand All @@ -809,6 +822,7 @@ pub(crate) async fn apply_agent_config(
oauth_configuration: bool,
claude_code_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_desktop_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_desktop_egress_allowed_hosts: Option<Vec<String>>,
) -> Result<AgentConfigActionResult, String> {
let client = AgentClient::parse(&client)?;
let home = app
Expand All @@ -835,6 +849,8 @@ pub(crate) async fn apply_agent_config(
&model,
claude_desktop_model_mappings,
)?;
let claude_desktop_egress_allowed_hosts =
resolve_claude_desktop_egress_allowed_hosts(client, claude_desktop_egress_allowed_hosts)?;
if let Some(mappings) = claude_desktop_model_mappings.as_ref() {
ensure_claude_desktop_model_aliases(&config, mappings, &prepared.models).await?;
}
Expand All @@ -853,6 +869,7 @@ pub(crate) async fn apply_agent_config(
oauth_configuration,
claude_code_model_mappings: claude_code_model_mappings.as_ref(),
claude_desktop_model_mappings: claude_desktop_model_mappings.as_ref(),
claude_desktop_egress_allowed_hosts: claude_desktop_egress_allowed_hosts.as_deref(),
},
)
}
Expand All @@ -875,6 +892,7 @@ pub(crate) fn close_agent_config_modification(
}

#[tauri::command]
#[allow(clippy::too_many_arguments)]
pub(crate) async fn reset_agent_config_to_default(
app: tauri::AppHandle,
gui_config_state: tauri::State<'_, GuiConfigState>,
Expand All @@ -883,6 +901,7 @@ pub(crate) async fn reset_agent_config_to_default(
oauth_configuration: bool,
claude_code_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_desktop_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_desktop_egress_allowed_hosts: Option<Vec<String>>,
) -> Result<AgentConfigActionResult, String> {
let client = AgentClient::parse(&client)?;
let home = app
Expand All @@ -909,6 +928,8 @@ pub(crate) async fn reset_agent_config_to_default(
&model,
claude_desktop_model_mappings,
)?;
let claude_desktop_egress_allowed_hosts =
resolve_claude_desktop_egress_allowed_hosts(client, claude_desktop_egress_allowed_hosts)?;
if let Some(mappings) = claude_desktop_model_mappings.as_ref() {
ensure_claude_desktop_model_aliases(&config, mappings, &prepared.models).await?;
}
Expand All @@ -926,6 +947,7 @@ pub(crate) async fn reset_agent_config_to_default(
oauth_configuration,
claude_code_model_mappings: claude_code_model_mappings.as_ref(),
claude_desktop_model_mappings: claude_desktop_model_mappings.as_ref(),
claude_desktop_egress_allowed_hosts: claude_desktop_egress_allowed_hosts.as_deref(),
})
}

Expand All @@ -952,6 +974,7 @@ pub(crate) async fn set_agent_config_enabled(
force_restore: bool,
claude_code_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_desktop_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_desktop_egress_allowed_hosts: Option<Vec<String>>,
) -> Result<AgentConfigActionResult, String> {
let client = AgentClient::parse(&client)?;
let home = app
Expand Down Expand Up @@ -979,6 +1002,10 @@ pub(crate) async fn set_agent_config_enabled(
&model,
claude_desktop_model_mappings,
)?;
let claude_desktop_egress_allowed_hosts = resolve_claude_desktop_egress_allowed_hosts(
client,
claude_desktop_egress_allowed_hosts,
)?;
if let Some(mappings) = claude_desktop_model_mappings.as_ref() {
ensure_claude_desktop_model_aliases(&config, mappings, &prepared.models).await?;
}
Expand All @@ -997,6 +1024,7 @@ pub(crate) async fn set_agent_config_enabled(
oauth_configuration: false,
claude_code_model_mappings: claude_code_model_mappings.as_ref(),
claude_desktop_model_mappings: claude_desktop_model_mappings.as_ref(),
claude_desktop_egress_allowed_hosts: claude_desktop_egress_allowed_hosts.as_deref(),
},
)
} else {
Expand All @@ -1016,6 +1044,7 @@ pub(crate) async fn update_agent_config(
model: String,
claude_code_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_desktop_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_desktop_egress_allowed_hosts: Option<Vec<String>>,
) -> Result<AgentConfigActionResult, String> {
let client = AgentClient::parse(&client)?;
let home = app
Expand All @@ -1039,6 +1068,8 @@ pub(crate) async fn update_agent_config(
&model,
claude_desktop_model_mappings,
)?;
let claude_desktop_egress_allowed_hosts =
resolve_claude_desktop_egress_allowed_hosts(client, claude_desktop_egress_allowed_hosts)?;
if let Some(mappings) = claude_desktop_model_mappings.as_ref() {
ensure_claude_desktop_model_aliases(&config, mappings, &prepared.models).await?;
}
Expand All @@ -1057,6 +1088,7 @@ pub(crate) async fn update_agent_config(
oauth_configuration: false,
claude_code_model_mappings: claude_code_model_mappings.as_ref(),
claude_desktop_model_mappings: claude_desktop_model_mappings.as_ref(),
claude_desktop_egress_allowed_hosts: claude_desktop_egress_allowed_hosts.as_deref(),
},
)
}
Expand Down
128 changes: 125 additions & 3 deletions src-tauri/src/agents/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub(crate) fn build_agent_updates(
oauth_configuration: false,
claude_code_model_mappings: None,
claude_desktop_model_mappings: None,
claude_desktop_egress_allowed_hosts: None,
},
)
}
Expand All @@ -40,6 +41,7 @@ pub(crate) fn build_agent_updates_with_oauth(
oauth_configuration,
claude_code_model_mappings,
claude_desktop_model_mappings,
claude_desktop_egress_allowed_hosts,
} = options;
let paths = agent_config_paths(client, home);
let root_base = format!("http://127.0.0.1:{port}");
Expand Down Expand Up @@ -98,6 +100,7 @@ pub(crate) fn build_agent_updates_with_oauth(
model,
models,
claude_desktop_model_mappings,
claude_desktop_egress_allowed_hosts,
)
.or_else(|_| {
build_claude_desktop_profile(
Expand All @@ -107,6 +110,7 @@ pub(crate) fn build_agent_updates_with_oauth(
model,
models,
claude_desktop_model_mappings,
claude_desktop_egress_allowed_hosts,
)
})?,
},
Expand Down Expand Up @@ -637,6 +641,113 @@ pub(crate) fn ordered_agent_models(
ordered
}

pub(crate) fn default_claude_desktop_egress_allowed_hosts() -> Vec<String> {
DEFAULT_CLAUDE_DESKTOP_EGRESS_ALLOWED_HOSTS
.iter()
.map(|host| (*host).to_string())
.collect()
}

fn claude_desktop_egress_host_is_valid(value: &str) -> bool {
if value == "*" {
return true;
}
if value.is_empty() || value.chars().any(char::is_whitespace) {
return false;
}

let mut parts = value.split(':');
let host = parts.next().unwrap_or_default();
if let Some(port) = parts.next() {
if parts.next().is_some()
|| port.is_empty()
|| !port.bytes().all(|byte| byte.is_ascii_digit())
|| port.parse::<u16>().ok().filter(|port| *port > 0).is_none()
{
return false;
}
}

let (wildcard, host) = match host.strip_prefix("*.") {
Some(host) => (true, host),
None => (false, host),
};
if host.is_empty() || host.contains('*') || host.len() > 253 {
return false;
}
if host.eq_ignore_ascii_case("localhost") {
return !wildcard;
}
if host.parse::<std::net::Ipv4Addr>().is_ok() {
return !wildcard;
}
if host.contains('.')
&& host
.bytes()
.all(|byte| byte.is_ascii_digit() || byte == b'.')
{
return false;
}

host.split('.').all(|label| {
!label.is_empty()
&& label.len() <= 63
&& label
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
&& label
.as_bytes()
.first()
.is_some_and(u8::is_ascii_alphanumeric)
&& label
.as_bytes()
.last()
.is_some_and(u8::is_ascii_alphanumeric)
})
}

pub(crate) fn normalize_claude_desktop_egress_allowed_hosts(
hosts: &[String],
) -> Result<Vec<String>, String> {
if hosts.is_empty() {
return Err("Claude Desktop 出站主机列表不能为空".to_string());
}
let mut normalized = Vec::with_capacity(hosts.len());
for host in hosts {
let value = host.trim().to_ascii_lowercase();
if !claude_desktop_egress_host_is_valid(&value) {
return Err(format!(
"Claude Desktop 出站主机格式无效: {host};仅支持 *、localhost、host[:port] 或 *.host[:port]"
));
}
if !normalized.iter().any(|existing| existing == &value) {
normalized.push(value);
}
}
Ok(normalized)
}

pub(crate) fn claude_desktop_egress_allowed_hosts_from_root(
root: &serde_json::Map<String, serde_json::Value>,
) -> Result<Option<Vec<String>>, String> {
let Some(value) = root.get(CLAUDE_DESKTOP_EGRESS_ALLOWED_HOSTS_KEY) else {
return Ok(None);
};
let entries = value
.as_array()
.ok_or_else(|| "Claude Desktop 出站主机必须是数组".to_string())?;
let hosts = entries
.iter()
.map(|entry| {
entry
.as_str()
.map(str::to_string)
.ok_or_else(|| "Claude Desktop 出站主机必须是字符串".to_string())
})
.collect::<Result<Vec<_>, _>>()?;
normalize_claude_desktop_egress_allowed_hosts(&hosts).map(Some)
}

pub(crate) fn build_claude_desktop_deployment_config(
existing: Option<&str>,
) -> Result<String, String> {
Expand All @@ -652,9 +763,20 @@ pub(crate) fn build_claude_desktop_profile(
model: &str,
models: &[AgentModelOption],
mappings: Option<&ClaudeDesktopModelMappings>,
egress_allowed_hosts: Option<&[String]>,
) -> Result<String, String> {
let mut root = parse_agent_json_object(existing, "Claude Desktop 网关配置")?;
root.remove("coworkEgressAllowedHosts");
let egress_allowed_hosts = match egress_allowed_hosts {
Some(hosts) => normalize_claude_desktop_egress_allowed_hosts(hosts)?,
None => claude_desktop_egress_allowed_hosts_from_root(&root)
.ok()
.flatten()
.unwrap_or_else(default_claude_desktop_egress_allowed_hosts),
};
root.insert(
CLAUDE_DESKTOP_EGRESS_ALLOWED_HOSTS_KEY.to_string(),
serde_json::json!(egress_allowed_hosts),
);
root.insert(
"disableDeploymentModeChooser".to_string(),
serde_json::json!(true),
Expand Down Expand Up @@ -841,7 +963,7 @@ pub(crate) fn remove_claude_desktop_managed_configuration(
if update_agent_json_file(&paths[2], "Claude Desktop 网关配置", |root| {
let mut updated = false;
for key in [
"coworkEgressAllowedHosts",
CLAUDE_DESKTOP_EGRESS_ALLOWED_HOSTS_KEY,
"disableDeploymentModeChooser",
"inferenceGatewayApiKey",
"inferenceGatewayAuthScheme",
Expand Down Expand Up @@ -1614,7 +1736,7 @@ pub(crate) fn build_restored_claude_desktop_config(
0 | 1 => restore_json_key(&mut root, original_root.as_ref(), "deploymentMode"),
2 => {
for key in [
"coworkEgressAllowedHosts",
CLAUDE_DESKTOP_EGRESS_ALLOWED_HOSTS_KEY,
"disableDeploymentModeChooser",
"inferenceGatewayApiKey",
"inferenceGatewayAuthScheme",
Expand Down
Loading