Docker sandbox real test + cloud merge status endpoint
- DockerExecutor: extra mounts (host.py work/state, repo, codex-home
at same paths), worker --docker-extra-mounts, container user
configurable; Dockerfile copies codex-resources beside the codex
binary and puts bwrap on PATH (previous /usr/local/lib layout
made codex report bubblewrap unavailable inside the container).
- Real Docker task on external host: triple added in ~18s, merged
inside a container, main repo now add+mul2+triple, --rm cleanup.
Honest boundary: this host/kernel forbids unprivileged user
namespaces, so bwrap fails on host and in containers; production
mode here is Docker isolation + inner danger-full-access, not
double sandboxing.
- Cloud merge status: POST /tasks/{id}/merge syncs status only
(merge itself stays with host.py merge); completed -> merged,
maintainer+ (approve_merge), other states and repeat calls 409,
audit merge_task. Tests: cloud 223, host 27, all passing.
This commit is contained in:
1 parent
631580beab
commit
07a8eaaa73
9 files changed
+1082
-37
No files matched your search
+76
-5
@@ -48,6 +48,12 @@ interrupted,不用执行结果覆盖它。
|
||||
|
||||
可注入:runner(假 CLI,见 sandbox.LocalExecutor)与 env 映射
|
||||
均可注入,单测不跑真实模型回合、不联网、不登录任何服务。
|
||||
|
||||
执行器选择:Worker 默认用 LocalExecutor;传入 DockerExecutor
|
||||
(或 CLI --executor docker)时,派发走容器内 host.py,取消时
|
||||
按任务派生的容器名 docker kill,不只改数据库状态;本机无
|
||||
docker 时 DockerExecutor 在调 runner 前就明确报错,任务标
|
||||
failed 且 error_text 含 docker 缺失,绝不静默降级为本地执行。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -66,7 +72,7 @@ from cloud.db import connect as _db_connect
|
||||
from cloud.logging_config import get_logger, log_event, setup_logging
|
||||
from cloud.models import ModelRouter, is_fallbackable, record_fallback
|
||||
from cloud.queue import budget_exceeded, ready_tasks
|
||||
from cloud.sandbox import LocalExecutor
|
||||
from cloud.sandbox import DockerExecutor, LocalExecutor
|
||||
from cloud.sandbox import _default_runner as _sandbox_default_runner
|
||||
|
||||
# 与 server.TERMINAL_STATUSES 对齐:这些状态的任务不再被认领或取消。
|
||||
@@ -189,6 +195,7 @@ class Worker:
|
||||
self.process_factory = process_factory
|
||||
self._running_proc = None
|
||||
self._running_task_id = None
|
||||
self._running_container = None
|
||||
self._loop_stop = False
|
||||
self.logger = get_logger("cloud.worker")
|
||||
|
||||
@@ -419,6 +426,18 @@ class Worker:
|
||||
pass
|
||||
self._running_proc = None
|
||||
self._running_task_id = None
|
||||
# Docker 取消:按任务派生的容器名真杀,不只改数据库状态。
|
||||
# 同步路径(runner 阻塞在 docker run 里)下 _running_proc
|
||||
# 为空,此时靠 _running_container 杀容器;kill 尽力而为,
|
||||
# 失败也不影响后续的状态标记。
|
||||
if self._running_task_id == task_id and \
|
||||
self._running_container is not None and \
|
||||
hasattr(self.executor, "kill_container"):
|
||||
try:
|
||||
self.executor.kill_container(self._running_container)
|
||||
except Exception:
|
||||
pass
|
||||
self._running_container = None
|
||||
conn = _db_connect(self.db_path)
|
||||
try:
|
||||
cur = conn.execute(
|
||||
@@ -451,6 +470,11 @@ class Worker:
|
||||
return task is not None and task["status"] == "interrupted"
|
||||
|
||||
# -- 缺 key 门 ------------------------------------------------------
|
||||
def _executor_name(self):
|
||||
"""执行器名:DockerExecutor -> docker,其它 -> local。"""
|
||||
if isinstance(self.executor, DockerExecutor):
|
||||
return "docker"
|
||||
return "local"
|
||||
def key_available(self):
|
||||
"""只查存在性:名在环境里且值非空即 True;值本身绝不返回。"""
|
||||
try:
|
||||
@@ -557,7 +581,7 @@ class Worker:
|
||||
return {"dispatched": True, "task_id": task_id,
|
||||
"host_name": self.executor._host_name(task)
|
||||
if hasattr(self.executor, "_host_name") else task_id,
|
||||
"status": "failed", "executor": "local",
|
||||
"status": "failed", "executor": self._executor_name(),
|
||||
"stdout": "", "error": error, "events": [],
|
||||
"cost_record_id": None}
|
||||
|
||||
@@ -565,6 +589,15 @@ class Worker:
|
||||
fallback_count = 0
|
||||
result = None
|
||||
error = None
|
||||
# Docker 同步路径:dispatch 阻塞在 docker run 里时,取消
|
||||
# 线程靠 _running_container 按名杀容器(见 cancel())。
|
||||
if hasattr(self.executor, "container_name"):
|
||||
try:
|
||||
self._running_container = \
|
||||
self.executor.container_name(task)
|
||||
self._running_task_id = task_id
|
||||
except Exception:
|
||||
self._running_container = None
|
||||
try:
|
||||
if self._use_popen():
|
||||
result = self._execute_via_process(task)
|
||||
@@ -585,6 +618,12 @@ class Worker:
|
||||
"status": "failed", "stdout": "", "returncode": None,
|
||||
"host_status": None, "stderr": ""}
|
||||
error = str(exc)
|
||||
# 同步路径执行结束:清掉取消用的容器标记(取消路径里
|
||||
# 若已清过,这里再清一次无害)。
|
||||
self._running_container = None
|
||||
if self._running_task_id == task_id and \
|
||||
self._running_proc is None:
|
||||
self._running_task_id = None
|
||||
|
||||
# 取消优先:执行期间被 cancel 的任务保持 interrupted,
|
||||
# 绝不用执行结果覆盖它(见模块 docstring 的取消说明)。
|
||||
@@ -599,7 +638,7 @@ class Worker:
|
||||
return {"dispatched": True, "task_id": task_id,
|
||||
"host_name": self.executor._host_name(task)
|
||||
if hasattr(self.executor, "_host_name") else task_id,
|
||||
"status": "interrupted", "executor": "local",
|
||||
"status": "interrupted", "executor": self._executor_name(),
|
||||
"stdout": (result or {}).get("stdout", ""),
|
||||
"error": "cancelled", "events": [],
|
||||
"cost_record_id": None}
|
||||
@@ -649,7 +688,7 @@ class Worker:
|
||||
return {"dispatched": True, "task_id": task_id,
|
||||
"host_name": self.executor._host_name(task)
|
||||
if hasattr(self.executor, "_host_name") else task_id,
|
||||
"status": final, "executor": "local",
|
||||
"status": final, "executor": self._executor_name(),
|
||||
"stdout": stdout, "error": error, "events": events,
|
||||
"cost_record_id": cost_id}
|
||||
|
||||
@@ -719,6 +758,21 @@ class Worker:
|
||||
return all_results
|
||||
|
||||
|
||||
def _build_cli_executor(args, host_py):
|
||||
"""CLI 的执行器构造:local 时返回 None(Worker 默认 LocalExecutor),
|
||||
docker 时返回 DockerExecutor(env_key 名用 Worker 的解析规则,
|
||||
值本身绝不从这里传——DockerExecutor 只查存在性,-e 只传名)。"""
|
||||
if getattr(args, "executor", "local") != "docker":
|
||||
return None
|
||||
return DockerExecutor(
|
||||
host_py, config_path=args.config, image=args.docker_image,
|
||||
cpus=args.docker_cpus, memory=args.docker_memory,
|
||||
user=args.docker_user, worktrees_root=args.docker_worktrees_root,
|
||||
env_key=resolve_env_key(args.env_key, args.codex_config),
|
||||
timeout=args.docker_timeout,
|
||||
extra_mounts=args.docker_extra_mounts)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
"""worker CLI:默认单次 run_all;--loop 进常驻循环。
|
||||
|
||||
@@ -737,6 +791,22 @@ def main(argv=None):
|
||||
parser.add_argument("--model", default=None)
|
||||
parser.add_argument("--max-turns", type=int, default=None)
|
||||
parser.add_argument("--max-tasks", type=int, default=None)
|
||||
parser.add_argument("--executor", default="local",
|
||||
choices=["local", "docker"],
|
||||
help="执行器:local 调 host.py 子进程;docker"
|
||||
" 调 DockerExecutor(无 docker 时任务"
|
||||
"明确失败,不静默降级)")
|
||||
parser.add_argument("--docker-image", default="codex-app-server:latest")
|
||||
parser.add_argument("--docker-cpus", default="2")
|
||||
parser.add_argument("--docker-memory", default="2g")
|
||||
parser.add_argument("--docker-user", default="1000:1000")
|
||||
parser.add_argument("--docker-worktrees-root", default=None)
|
||||
parser.add_argument("--docker-timeout", type=float, default=None)
|
||||
parser.add_argument("--docker-extra-mounts", default=None,
|
||||
help="docker 额外挂载,逗号分隔 host:container[:ro|rw],"
|
||||
"用于容器内 host.py 写 work/、host-state.json,"
|
||||
"以及挂载原仓库和 codex-home(同路径挂载,"
|
||||
"容器内配置无需改路径)")
|
||||
parser.add_argument("--loop", action="store_true",
|
||||
help="常驻循环,每 --interval 秒跑一轮")
|
||||
parser.add_argument("--interval", type=int, default=300)
|
||||
@@ -750,7 +820,8 @@ def main(argv=None):
|
||||
worker = Worker(args.db, host_py, config_path=args.config,
|
||||
repo=args.repo, env_key=args.env_key,
|
||||
codex_config_path=args.codex_config, model=args.model,
|
||||
max_turns=args.max_turns)
|
||||
max_turns=args.max_turns,
|
||||
executor=_build_cli_executor(args, host_py))
|
||||
# CLI 起 worker 前先建表/迁移:全新库直接起循环不应因缺表而炸。
|
||||
try:
|
||||
from cloud.server import init_db as _init_db
|
||||
|
||||
Reference in new issue
Block a user