支持Agent独立工具策略
扩展项目权限策略,按Agent保存独立拒绝和确认命令。 后台Runtime按项目级与Agent级策略叠加生成工具快照并拦截执行。 主聊天新增agent-policy命令并保留既有策略确认流程。 补充前端和Rust测试覆盖独立Agent策略读写与执行拦截。 同步AI游戏创作App技术方案和项目决策记录。
This commit is contained in:
@@ -853,22 +853,22 @@ struct AgentRuntimeToolPlan {
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AgentRuntimeToolAction {
|
||||
pub(crate) struct AgentRuntimeToolAction {
|
||||
#[serde(default)]
|
||||
tool: String,
|
||||
pub(crate) tool: String,
|
||||
#[serde(default)]
|
||||
reason: Option<String>,
|
||||
pub(crate) reason: Option<String>,
|
||||
#[serde(default)]
|
||||
input: serde_json::Value,
|
||||
pub(crate) input: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AgentRuntimeToolObservation {
|
||||
tool: String,
|
||||
status: String,
|
||||
summary: String,
|
||||
detail: Option<String>,
|
||||
pub(crate) struct AgentRuntimeToolObservation {
|
||||
pub(crate) tool: String,
|
||||
pub(crate) status: String,
|
||||
pub(crate) summary: String,
|
||||
pub(crate) detail: Option<String>,
|
||||
}
|
||||
|
||||
impl AgentRuntimeToolObservation {
|
||||
@@ -1119,7 +1119,7 @@ fn build_game_creator_agent_background_tool_plan_request(
|
||||
serde_json::to_string_pretty(observations)
|
||||
.map_err(|error| format!("序列化 Agent 工具观察失败:{error}"))?
|
||||
};
|
||||
let tool_policy = agent_runtime_tool_policy_snapshot_at(root)?;
|
||||
let tool_policy = agent_runtime_tool_policy_snapshot_at(root, agent_id)?;
|
||||
let tool_policy_json = serde_json::to_string_pretty(&tool_policy)
|
||||
.map_err(|error| format!("序列化 Agent 工具策略失败:{error}"))?;
|
||||
let prompt = format!(
|
||||
@@ -1188,7 +1188,7 @@ fn parse_game_creator_agent_tool_plan_response(
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
async fn execute_game_creator_agent_runtime_tool_action(
|
||||
pub(crate) async fn execute_game_creator_agent_runtime_tool_action(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
task: &str,
|
||||
@@ -1197,7 +1197,9 @@ async fn execute_game_creator_agent_runtime_tool_action(
|
||||
let tool = action.tool.trim();
|
||||
let command_id = game_creator_agent_runtime_tool_command_id(tool);
|
||||
if let Some(command_id) = command_id {
|
||||
if let Some(blocked) = game_creator_agent_runtime_tool_policy_block(root, command_id) {
|
||||
if let Some(blocked) =
|
||||
game_creator_agent_runtime_tool_policy_block(root, agent_id, command_id)
|
||||
{
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: tool.to_string(),
|
||||
status: "blocked".to_string(),
|
||||
@@ -1296,8 +1298,9 @@ fn agent_runtime_executable_tools() -> Vec<&'static str> {
|
||||
|
||||
fn agent_runtime_tool_policy_snapshot_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
) -> Result<AgentRuntimeToolPolicySnapshot, String> {
|
||||
let view = read_project_permission_policy_at(root)?;
|
||||
let policy = agent_runtime_effective_tool_policy_at(root, agent_id)?;
|
||||
let mut auto_tools = Vec::new();
|
||||
let mut confirm_tools = Vec::new();
|
||||
let mut denied_tools = Vec::new();
|
||||
@@ -1305,15 +1308,13 @@ fn agent_runtime_tool_policy_snapshot_at(
|
||||
let Some(command_id) = game_creator_agent_runtime_tool_command_id(tool) else {
|
||||
continue;
|
||||
};
|
||||
if view
|
||||
.policy
|
||||
if policy
|
||||
.denied_commands
|
||||
.iter()
|
||||
.any(|command| command == command_id)
|
||||
{
|
||||
denied_tools.push(tool.to_string());
|
||||
} else if view
|
||||
.policy
|
||||
} else if policy
|
||||
.confirm_commands
|
||||
.iter()
|
||||
.any(|command| command == command_id)
|
||||
@@ -1339,13 +1340,51 @@ fn refresh_game_creator_agent_runtime_tool_policy(
|
||||
root: &Path,
|
||||
state: &mut AgentRuntimeState,
|
||||
) -> Result<(), String> {
|
||||
state.tool_policy = agent_runtime_tool_policy_snapshot_at(root)?;
|
||||
state.tool_policy = agent_runtime_tool_policy_snapshot_at(root, &state.agent_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn game_creator_agent_runtime_tool_policy_block(root: &Path, command_id: &str) -> Option<String> {
|
||||
fn agent_runtime_effective_tool_policy_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
) -> Result<ProjectAgentPermissionPolicy, String> {
|
||||
let view = match read_project_permission_policy_at(root) {
|
||||
Ok(view) => view,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
||||
let mut denied_commands = view.policy.denied_commands.clone();
|
||||
let mut confirm_commands = view.policy.confirm_commands.clone();
|
||||
if let Some(agent_policy) = view.policy.agent_policies.get(&agent_id) {
|
||||
for command_id in &agent_policy.denied_commands {
|
||||
if !denied_commands.iter().any(|command| command == command_id) {
|
||||
denied_commands.push(command_id.clone());
|
||||
}
|
||||
}
|
||||
for command_id in &agent_policy.confirm_commands {
|
||||
if !confirm_commands.iter().any(|command| command == command_id) {
|
||||
confirm_commands.push(command_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
confirm_commands.retain(|command| !denied_commands.contains(command));
|
||||
Ok(ProjectAgentPermissionPolicy {
|
||||
denied_commands,
|
||||
confirm_commands,
|
||||
})
|
||||
}
|
||||
|
||||
fn game_creator_agent_runtime_tool_policy_block(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
command_id: &str,
|
||||
) -> Option<String> {
|
||||
let view = match read_project_permission_policy_at(root) {
|
||||
Ok(view) => view,
|
||||
Err(error) => return Some(error),
|
||||
};
|
||||
let agent_id = match normalize_game_creator_runtime_agent_id(agent_id) {
|
||||
Ok(agent_id) => agent_id,
|
||||
Err(error) => return Some(error),
|
||||
};
|
||||
if view
|
||||
@@ -1356,6 +1395,20 @@ fn game_creator_agent_runtime_tool_policy_block(root: &Path, command_id: &str) -
|
||||
{
|
||||
return Some(format!("项目权限策略拒绝执行:{command_id}"));
|
||||
}
|
||||
if view
|
||||
.policy
|
||||
.agent_policies
|
||||
.get(&agent_id)
|
||||
.map(|policy| {
|
||||
policy
|
||||
.denied_commands
|
||||
.iter()
|
||||
.any(|command| command == command_id)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Some(format!("Agent 权限策略拒绝执行:{agent_id} / {command_id}"));
|
||||
}
|
||||
if view
|
||||
.policy
|
||||
.confirm_commands
|
||||
@@ -1364,6 +1417,22 @@ fn game_creator_agent_runtime_tool_policy_block(root: &Path, command_id: &str) -
|
||||
{
|
||||
return Some(format!("项目权限策略要求用户确认:{command_id}"));
|
||||
}
|
||||
if view
|
||||
.policy
|
||||
.agent_policies
|
||||
.get(&agent_id)
|
||||
.map(|policy| {
|
||||
policy
|
||||
.confirm_commands
|
||||
.iter()
|
||||
.any(|command| command == command_id)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Some(format!(
|
||||
"Agent 权限策略要求用户确认:{agent_id} / {command_id}"
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
@@ -644,6 +644,17 @@ struct LocalConversationResult {
|
||||
struct ProjectPermissionPolicy {
|
||||
denied_commands: Vec<String>,
|
||||
confirm_commands: Vec<String>,
|
||||
#[serde(default)]
|
||||
agent_policies: BTreeMap<String, ProjectAgentPermissionPolicy>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ProjectAgentPermissionPolicy {
|
||||
#[serde(default)]
|
||||
denied_commands: Vec<String>,
|
||||
#[serde(default)]
|
||||
confirm_commands: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Serialize)]
|
||||
|
||||
@@ -487,6 +487,7 @@ impl Default for ProjectPermissionPolicy {
|
||||
.filter(|command| command.permission == GameCreationAppPermission::Confirm)
|
||||
.map(|command| command.id.to_string())
|
||||
.collect(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -533,6 +534,7 @@ pub(crate) fn write_project_permission_policy_at(
|
||||
"recordType": "project.policy_write",
|
||||
"deniedCommands": policy.denied_commands,
|
||||
"confirmCommands": policy.confirm_commands,
|
||||
"agentPolicies": policy.agent_policies,
|
||||
}),
|
||||
)?;
|
||||
read_project_permission_policy_at(root)
|
||||
@@ -546,6 +548,18 @@ pub(crate) fn normalize_project_permission_policy(
|
||||
policy
|
||||
.confirm_commands
|
||||
.retain(|command| !policy.denied_commands.contains(command));
|
||||
let mut agent_policies = BTreeMap::new();
|
||||
for (agent_id, mut agent_policy) in policy.agent_policies {
|
||||
let agent_id = normalize_game_creator_runtime_agent_id(&agent_id)?;
|
||||
agent_policy.denied_commands = normalize_policy_command_ids(agent_policy.denied_commands)?;
|
||||
agent_policy.confirm_commands =
|
||||
normalize_policy_command_ids(agent_policy.confirm_commands)?;
|
||||
agent_policy
|
||||
.confirm_commands
|
||||
.retain(|command| !agent_policy.denied_commands.contains(command));
|
||||
agent_policies.insert(agent_id, agent_policy);
|
||||
}
|
||||
policy.agent_policies = agent_policies;
|
||||
Ok(policy)
|
||||
}
|
||||
|
||||
|
||||
@@ -1401,6 +1401,7 @@ fn agent_runtime_tool_policy_snapshot_reflects_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: vec!["file.write".to_string()],
|
||||
confirm_commands: vec!["memory.write".to_string(), "task.update".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -1452,6 +1453,116 @@ fn agent_runtime_tool_policy_snapshot_reflects_project_policy() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_runtime_tool_policy_snapshot_reflects_agent_policy() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
|
||||
let mut agent_policies = BTreeMap::new();
|
||||
agent_policies.insert(
|
||||
"design-director".to_string(),
|
||||
ProjectAgentPermissionPolicy {
|
||||
denied_commands: vec!["file.read".to_string()],
|
||||
confirm_commands: vec!["memory.write".to_string()],
|
||||
},
|
||||
);
|
||||
write_project_permission_policy_at(
|
||||
&root,
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies,
|
||||
},
|
||||
)
|
||||
.expect("write agent policy");
|
||||
|
||||
let design_runtime = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"检查策划 Agent 工具策略",
|
||||
"design-policy-snapshot-run",
|
||||
"agent-chat",
|
||||
"读取工具策略",
|
||||
vec!["核对工具策略".to_string()],
|
||||
)
|
||||
.expect("start design runtime");
|
||||
let art_runtime = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
"art-director",
|
||||
"检查美术 Agent 工具策略",
|
||||
"art-policy-snapshot-run",
|
||||
"agent-chat",
|
||||
"读取工具策略",
|
||||
vec!["核对工具策略".to_string()],
|
||||
)
|
||||
.expect("start art runtime");
|
||||
|
||||
assert!(design_runtime
|
||||
.tool_policy
|
||||
.denied_tools
|
||||
.contains(&"file.read".to_string()));
|
||||
assert!(design_runtime
|
||||
.tool_policy
|
||||
.confirm_tools
|
||||
.contains(&"memory.write".to_string()));
|
||||
assert!(design_runtime
|
||||
.tool_policy
|
||||
.confirm_tools
|
||||
.contains(&"blackboard.write".to_string()));
|
||||
assert!(art_runtime
|
||||
.tool_policy
|
||||
.auto_tools
|
||||
.contains(&"file.read".to_string()));
|
||||
assert!(art_runtime
|
||||
.tool_policy
|
||||
.auto_tools
|
||||
.contains(&"memory.write".to_string()));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_tool_action_respects_agent_policy() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
|
||||
let mut agent_policies = BTreeMap::new();
|
||||
agent_policies.insert(
|
||||
"design-director".to_string(),
|
||||
ProjectAgentPermissionPolicy {
|
||||
denied_commands: vec!["file.read".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
},
|
||||
);
|
||||
write_project_permission_policy_at(
|
||||
&root,
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies,
|
||||
},
|
||||
)
|
||||
.expect("write agent policy");
|
||||
|
||||
let observation = execute_game_creator_agent_runtime_tool_action(
|
||||
&root,
|
||||
"design-director",
|
||||
"读取设计笔记",
|
||||
&AgentRuntimeToolAction {
|
||||
tool: "file.read".to_string(),
|
||||
reason: Some("需要读取设计笔记".to_string()),
|
||||
input: serde_json::json!({ "path": "game/notes.txt" }),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(observation.status, "blocked");
|
||||
assert_eq!(
|
||||
observation.summary,
|
||||
"Agent 权限策略拒绝执行:design-director / file.read"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn role_agent_runtime_turn_persists_session_events_and_index() {
|
||||
let root = unique_project_path();
|
||||
@@ -2422,6 +2533,7 @@ async fn background_agent_runtime_can_write_blackboard_and_message_other_agent()
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("allow runtime collaboration writes");
|
||||
@@ -2543,6 +2655,7 @@ async fn background_agent_runtime_can_delegate_task_to_other_agent() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("allow runtime delegation");
|
||||
@@ -2678,6 +2791,7 @@ async fn background_agent_runtime_delegate_respects_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["agent.delegate".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -2772,6 +2886,7 @@ async fn background_agent_runtime_can_write_memory_and_project_files() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("allow runtime writes");
|
||||
@@ -2900,6 +3015,7 @@ async fn background_agent_runtime_write_tools_respect_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["memory.write".to_string(), "conversation.write".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -3000,6 +3116,7 @@ async fn background_agent_runtime_file_and_memory_writes_respect_project_policy(
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["memory.write".to_string(), "file.write".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -3109,6 +3226,7 @@ async fn background_agent_runtime_can_list_manifest_tasks() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("allow task list");
|
||||
@@ -3203,6 +3321,7 @@ async fn background_agent_runtime_task_list_respects_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["task.list".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -3285,6 +3404,7 @@ async fn background_agent_runtime_can_update_manifest_task_status() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("allow task update");
|
||||
@@ -3382,6 +3502,7 @@ async fn background_agent_runtime_task_update_respects_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["task.update".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -3474,6 +3595,7 @@ async fn background_agent_runtime_can_run_limited_static_smoke() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("allow limited command");
|
||||
@@ -3579,6 +3701,7 @@ async fn background_agent_runtime_limited_command_respects_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["command.run_limited".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -3664,6 +3787,7 @@ async fn background_agent_runtime_can_start_local_preview() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("allow preview start");
|
||||
@@ -3768,6 +3892,7 @@ async fn background_agent_runtime_preview_start_respects_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["preview.start".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -3853,6 +3978,7 @@ async fn background_agent_runtime_can_generate_platform_art_asset() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("allow asset generation");
|
||||
@@ -3972,6 +4098,7 @@ async fn background_agent_runtime_asset_generation_respects_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["canvas.asset_generate".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -4061,6 +4188,7 @@ async fn background_agent_runtime_tool_action_respects_confirm_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["file.read".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -4159,6 +4287,7 @@ async fn background_agent_runtime_can_list_project_files() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("allow file list");
|
||||
@@ -4257,6 +4386,7 @@ async fn background_agent_runtime_file_list_respects_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["file.list".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -4345,6 +4475,7 @@ async fn background_agent_runtime_can_diff_checkpoint() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("allow project diff");
|
||||
@@ -4441,6 +4572,7 @@ async fn background_agent_runtime_project_diff_respects_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["project.diff".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -4542,6 +4674,7 @@ async fn background_agent_runtime_can_read_other_agent_status() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("allow agent status");
|
||||
@@ -4653,6 +4786,7 @@ async fn background_agent_runtime_run_status_respects_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["agent.run_status".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -7089,6 +7223,7 @@ async fn generate_platform_art_asset_respects_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: vec!["canvas.asset_generate".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -7234,6 +7369,7 @@ fn local_memory_reads_respect_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: vec!["memory.read".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -7259,6 +7395,7 @@ fn local_agent_memory_writes_respect_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: vec!["memory.write".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -7565,6 +7702,7 @@ fn local_conversation_write_respects_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: vec!["conversation.write".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -7603,6 +7741,7 @@ fn local_conversation_read_respects_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: vec!["conversation.read".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -7983,6 +8122,7 @@ fn project_permission_policy_can_deny_mutating_commands() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: vec!["file.write".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -8003,6 +8143,7 @@ fn project_permission_policy_read_respects_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: vec!["project.policy_read".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -8023,6 +8164,7 @@ fn local_game_manifest_reads_respect_declared_command_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: vec!["project.status".to_string(), "asset.list".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -8052,6 +8194,7 @@ fn local_project_file_read_and_list_respect_project_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: vec!["file.list".to_string(), "file.read".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -8077,6 +8220,7 @@ fn local_project_file_read_can_enforce_agent_trace_read_policy() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: vec!["agent.trace_read".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -8859,6 +9003,7 @@ fn preview_open_respects_project_policy_when_project_is_provided() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: vec!["preview.open".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
@@ -8886,6 +9031,7 @@ fn preview_status_respects_project_policy_when_project_is_provided() {
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: vec!["preview.status".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
|
||||
@@ -877,6 +877,12 @@ function AgentRuntimeStatusPanel({
|
||||
interface ProjectPermissionPolicy {
|
||||
deniedCommands: string[];
|
||||
confirmCommands: string[];
|
||||
agentPolicies?: Record<string, ProjectAgentPermissionPolicy>;
|
||||
}
|
||||
|
||||
interface ProjectAgentPermissionPolicy {
|
||||
deniedCommands: string[];
|
||||
confirmCommands: string[];
|
||||
}
|
||||
|
||||
interface ProjectPermissionPolicyView {
|
||||
@@ -4749,6 +4755,10 @@ const chatCommandHelp = [
|
||||
'/policy-allow 命令:移除项目内某个命令拒绝项',
|
||||
'/policy-confirm 命令:执行前每次确认',
|
||||
'/policy-auto 命令:恢复自动执行',
|
||||
'/agent-policy-deny Agent 命令:拒绝某个 Agent 调用工具',
|
||||
'/agent-policy-allow Agent 命令:移除某个 Agent 的拒绝项',
|
||||
'/agent-policy-confirm Agent 命令:某个 Agent 调用工具前要求确认',
|
||||
'/agent-policy-auto Agent 命令:恢复某个 Agent 自动执行',
|
||||
'/tasks:查看任务拆分',
|
||||
'/agents:查看每个 Agent 的当前状态',
|
||||
'/agent-conversations:列出 Agent 对话读取命令',
|
||||
@@ -4812,6 +4822,14 @@ function missingChatCommandArgumentMessage(prompt: string) {
|
||||
return '格式:/policy-confirm project.index';
|
||||
case '/policy-auto':
|
||||
return '格式:/policy-auto project.index';
|
||||
case '/agent-policy-deny':
|
||||
return '格式:/agent-policy-deny design-director file.read';
|
||||
case '/agent-policy-allow':
|
||||
return '格式:/agent-policy-allow design-director file.read';
|
||||
case '/agent-policy-confirm':
|
||||
return '格式:/agent-policy-confirm design-director memory.write';
|
||||
case '/agent-policy-auto':
|
||||
return '格式:/agent-policy-auto design-director memory.write';
|
||||
case '/read':
|
||||
return '格式:/read game/index.html';
|
||||
case '/asset-register':
|
||||
@@ -9841,10 +9859,25 @@ function summarizeProjectDiff(result: LocalProjectDiffResult) {
|
||||
}
|
||||
|
||||
function summarizeProjectPolicy(view: ProjectPermissionPolicyView) {
|
||||
const agentPolicies = view.policy.agentPolicies ?? {};
|
||||
const agentPolicyLines = Object.entries(agentPolicies)
|
||||
.slice(0, 8)
|
||||
.map(
|
||||
([agentId, policy]) =>
|
||||
`Agent ${agentId}:拒绝 ${formatProjectPolicyCommandList(
|
||||
policy.deniedCommands,
|
||||
)};确认 ${formatProjectPolicyCommandList(policy.confirmCommands)}`,
|
||||
);
|
||||
if (Object.keys(agentPolicies).length > agentPolicyLines.length) {
|
||||
agentPolicyLines.push(
|
||||
`Agent 策略还有 ${Object.keys(agentPolicies).length - agentPolicyLines.length} 项`,
|
||||
);
|
||||
}
|
||||
return [
|
||||
`策略:${view.path}`,
|
||||
`拒绝:${formatProjectPolicyCommandList(view.policy.deniedCommands)}`,
|
||||
`确认:${formatProjectPolicyCommandList(view.policy.confirmCommands)}`,
|
||||
...agentPolicyLines,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -11218,6 +11251,22 @@ function formatAgentCardRuntimeStatus(agent: AgentStatusCard) {
|
||||
.join(' · ');
|
||||
}
|
||||
|
||||
function formatAgentPolicySummary(policy: ProjectPermissionPolicy) {
|
||||
const entries = Object.entries(policy.agentPolicies ?? {});
|
||||
if (entries.length === 0) {
|
||||
return '无 Agent 独立策略';
|
||||
}
|
||||
return entries
|
||||
.slice(0, 4)
|
||||
.map(
|
||||
([agentId, agentPolicy]) =>
|
||||
`${agentId} 拒绝:${formatProjectPolicyCommandList(
|
||||
agentPolicy.deniedCommands,
|
||||
)} 确认:${formatProjectPolicyCommandList(agentPolicy.confirmCommands)}`,
|
||||
)
|
||||
.join(';');
|
||||
}
|
||||
|
||||
function formatAgentRecentRuntimeTask(task: AgentRuntimeTaskRecord) {
|
||||
return `${task.status} / ${task.phase} · ${
|
||||
task.task || task.currentAction || task.runId
|
||||
@@ -11787,7 +11836,9 @@ export function pendingCommandDetail(
|
||||
if (command.id === 'project.policy_write') {
|
||||
return `写入 ${projectPath}/.agent/policy.json · 拒绝:${formatProjectPolicyCommandList(
|
||||
command.policy.deniedCommands,
|
||||
)} · 确认:${formatProjectPolicyCommandList(command.policy.confirmCommands)}`;
|
||||
)} · 确认:${formatProjectPolicyCommandList(
|
||||
command.policy.confirmCommands,
|
||||
)} · Agent:${formatAgentPolicySummary(command.policy)}`;
|
||||
}
|
||||
if (command.id === 'preview.start') {
|
||||
return `启动 ${projectPath}/game/ 并交给外部浏览器`;
|
||||
@@ -14873,7 +14924,7 @@ export function App() {
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
|
||||
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
|
||||
},
|
||||
]);
|
||||
return;
|
||||
@@ -14882,6 +14933,69 @@ export function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
prompt.startsWith('/agent-policy-deny ') ||
|
||||
prompt.startsWith('/agent-policy-allow ') ||
|
||||
prompt.startsWith('/agent-policy-confirm ') ||
|
||||
prompt.startsWith('/agent-policy-auto ')
|
||||
) {
|
||||
if (!requireChatProjectForUserAction()) {
|
||||
return;
|
||||
}
|
||||
const mode = prompt.startsWith('/agent-policy-deny ')
|
||||
? 'deny'
|
||||
: prompt.startsWith('/agent-policy-allow ')
|
||||
? 'allow'
|
||||
: prompt.startsWith('/agent-policy-confirm ')
|
||||
? 'confirm'
|
||||
: 'auto';
|
||||
const commandPrefix =
|
||||
mode === 'deny'
|
||||
? '/agent-policy-deny '
|
||||
: mode === 'allow'
|
||||
? '/agent-policy-allow '
|
||||
: mode === 'confirm'
|
||||
? '/agent-policy-confirm '
|
||||
: '/agent-policy-auto ';
|
||||
const args = prompt.slice(commandPrefix.length).trim();
|
||||
const [agentId = '', commandId = ''] = args.split(/\s+/, 2);
|
||||
if (!agentId || !commandId) {
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text:
|
||||
mode === 'deny' || mode === 'allow'
|
||||
? '格式:/agent-policy-deny design-director file.read'
|
||||
: '格式:/agent-policy-confirm design-director memory.write',
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (!isRegisteredGameCreationCommandId(commandId)) {
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{ role: 'assistant', text: `未知内置命令:${commandId}` },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(mode === 'confirm' || mode === 'auto') &&
|
||||
!isProjectPolicyConfirmableCommandId(commandId)
|
||||
) {
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
void queueAgentPolicyMutation(agentId, commandId, mode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (prompt === '/llm-status') {
|
||||
void executeLlmConfigStatus();
|
||||
return;
|
||||
@@ -16908,6 +17022,137 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function queueAgentPolicyMutation(
|
||||
agentId: string,
|
||||
commandId: string,
|
||||
mode: 'deny' | 'allow' | 'confirm' | 'auto',
|
||||
) {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
const nextProjectPath = requireChatProjectForUserAction();
|
||||
if (!nextProjectPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await invoke<ProjectPermissionPolicyView>(
|
||||
'read_project_permission_policy',
|
||||
{ projectPath: nextProjectPath },
|
||||
);
|
||||
const agentPolicies = { ...(result.policy.agentPolicies ?? {}) };
|
||||
const agentPolicy = agentPolicies[agentId] ?? {
|
||||
deniedCommands: [],
|
||||
confirmCommands: [],
|
||||
};
|
||||
const alreadyDenied = agentPolicy.deniedCommands.includes(commandId);
|
||||
const alreadyConfirmed = agentPolicy.confirmCommands.includes(commandId);
|
||||
if (mode === 'deny' && alreadyDenied) {
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text: `Agent ${agentId} 的命令已在拒绝列表中:${commandId}`,
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (mode === 'allow' && !alreadyDenied) {
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text: `Agent ${agentId} 的命令不在拒绝列表中:${commandId}`,
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (mode === 'confirm' && alreadyConfirmed) {
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text: `Agent ${agentId} 的命令已在确认列表中:${commandId}`,
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (mode === 'auto' && !alreadyConfirmed) {
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text: `Agent ${agentId} 的命令不在确认列表中:${commandId}`,
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
const deniedCommands =
|
||||
mode === 'deny'
|
||||
? [...new Set([...agentPolicy.deniedCommands, commandId])]
|
||||
: mode === 'allow'
|
||||
? agentPolicy.deniedCommands.filter(
|
||||
(value) => value !== commandId,
|
||||
)
|
||||
: mode === 'confirm'
|
||||
? agentPolicy.deniedCommands.filter(
|
||||
(value) => value !== commandId,
|
||||
)
|
||||
: agentPolicy.deniedCommands;
|
||||
const confirmCommands =
|
||||
mode === 'confirm'
|
||||
? [...new Set([...agentPolicy.confirmCommands, commandId])]
|
||||
: mode === 'deny'
|
||||
? agentPolicy.confirmCommands.filter(
|
||||
(value) => value !== commandId,
|
||||
)
|
||||
: mode === 'auto'
|
||||
? agentPolicy.confirmCommands.filter(
|
||||
(value) => value !== commandId,
|
||||
)
|
||||
: agentPolicy.confirmCommands;
|
||||
agentPolicies[agentId] = {
|
||||
deniedCommands,
|
||||
confirmCommands,
|
||||
};
|
||||
queuePendingCommand({
|
||||
id: 'project.policy_write',
|
||||
policy: {
|
||||
...result.policy,
|
||||
agentPolicies,
|
||||
},
|
||||
});
|
||||
const action =
|
||||
mode === 'deny'
|
||||
? '拒绝'
|
||||
: mode === 'allow'
|
||||
? '允许'
|
||||
: mode === 'confirm'
|
||||
? '确认'
|
||||
: '自动执行';
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text: `准备${action} Agent ${agentId} 命令:${commandId}`,
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
async function executeProjectPolicyWrite(
|
||||
policy: ProjectPermissionPolicy,
|
||||
announceToChat: boolean,
|
||||
|
||||
@@ -17504,6 +17504,12 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
policy: {
|
||||
deniedCommands,
|
||||
confirmCommands: ['game.static_smoke'],
|
||||
agentPolicies: {
|
||||
'design-director': {
|
||||
deniedCommands: ['file.read'],
|
||||
confirmCommands: ['memory.write'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -17528,6 +17534,9 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
expect(screen.queryByText(/file\.write\.13/)).toBeNull();
|
||||
expect(screen.getByText(/拒绝:.*还有 2 项/)).not.toBeNull();
|
||||
expect(screen.getByText(/确认:game\.static_smoke/)).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText(/Agent design-director:拒绝 file\.read;确认 memory\.write/),
|
||||
).not.toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
});
|
||||
@@ -17541,6 +17550,12 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
let policy = {
|
||||
deniedCommands: ['file.delete'],
|
||||
confirmCommands: ['game.static_smoke'],
|
||||
agentPolicies: {
|
||||
'design-director': {
|
||||
deniedCommands: ['file.read'],
|
||||
confirmCommands: ['memory.write'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
@@ -17598,6 +17613,12 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
policy: {
|
||||
deniedCommands: ['file.delete', 'file.write'],
|
||||
confirmCommands: ['game.static_smoke'],
|
||||
agentPolicies: {
|
||||
'design-director': {
|
||||
deniedCommands: ['file.read'],
|
||||
confirmCommands: ['memory.write'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(invoke).toHaveBeenCalledWith('append_local_permission_log', {
|
||||
@@ -17607,6 +17628,92 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('updates per-agent project policy from chat after confirmation', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'local-project-draft',
|
||||
'未命名游戏原型',
|
||||
);
|
||||
let policy = {
|
||||
deniedCommands: ['project.index'],
|
||||
confirmCommands: [],
|
||||
agentPolicies: {
|
||||
'art-director': {
|
||||
deniedCommands: [],
|
||||
confirmCommands: ['memory.write'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'append_local_permission_log') {
|
||||
return {};
|
||||
}
|
||||
if (command === 'init_local_game_project') {
|
||||
const projectPath = String(args?.projectPath ?? '');
|
||||
return {
|
||||
projectPath,
|
||||
manifestPath: `${projectPath}/.agent/manifest.json`,
|
||||
manifest,
|
||||
};
|
||||
}
|
||||
if (command === 'read_project_permission_policy') {
|
||||
return {
|
||||
path: '.agent/policy.json',
|
||||
policy,
|
||||
};
|
||||
}
|
||||
if (command === 'write_project_permission_policy') {
|
||||
policy = args?.policy as typeof policy;
|
||||
return {
|
||||
path: '.agent/policy.json',
|
||||
policy,
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderAppAt('/');
|
||||
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/agent-policy-deny design-director file.read');
|
||||
expect(
|
||||
await screen.findByText('准备拒绝 Agent design-director 命令:file.read'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Agent:art-director 拒绝:无 确认:memory\.write;design-director 拒绝:file\.read 确认:无/,
|
||||
),
|
||||
).not.toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
|
||||
expect(
|
||||
await screen.findByText(/Agent design-director:拒绝 file\.read;确认 无/),
|
||||
).not.toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
policy: {
|
||||
deniedCommands: ['project.index'],
|
||||
confirmCommands: [],
|
||||
agentPolicies: {
|
||||
'art-director': {
|
||||
deniedCommands: [],
|
||||
confirmCommands: ['memory.write'],
|
||||
},
|
||||
'design-director': {
|
||||
deniedCommands: ['file.read'],
|
||||
confirmCommands: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('updates project policy confirm commands from chat after confirmation', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'local-project-draft',
|
||||
@@ -18237,7 +18344,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
|
||||
'当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(screen.queryByText('project.policy_write')).toBeNull();
|
||||
|
||||
@@ -4060,6 +4060,7 @@
|
||||
- 2026-07-06 调整:AI 游戏创作 App 主聊天普通文本改为进入主聊天 Agent,而不是直接排队 `game.generate_draft`;主聊天 Agent 读取短期记忆、长期记忆、项目黑板、最近项目对话和本地资产摘要作为背景,支持 `agentLlm.chat` 单独 provider 配置,但只做自然语言交互、澄清和 slash 命令建议,不写项目、不运行工具、不伪装生成结果。显式 `/generate <创作想法>` 或 `/draft <创作想法>` 才进入 `game.generate_draft` 待确认流。
|
||||
- 2026-07-09 调整:AI 游戏创作 App 新增 Agent Runtime V1 最小可观测状态。单 Agent 对话和生成 loop 中的角色 brief 必须写 `.agent/runtime/agents/<agentId>.json` 与 `.agent/runtime/events/<agentId>.jsonl`,记录 `agentId`、`taskId`、`sessionId`、`runId`、`source`、`status`、`phase`、当前任务 / 动作、计划、观测、允许工具、最近回复和错误;单 Agent 流式聊天事件要回传最新 `runtimeState`,开发单 Agent 聊天页和项目内单 Agent 对话弹窗只读展示该状态,读取失败必须可见提示,不得静默伪装为空状态。`source=agent-chat` 表示开发者单 Agent 对话,`source=generate-draft` 表示生成 loop 角色 brief;carry-over brief 只记录继承和完成,不伪装成重新调用 LLM。Runtime state 写入使用临时文件替换,event JSONL 读取跳过坏行,用户 prompt / 回复摘要进入 runtime 与 `agent.db` 前复用敏感上下文过滤;`.agent/runtime/` 是运行观测状态,不进入项目索引、checkpoint diff 或 restore 删除范围。该层仍是本地 JSONL 状态与事件,不引入 SQLite、常驻独立进程、远程 runner 或可中断上游 LLM 的承诺。
|
||||
- 2026-07-10 调整:Agent Runtime state 新增 `toolPolicy`,从项目权限策略派生工具级 `allowedTools`、`autoTools`、`confirmTools` 和 `deniedTools`。后台 planning prompt 必须带入该快照,让 Agent 在规划阶段知道工具策略;执行阶段仍由 Runtime 白名单和项目权限 gate 决定。`blackboard.write` 继承 `memory.write` 策略,`agent.message` 继承 `conversation.write` 策略,`agent.delegate` 使用独立 `agent.delegate` 策略。
|
||||
- 2026-07-10 调整:`.agent/policy.json` 支持 `agentPolicies`,用规范 Agent id 保存单个 Agent 的 `deniedCommands / confirmCommands`。Runtime 计算有效工具策略时把项目级策略和 Agent 级策略叠加,项目级策略继续对所有 Agent 生效,Agent 级策略只能进一步拒绝或要求确认,不能放宽项目级策略;拒绝优先于确认。主聊天新增 `/agent-policy-deny Agent 命令`、`/agent-policy-allow Agent 命令`、`/agent-policy-confirm Agent 命令` 和 `/agent-policy-auto Agent 命令`,继续通过 `project.policy_write` 确认卡写入策略。
|
||||
- 2026-07-10 调整:Agent Runtime state 新增 `recentToolCalls`,后台 loop 每次执行白名单工具后记录最近 20 条结构化动作,包含 tool、status、reason、summary、detail 和 updatedAt。状态面板展示最近动作时使用该字段,不解析 observation 文本;写入前继续过滤敏感上下文,不保存原始密钥或任意未过滤输入。
|
||||
- 2026-07-10 调整:Agent Runtime state 新增 `currentGoal` 和 `waitingOn`。`currentGoal` 固定表达本轮任务目标,`waitingOn` 表达当前等待 LLM、工具观察、开发者输入或失败处理;后台任务生命周期、`agent.run_status` observation、下一轮 planning prompt、开发单 Agent 对话页、项目内 Agent 对话弹窗和主窗口 Agent 状态列表都必须展示同一份目标 / 等待状态。
|
||||
- 2026-07-10 调整:Agent Runtime state 新增 `loopIteration / maxLoopIterations / toolActionBudget`。后台 Agent loop 每轮规划前刷新当前轮次、最大轮次和每轮工具动作预算;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示该进度,字段只做运行观测,不改变 loop 上限或权限 gate。
|
||||
|
||||
@@ -34,6 +34,7 @@ Agent Runtime 负责:
|
||||
- 编排能力:任务拆分、任务图依赖、专业组调度、多智能体协作;Runtime V1 会为单 Agent 对话和生成 loop 中的角色 brief 写入独立 runtime state / event,先解决“每个 Agent 正在做什么、跑到哪一步、最近一次 task/run 是什么”的可观测性。
|
||||
- 后台任务能力:开发窗口单 Agent 聊天和项目内 Agent 对话弹窗可把当前输入投递为单 Agent 后台任务,Tauri 命令 `start_game_creator_agent_runtime_task` 会立即写入该 Agent 的 `.agent/runtime/agents/<agentId>.json`、`.agent/runtime/events/<agentId>.jsonl`、`.agent/runtime/tasks/<agentId>.jsonl` 和 `.agent/conversations/agents/<agentId>.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:每轮让该 Agent 输出 `thinkingSummary / plan / actions / response`,Runtime 按白名单和项目权限策略执行工具动作,写入 `thinking_summary / plan / action / observation / response / error` 事件,再把 observation 放入下一轮 prompt 让 Agent 修正计划、继续行动或用空 actions + response 收束;后台任务最多执行 3 轮 loop,仍未收束时再按最后计划和全部观察生成最终回复并追加回对话。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`project.diff`、`file.list`、`file.read`、`agent.run_status`,以及受策略保护的写/运行工具 `memory.write`、`file.write`、`command.run_limited`、`blackboard.write`、`agent.message` 和 `agent.delegate`;`memory.write` 可追加或覆盖本 Agent 私有记忆、项目长期/短期记忆或黑板,`file.write` 只能写项目内相对路径并记录审计,`command.run_limited` 只接受 `game.static_smoke` 并复用本地静态自检安全边界,`blackboard.write` 追加 `memory/blackboard.md`,`agent.message` 给目标 `.agent/conversations/agents/<agentId>.jsonl` 写入 tool 留言,`agent.delegate` 把任务投递到目标 Agent 的独立后台队列,策略要求确认或拒绝时不执行写入、运行或委派,只把策略结果作为 observation 回给 Agent。`read_game_creator_agent_runtime` 会按 `runId` 去重返回最近任务和最近事件,`read_game_creator_agent_runtimes` 批量读取所有规范 taskId 的 runtime;开发窗口和项目内 Agent 对话弹窗的 Runtime 状态面板展示最近事件、最近任务、当前目标、当前任务、当前动作、等待对象、下一步和运行阶段,主窗口 Agent 状态列表展示当前目标、任务、动作、等待对象和运行阶段摘要。不同 Agent 使用各自 runtime 锁,可以并行运行;同一 Agent 已有运行任务时,新任务会先写成 `pending / queued`,由当前后台 drain 在完成后串行继续执行。该能力仍属于 Runtime V1 的进程内任务,不是独立 OS 进程或跨重启离线常驻 worker。
|
||||
- 2026-07-10 补充:Agent Runtime state 新增 `toolPolicy`,按当前项目 `.agent/policy.json` 派生工具级 `allowedTools / autoTools / confirmTools / deniedTools` 快照;后台 planning prompt 会带入该快照,让 Agent 在规划时知道哪些工具会自动执行、需要确认或被拒绝。`blackboard.write` 继承 `memory.write` 策略,`agent.message` 继承 `conversation.write` 策略,`agent.delegate` 使用独立 `agent.delegate` 策略;实际执行仍以 Runtime 的白名单和项目权限 gate 为准。
|
||||
- 2026-07-10 补充:`.agent/policy.json` 新增 `agentPolicies`,可按规范 Agent id 分别配置 `deniedCommands / confirmCommands`。有效策略为“项目级策略 + Agent 级策略”的保守叠加:项目级拒绝 / 确认仍对所有 Agent 生效,Agent 级策略只能进一步限制该 Agent,不能放宽项目级策略,拒绝优先于确认。主聊天新增 `/agent-policy-deny Agent 命令`、`/agent-policy-allow Agent 命令`、`/agent-policy-confirm Agent 命令` 和 `/agent-policy-auto Agent 命令`,写入前仍走 `project.policy_write` 确认卡。
|
||||
- 2026-07-10 补充:Agent Runtime state 新增 `recentToolCalls`,每次后台工具执行后记录最近 20 条结构化工具动作,包含 tool、status、reason、summary、detail 和 updatedAt;开发窗口、项目内 Agent 对话弹窗和主窗口 Agent 状态列表可直接展示“最近动作”,不再只能从 observation 字符串里猜测 action / observation 对应关系。字段只保存过滤后的摘要和观察细节,不保存原始 API Key 或任意未过滤输入。
|
||||
- 2026-07-10 补充:Agent Runtime state 新增 `currentGoal` 和 `waitingOn`,把本轮目标与当前等待对象从 `currentTask / currentAction / nextStep` 中显式拆出来;后台任务启动、工具 observation、完成和失败都会刷新该状态,开发窗口、项目内 Agent 对话弹窗、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示同一份目标 / 等待信息,避免开发者只能从动作文本里猜 Agent 卡在 LLM、工具、同伴还是人工输入。
|
||||
- 2026-07-10 补充:Agent Runtime state 新增 `loopIteration / maxLoopIterations / toolActionBudget`,结构化记录后台 Agent loop 当前轮次、最大轮次和每轮工具动作预算;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示该进度,帮助判断 Agent 是刚开始规划、正在 replan,还是接近本轮 loop 上限。该字段只做运行观测,不改变后台 loop 的执行上限或工具权限。
|
||||
|
||||
Reference in New Issue
Block a user