123 lines
4.4 KiB
Python
123 lines
4.4 KiB
Python
"""沙箱执行器:可插拔的任务执行接口。
|
||
|
||
零第三方依赖,只用 Python 标准库;不重写 agent 逻辑。
|
||
|
||
- Executor:基类,子类必须实现 execute(task)。
|
||
- LocalExecutor:先行实现,复用 host.py 现有 worktree + codex 沙箱,
|
||
通过 host.py CLI 子进程做只读查询封装;不直接跑模型。
|
||
- DockerExecutor:占位,未接通容器执行。
|
||
- CloudExecutor:占位,未接通云端执行。
|
||
"""
|
||
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
|
||
MAX_STDOUT_CHARS = 2000
|
||
|
||
|
||
class Executor:
|
||
"""执行器基类。
|
||
|
||
task 是 dict,含 id、project_id、repo、prompt、branch(可选)。
|
||
execute(task) -> dict,至少含 executor、task_id、status、stdout。
|
||
"""
|
||
|
||
def execute(self, task) -> dict:
|
||
raise NotImplementedError("子类必须实现 execute(task)")
|
||
|
||
|
||
def _truncate_stdout(text):
|
||
if text is None:
|
||
return ""
|
||
return str(text)[:MAX_STDOUT_CHARS]
|
||
|
||
|
||
def _default_runner(cmd):
|
||
"""subprocess.run 的薄封装:捕获输出、文本模式。"""
|
||
return subprocess.run(cmd, capture_output=True, text=True)
|
||
|
||
|
||
class LocalExecutor(Executor):
|
||
"""本地执行器:调 host.py CLI 子进程做执行封装。
|
||
|
||
真正跑模型回合的 dispatch/run 由控制面在确认模型 key 供给后才
|
||
调用,本模块只做执行器骨架和 CLI 调用封装。因此 execute() 在此
|
||
只发 host.py 的只读命令(例如 status),绝不在 execute 里直接
|
||
跑模型回合;需要跑模型时,控制面另行调用 host.py dispatch/run。
|
||
|
||
构造函数:LocalExecutor(host_py_path, config_path=None, runner=None)。
|
||
runner 默认是 subprocess.run 的薄封装;单测可注入假 runner,假
|
||
runner 返回 CompletedProcess 风格的 result 对象(含 returncode、
|
||
stdout),本类只读这两个字段,不依赖其它字段。
|
||
"""
|
||
|
||
def __init__(self, host_py_path, config_path=None, runner=None):
|
||
self.host_py_path = host_py_path
|
||
self.config_path = config_path
|
||
self.runner = runner if runner is not None else _default_runner
|
||
|
||
def _command(self, task_id):
|
||
cmd = [sys.executable, self.host_py_path]
|
||
if self.config_path is not None:
|
||
cmd += ["--config", self.config_path]
|
||
cmd += ["status", "--task", str(task_id)]
|
||
return cmd
|
||
|
||
def execute(self, task) -> dict:
|
||
task_id = task.get("id") if isinstance(task, dict) else None
|
||
if task_id is None:
|
||
raise ValueError("任务缺少 id")
|
||
result = self.runner(self._command(task_id))
|
||
returncode = getattr(result, "returncode", 1)
|
||
stdout = _truncate_stdout(getattr(result, "stdout", ""))
|
||
return {
|
||
"executor": "local",
|
||
"task_id": task_id,
|
||
"status": "completed" if returncode == 0 else "failed",
|
||
"stdout": stdout,
|
||
"returncode": returncode,
|
||
}
|
||
|
||
|
||
class DockerExecutor(Executor):
|
||
"""容器执行器(占位,未接通)。
|
||
|
||
本机无 docker(shutil.which("docker") 为 None)时 raise
|
||
RuntimeError("docker not available"),绝不静默降级为本地执行。
|
||
有 docker 时也只返回占位失败,不真跑容器。
|
||
|
||
后续接线:容器挂载(把任务 worktree 以只读/受限写挂载进容器)、
|
||
网络隔离(默认无外网,仅放行模型 API 所需出口)、资源限额与
|
||
容器回收,届时再替换本占位实现。
|
||
"""
|
||
|
||
def execute(self, task) -> dict:
|
||
task_id = task.get("id") if isinstance(task, dict) else None
|
||
if task_id is None:
|
||
raise ValueError("任务缺少 id")
|
||
if shutil.which("docker") is None:
|
||
raise RuntimeError("docker not available")
|
||
return {
|
||
"executor": "docker",
|
||
"task_id": task_id,
|
||
"status": "failed",
|
||
"stdout": "docker executor not implemented: "
|
||
"container mount and network isolation pending",
|
||
"returncode": None,
|
||
}
|
||
|
||
|
||
class CloudExecutor(Executor):
|
||
"""云端执行器(占位,未接通)。
|
||
|
||
execute(task) 一律 raise RuntimeError,绝不假装已接通云端。
|
||
接通前提:真实云账号 + 预算上限配置,届时由控制面注入凭证与
|
||
限额后再替换本占位实现。
|
||
"""
|
||
|
||
def execute(self, task) -> dict:
|
||
raise RuntimeError(
|
||
"cloud executor not connected: missing cloud account "
|
||
"and budget limit")
|