删除策划专用文件与GDD命令

移除旧 Fast GDD 打开命令及 Tauri 注册

删除规划目录和 GDD 投影的专用文件保护分支

保留通用 Agent 控制面与 DirectCodex 文件安全边界
This commit is contained in:
2026-09-15 07:27:30 +00:00
parent bcbf398b65
commit 12092ceb50
6 changed files with 1 additions and 180 deletions
@@ -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);
@@ -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<PathBuf, String> {
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,
@@ -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,
@@ -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",
@@ -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
@@ -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<LocalProjectFileMutationResult, String> {
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<LocalProjectFileMutationResult, String> {
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() {