diff --git a/host.py b/host.py index db5d435..2b006dc 100644 --- a/host.py +++ b/host.py @@ -205,6 +205,23 @@ def default_codex_bin(): # 实时链路:单条通知里 item 的上限行数(防失控回合把文件写爆)。 TRACE_MAX_LINES = 5000 +# agent shell 绝不能看到的变量(codex shell_environment_policy.exclude)。 +# 实弹证实:不设此策略时 agent shell 直接继承 server 进程 env, +# XCM_API_KEY 在 local 路可被模型拿到;server 自己的中转调用走 +# 进程 env(env_key),不受 shell 策略影响,所以只排 shell 不影响 +# 模型调用。inherit 保持默认 all(PATH、代理变量等工作流变量不 +# 动);gh 用文件登录不用 env,故不排 *_TOKEN;四个精确名是当前 +# worker env 里的全部密钥类变量,三条模式兜底将来新增的同类变量。 +SHELL_ENV_EXCLUDE = [ + "XCM_API_KEY", + "CLOUD_DB_PASSWORD", + "GITHUB_OAUTH_CLIENT_SECRET", + "GITHUB_WEBHOOK_SECRET", + "*_KEY", + "*_SECRET", + "*_PASSWORD", +] + def _summarize_trace_item(method, params): """app-server 通知 → 链路条目列表(实时链路用)。 @@ -302,6 +319,20 @@ def _append_trace_marker(state_path, task_name, entry): pass +def _server_command(codex_bin): + """app-server 启动命令:-c 必须放在子命令之前。 + + shell_environment_policy.exclude 只过滤 agent shell 看到的 + 变量,不影响 server 进程自己的中转调用(env_key 直读进程 env)。 + """ + return [ + codex_bin, + "-c", + "shell_environment_policy.exclude=" + json.dumps(SHELL_ENV_EXCLUDE), + "app-server", + ] + + class Server: """单个 app-server 进程,多 thread 并发。""" @@ -365,7 +396,7 @@ class Server: os.makedirs(self.codex_home, exist_ok=True) env["CODEX_HOME"] = self.codex_home self.proc = subprocess.Popen( - [self.codex_bin, "app-server"], + _server_command(self.codex_bin), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, env=env, ) diff --git a/tests/test_host.py b/tests/test_host.py index 5592521..a0bc21e 100644 --- a/tests/test_host.py +++ b/tests/test_host.py @@ -52,6 +52,21 @@ class HostPureFunctionsTest(unittest.TestCase): missing = os.path.join(tmp, "missing.json") self.assertEqual(host._load_config_file(missing), {}) + def test_server_command_excludes_secrets_from_agent_shell(self): + cmd = host._server_command("/usr/bin/codex") + self.assertEqual(cmd[0], "/usr/bin/codex") + self.assertEqual(cmd[-1], "app-server") + self.assertIn("-c", cmd) + flag = cmd[cmd.index("-c") + 1] + self.assertTrue( + flag.startswith("shell_environment_policy.exclude="), flag) + excluded = json.loads( + flag.split("=", 1)[1]) + for name in ("XCM_API_KEY", "CLOUD_DB_PASSWORD", + "GITHUB_OAUTH_CLIENT_SECRET", "GITHUB_WEBHOOK_SECRET", + "*_KEY", "*_SECRET", "*_PASSWORD"): + self.assertIn(name, excluded) + def _init_git_repo(tmp): subprocess.run(["git", "init", "-q"], cwd=tmp, check=True)