websearch: MCP env 写进租户 config,不只靠 docker -e
2026-10-30 实测:Codex bash 里有 SEARXNG_URL + proxy, MCP 链却只有 duckduckgo、DNS 瞬间失败——Codex 的 MCP 子进程不继承 Codex 进程 env;给租户 config 加 [mcp_servers.websearch.env] 后同一任务立刻返回真实 SearXNG 结果。修法:worker 在 _docker_task_context 算出网关后、容器启动前,用 ensure_websearch_mcp_env 把 proxy + 搜索 env 写进租户 config(网关此时才已知, 播种时写不死;写失败只记日志不阻塞)。 测试:新增 test_websearch_mcp_env 4 项,相关共 33 项全过。
This commit is contained in:
1 parent
08d56b15ce
commit
71e31b22a0
3 files changed
+164
No files matched your search
@@ -0,0 +1,71 @@
|
||||
"""ensure_websearch_mcp_env: MCP env lives in config.toml, because
|
||||
Codex's MCP subprocess does not inherit the Codex process env
|
||||
(2026-10-30: Codex bash had SEARXNG_URL + proxy, MCP chain was
|
||||
still ["duckduckgo"] only; adding the config env section made
|
||||
the same task return real SearXNG results)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
from cloud.websearch import ensure_websearch_mcp_env # noqa: E402
|
||||
|
||||
|
||||
BASE = ("model = \"gpt-6.1-sol\"\n\n"
|
||||
"[mcp_servers.websearch]\n"
|
||||
"command = \"python3\"\n"
|
||||
"args = [\"/var/lib/codex-app-server/codex-home/"
|
||||
"websearch_mcp.py\"]\n")
|
||||
|
||||
|
||||
class TestMcpEnv(unittest.TestCase):
|
||||
def _home(self, text):
|
||||
tmp = tempfile.mkdtemp()
|
||||
with open(os.path.join(tmp, "config.toml"), "w") as fh:
|
||||
fh.write(text)
|
||||
return tmp
|
||||
|
||||
def _read(self, home):
|
||||
with open(os.path.join(home, "config.toml")) as fh:
|
||||
return fh.read()
|
||||
|
||||
def test_insert_env_after_websearch_section(self):
|
||||
home = self._home(BASE)
|
||||
self.assertTrue(ensure_websearch_mcp_env(
|
||||
home, {"SEARXNG_URL": "http://searx.test",
|
||||
"http_proxy": "http://10.0.0.1:8888"}))
|
||||
text = self._read(home)
|
||||
self.assertIn("[mcp_servers.websearch.env]", text)
|
||||
self.assertIn('SEARXNG_URL = "http://searx.test"', text)
|
||||
self.assertIn('http_proxy = "http://10.0.0.1:8888"', text)
|
||||
self.assertIn('command = "python3"', text)
|
||||
|
||||
def test_replace_existing_env_preserves_other_sections(self):
|
||||
home = self._home(BASE + "[mcp_servers.websearch.env]\n"
|
||||
"SEARXNG_URL = \"http://old.test\"\n\n"
|
||||
"[other]\nfoo = 1\n")
|
||||
self.assertTrue(ensure_websearch_mcp_env(
|
||||
home, {"SEARXNG_URL": "http://new.test"}))
|
||||
text = self._read(home)
|
||||
self.assertIn('SEARXNG_URL = "http://new.test"', text)
|
||||
self.assertNotIn("old.test", text)
|
||||
self.assertIn("[other]\nfoo = 1", text)
|
||||
|
||||
def test_empty_env_writes_nothing(self):
|
||||
home = self._home(BASE)
|
||||
self.assertFalse(ensure_websearch_mcp_env(home, {}))
|
||||
self.assertEqual(self._read(home), BASE)
|
||||
|
||||
def test_idempotent_second_write(self):
|
||||
home = self._home(BASE)
|
||||
env = {"SEARXNG_URL": "http://searx.test"}
|
||||
self.assertTrue(ensure_websearch_mcp_env(home, env))
|
||||
self.assertFalse(ensure_websearch_mcp_env(home, env))
|
||||
self.assertEqual(self._read(home).count(
|
||||
"[mcp_servers.websearch.env]"), 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -647,3 +647,79 @@ def ensure_codex_home_websearch(codex_home_dir, command_args=None,
|
||||
"config_changed": config_changed,
|
||||
"script_changed": script_changed,
|
||||
"script_differs": script_differs}
|
||||
|
||||
|
||||
def ensure_websearch_mcp_env(codex_home_dir, env):
|
||||
"""Write [mcp_servers.websearch.env] into a tenant config.toml.
|
||||
|
||||
Why this exists (measured 2026-10-30, not guessed): Codex's
|
||||
MCP server subprocess does NOT inherit the Codex process env.
|
||||
A Docker task whose Codex bash env contained SEARXNG_URL and
|
||||
http_proxy still ran web_search with chain ["duckduckgo"]
|
||||
only, failing instantly with "Temporary failure in name
|
||||
resolution" -- SEARXNG_URL never reached the MCP process, and
|
||||
without a proxy the container has no direct DNS. Adding an
|
||||
explicit ``env`` table to the MCP config made the same task
|
||||
return real SearXNG results. Container env (docker -e) is
|
||||
therefore NOT sufficient for MCP tools; the env must live in
|
||||
config.toml, where Codex actually reads it for MCP servers.
|
||||
|
||||
Idempotent: an existing env subsection is replaced in place,
|
||||
other sections are left untouched, and a missing websearch
|
||||
section is created first (command/args only, no env values
|
||||
invented). Values are written as TOML basic strings via
|
||||
json.dumps (safe for URLs and proxy URLs). Returns True when
|
||||
the file was written, False when env is empty or the file
|
||||
could not be written (caller logs, never crashes the task).
|
||||
"""
|
||||
if not env:
|
||||
return False
|
||||
home = os.path.abspath(codex_home_dir)
|
||||
config_path = os.path.join(home, "config.toml")
|
||||
try:
|
||||
existing = ""
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, encoding="utf-8") as fh:
|
||||
existing = fh.read()
|
||||
if "[mcp_servers.websearch]" not in existing:
|
||||
existing += ("\n[mcp_servers.websearch]\n"
|
||||
"command = \"python3\"\n"
|
||||
"args = [\"/var/lib/codex-app-server/"
|
||||
"codex-home/websearch_mcp.py\"]\n")
|
||||
env_lines = ["[mcp_servers.websearch.env]"]
|
||||
for key in sorted(env):
|
||||
env_lines.append("%s = %s" % (key, json.dumps(env[key])))
|
||||
env_block = "\n".join(env_lines) + "\n"
|
||||
lines = existing.splitlines(keepends=True)
|
||||
start = end = None
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip() == "[mcp_servers.websearch.env]":
|
||||
start = i
|
||||
elif start is not None and line.startswith("["):
|
||||
end = i
|
||||
break
|
||||
if start is not None:
|
||||
if end is None:
|
||||
end = len(lines)
|
||||
lines[start:end] = [env_block]
|
||||
new_text = "".join(lines)
|
||||
else:
|
||||
insert_at = None
|
||||
in_section = False
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip() == "[mcp_servers.websearch]":
|
||||
in_section = True
|
||||
elif in_section and line.startswith("["):
|
||||
insert_at = i
|
||||
break
|
||||
if insert_at is None:
|
||||
insert_at = len(lines)
|
||||
lines.insert(insert_at, env_block)
|
||||
new_text = "".join(lines)
|
||||
if new_text == existing:
|
||||
return False
|
||||
with open(config_path, "w", encoding="utf-8") as fh:
|
||||
fh.write(new_text)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
@@ -166,6 +166,7 @@ from cloud.peer_messages import peer_message_prefix as _peer_prefix
|
||||
from cloud.peer_messages import row_get as _row_get
|
||||
from cloud.sub2api import ensure_user_gateway_key as _ensure_sub2api_key
|
||||
from cloud.websearch import ensure_codex_home_websearch as _ensure_websearch_mcp
|
||||
from cloud.websearch import ensure_websearch_mcp_env as _ensure_websearch_mcp_env
|
||||
from cloud.skill_seed import ensure_codex_home_skills as _ensure_skills
|
||||
from cloud.sub2api import gateway_name as _gateway_name
|
||||
from cloud.sub2api import gateway_origin as _gateway_origin
|
||||
@@ -2823,6 +2824,22 @@ class Worker:
|
||||
}
|
||||
if search_env:
|
||||
proxy_env = _merge_env(proxy_env, search_env)
|
||||
# MCP env must live in config.toml, not just docker -e:
|
||||
# Codex's MCP subprocess does not inherit the Codex
|
||||
# process env (2026-10-30实测:Codex bash 里有
|
||||
# SEARXNG_URL + proxy,MCP 链却只有 duckduckgo,
|
||||
# DNS 瞬间失败;给 config 加 [mcp_servers.websearch.env]
|
||||
# 后同一任务立刻返回真实 SearXNG 结果)。网关在此时
|
||||
# 才已知,所以这里、容器启动前写租户 config,绝不
|
||||
# 在播种时写死一个网关。写失败只记日志,不阻塞任务。
|
||||
if self.tenant_isolation and proxy_env:
|
||||
try:
|
||||
home = self._tenant_dirs(
|
||||
task.get("project_id"))["codex_home"]
|
||||
_ensure_websearch_mcp_env(home, proxy_env)
|
||||
except Exception as exc:
|
||||
self.logger.warning(
|
||||
"websearch MCP env write failed: %s" % exc)
|
||||
return (True, tenant_mounts, tenant_network, proxy_env, None)
|
||||
|
||||
def _run_fork(self, task, quota_check=None):
|
||||
|
||||
Reference in new issue
Block a user