diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs index 4d4616f77..ab8410646 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs @@ -10,18 +10,6 @@ pub(in crate::agent) fn agent_role_project_path_mutation_block( if autonomous_relaxed_run_at(root, agent_id, run_id).unwrap_or(false) { return None; } - if is_agent_planning_storage_path(path) || is_plan_fast_gdd_projection_path(path) { - return Some(AgentRuntimeToolObservation { - tool: tool.to_string(), - status: "blocked".to_string(), - summary: if is_agent_planning_storage_path(path) { - "`.agent/planning/**` 只能由立项策划 Runtime 专用存储层写入".to_string() - } else { - "`game/fast_gdd.md` 只能由立项策划 Runtime renderer 写入".to_string() - }, - detail: Some(format!("agentId={agent_id} · runId={run_id} · path={path}")), - }); - } match autonomous_owner_artifact_validation_available_for_run_at(root, agent_id, run_id) { Ok(true) => { let allowed = autonomous_manifest_owner_artifact_paths(agent_id); diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index b3cc58f28..9a4168e9d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -876,104 +876,6 @@ pub(crate) fn validated_local_project_directory_path( Ok(path.to_path_buf()) } -/// Open the approved Fast GDD Markdown in whatever application the OS has -/// registered for it. -/// -/// The GDD is the one product artifact the 立项策划 lane hands back, and it is -/// already on disk — `plan.submit_gdd` renders `game/fast_gdd.md` and the -/// approval receipt re-renders it with the approved header. This command only -/// hands that existing path to the shell; it never creates or rewrites it. -#[tauri::command] -pub(crate) fn open_local_project_plan_gdd_markdown( - app: tauri::AppHandle, - project_path: String, -) -> Result<(), String> { - let path = validated_local_project_plan_gdd_markdown_path(project_path.trim())?; - app.opener() - .open_path(path.to_string_lossy().into_owned(), None::<&str>) - .map_err(|error| format!("打开 Fast GDD 文件失败:{error}")) -} - -pub(crate) fn validated_local_project_plan_gdd_markdown_path( - project_path: &str, -) -> Result { - let root = validated_local_project_directory_path(project_path)?; - // `resolve_local_project_path` 是项目内路径的唯一安全入口:它做根校验、相对路径 - // 归一化,并逐段拒绝符号链接。这里的相对路径是常量,但仍然走它——GDD 的渲染侧 - // (`planning_storage`)用的也是同一个解析器,两边对「项目内的这个文件」必须是 - // 同一个判定,不能一边解析一边拼字符串。 - let path = resolve_local_project_path(&root, PLAN_FAST_GDD_PATH)?; - match fs::symlink_metadata(&path) { - Ok(metadata) if metadata.file_type().is_file() => Ok(path), - Ok(_) => Err("Fast GDD 产物不是普通文件".to_string()), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - Err("Fast GDD 产物尚未生成,请先完成立项策划审批".to_string()) - } - Err(error) => Err(format!("读取 Fast GDD 产物失败:{error}")), - } -} - -#[cfg(test)] -mod plan_gdd_markdown_path_tests { - use super::*; - - fn fixture() -> tempfile::TempDir { - let temporary = tempfile::tempdir().expect("create GDD path fixture"); - crate::project::init_local_game_project_at( - &temporary.path().join("project"), - "gdd-open", - "打开 GDD 产物", - ) - .expect("initialize GDD path fixture"); - temporary - } - - #[test] - fn resolves_the_rendered_markdown_under_the_project_root() { - let temporary = fixture(); - let root = temporary.path().join("project"); - fs::create_dir_all(root.join("game")).expect("create game directory"); - fs::write(root.join(PLAN_FAST_GDD_PATH), "# Fast GDD").expect("render markdown"); - - let resolved = - validated_local_project_plan_gdd_markdown_path(&root.to_string_lossy().into_owned()) - .expect("resolve rendered markdown"); - - assert_eq!(resolved, root.join(PLAN_FAST_GDD_PATH)); - } - - #[test] - fn refuses_to_open_a_markdown_that_has_not_been_rendered_yet() { - // 恢复态下 `plan.submit_gdd` 的 Markdown 渲染可能还没落盘。这时按钮必须给出 - // 明确原因,而不是把一个不存在的路径丢给 shell 由系统弹一个无从解释的错误。 - let temporary = fixture(); - let root = temporary.path().join("project"); - - let error = - validated_local_project_plan_gdd_markdown_path(&root.to_string_lossy().into_owned()) - .expect_err("missing markdown must fail closed"); - - assert!(error.contains("尚未生成"), "unexpected error: {error}"); - } - - #[test] - fn refuses_a_project_path_that_is_not_an_initialized_project() { - let temporary = tempfile::tempdir().expect("create bare fixture"); - let error = validated_local_project_plan_gdd_markdown_path( - &temporary.path().to_string_lossy().into_owned(), - ) - .expect_err("a directory without .agent is not a project root"); - assert!(!error.is_empty()); - } - - #[test] - fn refuses_a_relative_project_path() { - let error = validated_local_project_plan_gdd_markdown_path("relative/project") - .expect_err("relative project path must fail"); - assert!(error.contains("绝对路径"), "unexpected error: {error}"); - } -} - #[tauri::command] pub(crate) async fn get_local_game_manifest( project_path: String, diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index f3924a219..61071b6cc 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -2658,7 +2658,6 @@ fn main() { read_agc_plugin_panel, set_agc_plugin_project_path, open_local_project_directory, - open_local_project_plan_gdd_markdown, control_agent_run, generate_local_game_draft, chat_with_game_creator_agent, diff --git a/apps/ai-game-creator-shell/src-tauri/src/patchset.rs b/apps/ai-game-creator-shell/src-tauri/src/patchset.rs index ca3125e28..dbe032e3e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/patchset.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/patchset.rs @@ -7,9 +7,7 @@ use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; use unicode_normalization::UnicodeNormalization; -use crate::project::{ - is_plan_fast_gdd_projection_path, normalize_relative_path, validate_project_root, -}; +use crate::project::{normalize_relative_path, validate_project_root}; const PROJECT_PATCHSET_MAX_CHANGES: usize = 12; const PROJECT_PATCHSET_MAX_TOTAL_BODY_BYTES: usize = 256 * 1024; @@ -466,11 +464,6 @@ fn normalize_and_validate_patchset_inputs( let path = normalize_relative_path(change.path())?; reject_sensitive_patchset_path(&path)?; - if is_plan_fast_gdd_projection_path(&path) { - return Err( - "project.patchset 不得直接修改 Runtime-owned game/fast_gdd.md 投影".to_string(), - ); - } let change = match change { ParsedProjectPatchsetChange::Create { content, .. } => { validate_input_text(&content, &path)?; @@ -1372,11 +1365,6 @@ fn metadata_is_link_or_reparse(metadata: &Metadata) -> bool { } fn reject_sensitive_patchset_path(relative_path: &str) -> Result<(), String> { - if is_plan_fast_gdd_projection_path(relative_path) { - return Err( - "project.patchset 不得直接修改 Runtime-owned game/fast_gdd.md 投影".to_string(), - ); - } let components = relative_path .split('/') .map(str::to_ascii_lowercase) @@ -1706,8 +1694,6 @@ mod tests { for path in [ ".agent/runtime/state.json", - ".agent/planning/gdd.v1.json", - "game/fast_gdd.md", ".env.local", "config/private.pem", "data/runtime.sqlite", diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/checkpoint.rs b/apps/ai-game-creator-shell/src-tauri/src/project/checkpoint.rs index 380cc2836..70b12ab48 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/checkpoint.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/checkpoint.rs @@ -674,7 +674,6 @@ pub(crate) fn restore_local_project_checkpoint_at( pub(crate) fn should_skip_project_restore_path(relative_path: &str) -> bool { should_skip_project_snapshot_path(relative_path) - || is_plan_fast_gdd_projection_path(relative_path) || relative_path == ".agent/agent.db" || relative_path == PROJECT_PERMISSION_POLICY_PATH || relative_path == PROJECT_WRITE_LOCK_PATH diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs b/apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs index 6f067628b..9e1fc5cbd 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs @@ -30,7 +30,6 @@ pub(crate) fn list_local_project_files_at( if is_agent_runtime_private_control_path(&relative_path) || is_agent_checkpoint_control_path(&relative_path) || is_agent_workbench_control_path(&relative_path) - || is_agent_planning_storage_path(&relative_path) { continue; } @@ -127,56 +126,6 @@ pub(crate) fn reject_agent_runtime_private_control_path( Ok(()) } -/// `.agent/planning/**` is a Runtime-owned sidecar. It remains readable by -/// the narrow planning read tools, but generic project mutation helpers must -/// never be able to create, replace, patch, or delete it. Keeping this gate -/// separate from `reject_agent_runtime_private_control_path` is deliberate: -/// the planning Agent needs `file.read`/`file.list` observations while its -/// durable writer is still the only component allowed to mutate the sidecar. -pub(crate) fn is_agent_planning_storage_path(normalized_path: &str) -> bool { - let normalized_path = normalized_path.to_ascii_lowercase(); - normalized_path == ".agent/planning" - || normalized_path.starts_with(".agent/planning/") - || normalized_path == ".agent/planning-v2" - || normalized_path.starts_with(".agent/planning-v2/") -} - -pub(crate) fn is_agent_planning_managed_write_path(normalized_path: &str) -> bool { - is_agent_planning_storage_path(normalized_path) - || is_plan_fast_gdd_projection_path(normalized_path) -} - -pub(crate) fn reject_agent_planning_storage_write_path( - normalized_path: &str, -) -> Result<(), String> { - if is_agent_planning_managed_write_path(normalized_path) { - return Err( - "`.agent/planning/**`、`.agent/planning-v2/**` 与 `game/fast_gdd.md` 只能由立项策划 Runtime 专用存储层写入,通用文件写入被拒绝" - .to_string(), - ); - } - Ok(()) -} - -/// `game/fast_gdd.md` is the human-readable planning projection. It lives -/// outside `.agent/planning`, but it is still Runtime-owned and must not be -/// mutated by generic file tools. Keep this predicate write-only so planning -/// observations can continue to read the projection. -pub(crate) fn is_plan_fast_gdd_projection_path(normalized_path: &str) -> bool { - normalized_path.eq_ignore_ascii_case(PLAN_FAST_GDD_PATH) -} - -pub(crate) fn reject_plan_projection_write_path(normalized_path: &str) -> Result<(), String> { - reject_agent_planning_storage_write_path(normalized_path)?; - if is_plan_fast_gdd_projection_path(normalized_path) { - return Err( - "`game/fast_gdd.md` 只能由立项策划 Runtime renderer 写入,通用文件写入被拒绝" - .to_string(), - ); - } - Ok(()) -} - fn reject_agent_control_path_delete(normalized_path: &str) -> Result<(), String> { if matches!( normalized_path.split('/').next(), @@ -213,7 +162,6 @@ pub(crate) fn write_local_project_file_at( ) -> Result { let normalized_path = normalize_relative_path(relative_path)?; reject_agent_runtime_private_control_path(&normalized_path)?; - reject_plan_projection_write_path(&normalized_path)?; let path = resolve_local_project_path(root, &normalized_path)?; if path.exists() && !path.is_file() { return Err("只能写入文件".to_string()); @@ -233,7 +181,6 @@ pub(crate) fn delete_local_project_file_at( ) -> Result { let normalized_path = normalize_relative_path(relative_path)?; reject_agent_runtime_private_control_path(&normalized_path)?; - reject_plan_projection_write_path(&normalized_path)?; reject_agent_control_path_delete(&normalized_path)?; let path = resolve_local_project_path(root, &normalized_path)?; if !path.exists() {