dsh-eval-guard 0.6.0 → 0.7.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.
package/lib/index.js CHANGED
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import os from "node:os";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { execFileSync, spawn } from "node:child_process";
8
+ import { createHash } from "node:crypto";
8
9
  //#region src/config.ts
9
10
  /**
10
11
  * Plugin configuration. The Settings page writes this document through the
@@ -25,7 +26,11 @@ const Config = z.object({
25
26
  customPipUrl: z.string().default("").volatile(),
26
27
  gitProxyUrl: z.string().default("").volatile(),
27
28
  pipProxyUrl: z.string().default("").volatile(),
28
- cifarProxyUrl: z.string().default("").volatile()
29
+ cifarProxyUrl: z.string().default("").volatile(),
30
+ cifarMirror: z.string().default("toronto").volatile(),
31
+ customCifarUrl: z.string().default("").volatile(),
32
+ parallelEval: z.boolean().default(false).volatile(),
33
+ taskId: z.string().default("").volatile()
29
34
  });
30
35
  /** Clamp and normalize a live configuration object. */
31
36
  function resolveConfig(value) {
@@ -43,7 +48,11 @@ function resolveConfig(value) {
43
48
  customPipUrl: typeof value?.customPipUrl === "string" ? value.customPipUrl : "",
44
49
  gitProxyUrl: typeof value?.gitProxyUrl === "string" ? value.gitProxyUrl : "",
45
50
  pipProxyUrl: typeof value?.pipProxyUrl === "string" ? value.pipProxyUrl : "",
46
- cifarProxyUrl: typeof value?.cifarProxyUrl === "string" ? value.cifarProxyUrl : ""
51
+ cifarProxyUrl: typeof value?.cifarProxyUrl === "string" ? value.cifarProxyUrl : "",
52
+ cifarMirror: typeof value?.cifarMirror === "string" && value.cifarMirror !== "" ? value.cifarMirror : "toronto",
53
+ customCifarUrl: typeof value?.customCifarUrl === "string" ? value.customCifarUrl : "",
54
+ parallelEval: value?.parallelEval === true,
55
+ taskId: typeof value?.taskId === "string" ? value.taskId : ""
47
56
  };
48
57
  }
49
58
  //#endregion
@@ -82,7 +91,18 @@ const NEURAL_FP32_TASK = "neural-fp32";
82
91
  const NEURAL_FP32_TASK_LABEL = "Neural-FP32 · 运算与性能";
83
92
  const NEURAL_CIFAR_TASK = "neural-cifar";
84
93
  const NEURAL_CIFAR_TASK_LABEL = "Neural-CIFAR · 从零训练图像分类";
94
+ const NEURAL_PENDULUM_TASK = "neural-pendulum";
95
+ const NEURAL_PENDULUM_TASK_LABEL = "Neural-Pendulum · 从零训练三节摆控制器";
85
96
  /** Common pip indexes offered by the settings page. */
97
+ const CIFAR_MIRRORS = [{
98
+ id: "toronto",
99
+ label: "Toronto 官方",
100
+ url: "https://www.cs.toronto.edu/~kriz/cifar-10-binary.tar.gz"
101
+ }, {
102
+ id: "cave",
103
+ label: "Toronto Cave",
104
+ url: "https://cave.cs.toronto.edu/kriz/cifar-10-binary.tar.gz"
105
+ }];
86
106
  const PIP_MIRRORS = [
87
107
  {
88
108
  id: "pypi",
@@ -452,6 +472,15 @@ const SYSTEM_READ_DIRS = [
452
472
  "/sys",
453
473
  "/var/lib"
454
474
  ];
475
+ /**
476
+ * Individual read-only system files commonly needed by benign inspection tools
477
+ * such as uptime/who. Keep this list narrow; do not expose /run as a directory.
478
+ */
479
+ const SYSTEM_READ_FILES = [
480
+ "/run/utmp",
481
+ "/run/utmpx",
482
+ "/var/log/wtmp"
483
+ ];
455
484
  /** Python stdlib/venv directories that may live next to a workspace. */
456
485
  function pythonRuntimeDirs(workspace) {
457
486
  const dirs = /* @__PURE__ */ new Set();
@@ -574,6 +603,10 @@ function writePolicy(workspace, rules) {
574
603
  }
575
604
  } catch {}
576
605
  lines.push(line("read", "/proc"));
606
+ for (const file of SYSTEM_READ_FILES) {
607
+ lines.push(line("read", file));
608
+ for (const ancestor of ancestorsOf(file)) lines.push(line("exact", ancestor));
609
+ }
577
610
  lines.push(line("exact", "/"));
578
611
  for (const ancestor of ancestorsOf(workspace)) lines.push(line("exact", ancestor));
579
612
  for (const ancestor of ancestorsOf(runtime.node)) lines.push(line("exact", ancestor));
@@ -628,12 +661,26 @@ function bwrapArgs(workspace, cwd, rules, runtime) {
628
661
  const args = [
629
662
  "--unshare-all",
630
663
  "--die-with-parent",
631
- "--new-session"
664
+ "--new-session",
665
+ "--as-pid-1"
632
666
  ];
633
667
  for (const system of SYSTEM_READ_DIRS) try {
634
668
  if (fs.statSync(system).isDirectory()) args.push("--ro-bind", system, system);
635
669
  } catch {}
636
670
  args.push("--proc", "/proc", "--dev", "/dev");
671
+ const systemFiles = SYSTEM_READ_FILES.filter((file) => {
672
+ try {
673
+ return fs.statSync(file).isFile();
674
+ } catch {
675
+ return false;
676
+ }
677
+ });
678
+ if (systemFiles.some((file) => file.startsWith("/run/"))) args.push("--dir", "/run");
679
+ if (systemFiles.some((file) => file.startsWith("/var/log/"))) {
680
+ args.push("--dir", "/var");
681
+ args.push("--dir", "/var/log");
682
+ }
683
+ for (const file of systemFiles) args.push("--ro-bind", file, file);
637
684
  args.push("--bind", workspace, workspace);
638
685
  const tmp = path.join(workspace, ".tmp");
639
686
  ensureDir(tmp);
@@ -766,12 +813,15 @@ var Guard = class {
766
813
  startedAt = /* @__PURE__ */ new Map();
767
814
  runs = /* @__PURE__ */ new Map();
768
815
  sessionRuns = /* @__PURE__ */ new Map();
769
- pending = /* @__PURE__ */ new Map();
770
816
  offsets = /* @__PURE__ */ new Map();
771
817
  knownAgents = /* @__PURE__ */ new Set();
772
818
  toolCache = [];
773
819
  disposers = [];
774
820
  task;
821
+ /** Optional hook invoked when any run binds to an agent. */
822
+ onRunAssociated;
823
+ /** Optional automatic task binding for every main conversation. */
824
+ onAutoRun;
775
825
  /** @param ctx - host context; @param raw - live config; @param task - the one task pack. */
776
826
  constructor(ctx, services, raw, task) {
777
827
  this.ctx = ctx;
@@ -825,7 +875,11 @@ var Guard = class {
825
875
  customPipUrl: read("customPipUrl"),
826
876
  gitProxyUrl: read("gitProxyUrl"),
827
877
  pipProxyUrl: read("pipProxyUrl"),
828
- cifarProxyUrl: read("cifarProxyUrl")
878
+ cifarProxyUrl: read("cifarProxyUrl"),
879
+ cifarMirror: read("cifarMirror"),
880
+ customCifarUrl: read("customCifarUrl"),
881
+ parallelEval: read("parallelEval"),
882
+ taskId: read("taskId")
829
883
  });
830
884
  }
831
885
  /** Apply a new configuration document. */
@@ -999,11 +1053,9 @@ var Guard = class {
999
1053
  if (this.enabled) this.forceApproval(agent.session);
1000
1054
  const cwd = agent.session.header?.meta?.cwd;
1001
1055
  if (cwd !== void 0) {
1002
- const pending = this.pending.get(cwd);
1003
- if (pending !== void 0 && pending.expiresAt > Date.now()) {
1004
- this.pending.delete(cwd);
1005
- this.associate(pending.runId, agent);
1006
- }
1056
+ if (this.enabled) try {
1057
+ this.onAutoRun?.(agent, cwd);
1058
+ } catch {}
1007
1059
  }
1008
1060
  }
1009
1061
  /** An agent went away. */
@@ -1021,7 +1073,12 @@ var Guard = class {
1021
1073
  }
1022
1074
  }
1023
1075
  }
1024
- /** Associate one pending run with the new session. */
1076
+ /** Bind an already-registered run to an agent. */
1077
+ attachRun(agent, run) {
1078
+ this.runs.set(run.runId, run);
1079
+ this.associate(run.runId, agent);
1080
+ }
1081
+ /** Associate one run with the new session. */
1025
1082
  associate(runId, agent) {
1026
1083
  const run = this.runs.get(runId);
1027
1084
  if (run === void 0) return;
@@ -1034,6 +1091,9 @@ var Guard = class {
1034
1091
  this.sessionRuns.set(agent.id, runId);
1035
1092
  this.writeRun(run);
1036
1093
  this.writeUsage(runId, this.zeroUsage(run.startedAt));
1094
+ try {
1095
+ this.onRunAssociated?.(agent, run);
1096
+ } catch {}
1037
1097
  }
1038
1098
  /** Session event intake. */
1039
1099
  onSessionEvent(session, event) {
@@ -1268,10 +1328,9 @@ var Guard = class {
1268
1328
  } catch {}
1269
1329
  }
1270
1330
  }
1271
- /** Remove old sandbox policy files and expired pending associations. */
1331
+ /** Remove old sandbox policy files. */
1272
1332
  cleanupTemp() {
1273
1333
  const now = Date.now();
1274
- for (const [workspace, pending] of [...this.pending]) if (pending.expiresAt <= now) this.pending.delete(workspace);
1275
1334
  const tmp = path.join(guardRoot(), "tmp");
1276
1335
  try {
1277
1336
  for (const entry of fs.readdirSync(tmp)) {
@@ -1352,10 +1411,12 @@ var Guard = class {
1352
1411
  this.writeRun(run);
1353
1412
  this.writeUsage(run.runId, this.zeroUsage(null));
1354
1413
  fs.writeFileSync(path.join(guardRoot(), "runs", run.runId, "violations.jsonl"), "", "utf8");
1355
- this.pending.set(run.workspacePath, {
1356
- runId: run.runId,
1357
- expiresAt: Date.now() + 216e5
1358
- });
1414
+ }
1415
+ /** Resolve the registered run bound to one agent session. */
1416
+ runForSession(sessionId) {
1417
+ if (sessionId === void 0) return void 0;
1418
+ const runId = this.sessionRuns.get(sessionId);
1419
+ return runId === void 0 ? void 0 : this.runs.get(runId);
1359
1420
  }
1360
1421
  /** Build the prompt for one task with only installed tools listed. */
1361
1422
  promptFor(workspacePath, source, task = this.task) {
@@ -1390,6 +1451,7 @@ var JobManager = class {
1390
1451
  root;
1391
1452
  jobs = /* @__PURE__ */ new Map();
1392
1453
  children = /* @__PURE__ */ new Map();
1454
+ cancelHandlers = /* @__PURE__ */ new Map();
1393
1455
  constructor() {
1394
1456
  this.root = path.join(guardRoot(), "jobs");
1395
1457
  fs.mkdirSync(this.root, { recursive: true });
@@ -1466,6 +1528,7 @@ var JobManager = class {
1466
1528
  return this.info(job);
1467
1529
  }
1468
1530
  this.children.set(jobId, child);
1531
+ if (options.onCancel !== void 0) this.cancelHandlers.set(jobId, options.onCancel);
1469
1532
  const append = (chunk) => {
1470
1533
  try {
1471
1534
  fs.appendFileSync(logFile, chunk);
@@ -1482,6 +1545,7 @@ var JobManager = class {
1482
1545
  });
1483
1546
  child.on("exit", (code, signal) => {
1484
1547
  this.children.delete(jobId);
1548
+ this.cancelHandlers.delete(jobId);
1485
1549
  if (job.status === "cancelled") return;
1486
1550
  if (code === 0) try {
1487
1551
  const extra = options.onSuccess?.(this.detail(jobId));
@@ -1500,10 +1564,16 @@ var JobManager = class {
1500
1564
  } catch (error) {
1501
1565
  job.status = "failed";
1502
1566
  job.error = error instanceof Error ? error.message : String(error);
1567
+ try {
1568
+ options.onFailure?.(this.detail(jobId));
1569
+ } catch {}
1503
1570
  }
1504
1571
  else {
1505
1572
  job.status = "failed";
1506
1573
  job.error = signal !== null ? "进程被信号 " + signal + " 终止" : "退出码 " + String(code);
1574
+ try {
1575
+ options.onFailure?.(this.detail(jobId));
1576
+ } catch {}
1507
1577
  }
1508
1578
  job.endedAt = now();
1509
1579
  job.updatedAt = now();
@@ -1522,13 +1592,37 @@ var JobManager = class {
1522
1592
  job.error = "用户取消";
1523
1593
  this.write(job);
1524
1594
  const child = this.children.get(jobId);
1525
- if (child?.pid !== void 0) try {
1526
- process.kill(-child.pid, "SIGTERM");
1527
- } catch {
1528
- try {
1529
- child.kill("SIGTERM");
1595
+ const handler = this.cancelHandlers.get(jobId);
1596
+ this.cancelHandlers.delete(jobId);
1597
+ let cleaned = false;
1598
+ const cleanup = () => {
1599
+ if (cleaned) return;
1600
+ cleaned = true;
1601
+ if (handler !== void 0) try {
1602
+ handler(this.detail(jobId));
1530
1603
  } catch {}
1531
- }
1604
+ };
1605
+ if (child?.pid !== void 0) {
1606
+ child.once("exit", cleanup);
1607
+ try {
1608
+ process.kill(-child.pid, "SIGTERM");
1609
+ } catch {
1610
+ try {
1611
+ child.kill("SIGTERM");
1612
+ } catch {}
1613
+ }
1614
+ setTimeout(() => {
1615
+ if (cleaned) return;
1616
+ try {
1617
+ process.kill(-child.pid, "SIGKILL");
1618
+ } catch {
1619
+ try {
1620
+ child.kill("SIGKILL");
1621
+ } catch {}
1622
+ }
1623
+ cleanup();
1624
+ }, 2e3).unref?.();
1625
+ } else cleanup();
1532
1626
  return this.info(job);
1533
1627
  }
1534
1628
  /** Current state, including a bounded log tail and optional result. */
@@ -1704,6 +1798,14 @@ const TASKS = [
1704
1798
  timeLimitMinutes: 1440,
1705
1799
  timeLimitEnabled: false,
1706
1800
  needsSetup: true
1801
+ },
1802
+ {
1803
+ id: NEURAL_PENDULUM_TASK,
1804
+ label: NEURAL_PENDULUM_TASK_LABEL,
1805
+ description: "从零实现纯 C++17 神经网络基础设施,用三个关节角度控制预编译三节摆起摆并稳定保持;评测固定初始状态,统计 t_can。",
1806
+ timeLimitMinutes: 1440,
1807
+ timeLimitEnabled: false,
1808
+ needsSetup: true
1707
1809
  }
1708
1810
  ];
1709
1811
  /** The tank task definition (original prompt; only halftime mechanics removed). */
@@ -2008,6 +2110,34 @@ const NEURAL_CIFAR_DEFINITION = {
2008
2110
  ].join("\n");
2009
2111
  }
2010
2112
  };
2113
+ /** Neural-Pendulum swing-up task. */
2114
+ const NEURAL_PENDULUM_DEFINITION = {
2115
+ id: NEURAL_PENDULUM_TASK,
2116
+ label: NEURAL_PENDULUM_TASK_LABEL,
2117
+ description: TASKS[11].description,
2118
+ asset: path.join(packageRoot(), "assets", "neural-pendulum", "workspace.zip"),
2119
+ timeLimitMinutes: 1440,
2120
+ timeLimitEnabled: false,
2121
+ needsSetup: true,
2122
+ prompt() {
2123
+ return [
2124
+ "你是一名控制与神经网络工程师。任务:从零实现纯 C++17、仅 CPU、fp32 的神经网络基础设施,并用它训练一个控制器,控制预编译的三节摆从接近全向下起摆,使三根杆全部进入竖直向上容差并尽可能长时间保持。",
2125
+ "",
2126
+ "请先完整阅读 TASK.md;它是权威规则。硬约束:仅 C++17 标准库、仅 CPU、fp32;不使用 Python/OpenCL/CUDA/BLAS/Eigen/第三方库;必须从提交源码构建出 build/control 与 build/nn_selftest。",
2127
+ "",
2128
+ "环境细节:joint1 与 joint3 驱动,joint2 被动;控制频率 100 Hz;每回合最多 3000 步(30 s),达到上限后继续 step 返回 {\"error\":\"episode_finished\"},必须 reset。环境只返回三个关节角度(弧度,不除 pi),不返回 reward/loss/cost/done;环境不提供杆长、质量、惯量、角速度或 tip。训练 reset 带小范围随机初始角度和角速度;评测使用固定初始状态,无随机扰动。",
2129
+ "",
2130
+ "优化目标:从接近全向下起摆,使三根杆全部进入 0.05 rad 的竖直向上容差,并尽可能长时间保持;评测指标为 t_can。损失函数、奖励、优化器、网络结构和训练方法由你自行设计,不规定任何训练方案。",
2131
+ "",
2132
+ "评测会从干净副本重新构建提交源码,运行 build/nn_selftest,然后在固定初始状态上驱动 build/control,统计连续全向上保持时间 t_can。不要把任何未提交的二进制、临时构建产物或外部进程当作交付依赖。",
2133
+ "使用宿主工具 eval_submit 提交当前源码快照,使用 eval_test 请求宿主评测最新提交。提交快照、事件、测试结果和最终分数全部保存在工作区之外的宿主目录;不要尝试读取或修改这些数据,也不要依赖任何工作区内的测试结果文件。最终评测使用最新提交快照,按宿主事件时间积分计算分数。",
2134
+ "",
2135
+ "工作方式:先完成最小可构建的基础设施和 control 协议;再持续做控制/训练实验并记录可复现结果;最终保证工作区从零构建、nn_selftest 通过、control 可被评测驱动,并留下 NOTES.md/RESULTS.md。",
2136
+ "",
2137
+ "不要向我提问,也不要等待确认,独立完成。"
2138
+ ].join("\n");
2139
+ }
2140
+ };
2011
2141
  /** All task definitions keyed by id. */
2012
2142
  const TASK_DEFINITIONS = {
2013
2143
  [TANK_TASK]: TANK_DEFINITION,
@@ -2020,7 +2150,8 @@ const TASK_DEFINITIONS = {
2020
2150
  [FEATUREBENCH_TASK]: FEATUREBENCH_DEFINITION,
2021
2151
  [NEURAL_CPP_TASK]: NEURAL_CPP_DEFINITION,
2022
2152
  [NEURAL_FP32_TASK]: NEURAL_FP32_DEFINITION,
2023
- [NEURAL_CIFAR_TASK]: NEURAL_CIFAR_DEFINITION
2153
+ [NEURAL_CIFAR_TASK]: NEURAL_CIFAR_DEFINITION,
2154
+ [NEURAL_PENDULUM_TASK]: NEURAL_PENDULUM_DEFINITION
2024
2155
  };
2025
2156
  var TaskService = class {
2026
2157
  guard;
@@ -2031,6 +2162,272 @@ var TaskService = class {
2031
2162
  this.registry = registry;
2032
2163
  this.jobs = new JobManager();
2033
2164
  }
2165
+ /** Host-side directory for one workspace's authoritative task state. */
2166
+ hostDir(workspaceId) {
2167
+ return path.join(guardRoot(), "neural-pendulum", workspaceId.replace(/[^A-Za-z0-9_.-]/g, "_"));
2168
+ }
2169
+ hostEvents(workspaceId) {
2170
+ return path.join(this.hostDir(workspaceId), "events.jsonl");
2171
+ }
2172
+ appendHostEvent(workspaceId, event) {
2173
+ const dir = this.hostDir(workspaceId);
2174
+ fs.mkdirSync(dir, { recursive: true });
2175
+ fs.appendFileSync(this.hostEvents(workspaceId), JSON.stringify(event) + "\n", "utf8");
2176
+ }
2177
+ hostLatestSubmission(workspaceId) {
2178
+ const root = path.join(this.hostDir(workspaceId), "submissions");
2179
+ if (!fs.existsSync(root)) return void 0;
2180
+ const dirs = fs.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory());
2181
+ if (dirs.length === 0) return void 0;
2182
+ const id = dirs.map((entry) => entry.name).sort((a, b) => {
2183
+ const am = fs.statSync(path.join(root, a)).mtimeMs;
2184
+ return fs.statSync(path.join(root, b)).mtimeMs - am;
2185
+ })[0];
2186
+ return {
2187
+ id,
2188
+ path: path.join(root, id)
2189
+ };
2190
+ }
2191
+ copySnapshot(source, destination) {
2192
+ const excluded = /* @__PURE__ */ new Set([
2193
+ "build",
2194
+ ".git",
2195
+ ".eval",
2196
+ ".submissions",
2197
+ "node_modules",
2198
+ "__pycache__",
2199
+ ".venv"
2200
+ ]);
2201
+ let count = 0;
2202
+ const walk = (dir) => {
2203
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
2204
+ if (excluded.has(entry.name)) continue;
2205
+ const from = path.join(dir, entry.name);
2206
+ const to = path.join(destination, path.relative(source, from));
2207
+ if (entry.isDirectory()) {
2208
+ fs.mkdirSync(to, { recursive: true });
2209
+ walk(from);
2210
+ } else if (entry.isFile()) {
2211
+ fs.mkdirSync(path.dirname(to), { recursive: true });
2212
+ fs.copyFileSync(from, to);
2213
+ count++;
2214
+ }
2215
+ }
2216
+ };
2217
+ fs.mkdirSync(destination, { recursive: true });
2218
+ walk(source);
2219
+ return count;
2220
+ }
2221
+ /** Host-side submit: snapshot the workspace into the run directory. */
2222
+ hostSubmit(run) {
2223
+ const done = path.join(this.hostDir(run.workspaceId), "done");
2224
+ if (fs.existsSync(done)) return {
2225
+ ok: false,
2226
+ error: "task already completed"
2227
+ };
2228
+ const id = Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8);
2229
+ const metaPath = path.join(this.hostDir(run.workspaceId), "meta.json");
2230
+ if (!fs.existsSync(metaPath)) {
2231
+ fs.mkdirSync(this.hostDir(run.workspaceId), { recursive: true });
2232
+ fs.writeFileSync(metaPath, JSON.stringify({
2233
+ started_at: run.createdAt / 1e3,
2234
+ t_tgt: 10
2235
+ }), "utf8");
2236
+ }
2237
+ const destination = path.join(this.hostDir(run.workspaceId), "submissions", id);
2238
+ if (fs.existsSync(destination)) return {
2239
+ ok: false,
2240
+ error: "submission id collision"
2241
+ };
2242
+ const files = this.copySnapshot(run.workspacePath, destination);
2243
+ const hash = createHash("sha256");
2244
+ const hashDir = (dir) => {
2245
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
2246
+ const full = path.join(dir, entry.name);
2247
+ if (entry.isDirectory()) hashDir(full);
2248
+ else if (entry.isFile()) {
2249
+ hash.update(path.relative(destination, full));
2250
+ hash.update(fs.readFileSync(full));
2251
+ }
2252
+ }
2253
+ };
2254
+ hashDir(destination);
2255
+ this.appendHostEvent(run.workspaceId, {
2256
+ event: "submit",
2257
+ time: Date.now() / 1e3,
2258
+ submission_id: id,
2259
+ source_hash: "sha256:" + hash.digest("hex"),
2260
+ files
2261
+ });
2262
+ return {
2263
+ ok: true,
2264
+ submission: id,
2265
+ files
2266
+ };
2267
+ }
2268
+ async runHostEval(workspaceId) {
2269
+ const latest = this.hostLatestSubmission(workspaceId);
2270
+ if (latest === void 0) return {
2271
+ stage: "no_submission",
2272
+ t_can: 0,
2273
+ completed: false,
2274
+ error: ""
2275
+ };
2276
+ const temp = fs.mkdtempSync(path.join(guardRoot(), "pend-eval-"));
2277
+ try {
2278
+ const submissions = path.join(temp, ".eval", "submissions");
2279
+ fs.mkdirSync(submissions, { recursive: true });
2280
+ fs.cpSync(latest.path, path.join(submissions, latest.id), { recursive: true });
2281
+ const events = this.hostEvents(workspaceId);
2282
+ fs.writeFileSync(path.join(temp, ".eval", "events.jsonl"), fs.existsSync(events) ? fs.readFileSync(events) : "", "utf8");
2283
+ fs.writeFileSync(path.join(temp, ".eval", "meta.json"), JSON.stringify({
2284
+ started_at: Date.now() / 1e3,
2285
+ t_tgt: 10
2286
+ }), "utf8");
2287
+ const resultFile = path.join(temp, "result.json");
2288
+ const script = path.join(packageRoot(), "neural-pendulum-eval", "run_eval.py");
2289
+ const env = {
2290
+ ...process.env,
2291
+ EVAL_RESULT_FILE: resultFile,
2292
+ PYTHONPATH: "",
2293
+ PYTHONHOME: ""
2294
+ };
2295
+ const child = spawn("python3", [
2296
+ "-X",
2297
+ "utf8",
2298
+ script,
2299
+ "--workspace",
2300
+ temp
2301
+ ], { env });
2302
+ let stderr = "";
2303
+ child.stderr?.on("data", (chunk) => {
2304
+ stderr += String(chunk);
2305
+ });
2306
+ await new Promise((resolve, reject) => {
2307
+ child.on("error", reject);
2308
+ child.on("close", () => resolve());
2309
+ });
2310
+ if (!fs.existsSync(resultFile)) return {
2311
+ stage: "controller",
2312
+ t_can: 0,
2313
+ completed: false,
2314
+ error: stderr || "evaluator failed"
2315
+ };
2316
+ const value = JSON.parse(fs.readFileSync(resultFile, "utf8"));
2317
+ const error = String(value.error ?? "");
2318
+ let stage = "controller";
2319
+ if (error.includes("cmake") || error.includes("build")) stage = "compile";
2320
+ else if (error.includes("selftest") || value.infra_ok === false) stage = "infra";
2321
+ else if (error.includes("controller")) stage = "controller";
2322
+ else if (value.success === true) stage = "completed";
2323
+ else if (value.infra_ok === true) stage = "scoring";
2324
+ else stage = "simulation";
2325
+ return {
2326
+ stage,
2327
+ t_can: Number(value.t_can ?? 0),
2328
+ completed: value.success === true,
2329
+ error
2330
+ };
2331
+ } finally {
2332
+ fs.rmSync(temp, {
2333
+ recursive: true,
2334
+ force: true
2335
+ });
2336
+ }
2337
+ }
2338
+ /** Host-side test: evaluate the latest host submission and write the event. */
2339
+ async hostTest(run) {
2340
+ const done = path.join(this.hostDir(run.workspaceId), "done");
2341
+ if (fs.existsSync(done)) return {
2342
+ ok: false,
2343
+ stage: "completed",
2344
+ error: "task already completed"
2345
+ };
2346
+ const latest = this.hostLatestSubmission(run.workspaceId);
2347
+ if (latest === void 0) {
2348
+ this.appendHostEvent(run.workspaceId, {
2349
+ event: "test",
2350
+ time: Date.now() / 1e3,
2351
+ submission_id: "",
2352
+ stage: "no_submission",
2353
+ t_can: 0,
2354
+ completed: false,
2355
+ error: ""
2356
+ });
2357
+ return {
2358
+ ok: true,
2359
+ stage: "no_submission",
2360
+ t_can: 0,
2361
+ completed: false,
2362
+ error: ""
2363
+ };
2364
+ }
2365
+ const value = await this.runHostEval(run.workspaceId);
2366
+ const stage = String(value.stage ?? "simulation");
2367
+ const tCan = Number(value.t_can ?? 0);
2368
+ const completed = stage === "completed" && Number.isFinite(tCan);
2369
+ this.appendHostEvent(run.workspaceId, {
2370
+ event: "test",
2371
+ time: Date.now() / 1e3,
2372
+ submission_id: latest.id,
2373
+ stage,
2374
+ t_can: tCan,
2375
+ completed,
2376
+ error: String(value.error ?? "")
2377
+ });
2378
+ if (completed) fs.writeFileSync(done, String(Date.now() / 1e3), "utf8");
2379
+ return {
2380
+ ok: true,
2381
+ stage,
2382
+ t_can: tCan,
2383
+ completed,
2384
+ error: String(value.error ?? "")
2385
+ };
2386
+ }
2387
+ /** Build a temporary workspace containing the host-side authoritative state. */
2388
+ stageHostWorkspace(workspaceId) {
2389
+ if (this.hostLatestSubmission(workspaceId) === void 0) return void 0;
2390
+ const temp = fs.mkdtempSync(path.join(guardRoot(), "pend-eval-view-"));
2391
+ const state = path.join(temp, ".eval");
2392
+ fs.mkdirSync(path.join(state, "submissions"), { recursive: true });
2393
+ const root = path.join(this.hostDir(workspaceId), "submissions");
2394
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) if (entry.isDirectory()) fs.cpSync(path.join(root, entry.name), path.join(state, "submissions", entry.name), { recursive: true });
2395
+ const events = this.hostEvents(workspaceId);
2396
+ fs.writeFileSync(path.join(state, "events.jsonl"), fs.existsSync(events) ? fs.readFileSync(events) : "", "utf8");
2397
+ const meta = path.join(this.hostDir(workspaceId), "meta.json");
2398
+ fs.writeFileSync(path.join(state, "meta.json"), fs.existsSync(meta) ? fs.readFileSync(meta) : JSON.stringify({
2399
+ started_at: Date.now() / 1e3,
2400
+ t_tgt: 10
2401
+ }), "utf8");
2402
+ return temp;
2403
+ }
2404
+ /** Bind every main conversation to the configured active task, no install needed. */
2405
+ autoBind(agent, cwd) {
2406
+ if (!this.guard.active) return false;
2407
+ const taskId = this.guard.configuration.taskId;
2408
+ if (taskId === "") return false;
2409
+ const definition = TASK_DEFINITIONS[taskId];
2410
+ if (definition === void 0) return false;
2411
+ const workspaceId = (this.registry.list?.() ?? []).find((workspace) => path.resolve(workspace.path) === path.resolve(cwd))?.id ?? "path-" + createHash("sha1").update(cwd).digest("hex").slice(0, 16);
2412
+ const runId = this.newRunId();
2413
+ const prompt = this.promptNotice(this.guard.promptFor(cwd, this.materialZip(taskId), definition));
2414
+ const run = {
2415
+ runId,
2416
+ task: definition.id,
2417
+ workspaceId,
2418
+ workspacePath: cwd,
2419
+ sessionId: null,
2420
+ createdAt: Date.now(),
2421
+ startedAt: null,
2422
+ endedAt: null,
2423
+ status: "prepared",
2424
+ prompt,
2425
+ timeLimitMinutes: this.guard.configuration.timeLimitMinutes
2426
+ };
2427
+ this.guard.registerRun(run);
2428
+ this.guard.attachRun(agent, run);
2429
+ return true;
2430
+ }
2034
2431
  /** The clone URL for a task, honouring the selected source. */
2035
2432
  cloneUrl(definition, options) {
2036
2433
  const repo = definition.repoUrl ?? "";
@@ -2046,6 +2443,12 @@ var TaskService = class {
2046
2443
  if (value === "") return "";
2047
2444
  return /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : "http://" + value;
2048
2445
  }
2446
+ /** The CIFAR-10 binary archive URL for the selected mirror. */
2447
+ cifarUrl(options) {
2448
+ const mirror = options.cifarMirror ?? "toronto";
2449
+ if (mirror === "custom") return (options.customCifarUrl ?? "").trim() || CIFAR_MIRRORS[0].url;
2450
+ return CIFAR_MIRRORS.find((item) => item.id === mirror)?.url ?? CIFAR_MIRRORS[0].url;
2451
+ }
2049
2452
  /** The pip index URL for a task, honouring the selected mirror. */
2050
2453
  pipIndexUrl(options) {
2051
2454
  const index = options.pipIndex ?? "pypi";
@@ -2134,6 +2537,23 @@ var TaskService = class {
2134
2537
  });
2135
2538
  } catch {}
2136
2539
  }
2540
+ /** Remove every entry created by an install that never reached success. */
2541
+ wipeInstallWorkspace(workspacePath) {
2542
+ try {
2543
+ for (const entry of fs.readdirSync(workspacePath)) fs.rmSync(path.join(workspacePath, entry), {
2544
+ recursive: true,
2545
+ force: true
2546
+ });
2547
+ } catch {}
2548
+ }
2549
+ /** Remove the shared CIFAR lock only when it belongs to this install. */
2550
+ cleanupCifarLock(token) {
2551
+ const lock = path.join(os.homedir(), ".dsh", "eval-guard", "cache", "cifar10", ".download.lock");
2552
+ try {
2553
+ if (!fs.existsSync(lock)) return;
2554
+ if (fs.readFileSync(lock, "utf8").includes("token=" + token)) fs.rmSync(lock, { force: true });
2555
+ } catch {}
2556
+ }
2137
2557
  /** Start a non-blocking install. The settings page polls the returned job. */
2138
2558
  startInstall(workspaceId, taskId, options = {}) {
2139
2559
  const definition = TASK_DEFINITIONS[taskId];
@@ -2154,61 +2574,56 @@ var TaskService = class {
2154
2574
  ok: false,
2155
2575
  error: "请先启用评测守护,再安装评测任务。"
2156
2576
  };
2577
+ const initiallyEmpty = checked.empty;
2578
+ const installToken = "install-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
2579
+ const cleanupInstall = () => {
2580
+ if (!initiallyEmpty) return;
2581
+ this.wipeInstallWorkspace(checked.path);
2582
+ this.cleanupCifarLock(installToken);
2583
+ };
2157
2584
  try {
2158
2585
  fs.copyFileSync(this.materialZip(taskId), path.join(checked.path, "workspace.zip"));
2159
2586
  if (definition.needsSetup) this.extractMaterial(checked.path);
2160
2587
  } catch (error) {
2588
+ cleanupInstall();
2161
2589
  return {
2162
2590
  ok: false,
2163
2591
  error: "复制素材失败:" + (error instanceof Error ? error.message : String(error))
2164
2592
  };
2165
2593
  }
2166
2594
  const env = {
2595
+ EVAL_INSTALL_TOKEN: installToken,
2167
2596
  EVAL_CLONE_URL: this.cloneUrl(definition, options),
2168
2597
  EVAL_PIP_INDEX_URL: this.pipIndexUrl(options),
2169
2598
  GIT_TERMINAL_PROMPT: "0",
2170
2599
  EVAL_GIT_PROXY: this.proxyValue(options.gitProxyUrl),
2171
2600
  EVAL_PIP_PROXY: this.proxyValue(options.pipProxyUrl),
2172
- EVAL_CIFAR_PROXY: this.proxyValue(options.cifarProxyUrl)
2601
+ EVAL_CIFAR_PROXY: this.proxyValue(options.cifarProxyUrl),
2602
+ EVAL_CIFAR_URL: this.cifarUrl(options)
2173
2603
  };
2174
2604
  const command = definition.needsSetup ? ["bash", "setup.sh"] : [
2175
2605
  "bash",
2176
2606
  "-c",
2177
2607
  "true"
2178
2608
  ];
2609
+ const job = this.jobs.start({
2610
+ kind: "install",
2611
+ task: taskId,
2612
+ workspaceId,
2613
+ command,
2614
+ cwd: checked.path,
2615
+ env,
2616
+ onCancel: cleanupInstall,
2617
+ onFailure: cleanupInstall,
2618
+ onSuccess: () => {
2619
+ if (definition.needsSetup) this.cleanupWorkspace(checked.path);
2620
+ return { prompt: this.promptNotice(this.guard.promptFor(checked.path, this.materialZip(taskId), definition)) };
2621
+ }
2622
+ });
2623
+ if (job.status === "failed") cleanupInstall();
2179
2624
  return {
2180
2625
  ok: true,
2181
- jobId: this.jobs.start({
2182
- kind: "install",
2183
- task: taskId,
2184
- workspaceId,
2185
- command,
2186
- cwd: checked.path,
2187
- env,
2188
- onSuccess: () => {
2189
- if (definition.needsSetup) this.cleanupWorkspace(checked.path);
2190
- const runId = this.newRunId();
2191
- const prompt = this.guard.promptFor(checked.path, this.materialZip(taskId), definition);
2192
- const run = {
2193
- runId,
2194
- task: definition.id,
2195
- workspaceId,
2196
- workspacePath: checked.path,
2197
- sessionId: null,
2198
- createdAt: Date.now(),
2199
- startedAt: null,
2200
- endedAt: null,
2201
- status: "prepared",
2202
- prompt,
2203
- timeLimitMinutes: this.guard.configuration.timeLimitMinutes
2204
- };
2205
- this.guard.registerRun(run);
2206
- return {
2207
- runId,
2208
- prompt
2209
- };
2210
- }
2211
- }).id
2626
+ jobId: job.id
2212
2627
  };
2213
2628
  }
2214
2629
  /** Start a single-workspace evaluation job. */
@@ -2222,11 +2637,16 @@ var TaskService = class {
2222
2637
  ok: false,
2223
2638
  error: checked.error
2224
2639
  };
2640
+ if (!this.guard.active) return {
2641
+ ok: false,
2642
+ error: "请先启用评测守护。"
2643
+ };
2225
2644
  const script = path.join(packageRoot(), "eval", taskId + ".py");
2226
2645
  if (!fs.existsSync(script)) return {
2227
2646
  ok: false,
2228
2647
  error: "评测脚本不存在:" + script
2229
2648
  };
2649
+ const evalWorkspace = (taskId === "neural-pendulum" ? this.stageHostWorkspace(workspaceId) : void 0) ?? checked.path;
2230
2650
  return {
2231
2651
  ok: true,
2232
2652
  jobId: this.jobs.start({
@@ -2239,17 +2659,24 @@ var TaskService = class {
2239
2659
  "utf8",
2240
2660
  script,
2241
2661
  "--workspace",
2242
- checked.path
2662
+ evalWorkspace
2243
2663
  ],
2244
2664
  cwd: packageRoot(),
2245
2665
  env: {
2246
2666
  PYTHONPATH: "",
2247
2667
  PYTHONHOME: "",
2248
- EVAL_CIFAR_PROXY: this.proxyValue(this.guard.configuration.cifarProxyUrl)
2668
+ EVAL_CIFAR_PROXY: this.proxyValue(this.guard.configuration.cifarProxyUrl),
2669
+ EVAL_CIFAR_URL: this.cifarUrl(this.guard.configuration),
2670
+ EVAL_SUBMISSIONS_ROOT: path.join(guardRoot(), "submissions", workspaceId.replace(/[^A-Za-z0-9_.-]/g, "_"))
2249
2671
  }
2250
2672
  }).id
2251
2673
  };
2252
2674
  }
2675
+ /** Add the optional shared-machine notice to a task prompt. */
2676
+ promptNotice(prompt) {
2677
+ if (!this.guard.configuration.parallelEval) return prompt;
2678
+ return prompt + "\n\n注意:当前机器可能同时运行其他 agent 的评测/训练任务。运行环境的 CPU、内存、IO 与可用并发会因其他 agent 的行为而波动;请自行适应这种波动,不要假设环境稳定,也不要依赖固定墙钟时间或固定可用资源来判断进度。";
2679
+ }
2253
2680
  /** Copy the packaged workspace.zip to a stable material path for install/manual use. */
2254
2681
  materialZip(taskId = TANK_TASK) {
2255
2682
  const definition = TASK_DEFINITIONS[taskId];
@@ -2300,6 +2727,7 @@ var TaskService = class {
2300
2727
  if (definition.prepare !== void 0) definition.prepare(checked.path);
2301
2728
  if (definition.needsSetup) this.cleanupWorkspace(checked.path);
2302
2729
  } catch (error) {
2730
+ if (checked.empty) this.wipeInstallWorkspace(checked.path);
2303
2731
  return {
2304
2732
  ok: false,
2305
2733
  workspaceId,
@@ -2307,25 +2735,9 @@ var TaskService = class {
2307
2735
  error: "任务准备失败:" + (error instanceof Error ? error.message : String(error))
2308
2736
  };
2309
2737
  }
2310
- const runId = this.newRunId();
2311
- const prompt = promptOverride ?? this.guard.promptFor(checked.path, this.materialZip(taskId), definition);
2312
- const run = {
2313
- runId,
2314
- task: definition.id,
2315
- workspaceId,
2316
- workspacePath: checked.path,
2317
- sessionId: null,
2318
- createdAt: Date.now(),
2319
- startedAt: null,
2320
- endedAt: null,
2321
- status: "prepared",
2322
- prompt,
2323
- timeLimitMinutes: this.guard.configuration.timeLimitMinutes
2324
- };
2325
- this.guard.registerRun(run);
2738
+ const prompt = this.promptNotice(promptOverride ?? this.guard.promptFor(checked.path, this.materialZip(taskId), definition));
2326
2739
  return {
2327
2740
  ok: true,
2328
- runId,
2329
2741
  workspaceId,
2330
2742
  workspacePath: checked.path,
2331
2743
  prompt
@@ -2336,7 +2748,7 @@ var TaskService = class {
2336
2748
  const definition = TASK_DEFINITIONS[taskId];
2337
2749
  if (definition === void 0) return "";
2338
2750
  const workspace = this.registry.get(workspaceId);
2339
- return this.guard.promptFor(workspace?.path ?? "", this.materialZip(taskId), definition);
2751
+ return this.promptNotice(this.guard.promptFor(workspace?.path ?? "", this.materialZip(taskId), definition));
2340
2752
  }
2341
2753
  /** Filesystem paths for the manual channel. */
2342
2754
  manualPaths(workspaceId, taskId = TANK_TASK) {
@@ -2348,7 +2760,7 @@ var TaskService = class {
2348
2760
  const workspace = this.registry.get(workspaceId);
2349
2761
  return {
2350
2762
  source: this.materialZip(taskId),
2351
- prompt: this.guard.promptFor(workspace?.path ?? "", this.materialZip(taskId), definition)
2763
+ prompt: this.promptNotice(this.guard.promptFor(workspace?.path ?? "", this.materialZip(taskId), definition))
2352
2764
  };
2353
2765
  }
2354
2766
  /** A unique run id. */
@@ -2448,7 +2860,9 @@ function registerRoutes(ctx, guard, tasks) {
2448
2860
  customPipUrl: typeof body.customPipUrl === "string" ? body.customPipUrl : config.customPipUrl,
2449
2861
  gitProxyUrl: typeof body.gitProxyUrl === "string" ? body.gitProxyUrl : config.gitProxyUrl,
2450
2862
  pipProxyUrl: typeof body.pipProxyUrl === "string" ? body.pipProxyUrl : config.pipProxyUrl,
2451
- cifarProxyUrl: typeof body.cifarProxyUrl === "string" ? body.cifarProxyUrl : config.cifarProxyUrl
2863
+ cifarProxyUrl: typeof body.cifarProxyUrl === "string" ? body.cifarProxyUrl : config.cifarProxyUrl,
2864
+ cifarMirror: typeof body.cifarMirror === "string" ? body.cifarMirror : config.cifarMirror,
2865
+ customCifarUrl: typeof body.customCifarUrl === "string" ? body.customCifarUrl : config.customCifarUrl
2452
2866
  }));
2453
2867
  }
2454
2868
  });
@@ -2568,6 +2982,70 @@ function registerRoutes(ctx, guard, tasks) {
2568
2982
  });
2569
2983
  }
2570
2984
  //#endregion
2985
+ //#region src/submission-tools.ts
2986
+ const OUTPUT_SCHEMA = {
2987
+ type: "object",
2988
+ additionalProperties: true,
2989
+ properties: {
2990
+ ok: { type: "boolean" },
2991
+ submission: { type: "string" },
2992
+ files: { type: "number" },
2993
+ stage: { type: "string" },
2994
+ t_can: { type: "number" },
2995
+ completed: { type: "boolean" },
2996
+ error: { type: "string" }
2997
+ },
2998
+ required: ["ok"]
2999
+ };
3000
+ function render(_args, value) {
3001
+ return [{
3002
+ type: "text",
3003
+ text: JSON.stringify(value)
3004
+ }];
3005
+ }
3006
+ function resolve(guard, exec) {
3007
+ if (!guard.active) throw new Error("eval guard is disabled");
3008
+ const run = guard.runForSession(exec.agent?.id);
3009
+ if (run === void 0) throw new Error("eval task run is not active for this session");
3010
+ if (run.task !== "neural-pendulum") throw new Error("this tool is only available for the neural-pendulum task");
3011
+ return run;
3012
+ }
3013
+ /** Install the task tools into one agent's own scoped registry. */
3014
+ function registerAgentSubmissionTools(agent, guard, tasks) {
3015
+ const tools = agent.ctx?.tools;
3016
+ if (tools === void 0) return;
3017
+ const submit = {
3018
+ name: "eval_submit",
3019
+ description: "Submit the current workspace source as a new immutable snapshot for the active Neural-Pendulum run. Submission itself does not change the score.",
3020
+ parameters: {
3021
+ type: "object",
3022
+ properties: {}
3023
+ },
3024
+ output: {
3025
+ schema: OUTPUT_SCHEMA,
3026
+ render
3027
+ },
3028
+ timeoutMs: 6e4,
3029
+ execute: async (_args, exec) => tasks.hostSubmit(resolve(guard, exec))
3030
+ };
3031
+ const test = {
3032
+ name: "eval_test",
3033
+ description: "Run a host-side evaluation of the latest submitted snapshot and return stage, t_can, completed and error. Only successful tests update the score.",
3034
+ parameters: {
3035
+ type: "object",
3036
+ properties: {}
3037
+ },
3038
+ output: {
3039
+ schema: OUTPUT_SCHEMA,
3040
+ render
3041
+ },
3042
+ timeoutMs: 9e5,
3043
+ execute: async (_args, exec) => tasks.hostTest(resolve(guard, exec))
3044
+ };
3045
+ tools.register(submit);
3046
+ tools.register(test);
3047
+ }
3048
+ //#endregion
2571
3049
  //#region src/index.ts
2572
3050
  /** Stable Cordis plugin name. */
2573
3051
  const name = "eval-guard";
@@ -2612,7 +3090,12 @@ function apply(ctx, config) {
2612
3090
  on: (event, listener) => child.on(event, listener)
2613
3091
  };
2614
3092
  const guard = new Guard(ctx, services, config, TANK_DEFINITION);
2615
- registerRoutes(ctx, guard, new TaskService(guard, services.workspaceRegistry));
3093
+ const tasks = new TaskService(guard, services.workspaceRegistry);
3094
+ guard.onRunAssociated = (agent, run) => {
3095
+ registerAgentSubmissionTools(agent, guard, tasks);
3096
+ };
3097
+ guard.onAutoRun = (agent, cwd) => tasks.autoBind(agent, cwd);
3098
+ registerRoutes(ctx, guard, tasks);
2616
3099
  child.inject(["settings"], (settingsCtx) => {
2617
3100
  const settings = settingsCtx.get("settings");
2618
3101
  settingsCtx.effect(() => settings.configure({ auto: false }, ctx.fiber), "eval-guard: settings page");