mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-06-21 07:23:56 +08:00
fix(config): prevent auto-pair from clobbering gateway auth on concurrent writes
patch_gateway_origins() read a full config snapshot and wrote it back via write_openclaw_config. Because gateway fields are shallow-merged, a stale snapshot could revert concurrent edits to gateway.auth, gateway.port, etc. while auto_pair_device runs on startup or WS reconnect. Write only a partial delta for controlUi.allowedOrigins and add a regression test documenting the merge behavior. Co-authored-by: 晴天 <1186258278@users.noreply.github.com>
This commit is contained in:
@@ -814,6 +814,46 @@ pub fn save_openclaw_json(config: &Value) -> Result<(), String> {
|
||||
write_openclaw_config(config.clone())
|
||||
}
|
||||
|
||||
/// Append required Gateway control-ui origins without clobbering other gateway fields.
|
||||
///
|
||||
/// Callers must pass a **partial** delta (not a full config snapshot). `write_openclaw_config`
|
||||
/// shallow-merges top-level objects; a stale full snapshot would revert concurrent edits to
|
||||
/// `gateway.auth`, `gateway.port`, etc.
|
||||
pub fn append_gateway_allowed_origins(required: &[&str]) -> Result<(), String> {
|
||||
let config = load_openclaw_json().unwrap_or_else(|_| json!({}));
|
||||
let existing: Vec<String> = config
|
||||
.pointer("/gateway/controlUi/allowedOrigins")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|value| value.as_str().map(|value| value.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut merged = existing.clone();
|
||||
let mut changed = false;
|
||||
for origin in required {
|
||||
let origin = (*origin).to_string();
|
||||
if !merged.iter().any(|existing| existing == &origin) {
|
||||
merged.push(origin);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let delta = json!({
|
||||
"gateway": {
|
||||
"controlUi": {
|
||||
"allowedOrigins": merged
|
||||
}
|
||||
}
|
||||
});
|
||||
write_openclaw_config(delta)
|
||||
}
|
||||
|
||||
fn model_env_values_for_config(config: &Value) -> HashMap<String, String> {
|
||||
let mut values = HashMap::new();
|
||||
if let Some(env) = config.get("env").and_then(|v| v.as_object()) {
|
||||
@@ -7215,6 +7255,61 @@ mod write_openclaw_config_merge_tests {
|
||||
std::env::temp_dir().join(format!("clawpanel-{name}-{}-{suffix}", std::process::id()))
|
||||
}
|
||||
|
||||
/// Regression: auto-pair must patch allowedOrigins with a partial delta, not a stale
|
||||
/// full config snapshot — otherwise shallow gateway merge reverts concurrent auth/port edits.
|
||||
#[test]
|
||||
fn partial_gateway_patch_preserves_auth_and_port() {
|
||||
let existing = json!({
|
||||
"gateway": {
|
||||
"port": 9999,
|
||||
"auth": { "mode": "token", "token": "user-new-token" },
|
||||
"controlUi": { "allowedOrigins": ["http://localhost"] }
|
||||
}
|
||||
});
|
||||
let stale_full_snapshot = json!({
|
||||
"gateway": {
|
||||
"port": 18789,
|
||||
"auth": { "mode": "token", "token": "old-token" },
|
||||
"controlUi": {
|
||||
"allowedOrigins": [
|
||||
"http://localhost",
|
||||
"tauri://localhost",
|
||||
"http://localhost:1420"
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
let clobbered = merge_configs_preserving_fields(&existing, &stale_full_snapshot);
|
||||
assert_eq!(
|
||||
clobbered["gateway"]["auth"]["token"],
|
||||
"old-token",
|
||||
"full snapshot merge must reproduce the clobber bug for this test"
|
||||
);
|
||||
|
||||
let delta = json!({
|
||||
"gateway": {
|
||||
"controlUi": {
|
||||
"allowedOrigins": [
|
||||
"http://localhost",
|
||||
"tauri://localhost",
|
||||
"http://localhost:1420"
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
let merged = merge_configs_preserving_fields(&existing, &delta);
|
||||
assert_eq!(merged["gateway"]["auth"]["token"], "user-new-token");
|
||||
assert_eq!(merged["gateway"]["port"], 9999);
|
||||
assert_eq!(
|
||||
merged["gateway"]["controlUi"]["allowedOrigins"],
|
||||
json!([
|
||||
"http://localhost",
|
||||
"tauri://localhost",
|
||||
"http://localhost:1420"
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression guard: Issue #127 merge keeps full provider map when the UI payload
|
||||
/// only touches one provider — `sync_providers_to_agent_models` must use the same
|
||||
/// merged view (see `write_openclaw_config`), not the raw `config` argument.
|
||||
|
||||
@@ -297,50 +297,14 @@ pub fn auto_pair_device() -> Result<String, String> {
|
||||
/// 将 Tauri 应用的 origin 写入 gateway.controlUi.allowedOrigins
|
||||
/// 避免 Gateway 因 origin not allowed 拒绝 WebSocket 握手
|
||||
fn patch_gateway_origins() {
|
||||
let Ok(mut config) = super::config::load_openclaw_json() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Tauri 应用 + 本地开发服务器必须存在的 origin
|
||||
let required: Vec<String> = vec![
|
||||
"tauri://localhost".into(),
|
||||
"https://tauri.localhost".into(),
|
||||
"http://tauri.localhost".into(),
|
||||
"http://localhost:1420".into(),
|
||||
"http://127.0.0.1:1420".into(),
|
||||
const REQUIRED: &[&str] = &[
|
||||
"tauri://localhost",
|
||||
"https://tauri.localhost",
|
||||
"http://tauri.localhost",
|
||||
"http://localhost:1420",
|
||||
"http://127.0.0.1:1420",
|
||||
];
|
||||
|
||||
if let Some(obj) = config.as_object_mut() {
|
||||
let gateway = obj
|
||||
.entry("gateway")
|
||||
.or_insert_with(|| serde_json::json!({}));
|
||||
if let Some(gw) = gateway.as_object_mut() {
|
||||
let control_ui = gw
|
||||
.entry("controlUi")
|
||||
.or_insert_with(|| serde_json::json!({}));
|
||||
if let Some(cui) = control_ui.as_object_mut() {
|
||||
// 合并:保留用户已有的 origin,追加缺失的 Tauri origin
|
||||
let existing: Vec<String> = cui
|
||||
.get("allowedOrigins")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|s| s.as_str().map(String::from))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut merged = existing;
|
||||
for r in &required {
|
||||
if !merged.iter().any(|e| e == r) {
|
||||
merged.push(r.clone());
|
||||
}
|
||||
}
|
||||
cui.insert("allowedOrigins".to_string(), serde_json::json!(merged));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = super::config::save_openclaw_json(&config);
|
||||
let _ = super::config::append_gateway_allowed_origins(REQUIRED);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
Reference in New Issue
Block a user