修复运行续作与工具路径交接
无活动 Runtime 时将自然语言继续转换为新执行并保留显式恢复语义 成功响应落账前按工具契约规范化项目内绝对路径 拒绝重复键隐藏路径、动态 MCP 和项目外路径的交接放宽 补齐续作分流、路径重放和失败关闭回归测试 同步更新 Runtime 技术方案与共享踩坑记录
This commit is contained in:
@@ -239,6 +239,20 @@ fn resume_is_an_explicit_control_command() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn natural_language_resume_without_active_runtime_starts_a_new_run() {
|
||||
assert_eq!(
|
||||
normalize_interaction_action_without_active_runtime(AgentInteractionAction::Resume),
|
||||
AgentInteractionAction::Execute
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_interaction_action_without_active_runtime(AgentInteractionAction::Reply(
|
||||
"记得之前的工作".to_string()
|
||||
)),
|
||||
AgentInteractionAction::Reply("记得之前的工作".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parent_runtime_matching_is_scoped_to_requested_profile() {
|
||||
let mut standard = runtime("running", "planning", 0);
|
||||
|
||||
@@ -201,6 +201,7 @@ pub(super) fn handle_swarm_user_turn<W: Write>(
|
||||
} else {
|
||||
AgentInteractionAction::Execute
|
||||
};
|
||||
let action = normalize_interaction_action_without_active_runtime(action);
|
||||
writeln!(output, "[意图] {}", action.label())
|
||||
.map_err(|error| format!("写入终端失败:{error}"))?;
|
||||
match action {
|
||||
@@ -232,6 +233,15 @@ pub(super) fn handle_swarm_user_turn<W: Write>(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn normalize_interaction_action_without_active_runtime(
|
||||
action: AgentInteractionAction,
|
||||
) -> AgentInteractionAction {
|
||||
match action {
|
||||
AgentInteractionAction::Resume => AgentInteractionAction::Execute,
|
||||
action => action,
|
||||
}
|
||||
}
|
||||
|
||||
fn decide_interaction_action<W: Write>(
|
||||
root: &Path,
|
||||
parent_agent_id: &str,
|
||||
|
||||
@@ -250,6 +250,15 @@ fn validate_source_or_narrative_content(label: &str, value: &str) -> Result<(),
|
||||
|
||||
fn validate_tool_plan_arguments(root: &Path, label: &str, value: &str) -> Result<(), String> {
|
||||
validate_secret_tokens_and_controls(label, value)?;
|
||||
if crate::agent_native_tools::validate_agent_runtime_protocol_json(
|
||||
value,
|
||||
"校验 tool-plan arguments JSON 失败",
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
validate_json_like_sensitive_keys(label, value)?;
|
||||
validate_json_like_absolute_path_inputs(label, value)?;
|
||||
}
|
||||
match serde_json::from_str::<serde_json::Value>(value) {
|
||||
Ok(json) => {
|
||||
validate_json_sensitive_keys(label, &json)?;
|
||||
|
||||
@@ -13,8 +13,9 @@ use super::model::{
|
||||
AgentRuntimeToolPlanHandoffEntry, AgentRuntimeToolPlanHandoffLedger,
|
||||
AgentRuntimeToolPlanHandoffLookup, AgentRuntimeToolPlanHandoffResponse,
|
||||
AgentRuntimeToolPlanHandoffToolCall, AgentRuntimeToolPlanHandoffUsage,
|
||||
TOOL_PLAN_HANDOFF_MAX_ENTRIES, TOOL_PLAN_HANDOFF_REQUEST_RESERVE_BYTES,
|
||||
TOOL_PLAN_HANDOFF_SCHEMA_VERSION, TOOL_PLAN_HANDOFF_SIDECAR_MAX_BYTES,
|
||||
TOOL_PLAN_HANDOFF_ARGUMENTS_MAX_BYTES, TOOL_PLAN_HANDOFF_MAX_ENTRIES,
|
||||
TOOL_PLAN_HANDOFF_REQUEST_RESERVE_BYTES, TOOL_PLAN_HANDOFF_SCHEMA_VERSION,
|
||||
TOOL_PLAN_HANDOFF_SIDECAR_MAX_BYTES,
|
||||
};
|
||||
use super::storage_common::{response_fingerprint, validate_path_identity, write_ledger_at};
|
||||
#[cfg(unix)]
|
||||
@@ -241,6 +242,11 @@ pub(super) fn response_for_persistence(
|
||||
response: &LlmRunResponse,
|
||||
) -> Result<AgentRuntimeToolPlanHandoffResponse, String> {
|
||||
let thinking = normalize_thinking_for_persistence(&response.text);
|
||||
let tool_calls = response
|
||||
.tool_calls
|
||||
.iter()
|
||||
.map(|call| normalize_tool_call_for_persistence(root, call))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let persisted = AgentRuntimeToolPlanHandoffResponse {
|
||||
provider: response.provider,
|
||||
model: response.model.clone(),
|
||||
@@ -256,12 +262,181 @@ pub(super) fn response_for_persistence(
|
||||
.usage
|
||||
.as_ref()
|
||||
.map(AgentRuntimeToolPlanHandoffUsage::from),
|
||||
tool_calls: response
|
||||
.tool_calls
|
||||
.iter()
|
||||
.map(AgentRuntimeToolPlanHandoffToolCall::from)
|
||||
.collect(),
|
||||
tool_calls,
|
||||
};
|
||||
validate_response(root, &persisted)?;
|
||||
Ok(persisted)
|
||||
}
|
||||
|
||||
fn normalize_tool_call_for_persistence(
|
||||
root: &Path,
|
||||
call: &platform_llm::LlmToolCall,
|
||||
) -> Result<AgentRuntimeToolPlanHandoffToolCall, String> {
|
||||
let arguments = normalize_tool_call_arguments(root, &call.name, &call.arguments)?;
|
||||
Ok(AgentRuntimeToolPlanHandoffToolCall {
|
||||
id: call.id.clone(),
|
||||
name: call.name.clone(),
|
||||
arguments,
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_tool_call_arguments(
|
||||
root: &Path,
|
||||
tool_name: &str,
|
||||
arguments: &str,
|
||||
) -> Result<String, String> {
|
||||
if arguments.len() > TOOL_PLAN_HANDOFF_ARGUMENTS_MAX_BYTES
|
||||
|| !should_normalize_tool_call_paths(tool_name)
|
||||
|| crate::agent_native_tools::validate_agent_runtime_protocol_json(
|
||||
arguments,
|
||||
"校验待规范化 tool-plan arguments 失败",
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return Ok(arguments.to_string());
|
||||
}
|
||||
let Ok(mut value) = serde_json::from_str::<serde_json::Value>(arguments) else {
|
||||
return Ok(arguments.to_string());
|
||||
};
|
||||
let changed = if tool_name == crate::agent::AGENT_RUNTIME_TOOL_PLAN_FUNCTION_NAME {
|
||||
normalize_legacy_tool_plan_paths(root, &mut value)
|
||||
} else if let Some(runtime_tool) = runtime_tool_for_native_handoff_function(tool_name) {
|
||||
value
|
||||
.get_mut("input")
|
||||
.is_some_and(|input| normalize_runtime_tool_input_paths(root, runtime_tool, input))
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if !changed {
|
||||
return Ok(arguments.to_string());
|
||||
}
|
||||
serde_json::to_string(&value)
|
||||
.map_err(|error| format!("序列化 tool-plan 项目相对路径失败:{error}"))
|
||||
}
|
||||
|
||||
fn should_normalize_tool_call_paths(tool_name: &str) -> bool {
|
||||
tool_name == crate::agent::AGENT_RUNTIME_TOOL_PLAN_FUNCTION_NAME
|
||||
|| runtime_tool_for_native_handoff_function(tool_name).is_some()
|
||||
}
|
||||
|
||||
fn runtime_tool_for_native_handoff_function(tool_name: &str) -> Option<&'static str> {
|
||||
[
|
||||
"project.search",
|
||||
"file.list",
|
||||
"file.read",
|
||||
"file.write",
|
||||
"file.patch",
|
||||
"file.delete",
|
||||
"project.patchset",
|
||||
"project.git_commit",
|
||||
"command.exec",
|
||||
"command.start",
|
||||
"image.inspect",
|
||||
"canvas.asset_generate",
|
||||
]
|
||||
.into_iter()
|
||||
.find(|tool| {
|
||||
crate::agent_native_tools::native_runtime_function_name(tool).as_deref() == Some(tool_name)
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_legacy_tool_plan_paths(root: &Path, value: &mut serde_json::Value) -> bool {
|
||||
let Some(actions) = value
|
||||
.get_mut("actions")
|
||||
.and_then(serde_json::Value::as_array_mut)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
actions.iter_mut().fold(false, |changed, action| {
|
||||
let Some(tool) = action
|
||||
.get("tool")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::to_string)
|
||||
else {
|
||||
return changed;
|
||||
};
|
||||
let normalized = action
|
||||
.get_mut("input")
|
||||
.is_some_and(|input| normalize_runtime_tool_input_paths(root, &tool, input));
|
||||
normalized || changed
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_runtime_tool_input_paths(
|
||||
root: &Path,
|
||||
tool: &str,
|
||||
value: &mut serde_json::Value,
|
||||
) -> bool {
|
||||
match tool {
|
||||
"project.search" | "file.list" => normalize_string_field(root, value, "path", true),
|
||||
"file.read" | "file.write" | "file.patch" | "file.delete" => {
|
||||
normalize_string_field(root, value, "path", false)
|
||||
}
|
||||
"project.patchset" => value
|
||||
.get_mut("changes")
|
||||
.and_then(serde_json::Value::as_array_mut)
|
||||
.is_some_and(|changes| {
|
||||
changes.iter_mut().fold(false, |changed, change| {
|
||||
normalize_string_field(root, change, "path", false) || changed
|
||||
})
|
||||
}),
|
||||
"project.git_commit" | "image.inspect" => {
|
||||
normalize_string_array_field(root, value, "paths")
|
||||
}
|
||||
"command.exec" | "command.start" => normalize_string_field(root, value, "cwd", true),
|
||||
"canvas.asset_generate" => normalize_string_field(root, value, "outputPath", false),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_string_field(
|
||||
root: &Path,
|
||||
object: &mut serde_json::Value,
|
||||
key: &str,
|
||||
allow_project_root: bool,
|
||||
) -> bool {
|
||||
let Some(field) = object.get_mut(key) else {
|
||||
return false;
|
||||
};
|
||||
let Some(relative) = field
|
||||
.as_str()
|
||||
.and_then(|value| normalize_project_absolute_path(root, value, allow_project_root))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
*field = serde_json::Value::String(relative);
|
||||
true
|
||||
}
|
||||
|
||||
fn normalize_string_array_field(root: &Path, object: &mut serde_json::Value, key: &str) -> bool {
|
||||
object
|
||||
.get_mut(key)
|
||||
.and_then(serde_json::Value::as_array_mut)
|
||||
.is_some_and(|values| {
|
||||
values.iter_mut().fold(false, |changed, value| {
|
||||
let normalized = value
|
||||
.as_str()
|
||||
.and_then(|value| normalize_project_absolute_path(root, value, false))
|
||||
.is_some_and(|relative| {
|
||||
*value = serde_json::Value::String(relative);
|
||||
true
|
||||
});
|
||||
normalized || changed
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_project_absolute_path(
|
||||
root: &Path,
|
||||
value: &str,
|
||||
allow_project_root: bool,
|
||||
) -> Option<String> {
|
||||
let candidate = Path::new(value);
|
||||
if !candidate.is_absolute() {
|
||||
return None;
|
||||
}
|
||||
if candidate == root {
|
||||
return allow_project_root.then(|| ".".to_string());
|
||||
}
|
||||
crate::project::relative_project_path(root, candidate).ok()
|
||||
}
|
||||
|
||||
@@ -573,29 +573,199 @@ fn tool_plan_handoff_rejects_dangerous_content_and_invalid_calls_without_writing
|
||||
!tool_plan_handoff_path(project.path(), &identity.agent_id, &identity.run_id,).exists()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_plan_handoff_normalizes_project_absolute_paths_before_round_trip() {
|
||||
let project = tempdir().expect("tool-plan handoff project");
|
||||
let identity = identity("loop-0-repair-0");
|
||||
let project_path_argument = format!(r#"{{"path":"{}"}}"#, project.path().display());
|
||||
write_at(
|
||||
let game_path = project.path().join("game/index.html");
|
||||
let asset_path = project.path().join("assets/ui.png");
|
||||
let root_text = project.path().to_string_lossy().into_owned();
|
||||
let expected_content = format!("const projectRootExample = {root_text:?};");
|
||||
let entry = write(
|
||||
project.path(),
|
||||
&identity,
|
||||
0,
|
||||
&response(
|
||||
"safe text",
|
||||
vec![
|
||||
call(
|
||||
"call-project-path",
|
||||
"runtime_tool_file_write",
|
||||
&serde_json::json!({
|
||||
"reason": "写入页面",
|
||||
"input": {
|
||||
"path": game_path,
|
||||
"content": expected_content,
|
||||
},
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
call(
|
||||
"call-project-cwd",
|
||||
"runtime_tool_command_exec",
|
||||
&serde_json::json!({
|
||||
"reason": "运行测试",
|
||||
"input": {
|
||||
"program": "npm",
|
||||
"args": ["test"],
|
||||
"cwd": project.path(),
|
||||
},
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
call(
|
||||
"call-project-paths",
|
||||
"runtime_tool_project_git_commit",
|
||||
&serde_json::json!({
|
||||
"reason": "提交修改",
|
||||
"input": {
|
||||
"message": "测试",
|
||||
"paths": [game_path, asset_path],
|
||||
},
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
let replayed = entry.to_llm_response();
|
||||
let file_write: serde_json::Value =
|
||||
serde_json::from_str(&replayed.tool_calls[0].arguments).expect("file.write arguments");
|
||||
assert_eq!(file_write["input"]["path"], "game/index.html");
|
||||
assert_eq!(file_write["input"]["content"], expected_content);
|
||||
let command: serde_json::Value =
|
||||
serde_json::from_str(&replayed.tool_calls[1].arguments).expect("command arguments");
|
||||
assert_eq!(command["input"]["cwd"], ".");
|
||||
let git_commit: serde_json::Value =
|
||||
serde_json::from_str(&replayed.tool_calls[2].arguments).expect("git commit arguments");
|
||||
assert_eq!(
|
||||
git_commit["input"]["paths"],
|
||||
serde_json::json!(["game/index.html", "assets/ui.png"])
|
||||
);
|
||||
|
||||
let persisted = read_for_run_at(project.path(), &identity.agent_id, &identity.run_id)
|
||||
.expect("read normalized handoff")
|
||||
.expect("normalized handoff ledger");
|
||||
assert_eq!(persisted.entries[0].to_llm_response(), replayed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_plan_handoff_normalizes_legacy_wrapper_paths_without_changing_source() {
|
||||
let project = tempdir().expect("tool-plan handoff project");
|
||||
let identity = identity("loop-0-repair-0");
|
||||
let path = project.path().join("game/index.html");
|
||||
let old_text = format!("const root = {:?};", project.path().to_string_lossy());
|
||||
let arguments = serde_json::json!({
|
||||
"thinkingSummary": "修复页面",
|
||||
"planUpdate": null,
|
||||
"plan": [],
|
||||
"actions": [{
|
||||
"tool": "file.patch",
|
||||
"reason": "修复页面",
|
||||
"input": {
|
||||
"path": path,
|
||||
"oldText": old_text,
|
||||
"newText": "const ready = true;",
|
||||
"expectedReplacements": 1,
|
||||
},
|
||||
}],
|
||||
"response": "",
|
||||
})
|
||||
.to_string();
|
||||
let entry = write(
|
||||
project.path(),
|
||||
&identity,
|
||||
&identity.base_request_slot,
|
||||
0,
|
||||
&provider_request_id("project-path"),
|
||||
&response(
|
||||
"safe text",
|
||||
vec![call(
|
||||
"call-project-path",
|
||||
"file.write",
|
||||
&project_path_argument,
|
||||
"call-legacy-project-path",
|
||||
"submit_agent_tool_plan",
|
||||
&arguments,
|
||||
)],
|
||||
),
|
||||
)
|
||||
.expect_err("project path must fail");
|
||||
assert!(
|
||||
!tool_plan_handoff_path(project.path(), &identity.agent_id, &identity.run_id,).exists()
|
||||
);
|
||||
let replayed: serde_json::Value =
|
||||
serde_json::from_str(&entry.to_llm_response().tool_calls[0].arguments)
|
||||
.expect("legacy arguments");
|
||||
assert_eq!(replayed["actions"][0]["input"]["path"], "game/index.html");
|
||||
assert_eq!(replayed["actions"][0]["input"]["oldText"], old_text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_plan_handoff_does_not_rewrite_dynamic_mcp_or_unknown_arguments() {
|
||||
for (index, tool_name) in [
|
||||
"mcp_tool_0123456789abcdef01234567",
|
||||
"file.write",
|
||||
"unknown_tool",
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let project = tempdir().expect("tool-plan handoff project");
|
||||
let identity = identity("loop-0-repair-0");
|
||||
let arguments = serde_json::json!({
|
||||
"reason": "读取",
|
||||
"input": { "path": project.path().join("game/index.html") },
|
||||
})
|
||||
.to_string();
|
||||
let error = write_at(
|
||||
project.path(),
|
||||
&identity,
|
||||
&identity.base_request_slot,
|
||||
0,
|
||||
&provider_request_id(&format!("untrusted-project-path-{index}")),
|
||||
&response(
|
||||
"safe text",
|
||||
vec![call("call-untrusted-project-path", tool_name, &arguments)],
|
||||
),
|
||||
)
|
||||
.expect_err("dynamic MCP and unknown arguments must not be rewritten");
|
||||
assert!(error.contains("绝对路径"), "unexpected error: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_plan_handoff_does_not_normalize_duplicate_key_or_file_root_inputs() {
|
||||
for index in 0..3 {
|
||||
let project = tempdir().expect("tool-plan handoff project");
|
||||
let project_arguments = if index == 0 {
|
||||
format!(
|
||||
r#"{{"reason":"first","reason":"second","input":{{"path":{}}}}}"#,
|
||||
serde_json::to_string(&project.path().join("game/index.html"))
|
||||
.expect("serialize duplicate path")
|
||||
)
|
||||
} else if index == 1 {
|
||||
serde_json::json!({
|
||||
"reason": "错误地把项目根当文件",
|
||||
"input": { "path": project.path() },
|
||||
})
|
||||
.to_string()
|
||||
} else {
|
||||
r#"{"reason":"隐藏绝对路径","input":{"path":"/etc/passwd","path":"game/index.html"}}"#
|
||||
.to_string()
|
||||
};
|
||||
let identity = identity("loop-0-repair-0");
|
||||
let error = write_at(
|
||||
project.path(),
|
||||
&identity,
|
||||
&identity.base_request_slot,
|
||||
0,
|
||||
&provider_request_id(&format!("non-normalizable-project-path-{index}")),
|
||||
&response(
|
||||
"safe text",
|
||||
vec![call(
|
||||
"call-non-normalizable-project-path",
|
||||
"runtime_tool_file_write",
|
||||
&project_arguments,
|
||||
)],
|
||||
),
|
||||
)
|
||||
.expect_err("duplicate keys and file-root paths must remain rejected");
|
||||
assert!(error.contains("绝对路径"), "unexpected error: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3669,3 +3669,17 @@
|
||||
- 处理:计划叙述与规范源码内容字段只检查真实密钥 token 形状、凭据头标记和不安全控制字符;结构化敏感 JSON key、非内容字段的配置痕迹和绝对路径、真实 token、容量、thinking、身份、顺序及账本完整性继续失败关闭。成功 handoff 失败时只在 Runtime event/state 和 Agent DB 保存受控 `failureKind`、脱敏错误 SHA-256、字符数与 requestId,禁止保存正文、arguments、密钥和绝对路径。
|
||||
- 验证:必须同时覆盖 narrative 和 `oldText / newText / content / html / patch` 提及 `.env` / `game-creator.config` 可 round-trip,`path=.env.local` 与 `sk-...` 真实 token 仍拒绝,全部 handoff 回归通过;诊断审计必须断言不存在 `error / response / arguments` 原文。修复后的外部 Provider 重试仍需新起独立轮次,不能与故障轮或确定性回归拼接为 PASS。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/tool_plan_handoff/content_validation.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_control.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/real_e2e_checkpoint.rs`。
|
||||
|
||||
## 自然语言“继续修复”不能在无活动 Runtime 时落入空 resume
|
||||
|
||||
- 现象:旧 run 已取消且 `/resume` 明确报告无可恢复任务,用户随后输入“继续”“继续之前干的事情”或“那就继续修复”,interaction 仍返回 `resume`,宿主反复扫描后不新建任务。
|
||||
- 原因:interaction 模型能看到会话历史,却不知道宿主已经排除了 active、pending 和可 steer Runtime;宿主又把自然语言 `resume` 与显式 `/resume` 当成相同控制动作机械执行。
|
||||
- 处理:活动 Runtime、排队任务和 Goal 仍在 interaction 前优先 steer/恢复;只有这些门禁全部为空后,自然语言 interaction 返回的 `resume` 才规范化为 `execute` 并创建新 run。显式 `/resume` 继续保持纯恢复控制,不因无任务而隐式执行。
|
||||
- 验证:保留自然语言必须进入统一 interaction loop 与显式 `/resume` 命令测试,并新增无活动 Runtime 时 `Resume -> Execute`、普通 reply 不变的回归。
|
||||
|
||||
## tool-plan 中项目内绝对路径应在成功交接时规范化
|
||||
|
||||
- 现象:Provider 已成功返回原生工具调用,但 `path / paths / cwd / outputPath` 等结构化输入使用了当前项目根目录内的绝对路径;交接安全门禁以 `tool-plan-absolute-path` 失败,run 进入 `needs-reconciliation`,后续“继续”只能排队。
|
||||
- 原因:Runtime 工具最终只接受项目相对路径,但 Provider 不一定始终遵守提示;交接层此前只能拒绝全部绝对路径,无法区分“当前项目内、可无损转换”的输入与项目外越界输入。
|
||||
- 处理:成功响应写入 tool-plan handoff 前,只对内置 Runtime 原生函数和 legacy tool-plan wrapper 的合法、无重复 key JSON arguments 按工具 schema 的精确位置做规范化;仅改写完整字符串且位于当前项目根目录内的 `file.*.path`、`project.patchset.changes[*].path`、`project.git_commit.paths[*]`、`command.*.cwd`、`image.inspect.paths[*]` 与 `canvas.asset_generate.outputPath` 等真实路径字段。源码/叙述字段、任务产物描述和动态 MCP arguments 不改写。项目外绝对路径、畸形或重复 key JSON、敏感 key 和真实凭据继续失败关闭。规范化后的 handoff 同时作为当前进程执行值和重启 replay 值,避免 live/restart 语义漂移。
|
||||
- 验证:覆盖项目内 `path`、项目根 `cwd`、`paths` 数组的相对化与落账重放,源码 `content` 原文保持不变,动态 MCP 和项目外绝对路径仍拒绝,并运行全部 tool-plan handoff 回归。
|
||||
|
||||
@@ -733,3 +733,5 @@ game-project/
|
||||
- `canvas.asset_generate.replaceExisting` 默认并必须保持 `false`;只有静态专业 Agent 的 `delegated-*` 唯一 repair run 才能申请 `true`。Runtime 要求当前 delivery 带 `repairOfDelegationId`,原 delivery 已被同一父 Agent / 父 run 认领,原始与返工合同的目标 Agent 和精确 `expectedArtifacts` 路径一致;普通 run、未声明路径、错误 Agent、未认领原交付或缺失原图都失败关闭。图片生成仍服从 `design-foundation` / `art-asset-plan` 的固定输出路径、比例、尺寸、kind 和 label,禁止先删除正式图片;请求前记录旧文件 SHA-256,外部生成返回后在项目写锁内复核,旧图在网络请求期间变化即拒绝覆盖。授权替换先写私有临时文件,再以备份 / rename 切换;落盘或 manifest 登记失败时恢复旧图,不把新旧文件并存状态当作成功。
|
||||
- 2026-07-27 新起的“16 任务正式产物 + 两张真实画布图片 + current revision 静态 / 双视口浏览器 / PNG 证据 + 受限 repair 替换”独立外部 Provider 验收,使用 `npm run agc:test:chat -- --timeout-minutes 75`,约 `59m50s` 后以退出码 `0` 完整 **PASS**。同一轮真实生成并登记 `assets/ui-prototype.png`(`2829418` bytes)与 `assets/art-spritesheet.png`(`1361906` bytes),固定 `16` 个 manifest task 均为当前父 Run 下唯一 logical run、一次 started、一次 completed、零 failed / cancelled 和一次 manifest projection;七份基础正式产物、两张 PNG、当前 revision 的 `game.static_smoke`、desktop / mobile `lane-defense-v1` playtest、浏览器报告与截图全部通过。`turn.report=settled` 且唯一 assistant,busy / pending / running / confirmation / user-input / reconciliation 均为 `0`;隔离 Runner、一次性项目和隔离 AppData 已自动清理。此前失败轮继续独立保留,不与本轮拼接;未来合同变化仍须新起完整轮次复验。
|
||||
- 2026-07-27 补充 tool-plan 成功响应交接的内容边界:Provider 的自然语言计划叙述,以及结构化 arguments 中 `body / code / content / css / html / newText / oldText / patch / script / text` 等源码内容字段,只检查真实密钥 token 形状、凭据头标记和不安全控制字符;仅仅提及 `.env` 或 `game-creator.config` 不能阻断已经计费的安全响应。结构化输入中的敏感 JSON key、非内容字段中的配置痕迹或绝对路径、真实 token、容量、thinking、身份、顺序和账本完整性门禁仍失败关闭。成功 handoff 失败进入 reconciliation 时,Runtime 额外只持久化受控 `failureKind`、脱敏错误 SHA-256 和字符数,不保存 Provider 正文、function arguments、密钥或绝对路径。定向回归覆盖叙述/源码字段放行、`.env.local` 路径和真实 token 拒绝、全部 tool-plan handoff 回归及诊断零正文。
|
||||
- 自然语言 interaction 的 `resume` 只代表“继续当前未完成 Runtime”。宿主在进入 interaction 前已确认当前 Session 没有可 steer、pending 或 running 的 Runtime 时,模型返回的自然语言 `resume` 必须规范化为 `execute`,基于会话历史新建 run;显式 `/resume` 仍只执行恢复扫描且无任务时不新建,避免“那就继续修复”被反复吞成空恢复。
|
||||
- tool-plan 成功响应落账前,对内置 Runtime 原生函数与 legacy wrapper 的合法、无重复 key JSON arguments 按工具 schema 的精确位置做项目路径 canonicalization:`file.*.path`、`project.patchset.changes[*].path`、`project.git_commit.paths[*]`、`command.*.cwd`、`image.inspect.paths[*]` 与 `canvas.asset_generate.outputPath` 若是当前项目根目录内的完整绝对路径,转换为 `/` 分隔的项目相对路径后再校验、持久化并执行;源码/叙述字段、任务产物描述、动态 MCP arguments 和项目外绝对路径不得改写,后两者继续由绝对路径门禁失败关闭。项目根只允许搜索/列举范围与命令 cwd 规范化为 `.`,不能成为文件目标。当前进程与重启恢复都必须从同一份规范化 handoff 重放,禁止分别执行原响应和持久响应。
|
||||
|
||||
Reference in New Issue
Block a user