放开策划 Agent 文件工具的绝对路径与工作区外路径
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m33s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m56s
Project CI / Backend tests (pull_request) Successful in 3m58s
Project CI / Frontend tests (pull_request) Successful in 2m11s
Project CI / Native shell tests (pull_request) Successful in 6m3s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m53s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m2s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m42s
Project CI / Repository checks (pull_request) Successful in 2m15s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m33s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m56s
Project CI / Backend tests (pull_request) Successful in 3m58s
Project CI / Frontend tests (pull_request) Successful in 2m11s
Project CI / Native shell tests (pull_request) Successful in 6m3s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m53s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m2s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m42s
Project CI / Repository checks (pull_request) Successful in 2m15s
- design_tools 的 list_dir、read_file、write_file、patch_file、delete_path、search_text 接受绝对路径 - 相对路径仍以 design_artifacts 为基准,允许 .. 离开工作区 - 工作区内写入继续走私有文件写入,工作区外按普通文件创建父目录并写入 - 用户工作区浏览和附件导入仍限定在 design_artifacts - 删除仍不跟随符号链接,也不删除工作区根目录 - 既有测试改为期望 ../secret.md 写入成功 - 同步策划 Agent 方案、decision-log 与 pitfalls - 提示词未改
This commit is contained in:
@@ -3219,14 +3219,13 @@ mod tests {
|
||||
.iter()
|
||||
.any(|message| message.text.contains("读取资源失败")
|
||||
|| message.text.contains("未知资源")));
|
||||
assert!(view
|
||||
.messages
|
||||
.iter()
|
||||
.any(|message| message.text.contains("失败") && message.text.contains("路径")));
|
||||
assert_eq!(
|
||||
fs::read_to_string(root.join("secret.md")).expect("outside write"),
|
||||
"no"
|
||||
);
|
||||
assert!(root
|
||||
.join("design_artifacts/project/00_concept/design.md")
|
||||
.is_file());
|
||||
assert!(!root.join("secret.md").exists());
|
||||
|
||||
let request = request_id(&view);
|
||||
let next = decide_design_phase_at(&root, &resources, "t-retry", &request, true, |_| {})
|
||||
|
||||
@@ -204,7 +204,7 @@ pub(crate) fn execute_design_file_tool(
|
||||
match name {
|
||||
"list_dir" => {
|
||||
let relative = optional_tool_path(args)?;
|
||||
let (display, path) = resolve_design_workspace_path(root, &relative)?;
|
||||
let (display, path) = resolve_design_tool_path(root, &relative)?;
|
||||
if !path.is_dir() {
|
||||
return Ok(Value::String("不是目录".to_string()));
|
||||
}
|
||||
@@ -246,7 +246,7 @@ pub(crate) fn execute_design_file_tool(
|
||||
}
|
||||
"read_file" => {
|
||||
let relative = required_tool_path(args)?;
|
||||
let (display, path) = resolve_design_workspace_path(root, &relative)?;
|
||||
let (display, path) = resolve_design_tool_path(root, &relative)?;
|
||||
if !path.is_file() {
|
||||
return Err(format!("不是文件:{display}"));
|
||||
}
|
||||
@@ -260,8 +260,8 @@ pub(crate) fn execute_design_file_tool(
|
||||
.get("content")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("content 必须是字符串")?;
|
||||
let (display, path) = resolve_design_workspace_path(root, &relative)?;
|
||||
crate::write_game_creator_private_file(&path, content.as_bytes(), "策划工作区文件")?;
|
||||
let (display, path) = resolve_design_tool_path(root, &relative)?;
|
||||
write_design_tool_bytes(root, &path, content.as_bytes())?;
|
||||
Ok(Value::String(format!("已写入 {display}")))
|
||||
}
|
||||
"patch_file" => {
|
||||
@@ -302,7 +302,7 @@ pub(crate) fn execute_design_file_tool(
|
||||
}
|
||||
vec![(old.to_string(), new.to_string())]
|
||||
};
|
||||
let (display, path) = resolve_design_workspace_path(root, &relative)?;
|
||||
let (display, path) = resolve_design_tool_path(root, &relative)?;
|
||||
if !path.is_file() {
|
||||
return Err(format!("文件不存在:{display}"));
|
||||
}
|
||||
@@ -395,7 +395,7 @@ pub(crate) fn execute_design_file_tool(
|
||||
if updated == content {
|
||||
return Err(format!("没有产生修改:{display}"));
|
||||
}
|
||||
crate::write_game_creator_private_file(&path, updated.as_bytes(), "策划工作区文件")?;
|
||||
write_design_tool_bytes(root, &path, updated.as_bytes())?;
|
||||
Ok(Value::String(format!(
|
||||
"已局部修改 {display}({} 处)",
|
||||
normalized.len()
|
||||
@@ -403,7 +403,7 @@ pub(crate) fn execute_design_file_tool(
|
||||
}
|
||||
"delete_path" => {
|
||||
let relative = required_tool_path(args)?;
|
||||
let (display, path) = resolve_design_workspace_path(root, &relative)?;
|
||||
let (display, path) = resolve_design_tool_path(root, &relative)?;
|
||||
if display == "." {
|
||||
return Err("不能删除工作区根目录".to_string());
|
||||
}
|
||||
@@ -426,7 +426,7 @@ pub(crate) fn execute_design_file_tool(
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("缺少 query")?;
|
||||
let relative = optional_tool_path(args)?;
|
||||
let (_, path) = resolve_design_workspace_path(root, &relative)?;
|
||||
let (_, path) = resolve_design_tool_path(root, &relative)?;
|
||||
let mut hits = Vec::new();
|
||||
search_design_text(root, &path, query, &mut hits)?;
|
||||
Ok(Value::String(if hits.is_empty() {
|
||||
@@ -768,6 +768,63 @@ fn resolve_design_workspace_path(root: &Path, relative: &str) -> Result<(String,
|
||||
Ok((normalized, path))
|
||||
}
|
||||
|
||||
/// 文件工具使用的路径。相对路径以策划工作区为基准,允许 `..` 离开工作区,也接受绝对路径。
|
||||
/// 用户浏览和附件导入仍走 `resolve_design_workspace_path`。
|
||||
fn resolve_design_tool_path(root: &Path, raw: &str) -> Result<(String, PathBuf), String> {
|
||||
let relative = raw.trim().replace('\\', "/");
|
||||
if relative.is_empty() || relative == "." {
|
||||
return Ok((".".to_string(), ensure_design_workspace(root)?));
|
||||
}
|
||||
if Path::new(&relative).is_absolute() {
|
||||
let path = PathBuf::from(&relative);
|
||||
return Ok((design_tool_location(root, &path), path));
|
||||
}
|
||||
if !relative.split('/').any(|part| part == "..") {
|
||||
return resolve_design_workspace_path(root, &relative);
|
||||
}
|
||||
let mut path = ensure_design_workspace(root)?;
|
||||
for part in relative.split('/') {
|
||||
match part {
|
||||
"" | "." => {}
|
||||
".." => {
|
||||
path.pop();
|
||||
}
|
||||
other => path.push(other),
|
||||
}
|
||||
}
|
||||
Ok((design_tool_location(root, &path), path))
|
||||
}
|
||||
|
||||
fn design_tool_location(root: &Path, path: &Path) -> String {
|
||||
let Ok(workspace) = resolve_local_project_path(root, DESIGN_WORKSPACE_ROOT) else {
|
||||
return path.to_string_lossy().replace('\\', "/");
|
||||
};
|
||||
match path.strip_prefix(&workspace) {
|
||||
Ok(relative) if relative.as_os_str().is_empty() => ".".to_string(),
|
||||
Ok(relative) => relative.to_string_lossy().replace('\\', "/"),
|
||||
Err(_) => path.to_string_lossy().replace('\\', "/"),
|
||||
}
|
||||
}
|
||||
|
||||
fn design_tool_path_inside_workspace(root: &Path, path: &Path) -> bool {
|
||||
resolve_local_project_path(root, DESIGN_WORKSPACE_ROOT)
|
||||
.ok()
|
||||
.is_some_and(|workspace| path.starts_with(&workspace))
|
||||
}
|
||||
|
||||
fn write_design_tool_bytes(root: &Path, path: &Path, bytes: &[u8]) -> Result<(), String> {
|
||||
if design_tool_path_inside_workspace(root, path) {
|
||||
return crate::write_game_creator_private_file(path, bytes, "策划工作区文件");
|
||||
}
|
||||
if let Some(parent) = path
|
||||
.parent()
|
||||
.filter(|parent| !parent.as_os_str().is_empty())
|
||||
{
|
||||
fs::create_dir_all(parent).map_err(|error| format!("创建目录失败:{error}"))?;
|
||||
}
|
||||
fs::write(path, bytes).map_err(|error| format!("写入失败:{error}"))
|
||||
}
|
||||
|
||||
fn workspace_display_path(parent: &str, name: &str) -> String {
|
||||
if parent == "." || parent.is_empty() {
|
||||
name.to_string()
|
||||
@@ -827,7 +884,7 @@ fn search_design_text(
|
||||
let Ok(text) = fs::read_to_string(path) else {
|
||||
return Ok(());
|
||||
};
|
||||
let display = design_workspace_relative(root, path)?;
|
||||
let display = design_tool_location(root, path);
|
||||
for (index, line) in text.lines().enumerate() {
|
||||
if line.contains(query) {
|
||||
hits.push(format!("{display}:{}: {line}", index + 1));
|
||||
@@ -855,17 +912,6 @@ fn search_design_text(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn design_workspace_relative(root: &Path, path: &Path) -> Result<String, String> {
|
||||
let workspace = resolve_local_project_path(root, DESIGN_WORKSPACE_ROOT)?;
|
||||
let relative = path
|
||||
.strip_prefix(&workspace)
|
||||
.map_err(|_| "路径超出工作目录".to_string())?;
|
||||
if relative.as_os_str().is_empty() {
|
||||
return Ok(".".to_string());
|
||||
}
|
||||
Ok(relative.to_string_lossy().replace('\\', "/"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -887,7 +933,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_tools_stay_inside_workspace() {
|
||||
fn file_tools_edit_workspace_and_outside_files() {
|
||||
let temp = test_root();
|
||||
let root = temp.path();
|
||||
execute_design_file_tool(
|
||||
@@ -907,13 +953,17 @@ mod tests {
|
||||
.to_string();
|
||||
assert!(listing.contains("[目录] notes"));
|
||||
assert!(!listing.contains(".agent"));
|
||||
let escaped = execute_design_file_tool(
|
||||
let outside = execute_design_file_tool(
|
||||
root,
|
||||
"write_file",
|
||||
&json!({"path":"../secret.md","content":"no"}),
|
||||
)
|
||||
.expect_err("escape");
|
||||
assert!(escaped.contains("路径"));
|
||||
.expect("write outside workspace");
|
||||
assert!(outside.as_str().unwrap().contains("已写入"));
|
||||
assert_eq!(
|
||||
fs::read_to_string(root.join("secret.md")).expect("read outside"),
|
||||
"no"
|
||||
);
|
||||
let mismatch = execute_design_file_tool(
|
||||
root,
|
||||
"patch_file",
|
||||
|
||||
Reference in New Issue
Block a user