修复 Codex 隔离用户目录被系统临时路径符号链接阻断
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m31s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m58s
Project CI / Backend tests (pull_request) Successful in 3m57s
Project CI / Frontend tests (pull_request) Successful in 2m14s
Project CI / Native shell tests (pull_request) Successful in 6m8s
Project CI / Repository checks (pull_request) Failing after 56s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m49s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m37s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m49s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m31s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m58s
Project CI / Backend tests (pull_request) Successful in 3m57s
Project CI / Frontend tests (pull_request) Successful in 2m14s
Project CI / Native shell tests (pull_request) Successful in 6m8s
Project CI / Repository checks (pull_request) Failing after 56s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m49s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m37s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m49s
- 临时目录确认是普通目录后先解析真实路径,再创建 codex-home、workspace 和隔离用户目录 - 祖先符号链接检查不放宽,临时目录内部的符号链接仍然拒绝 - 补充一条符号链接祖先与内部链接测试 - 记录决策与排障说明
This commit is contained in:
@@ -2167,6 +2167,43 @@ fn configure_game_creator_codex_app_server_command_for_mode(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 新建的私有运行目录必须已经是普通目录。解析掉系统临时目录上的符号链接
|
||||
/// (macOS 的 `/var`、`/tmp`),后续私有子目录在真实路径上创建。祖先检查本身不放宽。
|
||||
fn canonical_codex_private_runtime_dir(
|
||||
path: &std::path::Path,
|
||||
) -> Result<std::path::PathBuf, String> {
|
||||
let metadata = std::fs::symlink_metadata(path)
|
||||
.map_err(|error| format!("读取 Codex 私有运行目录失败:{}: {error}", path.display()))?;
|
||||
if metadata.file_type().is_symlink() || !metadata.is_dir() {
|
||||
return Err(format!(
|
||||
"Codex 私有运行目录必须是普通目录:{}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
|
||||
if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
|
||||
return Err(format!(
|
||||
"Codex 私有运行目录不能是 Windows reparse point:{}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
let canonical = std::fs::canonicalize(path)
|
||||
.map_err(|error| format!("解析 Codex 私有运行目录失败:{}: {error}", path.display()))?;
|
||||
Ok(
|
||||
match canonical
|
||||
.to_str()
|
||||
.and_then(|value| value.strip_prefix("\\\\?\\"))
|
||||
{
|
||||
Some(stripped) => std::path::PathBuf::from(stripped),
|
||||
None => canonical,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn prepare_isolated_game_creator_codex_home(
|
||||
root: &std::path::Path,
|
||||
credential: &CodexAppServerCredential,
|
||||
@@ -2590,6 +2627,14 @@ impl CodexAppServerConnection {
|
||||
Some(working_dir.path()),
|
||||
))
|
||||
})?;
|
||||
// 叶子已确认是普通目录。先去掉系统临时目录上的符号链接,再创建私有子目录。
|
||||
let private_runtime_dir =
|
||||
canonical_codex_private_runtime_dir(working_dir.path()).map_err(|error| {
|
||||
platform_llm::LlmError::Transport(crate::sanitize_diagnostic_message(
|
||||
&error,
|
||||
Some(working_dir.path()),
|
||||
))
|
||||
})?;
|
||||
let (direct_provider_route, main_site_upstream) = match credential {
|
||||
CodexAppServerCredential::PlatformSession {
|
||||
api_base_url,
|
||||
@@ -2614,11 +2659,11 @@ impl CodexAppServerConnection {
|
||||
let remote_control_disable_reason =
|
||||
credential.remote_control_disable_reason(direct_provider_route.is_some());
|
||||
let isolated_codex_home = prepare_isolated_game_creator_codex_home(
|
||||
working_dir.path(),
|
||||
&private_runtime_dir,
|
||||
credential,
|
||||
direct_provider_route.is_some(),
|
||||
)?;
|
||||
let isolated_workspace = working_dir.path().join("workspace");
|
||||
let isolated_workspace = private_runtime_dir.join("workspace");
|
||||
if workspace_override.is_none() {
|
||||
std::fs::create_dir(&isolated_workspace).map_err(|error| {
|
||||
platform_llm::LlmError::Transport(format!(
|
||||
@@ -2668,7 +2713,7 @@ impl CodexAppServerConnection {
|
||||
&client_mcp_servers,
|
||||
)?;
|
||||
}
|
||||
let isolated_os_home = working_dir.path().join("home");
|
||||
let isolated_os_home = private_runtime_dir.join("home");
|
||||
let isolated_app_data = isolated_os_home.join("appdata");
|
||||
let isolated_local_app_data = isolated_os_home.join("local-appdata");
|
||||
for path in [
|
||||
@@ -2680,7 +2725,7 @@ impl CodexAppServerConnection {
|
||||
|error| {
|
||||
platform_llm::LlmError::Transport(format!(
|
||||
"创建 Codex app-server 隔离用户目录失败:{}",
|
||||
crate::sanitize_diagnostic_message(&error, Some(working_dir.path()))
|
||||
crate::sanitize_diagnostic_message(&error, Some(&private_runtime_dir))
|
||||
))
|
||||
},
|
||||
)?;
|
||||
@@ -6652,6 +6697,55 @@ mod tests {
|
||||
assert_eq!(resolved, outside.canonicalize().expect("canonical outside"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn codex_private_runtime_dir_resolves_a_symlink_ancestor_and_rejects_an_inner_link() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let temp = tempfile::tempdir().expect("temp dir");
|
||||
let real = temp.path().join("real-temp");
|
||||
let link = temp.path().join("link-temp");
|
||||
std::fs::create_dir(&real).expect("real temp");
|
||||
symlink(&real, &link).expect("temp symlink");
|
||||
let session = link.join("session");
|
||||
std::fs::create_dir(&session).expect("session through symlink");
|
||||
|
||||
let unresolved = crate::ensure_game_creator_private_directory_tree(
|
||||
&session.join("home"),
|
||||
"Codex 隔离用户目录",
|
||||
);
|
||||
assert!(
|
||||
unresolved
|
||||
.expect_err("symlink ancestor")
|
||||
.contains("路径不能包含符号链接")
|
||||
);
|
||||
|
||||
let resolved = canonical_codex_private_runtime_dir(&session).expect("resolve runtime dir");
|
||||
assert_eq!(
|
||||
resolved,
|
||||
std::fs::canonicalize(&session).expect("canonical session")
|
||||
);
|
||||
assert!(
|
||||
crate::ensure_game_creator_private_directory_tree(
|
||||
&resolved.join("home"),
|
||||
"Codex 隔离用户目录",
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
let planted = resolved.join("planted");
|
||||
std::fs::create_dir(&planted).expect("planted dir");
|
||||
symlink(&planted, resolved.join("alias")).expect("inner symlink");
|
||||
assert!(
|
||||
crate::ensure_game_creator_private_directory_tree(
|
||||
&resolved.join("alias").join("secret"),
|
||||
"Codex 隔离用户目录",
|
||||
)
|
||||
.expect_err("inner symlink")
|
||||
.contains("符号链接")
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn direct_project_pool_identity_follows_the_canonical_project_target() {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-09-29 Codex 私有运行目录先解析系统临时路径上的符号链接
|
||||
|
||||
- 决策:新建的 Codex 临时目录确认是普通目录后,先解析为真实路径并去掉 Windows `\\?\` 前缀,再创建 `codex-home`、`workspace` 和隔离用户目录。macOS 的 `/var`、`/tmp` 这类系统符号链接不再阻断 Game Agent 启动。
|
||||
- 范围:`validate_game_creator_private_path_ancestors` 不放宽。AppData、客户端配置,以及临时目录内部新出现的符号链接,仍然拒绝。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs`。
|
||||
|
||||
## 2026-09-29 策划文件工具说明去掉路径硬限制
|
||||
|
||||
- 决策:常驻提示词仍要求策划文件放在工作区内并使用相对路径。`read_file`、`write_file`、`list_dir`、`search_text`、`patch_file`、`delete_path` 的工具说明不再写「工作目录内」或「path 使用相对路径」。说明不宣布可以访问绝对路径或工作区外路径。
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# 踩坑与排障记录
|
||||
|
||||
## 2026-09-29 Codex 隔离用户目录被系统临时目录的符号链接拦下
|
||||
|
||||
- **现象**:Game Agent 回合还没改项目文件就失败。卡片是 `direct-codex-failure:v2 stage=code-generation code=runtime-failure`,摘要为「创建 Codex app-server 隔离用户目录失败:Codex 隔离用户目录 路径不能包含符号链接:<absolute-path>」。诊断文件同样把路径脱敏,看不出是哪一级目录。
|
||||
- **原因**:临时目录保留 `TMPDIR` 或 `/tmp` 的原始路径。macOS 上 `/var` 指向 `/private/var`,未设置 `TMPDIR` 时 `/tmp` 指向 `/private/tmp`。隔离用户目录会检查全部现存祖先,把这些系统符号链接当成私有目录里的链接拒绝。Linux 和 Windows 只在临时路径上确实有符号链接时同样失败。
|
||||
- **处理**:临时目录确认是普通目录后先解析成真实路径,再创建 `codex-home`、`workspace` 和 `home`。祖先检查不放宽;临时目录内部的符号链接仍然拒绝。
|
||||
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs` 的 `canonical_codex_private_runtime_dir`。
|
||||
|
||||
## 2026-09-29 dev 渠道改名后,更新路径不会重建快捷方式(旧图标一直指向旧安装)
|
||||
|
||||
- **现象**:改名前的 dev 客户端(展示名 `陶泥儿`,0.1.126)点更新后自动启动的是新版(`陶泥儿开发版`,0.1.160),但桌面/开始菜单里的 `陶泥儿.lnk` 打开的还是旧版。`%LOCALAPPDATA%` 下同时留着 `陶泥儿` 与 `陶泥儿开发版` 两个安装目录,`HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall` 下同时留着 `陶泥儿`(0.1.109)与 `陶泥儿开发版`(0.1.160)两个卸载项,而新版一份快捷方式都没有。
|
||||
|
||||
Reference in New Issue
Block a user