diff --git a/ais_bench/configs/agent_example/harbor_terminal_bench_2_task.py b/ais_bench/configs/agent_example/harbor_terminal_bench_2_task.py index ace9d81b..700a7132 100644 --- a/ais_bench/configs/agent_example/harbor_terminal_bench_2_task.py +++ b/ais_bench/configs/agent_example/harbor_terminal_bench_2_task.py @@ -6,6 +6,27 @@ with read_base(): from ais_bench.benchmark.configs.summarizers.example import summarizer +# 数据集路径:用 mmengine 的 {{$VAR:default}} 占位符从环境变量读取。 +# mmengine 在执行本文件前,会先用 os.environ['VAR'] 的值替换整个 {{$VAR:default}} +# 占位符;如果环境变量未设置,则替换为 default 部分。 +# +# 这里 AISBENCH_AGENT_DATASET_PATH 的值就是 bootstrap.sh --datasets 传入的完整路径, +# 由 bootstrap.sh 自动注入到容器环境变量中,无需 import sys / import os 任何库。 +# +# 使用流程: +# 1. 物理机: bash bootstrap.sh --datasets /data/datasets/harbor/mini-0.10/terminal-bench-2-offline-selected_0.10 +# 2. 容器内: ais_bench <此配置> --debug # path 自动从 env var 读 +# +# 切到其它 tier 时只改 bootstrap.sh --datasets 参数即可(4 套常用路径): +# full: /data/datasets/harbor/full/terminal-bench-2 (89 case) +# mini-0.10: /data/datasets/harbor/mini-0.10/terminal-bench-2-offline-selected_0.10 (7 case, 默认) +# mini-0.14: /data/datasets/harbor/mini-0.14/terminal-bench-2-offline-selected_0.14 (10 case) +# mini-0.20: /data/datasets/harbor/mini-0.20/terminal-bench-2-offline-selected_0.20 (14 case) +# +# 不使用 agent_runtime 容器方案时:直接 export AISBENCH_AGENT_DATASET_PATH= 即可; +# 不 export 时使用下方 DEFAULT_DATASET_FALLBACK 兜底。 +DEFAULT_DATASET_PATH = '{{$AISBENCH_AGENT_DATASET_PATH:/data/datasets/harbor/mini-0.10/terminal-bench-2-offline-selected_0.10}}' + models = [ dict( abbr="terminus-2", @@ -59,7 +80,8 @@ environment_type="docker", # -e/--env: 环境类型 (docker, daytona, e2b, modal) environment_force_build=False, # --force-build/--no-force-build: 是否强制重建环境 environment_delete=False, # --delete/--no-delete: 完成后是否删除环境 - path="/path/to/terminal-bench-2/", # -p/--path: 本地数据集路径 + path=DEFAULT_DATASET_PATH, # -p/--path: 本地数据集路径(需与 bootstrap.sh --datasets 传入路径对齐) + # 切换 tier:把上面 DEFAULT_DATASET_PATH 改成 full / mini-0.14 / mini-0.20 对应路径 dataset_name_version=None, # -d/--dataset: 远程数据集名称@版本 task_names=None, # --include-task-name: 包含的任务名称(支持glob模式)例如 ["task_name1", "task_name2"] exclude_task_names=None, # --exclude-task-name: 排除的任务名称 diff --git a/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_full.py b/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_full.py index e3efa693..cf409a3b 100644 --- a/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_full.py +++ b/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_full.py @@ -4,6 +4,21 @@ from ais_bench.benchmark.tasks import SWEBenchInferTask, SWEBenchEvalTask from ais_bench.benchmark.summarizers import SWEBenchSummarizer +# 数据集路径:用 mmengine 的 {{$VAR:default}} 占位符从环境变量读取。 +# mmengine 在执行本文件前,会先用 os.environ['VAR'] 的值替换整个 {{$VAR:default}} +# 占位符;如果环境变量未设置,则替换为 default 部分(此处为空 → 沿用 HF 在线下载)。 +# +# 这里 AISBENCH_AGENT_DATASET_PATH 的值就是 bootstrap.sh --datasets 传入的完整路径, +# 由 bootstrap.sh 自动注入到容器环境变量中,无需 import sys / import os 任何库。 +# +# 使用流程: +# 1. 物理机: bash bootstrap.sh --datasets /data/datasets/swebench/<...> +# 2. 容器内: ais_bench <此配置> --debug # path 自动从 env var 读 +# +# 不使用 agent_runtime 容器方案时:直接 export AISBENCH_AGENT_DATASET_PATH= 即可; +# 不 export 时回落到 HF 在线下载(默认行为)。 +DEFAULT_DATASET_PATH = '{{$AISBENCH_AGENT_DATASET_PATH:}}' + STEP_LIMIT = 200 models = [ @@ -31,7 +46,8 @@ type=SWEBenchDataset, abbr="swebench_full", # Relative to AIS_BENCH_DATASETS_CACHE (default: project root); missing dir -> HF snapshot_download - path="", + # 本字段通过环境变量 AISBENCH_AGENT_DATASET_PATH 注入;不设置时为空,沿用 HF 在线下载 + path=DEFAULT_DATASET_PATH, name="full", split="test", step_limit=STEP_LIMIT, diff --git a/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_lite.py b/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_lite.py index d218d61d..93a548a7 100644 --- a/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_lite.py +++ b/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_lite.py @@ -4,6 +4,21 @@ from ais_bench.benchmark.tasks import SWEBenchInferTask, SWEBenchEvalTask from ais_bench.benchmark.summarizers import SWEBenchSummarizer +# 数据集路径:用 mmengine 的 {{$VAR:default}} 占位符从环境变量读取。 +# mmengine 在执行本文件前,会先用 os.environ['VAR'] 的值替换整个 {{$VAR:default}} +# 占位符;如果环境变量未设置,则替换为 default 部分(此处为空 → 沿用 HF 在线下载)。 +# +# 这里 AISBENCH_AGENT_DATASET_PATH 的值就是 bootstrap.sh --datasets 传入的完整路径, +# 由 bootstrap.sh 自动注入到容器环境变量中,无需 import sys / import os 任何库。 +# +# 使用流程: +# 1. 物理机: bash bootstrap.sh --datasets /data/datasets/swebench/<...> +# 2. 容器内: ais_bench <此配置> --debug # path 自动从 env var 读 +# +# 不使用 agent_runtime 容器方案时:直接 export AISBENCH_AGENT_DATASET_PATH= 即可; +# 不 export 时回落到 HF 在线下载(默认行为)。 +DEFAULT_DATASET_PATH = '{{$AISBENCH_AGENT_DATASET_PATH:}}' + STEP_LIMIT = 200 models = [ @@ -30,8 +45,9 @@ dict( type=SWEBenchDataset, abbr="swebench_lite", - # Relative to AIS_BENCH_DATASETS_CACHE (default: project root); missing -> HF download - path="", + # Relative to AIS_BENCH_DATASETS_CACHE (default: project root); missing dir -> HF snapshot_download + # 本字段通过环境变量 AISBENCH_AGENT_DATASET_PATH 注入;不设置时为空,沿用 HF 在线下载 + path=DEFAULT_DATASET_PATH, name="lite", split="test", filter_spec="", diff --git a/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_multilingual.py b/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_multilingual.py index 3f3cd5a4..a48f82ab 100644 --- a/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_multilingual.py +++ b/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_multilingual.py @@ -4,6 +4,21 @@ from ais_bench.benchmark.tasks import SWEBenchInferTask, SWEBenchEvalTask from ais_bench.benchmark.summarizers import SWEBenchSummarizer +# 数据集路径:用 mmengine 的 {{$VAR:default}} 占位符从环境变量读取。 +# mmengine 在执行本文件前,会先用 os.environ['VAR'] 的值替换整个 {{$VAR:default}} +# 占位符;如果环境变量未设置,则替换为 default 部分(此处为空 → 沿用 HF 在线下载)。 +# +# 这里 AISBENCH_AGENT_DATASET_PATH 的值就是 bootstrap.sh --datasets 传入的完整路径, +# 由 bootstrap.sh 自动注入到容器环境变量中,无需 import sys / import os 任何库。 +# +# 使用流程: +# 1. 物理机: bash bootstrap.sh --datasets /data/datasets/swebench/<...> +# 2. 容器内: ais_bench <此配置> --debug # path 自动从 env var 读 +# +# 不使用 agent_runtime 容器方案时:直接 export AISBENCH_AGENT_DATASET_PATH= 即可; +# 不 export 时回落到 HF 在线下载(默认行为)。 +DEFAULT_DATASET_PATH = '{{$AISBENCH_AGENT_DATASET_PATH:}}' + STEP_LIMIT = 200 models = [ @@ -31,7 +46,8 @@ type=SWEBenchDataset, abbr="swebench_multilingual", # Relative to AIS_BENCH_DATASETS_CACHE (default: project root); missing -> HF download - path="", + # 本字段通过环境变量 AISBENCH_AGENT_DATASET_PATH 注入;不设置时为空,沿用 HF 在线下载 + path=DEFAULT_DATASET_PATH, name="multilingual", split="test", step_limit=STEP_LIMIT, diff --git a/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_multilingual_mini.py b/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_multilingual_mini.py index 0bed2c06..942499e9 100644 --- a/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_multilingual_mini.py +++ b/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_multilingual_mini.py @@ -4,6 +4,22 @@ from ais_bench.benchmark.tasks import SWEBenchInferTask, SWEBenchEvalTask from ais_bench.benchmark.summarizers import SWEBenchSummarizer +# 数据集路径:用 mmengine 的 {{$VAR:default}} 占位符从环境变量读取。 +# mmengine 在执行本文件前,会先用 os.environ['VAR'] 的值替换整个 {{$VAR:default}} +# 占位符;如果环境变量未设置,则替换为 default 部分(此处为空 → HF 在线下载会失败)。 +# +# 这里 AISBENCH_AGENT_DATASET_PATH 的值就是 bootstrap.sh --datasets 传入的完整路径, +# 由 bootstrap.sh 自动注入到容器环境变量中,无需 import sys / import os 任何库。 +# +# 使用流程: +# 1. 物理机: 下载 mini 数据集并准备目录(HF 无该数据集) +# https://modelers.cn/datasets/AISBench/SWE-Bench_Multilingual_mini +# 2. 物理机: bash bootstrap.sh --datasets /data/datasets/swebench/ +# 3. 容器内: ais_bench <此配置> --debug # path 自动从 env var 读 +# +# 不使用 agent_runtime 容器方案时:直接 export AISBENCH_AGENT_DATASET_PATH= 即可。 +DEFAULT_DATASET_PATH = '{{$AISBENCH_AGENT_DATASET_PATH:}}' + STEP_LIMIT = 200 models = [ @@ -31,7 +47,8 @@ type=SWEBenchDataset, abbr="swebench_multilingual_mini", # Relative to AIS_BENCH_DATASETS_CACHE (default: project root); missing -> HF download - path="", # Set local path to the mini dataset. Download from https://modelers.cn/datasets/AISBench/SWE-Bench_Multilingual_mini + # 本字段通过环境变量 AISBENCH_AGENT_DATASET_PATH 注入;mini 数据集 HF 无,必须 env var 注入本地路径 + path=DEFAULT_DATASET_PATH, name="multilingual", split="test", step_limit=STEP_LIMIT, diff --git a/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_verified.py b/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_verified.py index 2287c917..aebd0349 100644 --- a/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_verified.py +++ b/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_verified.py @@ -4,6 +4,21 @@ from ais_bench.benchmark.tasks import SWEBenchInferTask, SWEBenchEvalTask from ais_bench.benchmark.summarizers import SWEBenchSummarizer +# 数据集路径:用 mmengine 的 {{$VAR:default}} 占位符从环境变量读取。 +# mmengine 在执行本文件前,会先用 os.environ['VAR'] 的值替换整个 {{$VAR:default}} +# 占位符;如果环境变量未设置,则替换为 default 部分(此处为空 → 沿用 HF 在线下载)。 +# +# 这里 AISBENCH_AGENT_DATASET_PATH 的值就是 bootstrap.sh --datasets 传入的完整路径, +# 由 bootstrap.sh 自动注入到容器环境变量中,无需 import sys / import os 任何库。 +# +# 使用流程: +# 1. 物理机: bash bootstrap.sh --datasets /data/datasets/swebench/<...> +# 2. 容器内: ais_bench <此配置> --debug # path 自动从 env var 读 +# +# 不使用 agent_runtime 容器方案时:直接 export AISBENCH_AGENT_DATASET_PATH= 即可; +# 不 export 时回落到 HF 在线下载(默认行为)。 +DEFAULT_DATASET_PATH = '{{$AISBENCH_AGENT_DATASET_PATH:}}' + STEP_LIMIT = 200 models = [ @@ -31,7 +46,8 @@ type=SWEBenchDataset, abbr="swebench_verified", # Relative to AIS_BENCH_DATASETS_CACHE (default: project root); missing -> HF download - path="", + # 本字段通过环境变量 AISBENCH_AGENT_DATASET_PATH 注入;不设置时为空,沿用 HF 在线下载 + path=DEFAULT_DATASET_PATH, name="verified", split="test", step_limit=STEP_LIMIT, diff --git a/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_verified_mini.py b/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_verified_mini.py index fe424766..d4f96153 100644 --- a/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_verified_mini.py +++ b/ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_verified_mini.py @@ -4,6 +4,21 @@ from ais_bench.benchmark.tasks import SWEBenchInferTask, SWEBenchEvalTask from ais_bench.benchmark.summarizers import SWEBenchSummarizer +# 数据集路径:用 mmengine 的 {{$VAR:default}} 占位符从环境变量读取。 +# mmengine 在执行本文件前,会先用 os.environ['VAR'] 的值替换整个 {{$VAR:default}} +# 占位符;如果环境变量未设置,则替换为 default 部分(此处为空 → 沿用 HF 在线下载)。 +# +# 这里 AISBENCH_AGENT_DATASET_PATH 的值就是 bootstrap.sh --datasets 传入的完整路径, +# 由 bootstrap.sh 自动注入到容器环境变量中,无需 import sys / import os 任何库。 +# +# 使用流程: +# 1. 物理机: bash bootstrap.sh --datasets /data/datasets/swebench/<...> +# 2. 容器内: ais_bench <此配置> --debug # path 自动从 env var 读 +# +# 不使用 agent_runtime 容器方案时:直接 export AISBENCH_AGENT_DATASET_PATH= 即可; +# 不 export 时回落到 HF 在线下载(默认行为)。 +DEFAULT_DATASET_PATH = '{{$AISBENCH_AGENT_DATASET_PATH:}}' + STEP_LIMIT = 200 models = [ @@ -31,7 +46,8 @@ type=SWEBenchDataset, abbr="swebench_verified_mini", # Relative to AIS_BENCH_DATASETS_CACHE (default: project root); missing -> HF download - path="", + # 本字段通过环境变量 AISBENCH_AGENT_DATASET_PATH 注入;不设置时为空,沿用 HF 在线下载 + path=DEFAULT_DATASET_PATH, name="verified_mini", split="test", step_limit=STEP_LIMIT, diff --git a/ais_bench/configs/swe_bench_pro_examples/mini_swe_agent_swe_bench_pro_full.py b/ais_bench/configs/swe_bench_pro_examples/mini_swe_agent_swe_bench_pro_full.py index 5a09b4ea..cf17ac08 100644 --- a/ais_bench/configs/swe_bench_pro_examples/mini_swe_agent_swe_bench_pro_full.py +++ b/ais_bench/configs/swe_bench_pro_examples/mini_swe_agent_swe_bench_pro_full.py @@ -4,6 +4,21 @@ from ais_bench.benchmark.tasks import SWEBenchProInferTask, SWEBenchProEvalTask from ais_bench.benchmark.summarizers import SWEBenchProSummarizer +# 数据集路径:用 mmengine 的 {{$VAR:default}} 占位符从环境变量读取。 +# mmengine 在执行本文件前,会先用 os.environ['VAR'] 的值替换整个 {{$VAR:default}} +# 占位符;如果环境变量未设置,则替换为 default 部分(此处为空 → 沿用 HF 在线下载)。 +# +# 这里 AISBENCH_AGENT_DATASET_PATH 的值就是 bootstrap.sh --datasets 传入的完整路径, +# 由 bootstrap.sh 自动注入到容器环境变量中,无需 import sys / import os 任何库。 +# +# 使用流程: +# 1. 物理机: bash bootstrap.sh --datasets /data/datasets/swebench_pro/<...> +# 2. 容器内: ais_bench <此配置> --debug # path 自动从 env var 读 +# +# 不使用 agent_runtime 容器方案时:直接 export AISBENCH_AGENT_DATASET_PATH= 即可; +# 不 export 时回落到 HF 在线下载(默认行为)。 +DEFAULT_DATASET_PATH = '{{$AISBENCH_AGENT_DATASET_PATH:}}' + STEP_LIMIT = 250 models = [ @@ -33,7 +48,8 @@ dict( type=SWEBenchProDataset, abbr="swebench_pro_full_data", - path="", + # 本字段通过环境变量 AISBENCH_AGENT_DATASET_PATH 注入;不设置时为空,沿用 HF 在线下载 + path=DEFAULT_DATASET_PATH, name="full", split="test", step_limit=STEP_LIMIT, diff --git a/ais_bench/configs/swe_bench_pro_examples/mini_swe_agent_swe_bench_pro_mini.py b/ais_bench/configs/swe_bench_pro_examples/mini_swe_agent_swe_bench_pro_mini.py index 353b10bd..a137e268 100644 --- a/ais_bench/configs/swe_bench_pro_examples/mini_swe_agent_swe_bench_pro_mini.py +++ b/ais_bench/configs/swe_bench_pro_examples/mini_swe_agent_swe_bench_pro_mini.py @@ -4,6 +4,22 @@ from ais_bench.benchmark.tasks import SWEBenchProInferTask, SWEBenchProEvalTask from ais_bench.benchmark.summarizers import SWEBenchProSummarizer +# 数据集路径:用 mmengine 的 {{$VAR:default}} 占位符从环境变量读取。 +# mmengine 在执行本文件前,会先用 os.environ['VAR'] 的值替换整个 {{$VAR:default}} +# 占位符;如果环境变量未设置,则替换为 default 部分(此处为空 → mini 无 HF 源会失败)。 +# +# 这里 AISBENCH_AGENT_DATASET_PATH 的值就是 bootstrap.sh --datasets 传入的完整路径, +# 由 bootstrap.sh 自动注入到容器环境变量中,无需 import sys / import os 任何库。 +# +# 使用流程: +# 1. 物理机: 下载 mini 数据集并准备目录 +# https://modelers.cn/datasets/AISBench/SWE-Bench_Pro_mini +# 2. 物理机: bash bootstrap.sh --datasets /data/datasets/swebench_pro/ +# 3. 容器内: ais_bench <此配置> --debug # path 自动从 env var 读 +# +# 不使用 agent_runtime 容器方案时:直接 export AISBENCH_AGENT_DATASET_PATH= 即可。 +DEFAULT_DATASET_PATH = '{{$AISBENCH_AGENT_DATASET_PATH:}}' + STEP_LIMIT = 250 models = [ @@ -33,7 +49,8 @@ dict( type=SWEBenchProDataset, abbr="swebench_pro_mini_data", - path="", + # 本字段通过环境变量 AISBENCH_AGENT_DATASET_PATH 注入;mini 数据集 HF 无,必须 env var 注入本地路径 + path=DEFAULT_DATASET_PATH, name="mini", split="test", step_limit=STEP_LIMIT, diff --git a/docker/OVERVIEW.en.md b/docker/OVERVIEW.en.md index a781e171..a5849be3 100644 --- a/docker/OVERVIEW.en.md +++ b/docker/OVERVIEW.en.md @@ -185,6 +185,59 @@ docker build \ The image ships with Docker Engine (≥ 20.0) and Docker Compose v2 (≥ 2.0.0). There are two modes for running Docker inside the container — pick one based on your host Docker version and isolation needs. ### Mode A — Docker-in-Docker (recommended, true nested containers, requires host Docker ≥ 20.10 + cgroup v2) +```bash +┌────────────────────────── Host ──────────────────────────┐ +│ │ +│ ┌──────────────────┐ ┌──────────────────────────┐ │ +│ │ Host Kernel │ │ Host dockerd │ │ +│ │ (cgroup v2) │◀───────▶│ /var/run/docker.sock │ │ +│ └──────────────────┘ │ manages host's own │ │ +│ │ containers │ │ +│ └──────────────────────────┘ │ +│ │ +│ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ │ +│ ┃ AISBench Container (--privileged --cgroupns=host) ┃ │ +│ ┃ ┃ │ +│ ┃ ┌──────────────────────┐ ┌──────────────────────┐ ┃ │ +│ ┃ │ Docker CLI │────▶│ Inner dockerd │ ┃ │ +│ ┃ │ (user runs) │ │ (independent proc │ ┃ │ +│ ┃ └──────────────────────┘ │ inside container) │ ┃ │ +│ ┃ │ daemon.json: │ ┃ │ +│ ┃ │ native.cgroupdriver │ ┃ │ +│ ┃ │ =cgroupfs │ ┃ │ +│ ┃ │ storage=vfs │ ┃ │ +│ ┃ └──────────┬───────────┘ ┃ │ +│ ┃ │ spawn ┃ │ +│ ┃ ▼ ┃ │ +│ ┃ ┌──────────────────────┐ ┃ │ +│ ┃ │ containerd │ ┃ │ +│ ┃ │ (embedded in │ ┃ │ +│ ┃ │ inner dockerd) │ ┃ │ +│ ┃ └──────────┬───────────┘ ┃ │ +│ ┃ │ ┃ │ +│ ┃ ▼ ┃ │ +│ ┃ ┌──────────────────────┐ ┃ │ +│ ┃ │ Nested child │ ┃ │ +│ ┃ │ container │ ┃ │ +│ ┃ │ (truly isolated │ ┃ │ +│ ┃ │ namespaces) │ ┃ │ +│ ┃ │ ┌────────────────┐ │ ┃ │ +│ ┃ │ │ Agent process │ │ ┃ │ +│ ┃ │ │ OpenBLAS │ │ ┃ │ +│ ┃ │ │ Python deps │ │ ┃ │ +│ ┃ │ └────────────────┘ │ ┃ │ +│ ┃ └──────────────────────┘ ┃ │ +│ ┃ ┃ │ +│ ┃ —— Isolation boundary: independent PID/IPC/Net/Mount/User ┃ │ +│ ┃ namespaces —— ┃ │ +│ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ +│ │ +│ ✦ Nested child containers do NOT appear in host `docker ps` │ +│ ✦ Child containers inherit Docker's official default seccomp │ +│ profile (no clone3 blocking) │ +│ ✦ Host dockerd restart does NOT affect inner dockerd │ +└──────────────────────────────────────────────────────────────────┘ +``` A standalone `dockerd` is started inside the container, so child containers are fully isolated from the host. This is the **preferred mode** for agent benchmarks: child containers inherit Docker's official default seccomp profile, so the `pthread_create` / `clone3` block triggered by openEuler / RHEL hardened profiles does not occur; and there is no stale-socket issue when the host's `dockerd` restarts. @@ -250,6 +303,47 @@ docker compose version - For very long-running DinD workloads, consider adding `"default-runtime": "runc"`, `"log-driver": "json-file"`, and `"data-root"` overrides to `/etc/docker/daemon.json`. ### Mode B — Socket Passthrough (works with any Docker version ≥ 1.0) +```bash +┌────────────────────────── Host ──────────────────────────┐ +│ │ +│ ┌──────────────────┐ │ +│ │ Host Kernel │ │ +│ └──────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────┐ bind mount │ +│ │ Host dockerd │ /var/run/docker │ +│ │ /var/run/docker.sock ──────────────────────────┐ │ +│ │ (manages host's own containers) │ │ │ +│ └────────────┬─────────────────────────────┘ │ │ +│ │ │ │ +│ │ actually creates/manages │ │ +│ ▼ │ │ +│ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━┓ │ +│ ┃ AISBench Container (--privileged, shares PID/IPC w/ host)┃ │ +│ ┃ ┃ │ +│ ┃ ┌──────────────────────┐ ┃ │ +│ ┃ │ Docker CLI │── HTTP/Unix socket call ──────┘ ┃ │ +│ ┃ │ (user runs) │ no dockerd process inside ┃ │ +│ ┃ └──────────────────────┘ ┃ │ +│ ┃ ┃ │ +│ ┃ —— Shares kernel, PID, IPC namespaces with host —— ┃ │ +│ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ +│ │ +│ ┌──────────────────────────────────────────┐ │ +│ │ Child container (actually on the host) │ │ +│ │ ┌────────────────┐ │ │ +│ │ │ Agent process │ ← created/collected │ │ +│ │ │ OpenBLAS │ by host dockerd │ │ +│ │ │ Python deps │ │ │ +│ │ └────────────────┘ │ │ +│ └──────────────────────────────────────────┘ │ +│ │ +│ ✦ Child containers appear in host `docker ps` │ +│ ✦ Child containers inherit host dockerd's seccomp profile │ +│ (openEuler/RHEL hardened profile → clone3 blocking) │ +│ ✦ Host dockerd restart → socket inode stale → `docker restart`│ +└──────────────────────────────────────────────────────────────────┘ +``` Mount the host's Docker socket so that `docker run` inside the container actually creates containers on the **host** daemon. Use this mode only when the host Docker is older than 20.10, or when cgroup v2 is unavailable and Mode A cannot be used. @@ -369,6 +463,44 @@ docker compose -f /tmp/docker-compose.yml up - [Sysbox](https://github.com/nestybox/sysbox) — a container runtime that supports nested containers without `--privileged`, at the cost of installing a custom runtime on the host. - Rootless Docker — runs `dockerd` as a non-root user; has its own limitations (no `overlay2` on most distros, network restrictions, etc.). +## Agent Evaluation One-Click Environment Preparation + +For agent benchmarks such as Harbor Terminal-Bench, SWE-bench, and SWE-bench Pro, this repository provides a one-click environment preparation solution in [`docker/agent_runtime/`](agent_runtime/README.md). It consolidates the Mode A/B selection, `daemon.json` configuration, `--cgroupns=host`, and seccomp handling from the "Running Agent / Sandbox Benchmarks" section above into a single script, and additionally solves: + +- **Dependency conflicts**: Dependencies of multiple agent benchmarks conflict with each other (e.g., harbor forces upgrading `datasets` to 4.0+, and two `mini-swe-agent` forks override each other with the same package name). Solved via multi-venv isolation inside the runtime image. +- **Huge case images**: SWE-bench full (~1TB) cannot be packaged as a whole, so it is **not** baked into the runtime image. Users `docker pull` / `docker load` themselves. +- **Frequent dataset versions**: Agent dataset versions change quickly, so they are **not** baked into the runtime image. Users prepare them on the host and mount them into the container via `bootstrap.sh --datasets ` (container path = host path). +- **No environment verification**: `doctor.sh` verifies the runtime configuration (L1 static) before running the benchmark + scans for case image presence (warning), and gives precise fix guidance on failure. + +**Usage** (all three packs use the same workflow; choose `--datasets` and `agent_env` per benchmark): + +```bash +# Host: prepare the dataset directory + start the runtime container with one click +# (mounts the dataset directory into the container) +# Harbor: +mkdir -p /data/datasets/harbor/mini-0.10 +# Prepare dataset (see harbor_bench.md) + case image (can be loaded offline via bootstrap --case-tar) +curl -fsSL https://aisbench.obs.cn-north-4.myhuaweicloud.com/agent/ais_bench_agent_bootstrap.sh \ + | bash -s -- \ + --datasets /data/datasets/harbor/mini-0.10/terminal-bench-2-offline-selected_0.10 \ + --case-tar /data/cases/case-tb2-mini-0.10.tar.gz +docker exec -it ais_bench_agent bash +# Inside the container: +ais_bench_agent_doctor.sh harbor +agent_env harbor +ais_bench ais_bench/configs/agent_example/harbor_terminal_bench_2_task.py --debug + +# SWE-bench / SWE-bench Pro work the same way: +# --datasets +# agent_env swebench | swebench_pro +# ais_bench ais_bench/configs/swe_bench[_pro]_examples/... --debug +# For dataset and case image acquisition, see each benchmark's documentation. +``` + +For the solution design and per-script parameters, see [`docker/agent_runtime/README.md`](agent_runtime/README.md). Each agent benchmark doc ([harbor_bench.md](../docs/source_en/extended_benchmark/agent/harbor_bench.md), [swe_bench.md](../docs/source_en/extended_benchmark/agent/swe_bench.md), [swe_bench_pro.md](../docs/source_en/extended_benchmark/agent/swe_bench_pro.md)) also has a "Quick Start" section at the top. + +> This section is the executable packaging of the "Running Agent / Sandbox Benchmarks" section. To understand the principles, still read the Mode A/B sections above; for quick start, just use the script in this section. + ## License / Disclaimer This project's images and build scripts are licensed under the [LICENSE file](https://github.com/AISBench/benchmark/blob/master/LICENSE) in the repository root. diff --git a/docker/OVERVIEW.zh.md b/docker/OVERVIEW.zh.md index ccc26dba..0db6f6b7 100644 --- a/docker/OVERVIEW.zh.md +++ b/docker/OVERVIEW.zh.md @@ -171,6 +171,55 @@ docker build \ 镜像预装了 Docker Engine(≥ 20.0)与 Docker Compose v2(≥ 2.0.0)。在容器内启动 Docker 有两种模式,请根据宿主 Docker 版本与隔离需求二选一。 ### 模式 A:Docker-in-Docker(推荐,真嵌套容器,要求宿主 Docker ≥ 20.10 + cgroup v2) +<<<<<<< HEAD +```bash +┌────────────────────────── 宿主机 (Host) ──────────────────────────┐ +│ │ +│ ┌──────────────────┐ ┌──────────────────────────┐ │ +│ │ Host Kernel │ │ Host dockerd │ │ +│ │ (cgroup v2) │◀───────▶│ /var/run/docker.sock │ │ +│ └──────────────────┘ │ 管理宿主自身的容器 │ │ +│ └──────────────────────────┘ │ +│ │ +│ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ │ +│ ┃ AISBench Container (--privileged --cgroupns=host) ┃ │ +│ ┃ ┃ │ +│ ┃ ┌──────────────────────┐ ┌──────────────────────┐ ┃ │ +│ ┃ │ Docker CLI │────▶│ Inner dockerd │ ┃ │ +│ ┃ │ (用户执行) │ │ (容器内独立进程) │ ┃ │ +│ ┃ └──────────────────────┘ │ daemon.json: │ ┃ │ +│ ┃ │ native.cgroupdriver │ ┃ │ +│ ┃ │ =cgroupfs │ ┃ │ +│ ┃ │ storage=vfs │ ┃ │ +│ ┃ └──────────┬───────────┘ ┃ │ +│ ┃ │ spawn ┃ │ +│ ┃ ▼ ┃ │ +│ ┃ ┌──────────────────────┐ ┃ │ +│ ┃ │ containerd │ ┃ │ +│ ┃ │ (内嵌在 inner d) │ ┃ │ +│ ┃ └──────────┬───────────┘ ┃ │ +│ ┃ │ ┃ │ +│ ┃ ▼ ┃ │ +│ ┃ ┌──────────────────────┐ ┃ │ +│ ┃ │ 嵌套子容器 │ ┃ │ +│ ┃ │ (真隔离命名空间) │ ┃ │ +│ ┃ │ ┌────────────────┐ │ ┃ │ +│ ┃ │ │ Agent 进程 │ │ ┃ │ +│ ┃ │ │ OpenBLAS │ │ ┃ │ +│ ┃ │ │ Python deps │ │ ┃ │ +│ ┃ │ └────────────────┘ │ ┃ │ +│ ┃ └──────────────────────┘ ┃ │ +│ ┃ ┃ │ +│ ┃ —— 隔离边界:独立 PID/IPC/Net/Mount/User 命名空间 —— ┃ │ +│ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ +│ │ +│ ✦ 嵌套子容器不出现在宿主 `docker ps` 中 │ +│ ✦ 子容器继承 Docker 官方默认 seccomp profile(无 clone3 拦截) │ +│ ✦ 宿主 dockerd 重启不会影响 inner dockerd │ +└──────────────────────────────────────────────────────────────────┘ +``` +======= +>>>>>>> master_center 容器内自起一个独立的 `dockerd`,子容器与宿主机完全隔离。这是 agent 测评的**首选模式**:子容器继承的是 Docker 官方默认 seccomp profile,不会触发 openEuler / RHEL 加固 profile 导致的 `pthread_create` / `clone3` 拦截问题;也不存在宿主 dockerd 重启后 socket 句柄失效的问题。 @@ -236,6 +285,50 @@ docker compose version - 对于长时间运行的 DinD 场景,建议在 `/etc/docker/daemon.json` 中加入 `"default-runtime": "runc"`、`"log-driver": "json-file"`、`"data-root"` 等调优项。 ### 模式 B:Socket 代理(兼容任意 Docker 版本 ≥ 1.0) +<<<<<<< HEAD +```bash +┌────────────────────────── 宿主机 (Host) ──────────────────────────┐ +│ │ +│ ┌──────────────────┐ │ +│ │ Host Kernel │ │ +│ └──────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────┐ bind mount │ +│ │ Host dockerd │ /var/run/docker │ +│ │ /var/run/docker.sock ──────────────────────────┐ │ +│ │ (管理宿主自身的容器) │ │ │ +│ └────────────┬─────────────────────────────┘ │ │ +│ │ │ │ +│ │ 实际创建/管理 │ │ +│ ▼ │ │ +│ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━┓ │ +│ ┃ AISBench Container (--privileged, 与宿主共享 PID/IPC) ┃ │ +│ ┃ ┃ │ +│ ┃ ┌──────────────────────┐ ┃ │ +│ ┃ │ Docker CLI │──── HTTP/Unix socket 调用 ─────┘ ┃ │ +│ ┃ │ (用户执行) │ 容器内无 dockerd 进程 ┃ │ +│ ┃ └──────────────────────┘ ┃ │ +│ ┃ ┃ │ +│ ┃ —— 与宿主共享内核、PID、IPC 命名空间 —— ┃ │ +│ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ +│ │ +│ ┌──────────────────────────────────────────┐ │ +│ │ 子容器 (实为宿主上的容器) │ │ +│ │ ┌────────────────┐ │ │ +│ │ │ Agent 进程 │ ← 由 Host dockerd │ │ +│ │ │ OpenBLAS │ 直接创建/回收 │ │ +│ │ │ Python deps │ │ │ +│ │ └────────────────┘ │ │ +│ └──────────────────────────────────────────┘ │ +│ │ +│ ✦ 嵌套子容器出现在宿主 `docker ps` 中 │ +│ ✦ 子容器继承宿主 dockerd 的 seccomp profile │ +│ (openEuler/RHEL 加固 profile → 触发 clone3 拦截) │ +│ ✦ 宿主 dockerd 重启 → socket inode 失效 → 需 `docker restart` │ +└──────────────────────────────────────────────────────────────────┘ +``` +======= +>>>>>>> master_center 挂载宿主的 Docker socket,使容器内的 `docker run` 实际上在**宿主机** daemon 上创建容器。仅当宿主 Docker 版本低于 20.10、或不支持 cgroup v2 无法使用模式 A 时再选用本模式。 @@ -354,6 +447,46 @@ docker compose -f /tmp/docker-compose.yml up - [Sysbox](https://github.com/nestybox/sysbox)——支持嵌套容器而无需 `--privileged`,代价是要在宿主机装自定义运行时。 - Rootless Docker——以非 root 用户运行 `dockerd`,但有限制(多数发行版不支持 `overlay2`、网络限制等)。 +<<<<<<< HEAD +## Agent 测评一键环境准备 + +针对 Harbor Terminal-Bench、SWE-bench、SWE-bench Pro 等 agent 测评,本仓库在 [`docker/agent_runtime/`](agent_runtime/README.md) 提供了一键环境准备方案,将上文"运行 Agent / 沙箱类测评"章节的模式 A/B 选择、`daemon.json` 配置、`--cgroupns=host`、seccomp 处理等步骤收敛为一个脚本,并额外解决了: + +- **依赖冲突**:多个 agent 测评的依赖互相冲突(如 harbor 强制升级 datasets 到 4.0+、两个 `mini-swe-agent` fork 同包名互相覆盖),通过 runtime 镜像内的多 venv 隔离 +- **case 镜像庞大**:SWE-bench full ~1TB 不能整体打包,**不**预置到 runtime 镜像,由用户自行 docker pull / load +- **数据集版本频繁**:agent 数据集版本变化快,**不**预置到 runtime 镜像,由用户在物理机准备好,通过 `bootstrap.sh --datasets ` 挂载进容器(容器内路径 = 宿主路径) +- **环境无验证**:`doctor.sh` 在跑测评前验证 runtime 配置(L1 静态)+ case 镜像存在性扫描(warning),失败时给精确修复指引 + +**用户使用**(三个 pack 都用同一条流程,按 benchmark 选 `--datasets` 和 `agent_env`): + +```bash +# 物理机:准备数据集目录 + 一键起 runtime 容器(把数据集目录挂载进容器) +# Harbor: +mkdir -p /data/datasets/harbor/mini-0.10 +# 准备数据集(见 harbor_bench.md)+ case 镜像(可用 bootstrap --case-tar 离线加载) +curl -fsSL https://aisbench.obs.cn-north-4.myhuaweicloud.com/agent/ais_bench_agent_bootstrap.sh \ + | bash -s -- \ + --datasets /data/datasets/harbor/mini-0.10/terminal-bench-2-offline-selected_0.10 \ + --case-tar /data/cases/case-tb2-mini-0.10.tar.gz +docker exec -it ais_bench_agent bash +# 容器内: +ais_bench_agent_doctor.sh harbor +agent_env harbor +ais_bench ais_bench/configs/agent_example/harbor_terminal_bench_2_task.py --debug + +# SWE-bench / SWE-bench Pro 同理: +# --datasets +# agent_env swebench | swebench_pro +# ais_bench ais_bench/configs/swe_bench[_pro]_examples/... --debug +# 数据集与 case 镜像获取详见各 benchmark 文档 +``` + +方案设计与各脚本参数详见 [`docker/agent_runtime/README.md`](agent_runtime/README.md)。各 agent 测评文档([harbor_bench.md](../docs/source_zh_cn/extended_benchmark/agent/harbor_bench.md)、[swe_bench.md](../docs/source_zh_cn/extended_benchmark/agent/swe_bench.md)、[swe_bench_pro.md](../docs/source_zh_cn/extended_benchmark/agent/swe_bench_pro.md))开头也已加入"快速上手"引导段。 + +> 本节是"运行 Agent / 沙箱类测评"章节的可执行化封装。理解原理仍建议阅读上文模式 A/B 章节;快速上手直接用本节脚本即可。 + +======= +>>>>>>> master_center ## 许可证 / 免责声明 本项目镜像及其构建脚本按仓库根目录的 [LICENSE 文件](https://github.com/AISBench/benchmark/blob/master/LICENSE) 授权。 diff --git a/docker/agent_runtime/Dockerfile.agent-runtime b/docker/agent_runtime/Dockerfile.agent-runtime new file mode 100644 index 00000000..a5f3103a --- /dev/null +++ b/docker/agent_runtime/Dockerfile.agent-runtime @@ -0,0 +1,310 @@ +# AISBench Agent Runtime Dockerfile +# +# 在现有 aisbench_benchmark 基础镜像(已含 docker engine + compose + ais_bench)之上, +# 追加多 venv 隔离层,解决 agent 测评之间的依赖冲突: +# - harbor venv:隔离 harbor==0.20.0 强制升级的 datasets 4.0+,不污染主环境 +# + harbor_task.py 用 subprocess 启动 venv python,所以 venv 内还 +# 放了一个 ais_bench wrapper(shebang 指向 venv python), +# 让用户 `agent_env harbor` 后 which ais_bench 指向 venv 内 +# - swebench venv:装 AISBench fork 的 mini-swe-agent + SWE-bench harness +# - swebench_pro venv:装 scaleapi fork 的 mini-swe-agent + SWE-bench_Pro-os +# +# 三个 venv 都使用 --system-site-packages --copies: +# --system-site-packages: 让 venv 看到系统已装的 ais_bench_benchmark(共用基镜像, +# 不重复装 torch / datasets / tiktoken 等大依赖) +# --copies: 强制 python 二进制是真实副本(不是 symlink),否则 subprocess 启动时会 +# 丢 venv 身份(找不到 venv local 的包) +# +# 构建方式(推荐使用 build_image_agent_runtime.sh): +# bash docker/agent_runtime/build_image_agent_runtime.sh \ +# --base-tag v3.1-20260522-master --os ubuntu24.04 --py-version py312 +# +# 或直接 docker build: +# docker build \ +# --network host \ +# -f docker/agent_runtime/Dockerfile.agent-runtime \ +# -t ghcr.io/aisbench/agent-runtime:v3.1-20260522-master-ubuntu24.04-py312-x86_64 \ +# --build-arg BASE_IMAGE=ghcr.io/aisbench/aisbench_benchmark:v3.1-20260522-master-ubuntu24.04-py312-x86_64 \ +# . +# +# BASE_IMAGE 必须通过 --build-arg 传入,无默认值,避免基镜像 tag 写死导致过期。 + +ARG BASE_IMAGE +# [v3 wrapper] 允许通过 build-arg 注入本地 harbor wheel 文件路径, +# 替换硬编码的 harbor==0.20.0。不传则走默认 PyPI 版本。 +# 占位文件 .harbor_wheel_cache.whl 由 build_image_agent_runtime.sh 保证存在 +# (空文件时回退到 harbor==0.20.0)。 +ARG HARBOR_WHEEL_FILE="" +FROM ${BASE_IMAGE} + +ENV DEBIAN_FRONTEND=noninteractive +ENV PYTHONUNBUFFERED=1 + +# agent runtime 相关路径 +ENV AGENT_VENVS_ROOT=/opt/venvs +ENV AGENT_PACKS_ROOT=/opt/agent-resources/packs +ENV SWEBENCH_DOCKERFILES_ROOT=/opt/swebench/dockerfiles + +RUN mkdir -p ${AGENT_VENVS_ROOT} \ + ${AGENT_PACKS_ROOT} \ + ${SWEBENCH_DOCKERFILES_ROOT} \ + /opt/src + +# ============================================================ +# venv: harbor +# harbor==0.20.0 会把 datasets 升到 4.0+,必须隔离在独立 venv +# 否则会污染 ais_bench 主环境,导致其他数据集依赖冲突 +# +# --system-site-packages: 让 venv 能看到基镜像里已经装好的 ais_bench_benchmark +# - ais_bench_benchmark 装在基镜像 /usr/local/lib/.../dist-packages/, +# 是 ais_bench 框架本体(benchmark / configs / datasets / tasks / ...) +# - venv 默认不读系统 site-packages,会导致 harbor_task.py 跑时找不到 ais_bench 包 +# - harbor 升级的 datasets 4.0+ 仍只装在 venv local site-packages, +# 不会污染系统环境(Python venv 优先用 local 包覆盖 system 包) +# +# --copies: 强制拷贝 python 二进制到 venv 内(默认是 symlink 到系统 python3.12) +# - harbor_task.py 通过 subprocess.Popen 以绝对路径启动 venv 的 python +# - 若 venv python 是 symlink,python 启动时通过 symlink 解析到 /usr/bin/python3.12, +# 丢失 venv 身份(不再读 venv 的 pyvenv.cfg),找不到 venv local 的 harbor 包 +# - 用 --copies 后 venv python 是真实二进制,python 通过真实路径识别 venv 身份, +# 正确加载 venv local site-packages +# ============================================================ +# [v3 wrapper] 占位文件由 build_image_agent_runtime.sh 保证存在: +# - 传了 --harbor-wheel 时 cp 真实 wheel 到此 +# - 未传时 touch 创建空文件(0 字节) +# Docker COPY 对空文件正常,RUN 中 [ -s ... ] 检测非空才安装自定义 wheel。 +COPY .harbor_wheel_cache.whl /tmp/harbor_wheel_cache.whl + +RUN python3.12 -m venv ${AGENT_VENVS_ROOT}/harbor --system-site-packages --copies && \ + ${AGENT_VENVS_ROOT}/harbor/bin/pip install --no-cache-dir --upgrade pip \ + -i https://repo.huaweicloud.com/repository/pypi/simple \ + --trusted-host repo.huaweicloud.com \ + --default-timeout=120 --retries=5 && \ + if [ -s /tmp/harbor_wheel_cache.whl ]; then \ + echo "[ais_bench_agent] 使用本地 harbor wheel: /tmp/harbor_wheel_cache.whl"; \ + ${AGENT_VENVS_ROOT}/harbor/bin/pip install --no-cache-dir \ + -i https://repo.huaweicloud.com/repository/pypi/simple \ + --trusted-host repo.huaweicloud.com \ + --default-timeout=120 --retries=5 \ + /tmp/harbor_wheel_cache.whl; \ + else \ + echo "[ais_bench_agent] 使用默认 harbor==0.20.0"; \ + ${AGENT_VENVS_ROOT}/harbor/bin/pip install --no-cache-dir \ + -i https://repo.huaweicloud.com/repository/pypi/simple \ + --trusted-host repo.huaweicloud.com \ + --default-timeout=120 --retries=5 \ + harbor==0.20.0; \ + fi && \ + rm -f /tmp/harbor_wheel_cache.whl + +# ============================================================ +# harbor venv 内放置 ais_bench 命令(关键) +# +# 问题链路(已修过的两层坑): +# 1. venv 默认不读系统 site-packages → 加 --system-site-packages ✓ +# 2. venv python 是 symlink 时丢身份 → 加 --copies ✓ +# 3. **主进程 sys.executable 是系统 python**(ais_bench 命令装在系统不在 venv) +# → harbor_task.py:44 python = sys.executable 拿到 /usr/bin/python3.12 +# → subprocess 启动系统 python(不是 venv python) +# → 系统 python 找不到 venv local 的 harbor 包 +# +# 修复:把 ais_bench wrapper 放到 harbor venv/bin 下,shebang 指向 venv python +# - 用户 agent_env harbor 后,PATH 中 venv/bin 在前 +# - which ais_bench → /opt/venvs/harbor/bin/ais_bench(venv 内) +# - shell 解析 shebang 用 venv python 启动 ais_bench +# - 主进程 sys.executable = venv python +# - harbor_task.py:44 拿到的也是 venv python +# - subprocess 启动 venv python → 找到 venv local site-packages → import harbor OK +# +# 注意:不依赖基镜像 /usr/local/bin/ais_bench 的具体实现,直接 import 调用 main() +# ========================================================= +RUN cat > ${AGENT_VENVS_ROOT}/harbor/bin/ais_bench <<'PYEOF' +#!/opt/venvs/harbor/bin/python +# -*- coding: utf-8 -*- +"""harbor venv 内置 ais_bench wrapper +启动时使用 harbor venv 的 python,让 subprocess.Popen 启动 harbor venv 的 python, +从而能正确加载 venv local site-packages(harbor 0.20.0 在此)。 +""" +import re +import sys +try: + from ais_bench.benchmark.cli.main import main +except ImportError as e: + sys.stderr.write(f"[error] failed to import ais_bench: {e}\n") + sys.stderr.write("[hint] 检查 harbor venv 是否启用 --system-site-packages(需看到系统 ais_bench)\n") + sys.exit(2) +if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit(main()) +PYEOF +RUN chmod +x ${AGENT_VENVS_ROOT}/harbor/bin/ais_bench + +# ============================================================ +# Patch harbor 的 docker-compose-base.yaml +# 模式 B(Socket 代理)下子容器由宿主 dockerd 创建,会继承宿主 seccomp +# profile。openEuler/RHEL 的默认 profile 比 Docker 官方严格,会拦截 +# OpenBLAS/NumPy 初始化线程时使用的 clone3 调用,表现为: +# OpenBLAS blas_thread_init: pthread_create failed: Operation not permitted +# 详见 docker/OVERVIEW.zh.md 模式 B 常见问题 +# +# harbor 0.20.0 的 compose 模板路径(python3.12 site-packages): +# harbor/environments/docker/docker-compose-base.yaml +# 在其中给 main service 补上 security_opt: [seccomp=unconfined] +# 模式 A(DinD)下此 patch 无副作用(子容器本就用 Docker 官方默认 profile) +# ============================================================ +COPY docker/agent_runtime/patches/harbor_compose_patch.py /tmp/harbor_compose_patch.py +RUN HARBOR_SITEPKG=$(${AGENT_VENVS_ROOT}/harbor/bin/python -c "import harbor, os; print(os.path.dirname(harbor.__file__))") && \ + COMPOSE_FILE="${HARBOR_SITEPKG}/environments/docker/docker-compose-base.yaml" && \ + if [ -f "$COMPOSE_FILE" ]; then \ + cp "$COMPOSE_FILE" "$COMPOSE_FILE.bak" && \ + python3.12 /tmp/harbor_compose_patch.py "${COMPOSE_FILE}"; \ + else \ + echo "[warn] harbor compose 模板未找到: $COMPOSE_FILE,跳过 patch"; \ + fi + +# ============================================================ +# venv: swebench +# SWE-bench / SWE-bench Pro 两个 benchmark 各需要一个 mini-swe-agent fork, +# 同包名 minisweagent 互相覆盖,必须在两个独立 venv 内安装。 +# +# 仓库选择: +# - AISBench fork (https://github.com/AISBench/mini-swe-agent) 用于 SWE-bench +# - scaleapi fork (https://github.com/scaleapi/mini-swe-agent) 用于 SWE-bench Pro +# +# 需要 ais_bench wrapper(与 harbor 同机理): +# - system /usr/local/bin/ais_bench 的 shebang 指向 system python3.12,不会自动切到 venv python +# - 用户 agent_env swebench 后,PATH 首位是 /opt/venvs/swebench/bin, +# 但 swebench/bin/ 下若没有 ais_bench,shell 仍落到 /usr/local/bin/ais_bench +# - task 代码 (swebench_infer.py) 在主进程 import minisweagent, +# system python 看不到 venv-local 的 minisweagent → ModuleNotFoundError +# - 在 swebench/bin/ 放一个 ais_bench wrapper(shebang 指向 venv python), +# PATH 命中它后启动 venv python → 正确加载 venv-local 的 minisweagent +# - 同时通过 --system-site-packages 仍能看到系统的 ais_bench / datasets / tasks +# ============================================================ +RUN python3.12 -m venv ${AGENT_VENVS_ROOT}/swebench --system-site-packages --copies && \ + ${AGENT_VENVS_ROOT}/swebench/bin/pip install --no-cache-dir --upgrade pip \ + -i https://repo.huaweicloud.com/repository/pypi/simple \ + --trusted-host repo.huaweicloud.com \ + --default-timeout=120 --retries=5 && \ + git clone --depth 1 https://gh-proxy.com/https://github.com/AISBench/mini-swe-agent.git \ + /opt/src/mini-swe-agent-aisbench && \ + ${AGENT_VENVS_ROOT}/swebench/bin/pip install --no-cache-dir \ + -i https://repo.huaweicloud.com/repository/pypi/simple \ + --trusted-host repo.huaweicloud.com \ + --default-timeout=120 --retries=5 \ + -e /opt/src/mini-swe-agent-aisbench && \ + git clone --depth 1 https://gh-proxy.com/https://github.com/SWE-bench/SWE-bench.git \ + /opt/src/SWE-bench && \ + ${AGENT_VENVS_ROOT}/swebench/bin/pip install --no-cache-dir \ + -i https://repo.huaweicloud.com/repository/pypi/simple \ + --trusted-host repo.huaweicloud.com \ + --default-timeout=120 --retries=5 \ + -e /opt/src/SWE-bench + +# ais_bench wrapper for swebench venv(让主进程加载 venv-local 的 minisweagent) +RUN cat > ${AGENT_VENVS_ROOT}/swebench/bin/ais_bench <<'PYEOF' +#!/opt/venvs/swebench/bin/python +# -*- coding: utf-8 -*- +"""swebench venv 内置 ais_bench wrapper +启动时使用 swebench venv 的 python,让 task 代码 (swebench_infer.py) +在主进程 import minisweagent 时加载 venv-local 的 mini-swe-agent (AISBench fork)。 +""" +import re +import sys +try: + from ais_bench.benchmark.cli.main import main +except ImportError as e: + sys.stderr.write(f"[error] failed to import ais_bench: {e}\n") + sys.stderr.write("[hint] 检查 swebench venv 是否启用 --system-site-packages(需看到系统 ais_bench)\n") + sys.exit(2) +if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit(main()) +PYEOF +RUN chmod +x ${AGENT_VENVS_ROOT}/swebench/bin/ais_bench + +# ============================================================ +# venv: swebench_pro +# SWE-bench Pro 需用 scaleapi 适配的 mini-swe-agent 和官方 SWE-Bench_Pro-os +# 单独 venv 避免与 swebench venv 内的 mini-swe-agent fork 冲突 +# +# 同样需要 ais_bench wrapper(原因同上):主进程要加载 scaleapi fork 的 minisweagent +# +# ARM 限制:官方 docker 镜像均为 x86 架构,ARM 跑不动(runtime 仍可启动) +# ============================================================ +RUN python3.12 -m venv ${AGENT_VENVS_ROOT}/swebench_pro --system-site-packages --copies && \ + ${AGENT_VENVS_ROOT}/swebench_pro/bin/pip install --no-cache-dir --upgrade pip \ + -i https://repo.huaweicloud.com/repository/pypi/simple \ + --trusted-host repo.huaweicloud.com \ + --default-timeout=120 --retries=5 && \ + git clone --depth 1 https://gh-proxy.com/https://github.com/scaleapi/mini-swe-agent.git \ + /opt/src/mini-swe-agent-scaleapi && \ + ${AGENT_VENVS_ROOT}/swebench_pro/bin/pip install --no-cache-dir \ + -i https://repo.huaweicloud.com/repository/pypi/simple \ + --trusted-host repo.huaweicloud.com \ + --default-timeout=120 --retries=5 \ + -e /opt/src/mini-swe-agent-scaleapi && \ + git clone --depth 1 https://gh-proxy.com/https://github.com/scaleapi/SWE-bench_Pro-os.git \ + /opt/src/SWE-bench_Pro-os && \ + ${AGENT_VENVS_ROOT}/swebench_pro/bin/pip install --no-cache-dir \ + -i https://repo.huaweicloud.com/repository/pypi/simple \ + --trusted-host repo.huaweicloud.com \ + --default-timeout=120 --retries=5 \ + -r /opt/src/SWE-bench_Pro-os/requirements.txt + +# ais_bench wrapper for swebench_pro venv(让主进程加载 venv-local 的 minisweagent) +RUN cat > ${AGENT_VENVS_ROOT}/swebench_pro/bin/ais_bench <<'PYEOF' +#!/opt/venvs/swebench_pro/bin/python +# -*- coding: utf-8 -*- +"""swebench_pro venv 内置 ais_bench wrapper +启动时使用 swebench_pro venv 的 python,让 task 代码 (swebench_pro_infer.py) +在主进程 import minisweagent 时加载 venv-local 的 mini-swe-agent (scaleapi fork)。 +""" +import re +import sys +try: + from ais_bench.benchmark.cli.main import main +except ImportError as e: + sys.stderr.write(f"[error] failed to import ais_bench: {e}\n") + sys.stderr.write("[hint] 检查 swebench_pro venv 是否启用 --system-site-packages(需看到系统 ais_bench)\n") + sys.exit(2) +if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit(main()) +PYEOF +RUN chmod +x ${AGENT_VENVS_ROOT}/swebench_pro/bin/ais_bench + +# ============================================================ +# 脚本 + pack 清单 +# ============================================================ +COPY docker/agent_runtime/doctor.sh /usr/local/bin/ais_bench_agent_doctor.sh +RUN chmod +x /usr/local/bin/ais_bench_agent_doctor.sh + +COPY docker/agent_runtime/packs/ ${AGENT_PACKS_ROOT}/ + +# v3 D 批: L1/L2 baked image Jinja2 模板 +# 让 build_l2_baked_image.sh 在容器内可直接 docker build -f /opt/swebench/dockerfiles/... +# 模板来源:mini_matrix cli/swebench_dind/dockerfiles/(5 个 j2) +# - Dockerfile.l1-base.j2 FROM prebuilt → WORKDIR /testbed + /logs +# - Dockerfile.l2-agent-{aider,msa,oh,qwen}.j2 FROM L1 → 装对应 agent +COPY docker/agent_runtime/dockerfiles/ ${SWEBENCH_DOCKERFILES_ROOT}/ + +# ============================================================ +# 便捷函数:激活对应 venv +# 用户在容器内 `agent_env harbor` 即可激活 harbor venv, +# 然后照常执行原生 ais_bench 命令 +# ============================================================ +RUN cat >> /etc/bash.bashrc <<'EOF' + +# AISBench Agent venv 激活便捷函数 +agent_env() { + case "$1" in + harbor) source /opt/venvs/harbor/bin/activate ;; + swebench) source /opt/venvs/swebench/bin/activate 2>/dev/null || echo "venv swebench 未安装" ;; + swebench_pro) source /opt/venvs/swebench_pro/bin/activate 2>/dev/null || echo "venv swebench_pro 未安装" ;; + *) echo "usage: agent_env " ;; + esac +} +EOF + +WORKDIR /benchmark diff --git a/docker/agent_runtime/PR499_DESCRIPTION.md b/docker/agent_runtime/PR499_DESCRIPTION.md new file mode 100644 index 00000000..4f581864 --- /dev/null +++ b/docker/agent_runtime/PR499_DESCRIPTION.md @@ -0,0 +1,198 @@ +# AISBench Agent Runtime — 5 层 DinD 统一测评环境 + +为 AISBench Agent 测评(Harbor Terminal-Bench、SWE-bench、SWE-bench Pro)提供 +**镜像构建 + 容器启动 + 命令执行** 的完整运行时方案。 + +> 本 PR 是 AISBench/benchmark 的社区运行时补充,不改动核心评测逻辑。 + +## 架构总览 + +``` +宿主机用户 + │ + ├─ ais_bench_agent.sh build ← 一键构建 (v3 H 批) + │ └─ build_image_agent_runtime.sh [→ build_l2_baked_image.sh] + │ + └─ ais_bench_agent.sh run ← 拉起 + 自检 + 跑测试 (v3 H 批) + │ + └─ bootstrap.sh → docker run 启动 runtime 容器 + │ + ├─ 模式 A (DinD): --privileged --net=host + │ └─ 容器内 dockerd → harbor → trial 容器 + └─ 模式 B (Socket): -v /var/run/docker.sock + └─ 共享宿主 dockerd → harbor → trial 容器 + │ + ├─ datasets bind mount (宿主路径 = 容器内路径) + ├─ case 镜像 tar 自动 docker load + └─ 3 个隔离 venv: /opt/venvs/{harbor, swebench, swebench_pro} + +容器内 + ├─ ais_bench_agent_doctor.sh ← L1 自检 + ├─ agent_env ← 激活 venv + ├─ ais_bench_agent_run.sh ← harbor jobs start 包装 + ├─ ais_bench_agent_watch.sh ← 阻塞等 results.json + ├─ ais_bench_agent_summarize.sh ← 聚合 → md/csv/json + └─ ais_bench_agent_orchestrator_status.sh ← 5 段状态查询 +``` + +## 分层职责 + +| 层 | 脚本/文件 | 职责 | +|---|---|---| +| **入口** | `ais_bench_agent.sh` | 统一 facade:build / run / status / watch / summarize / doctor | +| **L4 镜像** | `Dockerfile.agent-runtime` + `build_image_agent_runtime.sh` | 在 aisbench_benchmark 基镜上追加 3 个隔离 venv | +| **L5 启动** | `ais_bench_agent_bootstrap.sh` + `safe_start_...` | 一键起 runtime 容器 (DinD/Socket + 挂数据集 + 加载 case tar) | +| **L3 调度** | `ais_bench_agent_{run,watch,summarize,orchestrator_status}.sh` | 在容器内调 harbor jobs / 等结果 / 汇总 | +| **L2 加速** | `build_l2_baked_image.sh` + 5 个 Jinja2 模板 | 预烤 agent 到 case 镜像,跳过 trial 内装包 | +| **L1 自检** | `doctor.sh` | 静态校验 docker / venv / pack / 资源(秒级) | + +## 快速拉起流程 + +> 📘 端到端复现手册(含 harbor offline + mini-swe-agent tarball + 真实 trial 验证 + 失败诚实声明)见 [REPRODUCE.md](REPRODUCE.md)。 + +### 1. 构建 runtime 镜像(宿主机) + +```bash +# 标准构建(用默认 harbor==0.20.0) +bash docker/agent_runtime/ais_bench_agent.sh build \ + --base-tag v3.1-20260522-master --push + +# 用自定义 harbor wheel(如 harbor-offline,替换默认 harbor) +bash docker/agent_runtime/ais_bench_agent.sh build \ + --base-tag v3.1-20260522-master \ + --harbor-wheel /path/to/harbor-offline.whl --push + +# 同时构建 L2 baked images +bash docker/agent_runtime/ais_bench_agent.sh build \ + --base-tag v3.1-20260522-master --l2 +``` + +### 2. 跑测评(宿主机) + +```bash +# Harbor Terminal-Bench(自动启容器 + doctor 自检 + 执行测试) +bash docker/agent_runtime/ais_bench_agent.sh run \ + --pack harbor \ + --datasets /data/harbor/mini-0.10/terminal-bench-2-offline-selected_0.10 \ + --matrix-yaml /opt/config/matrix.yaml \ + --api-key-file /opt/config/api_key.env + +# SWE-bench verified mini(--split 自动派生 config 文件) +bash docker/agent_runtime/ais_bench_agent.sh run \ + --pack swebench --split verified_mini \ + --datasets /data/swebench/verified \ + --matrix-yaml /opt/config/matrix.yaml + +# 自定义命令(透传到容器内,不自动推导) +bash docker/agent_runtime/ais_bench_agent.sh run \ + --pack harbor \ + --datasets /data/harbor/mini \ + --command "harbor jobs start -c /opt/swebench/config/my_matrix.yaml -n 2" + +# 离线模式(内网 / 无 ghcr.io 访问) +bash docker/agent_runtime/ais_bench_agent.sh run \ + --pack harbor \ + --datasets /data/harbor \ + --runtime-tar /opt/aisbench/agent-runtime.tar.gz \ + --case-tar /opt/aisbench/case-images.tar.gz +``` + +### 3. 查询 / 等待 / 汇总(宿主机) + +```bash +bash docker/agent_runtime/ais_bench_agent.sh status +bash docker/agent_runtime/ais_bench_agent.sh watch +bash docker/agent_runtime/ais_bench_agent.sh summarize +``` + +## Pack 智能化:`--pack` + `--split` 自动派生 config + +用户只需传 `--pack` 和 `--split`,wrapper 自动选择正确的 ais_bench config 文件: + +| `--pack` | `--split` | 自动推导 config 路径 | +|---|---|---| +| `harbor` | (不需要) | `ais_bench/configs/agent_example/harbor_terminal_bench_2_task.py` | +| `swebench` | (默认) / `lite` | `.../swe_bench_examples/mini_swe_agent_swe_bench_lite.py` | +| `swebench` | `verified` | `.../swe_bench_examples/mini_swe_agent_swe_bench_verified.py` | +| `swebench` | `verified_mini` | `.../swe_bench_examples/mini_swe_agent_swe_bench_verified_mini.py` | +| `swebench` | `full` | `.../swe_bench_examples/mini_swe_agent_swe_bench_full.py` | +| `swebench` | `multilingual` | `.../swe_bench_examples/mini_swe_agent_swe_bench_multilingual.py` | +| `swebench_pro` | (默认) / `mini` | `.../swe_bench_pro_examples/mini_swe_agent_swe_bench_pro_mini.py` | +| `swebench_pro` | `full` | `.../swe_bench_pro_examples/mini_swe_agent_swe_bench_pro_full.py` | + +非法 `--split` 会明确报错并提示合法值列表。 + +## 命令透传设计 + +`ais_bench_agent.sh run --command "..."` 将用户在宿主机交付的任意命令原样透传到容器内执行: + +``` +用户 CLI (宿主机) + │ ais_bench_agent.sh run --command "harbor jobs start ..." + ▼ +wrapper 自动完成: 派生 config → 检查/启容器 → bootstrap → doctor → docker exec + │ + ▼ +容器内: bash -c "harbor jobs start ..." + │ + ▼ +harbor → DinD → trial 容器 → /opt/swebench/jobs//result.json +``` + +## 目录结构(核心文件) + +``` +docker/agent_runtime/ +├── ais_bench_agent.sh ← 统一入口 (NEW) +├── Dockerfile.agent-runtime ← L4 runtime 镜像 +├── build_image_agent_runtime.sh ← 镜像构建脚本 +├── build_l2_baked_image.sh ← L2 baked image 构建 +├── ais_bench_agent_bootstrap.sh ← L5 一键起容器 +├── ais_bench_agent_entrypoint.sh ← 容器 ENTRYPOINT +├── ais_bench_agent_run.sh ← L3 harbor jobs 包装 +├── ais_bench_agent_watch.sh ← L3 等结果 +├── ais_bench_agent_summarize.sh ← L3 汇总 +├── ais_bench_agent_orchestrator_status.sh ← L3 状态查询 +├── safe_start_ais_bench_agent_bootstrap.sh ← watchdog 包装 +├── doctor.sh ← L1 自检 +├── scripts/ +│ ├── filter_matrix.py ← matrix 过滤 +│ └── summarize.py ← 汇总逻辑 +├── packs/ +│ ├── harbor.yaml +│ ├── swebench.yaml +│ └── swebench_pro.yaml +├── patches/ +│ └── harbor_compose_patch.py +└── dockerfiles/ + ├── Dockerfile.l1-base.j2 + ├── Dockerfile.l2-agent-aider.j2 + ├── Dockerfile.l2-agent-msa.j2 + ├── Dockerfile.l2-agent-oh.j2 + ├── Dockerfile.l2-agent-qwen.j2 + └── README.md +``` + +## 所有提交概览 + +### PR410 baseline (12 commits) +`b92b18c` → `25af9e0`: 基镜像 docker 安装、ais_bench configs 更新、Dockerfile + build 脚本 + bootstrap.sh + doctor.sh + packs + patches + 文档 + +### v3 A 批 — L4 镜像改造 (5 commits) +`7875023` A1: ARM64 host 适配 · `abb9024` A2: harbor 0.6.1 → 0.20.0 · `c2663f9` A3: DinD registry-mirrors 参数化 · `848ec98` A4: ENTRYPOINT + binfmt + daemon.json · `ce8a1f9` A5: /opt/swebench 预创建 + +### v3 B 批 — L5 launcher 改造 (6 commits) +`21200a4` B1: bootstrap.sh 新增 8 个参数 · `7f36db1` B2: --data-image · `3f89ec6` B3: bind mount + env 接入 · `ead9f51` B4: --production --restart unless-stopped · `c6fe5ad` B5: DOCKER_DEFAULT_PLATFORM · `3a6ded5` B6: 推荐 harbor jobs start + +### v3 C 批 — L3 调度接入 (4 commits) +`05e6221` C1: run.sh + filter_matrix.py · `6b17bcd` C2: watch.sh · `08e3f0f` C3: summarize.sh + summarize.py · `638d17b` C4: orchestrator_status.sh + +### v3 D 批 — L2 baked image (3 commits) +`d65f44d` D1: build_l2_baked_image.sh · `f0d0871` D2: 5 个 Jinja2 模板 · `c59c4f8` D3: bootstrap.sh --l2-image + +### v3 E/F/G/H 批 (4 commits) +`6cf5edb` E: bootstrap.sh --qemu · `43ebb27` F: 移除 --cgroupns=host · `085a826` G: safe_start watchdog · `1e523aa` **H: ais_bench_agent.sh 统一入口 + harbor wheel 可替换** + +--- + +🤖 Generated with [Claude Code](https://claude.com/claude-code) \ No newline at end of file diff --git a/docker/agent_runtime/README.md b/docker/agent_runtime/README.md new file mode 100644 index 00000000..2c3f3b2f --- /dev/null +++ b/docker/agent_runtime/README.md @@ -0,0 +1,270 @@ +# AISBench Agent Runtime + +为 AISBench Agent 测评(Harbor Terminal-Bench、SWE-bench、SWE-bench Pro 等)提供运行环境容器的镜像与脚本。 + +> 本仓库目录是 AISBench/benchmark 的社区运行时补充,不属于 benchmark 核心评测逻辑。用户使用本目录的脚本和镜像准备好 runtime 容器后,仍用原生 `ais_bench` 命令执行测评,原理与各 benchmark 文档完全一致。 + +> 📘 想从零跑通本 PR(含 harbor offline + mini-swe-agent tarball + 真实 trial 验证)?看 [REPRODUCE.md](REPRODUCE.md) — 618 行端到端复现手册,含失败原因诚实声明与 LLM 算力方案 gap 说明。 + +## 解决什么问题 + +Agent 测评的环境准备存在三大痛点: + +1. **依赖冲突**:harbor 强制升级 datasets 到 4.0+;SWE-bench 与 SWE-bench Pro 各需要一个不同 fork 的 `mini-swe-agent`,同包名互相覆盖。 +2. **容器配置易错**:DinD 模式 A/B、`--cgroupns=host`、`daemon.json`、seccomp,任一步漏配都会在跑测评时才报错。 +3. **数据集 / case 镜像版本变化频繁**:数据集和 case 镜像都有大量版本,烤入 runtime 镜像会很快过期。 + +本包通过分层解决这些问题: + +| 层 | 内容 | 解决 | +|---|---|---| +| `Dockerfile.agent-runtime` | 在 `aisbench_benchmark` 基镜之上追加 3 个 venv 隔离层(harbor / swebench / swebench_pro) | 依赖冲突 | +| `bootstrap.sh` | 一键起 runtime 容器,自动选 DinD/Socket 模式 + 挂载数据集 + 加载 case tar | 容器配置易错 + 数据集接入 | +| `doctor.sh` | 静态自检(L1,秒级)— 校验 docker / venv / pack / 资源 | 跑前验证 runtime 配置 | +| `packs/.yaml` | 各 benchmark 的元数据(venv 名 / 原生配置 / 文档) | 工具链与 benchmark 解耦 | + +## 数据集 / case 镜像由谁负责 + +**刻意不做的事**:本方案**不**预置 agent benchmark 的数据集和 case 沙箱镜像到 runtime 镜像中。原因是这两者版本变化频繁,烤入镜像后: + +- 数据集每次更新都要重新构建 runtime 镜像,对维护者负担大、对用户下载体积大 +- case 沙箱镜像一个 full 集 ~71GB,不能烤进基础镜像 + +**谁负责什么**: + +| 项 | 谁准备 | 怎么接入 runtime 容器 | +|---|---|---| +| runtime 镜像 | AISBench 维护者 | `docker pull ghcr.io/aisbench/agent-runtime:latest-...`(或 `--runtime-tar` 离线) | +| 数据集(task.toml 等) | 用户在物理机上准备好 | `bootstrap.sh --datasets <完整数据集路径>`(挂载到容器内同路径 + 注入 env var) | +| case 沙箱镜像 | 用户在物理机上准备好 tar | `bootstrap.sh --case-tar `(自动 `docker cp` 进容器 + `docker load`) | +| 模型调用参数 (api_base / model_names) | 用户改原生配置 | 容器内 `vim ais_bench/configs/agent_example/...` | + +## 快速入门(以 Harbor Terminal-Bench 为例 aarch64) +快速入门针对物理机20.0.0以下版本docker环境,其他环境请参考对应agent测评文档。 + +```bash +# 1. 物理机上准备数据集与镜像 tar(已有可跳过) + +git clone https://modelers.cn/AISBench/terminal-bench-2-offline-mini.git # 数据集准备 +wget https://aisbench.obs.cn-north-4.myhuaweicloud.com/agent/agent_runtime_image_v3.1-20260701-master-ubuntu24.04-py312-aarch64.tar.gz # 测评镜像准备(可选,不准备则自动获取最新) +wget https://aisbench.obs.cn-north-4.myhuaweicloud.com/terminal-bench-2-images/terminal-bench-2-offline-prepared-images-selected-0.10_aarch64.tar # case 镜像准备,按需从对应agent测评文档获取链接 +mkdir /path/to/test_wkp/ # 物理机创建一个空的工作目录 + +# 2. 物理机上一键起 runtime 容器(自动选 DinD/Socket 模式,自动挂载数据集,如果执行环境不通外网,可以先从其他环境获取ais_bench_agent_bootstrap.sh再bash执行 +# 自动把 case 镜像 tar 拷进容器内部 docker load 完) +curl -fsSL https://aisbench.obs.cn-north-4.myhuaweicloud.com/agent/ais_bench_agent_bootstrap.sh \ + | bash -s -- \ + --datasets /path/to/terminal-bench-2-offline-mini/terminal-bench-2-offline-selected_0.10/ \ + --runtime-tar /path/to/agent_runtime_image_v3.1-20260701-master-ubuntu24.04-py312-aarch64.tar.gz \ + --case-tar /path/to/terminal-bench-2-offline-prepared-images-selected-0.10.tar \ + --host-path /path/to/test_wkp/ \ + --container-name test_agent_run +# --datasets 指向的目录结构需与 terminal-bench-2-offline-mini 仓库的 terminal-bench-2-offline-selected_0.10/ 子目录结构一致 +# --runtime-tar (可选)提前准备的测评镜像 +# --case-tar 指向的 tar 结构需与对应 agent 测评文档的 case 镜像 tar 结构一致 +# --host-path 指向的目录需为空目录,容器内会自动创建同名目录挂载数据集和 case 镜像 +# --container-name 指向的容器名需唯一,否则会覆盖旧容器 + +# 3. 进入容器(case 镜像已在内部,直接可用) +docker exec -it test_agent_run bash + +# 4. (无需 vim)原生配置 path 自动从 AISBENCH_AGENT_DATASET_PATH 读 +# 仅需 vim 改 model_names / api_base +vim ais_bench/configs/agent_example/harbor_terminal_bench_2_task.py + +# 5. 验证 runtime 就绪 +ais_bench_agent_doctor.sh harbor + +# 6. 跑测评 +agent_env harbor +ais_bench ais_bench/configs/agent_example/harbor_terminal_bench_2_task.py --debug +``` + +切换数据集:销毁旧容器 + 重启 bootstrap(数据集路径 / case tar 一起更新): + +```bash +docker rm -f test_agent_run +bash ais_bench_agent_bootstrap.sh \ + --datasets /data/datasets/harbor/full/terminal-bench-2 \ + --case-tar /data/cases/terminal-bench-2-prepared-images_x86_64.tar.gz +``` + +## 目录结构 + +``` +agent_runtime/ +├── README.md # 本文件 +├── Dockerfile.agent-runtime # runtime 镜像构建文件(BASE_IMAGE 通过 --build-arg 传入,不写死) +├── build_image_agent_runtime.sh # 构建脚本(支持 --base-tag/--push/--upload/--multi-arch) +├── ais_bench_agent_bootstrap.sh # 一键起 runtime 容器(用户侧入口,需上传到 OBS) +├── doctor.sh # runtime 就绪验证(容器内,仅校验 docker/venv/config 不校验数据集/cases) +├── packs/ # 各 benchmark 的清单(name/runtime_venv/native_config/native_doc) +│ ├── harbor.yaml # Harbor Terminal-Bench +│ ├── swebench.yaml # SWE-bench(mini_swe_agent + SWE-bench harness) +│ └── swebench_pro.yaml # SWE-bench Pro(scaleapi 适配版) +└── patches/ # 构建期 / 启动期用的补丁脚本 + └── harbor_compose_patch.py # 给 harbor 的 docker-compose-base.yaml 加 seccomp=unconfined + network_mode=host +``` + +## ais_bench_agent_bootstrap.sh 用法 + +```bash +# 最简调用:挂载一个数据集目录 +bash ais_bench_agent_bootstrap.sh --datasets /data/datasets + +# 挂载多个目录 +bash ais_bench_agent_bootstrap.sh --datasets /data/datasets --datasets /data/extra + +# 强制模式 A/B +bash ais_bench_agent_bootstrap.sh --mode A --datasets /data/datasets + +# 自定义容器名(一台机器同时跑多个 runtime 时区分) +bash ais_bench_agent_bootstrap.sh --container-name my_eval_1 --datasets /data/datasets + +# 自定义 runtime 镜像(推荐显式传 tag,保证可复现性) +bash ais_bench_agent_bootstrap.sh \ + --runtime-image ghcr.io/aisbench/agent-runtime:v3.1-20260522-master-ubuntu24.04-py312-x86_64 \ + --datasets /data/datasets + +# 模式 B + 自定义 /benchmark 提取目标(仅 /opt 不可写时用) +bash ais_bench_agent_bootstrap.sh --mode B --host-path /data/ais_bench_host --datasets /data/datasets + +# 离线模式(内网/隔离环境):用宿主上已下载好的 tar 包加载 runtime 镜像 +# 完全跳过 docker pull / OBS 下载 +# 适用:内网部署机无法访问 ghcr.io,也无法访问外网 OBS +bash ais_bench_agent_bootstrap.sh --datasets /data/datasets --runtime-tar /opt/aisbench/agent-runtime-ubuntu24-py312-x86_64.tar.gz + +# 完全离线:runtime tar + case 镜像 tar 一起传入 +# 容器启动后会自动把 case tar 拷进容器并 docker load +bash ais_bench_agent_bootstrap.sh \ + --datasets /data/datasets \ + --runtime-tar /opt/aisbench/agent-runtime.tar.gz \ + --case-tar /opt/aisbench/case-tb2-mini-0.10.tar.gz + +# 一次加载多个 case 镜像(可多次 --case-tar,也可传一个目录) +bash ais_bench_agent_bootstrap.sh \ + --datasets /data/datasets \ + --runtime-tar /opt/aisbench/agent-runtime.tar.gz \ + --case-tar /opt/aisbench/case-tb2-mini-0.10.tar.gz \ + --case-tar /opt/aisbench/case-tb2-mini-0.14.tar.gz \ + --case-tar /opt/aisbench/case-tars/ # 目录下所有 .tar/.tar.gz/.tgz 都会被加载 +``` + +`--datasets` / `--host-path` / `--runtime-tar` / `--case-tar` 必须是**绝对路径**,且在**物理机上必须存在**(脚本会校验)。容器内路径与宿主路径相同。 + +`--datasets` 传入的完整路径会原封不动注入为容器内环境变量 `AISBENCH_AGENT_DATASET_PATH`,原生 ais_bench 配置(如 `harbor_terminal_bench_2_task.py`)直接把这个 env var 作为数据集 `path` 字段使用——**不拼接、不转换、完全一致**。所以: + +- **推荐**:把 `--datasets` 传成你准备好的 harbor benchmark 数据集完整路径(如 `/data/datasets/harbor/mini-0.10/terminal-bench-2-offline-selected_0.10`),这样配置无需 vim 改 path +- **多目录**:可多次传 `--datasets`,但 env var 只用首个;用户可手动 `export AISBENCH_AGENT_DATASET_PATH=...` 覆盖 + +### 命令行参数一览 + +| 参数 | 默认值 | 说明 | +|---|---|---| +| `--datasets ` | 无(不挂载) | 数据集目录,可多次 | +| `--runtime-tar ` | 无(走 pull) | runtime 镜像 tar,离线场景用 | +| `--case-tar ` | 无(容器内手动准备) | case 镜像 tar,文件或目录,可多次 | +| `--mode A\|B` | 自动判断 | 强制 DinD (A) 或 Socket 代理 (B) | +| `--container-name ` | `ais_bench_agent` | runtime 容器名 | +| `--runtime-image ` | `ghcr.io/aisbench/agent-runtime:latest-ubuntu24.04-py312-${ARCH}` | runtime 镜像 tag | +| `--host-path ` | `/opt/ais_bench_agent` | 模式 B 时 `/benchmark` 提取目标 | + +### 环境变量 + +仅保留一个 env 变量(其它配置请用 CLI 参数): + +| env | 默认值 | 说明 | +|---|---|---| +| `OBS_RUNTIME_TAR_BASE` | `https://aisbench.obs.cn-north-4.myhuaweicloud.com/agent/runtime` | OBS runtime tar 下载基址(一般无需改) | + +### 离线场景 + +内网/隔离环境的部署机无法访问 `ghcr.io/aisbench/agent-runtime` 或 OBS,但已通过 U 盘、内网代理等方式拿到了 runtime tar 包和 case 镜像 tar: + +1. **获取 tar 包**(任选其一): + - 让维护者跑 `build_image_agent_runtime.sh --upload 1` 上传到 OBS,内网用户从 OBS 下载 + - 在能访问外网的机器上 `docker save ghcr.io/aisbench/agent-runtime: -o agent-runtime.tar.gz` 后拷贝进来 +2. **部署机执行**: + ```bash + # 最小:只传 runtime tar(case 镜像仍需在容器内手动 pull / load) + bash ais_bench_agent_bootstrap.sh --datasets /data/datasets --runtime-tar /path/to/agent-runtime.tar.gz + + # 完全离线:runtime + case tar 都传 + bash ais_bench_agent_bootstrap.sh \ + --datasets /data/datasets \ + --runtime-tar /path/to/agent-runtime.tar.gz \ + --case-tar /path/to/case-tb2-mini-0.10.tar.gz + ``` +3. **行为**: + - `--runtime-tar`:完全跳过 `docker pull` 与 OBS 的 `curl` 下载;`docker load -i ` 后自动检测 tag(grep `agent-runtime`),优先匹配 `RUNTIME_IMAGE`;检测失败给精确错误 + - `--case-tar `:支持传单个 tar 或一个目录(目录会递归加载所有 `.tar` / `.tar.gz` / `.tgz`)。脚本会 `docker cp` 进容器,再用 `docker load -i` 加载。**支持 A/B 两种模式**(A 模式加载到容器内 DinD;B 模式加载到容器内,但因 socket 与宿主共享实际也加载到了宿主),模式A|B的具体介绍参考[OVERVIEW.zh.md](../OVERVIEW.zh.md#运行-agent--沙箱类测评在容器内使用-docker) + - 可多次 `--case-tar` + +模式 B(Socket 代理)默认会把 `/benchmark` 提取到宿主 `/opt/ais_bench_agent`。若你的环境 `/opt` 不可写(如某些只读根容器/沙箱),用 `--host-path` 改写到可写路径: + +```bash +bash ais_bench_agent_bootstrap.sh --datasets /data/datasets --host-path /data/ais_bench_host +``` + +正常物理机无需设置该变量。其余可配置环境变量见 `bootstrap.sh` 头部注释。 + +## 镜像构建 + +runtime 镜像基于 `aisbench_benchmark` 基镜构建,基镜像 tag 通过参数传入,不写死: + +```bash +# 基础构建(本地,当前架构) +bash docker/agent_runtime/build_image_agent_runtime.sh \ + --base-tag v3.1-20260522-master + +# 指定 OS/Python(默认 ubuntu24.04 + py312) +bash docker/agent_runtime/build_image_agent_runtime.sh \ + --base-tag v3.1-20260522-master --os ubuntu24.04 --py-version py312 + +# 构建并推送到远程仓库 +bash docker/agent_runtime/build_image_agent_runtime.sh \ + --base-tag v3.1-20260522-master --push 1 + +# 多架构构建并推送 +bash docker/agent_runtime/build_image_agent_runtime.sh \ + --base-tag v3.1-20260522-master --multi-arch 1 --push 1 + +# 构建、推送、并上传离线包到 OBS(供 ais_bench_agent_bootstrap.sh 回退下载) +bash docker/agent_runtime/build_image_agent_runtime.sh \ + --base-tag v3.1-20260522-master --push 1 --upload 1 +``` + +构建脚本会自动校验(4 项): +1. ais_bench 可用 +2. 3 个 venv(harbor / swebench / swebench_pro)都完整 + 3 个 venv 内都有 ais_bench wrapper + 两个 swebench venv 能 import minisweagent +3. doctor.sh / packs 就位 +4. harbor compose 模板已 patch `seccomp=unconfined` + +## 已支持的 pack + +| pack 名 | runtime_venv | 文档 | 说明 | +|---|---|---|---| +| `harbor` | harbor | [harbor_bench.md](../../docs/source_zh_cn/extended_benchmark/agent/harbor_bench.md) | Harbor Terminal-Bench 2.0 | +| `swebench` | swebench | [swe_bench.md](../../docs/source_zh_cn/extended_benchmark/agent/swe_bench.md) | SWE-bench(lite/verified/full/multilingual 等) | +| `swebench_pro` | swebench_pro | [swe_bench_pro.md](../../docs/source_zh_cn/extended_benchmark/agent/swe_bench_pro.md) | SWE-bench Pro(仅 x86) | + +pack.yaml 不声明数据集路径、不声明 case 镜像获取方式——这些完全交给用户掌控: +- 数据集路径:用户 `bootstrap.sh --datasets <完整数据集路径>` 时显式指定(要哪个就跑哪个) +- case 镜像:用户按 pack.yaml 的 `native_doc` 指向的文档自行 `docker pull` 或 `docker load` + +如果以后接更多 benchmark,每加一个 `packs/.yaml` 即可。 + +> 常用的 harbor 数据集目录名(仅供参考,与工具无关): +> - `/data/datasets/harbor/full/terminal-bench-2`(89 case,含少量外网任务) +> - `/data/datasets/harbor/mini-0.10/terminal-bench-2-offline-selected_0.10`(7 case) +> - `/data/datasets/harbor/mini-0.14/terminal-bench-2-offline-selected_0.14`(10 case) +> - `/data/datasets/harbor/mini-0.20/terminal-bench-2-offline-selected_0.20`(14 case) +> +> 用户在 `bootstrap.sh --datasets` 传入哪个路径,就由 env var `AISBENCH_AGENT_DATASET_PATH` 把哪个路径注入容器。 +> mini-* 系列基于 `terminal-bench-2-offline`(剔除外网任务后的 70 个 case)做 K-means 采样,完全离线可跑。 +> +> SWE-bench 数据集说明见 [swe_bench.md](../../docs/source_zh_cn/extended_benchmark/agent/swe_bench.md)(HF 下载);SWE-bench Pro 见 [swe_bench_pro.md](../../docs/source_zh_cn/extended_benchmark/agent/swe_bench_pro.md)。 + +## 详细方案 + +完整设计与各脚本参数说明见各脚本头部注释。 \ No newline at end of file diff --git a/docker/agent_runtime/README_en.md b/docker/agent_runtime/README_en.md new file mode 100644 index 00000000..1af0ee4f --- /dev/null +++ b/docker/agent_runtime/README_en.md @@ -0,0 +1,268 @@ +# AISBench Agent Runtime + +Provides images and scripts for the runtime container used by AISBench Agent evaluations (Harbor Terminal-Bench, SWE-bench, SWE-bench Pro, etc.). + +> This repository directory is a community runtime supplement to AISBench/benchmark and is not part of the core benchmark evaluation logic. After preparing the runtime container with the scripts and images in this directory, users still run evaluations with the native `ais_bench` command; the principles are identical to those in each benchmark's documentation. + +## What Problem Does It Solve + +Environment preparation for agent evaluation has three major pain points: + +1. **Dependency conflicts**: harbor forces datasets to upgrade to 4.0+; SWE-bench and SWE-bench Pro each need a different fork of `mini-swe-agent`, with the same package name overwriting each other. +2. **Error-prone container configuration**: DinD modes A/B, `--cgroupns=host`, `daemon.json`, seccomp — any missed step only surfaces as an error at evaluation time. +3. **Frequent dataset / case image version changes**: datasets and case images have many versions; baking them into the runtime image causes rapid expiration. + +This package solves these problems in layers: + +| Layer | Content | Solves | +|---|---|---| +| `Dockerfile.agent-runtime` | Adds 3 isolated venv layers on top of the `aisbench_benchmark` base image (harbor / swebench / swebench_pro) | Dependency conflicts | +| `bootstrap.sh` | One-click runtime container start: auto-selects DinD/Socket mode + mounts datasets + loads case tar | Error-prone container config + dataset injection | +| `doctor.sh` | Static self-check (L1, seconds) — validates docker / venv / pack / resources | Pre-run runtime validation | +| `packs/.yaml` | Metadata for each benchmark (venv name / native config / docs) | Decouples toolchain from benchmarks | + +## Who Prepares Datasets / Case Images + +**What is deliberately not done**: This solution does **not** pre-bake agent benchmark datasets and case sandbox images into the runtime image. The reason is that both change versions frequently, and baking them in means: + +- Every dataset update requires rebuilding the runtime image — a maintenance burden and a large download for users +- A full case sandbox image set is ~71GB and cannot be baked into a base image + +**Who is responsible for what**: + +| Item | Who prepares | How to integrate into the runtime container | +|---|---|---| +| runtime image | AISBench maintainers | `docker pull ghcr.io/aisbench/agent-runtime:latest-...` (or `--runtime-tar` offline) | +| Dataset (task.toml, etc.) | User prepares on the host | `bootstrap.sh --datasets ` (mounted at the same path inside the container + injected as env var) | +| case sandbox image | User prepares tar on the host | `bootstrap.sh --case-tar ` (auto `docker cp` into container + `docker load`) | +| Model call parameters (api_base / model_names) | User edits native config | Inside the container: `vim ais_bench/configs/agent_example/...` | + +## Quick Start (Harbor Terminal-Bench as example, aarch64) +The quick start targets host machines with Docker version below 20.0.0. For other environments, refer to the corresponding agent evaluation documentation. + +```bash +# 1. Prepare dataset and image tar on the host (skip if already present) + +git clone https://modelers.cn/AISBench/terminal-bench-2-offline-mini.git # Dataset preparation +wget https://aisbench.obs.cn-north-4.myhuaweicloud.com/agent/agent_runtime_image_v3.1-20260701-master-ubuntu24.04-py312-aarch64.tar.gz # Runtime image preparation (optional; if omitted, the latest is fetched automatically) +wget https://aisbench.obs.cn-north-4.myhuaweicloud.com/terminal-bench-2-images/terminal-bench-2-offline-prepared-images-selected-0.10_aarch64.tar # Case image preparation; get the link from the corresponding agent evaluation doc as needed +mkdir /path/to/test_wkp/ # Create an empty working directory on the host + +# 2. Start the runtime container with one click on the host (auto-selects DinD/Socket mode, auto-mounts datasets; if the environment has no external network, fetch ais_bench_agent_bootstrap.sh elsewhere first and run with bash) +# Auto-copies case image tar into the container and runs docker load +curl -fsSL https://aisbench.obs.cn-north-4.myhuaweicloud.com/agent/ais_bench_agent_bootstrap.sh \ + | bash -s -- \ + --datasets /path/to/terminal-bench-2-offline-mini/terminal-bench-2-offline-selected_0.10/ \ + --runtime-tar /path/to/agent_runtime_image_v3.1-20260701-master-ubuntu24.04-py312-aarch64.tar.gz \ + --case-tar /path/to/terminal-bench-2-offline-prepared-images-selected-0.10.tar \ + --host-path /path/to/test_wkp/ \ + --container-name test_agent_run +# --datasets must point to a directory structure consistent with the terminal-bench-2-offline-selected_0.10/ subdirectory of the terminal-bench-2-offline-mini repo +# --runtime-tar (optional) pre-downloaded runtime image +# --case-tar must point to a tar structure consistent with the case image tar described in the corresponding agent evaluation doc +# --host-path must be an empty directory; a same-named directory will be created inside the container to mount datasets and case images +# --container-name must be unique; otherwise the old container will be overwritten + +# 3. Enter the container (case images are already loaded and ready to use) +docker exec -it test_agent_run bash + +# 4. (No vim needed) The native config path is read automatically from AISBENCH_AGENT_DATASET_PATH +# Only vim model_names / api_base +vim ais_bench/configs/agent_example/harbor_terminal_bench_2_task.py + +# 5. Verify runtime is ready +ais_bench_agent_doctor.sh harbor + +# 6. Run evaluation +agent_env harbor +ais_bench ais_bench/configs/agent_example/harbor_terminal_bench_2_task.py --debug +``` + +Switch datasets: destroy the old container + restart bootstrap (update dataset path / case tar together): + +```bash +docker rm -f test_agent_run +bash ais_bench_agent_bootstrap.sh \ + --datasets /data/datasets/harbor/full/terminal-bench-2 \ + --case-tar /data/cases/terminal-bench-2-prepared-images_x86_64.tar.gz +``` + +## Directory Structure + +``` +agent_runtime/ +├── README.md # This file +├── Dockerfile.agent-runtime # runtime image build file (BASE_IMAGE passed via --build-arg, not hardcoded) +├── build_image_agent_runtime.sh # build script (supports --base-tag/--push/--upload/--multi-arch) +├── ais_bench_agent_bootstrap.sh # one-click runtime container start (user-side entry point, must be uploaded to OBS) +├── doctor.sh # runtime readiness verification (inside the container; only validates docker/venv/config, not datasets/cases) +├── packs/ # per-benchmark manifests (name/runtime_venv/native_config/native_doc) +│ ├── harbor.yaml # Harbor Terminal-Bench +│ ├── swebench.yaml # SWE-bench (mini_swe_agent + SWE-bench harness) +│ └── swebench_pro.yaml # SWE-bench Pro (scaleapi adapted version) +└── patches/ # patch scripts used at build / startup time + └── harbor_compose_patch.py # adds seccomp=unconfined + network_mode=host to harbor's docker-compose-base.yaml +``` + +## ais_bench_agent_bootstrap.sh Usage + +```bash +# Minimal call: mount a single dataset directory +bash ais_bench_agent_bootstrap.sh --datasets /data/datasets + +# Mount multiple directories +bash ais_bench_agent_bootstrap.sh --datasets /data/datasets --datasets /data/extra + +# Force mode A/B +bash ais_bench_agent_bootstrap.sh --mode A --datasets /data/datasets + +# Custom container name (to distinguish when running multiple runtimes on one machine) +bash ais_bench_agent_bootstrap.sh --container-name my_eval_1 --datasets /data/datasets + +# Custom runtime image (recommended: pass an explicit tag for reproducibility) +bash ais_bench_agent_bootstrap.sh \ + --runtime-image ghcr.io/aisbench/agent-runtime:v3.1-20260522-master-ubuntu24.04-py312-x86_64 \ + --datasets /data/datasets + +# Mode B + custom /benchmark extraction target (use only when /opt is not writable) +bash ais_bench_agent_bootstrap.sh --mode B --host-path /data/ais_bench_host --datasets /data/datasets + +# Offline mode (intranet/isolated environment): load runtime image from a tar already downloaded on the host +# Completely skips docker pull / OBS download +# Use case: intranet deployment machines that cannot reach ghcr.io or external OBS +bash ais_bench_agent_bootstrap.sh --datasets /data/datasets --runtime-tar /opt/aisbench/agent-runtime-ubuntu24-py312-x86_64.tar.gz + +# Fully offline: runtime tar + case image tar passed together +# After container start, case tar is auto-copied into the container and docker loaded +bash ais_bench_agent_bootstrap.sh \ + --datasets /data/datasets \ + --runtime-tar /opt/aisbench/agent-runtime.tar.gz \ + --case-tar /opt/aisbench/case-tb2-mini-0.10.tar.gz + +# Load multiple case images at once (--case-tar can be repeated, or a directory can be passed) +bash ais_bench_agent_bootstrap.sh \ + --datasets /data/datasets \ + --runtime-tar /opt/aisbench/agent-runtime.tar.gz \ + --case-tar /opt/aisbench/case-tb2-mini-0.10.tar.gz \ + --case-tar /opt/aisbench/case-tb2-mini-0.14.tar.gz \ + --case-tar /opt/aisbench/case-tars/ # all .tar/.tar.gz/.tgz under the directory will be loaded +``` + +`--datasets` / `--host-path` / `--runtime-tar` / `--case-tar` must be **absolute paths** and **must exist on the host** (the script validates them). The in-container path is the same as the host path. + +The full path passed to `--datasets` is injected verbatim into the container as the environment variable `AISBENCH_AGENT_DATASET_PATH`, and native ais_bench configs (e.g. `harbor_terminal_bench_2_task.py`) use this env var directly as the dataset `path` field — **no concatenation, no conversion, completely identical**. Therefore: + +- **Recommended**: pass `--datasets` as the full path of your prepared harbor benchmark dataset (e.g. `/data/datasets/harbor/mini-0.10/terminal-bench-2-offline-selected_0.10`); the config then needs no vim for path +- **Multiple directories**: `--datasets` can be passed multiple times, but the env var uses only the first; users can manually `export AISBENCH_AGENT_DATASET_PATH=...` to override + +### Command-Line Parameter Reference + +| Parameter | Default | Description | +|---|---|---| +| `--datasets ` | None (not mounted) | Dataset directory; can be passed multiple times | +| `--runtime-tar ` | None (pull) | runtime image tar; for offline scenarios | +| `--case-tar ` | None (prepare manually in container) | case image tar; file or directory; can be passed multiple times | +| `--mode A\|B` | Auto-detected | Force DinD (A) or Socket passthrough (B) | +| `--container-name ` | `ais_bench_agent` | runtime container name | +| `--runtime-image ` | `ghcr.io/aisbench/agent-runtime:latest-ubuntu24.04-py312-${ARCH}` | runtime image tag | +| `--host-path ` | `/opt/ais_bench_agent` | Extraction target for `/benchmark` in mode B | + +### Environment Variables + +Only one env variable is kept (use CLI parameters for other configs): + +| env | Default | Description | +|---|---|---| +| `OBS_RUNTIME_TAR_BASE` | `https://aisbench.obs.cn-north-4.myhuaweicloud.com/agent/runtime` | OBS runtime tar download base URL (usually no need to change) | + +### Offline Scenarios + +Deployment machines in intranet/isolated environments cannot reach `ghcr.io/aisbench/agent-runtime` or OBS, but have obtained the runtime tar package and case image tar via USB stick, intranet proxy, etc.: + +1. **Obtain tar package** (either): + - Ask a maintainer to run `build_image_agent_runtime.sh --upload 1` to upload to OBS; intranet users download from OBS + - On a machine with internet access, run `docker save ghcr.io/aisbench/agent-runtime: -o agent-runtime.tar.gz` and copy it in +2. **Run on the deployment machine**: + ```bash + # Minimal: pass only the runtime tar (case images still need manual pull / load inside the container) + bash ais_bench_agent_bootstrap.sh --datasets /data/datasets --runtime-tar /path/to/agent-runtime.tar.gz + + # Fully offline: pass both runtime + case tar + bash ais_bench_agent_bootstrap.sh \ + --datasets /data/datasets \ + --runtime-tar /path/to/agent-runtime.tar.gz \ + --case-tar /path/to/case-tb2-mini-0.10.tar.gz + ``` +3. **Behavior**: + - `--runtime-tar`: completely skips `docker pull` and OBS `curl` download; runs `docker load -i ` and auto-detects the tag (grep `agent-runtime`), preferring `RUNTIME_IMAGE`; gives a precise error on detection failure + - `--case-tar `: supports a single tar or a directory (a directory recursively loads all `.tar` / `.tar.gz` / `.tgz`). The script `docker cp`s it into the container, then runs `docker load -i`. **Supports both A/B modes** (mode A loads into the in-container DinD; mode B loads into the container, but since the socket is shared with the host, it is effectively loaded onto the host as well). For details on modes A/B, see [OVERVIEW.en.md](../OVERVIEW.en.md#running-agent--sandbox-benchmarks-docker-inside-the-container) + - `--case-tar` can be repeated + +Mode B (Socket passthrough) by default extracts `/benchmark` to the host's `/opt/ais_bench_agent`. If your environment's `/opt` is not writable (e.g. some read-only root containers/sandboxes), use `--host-path` to redirect to a writable path: + +```bash +bash ais_bench_agent_bootstrap.sh --datasets /data/datasets --host-path /data/ais_bench_host +``` + +Normal host machines do not need to set this variable. For other configurable environment variables, see the header comments of `bootstrap.sh`. + +## Image Build + +The runtime image is built on top of the `aisbench_benchmark` base image; the base image tag is passed via a parameter, not hardcoded: + +```bash +# Basic build (local, current architecture) +bash docker/agent_runtime/build_image_agent_runtime.sh \ + --base-tag v3.1-20260522-master + +# Specify OS/Python (default ubuntu24.04 + py312) +bash docker/agent_runtime/build_image_agent_runtime.sh \ + --base-tag v3.1-20260522-master --os ubuntu24.04 --py-version py312 + +# Build and push to a remote registry +bash docker/agent_runtime/build_image_agent_runtime.sh \ + --base-tag v3.1-20260522-master --push 1 + +# Multi-arch build and push +bash docker/agent_runtime/build_image_agent_runtime.sh \ + --base-tag v3.1-20260522-master --multi-arch 1 --push 1 + +# Build, push, and upload offline package to OBS (for ais_bench_agent_bootstrap.sh fallback download) +bash docker/agent_runtime/build_image_agent_runtime.sh \ + --base-tag v3.1-20260522-master --push 1 --upload 1 +``` + +The build script automatically validates (4 items): +1. ais_bench is available +2. All 3 venvs (harbor / swebench / swebench_pro) are complete + all 3 venvs contain the ais_bench wrapper + both swebench venvs can import minisweagent +3. doctor.sh / packs are in place +4. The harbor compose template has been patched with `seccomp=unconfined` + +## Supported Packs + +| pack name | runtime_venv | Documentation | Description | +|---|---|---|---| +| `harbor` | harbor | [harbor_bench.md](../../docs/source_en/extended_benchmark/agent/harbor_bench.md) | Harbor Terminal-Bench 2.0 | +| `swebench` | swebench | [swe_bench.md](../../docs/source_en/extended_benchmark/agent/swe_bench.md) | SWE-bench (lite/verified/full/multilingual, etc.) | +| `swebench_pro` | swebench_pro | [swe_bench_pro.md](../../docs/source_en/extended_benchmark/agent/swe_bench_pro.md) | SWE-bench Pro (x86 only) | + +pack.yaml does not declare dataset paths or case image acquisition methods — these are entirely under user control: +- Dataset path: explicitly specified by the user via `bootstrap.sh --datasets ` (run whichever you want) +- Case image: the user performs `docker pull` or `docker load` according to the document pointed to by pack.yaml's `native_doc` + +If you want to support more benchmarks in the future, just add a `packs/.yaml`. + +> Common harbor dataset directory names (for reference only, unrelated to the tool): +> - `/data/datasets/harbor/full/terminal-bench-2` (89 cases, including a few external-network tasks) +> - `/data/datasets/harbor/mini-0.10/terminal-bench-2-offline-selected_0.10` (7 cases) +> - `/data/datasets/harbor/mini-0.14/terminal-bench-2-offline-selected_0.14` (10 cases) +> - `/data/datasets/harbor/mini-0.20/terminal-bench-2-offline-selected_0.20` (14 cases) +> +> Whichever path the user passes to `bootstrap.sh --datasets` is injected into the container by the env var `AISBENCH_AGENT_DATASET_PATH`. +> The mini-* series is K-means sampled from `terminal-bench-2-offline` (70 cases after removing external-network tasks) and runs fully offline. +> +> For SWE-bench dataset notes, see [swe_bench.md](../../docs/source_en/extended_benchmark/agent/swe_bench.md) (HF download); for SWE-bench Pro, see [swe_bench_pro.md](../../docs/source_en/extended_benchmark/agent/swe_bench_pro.md). + +## Detailed Solution + +For the full design and parameter descriptions of each script, see the header comments of each script. diff --git a/docker/agent_runtime/REPRODUCE.md b/docker/agent_runtime/REPRODUCE.md new file mode 100644 index 00000000..d236f9e7 --- /dev/null +++ b/docker/agent_runtime/REPRODUCE.md @@ -0,0 +1,618 @@ +# PR #499 AISBench Agent Runtime 完整复现操作手册 + +> 目标:在独立目录下完整复现 [PR #499](https://github.com/AISBench/benchmark/pull/499) 的 AISBench Agent Runtime + 5 层 DinD 统一评测环境,并使用本地 case 镜像 + harbor offline + mini-swe-agent tarball 拉起实际 trial。 +> +> **✅ 最终验证**:trial `msa-echo-test__aD8PXUd` 端到端跑通,**reward=1.0**(2026-08-30 11:20~11:31)。完整 pipeline (matrix → dataset → setup → agent → verifier → artifact) 全部验证。 + +--- + +## ⚠️ 重要声明:失败原因诚实分析 & LLM 算力方案未确定 + +### 失败原因不是 PR 文档或安装包的问题 + +PR #499 文档**从未**指定固定端口的 LLM 服务,**从未**要求 mock 方案。`mini-swe-agent-ubuntu2204.tar.gz` 只包含 Python 包 + uv tool venv(`manifest.json` 标注 `agent=mini-swe-agent, version=2.4.6, base=ubuntu:22.04`),本身不绑定任何 LLM endpoint——它只通过 OpenAI 兼容协议调用。 + +复现过程中遇到的失败**全部**归因于以下三类问题(与 PR 文档、tarball 内容**无关**): + +| 问题类别 | 表现 | 根因 | +|---|---|---| +| **环境基础设施** | case 容器启动失败 (cgroup threaded mode) | DinD 容器默认配置 `--bridge=none`,cgroup 命名空间未共享 host | +| **环境基础设施** | case 容器无网络 | DinD dockerd `--iptables=false`,default bridge 不存在 | +| **环境基础设施** | snapshot apply 280s 超时 | msa-base 用 `python:3.11` (debian-slim) 而 snapshot base 是 `ubuntu:22.04`,1.4GB tar 重打包超时 | +| **复现者自加配置** | mock LLM 反复死 | 我自己起的 Python http.server 通过 `docker exec -d` 启动,进程生命周期不稳定 | +| **复现者自加配置** | agent 报 "No tool calls" | 我自己 mock 返回纯文本,没返回 OpenAI-style `tool_calls` 字段 | +| **复现者自加配置** | `/app` 不存在 | task.toml/environment/Dockerfile 由我编写,忘了 `mkdir -p /app` | +| **复现者自加配置** | `reward.txt` 找不到 | test.sh 由我编写,只 echo PASS 没写 `/logs/verifier/reward.txt` | + +**mock 方案是我自己加的,不是 PR 文档要求的**。目的是在没有外部 LLM 的情况下排查 pipeline 各环节是否打通,并不是 PR 推荐路径。 + +### LLM 算力方案未确定(PR #499 当前缺陷) + +⚠️ **本指南依赖外部商业 API(SiliconFlow + DeepSeek-V4-Flash)跑出 reward=1**,但**这不是 PR #499 想要建立的"自包含统一评测环境"**。当前复现存在以下根本性 gap: + +1. **PR #499 未指定 LLM 算力来源**——文档只提"用 harbor trial",不提模型/endpoint/部署位置 +2. **本机 aarch64 host 无可用 GPU**——复现环境只能通过外部 API 调用 +3. **API 配额和稳定性是外部依赖**——SiliconFlow 可能限流、断连、改 endpoint 格式 +4. **失败排查不易**——当 trial 报"agent timeout"时,可能是 LLM 服务的问题,也可能是 harbor pipeline 的问题,难以区分 + +### 给 PR 作者的具体建议 + +请在更新 PR 时明确以下内容(按优先级): + +1. **在 REPRODUCE.md 中明确 LLM 后端清单**——如本地 vllm-ascend、SiliconFlow、OpenAI、Anthropic 等,每个后端提供 `--ae` 模板 + 推荐模型 +2. **集成 LLM gateway 到 ais_bench_agent_bootstrap.sh**——让 PR 启动时自动部署/配置 LLM endpoint,而不是依赖 `--api-key-file` +3. **支持 LLM 后端的 fallback / health check**——trial 启动前验证 LLM 可达,避免"agent 跑到一半 LLM 断连" +4. **文档明确标注**:"trial 真实 reward 的达成依赖外部 LLM 服务,本指南不保证 offline 复现" +5. **可选:集成 mock LLM 作为离线 fallback**——但要明确 mock 不是真实验证(仅用于 pipeline 调试),不能作为最终判据 + +### 后续复现者请注意 + +- **如果 PR 后续把 LLM 算力方案明确化**,请同步更新本指南第 8 节 +- 本指南**不保证 offline 复现**——必须能访问至少一个外部 LLM endpoint +- mock 方案(用 Python http.server 模拟 OpenAI API)**仅用于 pipeline 调试**,不应作为最终 reward 验证手段 + +--- + +## 0. 环境前提 + +| 项 | 期望值 | +|---|---| +| 平台 | Linux aarch64(ARM64)或 x86_64 | +| 内核 | ≥ 5.15(必须 cgroup v2:`cat /proc/filesystems \| grep cgroup`) | +| Docker | ≥ 20.10,且 `docker info` 显示 `Storage Driver: overlay2` | +| 工作目录 | `/home/zengziyu/aisbench_reproduce/`(独立) | +| 网络 | github 直连 OK;docker.io 不稳定时需本地镜像缓存 | +| 必需工具 | `git`, `tar`, `bash` ≥ 4, `python3`, `qemu-user-static`(多架构用) | + +验证命令: +```bash +uname -m # aarch64 或 x86_64 +docker info | grep -E 'Cgroup|Storage' +ls /sys/fs/cgroup/cgroup2 # 存在即 cgroup v2 +``` + +--- + +## 1. 工作目录与 PR 克隆 + +```bash +ROOT=/home/zengziyu/aisbench_reproduce +mkdir -p $ROOT && cd $ROOT +git clone https://github.com/AISBench/benchmark.git +cd benchmark +git fetch origin pull/499/head:pr-499 +git checkout pr-499 +git log -1 --oneline # 应为 2a80af9 +``` + +> **严禁**修改 PR 文件。如发现需要改的,记入 `REPRODUCE_NOTES.md`。 + +--- + +## 2. 准备基镜像构建 + +### 2.1 docker.io 网络 workaround(如有) + +```bash +# 1ms.run 镜像源已在本地缓存 ubuntu:24.04 时 +docker tag docker.1ms.run/library/ubuntu:24.04 ubuntu:24.04 + +# aarch64 需要的 qemu 注册 +docker run --rm --privileged multiarch/qemu-user-static --reset -p yes +``` + +### 2.2 选择 tag + +REPRODUCE.md 默认 `--tag v3.1-20260522-master`,但**该 tag 太老,缺 harbor 0.20 配置**。建议使用: + +```bash +BASE_TAG=v3.1-20260827-master +``` + +--- + +## 3. 构建 L2 基镜像 + +```bash +cd $ROOT/benchmark +bash docker/build_image.sh \ + --tag $BASE_TAG \ + --use-cache 1 +# 注意:build_image.sh 必须用 --use-cache 1(带值),不能单用 --use-cache +``` + +构建结果: +- 镜像:`ghcr.io/aisbench/aisbench_benchmark:v3.1-20260827-master-ubuntu24.04-py312-aarch64` (~2.9GB) +- 验证:`ais_bench --help`, `docker --version`, `dockerd --version` 均返回成功 + +--- + +## 4. 构建 L4 Runtime 镜像 + +```bash +bash docker/agent_runtime/build_image_agent_runtime.sh \ + --base-tag $BASE_TAG \ + --use-cache 1 +``` + +构建结果: +- 镜像:`ghcr.io/aisbench/agent-runtime:v3.1-20260827-master-ubuntu24.04-py312-aarch64` (~3.5GB) +- 包含 `harbor==0.20.0`, `mini-swe-agent` venv 工具链, QEMU 等 + +--- + +## 5. 准备数据集与配置 + +### 5.1 下载数据集(terminal-bench 2 mini) + +```bash +mkdir -p $ROOT/data/harbor +cd $ROOT/data/harbor +# 从 harbor hub 或 huggingface 拉 terminal-bench-2 offline mini dataset +# 含 ~7 个 case:cobol-modernization, dna-insert, extract-elf, gpt2-codegolf, ... +``` + +数据集结构(harbor task 格式): +``` +data/harbor// +├── cobol-modernization/ +│ ├── task.toml # 任务元数据 +│ ├── instruction.md # agent 看到的指令 +│ ├── environment/ +│ │ └── Dockerfile # case 镜像构建脚本 +│ ├── solution/ # 参考解(agent 看不到) +│ └── tests/ # 验证脚本 +├── dna-insert/ +└── ... +``` + +### 5.2 写 matrix.yaml + +```bash +cat > $ROOT/config/matrix.yaml <<'EOF' +datasets: + - type: HarborTask + path: /home/zengziyu/aisbench_reproduce/data/harbor/ +EOF +``` + +> **注意**:harbor 0.20.0 不允许同一 task 配置同时给 `path` + `name`。 + +### 5.3 写 api_key.env + +```bash +cat > $ROOT/config/api_key.env <<'EOF' +OPENAI_API_KEY=sk-placeholder +OPENAI_API_BASE=https://api.openai.com/v1 +EOF +``` + +--- + +## 6. 拉起 5 层 DinD Runtime + +```bash +cd $ROOT/benchmark +bash docker/agent_runtime/ais_bench_agent_bootstrap.sh \ + --runtime-image ghcr.io/aisbench/agent-runtime:v3.1-20260827-master-ubuntu24.04-py312-aarch64 \ + --container-name test_harbor_repro \ + --mode A \ + --datasets $ROOT/data \ + --matrix-yaml $ROOT/config/matrix.yaml \ + --api-key-file $ROOT/config/api_key.env \ + --bind-jobs $ROOT/jobs \ + --bind-config $ROOT/config +``` + +启动后自动选择 **Mode A (DinD)**(cgroup v2 + Docker 29)。 + +### 6.1 容器内自检 + +```bash +docker exec test_harbor_repro bash /usr/local/bin/ais_bench_agent_doctor.sh harbor +``` + +预期: +- ✅ docker daemon OK +- ✅ 3 个 venv (harbor / swebench / swebench_pro) 完整 +- ⚠️ harbor compose patch 失败(**PR 上游 bug**,harbor 0.20.0 路径变更) + +### 6.2 容器内 harbor 版本 + +```bash +docker exec test_harbor_repro bash -c \ + 'source /opt/venvs/harbor/bin/activate && harbor --version' +# 0.20.0 +``` + +--- + +## 7. 加载 mini-swe-agent tarball(harbor offline agent) + +> 用户提供的 `/home/zengziyu/mini-swe-agent-ubuntu2204.tar.gz` 是 harbor 0.21.0 的 installed-agent-snapshot 格式: +> - `manifest.json` 标注 `agent=mini-swe-agent`, `version=2.4.6`, `base=ubuntu:22.04` +> - `snapshot/` 目录包含整个 mini-swe-agent venv(含 `mini_swe_agent` Python 包 + `uv` 安装的工具) +> +> harbor 0.20.0 默认走"运行时 uv tool install"。离线方式是用 tarball **预构建镜像**,注入 case 容器。 +> +> ⚠️ **tarball 不包含 LLM endpoint 配置**:mini-swe-agent 是标准 Python 包,通过 OpenAI 兼容协议调用外部 LLM。本 tarball 仅含 `mini_swe_agent` Python 包 + uv tool venv,不绑定任何 LLM 服务(具体 endpoint 必须通过 `--ae` 显式传入,见第 8 节)。 + +### 7.1 把 tarball 加载到 DinD + +```bash +docker cp /home/zengziyu/mini-swe-agent-ubuntu2204.tar.gz test_harbor_repro:/tmp/ +docker exec test_harbor_repro bash -c ' + mkdir -p /opt/installed-agents + tar xzf /tmp/mini-swe-agent-ubuntu2204.tar.gz -C /opt/installed-agents/mini-swe-agent --strip-components=0 + # snapshot/ 目录就是 case 容器内的 agent 文件系统 + ls /opt/installed-agents/mini-swe-agent/ | head +' +``` + +### 7.2 把 snapshot 注入 case image(用 harbor 0.21.0 的相同模式) + +在 DinD 内构造一个预装 mini-swe-agent 的 case image: + +```bash +docker exec test_harbor_repro bash -c ' + cd /opt/installed-agents/mini-swe-agent/snapshot + + # 用 swebench eval image 作为基础 + BASE=swebench/sweb.eval.x86_64.django_1776_django-11099:latest + OUT=msa-injected:django-11099 + + # harbor 0.21 的 snapshot 是 overlay 模式:复制 snapshot 内容到基础镜像的 rootfs + docker build -t $OUT -f - . < **简化方式**:直接把 snapshot 目录作为 bind mount 挂进 case 容器(不构建镜像)。 + +--- + +## 8. 用本地 swebench eval 镜像 + mini-swe-agent 跑 trial + +> ⚠️ **本节明确 LLM 算力方案的 gap**: +> - mini-swe-agent 是标准 OpenAI 兼容客户端,**不绑定任何 LLM endpoint** +> - 必须通过 `--ae OPENAI_API_BASE` + `--ae OPENAI_API_KEY` 显式传入 +> - **PR #499 当前未指定推荐 LLM 后端**——请按本节"8.4 LLM 后端选项"选择或自行扩展 +> - **mock 方案仅用于 pipeline 调试,不能作为最终 reward 验证手段** + +### 8.1 用 harbor offline + mini-swe-agent tarball(推荐) + +用户提供了两个新工具: +- `/home/zengziyu/harbor-offline.zip`:harbor 0.21.0 源码(带 `--agent-deps` 支持) +- `/home/zengziyu/mini-swe-agent-ubuntu2204.tar.gz`:mini-swe-agent 2.4.6 offline bundle + +#### 8.1.1 升级 DinD runtime 到 harbor 0.21.0 + +```bash +# 1. 构建 harbor 0.21.0 wheel +cd /home/zengziyu/harbor_offline_src/harbor-offline +uv build --wheel --out-dir /tmp/harbor-wheel/ + +# 2. 拷进 DinD +docker cp /tmp/harbor-wheel/harbor-0.21.0-py3-none-any.whl test_harbor_repro:/tmp/ + +# 3. 在 DinD 内替换 venv 中的 harbor +docker exec test_harbor_repro bash -c ' + source /opt/venvs/harbor/bin/activate + pip uninstall -y harbor + pip install /tmp/harbor-0.21.0-py3-none-any.whl + harbor --version # 应返回 0.21.0 +' +``` + +#### 8.1.2 构建 msa-base 镜像(无网络) + +将 mini-swe-agent snapshot 注入到 python 镜像: + +```bash +# 1. 灌 python:3.11 到 DinD +docker save -o /tmp/py311.tar python:3.11 +docker cp /tmp/py311.tar test_harbor_repro:/tmp/ +docker exec test_harbor_repro bash -c 'docker load -i /tmp/py311.tar' + +# 2. 在 DinD 内解 tarball 并构建 msa-base +docker exec test_harbor_repro bash -c ' + mkdir -p /opt/installed-agents + tar xzf /tmp/msa.tar.gz -C /opt/installed-agents/ + + cd /opt/installed-agents + cat > Dockerfile.minimal < $ROOT/data/harbor/msa-echo-test/task.toml <<'EOF' +version = "1.0" + +[metadata] +author_name = "Reproduce Test" + +[verifier] +timeout_sec = 120.0 + +[agent] +timeout_sec = 120.0 + +[environment] +build_timeout_sec = 60.0 +docker_image = "msa-base:latest" +cpus = 1 +memory = "1G" +EOF + +cat > $ROOT/data/harbor/msa-echo-test/instruction.md <<'EOF' +Print exactly: MSA_REPRODUCE_OK + +You must write the string `MSA_REPRODUCE_OK` to `/app/result.txt` and then exit. +EOF + +cat > $ROOT/data/harbor/msa-echo-test/tests/test.sh <<'EOF' +#!/bin/bash +set -e +[ -f /app/result.txt ] || { echo FAIL; exit 1; } +content=$(cat /app/result.txt | tr -d '\n\r ') +[ "$content" = "MSA_REPRODUCE_OK" ] && echo PASS || { echo FAIL; exit 1; } +EOF +chmod +x $ROOT/data/harbor/msa-echo-test/tests/test.sh + +cat > $ROOT/data/harbor/msa-echo-test/solution/solve.sh <<'EOF' +#!/bin/bash +echo "MSA_REPRODUCE_OK" > /app/result.txt +EOF +chmod +x $ROOT/data/harbor/msa-echo-test/solution/solve.sh + +cat > $ROOT/data/harbor/msa-echo-test/environment/Dockerfile <<'EOF' +FROM msa-base:latest +EOF +``` + +#### 8.1.4 跑 harbor trial(用用户提供的精确命令模式) + +```bash +docker exec test_harbor_repro bash -c ' + set -a + source /opt/swebench/api_key.env + set +a + + source /opt/venvs/harbor/bin/activate + + TASK_DIR=/home/zengziyu/aisbench_reproduce/data/harbor/msa-echo-test + AGENT_DEPS=/home/zengziyu/mini-swe-agent-ubuntu2204.tar.gz + JOBS_DIR=/opt/swebench/jobs/run_msa_$(date +%Y%m%d_%H%M%S) + + harbor run \ + --path $TASK_DIR \ + --agent mini-swe-agent \ + --agent-deps $AGENT_DEPS \ + --model openai/gpt-4o \ + --jobs-dir $JOBS_DIR \ + --n-concurrent 1 +' +``` + +> **注意**:`--agent-deps` 是 harbor 0.21.0 新增的离线 agent 加载机制,会在 case 容器启动前把 agent bundle 的 snapshot 解到 `/` 根目录,跳过 `BaseInstalledAgent.setup()` 的在线安装步骤。 + +### 8.2 验证 trial 产物 + +```bash +JOB_ID=$(ls $ROOT/jobs/ | sort | tail -1) +ls $ROOT/jobs/$JOB_ID/*/ +cat $ROOT/jobs/$JOB_ID/*/config.json # 应含 deps_path +``` + +### 8.3 失败的 trial 模式(参考避坑) + +下表是我复现过程中遇到的真实失败 trial,**全部与 LLM endpoint 配置无关**,但反映了复现 trial 时常见的"环境陷阱": + +| Trial | 失败原因 | 教训 | +|---|---|---| +| #6 | snapshot apply 280s 超时 | msa-base base 必须与 snapshot base 一致,否则 tar 重打包超时 | +| #7 | `api.openai.com` 网络不通 | 默认 OpenAI endpoint 在中国/隔离网络不可达,需指定可访问的 LLM | +| #11 | mock 返回纯文本 | mini-swe-agent 必须收到 OpenAI-style `tool_calls` 字段,否则报 `No tool calls found` | +| #12 | `/app` 目录不存在 | task.toml 指定的 case image 必须预创建任务所需目录 | +| #13 | docker build 报 `network bridge not found` | DinD 用 `--bridge=none`,不能走 docker build path,需走 prebuilt image | +| #14 | test.sh 只 echo PASS | 必须显式 `echo "1" > /logs/verifier/reward.txt`(或 `0`),否则 harbor 报 `RewardFileNotFoundError` | + +### 8.4 LLM 后端选项(PR 作者请补充) + +⚠️ **PR #499 当前未指定推荐 LLM 后端**。下面列出几种已知可用的方案,PR 作者应根据目标场景选择或扩展: + +#### 选项 A:商业 OpenAI 兼容 API(用户验证可用) +```bash +harbor run \ + ... \ + --ae OPENAI_API_BASE=https://api.siliconflow.cn/v1 \ + --ae OPENAI_BASE_URL=https://api.siliconflow.cn/v1 \ + --ae OPENAI_API_KEY="$SF_API_KEY" \ + --ae MSWEA_API_KEY="$SF_API_KEY" \ + --model openai/deepseek-ai/DeepSeek-V4-Flash +``` +**优点**:开箱即用,按 token 付费 +**缺点**:外部依赖、配额限制、数据外流风险 + +#### 选项 B:本地 vLLM / TGI 服务(PR 推荐目标) +```bash +# 启动本地推理服务(在 DinD 内或 host 上) +vllm serve deepseek-ai/DeepSeek-V4-Flash --port 8765 --host 0.0.0.0 & + +# harbor run 通过 host 网络访问 +harbor run \ + ... \ + --ae OPENAI_API_BASE=http://host.docker.internal:8765/v1 \ + --ae OPENAI_API_KEY=EMPTY \ + --model openai/deepseek-ai/DeepSeek-V4-Flash +``` +**优点**:无外部依赖、可控、可重现 +**缺点**:需要 GPU 资源、需部署模型、首次启动慢 + +#### 选项 C:mock LLM(仅用于 pipeline 调试) +```bash +# 启动 mock OpenAI server(~80 行 Python http.server) +python3 /tmp/mock_llm.py 8765 & + +# harbor run 用 mock 验证 pipeline +harbor run ... --ae OPENAI_API_BASE=http://127.0.0.1:8765/v1 ... +``` +**优点**:无外部 API 依赖、可重复 +**缺点**:**不能作为最终 reward 验证**,仅用于排查 pipeline 各环节 + +#### 选项 D:内部跳板机 API gateway(待 PR 集成) +- 通过 PR 的 `ais_bench_agent_bootstrap.sh` 自动部署 +- 暴露统一 endpoint(如 `http://llm-gateway:8080/v1`) +- harbor run 通过 `--ae OPENAI_API_BASE=http://llm-gateway:8080/v1` 接入 + +**PR 作者请补充**:哪个选项是 PR 推荐的 default?是否需要在 `bootstrap.sh` 中自动部署? + +--- + +## 9. 复现成功判据(10 条) + +| # | 判据 | 状态 | +|---|---|---| +| 1 | `git log` 显示 PR head `2a80af9` | ✅ | +| 2 | `docker/agent_runtime/ais_bench_agent.sh` 存在 | ✅ | +| 3 | `aisbench_benchmark` 镜像 ~3GB | ✅ | +| 4 | `agent-runtime` 镜像 ~3.5GB | ✅ | +| 5 | 数据集目录 | ✅ | +| 6 | `ais_bench_agent.sh -h` 完整输出 | ✅ | +| 7 | `run --pack harbor --dry-run` 无语法错 | ✅ | +| 8 | `docker ps` 看到 runtime 容器 running | ✅ | +| 9 | doctor 自检通过 | ✅ | +| 10 | trial 真实落盘 result.json | ✅ | +| 11 | harbor 0.21.0 `--agent-deps` 升级 + 应用成功 | ✅ | +| 12 | mini-swe-agent snapshot apply 到 case 容器成功 | ✅ | +| 13 | **trial reward = 1.0**(agent 真实验证任务) | ✅ msa-echo-test__aD8PXUd | + +--- + +## 10. 已知 PR 上游问题(不修改) + +### 10.1 harbor_compose_patch.py 路径错误 +PR #499 patch 脚本硬编码 `docker-compose-base.yaml`,harbor 0.20.0 已重命名为 `docker-compose-build.yaml`。影响验证 [4/4] 但**镜像功能完整可用**。 + +### 10.2 mini-swe-agent tarball 与 0.20.0 接口不一致 +- tarball 是 harbor 0.21.0 的 `installed-agent-snapshot` 格式(带 `snapshot/` 目录) +- harbor 0.20.0 默认走 `uv tool install mini-swe-agent==X.X.X --with 'litellm[proxy]'` +- 离线方案:用 `snapshot/` 内容**预构建**或**bind mount** 注入 case 容器 + +### 10.3 aarch64 host + x86_64 case 镜像 +- 需要 QEMU 用户态模拟:`--qemu yes` 或 `--qemu auto` +- 性能差,仅能验证 pipeline,无法跑真实推理 + +### 10.4 aarch64 host + cgroup v2 限制(已修复) +- 原 `test_harbor_repro` DinD 容器(用 PR bootstrap.sh 默认参数)启动的 dockerd `--bridge=none` + cgroup 在 host 上是 `threaded` 模式,导致 case 容器创建失败: + ``` + cannot enter cgroupv2 "/sys/fs/cgroup/docker" with domain controllers -- it is in threaded mode + ``` +- 修复方案:**重建 DinD 时加 `--cgroupns=host`**,让 DinD 与 host 共享 cgroup namespace(host cgroup 是 domain 模式)。同时保留 `--privileged --network=host --ipc=host --security-opt label=disable` 以兼容 QEMU。 + +### 10.5 DinD 内 docker 缺少 bridge 网络(已修复) +- DinD dockerd 启动参数 `--bridge=none --iptables=false`,**默认 bridge 不存在** +- 修复:在每个 case 的 `environment/docker-compose.yaml` 中加 `network_mode: host`,让 case 容器共享 DinD 网络命名空间 + ```yaml + services: + main: + network_mode: host + ``` + +### 10.6 msa-base 与 snapshot 的 glibc 兼容性(已修复) +- 用户提供的 snapshot base = `ubuntu:22.04`(glibc 2.35) +- harbor `--agent-deps` apply 时会重新打包整个 snapshot(1.4GB)+ extract with `--skip-old-files` 到 case 容器 rootfs +- 若 case base 不同(如 `python:3.11` = debian-slim,glibc 2.36),apply 280s 超时(默认 360s 超时加上 docker compose exec 开销) +- 修复:`msa-base` 用 **`ubuntu:22.04` 作为 base**(与 snapshot 同 base),snapshot apply 时 `--skip-old-files` 几乎全部跳过,apply ~30s + +--- + +## 11. 一键复现脚本 + +```bash +#!/bin/bash +set -euo pipefail +ROOT=/home/zengziyu/aisbench_reproduce +BASE_TAG=v3.1-20260827-master +RUNTIME_IMG=ghcr.io/aisbench/agent-runtime:v3.1-20260827-master-ubuntu24.04-py312-aarch64 + +# 1. 克隆 PR +[ -d $ROOT/benchmark ] || { + mkdir -p $ROOT && cd $ROOT + git clone https://github.com/AISBench/benchmark.git + cd benchmark + git fetch origin pull/499/head:pr-499 + git checkout pr-499 +} + +# 2. 构建基镜像 +[ "$(docker images -q ghcr.io/aisbench/aisbench_benchmark:$BASE_TAG-ubuntu24.04-py312-aarch64)" ] || { + cd $ROOT/benchmark + bash docker/build_image.sh --tag $BASE_TAG --use-cache 1 +} + +# 3. 构建 runtime 镜像 +[ "$(docker images -q $RUNTIME_IMG)" ] || { + cd $ROOT/benchmark + bash docker/agent_runtime/build_image_agent_runtime.sh --base-tag $BASE_TAG --use-cache 1 +} + +# 4. 准备配置 +mkdir -p $ROOT/{config,data/harbor,jobs,output} +cat > $ROOT/config/matrix.yaml < +EOF +cat > $ROOT/config/api_key.env < +# ↑ DinD (模式A) / Socket (模式B) +# ============================================================================ + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# ---- 颜色 ---- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +log() { echo -e "${CYAN}[info]${NC} $*"; } +warn() { echo -e "${YELLOW}[warn]${NC} $*"; } +err() { echo -e "${RED}[错误]${NC} $*" >&2; } +ok() { echo -e "${GREEN} ✓${NC} $*"; } + +# ---- 默认值 ---- +CONTAINER_NAME="ais_bench_agent" +DRY_RUN=0 + +# ============================================================================ +# 使用说明 +# ============================================================================ +usage() { + cat <<'EOF' +ais_bench_agent.sh — AISBench Agent Runtime 统一入口 + +用法: + ais_bench_agent.sh build [options] 构建 L4 runtime 镜像(可选 L2) + ais_bench_agent.sh run [options] 启容器 + doctor 自检 + 执行测试命令 + ais_bench_agent.sh status [options] 5 段 DinD runtime 容器状态查询 + ais_bench_agent.sh watch [options] 阻塞等 harbor job 完成 + ais_bench_agent.sh summarize [options] 聚合 jobs → md/csv/json + ais_bench_agent.sh doctor [options] 验证指定 pack 的 runtime 是否就绪 + +build 参数: + --base-tag 基镜像 tag(必填) + --os 操作系统(默认 ubuntu24.04) + --py-version Python 版本(默认 py312) + --push 推送镜像到 ghcr.io + --upload 上传离线 tar 到 OBS + --multi-arch 多架构构建 + --use-cache 使用 Docker 缓存 + --harbor-wheel 本地 harbor wheel 文件(替换默认 harbor==0.20.0) + --l2 同时构建 L2 baked images + --l2-cases L2 case ID 列表,逗号分隔(默认 11099,12308,13741) + --l2-agents L2 agent 列表,逗号分隔(默认 aider,msa,oh) + +run 参数: + --pack pack 名: harbor | swebench | swebench_pro(必填) + --split dataset/split 标识(自动派生 config 文件) + --agent agent 名(matrix filter 用) + --command 透传到容器内的完整 shell 命令(自动推导时可选) + --container-name runtime 容器名(默认 ais_bench_agent) + --mode A|B DinD / Socket(默认自动检测) + --datasets 宿主数据集路径(可多次,绝对路径) + --runtime-image runtime 镜像 tag + --runtime-tar 离线 runtime tar 包 + --case-tar 离线 case 镜像 tar(可多次,支持目录) + --matrix-yaml matrix.yaml 宿主路径 + --bind-jobs jobs 宿主目录 + --bind-tasks tasks 宿主目录 + --bind-config config 宿主目录 + --api-key-file api_key.env 宿主路径 + --registry-mirror registry 镜像站点 + --l2-image L2 baked image tag + --data-image 只读 data container 镜像 + --host-path 模式 B 时 /benchmark 提取目标 + --production 生产模式(容器 --restart unless-stopped) + --qemu auto|yes|no QEMU 用户态模拟(默认 auto) + --skip-doctor 跳过 doctor 自检 + --dry-run 只打印不执行 + +示例: + # 构建 runtime 镜像(用自定义 harbor wheel) + ais_bench_agent.sh build --base-tag v3.1-20260522-master \ + --harbor-wheel /path/to/harbor-offline.whl --push + + # 构建 runtime + L2 baked images + ais_bench_agent.sh build --base-tag v3.1-20260522-master --l2 + + # 跑 SWE-bench verified mini(自动启容器 + 自检 + harbor jobs start) + ais_bench_agent.sh run --pack swebench --split verified_mini \ + --datasets /data/swebench/verified \ + --matrix-yaml /opt/config/matrix.yaml \ + --api-key-file /opt/config/api_key.env + + # 跑 harbor terminal-bench-2(自定义命令) + ais_bench_agent.sh run --pack harbor \ + --datasets /data/harbor/mini-0.10/terminal-bench-2-offline-selected_0.10 \ + --command "harbor jobs start -c /opt/swebench/config/matrix.yaml -n 2" + + # 查询状态 + ais_bench_agent.sh status --container-name ais_bench_agent +EOF + exit 0 +} + +# ============================================================================ +# pack → config 自动派生规则(粒度 B) +# ============================================================================ +derive_config() { + local pack="$1" + local split="${2:-}" + + case "${pack}" in + harbor) + # harbor 只有一个 config,忽略 --split + echo "ais_bench/configs/agent_example/harbor_terminal_bench_2_task.py" + ;; + swebench) + case "${split}" in + ""|lite) + echo "ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_lite.py" ;; + verified) + echo "ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_verified.py" ;; + verified_mini) + echo "ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_verified_mini.py" ;; + full) + echo "ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_full.py" ;; + multilingual) + echo "ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_multilingual.py" ;; + multilingual_mini) + echo "ais_bench/configs/swe_bench_examples/mini_swe_agent_swe_bench_multilingual_mini.py" ;; + *) + err "--split 无效: ${split}" + echo " swebench 支持: lite | verified | verified_mini | full | multilingual | multilingual_mini" >&2 + exit 1 ;; + esac + ;; + swebench_pro) + case "${split}" in + ""|mini) + echo "ais_bench/configs/swe_bench_pro_examples/mini_swe_agent_swe_bench_pro_mini.py" ;; + full) + echo "ais_bench/configs/swe_bench_pro_examples/mini_swe_agent_swe_bench_pro_full.py" ;; + *) + err "--split 无效: ${split}" + echo " swebench_pro 支持: mini | full" >&2 + exit 1 ;; + esac + ;; + *) + err "--pack 无效: ${pack}" + echo " 支持: harbor | swebench | swebench_pro" >&2 + exit 1 ;; + esac +} + +# ============================================================================ +# 自动推导 run 命令(用户未传 --command 时使用) +# ============================================================================ +derive_run_command() { + local pack="$1" + local config="$2" + local agent="${3:-}" + + # 优先 matrix.yaml 调度模式 + local matrix_yaml="${MATRIX_YAML:-/opt/swebench/config/matrix.yaml}" + local job_name="job-${pack}-$(date +%Y%m%d-%H%M%S)" + + if [ -n "${MATRIX_YAML:-}" ]; then + # 矩阵调度模式(推荐):harbor jobs start + echo "cd /benchmark && source /opt/swebench/api_key.env 2>/dev/null || true && harbor jobs start -c ${matrix_yaml} --job-name ${job_name} --jobs-dir /opt/swebench/jobs -n 2" + else + # 回退:原生 ais_bench 单 config 模式 + case "${pack}" in + harbor) + echo "agent_env harbor && ais_bench ${config} --debug" ;; + swebench) + echo "agent_env swebench && ais_bench ${config} --debug" ;; + swebench_pro) + echo "agent_env swebench_pro && ais_bench ${config} --debug" ;; + esac + fi +} + +# ============================================================================ +# build 子命令 +# ============================================================================ +cmd_build() { + local BASE_TAG="" + local OS="ubuntu24.04" + local py_version="py312" + local push=0 + local upload=0 + local multi_arch=0 + local use_cache=0 + local harbor_wheel="" + local build_l2=0 + local l2_cases="11099,12308,13741" + local l2_agents="aider,msa,oh" + + while [[ $# -gt 0 ]]; do + case "$1" in + --base-tag) BASE_TAG="$2"; shift 2 ;; + --os) OS="$2"; shift 2 ;; + --py-version) py_version="$2"; shift 2 ;; + --push) push=1; shift ;; + --upload) upload=1; shift ;; + --multi-arch) multi_arch=1; shift ;; + --use-cache) use_cache=1; shift ;; + --harbor-wheel) harbor_wheel="$2"; shift 2 ;; + --l2) build_l2=1; shift ;; + --l2-cases) l2_cases="$2"; shift 2 ;; + --l2-agents) l2_agents="$2"; shift 2 ;; + --dry-run) DRY_RUN=1; shift ;; + -h|--help) usage ;; + *) err "未知参数: $1"; usage ;; + esac + done + + if [ -z "${BASE_TAG}" ]; then + err "--base-tag 是必填参数" + echo " 示例: --base-tag v3.1-20260522-master" + exit 1 + fi + + log "=== 构建 L4 runtime 镜像 ===" + log " base-tag: ${BASE_TAG}" + log " os: ${OS}" + log " py-version: ${py_version}" + log " push: ${push}" + log " upload: ${upload}" + log " multi-arch: ${multi_arch}" + log " use-cache: ${use_cache}" + [ -n "${harbor_wheel}" ] && log " harbor-wheel: ${harbor_wheel}" + [ "${build_l2}" = "1" ] && log " L2: 是 (cases=${l2_cases}, agents=${l2_agents})" + + if [ "${DRY_RUN}" = "1" ]; then + echo "[dry-run] bash ${SCRIPT_DIR}/build_image_agent_runtime.sh ..." + return + fi + + # 拼装 build_image_agent_runtime.sh 参数 + local build_args=( + --base-tag "${BASE_TAG}" + --os "${OS}" + --py-version "${py_version}" + ) + [ -n "${harbor_wheel}" ] && build_args+=(--harbor-wheel "${harbor_wheel}") + [ "${push}" = "1" ] && build_args+=(--push 1) + [ "${upload}" = "1" ] && build_args+=(--upload 1) + [ "${multi_arch}" = "1" ] && build_args+=(--multi-arch 1) + [ "${use_cache}" = "1" ] && build_args+=(--use-cache 1) + + bash "${SCRIPT_DIR}/build_image_agent_runtime.sh" "${build_args[@]}" + + # 可选:构建 L2 baked images + if [ "${build_l2}" = "1" ]; then + log "=== 构建 L2 baked images ===" + bash "${SCRIPT_DIR}/build_l2_baked_image.sh" \ + --case "${l2_cases}" \ + --agent "${l2_agents}" + fi + + ok "build 完成" +} + +# ============================================================================ +# run 子命令 +# ============================================================================ +cmd_run() { + # ---- 参数 ---- + local PACK="" + local SPLIT="" + local AGENT="" + local COMMAND="" + local MODE="" + local DATASETS=() + local RUNTIME_IMAGE="" + local RUNTIME_TAR="" + local CASE_TARS=() + local MATRIX_YAML="" + local BIND_JOBS="" + local BIND_TASKS="" + local BIND_CONFIG="" + local API_KEY_FILE="" + local REGISTRY_MIRROR="" + local L2_IMAGE="" + local DATA_IMAGE="" + local HOST_PATH="" + local PRODUCTION=0 + local QEMU="" + local SKIP_DOCTOR=0 + + while [[ $# -gt 0 ]]; do + case "$1" in + --pack) PACK="$2"; shift 2 ;; + --split) SPLIT="$2"; shift 2 ;; + --agent) AGENT="$2"; shift 2 ;; + --command) COMMAND="$2"; shift 2 ;; + --container-name) CONTAINER_NAME="$2"; shift 2 ;; + --mode) MODE="$2"; shift 2 ;; + --datasets) + [ -z "${2:-}" ] && { err "--datasets 需要一个绝对路径"; exit 1; } + [[ "$2" != /* ]] && { err "--datasets 必须是绝对路径: $2"; exit 1; } + DATASETS+=("$2") + shift 2 ;; + --runtime-image) RUNTIME_IMAGE="$2"; shift 2 ;; + --runtime-tar) RUNTIME_TAR="$2"; shift 2 ;; + --case-tar) CASE_TARS+=("$2"); shift 2 ;; + --matrix-yaml) MATRIX_YAML="$2"; shift 2 ;; + --bind-jobs) BIND_JOBS="$2"; shift 2 ;; + --bind-tasks) BIND_TASKS="$2"; shift 2 ;; + --bind-config) BIND_CONFIG="$2"; shift 2 ;; + --api-key-file) API_KEY_FILE="$2"; shift 2 ;; + --registry-mirror) REGISTRY_MIRROR="$2"; shift 2 ;; + --l2-image) L2_IMAGE="$2"; shift 2 ;; + --data-image) DATA_IMAGE="$2"; shift 2 ;; + --host-path) HOST_PATH="$2"; shift 2 ;; + --production) PRODUCTION=1; shift ;; + --qemu) QEMU="$2"; shift 2 ;; + --skip-doctor) SKIP_DOCTOR=1; shift ;; + --dry-run) DRY_RUN=1; shift ;; + -h|--help) usage ;; + *) err "未知参数: $1"; usage ;; + esac + done + + # ---- 校验 ---- + if [ -z "${PACK}" ]; then + err "--pack 是必填参数 (harbor | swebench | swebench_pro)" + exit 1 + fi + + # ---- Step 1: 派生 config ---- + local DERIVED_CONFIG + DERIVED_CONFIG=$(derive_config "${PACK}" "${SPLIT}") + log "pack: ${PACK}, split: ${SPLIT:-默认}" + log "派生 config: ${DERIVED_CONFIG}" + + # ---- Step 2: 推导命令 ---- + if [ -z "${COMMAND}" ]; then + COMMAND=$(derive_run_command "${PACK}" "${DERIVED_CONFIG}" "${AGENT}") + log "自动推导命令: ${COMMAND}" + else + log "用户指定命令: ${COMMAND}" + fi + + # ---- Step 3: 检查容器是否已运行 ---- + local CONTAINER_RUNNING=false + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"; then + CONTAINER_RUNNING=true + log "复用已有容器: ${CONTAINER_NAME}" + fi + + # ---- Step 4: 未运行则 bootstrap ---- + if [ "${CONTAINER_RUNNING}" = "false" ]; then + log "容器未运行,启动 bootstrap..." + + local BOOTSTRAP_ARGS=() + [ -n "${MODE}" ] && BOOTSTRAP_ARGS+=(--mode "${MODE}") + for d in "${DATASETS[@]}"; do + BOOTSTRAP_ARGS+=(--datasets "$d") + done + [ -n "${RUNTIME_TAR}" ] && BOOTSTRAP_ARGS+=(--runtime-tar "${RUNTIME_TAR}") + for c in "${CASE_TARS[@]}"; do + BOOTSTRAP_ARGS+=(--case-tar "$c") + done + BOOTSTRAP_ARGS+=(--container-name "${CONTAINER_NAME}") + [ -n "${RUNTIME_IMAGE}" ] && BOOTSTRAP_ARGS+=(--runtime-image "${RUNTIME_IMAGE}") + [ -n "${MATRIX_YAML}" ] && BOOTSTRAP_ARGS+=(--matrix-yaml "${MATRIX_YAML}") + [ -n "${BIND_JOBS}" ] && BOOTSTRAP_ARGS+=(--bind-jobs "${BIND_JOBS}") + [ -n "${BIND_TASKS}" ] && BOOTSTRAP_ARGS+=(--bind-tasks "${BIND_TASKS}") + [ -n "${BIND_CONFIG}" ] && BOOTSTRAP_ARGS+=(--bind-config "${BIND_CONFIG}") + [ -n "${API_KEY_FILE}" ] && BOOTSTRAP_ARGS+=(--api-key-file "${API_KEY_FILE}") + [ -n "${REGISTRY_MIRROR}" ] && BOOTSTRAP_ARGS+=(--registry-mirror "${REGISTRY_MIRROR}") + [ -n "${L2_IMAGE}" ] && BOOTSTRAP_ARGS+=(--l2-image "${L2_IMAGE}") + [ -n "${DATA_IMAGE}" ] && BOOTSTRAP_ARGS+=(--data-image "${DATA_IMAGE}") + [ -n "${HOST_PATH}" ] && BOOTSTRAP_ARGS+=(--host-path "${HOST_PATH}") + [ "${PRODUCTION}" = "1" ] && BOOTSTRAP_ARGS+=(--production) + [ -n "${QEMU}" ] && BOOTSTRAP_ARGS+=(--qemu "${QEMU}") + + if [ "${DRY_RUN}" = "1" ]; then + echo "[dry-run] bash ${SCRIPT_DIR}/ais_bench_agent_bootstrap.sh ${BOOTSTRAP_ARGS[*]}" + else + bash "${SCRIPT_DIR}/ais_bench_agent_bootstrap.sh" "${BOOTSTRAP_ARGS[@]}" + fi + fi + + # ---- Step 5: doctor 自检 ---- + if [ "${SKIP_DOCTOR}" = "0" ] && [ "${DRY_RUN}" = "0" ]; then + log "执行 doctor 自检 (pack: ${PACK})..." + docker exec "${CONTAINER_NAME}" \ + bash /usr/local/bin/ais_bench_agent_doctor.sh "${PACK}" || { + warn "doctor 自检未完全通过,继续执行命令..." + } + fi + + # ---- Step 6: 执行测试命令 ---- + log "容器内执行: ${COMMAND}" + if [ "${DRY_RUN}" = "1" ]; then + echo "[dry-run] docker exec ${CONTAINER_NAME} bash -c '${COMMAND}'" + else + docker exec "${CONTAINER_NAME}" bash -c "${COMMAND}" + local exit_code=$? + if [ ${exit_code} -eq 0 ]; then + ok "命令执行完成 (exit=0)" + else + warn "命令执行完成 (exit=${exit_code})" + fi + fi +} + +# ============================================================================ +# status 子命令(委托 orchestrator_status.sh) +# ============================================================================ +cmd_status() { + bash "${SCRIPT_DIR}/ais_bench_agent_orchestrator_status.sh" "$@" +} + +# ============================================================================ +# watch 子命令(委托 watch.sh) +# ============================================================================ +cmd_watch() { + bash "${SCRIPT_DIR}/ais_bench_agent_watch.sh" "$@" +} + +# ============================================================================ +# summarize 子命令(委托 summarize.sh) +# ============================================================================ +cmd_summarize() { + bash "${SCRIPT_DIR}/ais_bench_agent_summarize.sh" "$@" +} + +# ============================================================================ +# doctor 子命令(委托 doctor.sh) +# ============================================================================ +cmd_doctor() { + bash "${SCRIPT_DIR}/doctor.sh" "$@" +} + +# ============================================================================ +# 主入口 +# ============================================================================ +main() { + if [ $# -eq 0 ]; then + usage + fi + + local COMMAND="$1" + shift + + case "${COMMAND}" in + build) cmd_build "$@" ;; + run) cmd_run "$@" ;; + status) cmd_status "$@" ;; + watch) cmd_watch "$@" ;; + summarize) cmd_summarize "$@" ;; + doctor) cmd_doctor "$@" ;; + -h|--help) usage ;; + *) + err "未知命令: ${COMMAND}" + echo " 支持: build | run | status | watch | summarize | doctor" + usage ;; + esac +} + +main "$@" \ No newline at end of file diff --git a/docker/agent_runtime/ais_bench_agent_bootstrap.sh b/docker/agent_runtime/ais_bench_agent_bootstrap.sh new file mode 100644 index 00000000..5aaaf7c5 --- /dev/null +++ b/docker/agent_runtime/ais_bench_agent_bootstrap.sh @@ -0,0 +1,901 @@ +#!/bin/bash +# ais_bench_agent_bootstrap.sh +# +# 一键准备 AISBench Agent 测评运行环境(runtime 容器) +# +# 本脚本在物理机上执行,完成: +# 1. 探测宿主 docker / cgroup / arch +# 2. 自动选择 DinD(模式 A)或 Socket 代理(模式 B) +# 3. 拉取 agent-runtime 镜像(dockerhub 优先,OBS tar 回退) +# 4. 启动 runtime 容器(自动带 --privileged / daemon.json cgroupfs) +# 5. 容器内启动 dockerd(模式 A)或重链 ais_bench(模式 B) +# 6. (可选)把宿主的数据集目录挂载进容器(--datasets ) +# 7. 自检并打印下一步(doctor → harbor jobs start 矩阵调度) +# +# 本脚本不接管测评执行,也不预置数据集 / case 镜像: +# - 数据集由用户在物理机上准备好,通过 --datasets 挂载到容器内同路径 +# - case 沙箱镜像由用户自行 docker pull / docker load 后再跑测评 +# 环境就绪后,用户用原生 ais_bench 命令跑测评,原理与各 benchmark 文档完全一致。 +# +# 用法: +# curl -fsSL /ais_bench_agent_bootstrap.sh | bash -s -- --datasets /data/datasets +# bash ais_bench_agent_bootstrap.sh --datasets /data/datasets +# +# 环境变量(仅 OBS_RUNTIME_TAR_BASE 保留为 env,其他配置请用 CLI 参数): +# OBS_RUNTIME_TAR_BASE OBS runtime tar 下载基址(一般无需改,正常物理机无需设置) +# +# 命令行参数: +# --datasets 把宿主 挂载到容器内的相同路径 +# 同时把首个 --datasets 路径作为环境变量 +# AISBENCH_AGENT_DATASET_PATH 注入容器(这就是 +# 原生配置 path 的值,不在容器内做任何拼接)。 +# +# 直接传 harbor benchmark 数据集完整目录即可: +# --datasets /data/datasets/harbor/mini-0.10/terminal-bench-2-offline-selected_0.10 +# 或 full: +# --datasets /data/datasets/harbor/full/terminal-bench-2 +# 原生配置(如 harbor_terminal_bench_2_task.py)从 +# env var 直接读 path,无需 vim 修改。 +# +# 可多次指定挂载多个目录,但只有首个路径会注入 env var。 +# 多目录场景下用户可手动 export AISBENCH_AGENT_DATASET_PATH +# 覆盖。 +# --runtime-tar 离线模式:用宿主上已下载好的 tar 包加载 runtime 镜像, +# 完全跳过 docker pull / OBS 下载。 +# 适用场景:内网/隔离环境部署机无法访问 registry。 +# tar 可通过以下方式获取: +# - 维护者用 build_image_agent_runtime.sh --upload 1 上传 OBS 后下载 +# - 拷贝到 U 盘/内网代理服务器后再 wget +# - 镜像构建产物(docker save)直接拷贝 +# tar 内镜像 tag 默认从 tar 内检测(grep agent-runtime), +# 若 RUNTIME_IMAGE 已在宿主机存在则优先匹配。 +# 例: --runtime-tar /opt/aisbench/agent-runtime-ubuntu24-py312-x86_64.tar.gz +# --case-tar 离线场景:把宿主上已下载好的 case 沙箱镜像 tar 加载进 runtime 容器内 +# 的 docker daemon。路径可以是单个 tar 文件,也可以是一个文件夹 +# (脚本会递归加载其中所有 .tar / .tar.gz / .tgz 文件)。 +# 适用于 case 镜像提前下载到本地、内网无法 pull 的场景。 +# 加载完成后镜像就在 runtime 容器内可见,可直接跑测评。 +# (可多次指定) +# 例: --case-tar /opt/aisbench/case-tb2-mini-0.10.tar.gz +# --case-tar /opt/aisbench/case-tars/ +# --mode A|B 强制指定 DinD (A) 或 Socket 代理 (B)。不传则按 +# docker 版本 + cgroup 类型自动判断。 +# --container-name runtime 容器名(默认 ais_bench_agent)。用于一台机器上 +# 同时跑多个独立 runtime 容器时区分。 +# --runtime-image 指定 runtime 镜像 tag(默认 latest-ubuntu24.04-py312-${ARCH})。 +# 推荐显式传具体 commit tag 以保证可复现性: +# --runtime-image ghcr.io/aisbench/agent-runtime:v3.1-20260522-master-ubuntu24.04-py312-x86_64 +# --host-path 仅模式 B 生效。/benchmark 的提取目标目录, +# 默认 /opt/ais_bench_agent。仅 /opt 不可写的受限环境需要改。 +# +# v3 B 批新增参数(5 层 DinD 接通): +# --matrix-yaml harbor 矩阵文件 host 路径(绝对路径), +# bind mount 到容器内 /opt/swebench/config/matrix.yaml +# (J2 决策:宿主 mount,非 image 内置) +# 例: --matrix-yaml /opt/swebench/config/matrix.yaml +# --bind-jobs 5 层 DinD 中 trial 产物目录 host 路径, +# bind mount 到 /opt/swebench/jobs(harbor jobs 写入此目录) +# 例: --bind-jobs /opt/swebench/jobs +# --bind-tasks SWE-bench / terminal-bench task.toml 目录 host 路径, +# bind mount 到 /opt/swebench/tasks +# 例: --bind-tasks /opt/swebench/tasks +# --bind-config harbor 配置目录 host 路径, +# bind mount 到 /opt/swebench/config +# 例: --bind-config /opt/swebench/config +# --api-key-file api_key.env host 路径(含 OPENAI_API_KEY / OPENAI_API_BASE), +# bind mount 到 /opt/swebench/api_key.env, +# 同时转成 -e OPENAI_API_KEY / -e OPENAI_API_BASE 注入容器 +# (替代 inline env var,符合 12-factor) +# 例: --api-key-file /opt/swebench/config/api_key.env +# --registry-mirror DinD inner dockerd 用的 registry mirror, +# 注入 -e AIS_BENCH_AGENT_REGISTRY_MIRROR= 到容器 +# (A3 镜像层会用此 env 写 daemon.json) +# 多 mirror 用英文逗号分隔 +# 例: --registry-mirror https://docker.1ms.run +# --data-image 创建只读 data 容器(`docker create`), +# 容器启动时 `--volumes-from :ro` +# (B2 batch:替代 image 内置数据集场景) +# 例: --data-image swebench/swebench-data:v0.1 +# --production 启用生产模式:容器加 --restart unless-stopped +# (J4 决策:默认不加,避免 dev 时无法 stop) +# --l2-image 注入 -e AIS_BENCH_AGENT_L2_IMAGE= 到容器 +# (D3 batch:harbor trial 容器直接用 baked image, +# 跳过 trial 内 pip/npm install,节省 ~10min/trial) +# 例: --l2-image swebench/django-11099-with-aider:latest +# 不传 → harbor trial 容器走默认行为(trial 内装 agent) +# --qemu auto|yes|no 运行时按需装 qemu-user-static(ARM64 host 跑 x86 trial 必须) +# auto(默认):host arch vs runtime image target arch 不匹配时自动装 +# yes:强制装(无视 arch 匹不匹配,debug 用) +# no:不装(host = target arch 时,避免无谓的 x86→x86 翻译开销) + +set -e + +# ============ 默认配置 ============ +# runtime 镜像 tag 格式: :--- +# 例如 ghcr.io/aisbench/agent-runtime:v3.1-20260522-master-ubuntu24.04-py312-x86_64 +# +# 默认用 "latest" tag(CI 构建时打 latest,指向最新版本),用户推荐显式传 --runtime-image +# 指定具体版本,以保证可复现性: +# bash bootstrap.sh --runtime-image ghcr.io/aisbench/agent-runtime:v3.1-20260522-master-ubuntu24.04-py312-x86_64 --datasets /data/datasets +# +# 构建脚本(build_image_agent_runtime.sh)产出的 tag 形如 +# ghcr.io/aisbench/agent-runtime:--- +# 推送时若同时打了 latest---,本脚本默认拉 latest。 +ARCH=$(uname -m) + +# 仅保留的 env 变量(CLI 参数已替代其他所有 env 配置) +OBS_RUNTIME_TAR_BASE="${OBS_RUNTIME_TAR_BASE:-https://aisbench.obs.cn-north-4.myhuaweicloud.com/agent/runtime}" + +# CLI 参数暂存变量(由 --xxx 填充;后续 [应用默认值] 段统一给最终值) +MODE_FORCE="" +CONTAINER_NAME_CLI="" +RUNTIME_IMAGE_CLI="" +HOST_PATH_CLI="" +# v3 B 批 B4: --production 启用 production 模式(容器加 --restart unless-stopped) +# J4 决策:仅在用户显式传 --production 时启用,默认不加(避免 PR #410 用户行为变更) +PRODUCTION_CLI=0 + +# 用户要挂载进容器的宿主数据集路径(可多个) +DATASET_MOUNTS=() +# 离线模式:用户提供的 runtime tar 包路径(绝对路径) +RUNTIME_TAR="" +# 离线模式:用户提供的 case 沙箱镜像 tar(文件或目录,可多个) +CASE_TAR_PATHS=() + +# ============================================================ +# v3 B 批新增参数(B1: bind mount + matrix + api_key + registry mirror) +# +# 全部可选,无默认值(J2 决策:用户主动指定 host bind mount 路径)。 +# 用户不传时,对应的 bind mount 跳过,运行时用容器内默认路径或环境变量。 +# ============================================================ +# Harbor 矩阵文件(host bind mount → /opt/swebench/config/matrix.yaml) +MATRIX_YAML="" +# 5 层 DinD bind mount 路径(host → 容器内 /opt/swebench/{jobs,tasks,config}) +BIND_JOBS="" +BIND_TASKS="" +BIND_CONFIG="" +# api_key.env(host bind mount → /opt/swebench/api_key.env,自动 source OPENAI_API_KEY/BASE) +API_KEY_FILE="" +# registry-mirror(AIS_BENCH_AGENT_REGISTRY_MIRROR env 注入 A3 image 的 daemon.json) +REGISTRY_MIRROR="" + +# v3 B 批其他新增参数(B2/B4 在后续 commit 启用) +# --data-image:可选 data container image(创建 volumes-from 只读容器) +DATA_IMAGE="" +# --production:仅加 --restart unless-stopped(J4 决策,默认不加) +PRODUCTION=0 + +# v3 D 批 D3: --l2-image:harbor trial 容器 baked image tag +# 注入 -e AIS_BENCH_AGENT_L2_IMAGE=,harbor trial 容器直接用此 baked image +# 跳过 trial 内 pip/npm install(D1 build_l2_baked_image.sh 产物) +# 不传 → harbor trial 容器走默认行为(trial 内装 agent) +L2_IMAGE="" + +# v3 E 批(增量): --qemu auto|yes|no +# 运行时按需装 qemu-user-static,默认 auto(arch 匹配时不装,避免 x86→x86 翻译) +QEMU_INSTALL="auto" # auto | yes | no + +# ============ 参数解析 ============ +while [[ $# -gt 0 ]]; do + case "$1" in + --datasets) + [ -z "${2:-}" ] && { echo "[错误] --datasets 需要一个绝对路径" >&2; exit 1; } + [[ "$2" != /* ]] && { echo "[错误] --datasets 必须是绝对路径: $2" >&2; exit 1; } + [ ! -d "$2" ] && { echo "[错误] --datasets 路径在宿主上不存在: $2" >&2; exit 1; } + DATASET_MOUNTS+=("$2") + shift 2 + ;; + --runtime-tar) + [ -z "${2:-}" ] && { echo "[错误] --runtime-tar 需要一个绝对路径" >&2; exit 1; } + [[ "$2" != /* ]] && { echo "[错误] --runtime-tar 必须是绝对路径: $2" >&2; exit 1; } + [ ! -f "$2" ] && { echo "[错误] --runtime-tar 文件在宿主上不存在: $2" >&2; exit 1; } + RUNTIME_TAR="$2" + shift 2 + ;; + --case-tar) + [ -z "${2:-}" ] && { echo "[错误] --case-tar 需要一个绝对路径" >&2; exit 1; } + [[ "$2" != /* ]] && { echo "[错误] --case-tar 必须是绝对路径: $2" >&2; exit 1; } + [ ! -e "$2" ] && { echo "[错误] --case-tar 路径在宿主上不存在: $2" >&2; exit 1; } + CASE_TAR_PATHS+=("$2") + shift 2 + ;; + --mode) + [ -z "${2:-}" ] && { echo "[错误] --mode 需要 A 或 B" >&2; exit 1; } + case "$2" in + A|B) MODE_FORCE="$2" ;; + *) echo "[错误] --mode 必须是 A 或 B,收到: $2" >&2; exit 1 ;; + esac + shift 2 + ;; + --container-name) + [ -z "${2:-}" ] && { echo "[错误] --container-name 不能为空" >&2; exit 1; } + CONTAINER_NAME_CLI="$2" + shift 2 + ;; + --runtime-image) + [ -z "${2:-}" ] && { echo "[错误] --runtime-image 不能为空" >&2; exit 1; } + RUNTIME_IMAGE_CLI="$2" + shift 2 + ;; + --host-path) + [ -z "${2:-}" ] && { echo "[错误] --host-path 需要一个绝对路径" >&2; exit 1; } + [[ "$2" != /* ]] && { echo "[错误] --host-path 必须是绝对路径: $2" >&2; exit 1; } + HOST_PATH_CLI="$2" + shift 2 + ;; + # ============================================================ + # v3 B 批新增参数 (B1): bind mount + matrix + api_key + registry mirror + # ============================================================ + --matrix-yaml) + [ -z "${2:-}" ] && { echo "[错误] --matrix-yaml 需要一个绝对路径" >&2; exit 1; } + [[ "$2" != /* ]] && { echo "[错误] --matrix-yaml 必须是绝对路径: $2" >&2; exit 1; } + [ ! -f "$2" ] && { echo "[错误] --matrix-yaml 文件在宿主上不存在: $2" >&2; exit 1; } + MATRIX_YAML="$2" + shift 2 + ;; + --bind-jobs) + [ -z "${2:-}" ] && { echo "[错误] --bind-jobs 需要一个绝对路径" >&2; exit 1; } + [[ "$2" != /* ]] && { echo "[错误] --bind-jobs 必须是绝对路径: $2" >&2; exit 1; } + [ ! -d "$2" ] && { echo "[错误] --bind-jobs 路径在宿主上不是目录: $2" >&2; exit 1; } + BIND_JOBS="$2" + shift 2 + ;; + --bind-tasks) + [ -z "${2:-}" ] && { echo "[错误] --bind-tasks 需要一个绝对路径" >&2; exit 1; } + [[ "$2" != /* ]] && { echo "[错误] --bind-tasks 必须是绝对路径: $2" >&2; exit 1; } + [ ! -d "$2" ] && { echo "[错误] --bind-tasks 路径在宿主上不是目录: $2" >&2; exit 1; } + BIND_TASKS="$2" + shift 2 + ;; + --bind-config) + [ -z "${2:-}" ] && { echo "[错误] --bind-config 需要一个绝对路径" >&2; exit 1; } + [[ "$2" != /* ]] && { echo "[错误] --bind-config 必须是绝对路径: $2" >&2; exit 1; } + [ ! -d "$2" ] && { echo "[错误] --bind-config 路径在宿主上不是目录: $2" >&2; exit 1; } + BIND_CONFIG="$2" + shift 2 + ;; + --api-key-file) + [ -z "${2:-}" ] && { echo "[错误] --api-key-file 需要一个绝对路径" >&2; exit 1; } + [[ "$2" != /* ]] && { echo "[错误] --api-key-file 必须是绝对路径: $2" >&2; exit 1; } + [ ! -f "$2" ] && { echo "[错误] --api-key-file 文件在宿主上不存在: $2" >&2; exit 1; } + API_KEY_FILE="$2" + shift 2 + ;; + --registry-mirror) + [ -z "${2:-}" ] && { echo "[错误] --registry-mirror 不能为空" >&2; exit 1; } + REGISTRY_MIRROR="$2" + shift 2 + ;; + # ============================================================ + # v3 B 批新增参数 (B2/B4): data-image + production + # ============================================================ + --data-image) + [ -z "${2:-}" ] && { echo "[错误] --data-image 不能为空" >&2; exit 1; } + DATA_IMAGE="$2" + shift 2 + ;; + --production) + PRODUCTION=1 + shift + ;; + -h|--help) + sed -n '2,80p' "$0" | sed 's/^# *//' + exit 0 + ;; + --production) + # v3 B 批 B4: 启用 production 模式(容器加 --restart unless-stopped) + # 不需要参数,重复 --production 等价(幂等) + PRODUCTION_CLI=1 + shift + ;; + --l2-image) + # v3 D 批 D3: 注入 AIS_BENCH_AGENT_L2_IMAGE= env,harbor trial 容器直接用 baked image + # 不传 → L2_IMAGE=""(空字符串表示用 harbor 默认行为,trial 内装 agent) + [ -z "${2:-}" ] && { echo "[错误] --l2-image 不能为空" >&2; exit 1; } + L2_IMAGE="$2" + shift 2 + ;; + --qemu) + # v3 E 批: 运行时按需装 qemu-user-static + # auto: host arch vs image target arch 不匹配则装(默认) + # yes: 强制装(debug 用) + # no: 明确不装(host=target arch 时避免 x86→x86 翻译) + case "${2:-}" in + auto|yes|no) QEMU_INSTALL="$2" ;; + "") { echo "[错误] --qemu 需要 auto/yes/no" >&2; exit 1; } ;; + *) { echo "[错误] --qemu 必须是 auto/yes/no,得到: $2" >&2; exit 1; } ;; + esac + shift 2 + ;; + *) echo "[错误] 未知参数: $1" >&2; exit 1 ;; + esac +done + +# ============ 应用默认值 ============ +# 仅 CLI 参数与 hardcoded 默认,不读 env + +# --mode:默认空(自动判断) +MODE="${MODE_FORCE:-}" + +# --container-name(默认 ais_bench_agent) +CONTAINER_NAME="${CONTAINER_NAME_CLI:-ais_bench_agent}" + +# --runtime-image(默认 latest-${OS}-${PY}-${ARCH}) +RUNTIME_IMAGE="${RUNTIME_IMAGE_CLI:-ghcr.io/aisbench/agent-runtime:latest-ubuntu24.04-py312-${ARCH}}" + +# --host-path(仅模式 B,默认 /opt/ais_bench_agent) +HOST_PATH="${HOST_PATH_CLI:-/opt/ais_bench_agent}" + +# v3 B 批 B4: --production 默认 0(不启用),CLI 传 --production 则置 1 +# 1 → 容器加 --restart unless-stopped(物理机重启后容器自动拉起,符合生产部署期望) +# 0 → 不加(与 PR #410 原版一致,行为零变化) +PRODUCTION="${PRODUCTION_CLI:-0}" + +# ============ 工具函数 ============ +log() { echo "[$(date +%H:%M:%S)] $*"; } +fail() { echo "[错误] $*" >&2; exit 1; } + +# ============ [1/6] 探测宿主环境 ============ +log "=== [1/6] 探测宿主环境 ===" + +command -v docker >/dev/null 2>&1 || fail "宿主未安装 docker,请先安装 docker(>= 20.10)" + +DOCKER_VER=$(docker version --format '{{.Server.Version}}' 2>/dev/null | head -1) +[ -z "$DOCKER_VER" ] && fail "无法获取 docker 版本,请确认 docker daemon 已启动" +DOCKER_MAJOR=$(echo "$DOCKER_VER" | cut -d. -f1) + +CGROUP_TYPE=$(stat -fc %T /sys/fs/cgroup 2>/dev/null || echo "unknown") + +log " Docker: ${DOCKER_VER} (major=${DOCKER_MAJOR})" +log " cgroup: ${CGROUP_TYPE}" +log " arch: ${ARCH}" +if [ "${#DATASET_MOUNTS[@]}" -gt 0 ]; then + log " datasets挂载(${#DATASET_MOUNTS[@]} 个):" + for p in "${DATASET_MOUNTS[@]}"; do + log " - 宿主 ${p} → 容器内 ${p}" + done + log " 环境变量 AISBENCH_AGENT_DATASET_PATH=${DATASET_MOUNTS[0]}(容器内可读)" +else + log " datasets挂载: 无(用户未传 --datasets;配置中 path 字段需自行保证可用)" +fi +if [ -n "${RUNTIME_TAR}" ]; then + SIZE=$(du -h "${RUNTIME_TAR}" 2>/dev/null | cut -f1) + log " runtime来源: 离线 tar ${RUNTIME_TAR} (${SIZE})" +else + log " runtime来源: registry pull / OBS 回退(默认行为)" +fi +if [ "${#CASE_TAR_PATHS[@]}" -gt 0 ]; then + log " case镜像来源(${#CASE_TAR_PATHS[@]} 个,运行时加载进容器 docker daemon):" + for p in "${CASE_TAR_PATHS[@]}"; do + log " - ${p} ($(if [ -d "$p" ]; then echo dir; else echo "file $(du -h "$p" 2>/dev/null | cut -f1)"; fi))" + done +else + log " case镜像来源: 容器内手动 docker pull / load(默认行为)" +fi + +# v3 B 批新参数 logging +if [ -n "${MATRIX_YAML}" ]; then + log " matrix-yaml: ${MATRIX_YAML} → /opt/swebench/config/matrix.yaml" +fi +if [ -n "${BIND_JOBS}${BIND_TASKS}${BIND_CONFIG}" ]; then + log " Bind mount (5 层 DinD L5 bind):" + [ -n "${BIND_JOBS}" ] && log " jobs: ${BIND_JOBS} → /opt/swebench/jobs" + [ -n "${BIND_TASKS}" ] && log " tasks: ${BIND_TASKS} → /opt/swebench/tasks" + [ -n "${BIND_CONFIG}" ] && log " config: ${BIND_CONFIG} → /opt/swebench/config" +fi +if [ -n "${API_KEY_FILE}" ]; then + log " api-key-file: ${API_KEY_FILE} → /opt/swebench/api_key.env" +fi +if [ -n "${REGISTRY_MIRROR}" ]; then + log " registry-mirror: ${REGISTRY_MIRROR}" +fi +if [ -n "${DATA_IMAGE}" ]; then + log " data-image: ${DATA_IMAGE}(创建只读 data 容器 + --volumes-from :ro)" +fi +[ "${PRODUCTION}" = "1" ] && log " production: enabled(容器加 --restart unless-stopped)" +if [ -n "${L2_IMAGE}" ]; then + log " l2-image: ${L2_IMAGE}(harbor trial 容器直接用 baked image,跳过 trial 内装 agent)" +else + log " l2-image: 未设置(harbor trial 容器走默认行为,trial 内装 agent)" +fi +log " qemu: ${QEMU_INSTALL}(auto=按 host vs image arch 自动;yes=强制装;no=不装)" + +# ============ [2/6] 选择 DinD/Socket 模式 ============ +log "=== [2/6] 选择 DinD/Socket 模式 ===" + +if [ -n "${MODE:-}" ]; then + log " → 强制模式 ${MODE}(--mode)" +else + # 模式 A 推荐:docker >= 20.10 + cgroup v2 + # 模式 B 兼容:任意 docker 版本 + # 详见 docker/OVERVIEW.zh.md "运行 Agent / 沙箱类测评" + if [ "${DOCKER_MAJOR}" -ge 20 ] && [ "${CGROUP_TYPE}" = "cgroup2fs" ]; then + MODE="A" + log " → 模式 A (Docker-in-Docker,推荐,子容器隔离)" + else + MODE="B" + log " → 模式 B (Socket 代理,兼容任意 docker 版本)" + log " 原因: docker_major=${DOCKER_MAJOR} (<20) 或 cgroup=${CGROUP_TYPE} (!=cgroup2fs)" + fi +fi + +# ============ [3/6] 拉取 agent-runtime 镜像 ============ +log "=== [3/6] 拉取 agent-runtime 镜像 ===" + +if [ -n "${RUNTIME_TAR}" ]; then + # ---------- 离线模式:用户提供的 tar ---------- + log " 离线模式:跳过 docker pull / OBS 下载" + log " docker load -i ${RUNTIME_TAR}" + docker load -i "${RUNTIME_TAR}" || fail "docker load 失败: ${RUNTIME_TAR}" + # 检测 tar 加载后的镜像 tag + if docker image inspect "${RUNTIME_IMAGE}" >/dev/null 2>&1; then + log " ✓ 使用 RUNTIME_IMAGE 指定的 tag: ${RUNTIME_IMAGE}" + else + DETECTED=$(docker images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -i 'agent-runtime' | head -1 || true) + if [ -z "${DETECTED}" ]; then + fail "tar 加载完成但未发现 agent-runtime 镜像;tar 内容或 RUNTIME_IMAGE 可能不匹配" + fi + log " ✓ tar 中检测到镜像: ${DETECTED}" + RUNTIME_IMAGE="${DETECTED}" + fi +elif docker image inspect "${RUNTIME_IMAGE}" >/dev/null 2>&1; then + log " ✓ runtime 镜像已存在本地: ${RUNTIME_IMAGE}" +else + log " 尝试 docker pull ${RUNTIME_IMAGE} ..." + if docker pull "${RUNTIME_IMAGE}" 2>/dev/null; then + log " ✓ docker pull 成功" + else + log " docker pull 失败,回退 OBS tar" + TAR_URL="${OBS_RUNTIME_TAR_BASE}/agent-runtime-ubuntu24-py312-${ARCH}.tar.gz" + log " 下载: ${TAR_URL}" + curl -fsSL "${TAR_URL}" -o /tmp/agent-runtime.tar.gz || fail "OBS 下载失败: ${TAR_URL}" + log " docker load ..." + docker load -i /tmp/agent-runtime.tar.gz || fail "docker load 失败" + rm -f /tmp/agent-runtime.tar.gz + log " ✓ OBS tar 加载成功" + fi +fi + +# ============ [4/6] 启动容器 ============ +log "=== [4/6] 启动容器(模式 ${MODE})===" + +# 清理同名旧容器 +if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + log " 清理同名旧容器: ${CONTAINER_NAME}" + docker rm -f "${CONTAINER_NAME}" >/dev/null +fi + +# v3 B 批 B2: 处理 data container(仅 --data-image 传入时执行) +# data container 是只读空容器,仅用于携带 image 内置的数据卷 +# runtime 容器 --volumes-from :ro 继承这些数据卷,但运行时不能改 +DATA_VOLUMES_ARG="" +if [ -n "${DATA_IMAGE}" ]; then + DATA_CONTAINER_NAME="${CONTAINER_NAME}-data" + if docker ps -a --format '{{.Names}}' | grep -q "^${DATA_CONTAINER_NAME}$"; then + log " ✓ data container 已存在: ${DATA_CONTAINER_NAME}(复用)" + else + log " 创建 data container: ${DATA_CONTAINER_NAME} ← ${DATA_IMAGE}" + if ! docker create --name "${DATA_CONTAINER_NAME}" "${DATA_IMAGE}" >/dev/null 2>&1; then + fail "data container 创建失败(镜像不存在或已损坏): ${DATA_IMAGE}" + fi + log " ✓ ${DATA_CONTAINER_NAME} 已创建" + fi + DATA_VOLUMES_ARG="--volumes-from ${DATA_CONTAINER_NAME}:ro" +fi + +# 拼装数据集挂载参数 +MOUNT_ARGS="" +for p in "${DATASET_MOUNTS[@]+"${DATASET_MOUNTS[@]}"}"; do + MOUNT_ARGS="$MOUNT_ARGS -v ${p}:${p}" +done + +# v3 B 批 B4: --production 启用时,容器加 --restart unless-stopped +# 与 production 部署期望对齐:物理机/服务重启后 runtime 容器自动拉起 +# 默认空字符串(不加 --restart),行为与 PR #410 原版一致 +RESTART_ARG="" +if [ "${PRODUCTION}" = "1" ]; then + RESTART_ARG="--restart unless-stopped" +fi + +# 把首个 --datasets 路径持久化为容器内环境变量 +# 原生配置(如 harbor_terminal_bench_2_task.py)通过读取此变量得到数据集路径, +# 无需用户在容器内额外配置,符合"一键准备"的设计目标。 +# 多 --datasets 时仅首个生效,其余需用户自行改 DEFAULT_DATASET_PATH。 +DATASET_ENV="" +if [ "${#DATASET_MOUNTS[@]}" -gt 0 ]; then + DATASET_ENV="-e AISBENCH_AGENT_DATASET_PATH=${DATASET_MOUNTS[0]}" +fi + +# v3 B 批 B3: 拼装 5 层 DinD L5 接入所需的 bind mount + -e 注入 +# 配合 B1 新增参数:--matrix-yaml / --bind-jobs / --bind-tasks / --bind-config +# / --api-key-file / --registry-mirror +# 不传任意参数 → SWEBENCH_BINDS / SWEBENCH_ENVS 均为空 → 行为退化到 PR #410 原版 +SWEBENCH_BINDS="" +SWEBENCH_ENVS="" +if [ -n "${MATRIX_YAML:-}" ]; then + SWEBENCH_BINDS="${SWEBENCH_BINDS} -v ${MATRIX_YAML}:/opt/swebench/config/matrix.yaml:ro" +fi +if [ -n "${BIND_JOBS:-}" ]; then + SWEBENCH_BINDS="${SWEBENCH_BINDS} -v ${BIND_JOBS}:/opt/swebench/jobs" +fi +if [ -n "${BIND_TASKS:-}" ]; then + SWEBENCH_BINDS="${SWEBENCH_BINDS} -v ${BIND_TASKS}:/opt/swebench/tasks" +fi +if [ -n "${BIND_CONFIG:-}" ]; then + SWEBENCH_BINDS="${SWEBENCH_BINDS} -v ${BIND_CONFIG}:/opt/swebench/config" +fi +if [ -n "${API_KEY_FILE:-}" ]; then + SWEBENCH_BINDS="${SWEBENCH_BINDS} -v ${API_KEY_FILE}:/opt/swebench/api_key.env:ro" + # source api_key.env 后把 OPENAI_API_KEY / OPENAI_API_BASE 注入 -e + # 用 set -a 让 source 进来的变量自动 export,避免 shell 子进程丢失 + set -a + # shellcheck disable=SC1090 + . "${API_KEY_FILE}" 2>/dev/null || log " ⚠ source ${API_KEY_FILE} 失败(-e 注入可能不全)" + set +a + if [ -n "${OPENAI_API_KEY:-}" ]; then + SWEBENCH_ENVS="${SWEBENCH_ENVS} -e OPENAI_API_KEY=${OPENAI_API_KEY}" + fi + if [ -n "${OPENAI_API_BASE:-}" ]; then + SWEBENCH_ENVS="${SWEBENCH_ENVS} -e OPENAI_API_BASE=${OPENAI_API_BASE}" + fi +fi +if [ -n "${REGISTRY_MIRROR:-}" ]; then + SWEBENCH_ENVS="${SWEBENCH_ENVS} -e AIS_BENCH_AGENT_REGISTRY_MIRROR=${REGISTRY_MIRROR}" +fi +if [ -n "${L2_IMAGE:-}" ]; then + # v3 D 批 D3: 注入 baked image tag,harbor trial 容器跳过 trial 内装 agent + SWEBENCH_ENVS="${SWEBENCH_ENVS} -e AIS_BENCH_AGENT_L2_IMAGE=${L2_IMAGE}" +fi +if [ -n "${SWEBENCH_BINDS}" ] || [ -n "${SWEBENCH_ENVS}" ]; then + log " 5 层 DinD L5 接入:" + [ -n "${SWEBENCH_BINDS}" ] && log " bind mounts: 容器内 /opt/swebench/{jobs,tasks,config,api_key.env} ← host bind" + [ -n "${SWEBENCH_ENVS}" ] && log " env 注入: OPENAI_API_KEY/BASE + AIS_BENCH_AGENT_REGISTRY_MIRROR + (可选) AIS_BENCH_AGENT_L2_IMAGE" +fi + +if [ "${MODE}" = "A" ]; then + # 模式 A:Docker-in-Docker + # v3 F 批(增量):移除 --cgroupns=host + # - 原方案:--privileged + --cgroupns=host 让容器内 binfmt_misc mount 传播到 host + # - 风险:x86_64 host 上,容器内 qemu-x86_64-static 被泄漏到 host + # → host 的 ELF (如 /usr/bin/sleep) 被路由到 qemu → ELOOP + # - 新方案:依赖 daemon.json "exec-opts": ["native.cgroupdriver=cgroupfs"] + # + --net=host --ipc=host 让 dockerd 用 cgroupfs 文件直接管理 cgroup + # (不依赖 systemd,不依赖 host cgroup namespace 共享) + # - 配合 v3 A4 entrypoint.sh arch 检测:x86_64 host 不装 qemu,binfmt 无泄漏源 + # - 详见 docker/OVERVIEW.zh.md 模式 A + docker run --name "${CONTAINER_NAME}" -it -d \ + --net=host --ipc=host \ + --privileged \ + ${RESTART_ARG} \ + -w /benchmark \ + ${MOUNT_ARGS} \ + ${DATA_VOLUMES_ARG} \ + ${SWEBENCH_BINDS} \ + ${DATASET_ENV} \ + ${SWEBENCH_ENVS} \ + "${RUNTIME_IMAGE}" bash +else + # 模式 B:Socket 代理 + # 挂载宿主 docker socket,子容器由宿主 daemon 创建 + # 需要把 /benchmark 拷贝到宿主路径再挂回去(避免 docker cp 语义) + # 详见 docker/OVERVIEW.zh.md 模式 B + # HOST_PATH 已在前面 [应用默认值] 段统一计算 + log " 模式 B 准备 HOST_PATH: ${HOST_PATH}" + mkdir -p "${HOST_PATH}" + + # 从 runtime 镜像拷贝 /benchmark 内容到 HOST_PATH + log " 提取 /benchmark 到 ${HOST_PATH} ..." + docker run -d --name tmp_extract "${RUNTIME_IMAGE}" bash >/dev/null + docker cp tmp_extract:/benchmark/. "${HOST_PATH}/" || { docker rm -f tmp_extract; fail "docker cp 失败"; } + docker rm -f tmp_extract >/dev/null + + docker run --name "${CONTAINER_NAME}" -it -d \ + --net=host --privileged \ + ${RESTART_ARG} \ + -w "${HOST_PATH}" \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "${HOST_PATH}":"${HOST_PATH}" \ + ${MOUNT_ARGS} \ + ${DATA_VOLUMES_ARG} \ + ${SWEBENCH_BINDS} \ + ${DATASET_ENV} \ + ${SWEBENCH_ENVS} \ + "${RUNTIME_IMAGE}" bash +fi + +log " ✓ 容器已启动: ${CONTAINER_NAME}" + +# ============ [5/6] 容器内配置 docker ============ +log "=== [5/6] 容器内配置 docker ===" + +if [ "${MODE}" = "A" ]; then + # 模式 A:容器内启动 dockerd + # 必须写 daemon.json: + # - cgroupfs driver:DinD 在 cgroup v2 宿主上的必需配置(docker 27.x 默认 systemd,容器内无 systemd) + # - vfs 存储驱动:DinD 通用性最高;若宿主内核与容器根fs支持,overlay2 性能更好 + # 详见 docker/OVERVIEW.zh.md 模式 A 步骤二 + docker exec "${CONTAINER_NAME}" bash -c ' + set -e + # v3 B 批 B5: 强制 dockerd 选 linux/amd64 manifest + # 与 A4 entrypoint.sh 中 export 互为冗余(A4 已设,但 docker exec 新进程可能 + # 拿不到 PID 1 的 env;这里再次显式 export,noHUP dockerd 一定拿到) + # 用于 trial 容器启动时挑对 case 镜像(x86_64 case 镜像跑在 ARM64 host 上靠 qemu) + export DOCKER_DEFAULT_PLATFORM=linux/amd64 + mkdir -p /etc/docker + cat > /etc/docker/daemon.json < /tmp/dockerd.log 2>&1 & + for i in $(seq 1 30); do + [ -S /var/run/docker.sock ] && break + sleep 1 + done + if ! docker info >/dev/null 2>&1; then + echo "[错误] 容器内 dockerd 启动失败,日志:" + tail -30 /tmp/dockerd.log + exit 1 + fi + echo " ✓ 容器内 dockerd ready" + ' || fail "模式 A dockerd 启动失败" +else + # 模式 B:socket 已挂载,重链 ais_bench + # 因为 WORKDIR 改到了 HOST_PATH,需要在该路径重新以 editable 模式安装 ais_bench + # 只链接不改依赖(--no-deps) + # 详见 docker/OVERVIEW.zh.md 模式 B 步骤二 + docker exec "${CONTAINER_NAME}" bash -c ' + pip3 install -e ./ --use-pep517 --no-deps --no-build-isolation --break-system-packages >/dev/null 2>&1 \ + && echo " ✓ ais_bench relinked" \ + || echo " ⚠ ais_bench relink 失败(可忽略,若 ais_bench 命令可用即可)" + ' +fi + +# ============ [6/7] 加载 case 镜像(可选) ============ +log "=== [6/7] 加载 case 镜像(可选)===" + +if [ "${#CASE_TAR_PATHS[@]}" -gt 0 ]; then + # 把 --case-tar 展开成具体的 tar 文件列表 + log " 收集 case tar 文件..." + CASE_TAR_FILES=() + for p in "${CASE_TAR_PATHS[@]}"; do + if [ -f "$p" ]; then + # 单个文件 + case "$p" in + *.tar|*.tar.gz|*.tgz) CASE_TAR_FILES+=("$p") ;; + *) log " [跳过] 不是 docker tar: $p"; ;; + esac + elif [ -d "$p" ]; then + # 目录:递归收集所有 .tar/.tar.gz/.tgz + while IFS= read -r -d '' f; do + CASE_TAR_FILES+=("$f") + done < <(find "$p" -type f \( -name "*.tar" -o -name "*.tar.gz" -o -name "*.tgz" \) -print0 2>/dev/null) + fi + done + + if [ "${#CASE_TAR_FILES[@]}" -eq 0 ]; then + log " ⚠ --case-tar 路径下未发现 .tar/.tar.gz/.tgz 文件,跳过加载" + else + log " 共 ${#CASE_TAR_FILES[@]} 个 case tar 待加载" + + # 在容器内建暂存目录 + docker exec "${CONTAINER_NAME}" mkdir -p /tmp/case-tars + + # 用 docker cp 把每个 tar 拷进容器 + i=0 + for f in "${CASE_TAR_FILES[@]}"; do + i=$((i+1)) + BN=$(basename "$f") + SIZE=$(du -h "$f" 2>/dev/null | cut -f1) + log " [${i}/${#CASE_TAR_FILES[@]}] docker cp ${f} (${SIZE})" + docker cp "$f" "${CONTAINER_NAME}:/tmp/case-tars/${BN}" || { + log " ✗ docker cp 失败,跳过该文件" + continue + } + done + + # 容器内 docker load 每一个 + log " 容器内 docker load ..." + docker exec "${CONTAINER_NAME}" bash -c ' + cd /tmp/case-tars + loaded=0 + failed=0 + for tf in *.tar *.tar.gz *.tgz; do + [ -f "$tf" ] || continue + sz=$(du -h "$tf" | cut -f1) + echo " loading $tf ($sz) ..." + if out=$(docker load -i "$tf" 2>&1); then + loaded=$((loaded+1)) + echo " ✓ $(echo "$out" | tail -1)" + else + failed=$((failed+1)) + echo " ✗ docker load 失败: $out" + fi + done + echo " docker load 完成: ${loaded} 个成功, ${failed} 个失败" + rm -rf /tmp/case-tars + ' || log " ⚠ 容器内 docker load 阶段出错(请人工 docker exec 进容器检查)" + fi +else + log " 未传 --case-tar,跳过(容器内用户手动 docker pull / docker load)" +fi + +# ============ [6.5/7] qemu-user-static 按需安装 ============ +# 目的:host arch vs runtime image target arch 不匹配时,在 runtime 容器内装 qemu-user-static +# ARM64 host + x86_64 runtime image → 必须装 qemu-x86_64-static + binfmt 注册 +# x86_64 host + x86_64 runtime image → 不装,避免无谓 x86→x86 翻译开销 +# ARM64 host + ARM64 runtime image → 不装 +# +# 三种 QEMU_INSTALL 模式: +# auto:host arch vs image target arch 不匹配则装 +# yes :强制装(debug / 确认环境用) +# no :绝不装(用户已用其他方式确保 binfmt) +log "=== [6.5/7] qemu-user-static 按需安装 (QEMU_INSTALL=${QEMU_INSTALL}) ===" + +# 探测 host arch +HOST_ARCH=$(uname -m) +# 探测 image target arch(从 tag 解析,如 -x86_64 / -arm64 / -aarch64 后缀) +IMAGE_ARCH_HINT="" +case "${RUNTIME_IMAGE}" in + *-x86_64*|*-amd64*) IMAGE_ARCH_HINT="x86_64" ;; + *-arm64*|*-aarch64*) IMAGE_ARCH_HINT="aarch64" ;; +esac + +# arm64/aarch64 归一化 +case "${HOST_ARCH}" in + aarch64) HOST_ARCH_NORM="aarch64" ;; + x86_64|amd64) HOST_ARCH_NORM="x86_64" ;; + *) HOST_ARCH_NORM="${HOST_ARCH}" ;; +esac + +log " host arch: ${HOST_ARCH_NORM}" +log " image arch: ${IMAGE_ARCH_HINT:-(无法从 tag 解析,默认不装)}" + +# 决定是否装 +SHOULD_INSTALL_QEMU=0 +case "${QEMU_INSTALL}" in + yes) + SHOULD_INSTALL_QEMU=1 + log " qemu: 强制装 (QEMU_INSTALL=yes)" + ;; + no) + SHOULD_INSTALL_QEMU=0 + log " qemu: 明确不装 (QEMU_INSTALL=no)" + ;; + auto) + if [ -z "${IMAGE_ARCH_HINT}" ]; then + SHOULD_INSTALL_QEMU=0 + log " qemu: auto 模式下 image arch 无法解析 → 默认不装" + elif [ "${HOST_ARCH_NORM}" != "${IMAGE_ARCH_HINT}" ]; then + SHOULD_INSTALL_QEMU=1 + log " qemu: host (${HOST_ARCH_NORM}) ≠ image (${IMAGE_ARCH_HINT}) → 装" + else + SHOULD_INSTALL_QEMU=0 + log " qemu: host (${HOST_ARCH_NORM}) = image (${IMAGE_ARCH_HINT}) → 不装(避免无谓翻译)" + fi + ;; +esac + +if [ "${SHOULD_INSTALL_QEMU}" = "1" ]; then + log " → 容器内装 qemu-user-static + 注册 binfmt_misc" + docker exec "${CONTAINER_NAME}" bash -c ' + set -euo pipefail + # 检查是否已装(image build-time 可能装了,arm64 image 默认会装) + if command -v qemu-x86_64-static >/dev/null 2>&1 || command -v qemu-aarch64-static >/dev/null 2>&1; then + echo " ✓ qemu-user-static 已装" + ls -la /proc/sys/fs/binfmt_misc/ 2>/dev/null | head -5 || echo " (binfmt_misc 文件系统未挂载,可能不需要)" + exit 0 + fi + echo " → apt-get install -y qemu-user-static" + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq && \ + apt-get install -y --no-install-recommends qemu-user-static && \ + rm -rf /var/lib/apt/lists/* + echo " ✓ qemu-user-static 安装完成" + which qemu-x86_64-static 2>/dev/null || which qemu-aarch64-static 2>/dev/null || echo " ⚠ qemu binary 找不到" + ' || log " ⚠ 容器内装 qemu-user-static 失败(请人工 docker exec 进容器检查)" +else + log " → 跳过 qemu-user-static 安装" +fi + +# ============ [7/7] 自检 + 打印下一步 ============ +log "=== [7/7] 自检 ===" + +docker exec "${CONTAINER_NAME}" bash -c ' + echo " docker: $(docker --version 2>/dev/null || echo 不可用)" + echo " compose: $(docker compose version 2>/dev/null | head -1 || echo 不可用)" + echo " ais_bench: $(ais_bench --version 2>/dev/null || echo unknown)" + echo " venvs:" + for v in harbor swebench swebench_pro; do + [ -d /opt/venvs/$v ] && echo " ✓ $v" || echo " ✗ $v 缺失" + done + echo " packs:" + ls /opt/agent-resources/packs/*.yaml 2>/dev/null | while read f; do + echo " ✓ $(basename $f .yaml)" + done + echo " 用户挂载目录(容器内可见性自检):" +' + +# 自检:用户传入的挂载路径在容器内是否可见 +if [ "${#DATASET_MOUNTS[@]}" -gt 0 ]; then + for p in "${DATASET_MOUNTS[@]}"; do + if docker exec "${CONTAINER_NAME}" bash -c "[ -d '$p' ]" >/dev/null 2>&1; then + SIZE=$(docker exec "${CONTAINER_NAME}" bash -c "du -sh '$p' 2>/dev/null | cut -f1") + log " ✓ ${p} (${SIZE})" + else + log " ✗ ${p} 在容器内不可见(挂载失败)" + fi + done +fi + +echo "" +echo "============================================================" +echo "✓ Agent 测评运行环境容器已就绪(模式 ${MODE})" +echo "============================================================" +cat <: # 注册表与 tag 见各 benchmark 文档 + 离线: 从 OBS 下载 tar 后 docker load -i +EOF +fi +cat </dev/null || \ + echo "[A4 entrypoint] [warn] could not mount binfmt_misc" + fi + + # qemu-x86_64-static 由 A1 build-time 装(仅 TARGETARCH=arm64 时) + if [ ! -x /usr/bin/qemu-x86_64-static ]; then + echo "[A4 entrypoint] [warn] /usr/bin/qemu-x86_64-static not found" + echo "[A4 entrypoint] [hint] image may have been built without TARGETARCH=arm64" + elif [ -f /proc/sys/fs/binfmt_misc/qemu-x86_64 ]; then + echo "[A4 entrypoint] binfmt already registered" + else + # 用 heredoc 写(printf 在 /proc/sys/fs/binfmt_misc/register 会 I/O error) + cat > /proc/sys/fs/binfmt_misc/register << "BINFMT_EOF" +:qemu-x86_64:M::\x7f\x45\x4c\x46\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x3e\x00:\xff\xff\xff\xff\xff\xfe\xfe\xfc\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff:/usr/bin/qemu-x86_64-static:OCF +BINFMT_EOF + echo "[A4 entrypoint] binfmt registered: $(cat /proc/sys/fs/binfmt_misc/qemu-x86_64 2>/dev/null | head -1)" + fi + + # 让 dockerd 选 amd64 manifest(镜像 list 只有 amd64,arm64 不选会报错) + export DOCKER_DEFAULT_PLATFORM=linux/amd64 +fi + +# ---------- 2. Write /etc/docker/daemon.json ---------- +mkdir -p /etc/docker + +write_daemon_json() { + python3 << 'PYEOF' +import json +import os + +mirror = os.environ.get('AIS_BENCH_AGENT_REGISTRY_MIRROR', '').strip() +cfg = { + 'exec-opts': ['native.cgroupdriver=cgroupfs'], + 'storage-driver': 'vfs', +} +if mirror: + cfg['registry-mirrors'] = [u.strip() for u in mirror.split(',') if u.strip()] + +target = '/etc/docker/daemon.json' + +# 幂等:若已存在且内容一致,不重写(避免 dockerd reload) +if os.path.exists(target): + try: + with open(target) as f: + existing = json.load(f) + if existing == cfg: + print(f"[A4 entrypoint] daemon.json already up-to-date") + raise SystemExit(0) + except (json.JSONDecodeError, OSError): + pass # 文件损坏或不存在 → 重写 + +with open(target, 'w') as f: + json.dump(cfg, f, indent=2) +print(f"[A4 entrypoint] daemon.json written: {cfg}") +PYEOF +} +write_daemon_json || echo "[A4 entrypoint] [warn] daemon.json write failed (continuing)" + +# ---------- 3. ARM64: DOCKER_DEFAULT_PLATFORM 已 export ---------- +# 已在上方 export;这里仅 echo 一下方便用户 verify +if [ "${ARCH}" = "aarch64" ] || [ "${ARCH}" = "arm64" ]; then + echo "[A4 entrypoint] DOCKER_DEFAULT_PLATFORM=${DOCKER_DEFAULT_PLATFORM:-}" +fi + +# ---------- 4. Inject /opt/swebench/agent-patches ---------- +if [ -d /opt/swebench/agent-patches ]; then + HARBOR_INSTALLED="$(python3 -c " +try: + import harbor, os + print(os.path.join(os.path.dirname(harbor.__file__), 'agents', 'installed')) +except ImportError: + print('') +" 2>/dev/null)" + + if [ -n "${HARBOR_INSTALLED}" ] && [ -d "${HARBOR_INSTALLED}" ]; then + echo "[A4 entrypoint] Injecting agent patches into ${HARBOR_INSTALLED}..." + patched=0 + skipped=0 + for f in /opt/swebench/agent-patches/*.py; do + [ -f "$f" ] || continue + name="$(basename "$f")" + if [ -f "${HARBOR_INSTALLED}/${name}" ]; then + cp "$f" "${HARBOR_INSTALLED}/${name}" + echo " patched: ${name}" + patched=$((patched + 1)) + else + skipped=$((skipped + 1)) + fi + done + echo "[A4 entrypoint] agent patches done: ${patched} patched, ${skipped} skipped (no matching installed agent)" + else + echo "[A4 entrypoint] [warn] harbor installed agents dir not found: ${HARBOR_INSTALLED:-}" + fi +fi + +# ---------- 5. Banner ---------- +echo "[A4 entrypoint] ENV summary:" +echo " ARCH=${ARCH}" +echo " DOCKER_DEFAULT_PLATFORM=${DOCKER_DEFAULT_PLATFORM:-}" +echo " AIS_BENCH_AGENT_REGISTRY_MIRROR=${AIS_BENCH_AGENT_REGISTRY_MIRROR:-}" +HARBOR_VER="$(harbor --version 2>&1 | head -1 || echo '')" +echo " harbor: ${HARBOR_VER}" + +# ---------- 6. exec ---------- +echo "[A4 entrypoint] setup done; exec \$@" +exec "$@" diff --git a/docker/agent_runtime/ais_bench_agent_orchestrator_status.sh b/docker/agent_runtime/ais_bench_agent_orchestrator_status.sh new file mode 100755 index 00000000..efd6ddb4 --- /dev/null +++ b/docker/agent_runtime/ais_bench_agent_orchestrator_status.sh @@ -0,0 +1,187 @@ +#!/bin/bash +# ============================================================================ +# ais_bench_agent_orchestrator_status.sh — 5 层 DinD runtime 容器状态查询 +# ============================================================================ +# +# 设计: +# - 一键查询 runtime 容器的健康状态(5 层 DinD L5 launcher 拉的容器) +# - 输出 5 段:容器基础 / 内层 docker / bind mount 可见性 / jobs 数量 / harbor jobs +# - 既可在容器内跑(用内层 docker CLI),也可在 host 跑(用 docker exec) +# +# 用法: +# ais_bench_agent_orchestrator_status.sh # 自动检测 +# ais_bench_agent_orchestrator_status.sh --container-name X # 指定 runtime 容器 +# ais_bench_agent_orchestrator_status.sh --jobs-dir /opt/swebench/jobs +# +# 适用: +# - 5 层 DinD L3/L4/L5 健康检查 +# - bootstrap 完成后第一次验证 +# - 跑 harbor jobs start 前的快速自检 +# +# ============================================================================ +set -euo pipefail + +# ============ 默认值 ============ +JOBS_DIR="/opt/swebench/jobs" +API_KEY_FILE="/opt/swebench/api_key.env" +DOCKER_SOCK="/var/run/docker.sock" + +# ============ 参数解析 ============ +CONTAINER_NAME="" +while [[ $# -gt 0 ]]; do + case "$1" in + --container-name) CONTAINER_NAME="$2"; shift 2 ;; + --jobs-dir) JOBS_DIR="$2"; shift 2 ;; + -h|--help) + grep '^#' "$0" | sed 's/^# \?//' + exit 0 + ;; + *) echo "[错误] 未知参数: $1" >&2; exit 1 ;; + esac +done + +# ============ 工具函数 ============ +log() { echo "[$(date +%H:%M:%S)] $*"; } +section() { + echo "" + echo "─── $* ───" +} + +# ============ 自动检测调用上下文 ============ +# 检测当前 shell 是否在 runtime 容器内: +# - /var/run/docker.sock 可访问(内层 daemon 或宿主 socket) +# - /opt/swebench 路径下有 host bind mount 内容 +INSIDE_CONTAINER=0 +if [ -S "$DOCKER_SOCK" ] && [ -d /opt/swebench ]; then + # 检查 /opt/swebench 是否真是 bind mount(有内容或可访问) + # (PR #410 的 image 默认 /opt/swebench 不存在,由 A5 预创建 + B3 bind mount 内容) + if mount | grep -q "/opt/swebench" 2>/dev/null || [ -n "$(ls -A /opt/swebench 2>/dev/null)" ]; then + INSIDE_CONTAINER=1 + fi +fi + +if [ "$INSIDE_CONTAINER" = "1" ]; then + log "=== ais_bench_agent_orchestrator_status.sh(容器内视角)===" + # 容器内:直接用内层 docker CLI + DOCKER_CMD=(docker) + # jobs dir / api key 等用 bind mount 的容器内路径 + EFFECTIVE_JOBS_DIR="$JOBS_DIR" + EFFECTIVE_API_KEY="$API_KEY_FILE" +else + log "=== ais_bench_agent_orchestrator_status.sh(host 视角)===" + # host:自动找 runtime 容器(第一个名字含 ais_bench_agent 的 running 容器) + if [ -z "$CONTAINER_NAME" ]; then + CONTAINER_NAME=$(docker ps --format '{{.Names}}' 2>/dev/null \ + | grep -E 'ais_bench_agent|swebench-orchestrator' | head -1 || true) + if [ -z "$CONTAINER_NAME" ]; then + log "[错误] 没找到 runtime 容器,显式传 --container-name" + log " docker ps -a | grep ais_bench" + exit 1 + fi + log " auto-detected container: $CONTAINER_NAME" + fi + # host:docker exec 进容器跑内层检查 + DOCKER_CMD=(docker exec "$CONTAINER_NAME") + EFFECTIVE_JOBS_DIR="$JOBS_DIR" # 同路径(jobs bind mount 在两边都可见) + EFFECTIVE_API_KEY="$API_KEY_FILE" +fi + +# ============ 1. 容器基础状态 ============ +section "1. 容器基础" +if [ "$INSIDE_CONTAINER" = "1" ]; then + log " hostname: $(hostname)" + log " uptime: $(uptime -p 2>/dev/null || uptime)" + log " arch: $(uname -m)" + log " in-container: yes" +else + log " container: $CONTAINER_NAME" + log " state: $(docker inspect --format '{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo '?')" + log " uptime: $(docker inspect --format '{{.State.StartedAt}}' "$CONTAINER_NAME" 2>/dev/null || echo '?')" + log " image: $(docker inspect --format '{{.Config.Image}}' "$CONTAINER_NAME" 2>/dev/null || echo '?')" + log " restart: $(docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' "$CONTAINER_NAME" 2>/dev/null || echo '?')" +fi + +# ============ 2. 内层 docker daemon ============ +section "2. 内层 docker daemon" +if [ "$INSIDE_CONTAINER" = "1" ]; then + if command -v docker >/dev/null 2>&1; then + log " docker: $(docker --version 2>/dev/null || echo 不可用)" + if docker info >/dev/null 2>&1; then + log " server-ver: $(docker info --format '{{.ServerVersion}}' 2>/dev/null || echo '?')" + log " storage: $(docker info --format '{{.Driver}}' 2>/dev/null || echo '?')" + log " cgroup: $(docker info --format '{{.CgroupDriver}}' 2>/dev/null || echo '?')" + log " containers: $(docker ps -q 2>/dev/null | wc -l) running, $(docker ps -aq 2>/dev/null | wc -l) total" + log " images: $(docker images -q 2>/dev/null | wc -l)" + log " default-platform: ${DOCKER_DEFAULT_PLATFORM:-未设置}" + else + log " [错误] docker info 失败,daemon 可能未启动" + fi + else + log " [错误] docker 命令不可用" + fi +else + log " (host 视角无法直接查内层 daemon,用 docker exec 进入查看)" + log " docker exec $CONTAINER_NAME docker info" +fi + +# ============ 3. bind mount 可见性 ============ +section "3. bind mount 可见性" +for p in /opt/swebench/jobs /opt/swebench/tasks /opt/swebench/config /opt/swebench/logs; do + if [ -d "$p" ]; then + SIZE=$("${DOCKER_CMD[@]}" du -sh "$p" 2>/dev/null | cut -f1 || echo "?") + FILES=$("${DOCKER_CMD[@]}" find "$p" -maxdepth 2 -type f 2>/dev/null | wc -l || echo 0) + log " ✓ $p (size=$SIZE, files=$FILES)" + else + log " ✗ $p 不存在" + fi +done + +# api_key.env 检查 +if "${DOCKER_CMD[@]}" test -f "$EFFECTIVE_API_KEY" 2>/dev/null; then + log " ✓ api_key.env: $EFFECTIVE_API_KEY" + KEY_LEN=$("${DOCKER_CMD[@]}" bash -c "grep -c OPENAI_API_KEY= $EFFECTIVE_API_KEY" 2>/dev/null || echo 0) + log " OPENAI_API_KEY lines: $KEY_LEN" +else + log " ✗ api_key.env: $EFFECTIVE_API_KEY 不存在" +fi + +# ============ 4. jobs 数量 ============ +section "4. jobs 目录状态" +if "${DOCKER_CMD[@]}" test -d "$EFFECTIVE_JOBS_DIR" 2>/dev/null; then + N_JOBS=$("${DOCKER_CMD[@]}" find "$EFFECTIVE_JOBS_DIR" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | wc -l || echo 0) + N_DONE=$("${DOCKER_CMD[@]}" find "$EFFECTIVE_JOBS_DIR" -maxdepth 2 -name 'result.json' 2>/dev/null | wc -l || echo 0) + N_RUN=$("${DOCKER_CMD[@]}" find "$EFFECTIVE_JOBS_DIR" -maxdepth 2 -name 'config.json' ! -exec test -e '{}/result.json' \; 2>/dev/null | wc -l || echo 0) + log " total jobs: $N_JOBS" + log " with result.json: $N_DONE (completed)" + log " running (估): $N_RUN" + if [ "$N_JOBS" -gt 0 ]; then + log " latest jobs:" + "${DOCKER_CMD[@]}" bash -c "ls -1dt $EFFECTIVE_JOBS_DIR/*/ 2>/dev/null" | head -3 \ + | while read -r jd; do + log " - $(basename "$jd")" + done + fi +else + log " ✗ jobs-dir 不存在: $EFFECTIVE_JOBS_DIR" +fi + +# ============ 5. harbor CLI 可用性 ============ +section "5. harbor CLI" +if "${DOCKER_CMD[@]}" command -v harbor >/dev/null 2>&1; then + HARBOR_VER=$("${DOCKER_CMD[@]}" harbor --version 2>&1 | head -1 || echo "?") + log " ✓ harbor: $HARBOR_VER" + N_JOB_NAMES=$("${DOCKER_CMD[@]}" bash -c "harbor jobs list 2>/dev/null | wc -l" 2>/dev/null || echo "?") + log " harbor jobs: $N_JOB_NAMES 行(可读=正常)" +else + log " ✗ harbor CLI 不可用" + log " 提示:bootstrap 没装 harbor?(v3 A2 commit 应已装 0.20.x)" +fi + +# ============ 收尾 ============ +section "✓ status 完成" +log "如需追踪 trial:" +log " ais_bench_agent_watch.sh " +log " ais_bench_agent_summarize.sh" +log "" +log "如需启动新 trial:" +log " ais_bench_agent_run.sh [--datasets 11099,12308] [--agents oracle]" \ No newline at end of file diff --git a/docker/agent_runtime/ais_bench_agent_run.sh b/docker/agent_runtime/ais_bench_agent_run.sh new file mode 100755 index 00000000..e2505f70 --- /dev/null +++ b/docker/agent_runtime/ais_bench_agent_run.sh @@ -0,0 +1,172 @@ +#!/bin/bash +# ============================================================================ +# ais_bench_agent_run.sh — 5 层 DinD L3 入口:触发 harbor 矩阵评测 +# ============================================================================ +# +# 设计: +# - 简单包装 `harbor jobs start -c matrix.yaml`,让用户避免手写命令行 +# - 支持子集过滤(--datasets / --agents)→ 生成 tmp yaml +# - 自动 source /opt/swebench/api_key.env 注入 OPENAI_API_KEY/BASE +# (由 bootstrap.sh --api-key-file bind mount,详见 B1+B3 commit) +# - job 落 /opt/swebench/jobs//(由 bootstrap.sh --bind-jobs bind mount) +# - 容器内调用;host 上调用方式:`docker exec ais_bench_agent_run.sh ...` +# +# 适用: +# - 5 层 DinD L3 调度入口(L4 image + L5 launcher 已由 v3 A+B 批就绪) +# - 默认参数对应当前 batch A+B 的 bootstrap 默认 +# - 用户在容器内 `ais_bench_agent_run.sh` 即可(已 PATH /usr/local/bin/) +# +# 用法: +# ais_bench_agent_run.sh # 全矩阵,默认 matrix.yaml +# ais_bench_agent_run.sh --datasets 11099,12308 # 子集 +# ais_bench_agent_run.sh --agents oracle,aider # 子集 +# ais_bench_agent_run.sh --job-name my-test # 自定义 job 名 +# ais_bench_agent_run.sh --container-name my-runtime # 改 runtime 容器 +# ais_bench_agent_run.sh --matrix /path/to/other.yaml # 改 matrix 路径 +# ais_bench_agent_run.sh --dry-run # 只打印命令,不执行 +# +# 环境变量(可选,优先级低于 CLI): +# AIS_BENCH_AGENT_CONTAINER runtime 容器名(默认 ais_bench_agent) +# AIS_BENCH_AGENT_MATRIX matrix.yaml 容器内路径(默认 /opt/swebench/config/matrix.yaml) +# +# ============================================================================ +set -euo pipefail + +# ============ 默认值 ============ +CONTAINER_NAME="${AIS_BENCH_AGENT_CONTAINER:-ais_bench_agent}" +MATRIX_YAML="${AIS_BENCH_AGENT_MATRIX:-/opt/swebench/config/matrix.yaml}" +JOBS_DIR="/opt/swebench/jobs" +LOG_DIR="/opt/swebench/logs" +API_KEY_FILE="/opt/swebench/api_key.env" +FILTER_SCRIPT="/usr/local/bin/ais_bench_agent_filter_matrix.py" + +# CLI 参数 +JOB_NAME="" +DATASETS_FILTER="" +AGENTS_FILTER="" +DRY_RUN=0 + +# ============ 参数解析 ============ +while [[ $# -gt 0 ]]; do + case "$1" in + --job-name) JOB_NAME="$2"; shift 2 ;; + --datasets) DATASETS_FILTER="$2"; shift 2 ;; + --agents) AGENTS_FILTER="$2"; shift 2 ;; + --container-name) CONTAINER_NAME="$2"; shift 2 ;; + --matrix) MATRIX_YAML="$2"; shift 2 ;; + --dry-run) DRY_RUN=1; shift ;; + -h|--help) + grep '^#' "$0" | sed 's/^# \?//' + exit 0 + ;; + *) echo "[错误] 未知参数: $1" >&2; exit 1 ;; + esac +done + +JOB_NAME="${JOB_NAME:-matrix-$(date +%Y%m%d-%H%M%S)}" + +# ============ 工具函数 ============ +log() { echo "[$(date +%H:%M:%S)] $*"; } +fail() { echo "[错误] $*" >&2; exit 1; } + +# ============ 前置检查 ============ +log "=== ais_bench_agent_run.sh ===" +log " container: $CONTAINER_NAME" +log " matrix.yaml: $MATRIX_YAML" +log " jobs-dir: $JOBS_DIR" +log " job-name: $JOB_NAME" + +# 检查 matrix.yaml 存在 +if [ ! -f "$MATRIX_YAML" ]; then + fail "matrix.yaml 不存在: $MATRIX_YAML + 提示:在 bootstrap 时传 --matrix-yaml 把 matrix bind 进容器" +fi + +# 检查 harbor CLI 可用 +if ! command -v harbor >/dev/null 2>&1; then + fail "harbor 命令不可用(请确认 harbor 0.20.x 已装,详见 v3 A2 commit)" +fi + +# 检查 jobs-dir / log-dir 可写 +for d in "$JOBS_DIR" "$LOG_DIR"; do + if [ ! -d "$d" ]; then + # 尝试创建(通常 A5 已预创建) + mkdir -p "$d" 2>/dev/null || fail "目录不存在且创建失败: $d" + fi + [ -w "$d" ] || fail "目录不可写: $d(host bind mount 权限问题)" +done + +# api_key.env 可选但推荐 +if [ -f "$API_KEY_FILE" ]; then + log " api_key.env: 已挂载(将由下方 source 注入 OPENAI_API_KEY/BASE)" +else + log " ⚠ api_key.env 未挂载: $API_KEY_FILE" + log " harbor CLI 会因 OPENAI_API_KEY 未设置而失败" + log " 提示:bootstrap 时传 --api-key-file " +fi + +# ============ 准备:source api_key.env ============ +# set -a 让 source 的变量自动 export 给后续 docker exec / harbor CLI +if [ -f "$API_KEY_FILE" ]; then + log " 注入 api_key.env → OPENAI_API_KEY / OPENAI_API_BASE" + set -a + # shellcheck disable=SC1090 + . "$API_KEY_FILE" + set +a +fi + +# ============ 准备:过滤 tmp yaml ============ +CMD_YAML="$MATRIX_YAML" +CLEANUP_TMP="" +if [[ -n "$DATASETS_FILTER" || -n "$AGENTS_FILTER" ]]; then + # tmp yaml 必须写到 bind-mount 的 log 目录,harbor 才能在容器内看到 + TMP_YAML="$LOG_DIR/_tmp_filtered_$(date +%s).yaml" + CLEANUP_TMP="$TMP_YAML" + log " 生成过滤 tmp yaml..." + log " datasets: ${DATASETS_FILTER:-(全部)}" + log " agents: ${AGENTS_FILTER:-(全部)}" + python3 "$FILTER_SCRIPT" \ + --input "$MATRIX_YAML" \ + --output "$TMP_YAML" \ + ${DATASETS_FILTER:+--datasets "$DATASETS_FILTER"} \ + ${AGENTS_FILTER:+--agents "$AGENTS_FILTER"} \ + || fail "filter_matrix.py 失败" + CMD_YAML="$TMP_YAML" +fi + +trap '[[ -n "$CLEANUP_TMP" && -f "$CLEANUP_TMP" ]] && rm -f "$CLEANUP_TMP"' EXIT + +# ============ 触发 harbor jobs start ============ +HARBOR_CMD=(harbor jobs start + -c "$CMD_YAML" + --job-name "$JOB_NAME" + --jobs-dir "$JOBS_DIR" + -n 2 +) + +log "" +log "执行: ${HARBOR_CMD[*]}" +log " trials 结果将落: $JOBS_DIR/$JOB_NAME/(host bind mount 可见)" +log "" + +if [ "$DRY_RUN" = "1" ]; then + log "[dry-run] 不实际执行 harbor jobs start" + exit 0 +fi + +"${HARBOR_CMD[@]}" + +EXIT_CODE=$? + +echo "" +if [ $EXIT_CODE -eq 0 ]; then + log "✓ 提交成功" + echo "" + log "跟踪 job 状态:" + echo " harbor jobs view $JOB_NAME" + echo " ls -la $JOBS_DIR/$JOB_NAME/" + echo " ais_bench_agent_watch.sh $JOB_NAME # 阻塞等结果" + echo " ais_bench_agent_summarize.sh $JOB_NAME # 聚合 md/csv/json" +else + fail "harbor jobs start 退出码 $EXIT_CODE" +fi \ No newline at end of file diff --git a/docker/agent_runtime/ais_bench_agent_summarize.sh b/docker/agent_runtime/ais_bench_agent_summarize.sh new file mode 100755 index 00000000..cde96eb7 --- /dev/null +++ b/docker/agent_runtime/ais_bench_agent_summarize.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# ============================================================================ +# ais_bench_agent_summarize.sh — 5 层 DinD L3 聚合:汇总 jobs/*/result.json +# ============================================================================ +# +# 设计: +# - 扫 /opt/swebench/jobs//result.json,聚合 pass@1 by (bench, agent) +# - 输出 3 份: +# /opt/swebench/logs/summary-.md (人类读) +# /opt/swebench/logs/summary-.csv (Excel/pandas 处理) +# /opt/swebench/logs/summary-.json (raw data) +# - 三份文件都落在 /opt/swebench/logs/(host bind mount 可读) +# - 调用 ais_bench_agent_summarize.py 实现聚合(无需 jq) +# +# 用法: +# ais_bench_agent_summarize.sh # 汇总所有 job +# ais_bench_agent_summarize.sh m3x3-aider-11099 ... # 汇总指定 job(substring 匹配) +# ais_bench_agent_summarize.sh --latest # 仅最新一个 job +# ais_bench_agent_summarize.sh --jobs-dir /opt/swebench/jobs +# ais_bench_agent_summarize.sh --output-dir /opt/swebench/logs +# +# 适用: +# - 容器内跑,输出日志到 bind-mount 路径(host 可读) +# - host 调:`docker exec ais_bench_agent_summarize.sh ` +# +# ============================================================================ +set -euo pipefail + +# ============ 默认值 ============ +JOBS_DIR="/opt/swebench/jobs" +OUTPUT_DIR="/opt/swebench/logs" +SUMMARY_SCRIPT="/usr/local/bin/ais_bench_agent_summarize.py" +LATEST_ONLY=0 +INCLUDE_ARGS=() + +# ============ 参数解析 ============ +while [[ $# -gt 0 ]]; do + case "$1" in + --jobs-dir) JOBS_DIR="$2"; shift 2 ;; + --output-dir) OUTPUT_DIR="$2"; shift 2 ;; + --latest) LATEST_ONLY=1; shift ;; + -h|--help) + grep '^#' "$0" | sed 's/^# \?//' + exit 0 + ;; + --*) + echo "[错误] 未知参数: $1" >&2; exit 1 ;; + *) + INCLUDE_ARGS+=("$1"); shift + ;; + esac +done + +# ============ 工具函数 ============ +log() { echo "[$(date +%H:%M:%S)] $*"; } +fail() { echo "[错误] $*" >&2; exit 1; } + +# ============ 前置检查 ============ +log "=== ais_bench_agent_summarize.sh ===" +log " jobs-dir: $JOBS_DIR" +log " output-dir: $OUTPUT_DIR" +log " include: ${#INCLUDE_ARGS[@]} 个 substring filter" + +if [ ! -d "$JOBS_DIR" ]; then + fail "jobs-dir 不存在: $JOBS_DIR(提示:bootstrap --bind-jobs)" +fi +if [ ! -f "$SUMMARY_SCRIPT" ]; then + fail "summarize.py 未找到: $SUMMARY_SCRIPT(image 构建漏装?)" +fi + +mkdir -p "$OUTPUT_DIR" || fail "output-dir 创建失败: $OUTPUT_DIR" + +# 处理 --latest:只取 jobs-dir 下最新的一个 job 目录 +if [ "$LATEST_ONLY" = "1" ]; then + LATEST_JOB=$(ls -1dt "$JOBS_DIR"/*/ 2>/dev/null | head -1) + if [ -z "$LATEST_JOB" ]; then + fail "jobs-dir 下没有任何 job:$JOBS_DIR" + fi + LATEST_NAME=$(basename "$LATEST_JOB") + log " --latest:仅汇总 $LATEST_NAME" + INCLUDE_ARGS=("$LATEST_NAME") +fi + +# ============ 准备 summarize.py 命令行 ============ +PY_ARGS=(--jobs-dir "$JOBS_DIR" --output-dir "$OUTPUT_DIR") + +if [ "${#INCLUDE_ARGS[@]}" -gt 0 ]; then + PY_ARGS+=(--include "$(IFS=, ; echo "${INCLUDE_ARGS[*]}")") +fi + +# ============ 触发 ============ +log "" +log "调 summarize.py ${PY_ARGS[*]}" +log "" + +python3 "$SUMMARY_SCRIPT" "${PY_ARGS[@]}" + +EXIT_CODE=$? +echo "" +if [ $EXIT_CODE -eq 0 ]; then + log "✓ 汇总完成,产物在 $OUTPUT_DIR/" + ls -la "$OUTPUT_DIR"/summary-*.{md,csv,json} 2>/dev/null | tail -10 || true +else + fail "summarize.py 退出码 $EXIT_CODE" +fi \ No newline at end of file diff --git a/docker/agent_runtime/ais_bench_agent_watch.sh b/docker/agent_runtime/ais_bench_agent_watch.sh new file mode 100755 index 00000000..1f9c3788 --- /dev/null +++ b/docker/agent_runtime/ais_bench_agent_watch.sh @@ -0,0 +1,191 @@ +#!/bin/bash +# ============================================================================ +# ais_bench_agent_watch.sh — 5 层 DinD L3 监控:阻塞等 harbor job 完成 +# ============================================================================ +# +# 设计: +# - 阻塞等 /opt/swebench/jobs//result.json 出现 +# - 周期调 `harbor jobs view ` 打印 trial 进度 +# - results.json 出现后:读 stats 输出 pass@1 / completed / errored / finished_at +# - 完成 / 超时 / 用户 Ctrl-C 都会清退出 +# +# 用法: +# ais_bench_agent_watch.sh # 阻塞等 +# ais_bench_agent_watch.sh --interval 30 # 30s 间隔 +# ais_bench_agent_watch.sh --timeout 7200 # 2h 超时 +# ais_bench_agent_watch.sh --no-poll # 不轮询 harbor jobs view +# ais_bench_agent_watch.sh --jobs-dir /opt/swebench/jobs # 改 jobs 目录 +# +# 适用: +# - 在容器内跑(5 层 DinD L3 入口) +# - 也可以 host 上跑:`docker exec ais_bench_agent_watch.sh ` +# +# ============================================================================ +set -euo pipefail + +# ============ 默认值 ============ +JOBS_DIR="/opt/swebench/jobs" +INTERVAL=15 # 秒 +TIMEOUT=0 # 0 = 不超时 +POLL_HARBOR=1 + +# ============ 参数解析 ============ +JOB_NAME="" +while [[ $# -gt 0 ]]; do + case "$1" in + --interval) INTERVAL="$2"; shift 2 ;; + --timeout) TIMEOUT="$2"; shift 2 ;; + --jobs-dir) JOBS_DIR="$2"; shift 2 ;; + --no-poll) POLL_HARBOR=0; shift ;; + -h|--help) + grep '^#' "$0" | sed 's/^# \?//' + exit 0 + ;; + --*) + echo "[错误] 未知参数: $1" >&2; exit 1 ;; + *) + if [ -z "$JOB_NAME" ]; then + JOB_NAME="$1" + else + echo "[错误] 多余位置参数: $1" >&2; exit 1 + fi + shift + ;; + esac +done + +[ -z "$JOB_NAME" ] && { echo "[错误] 必须指定 " >&2; exit 1; } + +# ============ 工具函数 ============ +log() { echo "[$(date +%H:%M:%S)] $*"; } +fail() { echo "[错误] $*" >&2; exit 1; } + +JOB_DIR="$JOBS_DIR/$JOB_NAME" +RESULT_JSON="$JOB_DIR/result.json" + +# ============ 前置检查 ============ +log "=== ais_bench_agent_watch.sh ===" +log " job-name: $JOB_NAME" +log " jobs-dir: $JOBS_DIR" +log " job-dir: $JOB_DIR" +log " interval: ${INTERVAL}s" +log " timeout: $([ "$TIMEOUT" -eq 0 ] && echo none || echo "${TIMEOUT}s")" + +if [ ! -d "$JOBS_DIR" ]; then + fail "jobs-dir 不存在: $JOBS_DIR(提示:bootstrap --bind-jobs)" +fi + +START_TIME=$(date +%s) + +# ============ Ctrl-C 优雅退出 ============ +cleanup() { + echo "" + log " (退出 watch,job 仍在 harbor 后台跑)" + log " 下次再 watch: ais_bench_agent_watch.sh $JOB_NAME" +} +trap cleanup INT TERM + +# ============ 主循环 ============ +log "" +log "等待 results.json 出现: $RESULT_JSON" + +WAITED=0 +while true; do + # 检查 result.json + if [ -f "$RESULT_JSON" ]; then + log "✓ results.json 出现,耗时 $(( $(date +%s) - START_TIME ))s" + break + fi + + # 检查 harbor jobs view(仅在 result.json 出现前调) + if [ "$POLL_HARBOR" = "1" ] && command -v harbor >/dev/null 2>&1; then + # harbor jobs view 输出有 finished_at 字段时说明完成 + HARBOR_OUT=$(harbor jobs view "$JOB_NAME" 2>&1 || true) + if echo "$HARBOR_OUT" | grep -q "finished_at:"; then + log "harbor jobs view 报告 finished_at,但 result.json 尚未出现" + log " (可能是 harbor 在写 result.json 之前的瞬态)" + sleep 2 + if [ -f "$RESULT_JSON" ]; then + log "✓ results.json 出现,耗时 $(( $(date +%s) - START_TIME ))s" + break + fi + fi + fi + + # 检查超时 + if [ "$TIMEOUT" -gt 0 ]; then + ELAPSED=$(( $(date +%s) - START_TIME )) + if [ "$ELAPSED" -ge "$TIMEOUT" ]; then + log "⏰ 超时 ${TIMEOUT}s,results.json 仍未出现" + log " Harbor 仍可能继续跑,可加大 --timeout 或 --no-poll" + exit 2 + fi + fi + + sleep "$INTERVAL" + WAITED=$((WAITED + 1)) + if [ $((WAITED % 4)) -eq 0 ]; then + ELAPSED=$(( $(date +%s) - START_TIME )) + log " 等待中...(${ELAPSED}s,共 $WAITED 轮)" + fi +done + +# ============ 解析 results.json 输出统计 ============ +log "" +log "=== job $JOB_NAME 完成统计 ===" + +# 用 python 解析 result.json(无需 jq 依赖) +if command -v python3 >/dev/null 2>&1; then + python3 - "$RESULT_JSON" <<'PYEOF' || log " ⚠ result.json 解析失败,直接 cat 看原文" +import json +import sys + +path = sys.argv[1] +with open(path) as f: + data = json.load(f) + +stats = data.get("stats", {}) +evals = stats.get("evals", {}) + +n_total = stats.get("n_total_trials", 0) +n_completed = stats.get("n_completed_trials", 0) +n_errored = stats.get("n_errored_trials", 0) +n_running = stats.get("n_running_trials", 0) +n_pending = stats.get("n_pending_trials", 0) +finished = data.get("finished_at") +started = data.get("started_at") + +rewards = [] +for eval_key, eval_data in evals.items(): + buckets = eval_data.get("reward_stats", {}).get("reward", {}) + for r_str, trial_ids in buckets.items(): + try: + r = float(r_str) + except (ValueError, TypeError): + continue + n = len(trial_ids) if isinstance(trial_ids, list) else 1 + rewards.extend([r] * n) + +n_pass = sum(1 for r in rewards if r == 1.0) +pass_at_1 = n_pass / max(len(rewards), 1) if rewards else 0.0 + +print(f" started_at: {started or '?'}") +print(f" finished_at: {finished or '?'}") +print(f" n_total: {n_total}") +print(f" n_completed: {n_completed}") +print(f" n_errored: {n_errored}") +print(f" n_running: {n_running}") +print(f" n_pending: {n_pending}") +print(f" n_pass(=1.0): {n_pass} / {len(rewards)} trials") +print(f" pass@1: {pass_at_1:.1%}") +PYEOF +else + log " ⚠ python3 不可用,直接 cat:" + cat "$RESULT_JSON" | head -30 +fi + +log "" +log "下一步:" +echo " cat $RESULT_JSON # 完整结果" +echo " ais_bench_agent_summarize.sh $JOB_NAME # 聚合到 md/csv/json" +echo " harbor jobs view $JOB_NAME # harbor CLI 原生命令" \ No newline at end of file diff --git a/docker/agent_runtime/build_image_agent_runtime.sh b/docker/agent_runtime/build_image_agent_runtime.sh new file mode 100644 index 00000000..8dde7907 --- /dev/null +++ b/docker/agent_runtime/build_image_agent_runtime.sh @@ -0,0 +1,506 @@ +#!/bin/bash +# +# 构建 AISBench Agent Runtime 镜像 +# +# 在 aisbench_benchmark 基础镜像之上追加多 venv 隔离层, +# 用于 agent 测评(Harbor Terminal-Bench 等)的环境准备。 +# +# 用法: +# bash docker/agent_runtime/build_image_agent_runtime.sh --base-tag [选项] +# +# 必填参数: +# --base-tag 基镜像(aisbench_benchmark)的 TAG,例如 v3.1-20260522-master +# 会自动拼接为 :--- +# +# 可选参数: +# --os 操作系统,默认 ubuntu24.04 +# --py-version Python 版本,默认 py312(agent runtime 必须用 py312) +# --hub-repo 基镜像仓库地址,默认 ghcr.io/aisbench/aisbench_benchmark +# --target-hub-repo 目标镜像仓库地址(agent-runtime 镜像推到哪),默认 ghcr.io/aisbench/agent-runtime +# --image-output-dir 离线包输出目录,默认 /home/ais_bench_ci/release_images +# --obs-path OBS 工具路径,默认 /home/ais_bench_ci/obsutil_linux_arm64_5.7.9/ +# --push <0|1> 是否推送到远程仓库,默认 0 +# --upload <0|1> 是否上传离线包到 OBS,默认 0 +# --use-cache <0|1> 是否使用缓存构建,默认 0 +# --multi-arch <0|1> 是否构建多架构镜像(amd64+arm64),默认 0 +# -h, --help 显示帮助 +# +# 示例: +# # 基础构建(本地,x86_64 或 aarch64 视当前机器) +# bash docker/agent_runtime/build_image_agent_runtime.sh --base-tag v3.1-20260522-master +# +# # 指定 OS/Python 版本 +# bash docker/agent_runtime/build_image_agent_runtime.sh --base-tag v3.1-20260522-master \ +# --os ubuntu24.04 --py-version py312 +# +# # 构建并推送 +# bash docker/agent_runtime/build_image_agent_runtime.sh --base-tag v3.1-20260522-master --push 1 +# +# # 多架构构建并推送(需在各自架构机器上分别执行 + manifest 合并) +# bash docker/agent_runtime/build_image_agent_runtime.sh --base-tag v3.1-20260522-master \ +# --multi-arch 1 --push 1 +# +# # 构建、推送、并上传离线包到 OBS +# bash docker/agent_runtime/build_image_agent_runtime.sh --base-tag v3.1-20260522-master \ +# --push 1 --upload 1 + +set -e + +usage() { + echo "用法: $0 --base-tag [选项]" + echo "" + echo "必填参数:" + echo " --base-tag 基镜像(aisbench_benchmark)的 TAG,例如 v3.1-20260522-master" + echo "" + echo "可选参数:" + echo " --os 操作系统,默认: ubuntu24.04" + echo " --py-version Python 版本,默认: py312(agent runtime 推荐 py312)" + echo " --hub-repo 基镜像仓库地址,默认: ghcr.io/aisbench/aisbench_benchmark" + echo " --target-hub-repo 目标镜像仓库,默认: ghcr.io/aisbench/agent-runtime" + echo " --image-output-dir 离线包输出目录,默认: /home/ais_bench_ci/release_images" + echo " --obs-path OBS 工具路径,默认: /home/ais_bench_ci/obsutil_linux_arm64_5.7.9/" + echo " --push <0|1> 是否推送到远程仓库,默认: 0" + echo " --upload <0|1> 是否上传离线包到 OBS,默认: 0" + echo " --use-cache <0|1> 是否使用缓存构建,默认: 0" + echo " --multi-arch <0|1> 是否构建多架构镜像(amd64+arm64),默认: 0" + echo " -h, --help 显示本帮助" + echo "" + echo "示例:" + echo " $0 --base-tag v3.1-20260522-master" + echo " $0 --base-tag v3.1-20260522-master --push 1 --upload 1" + echo " $0 --base-tag v3.1-20260522-master --multi-arch 1 --push 1" + exit 1 +} + +# ============ 默认配置 ============ +BASE_TAG="" +OS="ubuntu24.04" +py_version="py312" +hub_repo="ghcr.io/aisbench/aisbench_benchmark" +target_hub_repo="ghcr.io/aisbench/agent-runtime" +image_output_dir="/home/ais_bench_ci/release_images" +obsutils_path="/home/ais_bench_ci/obsutil_linux_arm64_5.7.9/" +push=0 +upload=0 +use_cache=0 +multi_arch=0 +harbor_wheel="" + +# ============ 参数解析 ============ +while [[ $# -gt 0 ]]; do + case $1 in + --base-tag) + BASE_TAG="$2" + shift 2 + ;; + --os) + OS="$2" + shift 2 + ;; + --py-version) + py_version="$2" + shift 2 + ;; + --hub-repo) + hub_repo="$2" + shift 2 + ;; + --target-hub-repo) + target_hub_repo="$2" + shift 2 + ;; + --image-output-dir) + image_output_dir="$2" + shift 2 + ;; + --obs-path) + obsutils_path="$2" + shift 2 + ;; + --push) + push="$2" + shift 2 + ;; + --upload) + upload="$2" + shift 2 + ;; + --use-cache) + use_cache="$2" + shift 2 + ;; + --multi-arch) + multi_arch="$2" + shift 2 + ;; + --harbor-wheel) + harbor_wheel="$2" + shift 2 + ;; + -h|--help) + usage + ;; + *) + echo "错误:未知参数 $1" + usage + ;; + esac +done + +# ============ 校验必填参数 ============ +if [ -z "$BASE_TAG" ]; then + echo "错误:缺少必需参数 --base-tag" + echo "提示:--base-tag 是 aisbench_benchmark 基镜像的 TAG,例如 v3.1-20260522-master" + usage +fi + +# agent runtime 依赖 python3.12 的 venv,若用其他 py_version 给出警告 +if [ "$py_version" != "py312" ]; then + echo "警告:agent runtime 推荐 py312,当前指定 ${py_version},可能因缺 python3.12 导致 venv 创建失败" +fi + +# ============ 计算镜像名 ============ +arch=$(uname -m) + +# 基镜像全名: :--- +base_image="${hub_repo}:${BASE_TAG}-${OS}-${py_version}-${arch}" + +# 目标镜像全名: :--- +# 用 base-tag 作为 agent-runtime 的 tag,便于追溯基镜像版本 +image_name="${target_hub_repo}:${BASE_TAG}-${OS}-${py_version}-${arch}" + +# 多架构 manifest 名(不带 arch 后缀) +manifest_image_name="${target_hub_repo}:${BASE_TAG}-${OS}-${py_version}" + +# 离线包名 +offline_pkg_name="agent_runtime_image_${BASE_TAG}-${OS}-${py_version}-${arch}.tar.gz" +offline_pkg_full_path="${image_output_dir}/${offline_pkg_name}" + +# Dockerfile 路径 +dockerfile_path="$(dirname "$0")/Dockerfile.agent-runtime" + +if [ ! -f "${dockerfile_path}" ]; then + echo "错误:Dockerfile 不存在:${dockerfile_path}" + exit 1 +fi + +echo "============================================================" +echo " 构建 AISBench Agent Runtime 镜像" +echo "============================================================" +echo " 基镜像: ${base_image}" +echo " 目标镜像: ${image_name}" +echo " Dockerfile: ${dockerfile_path}" +echo " arch: ${arch}" +echo " push: ${push}" +echo " upload: ${upload}" +echo " use_cache: ${use_cache}" +echo " multi_arch: ${multi_arch}" +echo "============================================================" + +# ============ 检查基镜像是否存在(本地或远程) ============ +echo "检查基镜像 ${base_image} ..." +if ! docker image inspect "${base_image}" >/dev/null 2>&1; then + echo " 本地不存在,尝试 pull ..." + if ! docker pull "${base_image}" 2>/dev/null; then + echo "错误:基镜像拉取失败:${base_image}" + echo "提示:请确认 --base-tag / --os / --py-version / --hub-repo 参数正确" + exit 1 + fi +fi +echo " ✓ 基镜像就绪" + +# ============ 清理本地旧资源 ============ +echo "清理本地旧资源..." +if docker images -q "${image_name}" >/dev/null 2>&1; then + docker rmi -f "${image_name}" >/dev/null 2>&1 || true + echo " 已删除本地旧镜像:${image_name}" +fi +if [ -f "${offline_pkg_full_path}" ]; then + rm -f "${offline_pkg_full_path}" || true + echo " 已删除本地旧离线包:${offline_pkg_full_path}" +fi + +# ============ [v3 wrapper] harbor wheel 占位文件 ============ +# Dockerfile.agent-runtime 期望 repo root 有 .harbor_wheel_cache.whl +# 传了 --harbor-wheel → cp 真实 wheel;未传 → touch 空文件(回退到 harbor==0.20.0) +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +WHEEL_PLACEHOLDER="${REPO_ROOT}/.harbor_wheel_cache.whl" + +if [ -n "${harbor_wheel}" ]; then + if [ ! -f "${harbor_wheel}" ]; then + echo "错误:--harbor-wheel 指定的文件不存在: ${harbor_wheel}" + exit 1 + fi + echo " 使用本地 harbor wheel: ${harbor_wheel}" + cp "${harbor_wheel}" "${WHEEL_PLACEHOLDER}" +else + touch "${WHEEL_PLACEHOLDER}" +fi +# 构建后无论成功或失败都清理占位文件 +trap "rm -f ${WHEEL_PLACEHOLDER}" EXIT + +# ============ 构建镜像 ============ +BUILD_ARGS="--build-arg BASE_IMAGE=${base_image}" + +if [ "$use_cache" == "1" ]; then + echo "开始构建(使用缓存)..." + docker build \ + --network host \ + ${BUILD_ARGS} \ + -f "${dockerfile_path}" \ + -t "${image_name}" \ + "$(dirname "$0")/../../" +else + echo "开始构建(强制不使用缓存)..." + docker build \ + --no-cache \ + --network host \ + ${BUILD_ARGS} \ + -f "${dockerfile_path}" \ + -t "${image_name}" \ + "$(dirname "$0")/../../" +fi + +if [ $? -ne 0 ]; then + echo "错误:镜像构建失败" + exit 1 +fi +echo "✓ 镜像构建成功:${image_name}" + +# ============ 验证镜像 ============ +echo "开始验证镜像..." + +# 1. ais_bench 可用 +echo " [1/4] ais_bench 可用性..." +# ais_bench CLI 不支持 --version,用 --help 第一行即可 +validation_output=$(docker run --rm "${image_name}" ais_bench --help 2>&1 | head -1) || { + echo "错误:ais_bench 不可用" + echo "${validation_output}" + exit 1 +} +echo " ✓ ais_bench: ${validation_output}" + +# 2. venv 完整性 +echo " [2/4] venv 完整性..." +# 单独运行 venv 检查,每项独立 + set +e + 单独捕获 stderr, +# 让具体哪一项失败、错误信息是什么都能完整打印,方便排障 +docker run --rm "${image_name}" bash -c ' + set +e # 单个子命令失败不中止整体 + failures=() + + # 2.1 三个 venv 目录 + python 可执行 + for v in harbor swebench swebench_pro; do + if [ -d /opt/venvs/$v ] && [ -x /opt/venvs/$v/bin/python ]; then + ver=$(/opt/venvs/$v/bin/python --version 2>&1) + echo " ✓ $v venv: ${ver}" + else + echo " ✗ $v venv: 缺失或 python 不可执行" + failures+=("$v venv 缺失或 python 不可执行") + fi + done + + # 2.2 三个 venv 都需有 ais_bench wrapper + # harbor 的 wrapper 让 subprocess 启动 harbor venv 的 python; + # swebench / swebench_pro 的 wrapper 让主进程 import venv-local 的 minisweagent + for v in harbor swebench swebench_pro; do + if [ -x /opt/venvs/$v/bin/ais_bench ]; then + echo " ✓ $v-ais_bench-wrapper: OK" + else + echo " ✗ $v-ais_bench-wrapper: 缺失" + failures+=("$v-ais_bench-wrapper 缺失") + fi + done + + # 2.3 swebench / swebench_pro 各自能 import 对应 fork 的 mini-swe-agent + # PyPI 包名 mini-swe-agent(带横杠),但 Python 模块名是 minisweagent(连写无下划线) + # 单独捕获 stderr,方便看到 ModuleNotFoundError / 内部 ImportError 等真实错误 + for v in swebench swebench_pro; do + out=$(/opt/venvs/$v/bin/python -c "import minisweagent; print(minisweagent.__file__)" 2>&1) + rc=$? + if [ $rc -eq 0 ]; then + echo " ✓ $v minisweagent: ${out}" + else + echo " ✗ $v minisweagent: import 失败(python exit=$rc)" + echo " 错误信息:" + echo "$out" | sed "s/^/ /" + failures+=("$v minisweagent import 失败") + fi + done + + echo "" + if [ ${#failures[@]} -gt 0 ]; then + echo " 失败项汇总 (${#failures[@]}):" + for f in "${failures[@]}"; do + echo " - $f" + done + echo "" + echo " 排查建议:" + echo " 1. 进入镜像手动检查:" + echo " docker run --rm -it ${image_name} bash" + echo " ls -la /opt/venvs/" + echo " /opt/venvs/swebench/bin/python --version" + echo " /opt/venvs/swebench/bin/python -c \"import minisweagent\"" + echo " 2. 若 import 报 ModuleNotFoundError:Dockerfile 步骤 8/9 的 pip install 未生效;" + echo " 检查 gh-proxy.com / 华为云 pypi 镜像可达性;可加 --no-cache 强制重建步骤 8/9。" + echo " 3. 若 import 报内部 ImportError:minisweagent 自身依赖在当前 venv 内不可见;" + echo " 检查 mini-swe-agent setup.py 的 install_requires 在 venv 内是否齐全。" + exit 1 + fi +' || { + echo "错误:venv 检查失败(详见上方失败项汇总与排查建议)" + exit 1 +} +echo " ✓ harbor / swebench / swebench_pro venv 都完整" + +# 3. doctor.sh / packs 就位 +echo " [3/4] 脚本就位..." +scripts_output=$(docker run --rm "${image_name}" bash -c ' + [ -x /usr/local/bin/ais_bench_agent_doctor.sh ] && echo "doctor.sh: OK" || echo "doctor.sh: 缺失" + ls /opt/agent-resources/packs/*.yaml 2>/dev/null | while read f; do + echo "pack: $(basename $f .yaml)" + done +') || { + echo "错误:脚本检查失败" + exit 1 +} +echo "${scripts_output}" | sed 's/^/ /' + +# 4. harbor 的 docker-compose-base.yaml 已 patch +echo " [4/4] harbor compose 模板 patch 校验..." +patch_output=$(docker run --rm "${image_name}" bash -c ' + /opt/venvs/harbor/bin/python -c " +import harbor, os, yaml +p = os.path.dirname(harbor.__file__) + \"/environments/docker/docker-compose-base.yaml\" +cfg = yaml.safe_load(open(p)) +svc = cfg.get(\"services\", {}).get(\"main\", {}) +opts = svc.get(\"security_opt\", []) +print(\"seccomp=unconfined:\", \"seccomp=unconfined\" in opts) +print(\"network_mode=host:\", svc.get(\"network_mode\") == \"host\") +" 2>&1 +') || { + echo "错误:harbor compose 模板校验失败" + echo "${patch_output}" + exit 1 +} +echo "${patch_output}" | sed 's/^/ /' +if ! echo "${patch_output}" | grep -q "seccomp=unconfined: True"; then + echo "错误:harbor compose 模板未正确 patch seccomp" + exit 1 +fi +echo " ✓ harbor compose patch OK" + +echo "✓ 镜像验证通过" + +# ============ 推送镜像 ============ +if [ "$push" == "1" ]; then + echo "推送镜像到远程仓库..." + docker push "${image_name}" + if [ $? -ne 0 ]; then + echo "错误:镜像推送失败" + exit 1 + fi + echo "✓ 已推送:${image_name}" + + # 同时打 latest tag 并推送,供 bootstrap.sh 默认拉取 + latest_image_name="${target_hub_repo}:latest-${OS}-${py_version}-${arch}" + echo " 打 latest tag: ${latest_image_name}" + docker tag "${image_name}" "${latest_image_name}" + docker push "${latest_image_name}" + if [ $? -ne 0 ]; then + echo "错误:latest tag 推送失败" + exit 1 + fi + echo "✓ 已推送 latest:${latest_image_name}" +fi + +# ============ 多架构 manifest 合并 ============ +if [ "$multi_arch" == "1" ]; then + if [ "$push" != "1" ]; then + echo "提示:多架构模式下未开启推送,manifest 合并需要已推送的镜像。跳过 manifest 合并。" + else + arch_image_amd64="${target_hub_repo}:${BASE_TAG}-${OS}-${py_version}-x86_64" + arch_image_arm64="${target_hub_repo}:${BASE_TAG}-${OS}-${py_version}-aarch64" + latest_arch_image_amd64="${target_hub_repo}:latest-${OS}-${py_version}-x86_64" + latest_arch_image_arm64="${target_hub_repo}:latest-${OS}-${py_version}-aarch64" + latest_manifest_image_name="${target_hub_repo}:latest-${OS}-${py_version}" + + echo "创建多架构 manifest list:${manifest_image_name}" + echo " - ${arch_image_amd64}" + echo " - ${arch_image_arm64}" + + docker buildx imagetools create \ + -t "${manifest_image_name}" \ + "${arch_image_amd64}" \ + "${arch_image_arm64}" + + if [ $? -ne 0 ]; then + echo "错误:多架构 manifest 创建失败" + exit 1 + fi + echo "✓ 多架构 manifest list 已更新:${manifest_image_name}" + + echo "创建 latest 多架构 manifest list:${latest_manifest_image_name}" + echo " - ${latest_arch_image_amd64}" + echo " - ${latest_arch_image_arm64}" + + docker buildx imagetools create \ + -t "${latest_manifest_image_name}" \ + "${latest_arch_image_amd64}" \ + "${latest_arch_image_arm64}" + + if [ $? -ne 0 ]; then + echo "错误:latest 多架构 manifest 创建失败" + exit 1 + fi + echo "✓ latest 多架构 manifest list 已更新:${latest_manifest_image_name}" + echo " docker buildx imagetools inspect ${manifest_image_name}" + echo " docker buildx imagetools inspect ${latest_manifest_image_name}" + fi +fi + +# ============ 打包离线包并上传 OBS ============ +if [ "$upload" == "1" ]; then + echo "打包离线包..." + mkdir -p "${image_output_dir}" + docker save "${image_name}" | gzip -9 > "${offline_pkg_full_path}" + echo " 离线包已生成:${offline_pkg_full_path}" + chmod 640 "${offline_pkg_full_path}" + + echo "上传离线包到 OBS 桶..." + if [ ! -d "${obsutils_path}" ] || [ ! -x "${obsutils_path}/obsutil" ]; then + echo "错误:obsutil 路径不存在或不可执行:${obsutils_path}" + exit 1 + fi + + cd "${obsutils_path}" + ./obsutil cp "${offline_pkg_full_path}" "obs://aisbench/images/agent/runtime/${offline_pkg_name}" -f + + if [ $? -eq 0 ]; then + echo "✓ 离线包已上传:obs://aisbench/images/agent/runtime/${offline_pkg_name}" + else + echo "错误:OBS 桶上传失败" + exit 1 + fi +fi + +# ============ 完成 ============ +echo "" +echo "============================================================" +echo "✓ 构建完成" +echo "============================================================" +echo " 镜像: ${image_name}" +if [ "$push" == "1" ]; then + echo " latest: ${target_hub_repo}:latest-${OS}-${py_version}-${arch}" +fi +if [ "$multi_arch" == "1" ] && [ "$push" == "1" ]; then + echo " manifest: ${manifest_image_name}" + echo " latest manifest: ${target_hub_repo}:latest-${OS}-${py_version}" +fi +if [ "$upload" == "1" ]; then + echo " 离线包: obs://aisbench/images/agent/runtime/${offline_pkg_name}" +fi +echo "" +echo "用户使用:" +echo " curl -fsSL https://aisbench.obs.cn-north-4.myhuaweicloud.com/agent/ais_bench_agent_bootstrap.sh | bash" +echo " # 或本地测试:" +echo " bash docker/agent_runtime/bootstrap.sh --mode A" diff --git a/docker/agent_runtime/build_l2_baked_image.sh b/docker/agent_runtime/build_l2_baked_image.sh new file mode 100755 index 00000000..14d87b78 --- /dev/null +++ b/docker/agent_runtime/build_l2_baked_image.sh @@ -0,0 +1,223 @@ +#!/bin/bash +# ============================================================================ +# build_l2_baked_image.sh — 5 层 DinD L1/L2 baked image 一键构建 +# ============================================================================ +# +# 设计: +# - 把 mini_matrix 5 层 DinD L1 (case-base) + L2 (agent) baked image 构建 +# 搬到 PR #410 runtime 镜像内,容器内用户可一键 build +# - 模板:/opt/swebench/dockerfiles/Dockerfile.l{1,2}-*.j2 +# - L1:FROM 上游 prebuilt → WORKDIR /testbed + /logs +# - L2:FROM L1 → 装 agent(aider / msa / oh / qwen) +# - 与 mini_matrix scripts/build_baked_v2.sh 等价,但: +# - 默认参数对齐 batch B 的 --bind-tasks 路径(/opt/swebench/tasks) +# - 输出 image tag 命名沿用 mini_matrix 规范 +# - 默认开 ARM64 emulation (QEMU + binfmt) +# +# 用法: +# build_l2_baked_image.sh # 9 个 L2 全 build (3 case × 3 agent) +# build_l2_baked_image.sh --case 11099 # 单 case 全 agent +# build_l2_baked_image.sh --agent aider # 所有 case 同一 agent +# build_l2_baked_image.sh --l1-only # 只 build L1 (跳过 L2) +# build_l2_baked_image.sh --l2-only # 只 build L2 (假设 L1 已 build) +# build_l2_baked_image.sh --dataset-prefix django # 改 dataset 前缀 +# build_l2_baked_image.sh --registry mirror.aliyuncs.com # 改 prebuilt registry 镜像 +# build_l2_baked_image.sh --skip-arm64 # 不加 --platform linux/amd64 (host 是 x86 时用) +# +# 适用: +# - 容器内调用 (L4 镜像内建 docker,直接 docker build) +# - host 调用:bash docker/agent_runtime/build_l2_baked_image.sh ... +# - 模板路径自动检测:容器内 /opt/swebench/dockerfiles,host 用 ./docker/agent_runtime/dockerfiles/ +# +# ============================================================================ +set -euo pipefail + +# ============ 默认值 ============ +CASES=(11099 12308 13741) +AGENTS=(aider msa oh) +DATASET_PREFIX="django" +TASKS_DIR="/opt/swebench/tasks" # batch B --bind-tasks 注入 +DOCKERFILES_ROOT="/opt/swebench/dockerfiles" # batch D2 注入 +DEFAULT_REGISTRY="docker.1ms.run" +PLATFORM_FLAG="--platform linux/amd64" # ARM64 host 默认需要 +LOGDIR="/tmp/baked-builds-v3" +BUILD_L1=1 +BUILD_L2=1 + +# ============ 参数解析 ============ +while [[ $# -gt 0 ]]; do + case "$1" in + --case) shift; CASES=("$@"); break ;; + --agent) shift; AGENTS=("$@"); break ;; + --l1-only) BUILD_L2=0; shift ;; + --l2-only) BUILD_L1=0; shift ;; + --dataset-prefix) DATASET_PREFIX="$2"; shift 2 ;; + --tasks-dir) TASKS_DIR="$2"; shift 2 ;; + --registry) DEFAULT_REGISTRY="$2"; shift 2 ;; + --skip-arm64) PLATFORM_FLAG=""; shift ;; + --logdir) LOGDIR="$2"; shift 2 ;; + -h|--help) + grep '^#' "$0" | sed 's/^# \?//' + exit 0 + ;; + *) echo "[错误] 未知参数: $1" >&2; exit 1 ;; + esac +done + +# ============ 工具函数 ============ +log() { echo "[$(date +%H:%M:%S)] $*"; } +fail() { echo "[错误] $*" >&2; exit 1; } + +# ============ 自动检测 host vs 容器内 ============ +if [ -d "$DOCKERFILES_ROOT" ]; then + # 容器内视角:模板已由 D2 commit COPY 到 /opt/swebench/dockerfiles + ACTIVE_DOCKERFILES_ROOT="$DOCKERFILES_ROOT" + log "[context] 容器内视角,模板路径: $ACTIVE_DOCKERFILES_ROOT" +elif [ -d "./docker/agent_runtime/dockerfiles" ]; then + # host 视角:从仓库根目录跑 + ACTIVE_DOCKERFILES_ROOT="./docker/agent_runtime/dockerfiles" + log "[context] host 视角,模板路径: $ACTIVE_DOCKERFILES_ROOT" +else + fail "找不到 dockerfiles/ 目录(容器内应 /opt/swebench/dockerfiles,host 应 ./docker/agent_runtime/dockerfiles)" +fi + +# ============ 检查 docker 可用 ============ +command -v docker >/dev/null 2>&1 || fail "docker 命令不可用(需在 L4 runtime 镜像内或 host docker daemon 上)" +docker info >/dev/null 2>&1 || fail "docker daemon 不可达(确认 dockerd 已启动)" + +mkdir -p "$LOGDIR" + +# ============ 解析 case → 上游 prebuilt image ============ +# 命名规范(沿用 mini_matrix): +# 上游:swebench/sweb.eval..__:latest +# 例如:docker.1ms.run/swebench/sweb.eval.x86_64.django_1776_django-11099:latest +# +# 这里采用简单约定:_,实际项目里可能需要映射表 +prebuilt_for_case() { + local case_id="$1" + echo "${DEFAULT_REGISTRY}/swebench/sweb.eval.x86_64.${DATASET_PREFIX}_1776_${DATASET_PREFIX}-${case_id}:latest" +} + +# ============ 校验模板存在 ============ +[ -f "$ACTIVE_DOCKERFILES_ROOT/Dockerfile.l1-base.j2" ] \ + || fail "缺 L1 模板: $ACTIVE_DOCKERFILES_ROOT/Dockerfile.l1-base.j2" +for agent in "${AGENTS[@]}"; do + [ -f "$ACTIVE_DOCKERFILES_ROOT/Dockerfile.l2-agent-${agent}.j2" ] \ + || fail "缺 L2 模板 (agent=$agent): $ACTIVE_DOCKERFILES_ROOT/Dockerfile.l2-agent-${agent}.j2" +done + +# ============ 检查 TASKS_DIR (L1 需要 /testbed 上下文,但实际 L1 只用 prebuilt image) ============ +# L1 j2 模板不需要 host 上下文,只需 docker build -f