diff --git a/CHANGELOG.md b/CHANGELOG.md index 2352629a65..4a91259a77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1397,12 +1397,19 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. ### Fixed +- **mcp:** Ordinary users can now install DingTalk, WeCom and HubSpot from + **Capability Center → Plugins**. Saving one of these bundled + connections as a non-administrator, including editing or enabling it later, + failed with `500 Internal Server Error` because the personal-connection check + read a `cwd` field the request model does not declare. A bundled connection + with a working directory still requires an administrator. ([#7400]) + - **examples:** The extension example's package tests pass again. Its test registry lacked `knowledge_provider()`, so it no longer matched the `ExtensionRegistry` protocol and the README's `pytest` run failed. Extension Package Check now also runs the tests of bundled examples that ship them, so a contract change that breaks them fails its pull request. ([#7364]) - + - **gateway:** Run requests with a malformed body now get a client error instead of `500 Internal Server Error`. A `config.configurable` or `config.context` that is not an object, an invalid `assistant_id`, a @@ -13052,3 +13059,4 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#7326]: https://github.com/bytedance/deer-flow/pull/7326 [#7362]: https://github.com/bytedance/deer-flow/pull/7362 [#7364]: https://github.com/bytedance/deer-flow/pull/7364 +[#7400]: https://github.com/bytedance/deer-flow/pull/7400 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index f1d4be7fef..b91b51eaa2 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -1112,11 +1112,16 @@ ### 修复 +- **MCP:** 普通用户现在可以在“能力中心 → 插件”中安装钉钉、企业微信和 HubSpot。 + 此前非管理员保存这些内置连接(包括之后编辑或启用)会返回 + `500 Internal Server Error`,因为个人连接检查读取了请求模型未声明的 `cwd` 字段。 + 设置了工作目录的内置连接仍需管理员权限。([#7400]) + - **示例:** 扩展示例的包测试重新通过。其测试用 registry 缺少 `knowledge_provider()`,不再满足 `ExtensionRegistry` 协议,按 README 运行 `pytest` 会失败。Extension Package Check 现在也会运行自带测试的内置示例的测试, 因此破坏这些测试的契约变更会在其 PR 中失败。([#7364]) - + - **网关:** 请求体格式错误的运行请求现在返回客户端错误,而不是 `500 Internal Server Error`。此前 `config.configurable` 或 `config.context` 不是对象、`assistant_id` 无效、`agent_name` 不是字符串,或 AI 消息的 `tool_calls` @@ -10419,3 +10424,4 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#7326]: https://github.com/bytedance/deer-flow/pull/7326 [#7362]: https://github.com/bytedance/deer-flow/pull/7362 [#7364]: https://github.com/bytedance/deer-flow/pull/7364 +[#7400]: https://github.com/bytedance/deer-flow/pull/7400 diff --git a/backend/app/gateway/routers/personal_mcp.py b/backend/app/gateway/routers/personal_mcp.py index d4adbc6e01..5d82540052 100644 --- a/backend/app/gateway/routers/personal_mcp.py +++ b/backend/app/gateway/routers/personal_mcp.py @@ -50,7 +50,9 @@ def _validate_personal_server(server: mcp.McpServerConfigResponse, *, admin: boo if not admin: from deerflow.capabilities.business import is_bundled_connection - bundled = server.type == "stdio" and is_bundled_connection(server.command, server.args, server.env) and not server.cwd + # The response model does not declare `cwd`; a supplied one is an extra. + cwd = (server.model_extra or {}).get("cwd") + bundled = server.type == "stdio" and is_bundled_connection(server.command, server.args, server.env) and not cwd if (server.type not in {"http", "sse"} and not bundled) or server.oauth is not None: raise HTTPException(403, "Only an administrator may configure host commands or OAuth token endpoints") from deerflow.community.url_safety import validate_public_http_url diff --git a/backend/tests/test_personal_mcp.py b/backend/tests/test_personal_mcp.py index f665e6f8cd..0f237a5387 100644 --- a/backend/tests/test_personal_mcp.py +++ b/backend/tests/test_personal_mcp.py @@ -288,6 +288,70 @@ def test_untrusted_users_cannot_launch_packages_or_connect_to_private_hosts(pers assert read_user_mcp_config("alice")["mcpServers"]["github"]["personal_public_network"] is True +@pytest.mark.parametrize( + "provider,credentials", + [ + ("dingtalk", {"access_token": "robot-token", "sign_secret": "SEC-secret"}), + ("wecom", {"webhook_key": "webhook-secret"}), + ("hubspot", {"access_token": "private-token"}), + ], +) +def test_ordinary_users_install_and_manage_bundled_connections(personal_client, monkeypatch, tmp_path, provider, credentials): + from app.gateway.deps import get_config + from app.gateway.routers import capabilities + from deerflow.capabilities.business import is_bundled_connection + from deerflow.mcp.personal_access import requires_admin + + app = personal_client.app + app.include_router(capabilities.router) + app.dependency_overrides[get_config] = lambda: SimpleNamespace() + platform = tmp_path / "deployment.json" + platform.write_text(json.dumps({"mcpServers": {}})) + monkeypatch.setenv("DEER_FLOW_EXTENSIONS_CONFIG_PATH", str(platform)) + user = {"test-user": "alice", "test-role": "user"} + name = "my-" + provider + # The web UI installs every catalog connection with scope "user". + response = personal_client.post("/api/capabilities/installations", headers=user, json={"plugin_id": provider, "name": name, "scope": "user", "configuration": credentials}) + assert response.status_code == 200, response.text + assert [item["name"] for item in response.json()["items"]] == [name] + for secret in credentials.values(): + assert secret not in response.text + saved = next(iter(load_user_mcp_config("alice").mcp_servers.values())) + assert is_bundled_connection(saved.command, saved.args, saved.env) + assert set(saved.env.values()) == set(credentials.values()) + # Saved under ordinary-user policy, so discovery and calls need no admin role. + assert not requires_admin(saved) + + masked = personal_client.get("/api/mcp/personal/config", headers=user).json()["mcp_servers"][name] + masked["description"] = "Team notifications" + assert personal_client.put("/api/mcp/personal/config/server", headers=user, json={"server_name": name, "server": masked}).status_code == 200 + for enabled in (False, True): + assert personal_client.patch("/api/mcp/personal/config", headers=user, json={"server_name": name, "enabled": enabled}).status_code == 200 + saved = next(iter(load_user_mcp_config("alice").mcp_servers.values())) + assert saved.description == "Team notifications" + assert set(saved.env.values()) == set(credentials.values()) + assert personal_client.delete("/api/mcp/personal/config/servers/" + name, headers=user).status_code == 200 + assert read_user_mcp_config("alice")["mcpServers"] == {} + + +def test_bundled_launcher_with_a_working_directory_requires_an_administrator(personal_client): + from deerflow.capabilities.business import connection_config + + definition = {**connection_config("hubspot", {"access_token": "private-token"}), "cwd": "/srv"} + user = {"test-user": "alice", "test-role": "user"} + response = personal_client.post("/api/mcp/personal/config/servers", headers=user, json={"mcp_servers": {"crm": definition}}) + assert response.status_code == 403 + assert read_user_mcp_config("alice")["mcpServers"] == {} + + assert personal_client.post("/api/mcp/personal/config/servers", headers={"test-user": "alice"}, json={"mcp_servers": {"crm": definition}}).status_code == 200 + before = user_mcp_config_path("alice").read_bytes() + masked = personal_client.get("/api/mcp/personal/config", headers=user).json()["mcp_servers"]["crm"] + assert masked["cwd"] == "/srv" + assert personal_client.put("/api/mcp/personal/config/server", headers=user, json={"server_name": "crm", "server": masked}).status_code == 403 + assert personal_client.patch("/api/mcp/personal/config", headers=user, json={"server_name": "crm", "enabled": True}).status_code == 403 + assert user_mcp_config_path("alice").read_bytes() == before + + @pytest.mark.asyncio async def test_personal_network_rechecks_destination_before_each_request(monkeypatch): from deerflow.mcp import personal_network