Merge remote-tracking branch 'origin/master' into feat/agc-canvas-resource-workbench-v3

This commit is contained in:
2026-09-10 21:24:05 +08:00
14 changed files with 95 additions and 2902 deletions
@@ -3640,7 +3640,38 @@ pub(crate) fn merge_game_creator_config_file(
Ok(())
}
fn read_game_creator_config_file(path: &Path) -> Result<Option<String>, String> {
/// Only files inside the managed runtime config directory (real credentials)
/// may use the private-read channel: it hardens the owner/DACL on every read.
/// Repository-adjacent fallback templates and overlay files are shared inputs
/// that can be git-tracked; privatizing one on read silently locks the
/// worktree template to whichever account happened to run the dev CLI, so
/// they must go through the non-mutating snapshot channel instead.
pub(crate) fn game_creator_config_path_is_runtime_managed(path: &Path) -> bool {
game_creator_runtime_config_dir().is_some_and(|directory| path.starts_with(directory))
}
fn read_game_creator_snapshot_file_to_string(
path: &Path,
label: &str,
max_bytes: u64,
) -> Result<String, String> {
let (mut file, metadata) = open_project_snapshot_regular_file(path, label)?;
if metadata.len() > max_bytes {
return Err(format!("{label}过大,已拒绝读取:{}", path.display()));
}
let mut content = String::with_capacity(metadata.len() as usize);
file.read_to_string(&mut content)
.map_err(|error| format!("读取{label}失败:{}: {error}", path.display()))?;
let final_metadata = file
.metadata()
.map_err(|error| format!("复核{label}失败:{}: {error}", path.display()))?;
if final_metadata.len() != metadata.len() {
return Err(format!("{label}读取期间文件发生漂移:{}", path.display()));
}
Ok(content)
}
pub(crate) fn read_game_creator_config_file(path: &Path) -> Result<Option<String>, String> {
let backup_path = game_creator_config_backup_path(path);
let path_exists = validate_game_creator_config_file_entry(path)?;
let read_path = if path_exists {
@@ -3650,7 +3681,11 @@ fn read_game_creator_config_file(path: &Path) -> Result<Option<String>, String>
} else {
return Ok(None);
};
let content = read_game_creator_private_file_to_string(read_path, "客户端配置", 256 * 1024)?;
let content = if game_creator_config_path_is_runtime_managed(read_path) {
read_game_creator_private_file_to_string(read_path, "客户端配置", 256 * 1024)?
} else {
read_game_creator_snapshot_file_to_string(read_path, "客户端配置", 256 * 1024)?
};
Ok(Some(content))
}
@@ -1184,6 +1184,56 @@ fn llm_config_check_reports_per_agent_status_without_leaking_keys() {
fs::remove_dir_all(root).ok();
}
#[test]
fn fallback_template_read_stays_on_snapshot_channel_outside_runtime_dir() {
let root = unique_project_path();
let template_dir = root.join("apps").join("ai-game-creator-shell");
fs::create_dir_all(&template_dir).expect("fallback template dir");
let template = template_dir.join(GAME_CREATOR_CONFIG_FILE_NAME);
fs::write(
&template,
"{\n \"llm\": { \"model\": \"fallback-template-model\" }\n}\n",
)
.expect("write fallback template");
// 设置一个与模板无关的 runtime dir,让分类断言真正覆盖"按路径归属"而非
// "runtime dir 为 None 时恒 false"的全局开关。
let runtime_root = unique_project_path();
fs::create_dir_all(&runtime_root).expect("unrelated runtime config dir");
let _guard = use_test_runtime_config_dir(runtime_root.clone());
// 仓库旁边的回退模板是共享输入,读取绝不能走会收紧 owner/DACL 的私有通道。
assert!(!game_creator_config_path_is_runtime_managed(&template));
assert!(game_creator_config_path_is_runtime_managed(
&runtime_root.join(GAME_CREATOR_CONFIG_FILE_NAME)
));
let content = read_game_creator_config_file(&template).expect("read fallback template");
assert!(content
.expect("fallback template content")
.contains("fallback-template-model"));
fs::remove_dir_all(root).ok();
fs::remove_dir_all(runtime_root).ok();
}
#[test]
fn runtime_config_read_stays_on_private_channel_inside_runtime_dir() {
let root = unique_project_path();
fs::create_dir_all(&root).expect("runtime config dir");
let config_path = root.join(GAME_CREATOR_CONFIG_FILE_NAME);
fs::write(
&config_path,
"{\n \"llm\": { \"model\": \"managed-config-model\" }\n}\n",
)
.expect("write runtime config");
let _guard = use_test_runtime_config_dir(root.clone());
assert!(game_creator_config_path_is_runtime_managed(&config_path));
let content = read_game_creator_config_file(&config_path).expect("read runtime config");
assert!(content
.expect("runtime config content")
.contains("managed-config-model"));
fs::remove_dir_all(root).ok();
}
#[test]
fn llm_config_check_reports_agent_specific_config_paths() {
let root = unique_project_path();