实现Runner托管的MCP动态工具
接入STDIO与Streamable HTTP MCP连接、动态工具目录和Runner协议 将mcp.call纳入持久动作、工具审批、私有结果和强杀恢复 增加AppData配置、开发配置界面及聊天CLI的MCP状态入口 补齐真实Provider E2E、全量回归、安全扫描和技术文档
This commit is contained in:
@@ -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
@@ -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
@@ -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 与 reconciliation;planning 后目录漂移回同 run replan,executing 后结果未知或 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 1;HTTP mutate 副作用后 pidfd 强杀 Runner,恢复只产生 reconciliation 1,marker 1,sidecar/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 变化会废弃旧连接并重新 initialize;required server 初始化或列目录失败时阻止 planning,optional 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 并核对两个 fingerprint;Runner 在 `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 content;image/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` suite,V1.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 条 reconciliation,marker 保持 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/15,Tauri 全量为 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 prefix,Goal、任务、计划、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/64000,32 组 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",
|
||||
|
||||
Reference in New Issue
Block a user