继续收敛 app-server 进程错误

保留执行器核验、OAuth、回合身份与图片解码正文

保留进程树归属、Job、等待和终止原因

补充进程树与 base64 回归验证
This commit is contained in:
kdletters
2026-10-06 01:31:40 +08:00
parent f8b7b44d66
commit 8710de7bde
4 changed files with 60 additions and 23 deletions
@@ -1780,8 +1780,8 @@ async fn stage_codex_app_server_image(
let (extension, encoded) = image_data_url_parts(image_url)?;
let bytes = base64::engine::general_purpose::STANDARD
.decode(encoded)
.map_err(|_| {
platform_llm::LlmError::InvalidRequest("多模态图片 base64 内容无效".to_string())
.map_err(|error| {
platform_llm::LlmError::InvalidRequest(format!("多模态图片 base64 内容无效:{error}"))
})?;
if bytes.is_empty() || bytes.len() > GAME_CREATOR_CODEX_APP_SERVER_IMAGE_MAX_BYTES {
return Err(platform_llm::LlmError::InvalidRequest(
@@ -2527,7 +2527,9 @@ impl CodexAppServerConnection {
Ok::<_, String>((executable, version))
})
.await
.map_err(|_| platform_llm::LlmError::InvalidConfig("Codex 执行器身份核验中断".into()))?
.map_err(|error| {
platform_llm::LlmError::InvalidConfig(format!("Codex 执行器身份核验中断:{error}"))
})?
.map_err(platform_llm::LlmError::InvalidConfig)?;
if workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
execution::validate_approval_version(&codex_cli_version)
@@ -3066,8 +3068,10 @@ impl CodexAppServerConnection {
let private_home = isolated_codex_home.clone();
tokio::task::spawn_blocking(move || handoff.remember(&private_home))
.await
.map_err(|_| {
platform_llm::LlmError::InvalidConfig("私有 OAuth 轮换状态保存中断".into())
.map_err(|error| {
platform_llm::LlmError::InvalidConfig(format!(
"私有 OAuth 轮换状态保存中断:{error}"
))
})?
.map_err(platform_llm::LlmError::InvalidConfig)?;
}
@@ -5501,9 +5505,9 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer(
.map(|state| state.client_turn_id)
})
.await
.map_err(|_| {
.map_err(|error| {
TurnError::HostStateUnavailable(HostStateUnavailable {
detail: "宿主 CLI 回合身份读取中断".to_string(),
detail: format!("宿主 CLI 回合身份读取中断:{error}"),
})
})?
.map_err(|detail| TurnError::HostStateUnavailable(HostStateUnavailable { detail }))?;
@@ -6650,6 +6654,18 @@ mod tests {
assert!(std::path::Path::new(staged_path).is_file());
}
#[tokio::test]
async fn app_server_invalid_base64_preserves_decoder_detail() {
let temp = tempfile::tempdir().expect("temp dir");
let error =
stage_codex_app_server_image(temp.path(), "data:image/png;base64,not-base64", 0)
.await
.unwrap_err()
.to_string();
assert!(error.contains("多模态图片 base64 内容无效"), "{error}");
assert!(error.contains("Invalid"), "{error}");
}
#[test]
fn app_server_multimodal_validation_rejects_system_images() {
let request = LlmRunRequest::new(vec![LlmMessage::multimodal(
@@ -46,15 +46,15 @@ impl OwnedProcessTree {
#[cfg(unix)]
let start_identity =
crate::process_identity::external_agent_runner_process_start_identity(pid)
.map_err(|_| "app-server-process-identity-unknown")?
.map_err(|error| format!("app-server-process-identity-unknown:{error}"))?
.filter(|value| !value.is_empty())
.ok_or("app-server-process-identity-unknown")?;
.ok_or_else(|| "app-server-process-identity-unknown:启动身份为空".to_string())?;
#[cfg(windows)]
let job = crate::process_session::WindowsProcessJob::assign_tokio_named(
child,
&format!("Local\\AGCCodex-{}", uuid::Uuid::new_v4()),
)
.map_err(|_| "app-server-process-job-unavailable")?;
.map_err(|error| format!("app-server-process-job-unavailable:{error}"))?;
#[cfg(not(any(windows, unix)))]
return Err("app-server-process-control-unsupported".into());
#[cfg(any(windows, unix))]
@@ -91,16 +91,26 @@ impl OwnedProcessTree {
return Err("app-server-process-owner-mismatch".into());
}
let deadline = tokio::time::Instant::now() + STOP_TIMEOUT;
if self.terminate_owned_tree().is_err() {
if let Err(error) = self.terminate_owned_tree() {
// 只清理仍持有的直属 Child 句柄;不能据此报告子树已回收。
let _ = child.start_kill();
let _ = tokio::time::timeout_at(deadline, child.wait()).await;
return Err("app-server-process-tree-termination-uncertain".into());
let kill_error = child
.start_kill()
.err()
.map(|error| format!(";直属进程终止失败:{error}"))
.unwrap_or_default();
let wait_error = match tokio::time::timeout_at(deadline, child.wait()).await {
Ok(Ok(_)) => String::new(),
Ok(Err(error)) => format!(";等待直属进程退出失败:{error}"),
Err(_) => ";等待直属进程退出超时".to_string(),
};
return Err(format!(
"app-server-process-tree-termination-uncertain:{error}{kill_error}{wait_error}"
));
}
tokio::time::timeout_at(deadline, child.wait())
.await
.map_err(|_| "app-server-process-exit-timeout")?
.map_err(|_| "app-server-process-exit-unconfirmed")?;
.map_err(|_| "app-server-process-exit-timeout:等待直属进程退出超过 5 秒".to_string())?
.map_err(|error| format!("app-server-process-exit-unconfirmed:{error}"))?;
loop {
if self.observed_tree_empty()? {
break;
@@ -127,7 +137,7 @@ impl OwnedProcessTree {
{
self.job
.is_empty()
.map_err(|_| "app-server-process-tree-state-unknown".into())
.map_err(|error| format!("app-server-process-tree-state-unknown:{error}"))
}
#[cfg(unix)]
{
@@ -141,7 +151,10 @@ impl OwnedProcessTree {
if std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) {
Ok(true)
} else {
Err("app-server-process-tree-state-unknown".into())
Err(format!(
"app-server-process-tree-state-unknown:{}",
std::io::Error::last_os_error()
))
}
}
#[cfg(not(any(windows, unix)))]
@@ -155,7 +168,7 @@ impl OwnedProcessTree {
{
self.job
.terminate()
.map_err(|_| "app-server-process-tree-termination-uncertain".into())
.map_err(|error| format!("app-server-process-tree-termination-uncertain:{error}"))
}
#[cfg(unix)]
{
@@ -165,7 +178,7 @@ impl OwnedProcessTree {
// leader 已消失或 PID 被复用时不盲杀 PGID;保留不确定状态。
let current =
crate::process_identity::external_agent_runner_process_start_identity(self.pid)
.map_err(|_| "app-server-process-identity-unknown")?;
.map_err(|error| format!("app-server-process-identity-unknown:{error}"))?;
if current.as_deref() != Some(self.start_identity.as_str()) {
return Err("app-server-process-owner-mismatch".into());
}
@@ -173,7 +186,10 @@ impl OwnedProcessTree {
if result == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) {
Ok(())
} else {
Err("app-server-process-tree-termination-uncertain".into())
Err(format!(
"app-server-process-tree-termination-uncertain:{}",
std::io::Error::last_os_error()
))
}
}
#[cfg(not(any(windows, unix)))]
@@ -186,7 +202,9 @@ impl OwnedProcessTree {
impl Drop for OwnedProcessTree {
fn drop(&mut self) {
// Drop 只做应急回收,永远不伪造完成证明;正式终态必须 await shutdown。
let _ = self.terminate_owned_tree();
if let Err(error) = self.terminate_owned_tree() {
app_log!("agent.codex_app_server.process_tree.drop_cleanup_failed error={error}");
}
}
}
@@ -52,7 +52,9 @@ struct ContextIdentity {
fn context_identity(root: &Path) -> Result<ContextIdentity, String> {
let manifest = read_existing_manifest_for_project(root)?;
let revision = read_game_creator_agent_runtime_project_revision(root)?.revision;
let canonical = root.canonicalize().map_err(|_| "项目上下文目录无法解析")?;
let canonical = root
.canonicalize()
.map_err(|error| format!("项目上下文目录无法解析:{error}"))?;
let active = list_active_turns()?.into_iter().find(|turn| {
Path::new(&turn.project_path).canonicalize().ok().as_ref() == Some(&canonical)
});
@@ -91,3 +91,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时,
- Direct 交付终态读取原先把 `session.snapshot()` 锁/状态错误折叠成 `None`,上层继续显示“尚未确认交付完成”;现沿 `code-generation` 失败出口保留终态读取正文,并在已有回合失败时同时保留原始失败与终态读取失败。
- 对话模型选择器读取 native 配置原先把 IPC/文件异常 `.catch(() => null)` 后只显示“读取客户端配置失败”;现通过统一可见错误脱敏出口保留具体正文,并保留配置不可读时不猜测模型路由的安全行为。
- 策划工作区事件订阅失败仍有一处直接静默结束,导致文件刷新失联却没有提示;现把订阅 IPC 正文写入工作区错误状态,继续保留手动刷新与已有清单读取错误出口。
- 新一轮原生扫出 app-server 进程与上下文漏损:Codex 执行器核验/OAuth 保存/回合身份的 JoinError、图片 base64 解码、Direct 上下文 canonicalize,以及进程树归属、Job、等待、终止和 Drop 清理错误原先只剩固定码;现保留 JoinError、解码器、OS/Job、等待和终止阶段正文,并为进程树 Drop 清理失败写入诊断日志。