feat: sync shared-home MCP scripts on control-plane startup

租户 config 的 mcp_servers.* 指向共享 home 路径,但宿主共享
home 里原先没有 MCP 脚本,宿主 app-server 启动 MCP 即文件
不存在、握手断掉,面板 mcp-servers-list 三服务器 toolsError
全是 handshake failed;14:43 的手工拷入能修一次,但下次
MCP 脚本改动后面板又会跑旧版。

本批只在服务端 main() 启动时同步一次:清单唯一定义在
cloud/shared_home_mcp.py(7 文件:playwright_mcp/browser/
remote、websearch_mcp/websearch、imagegen_mcp/imagegen,含
同目录兄弟模块),解析优先用 host-config-host.json 的
codex_home,兜底用 tenants_root 父目录/codex-home;缺失
拷入、不同刷新(原子替换)、相同不动;源缺失/目标不可写
只记 warning,绝不阻塞启动。create_server() 不做同步,
避免单测写向生产共享路径。

被否决的落点:worker _tenant_dirs(只在下个任务派发时跑,
面板要等下一个任务才更新,且不修共享路径本身);deploy
host_deploy.sh(会在 shell 里复制第二份清单,且重启不
部署时不刷新);面板请求时同步(把读请求变成写请求,
失败会把面板读数变成 502 风险)。
This commit is contained in:
host committed 2026-10-30 07:08:51 +00:00
1 parent 671b4bedef
commit b977a9dee8
3 files changed
+401

No files matched your search

+49
View File
@@ -107,6 +107,7 @@ from cloud.wait_ops import poll_task_statuses as _wait_poll # noqa: F401
# /wait 本身走首变轮询(_wait_poll_first),见 _task_wait。
from cloud.wait_ops import report_handoff as _handoff_report
from cloud.wait_ops import worktree_status as _wait_worktree_status
from cloud.shared_home_mcp import sync_shared_home_mcp as _sync_shared_home_mcp
from cloud.automations import ensure_automations_tables as _auto_ensure
from cloud.automations import list_automations as _auto_list
from cloud.todos import create_todo as _todo_create
@@ -425,6 +426,30 @@ def _local_worker_codex_home():
return None
def _resolve_shared_home_mcp(server=None):
"""共享 home 解析:供服务端启动时的 MCP 脚本同步用。
优先级(生产路径绝不硬编码在这里,全部来自已有解析):
1. _local_worker_codex_home():读 host-config-host.json 的
codex_home,生产即共享 home;
2. 兜底:tenants_root 的父目录 + codex-home(tenants_root
本身来自 server 属性 / CODEX_TENANTS_ROOT / 默认值)。
任一解析不出时返回 None,调用方跳过同步、不因此报错。
注意:本函数只解析路径,不做文件同步;文件清单与同步
语义只在 cloud/shared_home_mcp.py 里。
"""
local = _local_worker_codex_home()
if local:
return local
try:
root = _tenants_root_for(server)
except Exception:
return None
if not root:
return None
return os.path.join(os.path.dirname(root), "codex-home")
def _tenant_work_dir(server, project_id):
"""项目对应的租户工作目录:tenants_root/<安全项目名>/work,
skills/list 的 cwds 过滤用它(不传 cwd 时 app-server 会
@@ -10309,6 +10334,30 @@ def main(argv=None):
args = parser.parse_args(argv)
server = create_server(args.db, args.host, args.port,
backend=args.db_backend)
# 共享 home MCP 脚本同步:只在 main() 启动时做,不在
# create_server() 里做——create_server 是单测入口,单测若
# 在此同步会按 host-config-host.json 写向生产共享路径。
# systemd 两个控制面都跑 python3 -m cloud.server,走的
# 都是 main(),每次部署重启都会同步一次;同步失败只记
# warning,绝不阻塞启动(sync 本身也不抛异常,外层再
# 兜一层防解析等意外)。Docker 任务不受影响:容器内
# 该共享路径是租户 home 的挂载,宿主补齐不进容器。
try:
_shared_home = _resolve_shared_home_mcp(server)
if _shared_home:
_sync_result = _sync_shared_home_mcp(_shared_home)
if _sync_result.get("copied") or _sync_result.get("refreshed"):
_server_log.info(
"shared home MCP synced: copied=%s refreshed=%s",
_sync_result.get("copied"),
_sync_result.get("refreshed"))
if _sync_result.get("error"):
_server_log.warning(
"shared home MCP sync failed (non-fatal): %s",
_sync_result.get("error"))
except Exception as exc:
_server_log.warning(
"shared home MCP sync failed (non-fatal): %s", exc)
start_rate_limit_sweeper(server.rate_limiter, args.db)
print(
"cloud-control-plane listening on %s:%d db=%s"