From 7a920cee5918339ede699d19e72622fa998e2a9d Mon Sep 17 00:00:00 2001 From: host Date: Fri, 30 Oct 2026 03:07:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20imagegen=20MCP=20env=20=E5=BF=85?= =?UTF-8?q?=E9=A1=BB=E5=86=99=E6=AD=BB=E6=9C=AC=E5=9C=B0=E7=BD=91=E5=85=B3?= =?UTF-8?q?=20base=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 首轮生产验收失败根因:_prepare_tenant 只把 IMAGE_GEN_* 透传 进 MCP env,没写 IMAGE_GEN_BASE_URL,也没带 SUB2API_BASE_URL, 于是 imagegen.py 回落到公网 sub2.diante.me,而它不在 Tinyproxy 白名单里,API 调用 403 Filtered。 修法:worker 在写 MCP env 前用 imagegen.build_mcp_env 算好 base(显式 IMAGE_GEN_BASE_URL > SUB2API_BASE_URL),并把 网关 host 加进 NO_PROXY;组装逻辑抽成可单测 helper,新增 2 项回归测试(本地网关解析、显式 base 优先)。 --- cloud/imagegen.py | 33 +++++++++++++++++++++++++++++++++ cloud/tests/test_imagegen.py | 25 +++++++++++++++++++++++++ cloud/worker.py | 26 ++++++++++---------------- 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/cloud/imagegen.py b/cloud/imagegen.py index 97c22ec..652d320 100644 --- a/cloud/imagegen.py +++ b/cloud/imagegen.py @@ -306,3 +306,36 @@ def ensure_imagegen_mcp_env(codex_home_dir, env): return True except Exception: return False + + +def build_mcp_env(worker_environ, proxy_env=None): + """Build the [mcp_servers.imagegen.env] dict from worker env. + + The MCP process only sees config.toml env, never the worker's + own env, so IMAGE_GEN_BASE_URL must be resolved HERE (worker + IMAGE_GEN_BASE_URL, else SUB2API_BASE_URL, else absent and + imagegen.py falls back to the public relay domain -- which is + NOT on the Tinyproxy whitelist, so a missing base here caused + a 403 Filtered in the first production verification, 2026-10-30). + proxy_env supplies HTTP(S)_PROXY for downloading the returned + image URL; NO_PROXY gains the gateway host for direct API calls. + Never injects the task user's gateway key. + """ + env = dict(proxy_env) if proxy_env else {} + for key in ("IMAGE_GEN_BASE_URL", "IMAGE_GEN_API_KEY", + "IMAGE_GEN_MODEL"): + if worker_environ.get(key): + env[key] = worker_environ[key] + base = (worker_environ.get("IMAGE_GEN_BASE_URL", "") + or worker_environ.get("SUB2API_BASE_URL", "")) + if base: + env["IMAGE_GEN_BASE_URL"] = base + host = base.split("://", 1)[-1].split("/", 1)[0] + host = host.split(":", 1)[0] + parts = [p for p in env.get("NO_PROXY", "").split(",") if p] + for item in ("localhost", "127.0.0.1", host): + if item and item not in parts: + parts.append(item) + env["NO_PROXY"] = ",".join(parts) + env["no_proxy"] = ",".join(parts) + return env diff --git a/cloud/tests/test_imagegen.py b/cloud/tests/test_imagegen.py index 78a9145..34dec90 100644 --- a/cloud/tests/test_imagegen.py +++ b/cloud/tests/test_imagegen.py @@ -108,6 +108,31 @@ class ImageGenTests(unittest.TestCase): "arguments": {}}}) self.assertEqual(resp["error"]["code"], -32602) + def test_build_mcp_env_resolves_local_gateway_base(self): + # Regression: first production verification failed 403 + # Filtered because MCP env had no base URL and imagegen.py + # fell back to the public relay domain, which is not on + # the Tinyproxy whitelist. The local gateway origin must + # be resolved here and land in NO_PROXY. + env = imagegen.build_mcp_env( + {"SUB2API_BASE_URL": "http://172.17.0.1:28084", + "IMAGE_GEN_API_KEY": "k"}, + {"HTTP_PROXY": "http://10.20.20.1:8888", + "NO_PROXY": "localhost,127.0.0.1"}) + self.assertEqual(env["IMAGE_GEN_BASE_URL"], + "http://172.17.0.1:28084") + self.assertIn("172.17.0.1", env["NO_PROXY"]) + self.assertEqual(env["IMAGE_GEN_API_KEY"], "k") + self.assertNotIn("SUB2API_API_KEY", env) + + def test_build_mcp_env_explicit_base_wins(self): + env = imagegen.build_mcp_env( + {"IMAGE_GEN_BASE_URL": "https://relay.example", + "SUB2API_BASE_URL": "http://172.17.0.1:28084"}) + self.assertEqual(env["IMAGE_GEN_BASE_URL"], + "https://relay.example") + self.assertIn("relay.example", env["NO_PROXY"]) + def test_ensure_home_seeds_and_env(self): home = os.path.join(self._tmp.name, "home") os.makedirs(home) diff --git a/cloud/worker.py b/cloud/worker.py index f297cdb..562e4e4 100644 --- a/cloud/worker.py +++ b/cloud/worker.py @@ -2906,22 +2906,16 @@ class Worker: # 未配 IMAGE_GEN_API_KEY 时仍写 base/model,工具会 # 明确报“未配置”;绝不把任务自己的分组 2 key 塞进来。 try: - imagegen_env = dict(proxy_env) if proxy_env else {} - for key in ("IMAGE_GEN_BASE_URL", "IMAGE_GEN_API_KEY", - "IMAGE_GEN_MODEL"): - if os.environ.get(key): - imagegen_env[key] = os.environ[key] - base = (os.environ.get("IMAGE_GEN_BASE_URL", "") - or os.environ.get("SUB2API_BASE_URL", "")) - host = base.split("://", 1)[-1].split("/", 1)[0] - host = host.split(":", 1)[0] - no_proxy = imagegen_env.get("NO_PROXY", "") - parts = [p for p in no_proxy.split(",") if p] - for item in ("localhost", "127.0.0.1", host): - if item and item not in parts: - parts.append(item) - imagegen_env["NO_PROXY"] = ",".join(parts) - imagegen_env["no_proxy"] = ",".join(parts) + # env 组装在 imagegen.build_mcp_env 里(可单测): + # base 必须在这里算好写进 MCP env,MCP 进程只读 + # config.toml 里的 env,看不到 worker 的 + # SUB2API_BASE_URL;不写的话 imagegen.py 会回落 + # 到公网 sub2.diante.me,而它不在 Tinyproxy + # 白名单里,实测 403 Filtered(2026-10-30 首轮 + # 生产验收即因此失败)。本地网关 origin 优先, + # 因为它走 NO_PROXY 直连,不依赖白名单。 + from cloud.imagegen import build_mcp_env as _build_mcp_env + imagegen_env = _build_mcp_env(os.environ, proxy_env) home = self._tenant_dirs( task.get("project_id"))["codex_home"] _ensure_imagegen_mcp_env(home, imagegen_env)