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 55c1cf8e0..e515babc1 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 是用户选择的项目目录(工作区根),源码、素材、音效和其它资源按项目现有结构放置;先按需读取当前 cwd 下适用的 `AGENTS.md`、README 或项目说明,把它们当作项目规范参考。原生文件工具、patch 和命令参数使用 cwd 相对路径,例如 `index.html`、`style.css`、`game.js`、`assets/hero.png`;如果 Codex 原生文件修改不可用,可以按需用客户端 `agc_write_file` 把文本写入项目相对路径。`../`、绝对路径、`.agent/`、`.git/`、密钥文件和 Runtime 控制面属于客户端边界,不能请求扩权或直接改写。DirectProject 提供 Codex 原生文件、搜索、命令、图片查看、Skill,以及经审核的 `agc_tools` MCP。你可以按需选择这些能力:`agc_write_file` 写入代码、配置、资源依赖清单或说明文件;`agc_generate_image` 生成普通图片、角色图、视觉规范图(icon-spec)、UI 设计图或发布宣传图;`agc_edit_image` 修改已登记图片;`taonier_prepare_game_art` 准备完整游戏美术包及可用的 canonical 切片;`agc_list_registered_assets`、`agc_list_project_files`、`agc_list_account_assets`、`agc_import_account_assets` 用于发现和接入资源依赖;`agc_create_or_derive_resource` 用于视频、角色动画、音效或背景音乐;`agc_browser_playtest` 用于需要时的本地试玩观察;`agc_read_skill_resource` 用于按需读取审核 Skill。切图、资源依赖、规范图和试玩都只是可选工具提示,不要求调用、固定顺序或特定产物,AGC 不会据此替你拆任务、编排 DAG、做强验收或阻止继续执行;不要等待 Supervisor、harness 或宿主规划器。不要读取或输出凭据、Token、Cookie、auth.json、.env 或宿主私密路径;项目锁、付费提交、幂等键、下载校验和客户端投影由客户端处理。游戏文件真实变化后客户端可登记资源和版本,Codex 不直接保存或伪造项目版本。"; +const DIRECT_AGC_ENGINEERING_GUIDANCE: &str = "AGC 工程合同(仅说明项目边界,不是流程门槛):当前 Codex cwd 是用户选择的项目目录(工作区根),源码、素材、音效和其它资源按项目现有结构放置;先按需读取当前 cwd 下适用的 `AGENTS.md`、README 或项目说明,把它们当作项目规范参考。原生文件工具、patch 和命令参数使用 cwd 相对路径,例如 `index.html`、`style.css`、`game.js`、`assets/hero.png`;如果 Codex 原生文件修改不可用,可以按需用客户端 `agc_write_file` 把文本写入项目相对路径。调用 `agc_write_file` 时,content 必须是目标文件的完整原始 UTF-8 正文;不得把 command.exec 的 Exit code、Wall time、Output 包装、终端日志或解释文字一起复制进 content,命令结果只能用于判断,不能当作文件正文。`../`、绝对路径、`.agent/`、`.git/`、密钥文件和 Runtime 控制面属于客户端边界,不能请求扩权或直接改写。DirectProject 提供 Codex 原生文件、搜索、命令、图片查看、Skill,以及经审核的 `agc_tools` MCP。你可以按需选择这些能力:`agc_write_file` 写入代码、配置、资源依赖清单或说明文件;`agc_generate_image` 生成普通图片、角色图、视觉规范图(icon-spec)、UI 设计图或发布宣传图;`agc_edit_image` 修改已登记图片;`taonier_prepare_game_art` 准备完整游戏美术包及可用的 canonical 切片;`agc_list_registered_assets`、`agc_list_project_files`、`agc_list_account_assets`、`agc_import_account_assets` 用于发现和接入资源依赖;`agc_create_or_derive_resource` 用于视频、角色动画、音效或背景音乐;`agc_browser_playtest` 用于需要时的本地试玩观察;`agc_read_skill_resource` 用于按需读取审核 Skill。切图、资源依赖、规范图和试玩都只是可选工具提示,不要求调用、固定顺序或特定产物,AGC 不会据此替你拆任务、编排 DAG、做强验收或阻止继续执行;不要等待 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"; @@ -4407,6 +4407,8 @@ mod tests { assert!(prompt.contains("AGC 工程合同(仅说明项目边界,不是流程门槛)")); assert!(prompt.contains("先按需读取当前 cwd 下适用的 `AGENTS.md`")); assert!(prompt.contains("agc_write_file")); + assert!(prompt.contains("content 必须是目标文件的完整原始 UTF-8 正文")); + assert!(prompt.contains("不得把 command.exec 的 Exit code、Wall time、Output 包装")); assert!(prompt.contains("切图、资源依赖、规范图和试玩都只是可选工具提示")); assert!(prompt.contains("不要求调用、固定顺序或特定产物")); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs index 6fa8f196b..7330dd43b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs @@ -29,6 +29,23 @@ const DIRECT_TOOL_BRIDGE_MAX_RESOURCE_CALLS_PER_TURN: usize = 4; const DIRECT_TOOL_BRIDGE_MAX_ACCOUNT_ASSET_ID_CHARS: usize = 512; const DIRECT_TOOL_BRIDGE_MAX_LOCAL_ASSET_PATH_CHARS: usize = 512; +pub(crate) fn reject_command_output_wrapper(content: &str) -> Result<(), String> { + let mut lines = content.trim_start_matches('\u{feff}').lines(); + let exit_line = lines.next().map(str::trim).unwrap_or_default(); + let wall_time_line = lines.next().map(str::trim).unwrap_or_default(); + let output_line = lines.next().map(str::trim).unwrap_or_default(); + if exit_line.starts_with("Exit code:") + && wall_time_line.starts_with("Wall time:") + && output_line.eq_ignore_ascii_case("Output:") + { + return Err( + "工具参数 content 不能包含 command.exec 的 Exit code/Wall time/Output 包装;请只传原始 UTF-8 文件正文" + .to_string(), + ); + } + Ok(()) +} + struct DirectToolBridgeState { root: PathBuf, turn_authorization: StdMutex, @@ -1420,6 +1437,7 @@ fn bridge_write_file(root: &Path, arguments: &Value) -> Value { if content.chars().any(|character| character == '\0') { return Err("工具参数 content 不能包含 NUL".to_string()); } + reject_command_output_wrapper(content)?; let _lock = acquire_project_write_lock(root, "direct-codex.file.write")?; let written = write_local_project_file_at(root, &path, content)?; let revision = advance_agent_runtime_project_revision_locked(root)?; @@ -2475,6 +2493,18 @@ mod tests { fs::read_to_string(temporary.path().join("game/index.html")).expect("read written"), "" ); + let wrapped = bridge_write_file( + temporary.path(), + &json!({ + "path": "game/index.html", + "content": "Exit code: 0\nWall time: 0.1 seconds\nOutput:\n" + }), + ); + assert_eq!(wrapped.get("isError").and_then(Value::as_bool), Some(true)); + assert_eq!( + fs::read_to_string(temporary.path().join("game/index.html")).expect("read unchanged"), + "" + ); assert!( bridge_write_file( temporary.path(), diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs index 9844ea945..7c896bd3a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs @@ -77,6 +77,7 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool) -> Value { }, "content": { "type": "string", + "description": "目标文件的完整原始 UTF-8 正文;不要包含 command.exec 的 Exit code、Wall time、Output 包装、终端日志或解释文字", "maxLength": DIRECT_TOOLS_MCP_MAX_WRITE_CONTENT_BYTES } }, @@ -435,6 +436,7 @@ fn validate_write_file_arguments(arguments: &Value) -> Result<(), String> { if content.chars().any(|character| character == '\0') { return Err("工具参数 content 不能包含 NUL".to_string()); } + reject_command_output_wrapper(content)?; // Keep path normalization in the client bridge as the final authority; // this early check only gives Codex a quick, deterministic argument error. normalize_relative_path(&path).map(|_| ()) @@ -1279,6 +1281,16 @@ mod tests { "content": "" })) .is_ok()); + assert!(validate_write_file_arguments(&json!({ + "path": "game/index.html", + "content": "Exit code: 0\nWall time: 0.1 seconds\nOutput:\n" + })) + .is_err()); + assert!(validate_write_file_arguments(&json!({ + "path": "notes.txt", + "content": "说明:Exit code 只是一段普通文本" + })) + .is_ok()); assert!(validate_write_file_arguments(&json!({ "path": ".agent/manifest.json", "content": "{}"