实现Runner托管的MCP动态工具

接入STDIO与Streamable HTTP MCP连接、动态工具目录和Runner协议
将mcp.call纳入持久动作、工具审批、私有结果和强杀恢复
增加AppData配置、开发配置界面及聊天CLI的MCP状态入口
补齐真实Provider E2E、全量回归、安全扫描和技术文档
This commit is contained in:
AIGameCreator App
2026-07-16 01:07:29 +08:00
parent 543a5469f8
commit 0425afd5cf
23 changed files with 6182 additions and 101 deletions
@@ -18,5 +18,6 @@
"editorApi": {
"baseUrl": "http://127.0.0.1:8082",
"apiKey": ""
}
},
"mcpServers": {}
}
File diff suppressed because it is too large Load Diff
+150 -14
View File
@@ -503,6 +503,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chromiumoxide"
version = "0.9.1"
@@ -1452,11 +1458,13 @@ dependencies = [
"base64 0.22.1",
"chromiumoxide",
"futures",
"http 1.4.2",
"libc",
"platform-agent",
"platform-llm",
"portable-pty",
"reqwest 0.12.28",
"rmcp",
"serde",
"serde_json",
"sha2",
@@ -2497,7 +2505,19 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.13.0",
"cfg-if",
"cfg_aliases",
"cfg_aliases 0.1.1",
"libc",
]
[[package]]
name = "nix"
version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags 2.13.0",
"cfg-if",
"cfg_aliases 0.2.1",
"libc",
]
@@ -3073,7 +3093,7 @@ dependencies = [
"lazy_static",
"libc",
"log",
"nix",
"nix 0.28.0",
"serial2",
"shared_library",
"shell-words",
@@ -3179,6 +3199,20 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "process-wrap"
version = "9.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e842efad9119158434d193c6682e2ebee4b44d6ad801d7b349623b3f57cdf55"
dependencies = [
"futures",
"indexmap 2.14.0",
"nix 0.31.3",
"tokio",
"tracing",
"windows 0.62.2",
]
[[package]]
name = "quick-xml"
version = "0.39.4"
@@ -3415,15 +3449,19 @@ dependencies = [
"http-body 1.0.1",
"http-body-util",
"hyper 1.10.1",
"hyper-tls 0.6.0",
"hyper-util",
"js-sys",
"log",
"native-tls",
"percent-encoding",
"pin-project-lite",
"rustls-pki-types",
"serde",
"serde_json",
"sync_wrapper 1.0.2",
"tokio",
"tokio-native-tls",
"tokio-util",
"tower",
"tower-http",
@@ -3459,6 +3497,29 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "rmcp"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14db48ee17a9ba61810ab1a9c1beb7d06d8136ae39ac25a1137f10d357af01af"
dependencies = [
"async-trait",
"chrono",
"futures",
"http 1.4.2",
"pin-project-lite",
"process-wrap",
"reqwest 0.13.4",
"serde",
"serde_json",
"sse-stream",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
"tokio-util",
"tracing",
]
[[package]]
name = "rustc-hash"
version = "2.1.2"
@@ -3985,6 +4046,19 @@ dependencies = [
"system-deps",
]
[[package]]
name = "sse-stream"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39f24a9b78c40b90817bbcd1821c74ddfd74916aadd29403d001532a9195532d"
dependencies = [
"bytes",
"futures-util",
"http-body 1.0.1",
"http-body-util",
"pin-project-lite",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -4147,7 +4221,7 @@ dependencies = [
"tao-macros",
"unicode-segmentation",
"url",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-version",
"x11-dl",
@@ -4218,7 +4292,7 @@ dependencies = [
"webkit2gtk",
"webview2-com",
"window-vibrancy",
"windows",
"windows 0.61.3",
]
[[package]]
@@ -4359,7 +4433,7 @@ dependencies = [
"tauri-plugin",
"thiserror 2.0.18",
"url",
"windows",
"windows 0.61.3",
"zbus",
]
@@ -4385,7 +4459,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.61.3",
]
[[package]]
@@ -4410,7 +4484,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.61.3",
"wry",
]
@@ -4619,6 +4693,17 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
@@ -5249,7 +5334,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-implement",
"windows-interface",
@@ -5273,7 +5358,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
dependencies = [
"thiserror 2.0.18",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
]
@@ -5344,11 +5429,23 @@ version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections",
"windows-collections 0.2.0",
"windows-core 0.61.2",
"windows-future",
"windows-future 0.2.1",
"windows-link 0.1.3",
"windows-numerics",
"windows-numerics 0.2.0",
]
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections 0.3.2",
"windows-core 0.62.2",
"windows-future 0.3.2",
"windows-numerics 0.3.1",
]
[[package]]
@@ -5360,6 +5457,15 @@ dependencies = [
"windows-core 0.61.2",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core 0.62.2",
]
[[package]]
name = "windows-core"
version = "0.61.2"
@@ -5394,7 +5500,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-link 0.1.3",
"windows-threading",
"windows-threading 0.1.0",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core 0.62.2",
"windows-link 0.2.1",
"windows-threading 0.2.1",
]
[[package]]
@@ -5441,6 +5558,16 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core 0.62.2",
"windows-link 0.2.1",
]
[[package]]
name = "windows-registry"
version = "0.6.1"
@@ -5614,6 +5741,15 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-version"
version = "0.1.7"
@@ -5906,7 +6042,7 @@ dependencies = [
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-version",
"x11-dl",
@@ -11,6 +11,8 @@ tauri-build = { version = "2.6.2", features = [] }
base64 = "0.22"
chromiumoxide = "0.9.1"
futures = "0.3"
http = "1"
rmcp = { version = "2.2.0", default-features = false, features = ["client", "reqwest-native-tls", "transport-child-process", "transport-streamable-http-client-reqwest"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
@@ -24,7 +26,7 @@ tauri = { version = "2.11.2", features = [] }
tauri-plugin-dialog = "2.7.1"
tauri-plugin-opener = "2.5.4"
tempfile = "3"
tokio = { version = "1", features = ["io-util", "macros", "process", "rt-multi-thread", "time"] }
tokio = { version = "1", features = ["io-util", "macros", "process", "rt-multi-thread", "sync", "time"] }
url = "2"
unicode-normalization = "0.1"
zip = { version = "2", default-features = false, features = ["deflate"] }
File diff suppressed because one or more lines are too long
@@ -771,6 +771,13 @@ pub(crate) fn write_game_creator_app_config(
game_creator_app_config_view(load_game_creator_app_config()?)
}
#[tauri::command]
pub(crate) fn read_game_creator_mcp_catalog(
project_path: String,
) -> Result<GameCreatorMcpCatalog, String> {
read_external_agent_runner_mcp_catalog(Path::new(project_path.trim()))
}
#[tauri::command]
pub(crate) fn upload_local_asset(
project_path: String,
@@ -963,6 +963,11 @@ pub(crate) fn merge_game_creator_config_file(
if let Some(editor_api) = file_config.editor_api {
merge_game_creator_editor_api_config(&mut config.editor_api, editor_api);
}
if let Some(mcp_servers) = file_config.mcp_servers {
for (server_id, server) in mcp_servers {
config.mcp_servers.insert(server_id, server);
}
}
Ok(())
}
@@ -1228,6 +1233,7 @@ pub(crate) fn normalize_game_creator_app_config(
config.editor_api.base_url = trim_config_string(&config.editor_api.base_url)
.ok_or_else(|| "配置项 editorApi.baseUrl 不能为空".to_string())?;
config.editor_api.api_key = config.editor_api.api_key.trim().to_string();
config.mcp_servers = normalize_game_creator_mcp_servers(config.mcp_servers)?;
Ok(config)
}
@@ -1,4 +1,5 @@
use super::agent::{sanitize_prompt_context, write_agent_runtime_json_sidecar_with_max_bytes};
use super::mcp::GAME_CREATOR_MCP_CALL_TOOL;
use super::project::{
normalize_relative_path, resolve_local_project_path, unix_timestamp, validate_project_root,
};
@@ -392,6 +393,7 @@ pub(crate) fn validate_isolated_agent_tool_scope_at(
| "task.create"
| "task.update"
| "blackboard.write"
| GAME_CREATOR_MCP_CALL_TOOL
) {
return Err(format!(
"动态隔离子 Agent 默认拒绝无 writeScope 落点的工具:{tool}"
@@ -58,6 +58,7 @@ mod git_inspect;
mod goal;
mod image_inspect;
mod isolated_agent;
mod mcp;
mod patchset;
mod preview;
mod process_session;
@@ -83,6 +84,7 @@ use git_inspect::*;
use goal::*;
use image_inspect::*;
use isolated_agent::*;
use mcp::*;
use patchset::*;
use preview::*;
use process_session::*;
@@ -636,6 +638,7 @@ struct GameCreatorAppConfigFile {
llm: Option<GameCreatorLlmConfigFile>,
agent_llm: Option<BTreeMap<String, GameCreatorLlmConfigFile>>,
editor_api: Option<GameCreatorEditorApiConfigFile>,
mcp_servers: Option<BTreeMap<String, GameCreatorMcpServerConfig>>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
@@ -683,6 +686,56 @@ struct GameCreatorAppConfig {
#[serde(default)]
agent_llm: BTreeMap<String, GameCreatorLlmConfigFile>,
editor_api: GameCreatorEditorApiConfig,
#[serde(default)]
mcp_servers: BTreeMap<String, GameCreatorMcpServerConfig>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorMcpServerConfig {
#[serde(default = "default_game_creator_mcp_enabled")]
enabled: bool,
#[serde(default)]
required: bool,
#[serde(default = "default_game_creator_mcp_transport")]
transport: String,
#[serde(default)]
command: String,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
cwd: String,
#[serde(default)]
env: BTreeMap<String, String>,
#[serde(default)]
url: String,
#[serde(default)]
bearer_token: String,
#[serde(default)]
http_headers: BTreeMap<String, String>,
#[serde(default)]
allow_insecure_localhost: bool,
#[serde(default = "default_game_creator_mcp_startup_timeout_ms")]
startup_timeout_ms: u64,
#[serde(default = "default_game_creator_mcp_tool_timeout_ms")]
tool_timeout_ms: u64,
#[serde(default)]
enabled_tools: Vec<String>,
#[serde(default)]
disabled_tools: Vec<String>,
#[serde(default = "default_game_creator_mcp_approval_mode")]
default_approval_mode: String,
#[serde(default)]
tools: BTreeMap<String, GameCreatorMcpToolConfig>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameCreatorMcpToolConfig {
#[serde(default)]
enabled: Option<bool>,
#[serde(default)]
approval_mode: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -1128,6 +1181,7 @@ impl Default for GameCreatorAppConfig {
llm: GameCreatorLlmConfig::default(),
agent_llm: BTreeMap::new(),
editor_api: GameCreatorEditorApiConfig::default(),
mcp_servers: BTreeMap::new(),
}
}
}
@@ -1643,6 +1697,7 @@ fn main() {
check_game_creator_llm_config,
read_game_creator_app_config,
write_game_creator_app_config,
read_game_creator_mcp_catalog,
upload_local_asset,
register_local_asset,
import_canvas_asset,
File diff suppressed because it is too large Load Diff
@@ -12,9 +12,9 @@ use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::AgentRuntimeContextCompactionResult;
use crate::{AgentRuntimeContextCompactionResult, GameCreatorMcpCatalog};
pub(crate) const EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION: u32 = 3;
pub(crate) const EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION: u32 = 4;
const EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME: &str = "agent-runner.endpoint.json";
const EXTERNAL_AGENT_RUNNER_LOCK_FILE_NAME: &str = "agent-runner.lock";
@@ -32,6 +32,7 @@ const EXTERNAL_AGENT_RUNNER_RETRYABLE_WAKE_ERROR_CODE: &str = "runtime-wake-retr
const EXTERNAL_AGENT_RUNNER_CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
const EXTERNAL_AGENT_RUNNER_IO_TIMEOUT: Duration = Duration::from_secs(10);
const EXTERNAL_AGENT_RUNNER_CONTEXT_COMPACTION_IO_TIMEOUT: Duration = Duration::from_secs(6 * 60);
const EXTERNAL_AGENT_RUNNER_MCP_STATUS_IO_TIMEOUT: Duration = Duration::from_secs(6 * 60);
const EXTERNAL_AGENT_RUNNER_START_TIMEOUT: Duration = Duration::from_secs(6);
const EXTERNAL_AGENT_RUNNER_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(2);
const EXTERNAL_AGENT_RUNNER_LOOP_INTERVAL: Duration = Duration::from_millis(25);
@@ -2619,6 +2620,32 @@ fn handle_external_agent_runner_request(
"序列化 Agent Runner 状态失败",
),
},
"mcp.status" => {
if state.draining.load(Ordering::Acquire) {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"runner-draining",
"Agent Runner 正在排空并准备退出,拒绝新的 MCP 状态请求",
);
}
let result = (|| {
let root = external_agent_runner_request_root(&request)?;
let root = canonicalize_external_agent_runner_project_root(&root)?;
state.remember_root(&root);
let catalog =
tauri::async_runtime::block_on(crate::read_game_creator_mcp_catalog_at(&root))?;
serde_json::to_value(catalog)
.map_err(|error| format!("序列化 MCP catalog 失败:{error}"))
})();
match result {
Ok(catalog) => ExternalAgentRunnerResponse::success(&request.request_id, catalog),
Err(error) => ExternalAgentRunnerResponse::failure(
&request.request_id,
"mcp-status-failed",
redact_runner_secret(&error, &expected_token),
),
}
}
"runtime.wake_pending"
| "runtime.resume"
| "runtime.continue_action"
@@ -3194,10 +3221,10 @@ fn send_external_agent_runner_request_with_protocol_and_id(
}
fn external_agent_runner_client_read_timeout(method: &str) -> Duration {
if method == "runtime.compact" {
EXTERNAL_AGENT_RUNNER_CONTEXT_COMPACTION_IO_TIMEOUT
} else {
EXTERNAL_AGENT_RUNNER_IO_TIMEOUT
match method {
"runtime.compact" => EXTERNAL_AGENT_RUNNER_CONTEXT_COMPACTION_IO_TIMEOUT,
"mcp.status" => EXTERNAL_AGENT_RUNNER_MCP_STATUS_IO_TIMEOUT,
_ => EXTERNAL_AGENT_RUNNER_IO_TIMEOUT,
}
}
@@ -3489,6 +3516,15 @@ pub(crate) fn compact_external_agent_runner_context(
.map_err(|error| format!("解析 Agent Runner 上下文压缩结果失败:{error}"))
}
pub(crate) fn read_external_agent_runner_mcp_catalog(
root: &Path,
) -> Result<GameCreatorMcpCatalog, String> {
let result =
send_external_agent_runner_runtime_request(root, "mcp.status", None, None, None, None)?;
serde_json::from_value(result)
.map_err(|error| format!("解析 Agent Runner MCP catalog 失败:{error}"))
}
pub(crate) fn wake_external_agent_runner_pending(root: &Path) -> Result<(), String> {
send_external_agent_runner_runtime_request(root, "runtime.wake_pending", None, None, None, None)
.map(|_| ())
@@ -3756,6 +3792,15 @@ mod tests {
external_agent_runner_client_read_timeout("runtime.start"),
EXTERNAL_AGENT_RUNNER_IO_TIMEOUT
);
assert_eq!(
external_agent_runner_client_read_timeout("mcp.status"),
EXTERNAL_AGENT_RUNNER_MCP_STATUS_IO_TIMEOUT
);
assert!(
external_agent_runner_client_read_timeout("mcp.status")
> EXTERNAL_AGENT_RUNNER_IO_TIMEOUT
);
assert_eq!(EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION, 4);
}
fn test_endpoint(token: &str, boot_id: &str, port: u16) -> ExternalAgentRunnerEndpoint {
@@ -4257,7 +4302,7 @@ mod tests {
}
#[test]
fn draining_rejects_runtime_steer_and_compact() {
fn draining_rejects_runtime_steer_compact_and_mcp_status() {
let directory = unique_test_directory();
let token = "steer-draining-token-steer-draining-token";
let state = ExternalAgentRunnerServerState::new(
@@ -4311,6 +4356,25 @@ mod tests {
.map(|error| error.code.as_str()),
Some("runner-draining")
);
let mcp_response = handle_external_agent_runner_request(
ExternalAgentRunnerRequest {
protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
request_id: "draining-mcp-status-1".to_string(),
token: token.to_string(),
method: "mcp.status".to_string(),
params: ExternalAgentRunnerRequestParams {
root: Some(directory.0.to_string_lossy().into_owned()),
..ExternalAgentRunnerRequestParams::default()
},
},
&state,
);
assert!(!mcp_response.ok);
assert_eq!(
mcp_response.error.as_ref().map(|error| error.code.as_str()),
Some("runner-draining")
);
}
#[test]
@@ -16,6 +16,7 @@ enum SwarmChatInput {
Status,
History,
Compact,
Mcp,
Goal(SwarmGoalCommand),
InvalidGoal(String),
Quit,
@@ -222,6 +223,7 @@ fn run_game_creator_swarm_chat_with_input<W: Write>(
SwarmChatInput::Compact => {
handle_swarm_context_compaction(root, parent_agent_id, output)?
}
SwarmChatInput::Mcp => print_swarm_mcp_status(root, output)?,
SwarmChatInput::Goal(command) => {
let mut observer = SwarmRuntimeObserver::seed(root)?;
let Some(observation) =
@@ -403,6 +405,9 @@ fn prompt_swarm_decision<W: Write>(
handle_swarm_context_compaction(root, parent_agent_id, output)?;
return Ok(SwarmPromptDecision::Deferred);
}
SwarmChatInput::Mcp => {
print_swarm_mcp_status(root, output)?;
}
_ => {}
}
}
@@ -446,6 +451,7 @@ fn parse_swarm_chat_input(input: &str) -> Option<SwarmChatInput> {
"/status" => SwarmChatInput::Status,
"/history" => SwarmChatInput::History,
"/compact" => SwarmChatInput::Compact,
"/mcp" => SwarmChatInput::Mcp,
"/quit" | "/exit" => SwarmChatInput::Quit,
value => SwarmChatInput::Message(value.to_string()),
})
@@ -493,6 +499,7 @@ fn print_swarm_chat_help<W: Write>(output: &mut W) -> Result<(), String> {
.and_then(|_| writeln!(output, "/status 查看全部 Runtime 状态"))
.and_then(|_| writeln!(output, "/history 查看父 Agent 当前 Session 历史"))
.and_then(|_| writeln!(output, "/compact 压缩父 Agent 当前空闲 Session 历史"))
.and_then(|_| writeln!(output, "/mcp 查看 Runner MCP server 与工具目录"))
.and_then(|_| writeln!(output, "/goal <目标> 启动当前 Session 的持久 Goal"))
.and_then(|_| writeln!(output, "/goal 查看当前 Goal"))
.and_then(|_| writeln!(output, "/goal status 查看当前 Goal"))
@@ -505,6 +512,75 @@ fn print_swarm_chat_help<W: Write>(output: &mut W) -> Result<(), String> {
.map_err(|error| format!("写入终端失败:{error}"))
}
fn print_swarm_mcp_status<W: Write>(root: &Path, output: &mut W) -> Result<(), String> {
match read_external_agent_runner_mcp_catalog(root) {
Ok(catalog) => print_swarm_mcp_catalog(&catalog, output),
Err(error) => writeln!(output, "[MCP] 状态读取失败:{error}")
.map_err(|write_error| format!("写入终端失败:{write_error}")),
}
}
fn print_swarm_mcp_catalog<W: Write>(
catalog: &GameCreatorMcpCatalog,
output: &mut W,
) -> Result<(), String> {
writeln!(
output,
"[MCP] catalog={} servers={} tools={}",
catalog.fingerprint.chars().take(12).collect::<String>(),
catalog.servers.len(),
catalog.tools.len(),
)
.map_err(|error| format!("写入终端失败:{error}"))?;
for server in &catalog.servers {
writeln!(
output,
" server={} transport={} enabled={} connected={} required={} tools={}{}",
server.server_id,
server.transport,
server.enabled,
server.connected,
server.required,
server.tool_count,
server
.error
.as_deref()
.map(|error| format!(" error={error}"))
.unwrap_or_default(),
)
.map_err(|error| format!("写入终端失败:{error}"))?;
}
for tool in &catalog.tools {
let description = sanitize_prompt_context(&tool.description)
.chars()
.take(180)
.collect::<String>()
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
writeln!(
output,
" tool={}/{} approval={} readOnly={} schema={}{}",
tool.server_id,
tool.name,
tool.effective_approval_mode,
tool.read_only_hint,
serde_json::to_string(&tool.input_schema)
.unwrap_or_else(|_| "{}".to_string())
.chars()
.take(600)
.collect::<String>(),
if description.is_empty() {
String::new()
} else {
format!(" description={description}")
},
)
.map_err(|error| format!("写入终端失败:{error}"))?;
}
Ok(())
}
fn handle_swarm_context_compaction<W: Write>(
root: &Path,
parent_agent_id: &str,
@@ -900,6 +976,7 @@ fn wait_for_swarm_turn<W: Write>(
SwarmChatInput::Compact => {
handle_swarm_context_compaction(root, parent_agent_id, output)?
}
SwarmChatInput::Mcp => print_swarm_mcp_status(root, output)?,
SwarmChatInput::Goal(command) => {
let _ = handle_swarm_goal_command(root, parent_agent_id, command, output)?;
stable_since = None;
@@ -1771,6 +1848,7 @@ mod tests {
parse_swarm_chat_input("/compact"),
Some(SwarmChatInput::Compact)
);
assert_eq!(parse_swarm_chat_input("/mcp"), Some(SwarmChatInput::Mcp));
assert_eq!(
parse_swarm_chat_input("让策划和程序并行检查玩法"),
Some(SwarmChatInput::Message(
@@ -1840,6 +1918,7 @@ mod tests {
for command in [
"/status",
"/compact",
"/mcp",
"/goal <目标>",
"/goal status",
"/goal edit <目标>",
@@ -1851,6 +1930,55 @@ mod tests {
}
}
#[test]
fn swarm_mcp_catalog_is_bounded_and_omits_server_instructions() {
let instruction = "PRIVATE_MCP_SERVER_INSTRUCTIONS";
let catalog = GameCreatorMcpCatalog {
fingerprint: "a".repeat(64),
servers: vec![GameCreatorMcpServerStatus {
server_id: "fixture".to_string(),
enabled: true,
required: false,
transport: "stdio".to_string(),
connected: true,
server_name: Some("fixture-server".to_string()),
server_version: Some("1.0.0".to_string()),
instructions: instruction.to_string(),
instructions_chars: instruction.chars().count(),
tool_count: 1,
error: None,
}],
tools: vec![GameCreatorMcpCatalogTool {
server_id: "fixture".to_string(),
name: "lookup".to_string(),
title: Some("Fixture lookup".to_string()),
description: format!("{} DESCRIPTION_TAIL_SENTINEL", "D".repeat(240)),
input_schema: serde_json::json!({
"type": "object",
"description": format!("{} SCHEMA_TAIL_SENTINEL", "S".repeat(800)),
}),
output_schema: None,
read_only_hint: true,
destructive_hint: false,
open_world_hint: false,
configured_approval_mode: "auto".to_string(),
effective_approval_mode: "auto".to_string(),
fingerprint: "b".repeat(64),
}],
};
let mut output = Vec::new();
print_swarm_mcp_catalog(&catalog, &mut output).expect("print MCP catalog");
let output = String::from_utf8(output).expect("MCP output is utf-8");
assert!(output.contains("catalog=aaaaaaaaaaaa servers=1 tools=1"));
assert!(output.contains("server=fixture transport=stdio"));
assert!(output.contains("tool=fixture/lookup approval=auto readOnly=true"));
assert!(!output.contains(instruction));
assert!(!output.contains("DESCRIPTION_TAIL_SENTINEL"));
assert!(!output.contains("SCHEMA_TAIL_SENTINEL"));
assert!(output.len() < 1_200);
}
#[test]
fn goal_status_prints_identity_outcome_and_completion_standard() {
let goal = AgentGoalRecord {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,272 @@
import { appendFileSync } from 'node:fs';
import http from 'node:http';
import readline from 'node:readline';
const args = process.argv.slice(2);
const mode = args[0] ?? 'stdio';
const failList = args.includes('--fail-list');
const listDelayArgument = args.find((value) =>
value.startsWith('--list-delay-ms='),
);
const listDelayMs = Math.max(
0,
Number(listDelayArgument?.slice('--list-delay-ms='.length) ?? '0') || 0,
);
const mutateResponseDelayArgument = args.find((value) =>
value.startsWith('--mutate-response-delay-ms='),
);
const mutateResponseDelayMs = Math.max(
0,
Number(
mutateResponseDelayArgument?.slice('--mutate-response-delay-ms='.length) ??
'0',
) || 0,
);
const markerArgument = args.find((value) => value.startsWith('--marker='));
const markerPath = markerArgument?.slice('--marker='.length) ?? '';
const bearerTokenArgument = args.find((value) =>
value.startsWith('--bearer-token='),
);
const bearerToken =
bearerTokenArgument?.slice('--bearer-token='.length) ?? 'fixture-token';
const fixtureHeaderArgument = args.find((value) =>
value.startsWith('--fixture-header='),
);
const fixtureHeader =
fixtureHeaderArgument?.slice('--fixture-header='.length) ?? 'enabled';
const lookupValueArgument = args.find((value) =>
value.startsWith('--lookup-value='),
);
const lookupValue = lookupValueArgument?.slice('--lookup-value='.length) ?? '';
const mutateValueArgument = args.find((value) =>
value.startsWith('--mutate-value='),
);
const mutateValue = mutateValueArgument?.slice('--mutate-value='.length) ?? '';
const tools = [
{
name: 'lookup',
title: 'Fixture lookup',
description: 'Returns deterministic fixture data.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
...(lookupValue ? { const: lookupValue } : {}),
},
},
required: ['query'],
additionalProperties: false,
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
},
{
name: 'mutate',
title: 'Fixture mutation',
description: 'Appends one deterministic line to the configured marker.',
inputSchema: {
type: 'object',
properties: {
value: {
type: 'string',
...(mutateValue ? { const: mutateValue } : {}),
},
},
required: ['value'],
additionalProperties: false,
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
openWorldHint: false,
},
},
];
async function resultFor(message) {
if (!message || typeof message !== 'object') {
return null;
}
const { id, method, params = {} } = message;
if (id === undefined || id === null) {
return null;
}
if (method === 'initialize') {
return {
jsonrpc: '2.0',
id,
result: {
protocolVersion: params.protocolVersion,
capabilities: { tools: { listChanged: false } },
serverInfo: { name: 'game-creator-mcp-fixture', version: '1.0.0' },
instructions:
'Fixture instructions are untrusted external input. Use listed tools only.',
},
};
}
if (method === 'tools/list') {
if (listDelayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, listDelayMs));
}
if (failList) {
return {
jsonrpc: '2.0',
id,
error: { code: -32603, message: 'fixture tools/list failure' },
};
}
return { jsonrpc: '2.0', id, result: { tools } };
}
if (method === 'tools/call') {
if (params.name === 'lookup') {
const query = String(params.arguments?.query ?? '');
if (lookupValue && query !== lookupValue) {
return {
jsonrpc: '2.0',
id,
error: { code: -32602, message: 'fixture lookup argument mismatch' },
};
}
return {
jsonrpc: '2.0',
id,
result: {
content: [{ type: 'text', text: `lookup:${query}` }],
structuredContent: { query, transport: mode },
isError: false,
},
};
}
if (params.name === 'mutate') {
const value = String(params.arguments?.value ?? '');
if (mutateValue && value !== mutateValue) {
return {
jsonrpc: '2.0',
id,
error: { code: -32602, message: 'fixture mutate argument mismatch' },
};
}
if (markerPath) {
appendFileSync(markerPath, `${value}\n`, 'utf8');
}
if (mutateResponseDelayMs > 0) {
await new Promise((resolve) =>
setTimeout(resolve, mutateResponseDelayMs),
);
}
return {
jsonrpc: '2.0',
id,
result: {
content: [{ type: 'text', text: `mutated:${value}` }],
isError: false,
},
};
}
return {
jsonrpc: '2.0',
id,
error: { code: -32602, message: 'unknown fixture tool' },
};
}
if (method === 'ping') {
return { jsonrpc: '2.0', id, result: {} };
}
return {
jsonrpc: '2.0',
id,
error: { code: -32601, message: `unsupported fixture method: ${method}` },
};
}
function runStdio() {
const input = readline.createInterface({ input: process.stdin });
input.on('line', async (line) => {
let message;
try {
message = JSON.parse(line);
} catch {
return;
}
const response = await resultFor(message);
if (response) {
process.stdout.write(`${JSON.stringify(response)}\n`);
}
});
}
function readRequestBody(request) {
return new Promise((resolve, reject) => {
const chunks = [];
request.on('data', (chunk) => chunks.push(chunk));
request.on('end', () => {
try {
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
} catch (error) {
reject(error);
}
});
request.on('error', reject);
});
}
function runHttp() {
const requestedPort = Number(args[1] ?? '0');
const server = http.createServer(async (request, response) => {
if (request.url !== '/mcp') {
response.writeHead(404).end();
return;
}
if (request.method === 'GET') {
response.writeHead(405, { Allow: 'POST, DELETE' }).end();
return;
}
if (request.method === 'DELETE') {
response.writeHead(200).end();
return;
}
if (request.method !== 'POST') {
response.writeHead(405, { Allow: 'POST, DELETE' }).end();
return;
}
if (
request.headers.authorization !== `Bearer ${bearerToken}` ||
request.headers['x-mcp-fixture'] !== fixtureHeader
) {
response.writeHead(401).end();
return;
}
let message;
try {
message = await readRequestBody(request);
} catch {
response.writeHead(400).end();
return;
}
const result = await resultFor(message);
if (!result) {
response.writeHead(202, { 'Mcp-Session-Id': 'fixture-session' }).end();
return;
}
response.writeHead(200, {
'Content-Type': 'application/json',
'Mcp-Session-Id': 'fixture-session',
});
response.end(JSON.stringify(result));
});
server.listen(requestedPort, '127.0.0.1', () => {
const address = server.address();
process.stdout.write(`${JSON.stringify({ port: address.port })}\n`);
});
}
if (mode === 'http') {
runHttp();
} else {
runStdio();
}
File diff suppressed because it is too large Load Diff
+270 -18
View File
@@ -1051,7 +1051,10 @@ textarea {
white-space: nowrap;
}
.launcher-agent-chat-page > header .launcher-project-list-actions button:last-child {
.launcher-agent-chat-page
> header
.launcher-project-list-actions
button:last-child {
border-color: #111827;
background: #111827;
color: #fff;
@@ -1674,10 +1677,8 @@ textarea {
.launcher-development-canvas {
display: grid;
min-height: 300px;
background:
linear-gradient(#f3f4f6 1px, transparent 1px),
linear-gradient(90deg, #f3f4f6 1px, transparent 1px),
#fff;
background: linear-gradient(#f3f4f6 1px, transparent 1px),
linear-gradient(90deg, #f3f4f6 1px, transparent 1px), #fff;
background-size: 24px 24px;
place-items: center;
}
@@ -1740,28 +1741,24 @@ textarea {
}
.launcher-card-collage span:nth-child(1) {
background:
radial-gradient(circle at 45% 38%, #6b5c43 0 18%, transparent 19%),
background: radial-gradient(circle at 45% 38%, #6b5c43 0 18%, transparent 19%),
radial-gradient(circle at 62% 54%, #2f2a23 0 25%, transparent 26%),
linear-gradient(135deg, #91a17e, #2c3327);
}
.launcher-card-collage span:nth-child(2) {
background:
radial-gradient(circle at 58% 45%, #b28b5a 0 16%, transparent 17%),
background: radial-gradient(circle at 58% 45%, #b28b5a 0 16%, transparent 17%),
radial-gradient(circle at 48% 55%, #302114 0 24%, transparent 25%),
linear-gradient(135deg, #d9e8ef, #9fb0a0);
}
.launcher-card-collage span:nth-child(3) {
background:
radial-gradient(circle at 50% 54%, #eac9c0 0 22%, transparent 23%),
background: radial-gradient(circle at 50% 54%, #eac9c0 0 22%, transparent 23%),
linear-gradient(135deg, #f7d9d5, #dab6aa);
}
.launcher-card-collage span:nth-child(4) {
background:
radial-gradient(circle at 56% 50%, #32271b 0 22%, transparent 23%),
background: radial-gradient(circle at 56% 50%, #32271b 0 22%, transparent 23%),
linear-gradient(135deg, #8e6c4e, #2b211b);
}
@@ -1785,8 +1782,12 @@ textarea {
.launcher-card-pattern > div {
display: grid;
grid-template-columns: repeat(2, 1fr);
background:
linear-gradient(45deg, transparent 46%, #fff 46% 54%, transparent 54%),
background: linear-gradient(
45deg,
transparent 46%,
#fff 46% 54%,
transparent 54%
),
linear-gradient(135deg, #55405a, #e4b2a7);
}
@@ -1840,7 +1841,10 @@ textarea {
font-size: 11px;
}
.launcher-promo span:not(.launcher-countdown):not(:nth-child(3)):not(:nth-child(6)):not(:nth-child(9)) {
.launcher-promo
span:not(.launcher-countdown):not(:nth-child(3)):not(:nth-child(6)):not(
:nth-child(9)
) {
display: none;
}
@@ -2227,7 +2231,7 @@ h2 {
.settings-overlay {
position: fixed;
inset: 0;
z-index: 10;
z-index: 220;
display: grid;
place-items: center;
padding: 18px;
@@ -2237,7 +2241,7 @@ h2 {
.settings-panel {
display: grid;
gap: 10px;
width: min(760px, 100%);
width: min(920px, 100%);
max-height: calc(100vh - 36px);
padding: 18px;
overflow: auto;
@@ -2282,6 +2286,246 @@ h2 {
padding: 0;
}
.runtime-mcp-section {
grid-column: 1 / -1;
display: grid;
gap: 10px;
min-width: 0;
padding-top: 14px;
border-top: 1px solid #e2e7ef;
}
.runtime-mcp-header,
.runtime-mcp-server > header,
.runtime-mcp-catalog > header,
.runtime-mcp-tools article > div {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.runtime-mcp-header > div,
.runtime-mcp-server > header > div {
display: grid;
gap: 3px;
min-width: 0;
}
.runtime-mcp-header h3 {
margin: 0;
color: #18202f;
font-size: 14px;
}
.runtime-mcp-header span,
.runtime-mcp-server span,
.runtime-mcp-catalog span {
color: #647084;
font-size: 12px;
}
.runtime-mcp-header button,
.runtime-mcp-add button,
.runtime-mcp-server > header button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 32px;
padding: 0 10px;
border: 1px solid #cfd7e6;
border-radius: 6px;
color: #18202f;
background: #fff;
}
.runtime-mcp-header button:disabled,
.runtime-mcp-add button:disabled,
.runtime-mcp-server > header button:disabled {
color: #97a1b1;
background: #f5f7fa;
}
.runtime-mcp-add {
display: grid;
grid-template-columns: minmax(0, 1fr) 36px;
align-items: end;
gap: 8px;
}
.runtime-mcp-add label {
display: grid;
gap: 6px;
min-width: 0;
color: #647084;
font-size: 13px;
}
.runtime-mcp-add input {
min-width: 0;
height: 36px;
padding: 0 10px;
border: 1px solid #cfd7e6;
border-radius: 6px;
color: #18202f;
background: #fff;
}
.runtime-mcp-add button {
width: 36px;
padding: 0;
}
.runtime-mcp-list,
.runtime-mcp-tools {
display: grid;
gap: 8px;
min-width: 0;
}
.runtime-mcp-server {
min-width: 0;
padding: 10px 12px;
border: 1px solid #dce3ee;
border-radius: 7px;
background: #fbfcfe;
}
.runtime-mcp-server > header strong,
.runtime-mcp-catalog strong,
.runtime-mcp-tools strong {
min-width: 0;
color: #18202f;
font-size: 13px;
overflow-wrap: anywhere;
}
.runtime-mcp-server > header button {
width: 30px;
height: 30px;
padding: 0;
color: #a13737;
}
.runtime-mcp-state.is-connected {
color: #157347;
}
.runtime-mcp-server details {
margin-top: 8px;
border-top: 1px solid #e2e7ef;
}
.runtime-mcp-server summary {
padding: 9px 0 1px;
color: #475569;
font-size: 12px;
cursor: pointer;
user-select: none;
}
.runtime-mcp-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
padding-top: 10px;
}
.runtime-mcp-grid label {
display: grid;
gap: 6px;
min-width: 0;
color: #647084;
font-size: 13px;
}
.runtime-mcp-grid input,
.runtime-mcp-grid select,
.runtime-mcp-grid textarea {
min-width: 0;
width: 100%;
border: 1px solid #cfd7e6;
border-radius: 6px;
color: #18202f;
background: #fff;
}
.runtime-mcp-grid input,
.runtime-mcp-grid select {
height: 36px;
padding: 0 10px;
}
.runtime-mcp-grid textarea {
min-height: 84px;
padding: 8px 10px;
resize: vertical;
font:
12px/1.5 ui-monospace,
SFMono-Regular,
Consolas,
monospace;
}
.runtime-mcp-grid .settings-checkbox {
grid-template-columns: 18px minmax(0, 1fr);
align-self: end;
min-height: 36px;
}
.runtime-mcp-grid .settings-checkbox input {
width: 18px;
height: 18px;
padding: 0;
}
.runtime-mcp-field-wide {
grid-column: 1 / -1;
}
.runtime-mcp-empty {
padding: 12px;
border: 1px dashed #cfd7e6;
border-radius: 6px;
color: #647084;
font-size: 12px;
text-align: center;
}
.runtime-mcp-catalog {
display: grid;
gap: 8px;
min-width: 0;
padding: 10px 12px;
border: 1px solid #d7e3da;
border-radius: 7px;
background: #f8fcf9;
}
.runtime-mcp-tools article {
display: grid;
gap: 7px;
min-width: 0;
padding-top: 8px;
border-top: 1px solid #dce8df;
}
.runtime-mcp-tools pre {
max-height: 180px;
margin: 0;
padding: 8px;
overflow: auto;
border: 1px solid #dce3ee;
border-radius: 6px;
color: #263142;
background: #fff;
font-size: 11px;
line-height: 1.5;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.message-list {
flex: 1;
border: 1px solid #dde3ee;
@@ -2821,6 +3065,14 @@ iframe.preview-frame {
grid-template-columns: 1fr;
}
.runtime-mcp-grid {
grid-template-columns: 1fr;
}
.runtime-mcp-field-wide {
grid-column: auto;
}
.composer {
grid-template-columns: 1fr auto;
}
@@ -5878,6 +5878,183 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(screen.queryByRole('dialog', { name: '运行时配置' })).toBeNull();
});
it('configures MCP servers and reads the bounded Runner catalog', async () => {
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'read_game_creator_app_config') {
return {
path: '/home/test/AppData/game-creator.config.json',
config: {
llm: {
apiKey: '',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4.1',
apiKind: 'openai_responses',
reasoningEffort: 'high',
stream: true,
webSearchEnabled: false,
requestTimeoutMs: 180000,
maxRetries: 0,
retryBackoffMs: 500,
},
agentLlm: {},
editorApi: {
baseUrl: 'http://127.0.0.1:8082',
apiKey: '',
},
mcpServers: {
local: {
enabled: true,
required: false,
transport: 'stdio',
command: 'node',
args: ['fixture.mjs'],
cwd: '',
env: { MCP_FIXTURE_TOKEN: 'loaded-mcp-secret' },
url: '',
bearerToken: '',
httpHeaders: {},
allowInsecureLocalhost: false,
startupTimeoutMs: 10000,
toolTimeoutMs: 60000,
enabledTools: [],
disabledTools: [],
defaultApprovalMode: 'confirm',
tools: {},
},
},
},
};
}
if (command === 'write_game_creator_app_config') {
return {
path: '/home/test/AppData/game-creator.config.json',
config: args?.config,
};
}
if (command === 'read_game_creator_mcp_catalog') {
return {
fingerprint: 'catalog-fingerprint',
servers: [
{
serverId: 'local',
enabled: true,
required: false,
transport: 'streamableHttp',
connected: true,
serverName: 'fixture',
serverVersion: '1.0.0',
instructionsChars: 37,
toolCount: 1,
error: null,
},
],
tools: [
{
serverId: 'local',
name: 'lookup',
title: 'Lookup',
description: 'fixture tool',
inputSchema: {
type: 'object',
properties: { query: { type: 'string' } },
},
readOnlyHint: true,
effectiveApprovalMode: 'auto',
},
],
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderLauncherAt('/?launcher');
fireEvent.click(screen.getByRole('button', { name: '配置' }));
const mcpRegion = await screen.findByRole('region', {
name: 'MCP servers',
});
const serverCard = within(mcpRegion)
.getByText('local')
.closest('article') as HTMLElement;
fireEvent.click(within(serverCard).getByText('配置'));
fireEvent.change(within(serverCard).getByLabelText('local Transport'), {
target: { value: 'streamableHttp' },
});
fireEvent.change(within(serverCard).getByLabelText('local URL'), {
target: { value: 'http://127.0.0.1:43123/mcp' },
});
const bearerToken = within(serverCard).getByLabelText('local Bearer token');
expect(bearerToken).toHaveProperty('type', 'password');
expect(bearerToken.getAttribute('autocomplete')).toBe('off');
fireEvent.change(bearerToken, {
target: { value: 'new-mcp-secret' },
});
fireEvent.click(
within(serverCard).getByLabelText('local 允许不安全 localhost HTTP'),
);
fireEvent.change(
within(serverCard).getByLabelText('local HTTP headers JSON'),
{ target: { value: '{bad json' } },
);
fireEvent.click(screen.getByRole('button', { name: '保存' }));
expect(
await screen.findByText('MCP local 的 httpHeaders 不是有效 JSON'),
).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'write_game_creator_app_config',
expect.anything(),
);
fireEvent.change(
within(serverCard).getByLabelText('local HTTP headers JSON'),
{ target: { value: '{"X-Fixture":"enabled"}' } },
);
fireEvent.change(within(serverCard).getByLabelText('local 允许工具 JSON'), {
target: { value: '["lookup"]' },
});
fireEvent.change(within(serverCard).getByLabelText('local 工具覆盖 JSON'), {
target: {
value: '{"lookup":{"enabled":true,"approvalMode":"auto"}}',
},
});
fireEvent.click(screen.getByRole('button', { name: '保存' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', {
config: expect.objectContaining({
mcpServers: {
local: expect.objectContaining({
transport: 'streamableHttp',
url: 'http://127.0.0.1:43123/mcp',
bearerToken: 'new-mcp-secret',
httpHeaders: { 'X-Fixture': 'enabled' },
allowInsecureLocalhost: true,
enabledTools: ['lookup'],
defaultApprovalMode: 'confirm',
tools: {
lookup: { enabled: true, approvalMode: 'auto' },
},
}),
},
}),
});
});
fireEvent.click(screen.getByRole('button', { name: '测试连接' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('read_game_creator_mcp_catalog', {
projectPath: '/tmp/genarrative-ai-game-draft',
});
});
expect(await screen.findByText('已连接 · 1 个工具')).not.toBeNull();
expect(screen.getByRole('region', { name: 'MCP 连接状态' })).not.toBeNull();
expect(screen.getByText('local / Lookup')).not.toBeNull();
expect(screen.queryByText('loaded-mcp-secret')).toBeNull();
});
it('keeps runtime config open when Escape is pressed in an input', async () => {
const invoke = vi.fn(async (command: string) => {
if (command === 'read_game_creator_app_config') {
@@ -18780,6 +18957,7 @@ describe('AI 游戏创作 App 界面边界', () => {
baseUrl: 'http://127.0.0.1:8099',
apiKey: 'editor-new-secret',
},
mcpServers: {},
},
});
fireEvent.click(screen.getByRole('button', { name: '读取' }));
@@ -18884,6 +19062,7 @@ describe('AI 游戏创作 App 界面边界', () => {
baseUrl: 'http://127.0.0.1:8082',
apiKey: '',
},
mcpServers: {},
},
});
});
@@ -4629,6 +4629,16 @@
- 实现收口:显式用户约束由确定性保留层逐字钉住并继续做凭据/绝对路径脱敏;`runtime.compact` 单独使用 6 分钟 IPC 响应窗口,其他 Runner 方法仍为 10 秒;普通后台任务公共审计只保存 `taskChars + taskSha256`;终态旧 bundle 只有在完整身份、Goal、revision、verification、observation、sidecar、steer 校验通过后才可刷新 legacy plan 投影。
- 真实验收:2026-07-15 正式 `openai_chat / gpt-5.5` 路由的隔离 `context-compaction` suite PASS。30/30 轮、两次 compaction revision、一次 pidfd Runner 强杀恢复、早期约束召回和 29134/64000 最大估算输入均满足;30 个 tool-plan 与 2 个 compaction lifecycle 唯一闭合,fallback replay、重复 message/audit、工具重放和公共正文/summary/API Key/诱饵/项目路径/正式配置路径泄漏均为 0。首轮第 22 轮 Provider transport 终态按规则 FAIL 且零重放,新 disposable 项目完整重跑取得 PASS,全部一次性现场已按 sentinel 清理。
## 2026-07-15 AI 游戏创作 Agent Runtime V1.22 Runner-owned MCP 动态工具
- 决策:在 V1.21 token-aware compaction 之后接入 MCP;不新建平行 Agent 或绕开 Runtime 的直连工具层。使用官方 Rust SDK `rmcp`,首切片同时覆盖 STDIO 与 Streamable HTTP、server instructions、Bearer/static header、工具 allow/deny 和工具级审批;OAuth、resources/prompts、sampling、elicitation 与 task-mode 后续继续扩展,当前不得伪装已支持。
- 配置:`mcpServers` 只存 AppData,不使用 `.env` 或宿主环境凭据回退。STDIO 可执行文件从项目外受信任 PATH 解析并清空继承环境;HTTP 默认只允许 HTTPS,显式开关才允许 loopback HTTP。正式默认零 server,敏感字段不进入状态、日志、报告或普通用户 UI。
- 模型目录:Runner initialize 后刷新 `tools/list`,应用 allow/deny、数量和 schema 总预算,再把有界 instructions、description 与真实 input schema 注入 `submit_agent_tool_plan``mcp.call` 目录。catalog/tool fingerprint 由 Runtime 注入并绑定配置、server info、instructions、schema、annotations 与 execution metadata,模型不能伪造。
- 权限与恢复:`mcp.call` 进入共享命令契约;项目/Agent policy 和 server/tool 配置保守叠加,隔离 child 默认禁止。所有调用复用现有 durable action 的 `approved -> executing -> observed`、确认、steer、Goal 与 reconciliationplanning 后目录漂移回同 run replanexecuting 后结果未知或 Runner 退出禁止重发。
- 隐私:完整 `CallToolResult` 只写 `.agent/runtime/mcp-results` 私有 sidecar;模型只收到 token-bounded text/structured content,二进制只给 MIME/大小/哈希。公共面只保存 server/tool、审批、状态、参数/结果计数和指纹、相对 sidecar 路径及安全错误分类,禁止 arguments、正文、instructions、凭据和绝对路径。
- 验收:STDIO 与 Streamable HTTP fixture 都必须由真实 Provider 发现并调用;有副作用 fixture 还要覆盖确认和 Runner 强杀未知窗口,证明唯一调用、零自动重放、结果回灌、同一 Agent/Session/run 以及全部公共零正文/零凭据泄漏。
- 真实验收:2026-07-15 正式 `openai_chat / gpt-5.5` 路由的隔离 `mcp-runtime` suite PASS。正常 run 的 STDIO lookup、HTTP lookup、确认后 STDIO mutate 各执行 1 次,action/私有 sidecar/terminal receipt 各 3、assistant 1HTTP mutate 副作用后 pidfd 强杀 Runner,恢复只产生 reconciliation 1marker 1sidecar/receipt/assistant/重复调用均为 0。公共 arguments、结果正文、instructions、Bearer/static header、API Key、项目和配置绝对路径泄漏均为 0;确定性 MCP 15/15、Tauri 全量 800 passed / 4 ignored,隔离 Runner、fixture、AppData 和项目已清理。
## 2026-07-13 普通微信支付 V3 退款使用统一观察事务闭环
- 背景:普通微信支付 V3 的退款申请响应、退款结果回调、主动查单和商户平台手工退款发现可能重复、乱序或只出现其中一种;原充值订单只有单一终态,无法表达多次部分退款、权益回收欠款和会员人工处理。
@@ -901,12 +901,48 @@ V1.21 对标 Codex CLI 的 `model_context_window`、`model_auto_compact_token_li
真实长链收口时同步修正四项实现边界:显式用户约束由确定性保留层逐字钉住并继续做密钥/绝对路径脱敏;`runtime.compact` 单独使用 6 分钟 IPC 响应窗口,其他 Runner 方法仍保持 10 秒;普通后台任务公共审计只保存 `taskChars + taskSha256`,任务正文仅留在私有 task ledger/conversation;每 6 轮只做进度 checkpoint 与停滞检测,真正摘要只由 token 阈值或显式 `/compact` 触发。终态旧 context bundle 仅允许在完整 schema、身份、Goal、revision、verification、observation、sidecar 和 steer 校验通过后刷新 legacy plan 投影差异。
## V1.22 Runner-owned MCP 动态工具
V1.22 对标 Codex CLI 的 MCP tool 能力,在现有单 Agent Runtime 内增加动态外部工具目录,不新建平行 Agent、平行工具执行器或绕过权限的直连入口。实现使用官方 Rust MCP SDK `rmcp`;首个纵向切片覆盖 STDIO 与 Streamable HTTP、server instructions、Bearer/static header、工具 allow/deny 和工具级审批。OAuth、MCP resources/prompts、sampling、elicitation 与 task-mode 调用不在本切片宣称范围内,后续按同一 Runner 继续扩展。
### AppData 配置
- `game-creator.config.json` 新增 `mcpServers.<serverId>`。serverId 必须是 1-64 个 ASCII 字母、数字、点、下划线或连字符;配置包含 `enabled / required / transport / startupTimeoutMs / toolTimeoutMs / enabledTools / disabledTools / defaultApprovalMode / tools`。正式默认不启用任何外部 server。
- `transport=stdio` 必须配置裸可执行名 `command`,可选 `args / cwd / env`Runtime 从项目外受信任 PATH 解析可执行文件,清空继承环境,只注入安全 PATH、配置中的 env 与 Windows 启动所需的最小系统变量。显式 cwd 必须是已存在的项目外绝对目录;省略时固定使用已解析可执行文件所在目录,不继承 Runner 或 owning project cwd。命令、参数、env 名和值都受数量、字符和总字节上限约束;项目配置、`.env` 和宿主环境变量不作为凭据回退。
- `transport=streamableHttp` 必须配置 `https://` URL,只有显式 `allowInsecureLocalhost=true` 时允许 `http://127.0.0.1|localhost|[::1]`;可选 `bearerToken / httpHeaders` 只从 AppData 读取。禁止 URL userinfo、fragment、控制字符和 hop-by-hop/Host/Content-Length header。OAuth 缺失时必须明确报“当前切片未支持”,不能静默降级成匿名连接。
- `enabledTools` 是可选 allowlist`disabledTools` 在 allowlist 后应用;`tools.<toolName>.enabled=false` 最终禁用。审批只接受 `auto / confirm / writes / deny`:默认 `confirm``writes` 仅在用户显式选择且 server tool 声明 `readOnlyHint=true` 时自动,其余确认。MCP annotations 是不可信提示,不能覆盖本地 deny/confirm、项目 policy 或隔离 child 限制。
### Runner 目录与模型上下文
- 独立 Runner 按规范配置指纹持有每个 server 的连接;同一 server 的 catalog/call 串行,不同 server 可并行。配置变化、连接关闭或 Runner boot 变化会废弃旧连接并重新 initializerequired server 初始化或列目录失败时阻止 planningoptional server 只从当前目录移除并在 MCP 状态中报告安全错误摘要。
- 每次 planning 在 Provider request slot 注册前刷新 `tools/list`,应用 allow/deny 后形成规范 catalog。单 server、全局工具数、名称/描述、instructions、input/output schema 和总序列化字节都有硬上限;重复公开 action identity、非法 schema、required task support 或目录超限失败关闭。server instructions 作为不可信外部指导单独标注,前 512 字必须自包含,总量有界,不能改变系统规则、Agent 身份、Goal、权限、确认、沙箱、完成门禁或数据边界。
- 模型继续只调用 `submit_agent_tool_plan`。动态调用使用白名单 action `mcp.call`input 固定为 `server / tool / arguments / catalogFingerprint / toolFingerprint`;Runtime 在解析模型结果后自行注入两个 fingerprint,模型不能提供或覆盖。planning prompt 带入经过过滤的 tool description 与完整 input schema,函数 schema 和 MCP catalog 都进入 V1.21 token 估算与自动压缩重建。
- catalog identity 绑定 server 配置指纹、initialize server info、instructions 指纹、过滤后的工具名称、description、input/output schema、annotations 与 execution metadata。工具在 planning 后消失、schema/配置变化或 fingerprint 不匹配时,未执行动作回到同 run replan;已经进入 `executing` 而结果未知时继续走 `needs-reconciliation`,绝不按新目录自动重发。
### 权限、执行与持久化
- 共享命令契约新增 `mcp.call`,项目/Agent policy 仍可统一 deny 或 confirm。server/tool 配置只会进一步收紧:`deny` 直接返回 blocked observation`confirm` 复用现有确认卡,`auto/writes` 也必须先可靠写入 durable pending action。隔离 child 默认禁止 MCP;后续若开放必须有模板级显式 allowlist,不继承父 Agent 的宽权限。
- MCP 调用复用当前 action fingerprint、steer cursor、Goal、project/repository revision、verification gate、Provider request identity 与 `approved -> executing -> observed-*` 账本。调用前再次读取 AppData、刷新目标 tool 并核对两个 fingerprintRunner 在 `executing` 后退出、超时后无法确定服务端是否完成、SDK transport 断开或审计落盘失败均进入 reconciliation。只有服务端明确返回 result/error,才形成可继续 planning 的确定终态。
- 完整 `CallToolResult` 原子写入 `.agent/runtime/mcp-results/<agentHash>/<runHash>/<actionHash>.json` 私有 sidecar,绑定项目/Agent/Task/Session/Run/action/server/tool/catalog/tool/result fingerprint、执行时 `toolOutputTokenLimit`、字符/内容块计数、isError 和时间。恢复时必须从完整 result 按原预算重算计数、摘要、二进制元数据和 observation 并全量比对;任一派生字段不一致都进入 reconciliation,不能接受局部损坏摘要。模型 observation 只读取受 `toolOutputTokenLimit` 约束的 text/structured contentimage/audio/embedded resource 只给类型、MIME、大小和 SHA-256 元数据,本切片不把任意 MCP 二进制转发给 Provider。
- 公共 Runtime state/event/task/Agent DB/receipt/activity/output/report 只保存 server/tool、审批、状态、参数字符数与 SHA-256、结果内容块计数/字符数与 SHA-256、sidecar 相对路径和安全错误分类,不保存 arguments、返回正文、Bearer/header/env、server instructions、绝对路径或 SDK 原始错误。`agent.action_history` 同样只返回该安全摘要。
### 开发入口与验收
- 开发配置面板管理 MCP server,敏感字段沿用密码输入;保存前完成本地结构校验,连接测试走 Runner,不由 WebView 直接联网或启动进程。`agc:chat` / `agc:swarm` 提供 `/mcp` 查看 server 状态和有界工具目录;正式用户 Supervisor 首页不展示 MCP 配置或调试正文,但 Runtime 可以按已配置策略使用工具。
- 确定性测试覆盖两种 transport、initialize/instructions/tools list、allow/deny、审批映射、动态 schema/token 预算、配置/catalog 漂移、required/optional 失败、超时、Runner 强杀、pending/reconciliation、结果 sidecar、二进制降级和全部公共零正文。
- 真实本地 E2E 使用一次性 STDIO fixture 与 Streamable HTTP fixture,各自让真实 Provider 发现并调用至少一个只读工具;再让一个有副作用 fixture 停在确认、批准后只执行一次,并在调用窗口强杀 Runner 证明零重放。报告必须证明 tool schema 来自 MCP、server instructions 被标为不可信、同一 Agent/Session/run 身份稳定、结果可回灌、重复调用/assistant 为 0,且凭据、arguments、结果正文、项目/配置绝对路径公共泄漏为 0。
2026-07-15 使用正式 AppData 的 `openai_chat / gpt-5.5` 路由和一次性隔离配置执行 `mcp-runtime` suiteV1.22 整体 **PASS**。正常 run 由真实 Provider 从动态 schema 发现并调用 STDIO lookup、Streamable HTTP lookup 和 STDIO mutate 各 1 次;mutate 在工具级确认后执行,形成 3 个唯一 action、3 份私有结果 sidecar、3 条 terminal receipt 和 1 条最终 assistant。第二个 run 在 HTTP mutate 已产生一次副作用、结果尚未返回 Runner 的确定性窗口通过 Linux pidfd 强杀 Runner;恢复后只形成 1 条 reconciliationmarker 保持 1 次,sidecar、receipt、assistant 和重复调用均为 0。
真实报告同时证明正常与强杀 run 的 Agent/Session/run 身份稳定,参数由 MCP schema `const` 提供而非用户任务正文;公共 task/event/Agent DB/receipt/conversation/report 中 arguments、结果正文、server instructions、Bearer/static header、API Key、项目绝对路径和配置绝对路径泄漏均为 0。Runner、HTTP fixture、隔离 AppData 和一次性项目全部按 sentinel 清理。确定性 MCP 组为 15/15Tauri 全量为 800 passed / 4 ignored;开发配置窗在 1440px 与 900px 真实浏览器视口均无横向溢出或侧栏遮挡,console 0 error / 0 warning。
## 验收命令
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml structured_plan_ -- --nocapture`
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml agent_goal_ -- --nocapture`
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml goal_context_bundle_v4_migrates_v3_and_v2_then_rejects_plan_mismatch -- --nocapture`
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml response_stream_ -- --nocapture`
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml mcp_ -- --nocapture`
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml swarm_cli::tests -- --nocapture`
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml typed_goal_pause_and_cancel_require_durable_intent_and_keep_exact_run -- --nocapture`
- `npm run ai-game-creator-shell:typecheck`
@@ -920,6 +956,7 @@ V1.21 对标 Codex CLI 的 `model_context_window`、`model_auto_compact_token_li
- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite response-stream`
- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite web-search`
- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite context-compaction`
- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite mcp-runtime`
- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite full`
- `npm run check:encoding`
- `git diff --check`
@@ -564,4 +564,6 @@ game-project/
- 2026-07-15 当前正式 `openai_chat / gpt-5.5` 路由的三轮真实联网专项均 FAIL:上游接受搜索开启请求并完成 lifecycle,但模型没有获得原生搜索能力,无法命中动态 GitHub release baseline。客户端能力已落地但该路由不可启用;最终复验使用正式 AppData 同级的 `0600` 私有配置副本,源配置 inode/nlink/timestamps/hash 前后完全一致,隔离 Runner/AppData/项目和全部泄漏门禁均安全收束。
- 2026-07-15 起,同一 Runtime 文档的“V1.21 单 Agent token-aware 持久上下文压缩”作为长会话预算事实源。全局/per-Agent LLM 配置提供 context window、自动压缩阈值和工具输出 token 限额;后台 planning 超阈值时只压缩旧 conversation/observation prefixGoal、任务、计划、steer、pending、verification 与副作用身份逐字段保留。开发 Agent 窗口与 `agc:chat` 提供同一安全 `/compact`,正式用户首页不新增控制项。私有 sidecar、context bundle 绑定、Provider orphan barrier、公共零正文和 30 轮真实长链路按 Runtime V1.1 的 V1.21 章节验收。
- 2026-07-15 V1.21 已落地并完成真实验收:`context-compaction` suite 在正式 `openai_chat / gpt-5.5` 路由上完成 30/30 轮、两次压缩 revision、一次 Runner pidfd 强杀恢复和早期显式约束召回;最大估算输入 29134/6400032 组 Provider lifecycle 唯一闭合,重复 assistant/audit、工具重放以及公共正文、summary、API Key、诱饵、项目路径和正式配置路径泄漏均为 0。首轮第 22 轮 Provider transport 失败按单次请求终态停止且零重放,新 disposable 项目完整重跑后 PASS。
- 2026-07-15 起,同一 Runtime 文档的“V1.22 Runner-owned MCP 动态工具”作为外部工具扩展事实源。AppData 配置管理 STDIO / Streamable HTTP server、Bearer/static header、工具 allow/deny 与 `auto / confirm / writes / deny` 审批;独立 Runner 持有连接并把过滤后的真实 tool schema 和 server instructions 送入 planning。模型通过现有 `submit_agent_tool_plan` 请求 `mcp.call`,调用继续复用 durable pending action、确认、steer、Goal、reconciliation 和 V1.21 token 预算;完整结果只落私有 sidecar,正式用户首页不新增 MCP 调试配置。本切片不宣称 OAuth、resources/prompts、sampling、elicitation 或 MCP task-mode 已实现。
- 2026-07-15 V1.22 已落地并完成真实验收:开发配置窗可管理 server、敏感凭据、工具过滤和审批并通过 Runner 查看有界目录,`agc:chat` / `agc:swarm` 可用 `/mcp` 查询状态。正式 `openai_chat / gpt-5.5` 路由真实调用 STDIO/Streamable HTTP lookup 和确认后的 mutate,正常 run 的 action/sidecar/receipt 各 3 且最终 assistant 唯一;第二 run 在 HTTP mutate 副作用后强杀 Runner,只进入 1 次 reconciliation,调用、sidecar、receipt 和 assistant 均未重放。公共 arguments、结果正文、instructions、凭据和项目/配置路径泄漏为 0,一次性现场已清理。
- 开发模式可通过本地项目文件面板执行 `file.list/read/write/delete`,普通用户界面不暴露文件面板。
@@ -19,7 +19,7 @@ describe('AI 游戏创作 App 共享契约', () => {
it('keeps command permissions explicit', () => {
const commandIds = GAME_CREATION_APP_COMMANDS.map((command) => command.id);
expect(GAME_CREATION_APP_COMMANDS).toHaveLength(62);
expect(GAME_CREATION_APP_COMMANDS).toHaveLength(63);
expect(commandIds).toContain('project.git_inspect');
expect(commandIds).toContain('project.git_commit');
expect(commandIds).toContain('project.patchset');
@@ -29,6 +29,7 @@ describe('AI 游戏创作 App 共享契约', () => {
expect(commandIds).toContain('command.poll');
expect(commandIds).toContain('command.stdin');
expect(commandIds).toContain('command.terminate');
expect(commandIds).toContain('mcp.call');
expect(commandIds.indexOf('command.exec')).toBe(
commandIds.indexOf('command.run_limited') + 1,
);
@@ -167,6 +168,10 @@ describe('AI 游戏创作 App 共享契约', () => {
(command) => command.id === 'llm.config_check',
)?.permission,
).toBe('auto');
expect(
GAME_CREATION_APP_COMMANDS.find((command) => command.id === 'mcp.call')
?.permission,
).toBe('auto');
expect(
GAME_CREATION_APP_COMMANDS.find((command) => command.id === 'asset.list')
?.permission,
@@ -245,7 +250,7 @@ describe('AI 游戏创作 App 共享契约', () => {
(capability) => capability.id,
);
expect(GAME_CREATION_AGENT_CAPABILITIES).toHaveLength(39);
expect(GAME_CREATION_AGENT_CAPABILITIES).toHaveLength(40);
expect(capabilityIds).toEqual(
expect.arrayContaining([
'chat',
@@ -253,6 +258,7 @@ describe('AI 游戏创作 App 共享契约', () => {
'file-upload',
'llm-draft-generation',
'provider-web-search',
'mcp-tools',
'task-decomposition',
'orchestration',
'agent-loop',
@@ -43,6 +43,7 @@ export const GAME_CREATION_APP_COMMANDS = [
{ id: 'agent.capabilities', permission: 'auto' },
{ id: 'agent.audit', permission: 'auto' },
{ id: 'llm.config_check', permission: 'auto' },
{ id: 'mcp.call', permission: 'auto' },
{ id: 'game.generate_draft', permission: 'confirm' },
{ id: 'game.run_local', permission: 'confirm' },
{ id: 'file.list', permission: 'auto' },
@@ -100,6 +101,11 @@ export const GAME_CREATION_AGENT_CAPABILITIES = [
area: 'agent-runtime',
title: '持久上下文压缩',
},
{
id: 'mcp-tools',
area: 'agent-runtime',
title: 'Runner 托管的 MCP 动态工具',
},
{ id: 'task-decomposition', area: 'agent-runtime', title: '任务拆分' },
{ id: 'orchestration', area: 'agent-runtime', title: '任务编排' },
{
@@ -21,7 +21,7 @@ pub struct GameCreationAppCommandDescriptor {
pub permission: GameCreationAppPermission,
}
pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 62] = [
pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 63] = [
command("help.show", GameCreationAppPermission::Auto),
command("project.create", GameCreationAppPermission::Confirm),
command("project.status", GameCreationAppPermission::Auto),
@@ -52,6 +52,7 @@ pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 62] = [
command("agent.capabilities", GameCreationAppPermission::Auto),
command("agent.audit", GameCreationAppPermission::Auto),
command("llm.config_check", GameCreationAppPermission::Auto),
command("mcp.call", GameCreationAppPermission::Auto),
command("game.generate_draft", GameCreationAppPermission::Confirm),
command("game.run_local", GameCreationAppPermission::Confirm),
command("file.list", GameCreationAppPermission::Auto),
@@ -103,7 +104,7 @@ pub struct GameCreationAgentCapabilityDescriptor {
pub platforms: Option<&'static [&'static str]>,
}
pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescriptor; 39] = [
pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescriptor; 40] = [
capability("chat", "user", "聊天入口"),
capability("project-supervisor", "agent-runtime", "项目总控 Agent"),
capability("file-upload", "user", "上传文件"),
@@ -115,6 +116,7 @@ pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescript
"Provider 原生联网检索",
),
capability("context-compaction", "agent-runtime", "持久上下文压缩"),
capability("mcp-tools", "agent-runtime", "Runner 托管的 MCP 动态工具"),
capability("task-decomposition", "agent-runtime", "任务拆分"),
capability("orchestration", "agent-runtime", "任务编排"),
capability(
@@ -688,7 +690,7 @@ mod tests {
#[test]
fn command_contract_keeps_expected_permissions() {
assert_eq!(GAME_CREATION_APP_COMMANDS.len(), 62);
assert_eq!(GAME_CREATION_APP_COMMANDS.len(), 63);
let command_ids = GAME_CREATION_APP_COMMANDS
.iter()
@@ -929,6 +931,11 @@ mod tests {
.find(|command| command.id == "llm.config_check")
.expect("command should exist");
assert_eq!(llm_config_check.permission, GameCreationAppPermission::Auto);
let mcp_call = GAME_CREATION_APP_COMMANDS
.iter()
.find(|command| command.id == "mcp.call")
.expect("mcp.call should exist");
assert_eq!(mcp_call.permission, GameCreationAppPermission::Auto);
let asset_list = GAME_CREATION_APP_COMMANDS
.iter()
@@ -1031,7 +1038,7 @@ mod tests {
#[test]
fn capabilities_cover_standard_agent_runtime_needs() {
assert_eq!(GAME_CREATION_AGENT_CAPABILITIES.len(), 39);
assert_eq!(GAME_CREATION_AGENT_CAPABILITIES.len(), 40);
let ids = GAME_CREATION_AGENT_CAPABILITIES
.iter()
@@ -1043,6 +1050,7 @@ mod tests {
"project-supervisor",
"file-upload",
"provider-web-search",
"mcp-tools",
"task-decomposition",
"orchestration",
"agent-loop",