dsh-eval-guard 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +91 -13
  2. package/assets/bomberman/workspace.zip +0 -0
  3. package/assets/commit0/instance.json +30 -0
  4. package/assets/commit0/workspace.zip +0 -0
  5. package/assets/ctf/workspace.zip +0 -0
  6. package/assets/featurebench/hidden/test_regression.py +690 -0
  7. package/assets/featurebench/instance.json +16 -0
  8. package/assets/featurebench/workspace.zip +0 -0
  9. package/assets/swe-bench-pro/hidden/test.patch +36 -0
  10. package/assets/swe-bench-pro/instance.json +10 -0
  11. package/assets/swe-bench-pro/workspace.zip +0 -0
  12. package/assets/tank/workspace.zip +0 -0
  13. package/eval/README.md +18 -0
  14. package/eval/arena/arena/README.md +21 -0
  15. package/eval/arena/arena/bt.py +104 -0
  16. package/eval/arena/arena/clock.py +15 -0
  17. package/eval/arena/arena/core.py +265 -0
  18. package/eval/arena/arena/run.cmd +24 -0
  19. package/eval/arena/arena/run.sh +11 -0
  20. package/eval/arena/arena/schedule.py +79 -0
  21. package/eval/arena/arena/tournament.py +183 -0
  22. package/eval/arena/bomberman/bots/README.md +16 -0
  23. package/eval/arena/bomberman/bots/deepseek-v4.1-flash.cpp +447 -0
  24. package/eval/arena/bomberman/bots/doubao-seed-2.1.cpp +542 -0
  25. package/eval/arena/bomberman/bots/glm-5.3.cpp +406 -0
  26. package/eval/arena/bomberman/bots/gpt-6-astra.cpp +768 -0
  27. package/eval/arena/bomberman/bots/gpt-6-sol.cpp +444 -0
  28. package/eval/arena/bomberman/bots/kimi-k3.cpp +432 -0
  29. package/eval/arena/bomberman/bots/mimo-v2.6-pro.cpp +529 -0
  30. package/eval/arena/bomberman/bots/opus-5.5.cpp +548 -0
  31. package/eval/arena/bomberman/bots/qwen-3.8-max.cpp +836 -0
  32. package/eval/arena/bomberman/tournament/README.md +11 -0
  33. package/eval/arena/bomberman/tournament/bt.py +56 -0
  34. package/eval/arena/bomberman/tournament/evaluate.py +199 -0
  35. package/eval/arena/bomberman/tournament/run.py +79 -0
  36. package/eval/arena/bomberman/tournament/schedule-2to10.json.gz +0 -0
  37. package/eval/arena/bomberman/workspace/MANIFEST.json +13 -0
  38. package/eval/arena/bomberman/workspace/README.md +29 -0
  39. package/eval/arena/bomberman/workspace/RULES.md +100 -0
  40. package/eval/arena/bomberman/workspace/baseline.cpp +240 -0
  41. package/eval/arena/bomberman/workspace/clock.py +25 -0
  42. package/eval/arena/bomberman/workspace/engine/arena.py +198 -0
  43. package/eval/arena/bomberman/workspace/engine/build.py +35 -0
  44. package/eval/arena/bomberman/workspace/engine/engine.py +256 -0
  45. package/eval/arena/bomberman/workspace/engine/verify_replay.py +24 -0
  46. package/eval/arena/bomberman/workspace/run.cmd +28 -0
  47. package/eval/arena/bomberman/workspace/run.sh +16 -0
  48. package/eval/arena/bomberman/workspace/viewer/index.html +42 -0
  49. package/eval/arena/ctf/bots/README.md +18 -0
  50. package/eval/arena/ctf/bots/deepseek-v4.1-flash.cpp +601 -0
  51. package/eval/arena/ctf/bots/doubao-seed-2.1-turbo.cpp +320 -0
  52. package/eval/arena/ctf/bots/fable-5.1.cpp +634 -0
  53. package/eval/arena/ctf/bots/glm-5.3.cpp +689 -0
  54. package/eval/arena/ctf/bots/gpt-6-astra.cpp +657 -0
  55. package/eval/arena/ctf/bots/gpt-6.1-sol.cpp +725 -0
  56. package/eval/arena/ctf/bots/kimi-k3.cpp +667 -0
  57. package/eval/arena/ctf/bots/mimo-v2.6-pro.cpp +533 -0
  58. package/eval/arena/ctf/bots/opus-5.5.cpp +645 -0
  59. package/eval/arena/ctf/bots/qwen-3.8-max.cpp +726 -0
  60. package/eval/arena/ctf/bots/sonnet-5.5.cpp +569 -0
  61. package/eval/arena/ctf/tournament/README.md +15 -0
  62. package/eval/arena/ctf/tournament/formal-2to8.json +31 -0
  63. package/eval/arena/ctf/tournament/formal-9to15.json +31 -0
  64. package/eval/arena/ctf/workspace/README.md +41 -0
  65. package/eval/arena/ctf/workspace/RULES.md +119 -0
  66. package/eval/arena/ctf/workspace/arena.py +186 -0
  67. package/eval/arena/ctf/workspace/baseline.cpp +257 -0
  68. package/eval/arena/ctf/workspace/clock.py +15 -0
  69. package/eval/arena/ctf/workspace/core.py +265 -0
  70. package/eval/arena/ctf/workspace/engine.py +389 -0
  71. package/eval/arena/ctf/workspace/run.cmd +24 -0
  72. package/eval/arena/ctf/workspace/run.sh +11 -0
  73. package/eval/arena/ctf/workspace/viewer.html +71 -0
  74. package/eval/arena/land/bots/README.md +17 -0
  75. package/eval/arena/land/bots/deepseek-4.1f@workbuddy#1/bot.cpp +608 -0
  76. package/eval/arena/land/bots/glm-5.3@workbuddy#1/bot.cpp +521 -0
  77. package/eval/arena/land/bots/gpt-6-astra@codex#1/bot.cpp +373 -0
  78. package/eval/arena/land/bots/gpt-6-sol@codex#1/bot.cpp +341 -0
  79. package/eval/arena/land/bots/kimi-k3@workbuddy#1/bot.cpp +492 -0
  80. package/eval/arena/land/bots/mimo-2.6-pro@zcode#1/bot.cpp +652 -0
  81. package/eval/arena/land/bots/mystery-model@zcode#1/bot.cpp +466 -0
  82. package/eval/arena/land/bots/opus-5.5@claude-code#1/bot.cpp +630 -0
  83. package/eval/arena/land/bots/sonnet-5@claude-code#1/bot.cpp +619 -0
  84. package/eval/arena/land/tournament/make_replay_page.py +17 -0
  85. package/eval/arena/land/tournament/rate.py +290 -0
  86. package/eval/arena/land/tournament/round_robin.py +172 -0
  87. package/eval/arena/land/tournament/sparring.cpp +356 -0
  88. package/eval/arena/land/workspace/RULES.md +114 -0
  89. package/eval/arena/land/workspace/TASK.md +46 -0
  90. package/eval/arena/land/workspace/anchors/.build/greedy_f2e6d2dea5ca +0 -0
  91. package/eval/arena/land/workspace/anchors/greedy.cpp +194 -0
  92. package/eval/arena/land/workspace/anchors/random.cpp +80 -0
  93. package/eval/arena/land/workspace/clock.py +64 -0
  94. package/eval/arena/land/workspace/engine/build.py +33 -0
  95. package/eval/arena/land/workspace/engine/game.py +273 -0
  96. package/eval/arena/land/workspace/engine/referee.py +308 -0
  97. package/eval/arena/land/workspace/starter/.build/bot_be32b45b4699 +0 -0
  98. package/eval/arena/land/workspace/starter/bot.cpp +82 -0
  99. package/eval/arena/land/workspace/viewer/viewer.html +210 -0
  100. package/eval/arena/tank/bots/README.md +24 -0
  101. package/eval/arena/tank/bots/half1/deepseek-v4.1-flash.cpp +367 -0
  102. package/eval/arena/tank/bots/half1/doubao-seed-evolving.cpp +402 -0
  103. package/eval/arena/tank/bots/half1/gemini-4-argon.cpp +1132 -0
  104. package/eval/arena/tank/bots/half1/glm-5.3.cpp +551 -0
  105. package/eval/arena/tank/bots/half1/gpt-6-astra.cpp +233 -0
  106. package/eval/arena/tank/bots/half1/gpt-6.1-sol-ultrafast.cpp +350 -0
  107. package/eval/arena/tank/bots/half1/gpt-6.1-sol.cpp +78 -0
  108. package/eval/arena/tank/bots/half1/grok-4.7.cpp +1099 -0
  109. package/eval/arena/tank/bots/half1/haiku-5.5.cpp +210 -0
  110. package/eval/arena/tank/bots/half1/kimi-k3.cpp +353 -0
  111. package/eval/arena/tank/bots/half1/mimo-v2.6-pro.cpp +336 -0
  112. package/eval/arena/tank/bots/half1/muse-wonder-full.cpp +560 -0
  113. package/eval/arena/tank/bots/half1/opus-5.5.cpp +435 -0
  114. package/eval/arena/tank/bots/half1/qwen-3.8-max.cpp +537 -0
  115. package/eval/arena/tank/bots/half2/deepseek-v4.1-flash.cpp +425 -0
  116. package/eval/arena/tank/bots/half2/doubao-seed-evolving.cpp +308 -0
  117. package/eval/arena/tank/bots/half2/gemini-4-argon.cpp +1390 -0
  118. package/eval/arena/tank/bots/half2/glm-5.3.cpp +651 -0
  119. package/eval/arena/tank/bots/half2/gpt-6-astra.cpp +241 -0
  120. package/eval/arena/tank/bots/half2/gpt-6.1-sol-ultrafast.cpp +356 -0
  121. package/eval/arena/tank/bots/half2/gpt-6.1-sol.cpp +87 -0
  122. package/eval/arena/tank/bots/half2/grok-4.7.cpp +1205 -0
  123. package/eval/arena/tank/bots/half2/haiku-5.5.cpp +210 -0
  124. package/eval/arena/tank/bots/half2/kimi-k3.cpp +487 -0
  125. package/eval/arena/tank/bots/half2/mimo-v2.6-pro.cpp +336 -0
  126. package/eval/arena/tank/bots/half2/muse-wonder-full.cpp +560 -0
  127. package/eval/arena/tank/bots/half2/opus-5.5.cpp +476 -0
  128. package/eval/arena/tank/bots/half2/qwen-3.8-max.cpp +392 -0
  129. package/eval/arena/tank/tournament/README.md +24 -0
  130. package/eval/arena/tank/tournament/bt.py +218 -0
  131. package/eval/arena/tank/tournament/final.json +202 -0
  132. package/eval/arena/tank/tournament/halftime.json +146 -0
  133. package/eval/arena/tank/tournament/progress.json +911 -0
  134. package/eval/arena/tank/tournament/schedule.py +100 -0
  135. package/eval/arena/tank/tournament/sparring.cpp +928 -0
  136. package/eval/arena/tank/tournament/tournament.py +1426 -0
  137. package/eval/arena/tank/workspace/README.md +169 -0
  138. package/eval/arena/tank/workspace/RULES.md +443 -0
  139. package/eval/arena/tank/workspace/arena.py +862 -0
  140. package/eval/arena/tank/workspace/baseline.cpp +259 -0
  141. package/eval/arena/tank/workspace/clock.py +103 -0
  142. package/eval/arena/tank/workspace/core.py +712 -0
  143. package/eval/arena/tank/workspace/engine.py +573 -0
  144. package/eval/arena/tank/workspace/history/.lock +0 -0
  145. package/eval/arena/tank/workspace/history/20261017-193610-08715287f91f-bot.cpp +109 -0
  146. package/eval/arena/tank/workspace/history/20261017-193619-f00b2d5635fa-gpt-6-astra.cpp +241 -0
  147. package/eval/arena/tank/workspace/history/20261017-193643-63319e0a2b1a-gpt-6_1-sol-ultrafast.cpp +356 -0
  148. package/eval/arena/tank/workspace/history/20261017-193647-41c1341d0a8a-gemini-4-argon.cpp +1390 -0
  149. package/eval/arena/tank/workspace/history/20261017-193652-e34c0f45539a-gpt-6_1-sol.cpp +87 -0
  150. package/eval/arena/tank/workspace/history/20261017-193654-c026348ce96e-opus-5_5.cpp +476 -0
  151. package/eval/arena/tank/workspace/history/20261017-193655-4a21366e94d1-muse-wonder-full.cpp +560 -0
  152. package/eval/arena/tank/workspace/history/20261017-193657-dbc4e9969c52-mimo-v2_6-pro.cpp +336 -0
  153. package/eval/arena/tank/workspace/history/20261017-193703-723cd036eb31-grok-4_7.cpp +1205 -0
  154. package/eval/arena/tank/workspace/history/20261017-193707-1ebbb227be68-deepseek-v4_1-flash.cpp +425 -0
  155. package/eval/arena/tank/workspace/history/20261017-193709-aeb0e7202ad5-doubao-seed-evolving.cpp +308 -0
  156. package/eval/arena/tank/workspace/history/20261017-193710-b21ebeb9bc76-kimi-k3.cpp +487 -0
  157. package/eval/arena/tank/workspace/history/20261017-193712-54cd86531905-haiku-5_5.cpp +210 -0
  158. package/eval/arena/tank/workspace/history/20261017-193714-efdb8e35c0bf-qwen-3_8-max.cpp +392 -0
  159. package/eval/arena/tank/workspace/history/20261017-193715-681423c23fdb-glm-5_3.cpp +651 -0
  160. package/eval/arena/tank/workspace/history/20261017-193717-1a8b3c75181d-sparring.cpp +928 -0
  161. package/eval/arena/tank/workspace/history/20261017-193720-1e0803a541e8-baseline.cpp +259 -0
  162. package/eval/arena/tank/workspace/history/log.jsonl +103 -0
  163. package/eval/arena/tank/workspace/judge/cycles.hpp +53 -0
  164. package/eval/arena/tank/workspace/judge/fast.cpp +337 -0
  165. package/eval/arena/tank/workspace/judge/referee.cpp +125 -0
  166. package/eval/arena/tank/workspace/judge/speed.cpp +99 -0
  167. package/eval/arena/tank/workspace/run.cmd +31 -0
  168. package/eval/arena/tank/workspace/run.sh +23 -0
  169. package/eval/arena/tank/workspace/tank.hpp +1762 -0
  170. package/eval/arena/tank/workspace/tank_main.cpp +215 -0
  171. package/eval/arena/tank/workspace/viewer.html +1150 -0
  172. package/eval/arena/warehouse/workspace/SPEC.md +130 -0
  173. package/eval/arena/warehouse/workspace/TASK.md +71 -0
  174. package/eval/arena/warehouse/workspace/baseline/common.h +88 -0
  175. package/eval/arena/warehouse/workspace/baseline/main.cpp +27 -0
  176. package/eval/arena/warehouse/workspace/baseline/old_stuff.cpp +87 -0
  177. package/eval/arena/warehouse/workspace/baseline/sim.cpp +427 -0
  178. package/eval/arena/warehouse/workspace/baseline/util.cpp +220 -0
  179. package/eval/arena/warehouse/workspace/clock.py +42 -0
  180. package/eval/arena/warehouse/workspace/src/common.h +88 -0
  181. package/eval/arena/warehouse/workspace/src/main.cpp +27 -0
  182. package/eval/arena/warehouse/workspace/src/old_stuff.cpp +87 -0
  183. package/eval/arena/warehouse/workspace/src/sim.cpp +427 -0
  184. package/eval/arena/warehouse/workspace/src/util.cpp +220 -0
  185. package/eval/arena/warehouse/workspace/tools/bench.py +119 -0
  186. package/eval/arena/warehouse/workspace/tools/check_input.py +161 -0
  187. package/eval/arena/warehouse/workspace/tools/gen.py +132 -0
  188. package/eval/arena/warehouse/workspace/tools/measure.py +107 -0
  189. package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.in +1420 -0
  190. package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.out +9162 -0
  191. package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.in +1465 -0
  192. package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.out +3439 -0
  193. package/eval/arena/warehouse/workspace/workloads/public/pub_flood.in +2757 -0
  194. package/eval/arena/warehouse/workspace/workloads/public/pub_flood.out +3126 -0
  195. package/eval/arena/warehouse/workspace/workloads/public/pub_small.in +731 -0
  196. package/eval/arena/warehouse/workspace/workloads/public/pub_small.out +2578 -0
  197. package/eval/arena/warehouse/workspace/workloads/public/pub_wide.in +528 -0
  198. package/eval/arena/warehouse/workspace/workloads/public/pub_wide.out +2026 -0
  199. package/eval/bomberman.py +20 -0
  200. package/eval/commit0.py +21 -0
  201. package/eval/common.py +61 -0
  202. package/eval/ctf.py +20 -0
  203. package/eval/featurebench.py +25 -0
  204. package/eval/land.py +20 -0
  205. package/eval/legacy/bomberman-prices.json +4 -0
  206. package/eval/legacy/bomberman.py +161 -0
  207. package/eval/legacy/commit0.py +91 -0
  208. package/eval/legacy/ctf-prices.json +4 -0
  209. package/eval/legacy/ctf.py +178 -0
  210. package/eval/legacy/featurebench.py +103 -0
  211. package/eval/legacy/land-prices.json +4 -0
  212. package/eval/legacy/land.py +177 -0
  213. package/eval/legacy/swe_bench_pro.py +116 -0
  214. package/eval/legacy/tank-prices.json +3 -0
  215. package/eval/legacy/tank.py +408 -0
  216. package/eval/legacy/warehouse-prices.json +4 -0
  217. package/eval/legacy/warehouse.py +365 -0
  218. package/eval/swe-bench-pro.py +25 -0
  219. package/eval/tank.py +20 -0
  220. package/eval/warehouse.py +20 -0
  221. package/lib/client/index.js +376 -63
  222. package/lib/client/index.js.map +1 -1
  223. package/lib/index.js +684 -27
  224. package/package.json +2 -1
package/lib/index.js CHANGED
@@ -4,7 +4,7 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import os from "node:os";
6
6
  import { fileURLToPath } from "node:url";
7
- import { spawn } from "node:child_process";
7
+ import { execFileSync, spawn } from "node:child_process";
8
8
  //#region src/config.ts
9
9
  /**
10
10
  * Plugin configuration. The Settings page writes this document through the
@@ -18,7 +18,13 @@ const Config = z.object({
18
18
  timeLimitMinutes: z.number().default(90).volatile(),
19
19
  forbiddenPaths: z.string().default("").volatile(),
20
20
  toolBlacklistUseDefault: z.boolean().default(true).volatile(),
21
- toolBlacklist: z.string().default("").volatile()
21
+ toolBlacklist: z.string().default("").volatile(),
22
+ cloneSource: z.string().default("github").volatile(),
23
+ customCloneUrl: z.string().default("").volatile(),
24
+ pipIndex: z.string().default("pypi").volatile(),
25
+ customPipUrl: z.string().default("").volatile(),
26
+ gitProxyUrl: z.string().default("").volatile(),
27
+ pipProxyUrl: z.string().default("").volatile()
22
28
  });
23
29
  /** Clamp and normalize a live configuration object. */
24
30
  function resolveConfig(value) {
@@ -29,7 +35,13 @@ function resolveConfig(value) {
29
35
  timeLimitMinutes: Math.min(1440, Math.max(1, Math.round(minutes))),
30
36
  forbiddenPaths: typeof value?.forbiddenPaths === "string" ? value.forbiddenPaths : "",
31
37
  toolBlacklistUseDefault: value?.toolBlacklistUseDefault !== false,
32
- toolBlacklist: typeof value?.toolBlacklist === "string" ? value.toolBlacklist : ""
38
+ toolBlacklist: typeof value?.toolBlacklist === "string" ? value.toolBlacklist : "",
39
+ cloneSource: typeof value?.cloneSource === "string" && value.cloneSource !== "" ? value.cloneSource : "github",
40
+ customCloneUrl: typeof value?.customCloneUrl === "string" ? value.customCloneUrl : "",
41
+ pipIndex: typeof value?.pipIndex === "string" && value.pipIndex !== "" ? value.pipIndex : "pypi",
42
+ customPipUrl: typeof value?.customPipUrl === "string" ? value.customPipUrl : "",
43
+ gitProxyUrl: typeof value?.gitProxyUrl === "string" ? value.gitProxyUrl : "",
44
+ pipProxyUrl: typeof value?.pipProxyUrl === "string" ? value.pipProxyUrl : ""
33
45
  };
34
46
  }
35
47
  //#endregion
@@ -42,6 +54,10 @@ const PROMPT_ROUTE = "/api/eval-guard/prompt";
42
54
  const EVENTS_ROUTE = "/api/eval-guard/events";
43
55
  const REVEAL_ROUTE = "/api/eval-guard/reveal";
44
56
  const MANUAL_ROUTE = "/api/eval-guard/manual";
57
+ const JOBS_ROUTE = "/api/eval-guard/jobs";
58
+ const JOB_ROUTE = "/api/eval-guard/job";
59
+ const JOB_CANCEL_ROUTE = "/api/eval-guard/job/cancel";
60
+ const EVAL_ROUTE = "/api/eval-guard/evaluate";
45
61
  /** Supported task ids. */
46
62
  const TANK_TASK = "tank";
47
63
  const TANK_TASK_LABEL = "第 6 集 · 坦克大战";
@@ -52,6 +68,45 @@ const LAND_TASK_LABEL = "第 1 集 · 圈地";
52
68
  const BOMBERMAN_TASK = "bomberman";
53
69
  const BOMBERMAN_TASK_LABEL = "第 2 集 · 炸弹人";
54
70
  const CTF_TASK_LABEL = "第 3 集 · 夺旗";
71
+ const COMMIT0_TASK = "commit0";
72
+ const COMMIT0_TASK_LABEL = "Commit0 · 单库实现";
73
+ const SWE_BENCH_PRO_TASK = "swe-bench-pro";
74
+ const SWE_BENCH_PRO_TASK_LABEL = "SWE-Bench Pro · 单任务";
75
+ const FEATUREBENCH_TASK = "featurebench";
76
+ const FEATUREBENCH_TASK_LABEL = "FeatureBench · 单 feature";
77
+ /** Common pip indexes offered by the settings page. */
78
+ const PIP_MIRRORS = [
79
+ {
80
+ id: "pypi",
81
+ label: "PyPI",
82
+ url: "https://pypi.org/simple"
83
+ },
84
+ {
85
+ id: "tsinghua",
86
+ label: "清华 TUNA",
87
+ url: "https://pypi.tuna.tsinghua.edu.cn/simple"
88
+ },
89
+ {
90
+ id: "aliyun",
91
+ label: "阿里云",
92
+ url: "https://mirrors.aliyun.com/pypi/simple"
93
+ },
94
+ {
95
+ id: "tencent",
96
+ label: "腾讯云",
97
+ url: "https://mirrors.cloud.tencent.com/pypi/simple"
98
+ },
99
+ {
100
+ id: "bfsu",
101
+ label: "北外",
102
+ url: "https://mirrors.bfsu.edu.cn/pypi/web/simple"
103
+ },
104
+ {
105
+ id: "sjtu",
106
+ label: "上海交大",
107
+ url: "https://mirror.sjtu.edu.cn/pypi/web/simple"
108
+ }
109
+ ];
55
110
  //#endregion
56
111
  //#region src/policy.ts
57
112
  /**
@@ -755,7 +810,13 @@ var Guard = class {
755
810
  timeLimitMinutes: read("timeLimitMinutes"),
756
811
  forbiddenPaths: read("forbiddenPaths"),
757
812
  toolBlacklistUseDefault: read("toolBlacklistUseDefault"),
758
- toolBlacklist: read("toolBlacklist")
813
+ toolBlacklist: read("toolBlacklist"),
814
+ cloneSource: read("cloneSource"),
815
+ customCloneUrl: read("customCloneUrl"),
816
+ pipIndex: read("pipIndex"),
817
+ customPipUrl: read("customPipUrl"),
818
+ gitProxyUrl: read("gitProxyUrl"),
819
+ pipProxyUrl: read("pipProxyUrl")
759
820
  });
760
821
  }
761
822
  /** Apply a new configuration document. */
@@ -770,8 +831,6 @@ var Guard = class {
770
831
  const wasEnabled = this.enabled;
771
832
  if (!wasEnabled && next.enabled) this.enable();
772
833
  else if (wasEnabled && !next.enabled) this.disable();
773
- else if (this.enabled && !next.timeLimitEnabled) this.deadlines.clear();
774
- else if (this.enabled && next.timeLimitEnabled) this.rearmRunning();
775
834
  }
776
835
  /** Whether the guard is active. */
777
836
  get active() {
@@ -898,7 +957,6 @@ var Guard = class {
898
957
  };
899
958
  };
900
959
  this.enabled = true;
901
- this.rearmRunning();
902
960
  for (const agent of this.services.agents.list()) if (this.isMain(agent.session)) this.forceApproval(agent.session);
903
961
  this.record({
904
962
  kind: "notice",
@@ -919,15 +977,6 @@ var Guard = class {
919
977
  detail: "评测守护已停用。"
920
978
  });
921
979
  }
922
- /** Re-arm deadlines for running main conversations after an enable/config change. */
923
- rearmRunning() {
924
- if (!this.enabled || !this.configuration.timeLimitEnabled) return;
925
- for (const agent of this.services.agents.list()) {
926
- if (!this.isMain(agent.session)) continue;
927
- const jobs = (this.services.jobs?.list(agent.id) ?? []).filter((job) => job.status === "running" || job.status === "stopping");
928
- if (agent.status === "running" || jobs.length > 0) this.arm(agent.session.id);
929
- }
930
- }
931
980
  /** Set the deadline for one session. */
932
981
  arm(sessionId) {
933
982
  const config = this.configuration;
@@ -985,7 +1034,8 @@ var Guard = class {
985
1034
  if (event.type === "user/message") {
986
1035
  if ((event.data?.source)?.kind === "user") {
987
1036
  this.startedAt.set(session.id, Date.now());
988
- this.arm(session.id);
1037
+ if (this.configuration.timeLimitEnabled) this.arm(session.id);
1038
+ else this.deadlines.delete(session.id);
989
1039
  }
990
1040
  } else if (event.type === "assistant/message") {
991
1041
  const usage = event.data?.usage;
@@ -1318,6 +1368,204 @@ var Guard = class {
1318
1368
  }
1319
1369
  };
1320
1370
  //#endregion
1371
+ //#region src/jobs.ts
1372
+ /** Persistent background jobs for task setup and single-result evaluation. */
1373
+ function now() {
1374
+ return Date.now();
1375
+ }
1376
+ function newId() {
1377
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "").slice(0, 14) + "-" + Math.random().toString(16).slice(2, 8);
1378
+ }
1379
+ /** Jobs live under the guard root and survive settings unmounts and page reloads. */
1380
+ var JobManager = class {
1381
+ root;
1382
+ jobs = /* @__PURE__ */ new Map();
1383
+ children = /* @__PURE__ */ new Map();
1384
+ constructor() {
1385
+ this.root = path.join(guardRoot(), "jobs");
1386
+ fs.mkdirSync(this.root, { recursive: true });
1387
+ for (const name of fs.readdirSync(this.root)) {
1388
+ const file = path.join(this.root, name, "job.json");
1389
+ if (!fs.existsSync(file)) continue;
1390
+ try {
1391
+ const stored = JSON.parse(fs.readFileSync(file, "utf8"));
1392
+ if (stored.status === "running" || stored.status === "queued") {
1393
+ stored.status = "interrupted";
1394
+ stored.error = "服务重启,任务已中断";
1395
+ stored.endedAt = now();
1396
+ this.write(stored);
1397
+ }
1398
+ this.jobs.set(stored.id, stored);
1399
+ } catch {}
1400
+ }
1401
+ }
1402
+ dir(idValue) {
1403
+ return path.join(this.root, idValue);
1404
+ }
1405
+ write(job) {
1406
+ fs.mkdirSync(this.dir(job.id), { recursive: true });
1407
+ fs.writeFileSync(path.join(this.dir(job.id), "job.json"), JSON.stringify(job, void 0, 2));
1408
+ }
1409
+ /** Start one child process and stream its output to a persistent log. */
1410
+ start(options) {
1411
+ const jobId = newId();
1412
+ const directory = this.dir(jobId);
1413
+ fs.mkdirSync(directory, { recursive: true });
1414
+ const logFile = path.join(directory, "log.txt");
1415
+ fs.writeFileSync(logFile, "");
1416
+ const job = {
1417
+ id: jobId,
1418
+ kind: options.kind,
1419
+ task: options.task,
1420
+ workspaceId: options.workspaceId,
1421
+ status: "running",
1422
+ createdAt: now(),
1423
+ startedAt: now(),
1424
+ endedAt: null,
1425
+ updatedAt: now(),
1426
+ command: options.command,
1427
+ cwd: options.cwd,
1428
+ logFile,
1429
+ resultFile: path.join(directory, "result.json")
1430
+ };
1431
+ this.jobs.set(jobId, job);
1432
+ this.write(job);
1433
+ let child;
1434
+ try {
1435
+ child = spawn(options.command[0], options.command.slice(1), {
1436
+ cwd: options.cwd,
1437
+ env: {
1438
+ ...process.env,
1439
+ ...options.env ?? {},
1440
+ PYTHONUNBUFFERED: "1",
1441
+ EVAL_JOB_DIR: directory,
1442
+ EVAL_RESULT_FILE: job.resultFile
1443
+ },
1444
+ detached: true,
1445
+ stdio: [
1446
+ "ignore",
1447
+ "pipe",
1448
+ "pipe"
1449
+ ]
1450
+ });
1451
+ } catch (error) {
1452
+ job.status = "failed";
1453
+ job.error = error instanceof Error ? error.message : String(error);
1454
+ job.endedAt = now();
1455
+ job.updatedAt = now();
1456
+ this.write(job);
1457
+ return this.info(job);
1458
+ }
1459
+ this.children.set(jobId, child);
1460
+ const append = (chunk) => {
1461
+ try {
1462
+ fs.appendFileSync(logFile, chunk);
1463
+ } catch {}
1464
+ };
1465
+ child.stdout?.on("data", (chunk) => append(chunk));
1466
+ child.stderr?.on("data", (chunk) => append(chunk));
1467
+ child.on("error", (error) => {
1468
+ job.status = "failed";
1469
+ job.error = error.message;
1470
+ job.endedAt = now();
1471
+ job.updatedAt = now();
1472
+ this.write(job);
1473
+ });
1474
+ child.on("exit", (code, signal) => {
1475
+ this.children.delete(jobId);
1476
+ if (job.status === "cancelled") return;
1477
+ if (code === 0) try {
1478
+ const extra = options.onSuccess?.(this.detail(jobId));
1479
+ if (extra !== void 0) {
1480
+ if (extra.prompt !== void 0) job.prompt = extra.prompt;
1481
+ if (extra.runId !== void 0) job.runId = extra.runId;
1482
+ if (extra.result !== void 0) {
1483
+ job.result = extra.result;
1484
+ fs.writeFileSync(job.resultFile, JSON.stringify(extra.result, void 0, 2));
1485
+ }
1486
+ }
1487
+ if (fs.existsSync(job.resultFile)) try {
1488
+ job.result = JSON.parse(fs.readFileSync(job.resultFile, "utf8"));
1489
+ } catch {}
1490
+ job.status = "succeeded";
1491
+ } catch (error) {
1492
+ job.status = "failed";
1493
+ job.error = error instanceof Error ? error.message : String(error);
1494
+ }
1495
+ else {
1496
+ job.status = "failed";
1497
+ job.error = signal !== null ? "进程被信号 " + signal + " 终止" : "退出码 " + String(code);
1498
+ }
1499
+ job.endedAt = now();
1500
+ job.updatedAt = now();
1501
+ this.write(job);
1502
+ });
1503
+ return this.info(job);
1504
+ }
1505
+ /** Cancel a running job and its process group. */
1506
+ cancel(jobId) {
1507
+ const job = this.jobs.get(jobId);
1508
+ if (job === void 0) return void 0;
1509
+ if (job.status !== "running" && job.status !== "queued") return this.info(job);
1510
+ job.status = "cancelled";
1511
+ job.endedAt = now();
1512
+ job.updatedAt = now();
1513
+ job.error = "用户取消";
1514
+ this.write(job);
1515
+ const child = this.children.get(jobId);
1516
+ if (child?.pid !== void 0) try {
1517
+ process.kill(-child.pid, "SIGTERM");
1518
+ } catch {
1519
+ try {
1520
+ child.kill("SIGTERM");
1521
+ } catch {}
1522
+ }
1523
+ return this.info(job);
1524
+ }
1525
+ /** Current state, including a bounded log tail and optional result. */
1526
+ get(jobId) {
1527
+ if (!this.jobs.has(jobId)) return void 0;
1528
+ return this.detail(jobId);
1529
+ }
1530
+ /** Recent jobs, optionally filtered by workspace and task. */
1531
+ list(workspaceId, task) {
1532
+ return [...this.jobs.values()].filter((job) => workspaceId === void 0 || job.workspaceId === workspaceId).filter((job) => task === void 0 || job.task === task).sort((a, b) => b.createdAt - a.createdAt).slice(0, 20).map((job) => this.info(job));
1533
+ }
1534
+ info(job) {
1535
+ return {
1536
+ id: job.id,
1537
+ kind: job.kind,
1538
+ task: job.task,
1539
+ workspaceId: job.workspaceId,
1540
+ status: job.status,
1541
+ createdAt: job.createdAt,
1542
+ startedAt: job.startedAt,
1543
+ endedAt: job.endedAt,
1544
+ updatedAt: job.updatedAt,
1545
+ error: job.error
1546
+ };
1547
+ }
1548
+ detail(jobId) {
1549
+ const job = this.jobs.get(jobId);
1550
+ let log = "";
1551
+ try {
1552
+ log = fs.readFileSync(job.logFile, "utf8").slice(-16e3);
1553
+ } catch {}
1554
+ let result;
1555
+ if (job.result !== void 0) result = job.result;
1556
+ else if (fs.existsSync(job.resultFile)) try {
1557
+ result = JSON.parse(fs.readFileSync(job.resultFile, "utf8"));
1558
+ } catch {}
1559
+ return {
1560
+ ...this.info(job),
1561
+ log,
1562
+ prompt: job.prompt,
1563
+ runId: job.runId,
1564
+ result
1565
+ };
1566
+ }
1567
+ };
1568
+ //#endregion
1321
1569
  //#region src/task.ts
1322
1570
  /**
1323
1571
  * Task packs: packaged workspace.zip install, original prompt construction, and
@@ -1329,27 +1577,100 @@ const TASKS = [
1329
1577
  {
1330
1578
  id: TANK_TASK,
1331
1579
  label: TANK_TASK_LABEL,
1332
- description: "1v1 坦克大战 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。"
1580
+ description: "1v1 坦克大战 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。",
1581
+ timeLimitMinutes: 90,
1582
+ timeLimitEnabled: true,
1583
+ needsSetup: false
1333
1584
  },
1334
1585
  {
1335
1586
  id: WAREHOUSE_TASK,
1336
1587
  label: WAREHOUSE_TASK_LABEL,
1337
- description: "单人单次仓储调度优化;解压 workspace.zip,提交 workspace/src/。"
1588
+ description: "单人单次仓储调度优化;解压 workspace.zip,提交 workspace/src/。",
1589
+ timeLimitMinutes: 90,
1590
+ timeLimitEnabled: true,
1591
+ needsSetup: false
1338
1592
  },
1339
1593
  {
1340
1594
  id: LAND_TASK,
1341
1595
  label: LAND_TASK_LABEL,
1342
- description: "圈地 1v1/4 人混战 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。"
1596
+ description: "圈地 1v1/4 人混战 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。",
1597
+ timeLimitMinutes: 90,
1598
+ timeLimitEnabled: true,
1599
+ needsSetup: false
1343
1600
  },
1344
1601
  {
1345
1602
  id: BOMBERMAN_TASK,
1346
1603
  label: BOMBERMAN_TASK_LABEL,
1347
- description: "炸弹人 2–10 人 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。"
1604
+ description: "炸弹人 2–10 人 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。",
1605
+ timeLimitMinutes: 90,
1606
+ timeLimitEnabled: true,
1607
+ needsSetup: false
1348
1608
  },
1349
1609
  {
1350
1610
  id: "ctf",
1351
1611
  label: CTF_TASK_LABEL,
1352
- description: "夺旗 2–15 人 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。"
1612
+ description: "夺旗 2–15 人 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。",
1613
+ timeLimitMinutes: 90,
1614
+ timeLimitEnabled: true,
1615
+ needsSetup: false
1616
+ },
1617
+ {
1618
+ id: COMMIT0_TASK,
1619
+ label: COMMIT0_TASK_LABEL,
1620
+ description: "Commit0 单库实现:按规格从零实现一个 Python 库,单元测试评分。",
1621
+ timeLimitMinutes: 480,
1622
+ timeLimitEnabled: false,
1623
+ needsSetup: true,
1624
+ repoUrl: "https://github.com/msiemens/tinydb",
1625
+ remotes: [{
1626
+ id: "github",
1627
+ label: "GitHub",
1628
+ url: "https://github.com/msiemens/tinydb.git"
1629
+ }]
1630
+ },
1631
+ {
1632
+ id: SWE_BENCH_PRO_TASK,
1633
+ label: SWE_BENCH_PRO_TASK_LABEL,
1634
+ description: "SWE-Bench Pro 单任务:在真实仓库中修复一个多文件 issue,隐藏测试评分。",
1635
+ timeLimitMinutes: 1440,
1636
+ timeLimitEnabled: false,
1637
+ needsSetup: true,
1638
+ repoUrl: "https://github.com/ansible/ansible",
1639
+ remotes: [{
1640
+ id: "github",
1641
+ label: "GitHub",
1642
+ url: "https://github.com/ansible/ansible.git"
1643
+ }, {
1644
+ id: "gitee",
1645
+ label: "Gitee 镜像",
1646
+ url: "https://gitee.com/mirrors/ansible.git"
1647
+ }]
1648
+ },
1649
+ {
1650
+ id: FEATUREBENCH_TASK,
1651
+ label: FEATUREBENCH_TASK_LABEL,
1652
+ description: "FeatureBench 单 feature:实现一个跨文件功能,隐藏测试评分。",
1653
+ timeLimitMinutes: 1440,
1654
+ timeLimitEnabled: false,
1655
+ needsSetup: true,
1656
+ repoUrl: "https://github.com/mwaskom/seaborn",
1657
+ remotes: [
1658
+ {
1659
+ id: "github",
1660
+ label: "GitHub",
1661
+ url: "https://github.com/mwaskom/seaborn.git"
1662
+ },
1663
+ {
1664
+ id: "gitee",
1665
+ label: "Gitee 镜像",
1666
+ url: "https://gitee.com/mirrors/seaborn.git"
1667
+ },
1668
+ {
1669
+ id: "gitcode",
1670
+ label: "GitCode 镜像",
1671
+ url: "https://gitcode.com/gh_mirrors/se/seaborn.git"
1672
+ }
1673
+ ]
1353
1674
  }
1354
1675
  ];
1355
1676
  /** The tank task definition (original prompt; only halftime mechanics removed). */
@@ -1358,6 +1679,9 @@ const TANK_DEFINITION = {
1358
1679
  label: TANK_TASK_LABEL,
1359
1680
  description: TASKS[0].description,
1360
1681
  asset: path.join(packageRoot(), "assets", "tank", "workspace.zip"),
1682
+ timeLimitMinutes: 90,
1683
+ timeLimitEnabled: true,
1684
+ needsSetup: false,
1361
1685
  prompt() {
1362
1686
  return [
1363
1687
  "按照要求编写尽可能强的坦克大战 bot。你的对手是其他顶尖 AI 模型写的 bot,它们可能主动设计战术。基准是一个中等强度、思路常规的 bot,它在正式比赛中是 0 分线;战胜基准不代表足够强,正式对手远强于基准。正式赛是 1 对 1 循环赛,用 Bradley-Terry 模型拟合实力值。",
@@ -1384,6 +1708,9 @@ const WAREHOUSE_DEFINITION = {
1384
1708
  label: WAREHOUSE_TASK_LABEL,
1385
1709
  description: TASKS[1].description,
1386
1710
  asset: path.join(packageRoot(), "assets", "warehouse", "workspace.zip"),
1711
+ timeLimitMinutes: 90,
1712
+ timeLimitEnabled: true,
1713
+ needsSetup: false,
1387
1714
  prompt() {
1388
1715
  return [
1389
1716
  "这是一场 AI 编程赛。你接手了一个代码混乱的老系统:src/ 里是一个仓储机器人调度仿真系统,功能正确,但运行很慢。你的任务:在输出完全不变的前提下,让它尽可能快。",
@@ -1407,6 +1734,9 @@ const LAND_DEFINITION = {
1407
1734
  label: LAND_TASK_LABEL,
1408
1735
  description: TASKS[2].description,
1409
1736
  asset: path.join(packageRoot(), "assets", "land", "workspace.zip"),
1737
+ timeLimitMinutes: 90,
1738
+ timeLimitEnabled: true,
1739
+ needsSetup: false,
1410
1740
  prompt() {
1411
1741
  return [
1412
1742
  "这是一场正式的 AI 编程对抗赛,你的对手是其他顶尖 AI 模型写的 bot,它们会主动切断你、抢你的地。比赛结果会公开排名,请拿出你的最高水平。",
@@ -1421,6 +1751,9 @@ const BOMBERMAN_DEFINITION = {
1421
1751
  label: BOMBERMAN_TASK_LABEL,
1422
1752
  description: TASKS[3].description,
1423
1753
  asset: path.join(packageRoot(), "assets", "bomberman", "workspace.zip"),
1754
+ timeLimitMinutes: 90,
1755
+ timeLimitEnabled: true,
1756
+ needsSetup: false,
1424
1757
  prompt() {
1425
1758
  return ["按照要求编写尽可能强bot,你的对手是全球其他顶尖ai,可能主动设计战术。基准只是很弱的bot,它在正式比赛为0分线。正式赛会对战多轮,用Bradley-Terry模型拟合实力值。", "解压 workspace.zip,阅读 README.md 和 RULES.md,编写支持2–10人的炸弹人 bot。尽量60分钟完成,90分钟必须交卷。允许本地工具、对战和复盘,禁止联网、调用其他AI或查看其他选手资料。提交 submission/bot.cpp,简述策略。"].join("\n");
1426
1759
  }
@@ -1431,25 +1764,173 @@ const CTF_DEFINITION = {
1431
1764
  label: CTF_TASK_LABEL,
1432
1765
  description: TASKS[4].description,
1433
1766
  asset: path.join(packageRoot(), "assets", "ctf", "workspace.zip"),
1767
+ timeLimitMinutes: 90,
1768
+ timeLimitEnabled: true,
1769
+ needsSetup: false,
1434
1770
  prompt() {
1435
1771
  return ["按照要求编写尽可能强的 bot,你的对手是全球其他顶尖 AI,可能主动设计战术。基准是一个中等强度、思路常规的 bot,它在正式比赛是 0 分线;战胜基准不代表足够强,正式对手远强于基准。正式赛会对战多轮,用 Bradley-Terry 模型拟合实力值。", "解压 workspace.zip,阅读 README.md 和 RULES.md,编写支持 2–15 方的夺旗 bot。你有 60 分钟,只看最终 bot 的强度,提前交卷不加分,请充分利用时间持续改进;可以适当超时,但到 90 分钟会强制交卷。允许本地工具、对战和复盘,禁止联网、调用其他 AI 或查看其他选手资料。提交 workspace/submission/bot.cpp,简述策略。"].join("\n");
1436
1772
  }
1437
1773
  };
1774
+ /** Run the task's host-side setup script with network access. */
1775
+ function runSetup(workspacePath) {
1776
+ execFileSync("bash", ["setup.sh"], {
1777
+ cwd: workspacePath,
1778
+ stdio: "inherit",
1779
+ env: {
1780
+ ...process.env,
1781
+ GIT_TERMINAL_PROMPT: "0"
1782
+ }
1783
+ });
1784
+ }
1785
+ /** Commit0 single-library task. */
1786
+ const COMMIT0_DEFINITION = {
1787
+ id: COMMIT0_TASK,
1788
+ label: COMMIT0_TASK_LABEL,
1789
+ description: TASKS[5].description,
1790
+ asset: path.join(packageRoot(), "assets", "commit0", "workspace.zip"),
1791
+ prepare: runSetup,
1792
+ timeLimitMinutes: 480,
1793
+ timeLimitEnabled: false,
1794
+ needsSetup: true,
1795
+ repoUrl: "https://github.com/msiemens/tinydb",
1796
+ remotes: [{
1797
+ id: "github",
1798
+ label: "GitHub",
1799
+ url: "https://github.com/msiemens/tinydb.git"
1800
+ }],
1801
+ prompt() {
1802
+ return [
1803
+ "这是 Commit0 的一个单库实现任务:从零实现一个 Python 库,使其通过仓库自带的完整单元测试。",
1804
+ "",
1805
+ "当前 workspace 已经由安装器准备好:包含该库的测试、规格文档和项目配置,但库的实现源码已被移除。请阅读 TASK.md 和规格文档,自己实现库,直到单元测试全部通过。",
1806
+ "",
1807
+ "要求:",
1808
+ "- 只能修改当前工作区内的文件;工作区外不可访问,包括 /tmp;",
1809
+ "- 禁止联网、禁止调用其他 AI 或子代理;",
1810
+ "- 使用工作区里的 .venv 运行测试,例如 .venv/bin/python -m pytest tests;",
1811
+ "- 不要删除、修改或伪造测试;评分以隐藏的测试执行为准;",
1812
+ "- 完成后用几句话总结实现思路。"
1813
+ ].join("\n");
1814
+ }
1815
+ };
1816
+ /** SWE-Bench Pro single-task task. */
1817
+ const SWE_BENCH_PRO_DEFINITION = {
1818
+ id: SWE_BENCH_PRO_TASK,
1819
+ label: SWE_BENCH_PRO_TASK_LABEL,
1820
+ description: TASKS[6].description,
1821
+ asset: path.join(packageRoot(), "assets", "swe-bench-pro", "workspace.zip"),
1822
+ prepare: runSetup,
1823
+ timeLimitMinutes: 1440,
1824
+ timeLimitEnabled: false,
1825
+ needsSetup: true,
1826
+ repoUrl: "https://github.com/ansible/ansible",
1827
+ remotes: [{
1828
+ id: "github",
1829
+ label: "GitHub",
1830
+ url: "https://github.com/ansible/ansible.git"
1831
+ }, {
1832
+ id: "gitee",
1833
+ label: "Gitee 镜像",
1834
+ url: "https://gitee.com/mirrors/ansible.git"
1835
+ }],
1836
+ prompt() {
1837
+ return [
1838
+ "这是 SWE-Bench Pro 的一个单任务:在真实的 Python 仓库中完成一个多文件 issue 修复。",
1839
+ "",
1840
+ "当前 workspace 已经由安装器准备好:包含目标仓库在指定 commit 的源码和依赖环境。请阅读 TASK.md 中的 issue 描述与要求,修改源码完成修复。",
1841
+ "",
1842
+ "要求:",
1843
+ "- 只能修改当前工作区内的文件;工作区外不可访问,包括 /tmp;",
1844
+ "- 禁止联网、禁止调用其他 AI 或子代理;",
1845
+ "- 使用工作区里的 .venv 运行测试,例如 .venv/bin/python -m pytest <测试文件>;",
1846
+ "- 不要删除、修改或伪造测试;评分会在你的代码上应用隐藏测试;",
1847
+ "- 完成后用几句话总结修改。"
1848
+ ].join("\n");
1849
+ }
1850
+ };
1851
+ /** FeatureBench single-feature task. */
1852
+ const FEATUREBENCH_DEFINITION = {
1853
+ id: FEATUREBENCH_TASK,
1854
+ label: FEATUREBENCH_TASK_LABEL,
1855
+ description: TASKS[7].description,
1856
+ asset: path.join(packageRoot(), "assets", "featurebench", "workspace.zip"),
1857
+ prepare: runSetup,
1858
+ timeLimitMinutes: 1440,
1859
+ timeLimitEnabled: false,
1860
+ needsSetup: true,
1861
+ repoUrl: "https://github.com/mwaskom/seaborn",
1862
+ remotes: [
1863
+ {
1864
+ id: "github",
1865
+ label: "GitHub",
1866
+ url: "https://github.com/mwaskom/seaborn.git"
1867
+ },
1868
+ {
1869
+ id: "gitee",
1870
+ label: "Gitee 镜像",
1871
+ url: "https://gitee.com/mirrors/seaborn.git"
1872
+ },
1873
+ {
1874
+ id: "gitcode",
1875
+ label: "GitCode 镜像",
1876
+ url: "https://gitcode.com/gh_mirrors/se/seaborn.git"
1877
+ }
1878
+ ],
1879
+ prompt() {
1880
+ return [
1881
+ "这是 FeatureBench 的一个单 feature 任务:在真实 Python 仓库中实现一个跨文件功能,并保持已有功能不退化。",
1882
+ "",
1883
+ "当前 workspace 已经由安装器准备好:包含目标仓库源码和依赖环境。请阅读 TASK.md 中的功能需求,修改源码完成实现。",
1884
+ "",
1885
+ "要求:",
1886
+ "- 只能修改当前工作区内的文件;工作区外不可访问,包括 /tmp;",
1887
+ "- 禁止联网、禁止调用其他 AI 或子代理;",
1888
+ "- 使用工作区里的 .venv 运行测试;",
1889
+ "- 不要删除、修改或伪造测试;评分会在你的代码上运行隐藏测试和回归测试;",
1890
+ "- 完成后用几句话总结实现思路。"
1891
+ ].join("\n");
1892
+ }
1893
+ };
1438
1894
  /** All task definitions keyed by id. */
1439
1895
  const TASK_DEFINITIONS = {
1440
1896
  [TANK_TASK]: TANK_DEFINITION,
1441
1897
  [WAREHOUSE_TASK]: WAREHOUSE_DEFINITION,
1442
1898
  [LAND_TASK]: LAND_DEFINITION,
1443
1899
  [BOMBERMAN_TASK]: BOMBERMAN_DEFINITION,
1444
- ["ctf"]: CTF_DEFINITION
1900
+ ["ctf"]: CTF_DEFINITION,
1901
+ [COMMIT0_TASK]: COMMIT0_DEFINITION,
1902
+ [SWE_BENCH_PRO_TASK]: SWE_BENCH_PRO_DEFINITION,
1903
+ [FEATUREBENCH_TASK]: FEATUREBENCH_DEFINITION
1445
1904
  };
1446
- /** Install, check, and prompt services for the task packs. */
1447
1905
  var TaskService = class {
1448
1906
  guard;
1449
1907
  registry;
1908
+ jobs;
1450
1909
  constructor(guard, registry) {
1451
1910
  this.guard = guard;
1452
1911
  this.registry = registry;
1912
+ this.jobs = new JobManager();
1913
+ }
1914
+ /** The clone URL for a task, honouring the selected source. */
1915
+ cloneUrl(definition, options) {
1916
+ const repo = definition.repoUrl ?? "";
1917
+ const source = options.cloneSource ?? "github";
1918
+ if (source === "custom" && options.customCloneUrl !== void 0 && options.customCloneUrl.trim() !== "") return options.customCloneUrl.trim().replace(/\/$/, "") + "/" + repo.replace(/^https?:\/\/github\.com\//, "");
1919
+ const remote = definition.remotes?.find((item) => item.id === source);
1920
+ if (remote !== void 0) return remote.url;
1921
+ return definition.remotes?.[0]?.url ?? repo;
1922
+ }
1923
+ /** Normalize a user-entered proxy address, or return an empty string. */
1924
+ proxyValue(raw) {
1925
+ const value = (raw ?? "").trim();
1926
+ if (value === "") return "";
1927
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : "http://" + value;
1928
+ }
1929
+ /** The pip index URL for a task, honouring the selected mirror. */
1930
+ pipIndexUrl(options) {
1931
+ const index = options.pipIndex ?? "pypi";
1932
+ if (index === "custom") return (options.customPipUrl ?? "").trim() || "https://pypi.org/simple";
1933
+ return PIP_MIRRORS.find((item) => item.id === index)?.url ?? "https://pypi.org/simple";
1453
1934
  }
1454
1935
  /** Check one workspace before install. */
1455
1936
  check(workspaceId) {
@@ -1487,6 +1968,118 @@ var TaskService = class {
1487
1968
  definition(taskId) {
1488
1969
  return TASK_DEFINITIONS[taskId];
1489
1970
  }
1971
+ /** Start a non-blocking install. The settings page polls the returned job. */
1972
+ startInstall(workspaceId, taskId, options = {}) {
1973
+ const definition = TASK_DEFINITIONS[taskId];
1974
+ if (definition === void 0) return {
1975
+ ok: false,
1976
+ error: "未知任务:" + taskId
1977
+ };
1978
+ const checked = this.check(workspaceId);
1979
+ if (!checked.ok) return {
1980
+ ok: false,
1981
+ error: checked.error
1982
+ };
1983
+ if (!checked.empty) return {
1984
+ ok: false,
1985
+ error: "工作区不是空的;未执行任何操作。"
1986
+ };
1987
+ if (!this.guard.active) return {
1988
+ ok: false,
1989
+ error: "请先启用评测守护,再安装评测任务。"
1990
+ };
1991
+ try {
1992
+ fs.copyFileSync(this.materialZip(taskId), path.join(checked.path, "workspace.zip"));
1993
+ } catch (error) {
1994
+ return {
1995
+ ok: false,
1996
+ error: "复制素材失败:" + (error instanceof Error ? error.message : String(error))
1997
+ };
1998
+ }
1999
+ const env = {
2000
+ EVAL_CLONE_URL: this.cloneUrl(definition, options),
2001
+ EVAL_PIP_INDEX_URL: this.pipIndexUrl(options),
2002
+ GIT_TERMINAL_PROMPT: "0",
2003
+ EVAL_GIT_PROXY: this.proxyValue(options.gitProxyUrl),
2004
+ EVAL_PIP_PROXY: this.proxyValue(options.pipProxyUrl)
2005
+ };
2006
+ const command = definition.needsSetup ? ["bash", "setup.sh"] : [
2007
+ "bash",
2008
+ "-c",
2009
+ "true"
2010
+ ];
2011
+ return {
2012
+ ok: true,
2013
+ jobId: this.jobs.start({
2014
+ kind: "install",
2015
+ task: taskId,
2016
+ workspaceId,
2017
+ command,
2018
+ cwd: checked.path,
2019
+ env,
2020
+ onSuccess: () => {
2021
+ const runId = this.newRunId();
2022
+ const prompt = this.guard.promptFor(checked.path, this.materialZip(taskId), definition);
2023
+ const run = {
2024
+ runId,
2025
+ task: definition.id,
2026
+ workspaceId,
2027
+ workspacePath: checked.path,
2028
+ sessionId: null,
2029
+ createdAt: Date.now(),
2030
+ startedAt: null,
2031
+ endedAt: null,
2032
+ status: "prepared",
2033
+ prompt,
2034
+ timeLimitMinutes: this.guard.configuration.timeLimitMinutes
2035
+ };
2036
+ this.guard.registerRun(run);
2037
+ return {
2038
+ runId,
2039
+ prompt
2040
+ };
2041
+ }
2042
+ }).id
2043
+ };
2044
+ }
2045
+ /** Start a single-workspace evaluation job. */
2046
+ startEval(workspaceId, taskId) {
2047
+ if (TASK_DEFINITIONS[taskId] === void 0) return {
2048
+ ok: false,
2049
+ error: "未知任务:" + taskId
2050
+ };
2051
+ const checked = this.check(workspaceId);
2052
+ if (!checked.ok) return {
2053
+ ok: false,
2054
+ error: checked.error
2055
+ };
2056
+ const script = path.join(packageRoot(), "eval", taskId + ".py");
2057
+ if (!fs.existsSync(script)) return {
2058
+ ok: false,
2059
+ error: "评测脚本不存在:" + script
2060
+ };
2061
+ return {
2062
+ ok: true,
2063
+ jobId: this.jobs.start({
2064
+ kind: "eval",
2065
+ task: taskId,
2066
+ workspaceId,
2067
+ command: [
2068
+ "python3",
2069
+ "-X",
2070
+ "utf8",
2071
+ script,
2072
+ "--workspace",
2073
+ checked.path
2074
+ ],
2075
+ cwd: packageRoot(),
2076
+ env: {
2077
+ PYTHONPATH: "",
2078
+ PYTHONHOME: ""
2079
+ }
2080
+ }).id
2081
+ };
2082
+ }
1490
2083
  /** Copy the packaged workspace.zip to a stable material path for install/manual use. */
1491
2084
  materialZip(taskId = TANK_TASK) {
1492
2085
  const definition = TASK_DEFINITIONS[taskId];
@@ -1533,12 +2126,13 @@ var TaskService = class {
1533
2126
  };
1534
2127
  try {
1535
2128
  fs.copyFileSync(this.materialZip(taskId), path.join(checked.path, "workspace.zip"));
2129
+ if (definition.prepare !== void 0) definition.prepare(checked.path);
1536
2130
  } catch (error) {
1537
2131
  return {
1538
2132
  ok: false,
1539
2133
  workspaceId,
1540
2134
  workspacePath: checked.path,
1541
- error: "复制素材失败:" + (error instanceof Error ? error.message : String(error))
2135
+ error: "任务准备失败:" + (error instanceof Error ? error.message : String(error))
1542
2136
  };
1543
2137
  }
1544
2138
  const runId = this.newRunId();
@@ -1645,7 +2239,8 @@ function registerRoutes(ctx, guard, tasks) {
1645
2239
  defaultBlacklist: guard.defaultBlacklist(),
1646
2240
  generatedDefault: guard.configuration.toolBlacklistUseDefault,
1647
2241
  workspaceRoot: dshHome(),
1648
- runsRoot: path.join(guardRoot(), "runs")
2242
+ runsRoot: path.join(guardRoot(), "runs"),
2243
+ pipMirrors: [...PIP_MIRRORS]
1649
2244
  });
1650
2245
  }
1651
2246
  });
@@ -1673,7 +2268,69 @@ function registerRoutes(ctx, guard, tasks) {
1673
2268
  const workspaceId = typeof body.workspaceId === "string" ? body.workspaceId : "";
1674
2269
  const taskId = typeof body.task === "string" && body.task !== "" ? body.task : TANK_TASK;
1675
2270
  if (workspaceId === "") return json({ error: "workspaceId is required" }, 400);
1676
- return json(tasks.install(workspaceId, taskId));
2271
+ const config = guard.configuration;
2272
+ return json(tasks.startInstall(workspaceId, taskId, {
2273
+ cloneSource: typeof body.cloneSource === "string" ? body.cloneSource : config.cloneSource,
2274
+ customCloneUrl: typeof body.customCloneUrl === "string" ? body.customCloneUrl : config.customCloneUrl,
2275
+ pipIndex: typeof body.pipIndex === "string" ? body.pipIndex : config.pipIndex,
2276
+ customPipUrl: typeof body.customPipUrl === "string" ? body.customPipUrl : config.customPipUrl,
2277
+ gitProxyUrl: typeof body.gitProxyUrl === "string" ? body.gitProxyUrl : config.gitProxyUrl,
2278
+ pipProxyUrl: typeof body.pipProxyUrl === "string" ? body.pipProxyUrl : config.pipProxyUrl
2279
+ }));
2280
+ }
2281
+ });
2282
+ add({
2283
+ path: EVAL_ROUTE,
2284
+ methods: ["POST"],
2285
+ requestBody: "buffered",
2286
+ fetch: async (request) => {
2287
+ let body = {};
2288
+ try {
2289
+ body = await request.json();
2290
+ } catch {
2291
+ return json({ error: "invalid body" }, 400);
2292
+ }
2293
+ const workspaceId = typeof body.workspaceId === "string" ? body.workspaceId : "";
2294
+ const taskId = typeof body.task === "string" && body.task !== "" ? body.task : TANK_TASK;
2295
+ if (workspaceId === "") return json({ error: "workspaceId is required" }, 400);
2296
+ return json(tasks.startEval(workspaceId, taskId));
2297
+ }
2298
+ });
2299
+ add({
2300
+ path: JOBS_ROUTE,
2301
+ methods: ["GET"],
2302
+ requestBody: "buffered",
2303
+ fetch: async (request) => {
2304
+ const workspaceId = query(request, "workspaceId");
2305
+ const taskId = query(request, "task");
2306
+ return json(tasks.jobs.list(workspaceId === "" ? void 0 : workspaceId, taskId === "" ? void 0 : taskId));
2307
+ }
2308
+ });
2309
+ add({
2310
+ path: JOB_ROUTE,
2311
+ methods: ["GET"],
2312
+ requestBody: "buffered",
2313
+ fetch: async (request) => {
2314
+ const jobId = query(request, "id");
2315
+ const job = tasks.jobs.get(jobId);
2316
+ return job === void 0 ? json({ error: "job not found" }, 404) : json(job);
2317
+ }
2318
+ });
2319
+ add({
2320
+ path: JOB_CANCEL_ROUTE,
2321
+ methods: ["POST"],
2322
+ requestBody: "buffered",
2323
+ fetch: async (request) => {
2324
+ let body = {};
2325
+ try {
2326
+ body = await request.json();
2327
+ } catch {
2328
+ return json({ error: "invalid body" }, 400);
2329
+ }
2330
+ const jobId = typeof body.id === "string" ? body.id : "";
2331
+ if (jobId === "") return json({ error: "id is required" }, 400);
2332
+ const job = tasks.jobs.cancel(jobId);
2333
+ return job === void 0 ? json({ error: "job not found" }, 404) : json(job);
1677
2334
  }
1678
2335
  });
1679
2336
  add({