删除策划专用文件与GDD命令
移除旧 Fast GDD 打开命令及 Tauri 注册 删除规划目录和 GDD 投影的专用文件保护分支 保留通用 Agent 控制面与 DirectCodex 文件安全边界
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user