阻止命令输出污染 AGC 文件写入
明确 agc_write_file 只接受原始 UTF-8 文件正文 拒绝 Exit code、Wall time、Output 包装并补充回归测试
This commit is contained in:
@@ -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("不要求调用、固定顺序或特定产物"));
|
||||
}
|
||||
|
||||
@@ -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<DirectToolBridgeTurnAuthorization>,
|
||||
@@ -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"),
|
||||
"<!doctype html><button>写入成功</button>"
|
||||
);
|
||||
let wrapped = bridge_write_file(
|
||||
temporary.path(),
|
||||
&json!({
|
||||
"path": "game/index.html",
|
||||
"content": "Exit code: 0\nWall time: 0.1 seconds\nOutput:\n<!doctype html><button>错误包装</button>"
|
||||
}),
|
||||
);
|
||||
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"),
|
||||
"<!doctype html><button>写入成功</button>"
|
||||
);
|
||||
assert!(
|
||||
bridge_write_file(
|
||||
temporary.path(),
|
||||
|
||||
@@ -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": "<html></html>"
|
||||
}))
|
||||
.is_ok());
|
||||
assert!(validate_write_file_arguments(&json!({
|
||||
"path": "game/index.html",
|
||||
"content": "Exit code: 0\nWall time: 0.1 seconds\nOutput:\n<html></html>"
|
||||
}))
|
||||
.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": "{}"
|
||||
|
||||
Reference in New Issue
Block a user