diff --git a/cloud/sandbox.py b/cloud/sandbox.py index 8f416f0..32de206 100644 --- a/cloud/sandbox.py +++ b/cloud/sandbox.py @@ -1440,6 +1440,11 @@ class DockerExecutor(Executor): 创建,续跑容器不带同一 --model-provider 时 Server 的 provider 表里没有它,thread/resume 报 provider not found (2026-10-29 probe15 实弹)。 + 密钥环境也必须透传:密钥类变量只按名进 docker run + 命令(-e SUB2API_API_KEY 不带值),值靠 _docker_cli_env + 放进 CLI 子进程环境;本方法此前漏传 env,容器内 key + 为空,每轮瞬间报 Missing SUB2API_API_KEY、无限空转 + (2026-10-29 长任务实弹:断点后 274 轮全失败)。 """ if not isinstance(task, dict) or task.get("id") is None: raise ValueError("任务缺少 id") @@ -1453,7 +1458,9 @@ class DockerExecutor(Executor): network=network, extra_env=extra_env) try: - result = self._call_runner(cmd, timeout=timeout) + result = self._call_runner( + cmd, timeout=timeout, + env=self._docker_cli_env(extra_env)) except Exception as exc: if exc.__class__.__name__ == "TimeoutExpired": self.kill_container(task) diff --git a/cloud/tests/test_p1_security_worker.py b/cloud/tests/test_p1_security_worker.py index 744cc17..17047f2 100644 --- a/cloud/tests/test_p1_security_worker.py +++ b/cloud/tests/test_p1_security_worker.py @@ -490,6 +490,41 @@ class P1WorkerTestCase(unittest.TestCase): "--model-provider" in cmd and "sub2api" in cmd for cmd in self.calls)) + # 12d. Docker 断点续跑必须把密钥环境透传给 runner: + # 密钥类变量只按名进 docker run(-e SUB2API_API_KEY 不带值), + # 值靠 _docker_cli_env 放进 CLI 子进程环境;漏传 env 时容器 + # 内 key 为空,每轮瞬间报 Missing SUB2API_API_KEY(2026-10-29 + # 长任务实弹:断点后 274 轮全失败、无限空转)。 + def test_docker_breakpoint_forwards_secret_env_to_runner(self): + seen = {} + + def dfake(cmd, timeout=None, env=None): + seen["cmd"] = list(cmd) + seen["env"] = env + return SimpleNamespace( + returncode=0, + stdout="goal met after rounds\n" + "usage input=10 cached=0 cache_write=0 output=5 " + "reasoning=0 total=15 completions=1\n", + stderr="") + + task = {"id": "T1", "prompt": "orig goal"} + docker = DockerExecutor(runner=dfake, env=FAKE_ENV) + with mock.patch("cloud.sandbox.shutil.which", + return_value="/usr/bin/docker"): + docker.goal_resume_breakpoint_task( + task, model_provider="sub2api", + extra_env={"SUB2API_API_KEY": "secret-placeholder", + "SUB2API_BASE_URL": "https://example.test"}) + # 命令里密钥只按名出现,不带值(不进 argv)。 + self.assertIn("SUB2API_API_KEY", seen["cmd"]) + self.assertNotIn("SUB2API_API_KEY=secret-placeholder", + seen["cmd"]) + # runner 的 CLI 环境里必须有值,否则容器内 key 为空。 + self.assertIsNotNone(seen["env"]) + self.assertEqual(seen["env"].get("SUB2API_API_KEY"), + "secret-placeholder") + # 13. 开新目标:goal-resume 带 --goal-text(旧行为保留) def test_goal_resume_new_goal_keeps_goal_text(self): calls = []