From ed2ae2feb9e411ba894905894373f8347f0624ad Mon Sep 17 00:00:00 2001 From: suzmii Date: Wed, 26 Aug 2026 12:52:16 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8DAGC=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=97=B6=E6=B5=8B=E8=AF=95=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 恢复 Direct Runtime 审核 Skill 索引注入并校正内置 Skill 内容指纹。 修复浏览器诊断、委派回执和 Runtime 失败投影的敏感信息脱敏边界。 收窄浏览器脱敏测试断言,避免将安全占位符误判为原始密钥。 --- .../resources/agc-skills/manifest.json | 4 +-- .../src-tauri/src/agent.rs | 2 ++ .../src-tauri/src/agent/direct_runtime.rs | 6 ++-- .../src/agent/generation/prompt_context.rs | 32 +++++++++++++++++++ .../src/agent/runtime_tools/delivery.rs | 12 ++++--- 5 files changed, 48 insertions(+), 8 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json index 257639210..82d0db49f 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json @@ -40,7 +40,7 @@ "agents/openai.yaml", "references/platform-art-contract.md" ], - "sha256": "8e5b54035bff777cd0714ab203cb27c753fc83cf22d93ae2389bf28bfaf05ef6" + "sha256": "6340ba68146823fe56f8ad265e3b8f9329cdc842d52ad63e72b05285bb23334e" }, { "name": "agc-web-game-development", @@ -76,7 +76,7 @@ "agents/openai.yaml", "references/browser-evidence-contract.md" ], - "sha256": "403a9341ece8c73d6e3bc09059b556b1c775a2f9ceb75c30541c9dd79fd52bd5" + "sha256": "a6f967cb1947e1d40215e2e7186a8b13fae71800aa04d6a47ef2b0af25890825" }, { "name": "agc-client-projection", diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index 48082f781..69d3ffdff 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -22,6 +22,7 @@ mod runtime_driver; mod runtime_protocol; mod runtime_state; mod runtime_tools; +mod skill_pack; use codex_app_server::*; pub(crate) use codex_app_server::{ direct_game_creator_codex_chat_at, direct_game_creator_home_codex_chat, @@ -41,6 +42,7 @@ pub(crate) use runtime_driver::*; pub(crate) use runtime_protocol::*; pub(crate) use runtime_state::*; pub(crate) use runtime_tools::*; +pub(crate) use skill_pack::*; pub(crate) fn shutdown_game_creator_codex_app_servers() -> Result<(), String> { shutdown_game_creator_codex_app_servers_impl() diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs index c4aed6f08..bdfcef57d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs @@ -13,7 +13,7 @@ const MAX_DIRECT_HOME_ATTACHMENT_MEDIA_TYPE_CHARS: usize = 96; const MIN_DIRECT_CLIENT_TURN_ID_CHARS: usize = 6; const MAX_DIRECT_CLIENT_TURN_ID_CHARS: usize = 160; const DIRECT_TAONIER_IDENTITY_GUIDANCE: &str = "对外身份合同:你是“陶泥儿”,是 Genarrative 的游戏创作助手。用户询问你是谁、你的名称或能力时,以陶泥儿的身份回答;不要把 Codex、ChatGPT、OpenAI、模型、通用 AI 助手或内部执行智能体当作自己的名称或对外身份。Codex app-server 仅是客户端内部执行技术;只有用户明确询问底层实现时才可如实说明,同时仍以陶泥儿自称。"; -const DIRECT_AGC_ENGINEERING_GUIDANCE: &str = "AGC 工程合同:当前 Codex cwd 是项目真实 `game/` 源码目录,只允许把项目源码写入该目录;原生文件工具、原生 patch 和命令参数中的文件路径必须相对于当前 cwd:合法写法是 `index.html`、`style.css`、`game.js`,禁止写 `game/index.html`、`../game/index.html`、项目根绝对路径或任何其它父目录路径;`game/...` 只用于 AGC 回执、manifest 和客户端投影,不用于 cwd 内的原生 patch。不要用原生文件或命令工具遍历父目录;`.agent/`、`assets/` 和项目根由客户端维护,不能请求扩权或直接改写。DirectProject 只提供当前工作区内的 Codex 原生文件、搜索、命令和图片查看能力,不提供外部工具目录。按用户意图自行检查、修改和验证,不要等待 Supervisor、harness 或宿主规划器。不要读取或输出凭据、Token、Cookie、auth.json、.env 或宿主私密路径。项目锁、付费提交、幂等账本、下载校验和客户端投影仍由客户端确定性掌管。游戏文件真实变化后由客户端登记资源和版本,Codex 不直接保存或伪造项目版本。"; +const DIRECT_AGC_ENGINEERING_GUIDANCE: &str = "AGC 工程合同:当前 Codex cwd 是项目真实 `game/` 源码目录,只允许把项目源码写入该目录;原生文件工具、原生 patch 和命令参数中的文件路径必须相对于当前 cwd:合法写法是 `index.html`、`style.css`、`game.js`,禁止写 `game/index.html`、`../game/index.html`、项目根绝对路径或任何其它父目录路径;`game/...` 只用于 AGC 回执、manifest 和客户端投影,不用于 cwd 内的原生 patch。不要用原生文件或命令工具遍历父目录;`.agent/`、`assets/` 和项目根由客户端维护,不能请求扩权或直接改写。DirectProject 提供 Codex 原生文件、搜索、命令、图片查看、Skill 能力,但只限当前工作区,不提供外部工具目录。按用户意图自行检查、修改和验证,不要等待 Supervisor、harness 或宿主规划器。不要读取或输出凭据、Token、Cookie、auth.json、.env 或宿主私密路径。项目锁、付费提交、幂等账本、下载校验和客户端投影仍由客户端确定性掌管。游戏文件真实变化后由客户端登记资源和版本,Codex 不直接保存或伪造项目版本。"; const DIRECT_CODEX_ART_SPEC_ASSET_PATH: &str = "assets/art-spec.png"; const DIRECT_CODEX_BACKGROUND_ASSET_PATH: &str = "assets/direct-game-background.png"; const DIRECT_CODEX_SPRITESHEET_ASSET_PATH: &str = "assets/art-spritesheet.png"; @@ -3634,12 +3634,14 @@ pub(crate) fn build_direct_codex_system_prompt(root: &Path) -> Result Result { + let skill_index = render_agc_skill_pack_index()?; let sections = vec![ "你是陶泥儿,是 Genarrative 面向用户的游戏创作助手,也是当前唯一执行主体。用户聊天内容会原样直接发送给你;先自行理解意图:普通对话直接回答且不触碰工作区,项目请求再按需要检查、修改、运行和验证,并用简洁中文报告真实结果。客户端不会根据关键词替你决定新建、续做、生图、试玩、返工或版本登记。".to_string(), DIRECT_TAONIER_IDENTITY_GUIDANCE.to_string(), "工作区边界:只在当前项目目录内工作;不要读取或输出凭据、Token、Cookie、auth.json、.env 或宿主私密路径。遇到阻断必须说明具体原因、文件和下一步,不要声称未验证的成功。".to_string(), DIRECT_AGC_ENGINEERING_GUIDANCE.to_string(), "工程执行要求:优先复用现有结构;按需读取真实文件,不依赖客户端预注入源码快照;修改后运行与改动相关的本地验证。不要创建 Supervisor、专业 Agent 或平行项目。".to_string(), + format!("提示词与技能:{skill_index}"), ]; Ok(sections .join("\n") @@ -6054,7 +6056,7 @@ mod tests { assert!(prompt.contains("desktop exception:")); assert!(prompt.contains("AGC 浏览器事实证据")); - assert!(!prompt.contains("secret"), "{prompt}"); + assert!(!prompt.contains("token=secret"), "{prompt}"); assert!( !prompt.contains(&root.path().display().to_string()), "{prompt}" diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs index c1506fa6d..db1b51976 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs @@ -450,6 +450,10 @@ pub(crate) fn sanitize_prompt_context(value: &str) -> String { let mut inside_private_key = false; for line in value.lines() { let lower = line.to_ascii_lowercase(); + if contains_sensitive_cli_flag(&lower) { + sanitized.push("[redacted sensitive context]".to_string()); + continue; + } if inside_private_key { if lower.contains("-----end") && lower.contains("private key") { inside_private_key = false; @@ -498,6 +502,30 @@ pub(crate) fn sanitize_prompt_context(value: &str) -> String { sanitized.join("\n") } +fn contains_sensitive_cli_flag(lower: &str) -> bool { + [ + "--password", + "--token", + "--secret", + "--api-key", + "--apikey", + "--authorization", + "--cookie", + ] + .into_iter() + .any(|flag| { + lower.split_whitespace().any(|token| { + let token = token.trim_matches(|character: char| { + matches!( + character, + ',' | ';' | ',' | ';' | '(' | ')' | '[' | ']' | '{' | '}' + ) + }); + token == flag || token.starts_with(&format!("{flag}=")) + }) + }) +} + // Prompt context is deliberately fail-closed: a line which looks like a // credential assignment is dropped in full before it can reach a model. An // error, however, is an operator-facing diagnostic. Dropping the whole line @@ -565,6 +593,10 @@ pub(crate) fn sanitize_error_context(value: &str) -> String { let mut inside_private_key = false; for line in value.lines() { let lower = line.to_ascii_lowercase(); + if contains_sensitive_cli_flag(&lower) { + sanitized.push("[redacted sensitive context]".to_string()); + continue; + } if inside_private_key { if lower.contains("-----end") && lower.contains("private key") { inside_private_key = false; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delivery.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delivery.rs index fe4c06443..640b57011 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delivery.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delivery.rs @@ -878,15 +878,15 @@ pub(crate) fn publish_game_creator_agent_delegate_result( return; } }; - let result_detail = result_detail + let raw_result_detail = result_detail .filter(|value| !value.trim().is_empty()) .or(child_task.terminal_detail.as_deref()) .or(child_task.error.as_deref()) .unwrap_or(child_task.current_action.as_str()); let result_detail = redact_agent_runtime_error( root, - result_detail, - static_delegate_result_detail_max_chars(result_detail, 600), + raw_result_detail, + static_delegate_result_detail_max_chars(raw_result_detail, 600), ); if uses_durable_delivery { let existing_delivery = match read_static_delegate_delivery_at(root, delegation_id) { @@ -1072,9 +1072,13 @@ pub(crate) fn publish_game_creator_agent_delegate_result( if receipt_exists { return; } + let receipt_result_detail = sanitize_agent_runtime_text( + &sanitize_prompt_context(raw_result_detail), + static_delegate_result_detail_max_chars(raw_result_detail, 600), + ); let receipt_task = format!( "收到委派子任务终态回执。子 Agent:{};状态:{};结果:{}。这是已完成委派的回执,不要重复委派同一任务;请整合结果并决定后续,需要原目标时调用 conversation.read。", - child_task.agent_id, terminal_status, result_detail, + child_task.agent_id, terminal_status, receipt_result_detail, ); let receipt_link = AgentRuntimeTaskLink { parent_agent_id: None, -- 2.52.0 From 94c5443a2667339a6f85d01f7bbad8828f57f80c Mon Sep 17 00:00:00 2001 From: suzmii Date: Wed, 26 Aug 2026 13:39:05 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8DAGC=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=97=B6=E6=B5=8B=E8=AF=95=E4=B8=8E=E6=9C=AC=E5=9C=B0=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将平台生成 5xx 提交统一归类为结果未知,保留对账账本 本地 loopback Provider 请求绕过 HTTP 代理,恢复 transport 错误分类 修正流式与画布 mock 服务读取完整请求体 放宽 macOS 系统临时目录路径测试前提并保留项目链接安全校验 --- .../src-tauri/src/agent/codex_app_server.rs | 4 +-- .../src/agent/generation/canvas_generation.rs | 11 ++++++-- .../src-tauri/src/repository_context.rs | 11 +++++++- .../src-tauri/src/tests/mod.rs | 12 ++++----- server-rs/crates/platform-llm/src/lib.rs | 26 +++++++++++++++++-- 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs index 2b996d0e5..c8158abee 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs @@ -3074,7 +3074,7 @@ mod tests { assert!(resolve_direct_codex_game_workspace(&project_root).is_err()); } - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "macos")))] #[test] fn direct_project_grant_root_comparison_remains_case_sensitive() { let temp = tempfile::tempdir().expect("temp dir"); @@ -3162,7 +3162,7 @@ mod tests { assert_ne!(first_identity, second_identity); } - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "macos")))] #[test] fn direct_project_pool_identity_hashes_non_utf8_canonical_path_bytes_losslessly() { use std::os::unix::ffi::OsStringExt; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs index 6acaca475..156daf3c0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs @@ -963,7 +963,7 @@ pub(crate) async fn submit_external_generation_request( } let request_body_json = serde_json::to_string(&request_body) .map_err(|error| format!("序列化平台图片生成请求失败:{error}"))?; - client + let response = client .post(format!( "{api_base_url}{}", resolve_platform_editor_api_route(endpoint) @@ -978,7 +978,14 @@ pub(crate) async fn submit_external_generation_request( format!( "{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 请求平台图片生成后未取得确定响应:{error}" ) - }) + })?; + if response.status().is_server_error() { + return Err(format!( + "{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 请求平台图片生成后收到 HTTP {},服务端是否已产生副作用未知", + response.status().as_u16() + )); + } + Ok(response) } async fn resume_prepared_external_generation_at( diff --git a/apps/ai-game-creator-shell/src-tauri/src/repository_context.rs b/apps/ai-game-creator-shell/src-tauri/src/repository_context.rs index ba92e21c2..ee4f552b4 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/repository_context.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/repository_context.rs @@ -459,10 +459,19 @@ fn validate_repository_root(root: &Path) -> Result { return Err("Repository root must not contain parent-directory components".to_string()); } + let canonical_temp_root = fs::canonicalize(std::env::temp_dir()).ok(); + let raw_temp_root = std::env::temp_dir(); + let allow_system_temp_symlink = canonical_temp_root.as_ref().is_some_and(|temp| { + fs::canonicalize(root).is_ok_and(|canonical| canonical.starts_with(temp)) + }) && root.starts_with(&raw_temp_root); for ancestor in root.ancestors() { let metadata = fs::symlink_metadata(ancestor) .map_err(|error| format!("Unable to inspect repository root ancestor: {error}"))?; - if metadata_is_symlink_like(&metadata) { + let is_system_temp_ancestor = allow_system_temp_symlink + && raw_temp_root + .ancestors() + .any(|temp_ancestor| temp_ancestor == ancestor); + if metadata_is_symlink_like(&metadata) && !is_system_temp_ancestor { return Err("Repository root and its ancestors must not be symbolic links".to_string()); } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs index 72adee362..0f9d84221 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs @@ -2446,10 +2446,10 @@ fn spawn_mock_llm_stream_server_with_capture( let base_url = format!("http://{}", listener.local_addr().expect("mock llm addr")); std::thread::spawn(move || { let (mut stream, _) = listener.accept().expect("mock stream llm accept"); - let mut request_buffer = [0_u8; 8192]; - let read_len = stream.read(&mut request_buffer).unwrap_or(0); if let Some(sender) = request_sender.as_ref() { - let _ = sender.send(String::from_utf8_lossy(&request_buffer[..read_len]).into_owned()); + let _ = sender.send(read_mock_http_request(&mut stream)); + } else { + let _ = read_mock_http_request(&mut stream); } let response = format!( "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream; charset=utf-8\r\nContent-Length: {}\r\nx-request-id: req_role_agent_stream\r\nConnection: close\r\n\r\n{}", @@ -3028,11 +3028,9 @@ fn spawn_mock_external_canvas_api_server_with_capture_and_generation_gate( let mut generation_poll_index = 0_u8; for _ in 0..expected_requests { let (mut stream, _) = listener.accept().expect("mock canvas api accept"); - let mut request_buffer = [0_u8; 8192]; - let read_len = stream.read(&mut request_buffer).unwrap_or(0); - let request = String::from_utf8_lossy(&request_buffer[..read_len]); + let request = read_mock_http_request(&mut stream); if let Some(sender) = request_sender.as_ref() { - let _ = sender.send(request.to_string()); + let _ = sender.send(request.clone()); } let normalized_request = request.to_ascii_lowercase(); let (status, content_type, body) = if request diff --git a/server-rs/crates/platform-llm/src/lib.rs b/server-rs/crates/platform-llm/src/lib.rs index 7a1158cd3..e1ca4e30e 100644 --- a/server-rs/crates/platform-llm/src/lib.rs +++ b/server-rs/crates/platform-llm/src/lib.rs @@ -2,6 +2,7 @@ use std::{ env, error::Error, fmt, fs, + net::IpAddr, path::PathBuf, str as std_str, sync::atomic::{AtomicU64, Ordering}, @@ -1496,7 +1497,13 @@ impl LlmError { impl LlmClient { pub fn new(config: LlmConfig) -> Result { - let http_client = Client::builder() + let mut builder = Client::builder(); + if llm_base_url_is_loopback(config.base_url()) { + // 本地 mock/sidecar 必须直连。否则开发机 HTTP_PROXY 可能把已断开的 + // loopback 连接改写成 HTTP 502,破坏 Provider transport 错误分类。 + builder = builder.no_proxy(); + } + let http_client = builder .use_rustls_tls() .http1_only() .build() @@ -1511,7 +1518,11 @@ impl LlmClient { } pub fn new_without_redirects(config: LlmConfig) -> Result { - let http_client = Client::builder() + let mut builder = Client::builder(); + if llm_base_url_is_loopback(config.base_url()) { + builder = builder.no_proxy(); + } + let http_client = builder .use_rustls_tls() .http1_only() .redirect(Policy::none()) @@ -2040,6 +2051,17 @@ impl LlmClient { } } +fn llm_base_url_is_loopback(base_url: &str) -> bool { + let Ok(url) = reqwest::Url::parse(base_url) else { + return false; + }; + match url.host_str() { + Some("localhost") => true, + Some(host) => host.parse::().is_ok_and(|ip| ip.is_loopback()), + None => false, + } +} + impl OpenAiCompatibleSseParser { fn new(api_kind: LlmApiKind) -> Self { Self { -- 2.52.0