dsh-eval-guard 0.7.2 → 0.7.4

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 (63) hide show
  1. package/ISSUES.md +239 -0
  2. package/README.md +48 -18
  3. package/assets/neural-cifar/workspace.zip +0 -0
  4. package/assets/neural-cpp/workspace.zip +0 -0
  5. package/assets/neural-fp32/workspace.zip +0 -0
  6. package/assets/neural-pendulum/SPEC.md +43 -35
  7. package/assets/neural-pendulum/TASK.md +139 -33
  8. package/assets/neural-pendulum/rl-feasibility/README.md +60 -0
  9. package/assets/neural-pendulum/rl-feasibility/RESULTS.md +370 -0
  10. package/assets/neural-pendulum/rl-feasibility/evaluate.py +107 -0
  11. package/assets/neural-pendulum/rl-feasibility/finalize_run.py +168 -0
  12. package/assets/neural-pendulum/rl-feasibility/models.py +124 -0
  13. package/assets/neural-pendulum/rl-feasibility/obs.py +205 -0
  14. package/assets/neural-pendulum/rl-feasibility/plot_selected.py +55 -0
  15. package/assets/neural-pendulum/rl-feasibility/reference_controller.py +112 -0
  16. package/assets/neural-pendulum/rl-feasibility/results/curves_selected.png +0 -0
  17. package/assets/neural-pendulum/rl-feasibility/results/reference_controller.json +417 -0
  18. package/assets/neural-pendulum/rl-feasibility/results/summary.json +1599 -0
  19. package/assets/neural-pendulum/rl-feasibility/results/table.md +39 -0
  20. package/assets/neural-pendulum/rl-feasibility/sim.py +324 -0
  21. package/assets/neural-pendulum/rl-feasibility/summarize.py +175 -0
  22. package/assets/neural-pendulum/rl-feasibility/task.py +342 -0
  23. package/assets/neural-pendulum/rl-feasibility/test_contract.py +188 -0
  24. package/assets/neural-pendulum/rl-feasibility/train_ppo.py +403 -0
  25. package/assets/neural-pendulum/rl-feasibility/train_sac.py +406 -0
  26. package/assets/neural-pendulum/sim.cpp +13 -2
  27. package/assets/neural-pendulum/sim_eval +0 -0
  28. package/assets/neural-pendulum/workspace.zip +0 -0
  29. package/eval/README.md +19 -9
  30. package/eval/bomberman.py +5 -3
  31. package/eval/commit0.py +10 -3
  32. package/eval/common.py +169 -5
  33. package/eval/ctf.py +4 -2
  34. package/eval/featurebench.py +12 -8
  35. package/eval/land.py +4 -2
  36. package/eval/legacy/bomberman.py +5 -2
  37. package/eval/legacy/ctf.py +5 -2
  38. package/eval/legacy/featurebench.py +55 -15
  39. package/eval/legacy/land.py +6 -3
  40. package/eval/legacy/swe_bench_pro.py +57 -16
  41. package/eval/legacy/tank.py +2 -2
  42. package/eval/legacy/warehouse.py +4 -4
  43. package/eval/neural-cpp.py +9 -6
  44. package/eval/neural-fp32.py +17 -5
  45. package/eval/swe-bench-pro.py +11 -7
  46. package/eval/tank.py +11 -2
  47. package/eval/warehouse.py +17 -1
  48. package/lib/client/index.js +56 -29
  49. package/lib/client/index.js.map +1 -1
  50. package/lib/index.js +883 -298
  51. package/native/dsh-eval-supervisor +0 -0
  52. package/native/supervisor.c +232 -36
  53. package/neural-cifar-eval/README.md +5 -0
  54. package/neural-cifar-eval/common.py +13 -0
  55. package/neural-cifar-eval/ops_eval.py +17 -5
  56. package/neural-cifar-eval/run_eval.py +42 -32
  57. package/neural-pendulum-eval/README.md +62 -0
  58. package/neural-pendulum-eval/ops_eval.py +603 -0
  59. package/neural-pendulum-eval/run_eval.py +218 -137
  60. package/neural-pendulum-eval/sim_eval +0 -0
  61. package/package.json +7 -11
  62. package/assets/neural-pendulum/hidden/test_infra.cpp +0 -631
  63. package/assets/neural-pendulum/include/nn/nn.hpp +0 -88
package/lib/index.js CHANGED
@@ -2,9 +2,9 @@ import { createRequire } from "node:module";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
5
6
  import os from "node:os";
6
7
  import { fileURLToPath } from "node:url";
7
- import { execFileSync, spawn } from "node:child_process";
8
8
  import { createHash } from "node:crypto";
9
9
  //#region src/config.ts
10
10
  /**
@@ -145,7 +145,16 @@ const PIP_MIRRORS = [
145
145
  * profile that lacks a plugin simply never mentions that plugin's tools.
146
146
  * @module dsh-eval-guard/policy
147
147
  */
148
- /** DSH-first default section. Only names that exist in the registry are surfaced. */
148
+ /**
149
+ * DSH-first default section. Only names that exist in the registry are surfaced
150
+ * in prompts; the enforcement set keeps every name so a tool installed later
151
+ * cannot slip through with an empty user document.
152
+ *
153
+ * The last block covers tools that can otherwise escape a one-shot evaluation:
154
+ * goal rounds and schedules keep working after the wall-clock deadline, and the
155
+ * drawio/canvas tools write host files directly (bypassing the fs sandbox) and
156
+ * can cross workspace boundaries.
157
+ */
149
158
  const BASE_BLACKLIST = [
150
159
  "web_search",
151
160
  "web_fetch",
@@ -181,7 +190,20 @@ const BASE_BLACKLIST = [
181
190
  "cordis_inspect_list",
182
191
  "cordis_inspect_query",
183
192
  "exit_plan_mode",
184
- "ask_user_question"
193
+ "ask_user_question",
194
+ "create_goal",
195
+ "get_goal",
196
+ "update_goal",
197
+ "schedule_create",
198
+ "schedule_list",
199
+ "schedule_update",
200
+ "schedule_delete",
201
+ "drawio_edit",
202
+ "drawio_render",
203
+ "drawio_template",
204
+ "drawio_validate",
205
+ "canvas_state",
206
+ "view_canvas"
185
207
  ];
186
208
  /** Third-party tools that reach an external service, subagent, or agent channel. */
187
209
  const PLUGIN_BLACKLIST_PATTERNS = [
@@ -197,7 +219,12 @@ const PLUGIN_BLACKLIST_PATTERNS = [
197
219
  /^computer_/,
198
220
  /^ssh_/,
199
221
  /^e2b_/,
200
- /^mcp_/
222
+ /^mcp_/,
223
+ /^drawio_/,
224
+ /^schedule_/,
225
+ /^(?:create|get|update)_goal$/,
226
+ /^canvas_/,
227
+ /^view_canvas$/
201
228
  ];
202
229
  /** Parse one blacklist document into ordered tool names (comments removed). */
203
230
  function parseBlacklist(text) {
@@ -229,11 +256,13 @@ function generateDefaultBlacklist(installed) {
229
256
  lines.push(...plugin);
230
257
  return lines.join("\n");
231
258
  }
232
- /** Whether a name belongs to the always-present DSH base section. */
233
- function isBaseBlacklistName(name) {
234
- return BASE_BLACKLIST.includes(name);
235
- }
236
- /** Parse the forbidden-paths document. Invalid lines are ignored. */
259
+ /**
260
+ * Parse the forbidden-paths document. Invalid lines are ignored.
261
+ *
262
+ * A line containing a glob character is treated as the prefix before the first
263
+ * wildcard, so a line like /home/<anything>/secret means "everything under
264
+ * /home"; this is deliberately conservative (never narrower than written).
265
+ */
237
266
  function parsePathRules(text) {
238
267
  const rules = [];
239
268
  for (const raw of text.split(/\r?\n/)) {
@@ -245,8 +274,10 @@ function parsePathRules(text) {
245
274
  line = line.slice(1).trim();
246
275
  }
247
276
  if (!line.startsWith("/")) continue;
277
+ const wildcard = line.search(/[*?]/);
278
+ if (wildcard >= 0) line = line.slice(0, wildcard);
248
279
  if (line.length > 1 && line.endsWith("/")) line = line.slice(0, -1);
249
- if (line.includes("*") || line.includes("?")) continue;
280
+ if (line === "") line = "/";
250
281
  rules.push({
251
282
  allow,
252
283
  path: path.normalize(line)
@@ -309,12 +340,30 @@ function applyRules(real, rules, base) {
309
340
  }
310
341
  /** Decide one absolute path for read or write access. */
311
342
  function checkPath(real, workspace, write, rules) {
312
- if (under$1(real, workspace)) return true;
313
- if (isNoTraceDevice(real) && !write) return true;
314
- if (write) return isNoTraceDevice(real);
315
- return applyRules(real, rules, baseReadAllowed(real, workspace));
343
+ const inWorkspace = under$1(real, workspace);
344
+ if (!write) {
345
+ if (inWorkspace) return true;
346
+ if (isNoTraceDevice(real)) return true;
347
+ return applyRules(real, rules, baseReadAllowed(real, workspace));
348
+ }
349
+ const base = inWorkspace || isNoTraceDevice(real);
350
+ if (!base) return false;
351
+ return applyRules(real, rules, base) && base;
316
352
  }
317
- /** Path-bearing argument keys audited for DSH-built-in and dsh-ops tools. */
353
+ /** Keys whose value is task content, not another path to audit. */
354
+ const CONTENT_KEYS = /* @__PURE__ */ new Set([
355
+ "content",
356
+ "old_string",
357
+ "new_string",
358
+ "text",
359
+ "code",
360
+ "prompt",
361
+ "description",
362
+ "justification",
363
+ "new_source",
364
+ "file_text"
365
+ ]);
366
+ /** Path-bearing argument keys audited for DSH-built-in and installed tools. */
318
367
  const PATH_KEYS = /* @__PURE__ */ new Set([
319
368
  "path",
320
369
  "file",
@@ -330,7 +379,16 @@ const PATH_KEYS = /* @__PURE__ */ new Set([
330
379
  "target",
331
380
  "input",
332
381
  "output",
333
- "script_path"
382
+ "script_path",
383
+ "source_path",
384
+ "source_paths",
385
+ "file_paths",
386
+ "paths",
387
+ "input_path",
388
+ "output_path",
389
+ "image",
390
+ "images",
391
+ "out"
334
392
  ]);
335
393
  /** Tools whose path arguments are read-only accesses. */
336
394
  const READ_TOOLS = /* @__PURE__ */ new Set([
@@ -342,14 +400,18 @@ const READ_TOOLS = /* @__PURE__ */ new Set([
342
400
  "load_workspace_dependencies",
343
401
  "ops_inspect_local_file",
344
402
  "ops_grep",
345
- "ops_glob"
403
+ "ops_glob",
404
+ "drawio_validate"
346
405
  ]);
347
406
  /** Tools whose path arguments are mutations. */
348
407
  const WRITE_TOOLS = /* @__PURE__ */ new Set([
349
408
  "write",
350
409
  "edit",
351
410
  "str_replace_editor",
352
- "ops_replace"
411
+ "ops_replace",
412
+ "drawio_edit",
413
+ "drawio_render",
414
+ "drawio_template"
353
415
  ]);
354
416
  /** Tools we deliberately do not inspect: the process sandbox owns their effects. */
355
417
  const PROCESS_TOOLS = /* @__PURE__ */ new Set([
@@ -365,18 +427,24 @@ const PROCESS_TOOLS = /* @__PURE__ */ new Set([
365
427
  "ops_job_list",
366
428
  "ops_job_kill"
367
429
  ]);
430
+ /** Whether one tool name takes file paths this module knows how to audit. */
431
+ function isAuditedTool(name) {
432
+ return READ_TOOLS.has(name) || WRITE_TOOLS.has(name);
433
+ }
368
434
  /** Collect every path-looking string under a known path key. */
369
435
  function collectPathArgs(value, out = []) {
370
436
  if (Array.isArray(value)) {
371
437
  for (const item of value) collectPathArgs(item, out);
372
438
  return out;
373
439
  }
374
- if (value !== null && typeof value === "object") for (const [key, child] of Object.entries(value)) if (PATH_KEYS.has(key)) {
375
- if (typeof child === "string") out.push(child);
376
- else if (Array.isArray(child)) for (const item of child) if (typeof item === "string") out.push(item);
377
- else collectPathArgs(item, out);
378
- else collectPathArgs(child, out);
379
- } else collectPathArgs(child, out);
440
+ if (value !== null && typeof value === "object") {
441
+ for (const [key, child] of Object.entries(value)) if (PATH_KEYS.has(key)) {
442
+ if (typeof child === "string") out.push(child);
443
+ else if (Array.isArray(child)) for (const item of child) if (typeof item === "string") out.push(item);
444
+ else collectPathArgs(item, out);
445
+ else collectPathArgs(child, out);
446
+ } else if (!CONTENT_KEYS.has(key)) collectPathArgs(child, out);
447
+ }
380
448
  return out;
381
449
  }
382
450
  /** Audit the path arguments of one DSH/ops tool call. Non-targeted tools pass. */
@@ -386,12 +454,12 @@ function auditToolCall(name, args, workspace, rules) {
386
454
  tool: name,
387
455
  write: false
388
456
  };
389
- if (!READ_TOOLS.has(name) && !WRITE_TOOLS.has(name)) return {
457
+ if (!isAuditedTool(name)) return {
390
458
  allow: true,
391
459
  tool: name,
392
460
  write: false
393
461
  };
394
- const write = WRITE_TOOLS.has(name);
462
+ const write = name === "str_replace_editor" && (args !== null && typeof args === "object" && !Array.isArray(args) ? args : void 0)?.["command"] === "view" ? false : WRITE_TOOLS.has(name);
395
463
  const paths = collectPathArgs(args);
396
464
  for (const target of paths) {
397
465
  const real = resolveCheckPath(target, workspace);
@@ -400,7 +468,7 @@ function auditToolCall(name, args, workspace, rules) {
400
468
  tool: name,
401
469
  path: real,
402
470
  write,
403
- reason: write ? "只允许修改当前工作区;该写入已被阻止。" : "只允许读取当前工作区与系统路径;该读取已被阻止。"
471
+ reason: write ? "只允许修改当前工作区;该写入已被阻止。" : "只允许读取当前工作区与系统只读文件;该读取已被阻止。"
404
472
  };
405
473
  }
406
474
  return {
@@ -427,6 +495,35 @@ function packageRoot() {
427
495
  function supervisorPath() {
428
496
  return path.join(packageRoot(), "native", "dsh-eval-supervisor");
429
497
  }
498
+ /**
499
+ * Probe the two host capabilities the guard depends on: a usable bwrap binary
500
+ * and unprivileged user namespaces. Returns a human-readable failure, or
501
+ * undefined when confinement is expected to work.
502
+ */
503
+ function probeConfinement() {
504
+ const version = spawnSync("bwrap", ["--version"], {
505
+ timeout: 2e3,
506
+ encoding: "utf8"
507
+ });
508
+ if (version.error !== void 0 || version.status !== 0) return "bubblewrap (bwrap) is unavailable; refusing to enable without process confinement";
509
+ const probe = spawnSync("bwrap", [
510
+ "--unshare-all",
511
+ "--die-with-parent",
512
+ "--ro-bind",
513
+ "/",
514
+ "/",
515
+ "--dev",
516
+ "/dev",
517
+ "--proc",
518
+ "/proc",
519
+ "--",
520
+ "/bin/true"
521
+ ], {
522
+ timeout: 5e3,
523
+ encoding: "utf8"
524
+ });
525
+ if (probe.error !== void 0 || probe.status !== 0) return "bubblewrap cannot create an unprivileged user namespace on this host; refusing to enable without process confinement";
526
+ }
430
527
  /** Launcher-provided facts; nothing here is a fixed install path. */
431
528
  let runtimeHome;
432
529
  let runtimeProfileDir;
@@ -481,6 +578,24 @@ const SYSTEM_READ_FILES = [
481
578
  "/run/utmpx",
482
579
  "/var/log/wtmp"
483
580
  ];
581
+ /** Host environment variables that are safe to expose to a confined command. */
582
+ const SAFE_ENV_KEYS = [
583
+ "USER",
584
+ "LOGNAME",
585
+ "SHELL",
586
+ "TZ",
587
+ "SSL_CERT_FILE",
588
+ "SSL_CERT_DIR",
589
+ "REQUESTS_CA_BUNDLE",
590
+ "CURL_CA_BUNDLE",
591
+ "NODE_EXTRA_CA_CERTS",
592
+ "GIT_SSL_CAINFO"
593
+ ];
594
+ /** Whether `root` is `target` or an ancestor of it. */
595
+ function containsPath(root, target) {
596
+ const relative = path.relative(root, target);
597
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
598
+ }
484
599
  /** Python stdlib/venv directories that may live next to a workspace. */
485
600
  function pythonRuntimeDirs(workspace) {
486
601
  const dirs = /* @__PURE__ */ new Set();
@@ -508,14 +623,39 @@ function ancestorsOf(target) {
508
623
  return out;
509
624
  }
510
625
  /** Resolve one package directory from the host launch world. */
626
+ /** Likely roots for the running DSH installation (app, checkout, or profile). */
627
+ function runtimeRoots() {
628
+ const roots = /* @__PURE__ */ new Set();
629
+ const starts = [fileURLToPath(new URL("..", import.meta.url)), process.cwd()];
630
+ if (process.argv[1] !== void 0) starts.push(path.dirname(process.argv[1]));
631
+ if (process.env.DSH_CHECKOUT !== void 0 && process.env.DSH_CHECKOUT !== "") starts.push(process.env.DSH_CHECKOUT);
632
+ for (const start of starts) {
633
+ let current = path.resolve(start);
634
+ for (let depth = 0; depth < 8; depth += 1) {
635
+ roots.add(current);
636
+ const parent = path.dirname(current);
637
+ if (parent === current) break;
638
+ current = parent;
639
+ }
640
+ }
641
+ return [...roots];
642
+ }
643
+ /** Checkout-relative guesses for DSH source layouts. */
644
+ const CHECKOUT_PATHS = {
645
+ "@deepseek-ai/dsh-ptc-runtime-node": ["packages/ptc-runtime/ptc-runtime-node"],
646
+ "@deepseek-ai/dsh-subprocess": ["packages/subprocess/subprocess"]
647
+ };
511
648
  function packageDir(name) {
512
649
  const candidates = [];
513
- try {
514
- const require = createRequire(path.join(process.cwd(), "noop.js"));
515
- candidates.push(path.dirname(require.resolve(name + "/package.json")));
516
- } catch {}
517
- if (name === "@deepseek-ai/dsh-ptc-runtime-node") candidates.push(path.join(process.cwd(), "packages", "ptc-runtime", "ptc-runtime-node"));
518
- if (name === "@deepseek-ai/dsh-subprocess") candidates.push(path.join(process.cwd(), "packages", "subprocess", "subprocess"));
650
+ const roots = runtimeRoots();
651
+ for (const root of roots) {
652
+ try {
653
+ const require = createRequire(path.join(root, "noop.js"));
654
+ candidates.push(path.dirname(require.resolve(name + "/package.json")));
655
+ } catch {}
656
+ candidates.push(path.join(root, "node_modules", name));
657
+ for (const relative of CHECKOUT_PATHS[name] ?? []) candidates.push(path.join(root, relative));
658
+ }
519
659
  for (const modules of profileNodeModules()) candidates.push(path.join(modules, name));
520
660
  for (const candidate of candidates) try {
521
661
  if (fs.statSync(path.join(candidate, "package.json")).isFile()) return fs.realpathSync.native(candidate);
@@ -650,6 +790,7 @@ function writePolicy(workspace, rules) {
650
790
  }
651
791
  for (const rule of rules) lines.push(line("rule", (rule.allow ? "allow " : "deny ") + rule.path));
652
792
  if (!(workspace === "/tmp" || workspace.startsWith("/tmp/"))) lines.push(line("tmpbind", path.join(workspace, ".tmp")));
793
+ else lines.push(line("tmpfs", "/tmp"));
653
794
  fs.writeFileSync(file, lines.join("\n") + "\n", {
654
795
  encoding: "utf8",
655
796
  mode: 384
@@ -657,7 +798,7 @@ function writePolicy(workspace, rules) {
657
798
  return file;
658
799
  }
659
800
  /** Build the bwrap profile arguments for one workspace. */
660
- function bwrapArgs(workspace, cwd, rules, runtime) {
801
+ function bwrapArgs(workspace, rules, runtime) {
661
802
  const args = [
662
803
  "--unshare-all",
663
804
  "--die-with-parent",
@@ -681,7 +822,6 @@ function bwrapArgs(workspace, cwd, rules, runtime) {
681
822
  args.push("--dir", "/var/log");
682
823
  }
683
824
  for (const file of systemFiles) args.push("--ro-bind", file, file);
684
- args.push("--bind", workspace, workspace);
685
825
  const tmp = path.join(workspace, ".tmp");
686
826
  ensureDir(tmp);
687
827
  const underHostTmp = workspace === "/tmp" || workspace.startsWith("/tmp/");
@@ -689,7 +829,9 @@ function bwrapArgs(workspace, cwd, rules, runtime) {
689
829
  if (underHostTmp) {
690
830
  args.push("--tmpfs", "/tmp");
691
831
  tmpdirForChild = tmp;
692
- } else args.push("--bind", tmp, "/tmp");
832
+ }
833
+ args.push("--bind", workspace, workspace);
834
+ if (!underHostTmp) args.push("--bind", tmp, "/tmp");
693
835
  const supervisorFile = supervisorPath();
694
836
  if (fs.existsSync(supervisorFile)) args.push("--ro-bind", supervisorFile, supervisorFile);
695
837
  args.push("--ro-bind", runtime.node, runtime.node);
@@ -712,7 +854,12 @@ function bwrapArgs(workspace, cwd, rules, runtime) {
712
854
  if (!rule.allow) continue;
713
855
  try {
714
856
  const real = fs.realpathSync.native(rule.path);
715
- if (real !== workspace && !real.startsWith(workspace + "/")) args.push("--ro-bind", real, real);
857
+ if (containsPath(real, workspace) || containsPath(workspace, real)) continue;
858
+ if (containsPath(real, guardRoot()) || containsPath(guardRoot(), real)) continue;
859
+ if (containsPath(real, dshHome()) || containsPath(dshHome(), real)) continue;
860
+ if (real === "/tmp" || real.startsWith("/tmp/")) continue;
861
+ if (SYSTEM_READ_DIRS.some((system) => real === system || real.startsWith(system + "/"))) continue;
862
+ args.push("--ro-bind", real, real);
716
863
  } catch {}
717
864
  }
718
865
  const home = path.join(workspace, ".home");
@@ -725,21 +872,11 @@ function bwrapArgs(workspace, cwd, rules, runtime) {
725
872
  config,
726
873
  data
727
874
  ]) ensureDir(dir);
728
- for (const name of [
729
- "LD_LIBRARY_PATH",
730
- "PYTHONPATH",
731
- "MVCAM_SDK_PATH",
732
- "AMENT_PREFIX_PATH",
733
- "COLCON_PREFIX_PATH",
734
- "CMAKE_PREFIX_PATH",
735
- "ROS_DISTRO",
736
- "ROS_VERSION",
737
- "ROS_PYTHON_VERSION",
738
- "NODE_OPTIONS",
739
- "NODE_PATH",
740
- "PYTHONSTARTUP",
741
- "PYTHONHOME"
742
- ]) args.push("--unsetenv", name);
875
+ args.push("--clearenv");
876
+ for (const name of SAFE_ENV_KEYS) {
877
+ const value = process.env[name];
878
+ if (value !== void 0 && value !== "") args.push("--setenv", name, value);
879
+ }
743
880
  args.push("--setenv", "PATH", "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin");
744
881
  args.push("--setenv", "HOME", home);
745
882
  args.push("--setenv", "TMPDIR", tmpdirForChild);
@@ -750,13 +887,16 @@ function bwrapArgs(workspace, cwd, rules, runtime) {
750
887
  args.push("--setenv", "LC_ALL", "C.UTF-8");
751
888
  args.push("--setenv", "TERM", "dumb");
752
889
  args.push("--setenv", "UV_USE_IO_URING", "0");
890
+ args.push("--setenv", "PIP_DISABLE_PIP_VERSION_CHECK", "1");
891
+ args.push("--setenv", "PIP_NO_INPUT", "1");
753
892
  return args;
754
893
  }
755
894
  /**
756
895
  * Wrap one argv in the strict sandbox.
757
896
  * @param argv - the exact command the consumer was about to spawn.
758
897
  * @param workspace - session cwd.
759
- * @param cwd - working directory the consumer will use (same as workspace in practice).
898
+ * @param cwd - explicit working directory override; the empty string lets the
899
+ * supervisor use its inherited cwd when that cwd lies inside the workspace.
760
900
  * @param rules - ordered forbidden-path rules.
761
901
  * @param auditFile - host-side audit path the supervisor appends to.
762
902
  */
@@ -771,6 +911,7 @@ function wrapSandbox(argv, workspace, cwd, rules, auditFile) {
771
911
  ensureDir(path.join(workspace, ".config"));
772
912
  ensureDir(path.join(workspace, ".local", "share"));
773
913
  const policyFile = writePolicy(workspace, rules);
914
+ const args = bwrapArgs(workspace, rules, runtimeFiles());
774
915
  return {
775
916
  argv: [
776
917
  supervisor,
@@ -778,9 +919,10 @@ function wrapSandbox(argv, workspace, cwd, rules, auditFile) {
778
919
  policyFile,
779
920
  "--audit",
780
921
  auditFile,
922
+ ...cwd === "" ? [] : ["--cwd", cwd],
781
923
  "--",
782
924
  "bwrap",
783
- ...bwrapArgs(workspace, cwd, rules, runtimeFiles()),
925
+ ...args,
784
926
  "--",
785
927
  ...argv
786
928
  ],
@@ -795,6 +937,39 @@ function wrapSandbox(argv, workspace, cwd, rules, auditFile) {
795
937
  * tool policy, wall-clock limit, run records, and audit events.
796
938
  * @module dsh-eval-guard/guard
797
939
  */
940
+ /** The cwd a session is confined to, or undefined when the header has none. */
941
+ function sessionCwd(session) {
942
+ const cwd = session?.header?.cwd;
943
+ return typeof cwd === "string" && cwd !== "" ? cwd : void 0;
944
+ }
945
+ /**
946
+ * Whether a session is a subagent child.
947
+ *
948
+ * DSH uses `origin: 'subagent'` and/or `delegationDepth > 0` for subagents.
949
+ * `parentSession` alone is NOT a subagent marker: a user-visible fork of a
950
+ * normal conversation also sets `parentSession` (seed lineage), and treating
951
+ * that as a subagent would leave the whole fork unguarded.
952
+ */
953
+ function sessionIsSubagent(session) {
954
+ const header = session?.header;
955
+ if (header === void 0) return false;
956
+ if (header.origin === "subagent") return true;
957
+ return typeof header.delegationDepth === "number" && header.delegationDepth > 0;
958
+ }
959
+ /** The session this one was forked/delegated from, if any. */
960
+ function sessionParent(session) {
961
+ const parent = session?.header?.parentSession;
962
+ return typeof parent === "string" && parent !== "" ? parent : void 0;
963
+ }
964
+ /** Display title for the enable warning; DSH's header has none, so fall back. */
965
+ function sessionTitle(session) {
966
+ const title = session?.header?.title;
967
+ if (typeof title === "string" && title !== "") return title;
968
+ const cwd = sessionCwd(session);
969
+ return cwd === void 0 ? void 0 : path.basename(cwd);
970
+ }
971
+ /** How long an idle session keeps its deadline alive before it is settled. */
972
+ const IDLE_GRACE_MS = 5e3;
798
973
  /** Event ring + run audit writer. */
799
974
  var Guard = class {
800
975
  ctx;
@@ -811,6 +986,7 @@ var Guard = class {
811
986
  approvalPrev = /* @__PURE__ */ new Map();
812
987
  deadlines = /* @__PURE__ */ new Map();
813
988
  startedAt = /* @__PURE__ */ new Map();
989
+ lastActivity = /* @__PURE__ */ new Map();
814
990
  runs = /* @__PURE__ */ new Map();
815
991
  sessionRuns = /* @__PURE__ */ new Map();
816
992
  offsets = /* @__PURE__ */ new Map();
@@ -818,6 +994,8 @@ var Guard = class {
818
994
  toolCache = [];
819
995
  disposers = [];
820
996
  task;
997
+ lastError;
998
+ enableRetryAt = 0;
821
999
  /** Optional hook invoked when any run binds to an agent. */
822
1000
  onRunAssociated;
823
1001
  /** Optional automatic task binding for every main conversation. */
@@ -892,13 +1070,21 @@ var Guard = class {
892
1070
  if (!this.rawConfig) return;
893
1071
  const next = this.configuration;
894
1072
  const wasEnabled = this.enabled;
895
- if (!wasEnabled && next.enabled) this.enable();
896
- else if (wasEnabled && !next.enabled) this.disable();
1073
+ if (!wasEnabled && next.enabled) {
1074
+ if (Date.now() < this.enableRetryAt) return;
1075
+ this.enable().catch((error) => {
1076
+ this.failEnable(error instanceof Error ? error.message : String(error));
1077
+ });
1078
+ } else if (wasEnabled && !next.enabled) this.disable();
897
1079
  }
898
1080
  /** Whether the guard is active. */
899
1081
  get active() {
900
1082
  return this.enabled;
901
1083
  }
1084
+ /** Why the guard refused to enable, if it did. */
1085
+ get lastFailure() {
1086
+ return this.lastError;
1087
+ }
902
1088
  /** Installed tool names currently visible in this deployment. */
903
1089
  refreshTools() {
904
1090
  try {
@@ -928,31 +1114,31 @@ var Guard = class {
928
1114
  pathRules() {
929
1115
  return parsePathRules(this.configuration.forbiddenPaths);
930
1116
  }
931
- /** Live conversations for the enable warning. */
1117
+ /** Jobs owned by one session (unowned/global jobs must never be attributed). */
1118
+ ownJobs(sessionId) {
1119
+ return (this.services.jobs?.list(sessionId) ?? []).filter((job) => job.owner === sessionId).filter((job) => job.status === "running" || job.status === "stopping");
1120
+ }
1121
+ /** Live main conversations for the enable warning. */
932
1122
  runningSessions() {
933
1123
  const out = [];
934
1124
  for (const agent of this.services.agents.list()) {
935
- const jobs = this.services.jobs?.list(agent.id) ?? [];
1125
+ if (!this.isMain(agent.session)) continue;
1126
+ const jobs = this.ownJobs(agent.id);
936
1127
  const running = agent.status === "running";
937
- const jobCount = jobs.filter((job) => job.status === "running" || job.status === "stopping").length;
938
- if (!running && jobCount === 0) continue;
1128
+ if (!running && jobs.length === 0) continue;
939
1129
  out.push({
940
1130
  sessionId: agent.id,
941
- title: agent.session.header?.meta?.title ?? agent.id,
942
- cwd: agent.session.header?.meta?.cwd ?? "",
1131
+ title: sessionTitle(agent.session) ?? agent.id,
1132
+ cwd: sessionCwd(agent.session) ?? "",
943
1133
  running,
944
- jobs: jobCount
1134
+ jobs: jobs.length
945
1135
  });
946
1136
  }
947
1137
  return out;
948
1138
  }
949
- /** Whether a session is a user main conversation (not a subagent or plugin child). */
1139
+ /** Whether a session is a user conversation (not a subagent/plugin child). */
950
1140
  isMain(session) {
951
- if (session === void 0) return false;
952
- const meta = session.header?.meta;
953
- if (meta?.origin === "subagent") return false;
954
- if (meta?.parentSession !== void 0 || meta?.delegationDepth !== void 0) return false;
955
- return true;
1141
+ return session !== void 0 && !sessionIsSubagent(session);
956
1142
  }
957
1143
  /** Approvals are auto-rejected while the guard is active. */
958
1144
  forceApproval(session) {
@@ -960,8 +1146,10 @@ var Guard = class {
960
1146
  if (approval === void 0) return;
961
1147
  const agent = this.services.agents.get(session.id);
962
1148
  if (agent === void 0) return;
963
- if (!this.approvalPrev.has(session.id)) this.approvalPrev.set(session.id, approval.effectivePolicy(session));
964
- approval.setPolicy(agent, "never");
1149
+ try {
1150
+ if (!this.approvalPrev.has(session.id)) this.approvalPrev.set(session.id, approval.effectivePolicy(session));
1151
+ approval.setPolicy(agent, "never");
1152
+ } catch {}
965
1153
  }
966
1154
  /** Restore the policy that existed before the guard touched this session. */
967
1155
  restoreApproval(sessionId) {
@@ -972,19 +1160,36 @@ var Guard = class {
972
1160
  const approval = this.services.approval;
973
1161
  if (agent !== void 0 && approval !== void 0) approval.setPolicy(agent, previous);
974
1162
  }
1163
+ /** Record an enable-time refusal and back off before retrying. */
1164
+ failEnable(error) {
1165
+ this.lastError = error;
1166
+ this.enableRetryAt = Date.now() + 5e3;
1167
+ this.record({
1168
+ kind: "notice",
1169
+ detail: "评测守护启用失败:" + error
1170
+ });
1171
+ try {
1172
+ this.ctx.logger.warn("eval-guard: %s", error);
1173
+ } catch {}
1174
+ return {
1175
+ ok: false,
1176
+ error
1177
+ };
1178
+ }
975
1179
  /** Turn the guard on. */
976
1180
  async enable() {
977
1181
  if (this.enabled) return { ok: true };
978
1182
  const sandbox = this.services.sandbox;
979
1183
  const policy = this.services.sandboxPolicy;
980
- if (sandbox === void 0 || policy === void 0) return {
981
- ok: false,
982
- error: "sandbox service is unavailable; refusing to enable without process confinement"
983
- };
984
- if (!fs.existsSync(supervisorPath())) return {
985
- ok: false,
986
- error: "native supervisor is not built; run npm run build:native"
987
- };
1184
+ if (sandbox === void 0 || policy === void 0) return this.failEnable("sandbox service is unavailable; refusing to enable without process confinement");
1185
+ if (!fs.existsSync(supervisorPath())) return this.failEnable("native supervisor is not built; run npm run build:native");
1186
+ try {
1187
+ fs.accessSync(supervisorPath(), fs.constants.X_OK);
1188
+ } catch {
1189
+ return this.failEnable("native supervisor is not executable; run npm run build:native");
1190
+ }
1191
+ const probe = probeConfinement();
1192
+ if (probe !== void 0) return this.failEnable(probe);
988
1193
  this.sandboxObject = sandbox;
989
1194
  this.policyObject = policy;
990
1195
  this.origConfine = sandbox.confine.bind(sandbox);
@@ -994,12 +1199,12 @@ var Guard = class {
994
1199
  if (!guard.enabled) return guard.origConfine(argv, executionPolicy, signal);
995
1200
  const sessionId = typeof executionPolicy["sessionId"] === "string" ? executionPolicy["sessionId"] : void 0;
996
1201
  const session = sessionId === void 0 ? void 0 : guard.services.sessions.get(sessionId);
997
- if (session === void 0 || !guard.isMain(session)) return guard.origConfine(argv, executionPolicy, signal);
998
- const workspace = typeof executionPolicy["workspaceRoot"] === "string" ? executionPolicy["workspaceRoot"] : session.header?.meta?.cwd;
999
- if (workspace === void 0 || workspace === "") throw new Error("dsh-eval-guard: session has no workspace");
1202
+ if (session === void 0) return guard.origConfine(argv, executionPolicy, signal);
1203
+ const workspace = sessionCwd(session);
1204
+ if (workspace === void 0) throw new Error("dsh-eval-guard: session has no workspace");
1000
1205
  const audit = guard.auditFileFor(session.id);
1001
1206
  return {
1002
- argv: wrapSandbox(argv, workspace, workspace, guard.pathRules(), audit).argv,
1207
+ argv: wrapSandbox(argv, workspace, "", guard.pathRules(), audit).argv,
1003
1208
  enforcement: "full",
1004
1209
  denialSignatures: [
1005
1210
  "operation not permitted",
@@ -1012,29 +1217,60 @@ var Guard = class {
1012
1217
  policy.resolve = function resolveHook(request = {}) {
1013
1218
  const resolved = guard.origResolve(request);
1014
1219
  if (!guard.enabled) return resolved;
1015
- const session = request["session"];
1016
- if (session === void 0 || !guard.isMain(session)) return resolved;
1220
+ if (request["session"] === void 0) return resolved;
1017
1221
  return {
1018
1222
  ...resolved,
1019
1223
  mode: "workspace-write"
1020
1224
  };
1021
1225
  };
1022
- this.enabled = true;
1023
- for (const agent of this.services.agents.list()) if (this.isMain(agent.session)) this.forceApproval(agent.session);
1024
- this.record({
1025
- kind: "notice",
1026
- detail: "评测守护已启用。"
1027
- });
1028
- return { ok: true };
1226
+ try {
1227
+ for (const agent of this.services.agents.list()) if (this.isMain(agent.session)) this.forceApproval(agent.session);
1228
+ try {
1229
+ const runtime = runtimeFiles();
1230
+ if (runtime.ptcProcess === void 0 || runtime.ptcControl === void 0) this.record({
1231
+ kind: "notice",
1232
+ detail: "未找到 run_code 运行时(PTC)文件;沙箱内 run_code 可能不可用。"
1233
+ });
1234
+ } catch {
1235
+ this.record({
1236
+ kind: "notice",
1237
+ detail: "无法读取 run_code 运行时(PTC)文件;沙箱内 run_code 可能不可用。"
1238
+ });
1239
+ }
1240
+ this.enabled = true;
1241
+ this.lastError = void 0;
1242
+ this.enableRetryAt = 0;
1243
+ this.record({
1244
+ kind: "notice",
1245
+ detail: "评测守护已启用。"
1246
+ });
1247
+ return { ok: true };
1248
+ } catch (error) {
1249
+ this.enabled = false;
1250
+ this.restoreHooks();
1251
+ return this.failEnable(error instanceof Error ? error.message : String(error));
1252
+ }
1253
+ }
1254
+ /** Restore the original sandbox/policy methods (idempotent). */
1255
+ restoreHooks() {
1256
+ if (this.sandboxObject !== void 0 && this.origConfine !== void 0) this.sandboxObject.confine = this.origConfine;
1257
+ if (this.policyObject !== void 0 && this.origResolve !== void 0) this.policyObject.resolve = this.origResolve;
1258
+ this.sandboxObject = void 0;
1259
+ this.policyObject = void 0;
1260
+ this.origConfine = void 0;
1261
+ this.origResolve = void 0;
1029
1262
  }
1030
1263
  /** Turn the guard off without touching already-running confined processes. */
1031
1264
  disable() {
1265
+ this.enableRetryAt = 0;
1032
1266
  if (!this.enabled) return;
1033
1267
  this.enabled = false;
1034
- if (this.sandboxObject !== void 0 && this.origConfine !== void 0) this.sandboxObject.confine = this.origConfine;
1035
- if (this.policyObject !== void 0 && this.origResolve !== void 0) this.policyObject.resolve = this.origResolve;
1036
- for (const sessionId of [...this.approvalPrev.keys()]) this.restoreApproval(sessionId);
1268
+ this.restoreHooks();
1269
+ for (const sessionId of [...this.approvalPrev.keys()]) try {
1270
+ this.restoreApproval(sessionId);
1271
+ } catch {}
1037
1272
  this.deadlines.clear();
1273
+ this.lastActivity.clear();
1038
1274
  this.record({
1039
1275
  kind: "notice",
1040
1276
  detail: "评测守护已停用。"
@@ -1045,24 +1281,31 @@ var Guard = class {
1045
1281
  const config = this.configuration;
1046
1282
  if (!config.timeLimitEnabled) return;
1047
1283
  this.deadlines.set(sessionId, Date.now() + config.timeLimitMinutes * 6e4);
1284
+ this.lastActivity.set(sessionId, Date.now());
1048
1285
  }
1049
1286
  /** A new main agent appeared. */
1050
1287
  onAgentCreated(agent) {
1051
1288
  if (!this.isMain(agent.session)) return;
1052
1289
  this.knownAgents.add(agent.id);
1053
1290
  if (this.enabled) this.forceApproval(agent.session);
1054
- const cwd = agent.session.header?.meta?.cwd;
1055
- if (cwd !== void 0) {
1056
- if (this.enabled) try {
1291
+ const cwd = sessionCwd(agent.session);
1292
+ if (cwd !== void 0 && this.enabled) {
1293
+ const existingRunId = this.sessionRuns.get(agent.id);
1294
+ const existing = existingRunId === void 0 ? void 0 : this.runs.get(existingRunId);
1295
+ if (existing !== void 0 && (existing.status === "active" || existing.status === "prepared")) return;
1296
+ try {
1057
1297
  this.onAutoRun?.(agent, cwd);
1058
1298
  } catch {}
1059
1299
  }
1060
1300
  }
1061
1301
  /** An agent went away. */
1062
1302
  onAgentDisposed(agent) {
1063
- this.restoreApproval(agent.id);
1303
+ try {
1304
+ this.restoreApproval(agent.id);
1305
+ } catch {}
1064
1306
  this.deadlines.delete(agent.id);
1065
1307
  this.startedAt.delete(agent.id);
1308
+ this.lastActivity.delete(agent.id);
1066
1309
  const runId = this.sessionRuns.get(agent.id);
1067
1310
  if (runId !== void 0) {
1068
1311
  const run = this.runs.get(runId);
@@ -1087,7 +1330,7 @@ var Guard = class {
1087
1330
  run.status = "active";
1088
1331
  run.provider = agent.options?.provider;
1089
1332
  run.model = agent.options?.model;
1090
- run.title = agent.session.header?.meta?.title;
1333
+ run.title = sessionTitle(agent.session);
1091
1334
  this.sessionRuns.set(agent.id, runId);
1092
1335
  this.writeRun(run);
1093
1336
  this.writeUsage(runId, this.zeroUsage(run.startedAt));
@@ -1098,8 +1341,10 @@ var Guard = class {
1098
1341
  /** Session event intake. */
1099
1342
  onSessionEvent(session, event) {
1100
1343
  if (!this.enabled || session === void 0 || !this.isMain(session)) return;
1344
+ this.lastActivity.set(session.id, Date.now());
1101
1345
  const runId = this.sessionRuns.get(session.id);
1102
1346
  const run = runId === void 0 ? void 0 : this.runs.get(runId);
1347
+ const activeRun = run !== void 0 && run.status === "active";
1103
1348
  if (event.type === "user/message") {
1104
1349
  if ((event.data?.source)?.kind === "user") {
1105
1350
  this.startedAt.set(session.id, Date.now());
@@ -1108,44 +1353,51 @@ var Guard = class {
1108
1353
  }
1109
1354
  } else if (event.type === "assistant/message") {
1110
1355
  const usage = event.data?.usage;
1111
- if (usage !== void 0 && runId !== void 0) this.addUsage(runId, usage);
1356
+ if (usage !== void 0 && runId !== void 0 && activeRun) this.addUsage(runId, usage);
1112
1357
  } else if (event.type === "tool/call") {
1113
- if (runId !== void 0) this.addToolCall(runId);
1358
+ if (runId !== void 0 && activeRun) this.addToolCall(runId);
1114
1359
  } else if (event.type === "turn/end") {
1115
- if (runId !== void 0) this.addTurn(runId);
1360
+ if (runId !== void 0 && activeRun) this.addTurn(runId);
1116
1361
  this.maybeComplete(session.id);
1117
1362
  } else if (event.type === "turn/start") {
1118
1363
  if (run !== void 0 && run.startedAt === null) run.startedAt = Date.now();
1119
1364
  }
1120
1365
  }
1121
- /** Clear a turn deadline once no work remains for that session. */
1366
+ /** Clear a turn deadline once no work remains for that session (with grace). */
1122
1367
  maybeComplete(sessionId) {
1123
- const agent = this.services.agents.get(sessionId);
1124
- const jobs = (this.services.jobs?.list(sessionId) ?? []).filter((job) => job.status === "running" || job.status === "stopping");
1125
- if (agent?.status === "running" || jobs.length > 0) return;
1368
+ if (this.services.agents.get(sessionId)?.status === "running" || this.ownJobs(sessionId).length > 0) return;
1369
+ const last = this.lastActivity.get(sessionId) ?? 0;
1370
+ if (Date.now() - last < IDLE_GRACE_MS) return;
1126
1371
  this.deadlines.delete(sessionId);
1127
1372
  }
1128
1373
  /** One-second clock: expire deadlines and tail supervisor audits. */
1129
1374
  tick() {
1130
- this.syncConfig();
1131
- if (!this.enabled) return;
1132
- const now = Date.now();
1133
- for (const [sessionId, deadline] of [...this.deadlines]) {
1134
- if (deadline > now) {
1135
- const agent = this.services.agents.get(sessionId);
1136
- const ownJobs = (this.services.jobs?.list(sessionId) ?? []).some((job) => job.status === "running" || job.status === "stopping");
1137
- const descendantRunning = this.descendants(sessionId).some((id) => {
1138
- if (this.services.agents.get(id)?.status === "running") return true;
1139
- return (this.services.jobs?.list(id) ?? []).some((job) => job.status === "running" || job.status === "stopping");
1140
- });
1141
- if (agent?.status !== "running" && !ownJobs && !descendantRunning) this.deadlines.delete(sessionId);
1142
- continue;
1375
+ try {
1376
+ this.syncConfig();
1377
+ if (!this.enabled) return;
1378
+ const now = Date.now();
1379
+ for (const [sessionId, deadline] of [...this.deadlines]) {
1380
+ if (deadline > now) {
1381
+ const agent = this.services.agents.get(sessionId);
1382
+ const ownJobs = this.ownJobs(sessionId).length > 0;
1383
+ const last = this.lastActivity.get(sessionId) ?? 0;
1384
+ const descendantRunning = this.descendants(sessionId).some((id) => {
1385
+ if (this.services.agents.get(id)?.status === "running") return true;
1386
+ return this.ownJobs(id).length > 0;
1387
+ });
1388
+ if (agent?.status !== "running" && !ownJobs && !descendantRunning && now - last >= IDLE_GRACE_MS) this.deadlines.delete(sessionId);
1389
+ continue;
1390
+ }
1391
+ this.deadlines.delete(sessionId);
1392
+ this.expire(sessionId);
1143
1393
  }
1144
- this.deadlines.delete(sessionId);
1145
- this.expire(sessionId);
1394
+ this.tailAudits();
1395
+ this.cleanupTemp();
1396
+ } catch (error) {
1397
+ try {
1398
+ this.ctx.logger.warn("eval-guard: tick failed: %s", error instanceof Error ? error.message : String(error));
1399
+ } catch {}
1146
1400
  }
1147
- this.tailAudits();
1148
- this.cleanupTemp();
1149
1401
  }
1150
1402
  /** Kill one session's turn, descendants, and jobs. */
1151
1403
  expire(sessionId) {
@@ -1159,9 +1411,11 @@ var Guard = class {
1159
1411
  reason
1160
1412
  });
1161
1413
  } catch {}
1162
- for (const job of this.services.jobs?.list(id) ?? []) if (job.status === "running" || job.status === "stopping") try {
1414
+ for (const job of this.ownJobs(id)) try {
1163
1415
  this.services.jobs?.kill(job.id, id, "评测时间上限");
1164
1416
  } catch {}
1417
+ this.deadlines.delete(id);
1418
+ this.lastActivity.delete(id);
1165
1419
  }
1166
1420
  const runId = this.sessionRuns.get(sessionId);
1167
1421
  if (runId !== void 0) {
@@ -1176,8 +1430,10 @@ var Guard = class {
1176
1430
  sessionId,
1177
1431
  detail: reason
1178
1432
  });
1433
+ return;
1179
1434
  }
1180
- } else this.record({
1435
+ }
1436
+ this.record({
1181
1437
  kind: "timeout",
1182
1438
  sessionId,
1183
1439
  detail: reason
@@ -1185,10 +1441,10 @@ var Guard = class {
1185
1441
  }
1186
1442
  /** Direct and transitive subagent sessions of one main session. */
1187
1443
  descendants(sessionId) {
1188
- const all = this.services.agents.list();
1189
1444
  const children = /* @__PURE__ */ new Map();
1190
- for (const agent of all) {
1191
- const parent = agent.session.header?.meta?.parentSession;
1445
+ for (const agent of this.services.agents.list()) {
1446
+ if (!sessionIsSubagent(agent.session)) continue;
1447
+ const parent = sessionParent(agent.session);
1192
1448
  if (parent === void 0) continue;
1193
1449
  const list = children.get(parent) ?? [];
1194
1450
  list.push(agent.id);
@@ -1208,7 +1464,7 @@ var Guard = class {
1208
1464
  checkTool(exec) {
1209
1465
  if (!this.enabled) return void 0;
1210
1466
  const agent = exec.agent;
1211
- if (agent === void 0 || !this.isMain(agent.session)) return void 0;
1467
+ if (agent === void 0) return void 0;
1212
1468
  const name = String(exec.name);
1213
1469
  if (WAIT_TOOLS.includes(name)) {
1214
1470
  this.record({
@@ -1228,8 +1484,17 @@ var Guard = class {
1228
1484
  });
1229
1485
  return "该工具在本次评测的黑名单中,调用已被阻止。";
1230
1486
  }
1231
- const workspace = agent.session.header?.meta?.cwd;
1232
- if (workspace === void 0 || workspace === "") return void 0;
1487
+ const workspace = sessionCwd(agent.session);
1488
+ if (workspace === void 0) {
1489
+ if (!isAuditedTool(name)) return void 0;
1490
+ this.record({
1491
+ kind: "path-denied",
1492
+ sessionId: agent.id,
1493
+ tool: name,
1494
+ detail: "会话没有工作区,无法审计路径。"
1495
+ });
1496
+ return "评测守护:无法确定会话工作区,文件类工具已被阻止。";
1497
+ }
1233
1498
  const decision = auditToolCall(name, exec.arguments, workspace, this.pathRules());
1234
1499
  if (!decision.allow) {
1235
1500
  this.record({
@@ -1420,13 +1685,7 @@ var Guard = class {
1420
1685
  }
1421
1686
  /** Build the prompt for one task with only installed tools listed. */
1422
1687
  promptFor(workspacePath, source, task = this.task) {
1423
- const present = new Set(this.toolCache);
1424
- const blacklist = parseBlacklist(this.blacklistText()).filter((name) => isBaseBlacklistName(name) || present.has(name));
1425
- return task.prompt(blacklist, {
1426
- source,
1427
- workspacePath,
1428
- minutes: this.configuration.timeLimitMinutes
1429
- });
1688
+ return task.prompt({ minutes: this.configuration.timeLimitMinutes });
1430
1689
  }
1431
1690
  /** The task definition. */
1432
1691
  get definition() {
@@ -1440,6 +1699,45 @@ var Guard = class {
1440
1699
  //#endregion
1441
1700
  //#region src/jobs.ts
1442
1701
  /** Persistent background jobs for task setup and single-result evaluation. */
1702
+ /**
1703
+ * The only host environment variables a setup/evaluation child inherits.
1704
+ *
1705
+ * Evaluation children execute agent-authored build scripts and binaries, so
1706
+ * the ambient DSH environment (credentials, session secrets, proxy settings)
1707
+ * must not leak into them. Everything the task needs is passed explicitly via
1708
+ * the EVAL_* variables in StartOptions.env.
1709
+ */
1710
+ function baseEnvironment(source = process.env) {
1711
+ const keep = [
1712
+ "PATH",
1713
+ "HOME",
1714
+ "USER",
1715
+ "LOGNAME",
1716
+ "SHELL",
1717
+ "LANG",
1718
+ "LANGUAGE",
1719
+ "LC_ALL",
1720
+ "LC_CTYPE",
1721
+ "LC_MESSAGES",
1722
+ "TZ",
1723
+ "TERM",
1724
+ "TMPDIR",
1725
+ "TMP",
1726
+ "TEMP",
1727
+ "SSL_CERT_FILE",
1728
+ "SSL_CERT_DIR",
1729
+ "GIT_SSL_CAINFO",
1730
+ "REQUESTS_CA_BUNDLE",
1731
+ "CURL_CA_BUNDLE",
1732
+ "NODE_EXTRA_CA_CERTS"
1733
+ ];
1734
+ const env = {};
1735
+ for (const key of keep) {
1736
+ const value = source[key];
1737
+ if (value !== void 0) env[key] = value;
1738
+ }
1739
+ return env;
1740
+ }
1443
1741
  function now() {
1444
1742
  return Date.now();
1445
1743
  }
@@ -1461,15 +1759,65 @@ var JobManager = class {
1461
1759
  try {
1462
1760
  const stored = JSON.parse(fs.readFileSync(file, "utf8"));
1463
1761
  if (stored.status === "running" || stored.status === "queued") {
1762
+ if (typeof stored.pid === "number" && stored.pid > 0 && this.stillOurProcess(stored)) try {
1763
+ process.kill(-stored.pid, "SIGTERM");
1764
+ } catch {
1765
+ try {
1766
+ process.kill(stored.pid, "SIGTERM");
1767
+ } catch {}
1768
+ }
1464
1769
  stored.status = "interrupted";
1465
1770
  stored.error = "服务重启,任务已中断";
1466
1771
  stored.endedAt = now();
1467
1772
  this.write(stored);
1773
+ if (stored.kind === "install" && stored.cleanupWorkspace !== void 0) {
1774
+ this.wipeInstallWorkspace(stored.cleanupWorkspace);
1775
+ if (stored.cifarToken !== void 0) this.cleanupCifarLock(stored.cifarToken);
1776
+ }
1468
1777
  }
1469
1778
  this.jobs.set(stored.id, stored);
1470
1779
  } catch {}
1471
1780
  }
1472
1781
  }
1782
+ /** Remove every top-level entry an interrupted install could have created. */
1783
+ wipeInstallWorkspace(workspacePath) {
1784
+ try {
1785
+ for (const entry of fs.readdirSync(workspacePath)) fs.rmSync(path.join(workspacePath, entry), {
1786
+ recursive: true,
1787
+ force: true
1788
+ });
1789
+ } catch {}
1790
+ }
1791
+ /** Remove the CIFAR lock only when it carries this install's token. */
1792
+ cleanupCifarLock(token) {
1793
+ try {
1794
+ const lock = path.join(guardRoot(), "cache", "cifar10", ".download.lock");
1795
+ if (fs.existsSync(lock) && fs.readFileSync(lock, "utf8").includes("token=" + token)) fs.rmSync(lock, { force: true });
1796
+ } catch {}
1797
+ }
1798
+ /** Read the Linux process start time from /proc/<pid>/stat (field 22). */
1799
+ processStartTime(pid) {
1800
+ try {
1801
+ const stat = fs.readFileSync("/proc/" + String(pid) + "/stat", "utf8");
1802
+ const end = stat.lastIndexOf(")");
1803
+ if (end < 0) return void 0;
1804
+ return stat.slice(end + 2).trim().split(/\s+/)[19];
1805
+ } catch {
1806
+ return;
1807
+ }
1808
+ }
1809
+ /** Whether a persisted PID still names the process this job started. */
1810
+ stillOurProcess(stored) {
1811
+ if (stored.pidStartTime !== void 0 && this.processStartTime(stored.pid) !== stored.pidStartTime) return false;
1812
+ if (stored.pidStartTime !== void 0) return true;
1813
+ try {
1814
+ const argv = fs.readFileSync("/proc/" + String(stored.pid) + "/cmdline", "utf8").split("\0").filter(Boolean);
1815
+ if (argv.length === 0) return false;
1816
+ return path.basename(argv[0]) === path.basename(stored.command[0] ?? "");
1817
+ } catch {
1818
+ return false;
1819
+ }
1820
+ }
1473
1821
  dir(idValue) {
1474
1822
  return path.join(this.root, idValue);
1475
1823
  }
@@ -1497,7 +1845,9 @@ var JobManager = class {
1497
1845
  command: options.command,
1498
1846
  cwd: options.cwd,
1499
1847
  logFile,
1500
- resultFile: path.join(directory, "result.json")
1848
+ resultFile: path.join(directory, "result.json"),
1849
+ ...options.cleanupWorkspace === void 0 ? {} : { cleanupWorkspace: options.cleanupWorkspace },
1850
+ ...options.cifarToken === void 0 ? {} : { cifarToken: options.cifarToken }
1501
1851
  };
1502
1852
  this.jobs.set(jobId, job);
1503
1853
  this.write(job);
@@ -1506,7 +1856,7 @@ var JobManager = class {
1506
1856
  child = spawn(options.command[0], options.command.slice(1), {
1507
1857
  cwd: options.cwd,
1508
1858
  env: {
1509
- ...process.env,
1859
+ ...baseEnvironment(),
1510
1860
  ...options.env ?? {},
1511
1861
  PYTHONUNBUFFERED: "1",
1512
1862
  EVAL_JOB_DIR: directory,
@@ -1528,6 +1878,11 @@ var JobManager = class {
1528
1878
  return this.info(job);
1529
1879
  }
1530
1880
  this.children.set(jobId, child);
1881
+ if (child.pid !== void 0) {
1882
+ job.pid = child.pid;
1883
+ job.pidStartTime = this.processStartTime(child.pid);
1884
+ this.write(job);
1885
+ }
1531
1886
  if (options.onCancel !== void 0) this.cancelHandlers.set(jobId, options.onCancel);
1532
1887
  const append = (chunk) => {
1533
1888
  try {
@@ -1536,17 +1891,29 @@ var JobManager = class {
1536
1891
  };
1537
1892
  child.stdout?.on("data", (chunk) => append(chunk));
1538
1893
  child.stderr?.on("data", (chunk) => append(chunk));
1539
- child.on("error", (error) => {
1894
+ let failureNotified = false;
1895
+ const fail = (error) => {
1896
+ if (job.status !== "running" && job.status !== "queued") return;
1540
1897
  job.status = "failed";
1541
- job.error = error.message;
1898
+ job.error = error;
1542
1899
  job.endedAt = now();
1543
1900
  job.updatedAt = now();
1544
1901
  this.write(job);
1902
+ if (failureNotified) return;
1903
+ failureNotified = true;
1904
+ try {
1905
+ options.onFailure?.(this.detail(jobId));
1906
+ } catch {}
1907
+ };
1908
+ child.on("error", (error) => {
1909
+ this.children.delete(jobId);
1910
+ this.cancelHandlers.delete(jobId);
1911
+ fail(error.message);
1545
1912
  });
1546
1913
  child.on("exit", (code, signal) => {
1547
1914
  this.children.delete(jobId);
1548
1915
  this.cancelHandlers.delete(jobId);
1549
- if (job.status === "cancelled") return;
1916
+ if (job.status === "cancelled" || job.status === "failed") return;
1550
1917
  if (code === 0) try {
1551
1918
  const extra = options.onSuccess?.(this.detail(jobId));
1552
1919
  if (extra !== void 0) {
@@ -1564,17 +1931,14 @@ var JobManager = class {
1564
1931
  } catch (error) {
1565
1932
  job.status = "failed";
1566
1933
  job.error = error instanceof Error ? error.message : String(error);
1567
- try {
1568
- options.onFailure?.(this.detail(jobId));
1569
- } catch {}
1570
- }
1571
- else {
1572
- job.status = "failed";
1573
- job.error = signal !== null ? "进程被信号 " + signal + " 终止" : "退出码 " + String(code);
1574
- try {
1575
- options.onFailure?.(this.detail(jobId));
1576
- } catch {}
1934
+ if (!failureNotified) {
1935
+ failureNotified = true;
1936
+ try {
1937
+ options.onFailure?.(this.detail(jobId));
1938
+ } catch {}
1939
+ }
1577
1940
  }
1941
+ else fail(signal !== null ? "进程被信号 " + signal + " 终止" : "退出码 " + String(code));
1578
1942
  job.endedAt = now();
1579
1943
  job.updatedAt = now();
1580
1944
  this.write(job);
@@ -1802,7 +2166,7 @@ const TASKS = [
1802
2166
  {
1803
2167
  id: NEURAL_PENDULUM_TASK,
1804
2168
  label: NEURAL_PENDULUM_TASK_LABEL,
1805
- description: "从零实现纯 C++17 神经网络基础设施,用三个关节角度控制预编译三节摆起摆并稳定保持;评测固定初始状态,统计 t_can。",
2169
+ description: "从零实现纯 C++17 神经网络基础设施(算子数值以评测侧 PyTorch 参考校验),用三个关节角度控制预编译三节摆起摆并稳定保持;评测固定初始状态,统计 t_can。",
1806
2170
  timeLimitMinutes: 1440,
1807
2171
  timeLimitEnabled: false,
1808
2172
  needsSetup: true
@@ -1817,14 +2181,14 @@ const TANK_DEFINITION = {
1817
2181
  timeLimitMinutes: 90,
1818
2182
  timeLimitEnabled: true,
1819
2183
  needsSetup: false,
1820
- prompt() {
2184
+ prompt({ minutes }) {
1821
2185
  return [
1822
2186
  "按照要求编写尽可能强的坦克大战 bot。你的对手是其他顶尖 AI 模型写的 bot,它们可能主动设计战术。基准是一个中等强度、思路常规的 bot,它在正式比赛中是 0 分线;战胜基准不代表足够强,正式对手远强于基准。正式赛是 1 对 1 循环赛,用 Bradley-Terry 模型拟合实力值。",
1823
2187
  "",
1824
2188
  "解压 workspace.zip,阅读 README.md 和 RULES.md,然后开始。",
1825
2189
  "",
1826
- "比赛是连续 90 分钟的单次任务:",
1827
- "- 你有 90 分钟修改 bot,从对话开始计时;到 90 分钟强制停止;",
2190
+ "比赛是连续 " + minutes + " 分钟的单次任务:",
2191
+ "- 你有 " + minutes + " 分钟修改 bot,从对话开始计时;到 " + minutes + " 分钟强制停止;",
1828
2192
  "- 最终只看 bot 的强度,提前交卷没有任何好处;觉得没什么可改了,先换一个思路再试;",
1829
2193
  "- 到点时 workspace/submission/bot.cpp 是什么就交什么:不要让它长时间停在编译不过的状态;",
1830
2194
  "- 允许使用本地工具、对战和复盘;禁止联网,禁止求助其他模型(包括启动子代理:subagent、Task / Agent 工具、后台并行代理之类),除主办方发给你的材料外禁止查看其他选手的资料;",
@@ -1846,7 +2210,7 @@ const WAREHOUSE_DEFINITION = {
1846
2210
  timeLimitMinutes: 90,
1847
2211
  timeLimitEnabled: true,
1848
2212
  needsSetup: false,
1849
- prompt() {
2213
+ prompt({ minutes }) {
1850
2214
  return [
1851
2215
  "这是一场 AI 编程赛。你接手了一个代码混乱的老系统:src/ 里是一个仓储机器人调度仿真系统,功能正确,但运行很慢。你的任务:在输出完全不变的前提下,让它尽可能快。",
1852
2216
  "",
@@ -1855,7 +2219,7 @@ const WAREHOUSE_DEFINITION = {
1855
2219
  "要点(详见 TASK.md):",
1856
2220
  "- 只能修改 src/;编译命令固定为 g++ -O2 -std=c++20 src/*.cpp,只用 C++ 标准库;",
1857
2221
  "- 对任何合法输入(以 tools/check_input.py --full 为准),输出都必须与基线逐字节相同;SPEC.md 是过时的旧文档,一切以基线代码的实际行为为准;",
1858
- "- 本次是连续 90 分钟的单次任务;clock.py 从解压那一刻开始计算,实际计时从对话开始计算,它只能作为参考,请尽快解压;",
2222
+ "- 本次是连续 " + minutes + " 分钟的单次任务;clock.py 从解压那一刻开始计算,实际计时从对话开始计算,它只能作为参考,请尽快解压;",
1859
2223
  "- 只能读写当前文件夹里的文件;可以使用本环境的所有工具,但不能联网搜索;",
1860
2224
  "- 不要向我提问,也不要等待我确认,直到完成为止。",
1861
2225
  "",
@@ -1872,11 +2236,11 @@ const LAND_DEFINITION = {
1872
2236
  timeLimitMinutes: 90,
1873
2237
  timeLimitEnabled: true,
1874
2238
  needsSetup: false,
1875
- prompt() {
2239
+ prompt({ minutes }) {
1876
2240
  return [
1877
2241
  "这是一场正式的 AI 编程对抗赛,你的对手是其他顶尖 AI 模型写的 bot,它们会主动切断你、抢你的地。比赛结果会公开排名,请拿出你的最高水平。",
1878
2242
  "",
1879
- "请解压当前目录的 workspace.zip,进入 workspace 文件夹,按 TASK.md 的要求编写一个尽可能强的圈地对战 bot,保存为 workspace/submission/bot.cpp。时间预算约 60 分钟,请自己把握进度,做完就可以结束;超过 90 分钟会被强制停止。只能读写当前文件夹里的文件,不能联网搜索,不要向我提问。"
2243
+ "请解压当前目录的 workspace.zip,进入 workspace 文件夹,按 TASK.md 的要求编写一个尽可能强的圈地对战 bot,保存为 workspace/submission/bot.cpp。时间预算约 " + Math.min(60, minutes) + " 分钟,请自己把握进度,做完就可以结束;超过 " + minutes + " 分钟会被强制停止。只能读写当前文件夹里的文件,不能联网搜索,不要向我提问。"
1880
2244
  ].join("\n");
1881
2245
  }
1882
2246
  };
@@ -1889,8 +2253,8 @@ const BOMBERMAN_DEFINITION = {
1889
2253
  timeLimitMinutes: 90,
1890
2254
  timeLimitEnabled: true,
1891
2255
  needsSetup: false,
1892
- prompt() {
1893
- return ["按照要求编写尽可能强bot,你的对手是全球其他顶尖ai,可能主动设计战术。基准只是很弱的bot,它在正式比赛为0分线。正式赛会对战多轮,用Bradley-Terry模型拟合实力值。", "解压 workspace.zip,阅读 README.md 和 RULES.md,编写支持2–10人的炸弹人 bot。尽量60分钟完成,90分钟必须交卷。允许本地工具、对战和复盘,禁止联网、调用其他AI或查看其他选手资料。提交 submission/bot.cpp,简述策略。"].join("\n");
2256
+ prompt({ minutes }) {
2257
+ return ["按照要求编写尽可能强bot,你的对手是全球其他顶尖ai,可能主动设计战术。基准只是很弱的bot,它在正式比赛为0分线。正式赛会对战多轮,用Bradley-Terry模型拟合实力值。", "解压 workspace.zip,阅读 README.md 和 RULES.md,编写支持2–10人的炸弹人 bot。尽量" + Math.min(60, minutes) + "分钟完成," + minutes + "分钟必须交卷。允许本地工具、对战和复盘,禁止联网、调用其他AI或查看其他选手资料。提交 submission/bot.cpp,简述策略。"].join("\n");
1894
2258
  }
1895
2259
  };
1896
2260
  /** CTF bot task: original participant prompt, unchanged. */
@@ -1902,28 +2266,17 @@ const CTF_DEFINITION = {
1902
2266
  timeLimitMinutes: 90,
1903
2267
  timeLimitEnabled: true,
1904
2268
  needsSetup: false,
1905
- prompt() {
1906
- 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");
2269
+ prompt({ minutes }) {
2270
+ return ["按照要求编写尽可能强的 bot,你的对手是全球其他顶尖 AI,可能主动设计战术。基准是一个中等强度、思路常规的 bot,它在正式比赛是 0 分线;战胜基准不代表足够强,正式对手远强于基准。正式赛会对战多轮,用 Bradley-Terry 模型拟合实力值。", "解压 workspace.zip,阅读 README.md 和 RULES.md,编写支持 2–15 方的夺旗 bot。你有 " + Math.min(60, minutes) + " 分钟,只看最终 bot 的强度,提前交卷不加分,请充分利用时间持续改进;可以适当超时,但到 " + minutes + " 分钟会强制交卷。允许本地工具、对战和复盘,禁止联网、调用其他 AI 或查看其他选手资料。提交 workspace/submission/bot.cpp,简述策略。"].join("\n");
1907
2271
  }
1908
2272
  };
1909
- /** Run the task's host-side setup script with network access. */
1910
- function runSetup(workspacePath) {
1911
- execFileSync("bash", ["setup.sh"], {
1912
- cwd: workspacePath,
1913
- stdio: "inherit",
1914
- env: {
1915
- ...process.env,
1916
- GIT_TERMINAL_PROMPT: "0"
1917
- }
1918
- });
1919
- }
1920
2273
  /** Commit0 single-library task. */
1921
2274
  const COMMIT0_DEFINITION = {
1922
2275
  id: COMMIT0_TASK,
1923
2276
  label: COMMIT0_TASK_LABEL,
1924
2277
  description: TASKS[5].description,
1925
2278
  asset: path.join(packageRoot(), "assets", "commit0", "workspace.zip"),
1926
- prepare: runSetup,
2279
+ testMaterial: ["tests", "pytest.ini"],
1927
2280
  timeLimitMinutes: 480,
1928
2281
  timeLimitEnabled: false,
1929
2282
  needsSetup: true,
@@ -1954,7 +2307,11 @@ const SWE_BENCH_PRO_DEFINITION = {
1954
2307
  label: SWE_BENCH_PRO_TASK_LABEL,
1955
2308
  description: TASKS[6].description,
1956
2309
  asset: path.join(packageRoot(), "assets", "swe-bench-pro", "workspace.zip"),
1957
- prepare: runSetup,
2310
+ testMaterial: [
2311
+ "test",
2312
+ "pytest.ini",
2313
+ "conftest.py"
2314
+ ],
1958
2315
  timeLimitMinutes: 1440,
1959
2316
  timeLimitEnabled: false,
1960
2317
  needsSetup: true,
@@ -1989,7 +2346,7 @@ const FEATUREBENCH_DEFINITION = {
1989
2346
  label: FEATUREBENCH_TASK_LABEL,
1990
2347
  description: TASKS[7].description,
1991
2348
  asset: path.join(packageRoot(), "assets", "featurebench", "workspace.zip"),
1992
- prepare: runSetup,
2349
+ testMaterial: ["tests", "conftest.py"],
1993
2350
  timeLimitMinutes: 1440,
1994
2351
  timeLimitEnabled: false,
1995
2352
  needsSetup: true,
@@ -2123,16 +2480,18 @@ const NEURAL_PENDULUM_DEFINITION = {
2123
2480
  return [
2124
2481
  "你是一名控制与神经网络工程师。任务:从零实现纯 C++17、仅 CPU、fp32 的神经网络基础设施,并用它训练一个控制器,控制预编译的三节摆从接近全向下起摆,使三根杆全部进入竖直向上容差并尽可能长时间保持。",
2125
2482
  "",
2126
- "请先完整阅读 TASK.md;它是权威规则。硬约束:仅 C++17 标准库、仅 CPU、fp32;不使用 Python/OpenCL/CUDA/BLAS/Eigen/第三方库;必须从提交源码构建出 build/control 与 build/libnn.a;基础设施需实现工作区公开头文件 include/nn/nn.hpp 的契约,隐藏测试由评测侧独立编译链接。",
2483
+ "请先完整阅读 TASK.md;它是权威规则。硬约束:仅 C++17 标准库、仅 CPU、fp32;不使用 Python/OpenCL/CUDA/BLAS/Eigen/第三方库;必须从提交源码构建出 build/nn_task 与 build/control。",
2127
2484
  "",
2128
- "环境细节:joint1 与 joint3 驱动,joint2 被动;控制频率 100 Hz;每回合最多 3000 步(30 s),达到上限后继续 step 返回 {\"error\":\"episode_finished\"},必须 reset。环境只返回三个关节角度(弧度,不除 pi),不返回 reward/loss/cost/done;环境不提供杆长、质量、惯量、角速度或 tip。训练 reset 带小范围随机初始角度和角速度;评测使用固定初始状态,无随机扰动。",
2485
+ "环境细节:joint1 与 joint3 驱动,joint2 被动;控制频率 100 Hz;每回合最多 3000 步(30 s),达到上限后继续 step 返回 {\"error\":\"episode_finished\"},必须 reset。theta=0 表示竖直向下、theta=pi 表示竖直向上;观测 a_i=wrap(theta_i-pi),即三个关节角度(弧度,不除 pi)。环境不返回 reward/loss/cost/done;不提供杆长、质量、惯量、角速度或 tip。训练 reset 带小范围随机初始角度和角速度;评测使用固定初始状态,无随机扰动。",
2129
2486
  "",
2130
2487
  "优化目标:从接近全向下起摆,使三根杆全部进入 0.05 rad 的竖直向上容差,并尽可能长时间保持;评测指标为 t_can。损失函数、奖励、优化器、网络结构和训练方法由你自行设计,不规定任何训练方案。",
2131
2488
  "",
2132
- "评测会从干净副本重新构建提交源码,编译并运行宿主侧隐藏基础设施测试,然后在固定初始状态上驱动 build/control,统计连续全向上保持时间 t_can。不要把任何未提交的二进制、临时构建产物或外部进程当作交付依赖。",
2489
+ "注意:评测侧用 PyTorch 计算算子参考值,但工作区/候选运行时不提供 PyTorch 或 numpy,也没有网络,无法 pip 安装;开发阶段请用 C++ 或 Python 标准库/独立 double 实现做数值对照。",
2490
+ "",
2491
+ "评测会从干净副本重新构建提交源码,先运行与 Neural-FP32/Neural-CIFAR 相同的算子数值评测:评测侧用 PyTorch 在相同数据上计算参考值,逐操作比较加权平均相对误差并对照阈值,任一算子不可执行、崩溃、超时、输出缺失或误差超阈值即判为基础设施失败;然后在固定初始状态上驱动 build/control,统计连续全向上保持时间 t_can。不要把任何未提交的二进制、临时构建产物或外部进程当作交付依赖。",
2133
2492
  "使用宿主工具 eval_submit 提交当前源码快照,使用 eval_test 请求宿主评测最新提交。提交快照、事件、测试结果和最终分数全部保存在工作区之外的宿主目录;不要尝试读取或修改这些数据,也不要依赖任何工作区内的测试结果文件。最终评测使用最新提交快照,按宿主事件时间积分计算分数。",
2134
2493
  "",
2135
- "工作方式:先完成最小可构建的基础设施和 control 协议;再持续做控制/训练实验并记录可复现结果;最终保证工作区从零构建、隐藏基础设施测试通过、control 可被评测驱动,并留下 NOTES.md/RESULTS.md。",
2494
+ "工作方式:先完成最小可构建的基础设施和 control 协议;再持续做控制/训练实验并记录可复现结果;最终保证工作区从零构建、build/nn_task 算子数值评测通过、control 可被评测驱动,并留下 NOTES.md/RESULTS.md。",
2136
2495
  "",
2137
2496
  "不要向我提问,也不要等待确认,独立完成。"
2138
2497
  ].join("\n");
@@ -2153,7 +2512,47 @@ const TASK_DEFINITIONS = {
2153
2512
  [NEURAL_CIFAR_TASK]: NEURAL_CIFAR_DEFINITION,
2154
2513
  [NEURAL_PENDULUM_TASK]: NEURAL_PENDULUM_DEFINITION
2155
2514
  };
2156
- var TaskService = class {
2515
+ /** SHA-256 of one file. */
2516
+ function sha256File(file) {
2517
+ const hash = createHash("sha256");
2518
+ hash.update(fs.readFileSync(file));
2519
+ return hash.digest("hex");
2520
+ }
2521
+ /** Order-stable SHA-256 of a directory tree (relative paths + contents). */
2522
+ function sha256Tree(dir) {
2523
+ const hash = createHash("sha256");
2524
+ const walk = (current) => {
2525
+ for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
2526
+ const full = path.join(current, entry.name);
2527
+ hash.update(path.relative(dir, full));
2528
+ if (entry.isDirectory()) walk(full);
2529
+ else if (entry.isFile()) hash.update(fs.readFileSync(full));
2530
+ else if (entry.isSymbolicLink()) hash.update("link:" + fs.readlinkSync(full));
2531
+ }
2532
+ };
2533
+ walk(dir);
2534
+ return hash.digest("hex");
2535
+ }
2536
+ /** The venv site-packages directories that exist under one workspace venv. */
2537
+ function sitePackageDirs(venv) {
2538
+ const out = [];
2539
+ for (const lib of ["lib", "lib64"]) {
2540
+ const base = path.join(venv, lib);
2541
+ let entries = [];
2542
+ try {
2543
+ entries = fs.readdirSync(base, { withFileTypes: true });
2544
+ } catch {
2545
+ continue;
2546
+ }
2547
+ for (const entry of entries) {
2548
+ if (!entry.isDirectory() || !entry.name.startsWith("python")) continue;
2549
+ const site = path.join(base, entry.name, "site-packages");
2550
+ if (fs.existsSync(site)) out.push(site);
2551
+ }
2552
+ }
2553
+ return out;
2554
+ }
2555
+ var TaskService = class TaskService {
2157
2556
  guard;
2158
2557
  registry;
2159
2558
  jobs;
@@ -2196,7 +2595,14 @@ var TaskService = class {
2196
2595
  ".submissions",
2197
2596
  "node_modules",
2198
2597
  "__pycache__",
2199
- ".venv"
2598
+ ".venv",
2599
+ ".tmp",
2600
+ ".home",
2601
+ ".cache",
2602
+ ".config",
2603
+ ".local",
2604
+ "data",
2605
+ "work"
2200
2606
  ]);
2201
2607
  let count = 0;
2202
2608
  const walk = (dir) => {
@@ -2265,13 +2671,85 @@ var TaskService = class {
2265
2671
  files
2266
2672
  };
2267
2673
  }
2268
- async runHostEval(workspaceId) {
2674
+ /** Map an evaluator result that has no explicit stage (older evaluator builds). */
2675
+ inferStage(value, error) {
2676
+ if (value.infra_ok === true) return value.success === true ? "completed" : "scoring";
2677
+ const text = error.toLowerCase();
2678
+ if (text.includes("cmake") || text.includes("build")) return "compile";
2679
+ if (text.includes("infrastructure") || text.includes("operator") || text.includes("mre")) return "infra";
2680
+ if (text.includes("controller") || text.includes("torque")) return "controller";
2681
+ return "simulation";
2682
+ }
2683
+ /** The target hold time used by the neural-pendulum scoring integral. */
2684
+ static FINAL_T_TGT = 10;
2685
+ /** One test event's instantaneous score, mirroring the task specification. */
2686
+ static eventScore(event) {
2687
+ const stage = String(event["stage"] ?? "");
2688
+ if (stage !== "scoring" && stage !== "completed" && event["valid"] !== true) return -1;
2689
+ const value = Number(event["t_can"] ?? 0);
2690
+ if (!Number.isFinite(value)) return -1;
2691
+ return (Math.min(Math.max(value, 0), TaskService.FINAL_T_TGT) - TaskService.FINAL_T_TGT) / TaskService.FINAL_T_TGT;
2692
+ }
2693
+ /**
2694
+ * Host-side authoritative final score: integral of the per-test score over
2695
+ * time, applying every test event at its REQUEST time and stopping at the
2696
+ * first completed request. Mirrors neural-pendulum-eval/run_eval.py, but the
2697
+ * host owns it because only the host knows the request timestamp and the
2698
+ * current test's outcome before the next test arrives.
2699
+ */
2700
+ static finalScoreFromEvents(startedAt, events, now) {
2701
+ const tests = events.filter((item) => (item["event"] ?? item["type"]) === "test");
2702
+ if (tests.length === 0) return -1 * Math.max(0, now - startedAt);
2703
+ let doneAt;
2704
+ let previous = startedAt;
2705
+ let current = -1;
2706
+ let total = 0;
2707
+ for (const item of tests) {
2708
+ const raw = Number(item["time"] ?? item["at"] ?? previous);
2709
+ const at = Number.isFinite(raw) ? Math.max(raw, previous) : previous;
2710
+ total += current * (at - previous);
2711
+ current = TaskService.eventScore(item);
2712
+ previous = at;
2713
+ if (item["completed"] === true) {
2714
+ doneAt = at;
2715
+ break;
2716
+ }
2717
+ }
2718
+ total += current * Math.max(0, (doneAt ?? now) - previous);
2719
+ return total;
2720
+ }
2721
+ /** Read host test events; malformed lines are ignored. */
2722
+ readHostEvents(workspaceId) {
2723
+ const file = this.hostEvents(workspaceId);
2724
+ if (!fs.existsSync(file)) return [];
2725
+ const out = [];
2726
+ for (const line of fs.readFileSync(file, "utf8").split("\n")) {
2727
+ if (line.trim() === "") continue;
2728
+ try {
2729
+ const value = JSON.parse(line);
2730
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) out.push(value);
2731
+ } catch {}
2732
+ }
2733
+ return out;
2734
+ }
2735
+ /** The task start time recorded at first submission, falling back to the run creation time. */
2736
+ hostStartedAt(workspaceId, run) {
2737
+ try {
2738
+ const meta = JSON.parse(fs.readFileSync(path.join(this.hostDir(workspaceId), "meta.json"), "utf8"));
2739
+ const value = Number(meta.started_at);
2740
+ if (Number.isFinite(value) && value > 0) return value;
2741
+ } catch {}
2742
+ return run.createdAt / 1e3;
2743
+ }
2744
+ /** @param startedAtSeconds - task start in epoch seconds (from host meta). */
2745
+ async runHostEval(workspaceId, startedAtSeconds) {
2269
2746
  const latest = this.hostLatestSubmission(workspaceId);
2270
2747
  if (latest === void 0) return {
2271
2748
  stage: "no_submission",
2272
2749
  t_can: 0,
2273
2750
  completed: false,
2274
- error: ""
2751
+ error: "",
2752
+ final_score: 0
2275
2753
  };
2276
2754
  const temp = fs.mkdtempSync(path.join(guardRoot(), "pend-eval-"));
2277
2755
  try {
@@ -2281,16 +2759,14 @@ var TaskService = class {
2281
2759
  const events = this.hostEvents(workspaceId);
2282
2760
  fs.writeFileSync(path.join(temp, ".eval", "events.jsonl"), fs.existsSync(events) ? fs.readFileSync(events) : "", "utf8");
2283
2761
  fs.writeFileSync(path.join(temp, ".eval", "meta.json"), JSON.stringify({
2284
- started_at: Date.now() / 1e3,
2762
+ started_at: startedAtSeconds,
2285
2763
  t_tgt: 10
2286
2764
  }), "utf8");
2287
2765
  const resultFile = path.join(temp, "result.json");
2288
2766
  const script = path.join(packageRoot(), "neural-pendulum-eval", "run_eval.py");
2289
2767
  const env = {
2290
- ...process.env,
2291
- EVAL_RESULT_FILE: resultFile,
2292
- PYTHONPATH: "",
2293
- PYTHONHOME: ""
2768
+ ...baseEnvironment(),
2769
+ EVAL_RESULT_FILE: resultFile
2294
2770
  };
2295
2771
  const child = spawn("python3", [
2296
2772
  "-X",
@@ -2298,35 +2774,61 @@ var TaskService = class {
2298
2774
  script,
2299
2775
  "--workspace",
2300
2776
  temp
2301
- ], { env });
2777
+ ], {
2778
+ env,
2779
+ detached: true
2780
+ });
2302
2781
  let stderr = "";
2303
2782
  child.stderr?.on("data", (chunk) => {
2304
2783
  stderr += String(chunk);
2305
2784
  });
2785
+ let timedOut = false;
2306
2786
  await new Promise((resolve, reject) => {
2307
- child.on("error", reject);
2308
- child.on("close", () => resolve());
2787
+ const timer = setTimeout(() => {
2788
+ timedOut = true;
2789
+ try {
2790
+ if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
2791
+ } catch {}
2792
+ try {
2793
+ child.kill("SIGKILL");
2794
+ } catch {}
2795
+ resolve();
2796
+ }, 12e5);
2797
+ child.on("error", (error) => {
2798
+ clearTimeout(timer);
2799
+ reject(error);
2800
+ });
2801
+ child.on("close", () => {
2802
+ clearTimeout(timer);
2803
+ resolve();
2804
+ });
2309
2805
  });
2806
+ if (timedOut) return {
2807
+ stage: "infra",
2808
+ t_can: 0,
2809
+ completed: false,
2810
+ error: "evaluation timed out after 20 minutes",
2811
+ final_score: 0
2812
+ };
2310
2813
  if (!fs.existsSync(resultFile)) return {
2311
- stage: "controller",
2814
+ stage: "infra",
2312
2815
  t_can: 0,
2313
2816
  completed: false,
2314
- error: stderr || "evaluator failed"
2817
+ error: stderr || "evaluator failed",
2818
+ final_score: 0
2315
2819
  };
2316
2820
  const value = JSON.parse(fs.readFileSync(resultFile, "utf8"));
2317
2821
  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";
2822
+ const rawStage = String(value.stage ?? "");
2823
+ const stage = rawStage !== "" ? rawStage : this.inferStage(value, error);
2824
+ const tCan = Number(value.t_can ?? 0);
2825
+ const finalScore = Number(value.final_score ?? 0);
2325
2826
  return {
2326
2827
  stage,
2327
- t_can: Number(value.t_can ?? 0),
2328
- completed: value.success === true,
2329
- error
2828
+ t_can: Number.isFinite(tCan) ? tCan : 0,
2829
+ completed: stage === "completed" && value.success === true,
2830
+ error,
2831
+ final_score: Number.isFinite(finalScore) ? finalScore : 0
2330
2832
  };
2331
2833
  } finally {
2332
2834
  fs.rmSync(temp, {
@@ -2343,11 +2845,12 @@ var TaskService = class {
2343
2845
  stage: "completed",
2344
2846
  error: "task already completed"
2345
2847
  };
2848
+ const requestedAt = Date.now() / 1e3;
2346
2849
  const latest = this.hostLatestSubmission(run.workspaceId);
2347
2850
  if (latest === void 0) {
2348
2851
  this.appendHostEvent(run.workspaceId, {
2349
2852
  event: "test",
2350
- time: Date.now() / 1e3,
2853
+ time: requestedAt,
2351
2854
  submission_id: "",
2352
2855
  stage: "no_submission",
2353
2856
  t_can: 0,
@@ -2362,20 +2865,29 @@ var TaskService = class {
2362
2865
  error: ""
2363
2866
  };
2364
2867
  }
2365
- const value = await this.runHostEval(run.workspaceId);
2868
+ const startedAt = this.hostStartedAt(run.workspaceId, run);
2869
+ const value = await this.runHostEval(run.workspaceId, startedAt);
2366
2870
  const stage = String(value.stage ?? "simulation");
2367
2871
  const tCan = Number(value.t_can ?? 0);
2368
- const completed = stage === "completed" && Number.isFinite(tCan);
2369
- this.appendHostEvent(run.workspaceId, {
2872
+ const completed = stage === "completed" && Number.isFinite(tCan) && tCan >= 10;
2873
+ const event = {
2370
2874
  event: "test",
2371
- time: Date.now() / 1e3,
2875
+ time: requestedAt,
2372
2876
  submission_id: latest.id,
2373
2877
  stage,
2374
2878
  t_can: tCan,
2375
2879
  completed,
2376
2880
  error: String(value.error ?? "")
2377
- });
2378
- if (completed) fs.writeFileSync(done, String(Date.now() / 1e3), "utf8");
2881
+ };
2882
+ const finalScore = TaskService.finalScoreFromEvents(startedAt, [...this.readHostEvents(run.workspaceId), event], Date.now() / 1e3);
2883
+ event["final_score"] = finalScore;
2884
+ this.appendHostEvent(run.workspaceId, event);
2885
+ if (completed) fs.writeFileSync(done, JSON.stringify({
2886
+ at: requestedAt,
2887
+ stage,
2888
+ t_can: tCan,
2889
+ final_score: finalScore
2890
+ }) + "\n", "utf8");
2379
2891
  return {
2380
2892
  ok: true,
2381
2893
  stage,
@@ -2386,17 +2898,28 @@ var TaskService = class {
2386
2898
  }
2387
2899
  /** Build a temporary workspace containing the host-side authoritative state. */
2388
2900
  stageHostWorkspace(workspaceId) {
2389
- if (this.hostLatestSubmission(workspaceId) === void 0) return void 0;
2901
+ const latest = this.hostLatestSubmission(workspaceId);
2902
+ if (latest === void 0) return void 0;
2390
2903
  const temp = fs.mkdtempSync(path.join(guardRoot(), "pend-eval-view-"));
2391
2904
  const state = path.join(temp, ".eval");
2392
2905
  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 });
2906
+ fs.cpSync(latest.path, path.join(state, "submissions", latest.id), { recursive: true });
2395
2907
  const events = this.hostEvents(workspaceId);
2396
2908
  fs.writeFileSync(path.join(state, "events.jsonl"), fs.existsSync(events) ? fs.readFileSync(events) : "", "utf8");
2397
2909
  const meta = path.join(this.hostDir(workspaceId), "meta.json");
2910
+ let fallbackStart = Date.now() / 1e3;
2911
+ try {
2912
+ for (const line of (fs.existsSync(events) ? fs.readFileSync(events, "utf8") : "").split("\n")) {
2913
+ if (line.trim() === "") continue;
2914
+ const time = Number(JSON.parse(line).time);
2915
+ if (Number.isFinite(time) && time > 0) {
2916
+ fallbackStart = time;
2917
+ break;
2918
+ }
2919
+ }
2920
+ } catch {}
2398
2921
  fs.writeFileSync(path.join(state, "meta.json"), fs.existsSync(meta) ? fs.readFileSync(meta) : JSON.stringify({
2399
- started_at: Date.now() / 1e3,
2922
+ started_at: fallbackStart,
2400
2923
  t_tgt: 10
2401
2924
  }), "utf8");
2402
2925
  return temp;
@@ -2455,6 +2978,67 @@ var TaskService = class {
2455
2978
  if (index === "custom") return (options.customPipUrl ?? "").trim() || "https://pypi.org/simple";
2456
2979
  return PIP_MIRRORS.find((item) => item.id === index)?.url ?? "https://pypi.org/simple";
2457
2980
  }
2981
+ /** Host-side pristine material for one workspace (tests, venv manifest). */
2982
+ baselineDir(workspaceId) {
2983
+ return path.join(guardRoot(), "task-baseline", workspaceId.replace(/[^A-Za-z0-9_.-]/g, "_"));
2984
+ }
2985
+ /**
2986
+ * Snapshot the test material a benchmark task ships with, so an agent cannot
2987
+ * pass by editing or weakening the tests. Runs once after a successful
2988
+ * install; the evaluator restores it before scoring.
2989
+ */
2990
+ snapshotTestMaterial(definition, workspaceId, workspacePath) {
2991
+ const names = definition.testMaterial ?? [];
2992
+ if (names.length === 0) return;
2993
+ const target = this.baselineDir(workspaceId);
2994
+ fs.mkdirSync(target, { recursive: true });
2995
+ for (const name of names) {
2996
+ const source = path.join(workspacePath, name);
2997
+ if (!fs.existsSync(source)) continue;
2998
+ const destination = path.join(target, name);
2999
+ fs.rmSync(destination, {
3000
+ recursive: true,
3001
+ force: true
3002
+ });
3003
+ fs.cpSync(source, destination, {
3004
+ recursive: true,
3005
+ dereference: false
3006
+ });
3007
+ }
3008
+ fs.writeFileSync(path.join(target, "manifest.json"), JSON.stringify({
3009
+ task: definition.id,
3010
+ names
3011
+ }, null, 2) + "\n", "utf8");
3012
+ }
3013
+ /**
3014
+ * Record a fingerprint of the venv's control plane (python target, pyvenv.cfg,
3015
+ * .pth files, pytest packages). The evaluator refuses to trust a venv whose
3016
+ * fingerprint no longer matches, which closes the "replace bin/python or
3017
+ * patch pytest" forgery path.
3018
+ */
3019
+ snapshotVenv(workspaceId, workspacePath) {
3020
+ const venv = path.join(workspacePath, ".venv");
3021
+ if (!fs.existsSync(venv)) return;
3022
+ const target = this.baselineDir(workspaceId);
3023
+ fs.mkdirSync(target, { recursive: true });
3024
+ const manifest = { files: {} };
3025
+ const python = path.join(venv, "bin", "python");
3026
+ try {
3027
+ manifest.python = fs.realpathSync.native(python);
3028
+ } catch {}
3029
+ const cfg = path.join(venv, "pyvenv.cfg");
3030
+ if (fs.existsSync(cfg)) manifest.files["pyvenv.cfg"] = sha256File(cfg);
3031
+ for (const site of sitePackageDirs(venv)) {
3032
+ for (const entry of fs.readdirSync(site, { withFileTypes: true })) {
3033
+ if (!entry.isFile() || !entry.name.endsWith(".pth")) continue;
3034
+ const file = path.join(site, entry.name);
3035
+ manifest.files[path.relative(venv, file)] = sha256File(file);
3036
+ }
3037
+ const pytest = path.join(site, "pytest");
3038
+ if (fs.existsSync(pytest)) manifest.pytest = sha256Tree(pytest);
3039
+ }
3040
+ fs.writeFileSync(path.join(target, "venv.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
3041
+ }
2458
3042
  /** Check one workspace before install. */
2459
3043
  check(workspaceId) {
2460
3044
  const workspace = this.registry.get(workspaceId);
@@ -2548,7 +3132,7 @@ var TaskService = class {
2548
3132
  }
2549
3133
  /** Remove the shared CIFAR lock only when it belongs to this install. */
2550
3134
  cleanupCifarLock(token) {
2551
- const lock = path.join(os.homedir(), ".dsh", "eval-guard", "cache", "cifar10", ".download.lock");
3135
+ const lock = path.join(dshHome(), "eval-guard", "cache", "cifar10", ".download.lock");
2552
3136
  try {
2553
3137
  if (!fs.existsSync(lock)) return;
2554
3138
  if (fs.readFileSync(lock, "utf8").includes("token=" + token)) fs.rmSync(lock, { force: true });
@@ -2613,10 +3197,16 @@ var TaskService = class {
2613
3197
  command,
2614
3198
  cwd: checked.path,
2615
3199
  env,
3200
+ cleanupWorkspace: checked.path,
3201
+ cifarToken: installToken,
2616
3202
  onCancel: cleanupInstall,
2617
3203
  onFailure: cleanupInstall,
2618
3204
  onSuccess: () => {
2619
3205
  if (definition.needsSetup) this.cleanupWorkspace(checked.path);
3206
+ try {
3207
+ this.snapshotTestMaterial(definition, workspaceId, checked.path);
3208
+ this.snapshotVenv(workspaceId, checked.path);
3209
+ } catch {}
2620
3210
  return { prompt: this.promptNotice(this.guard.promptFor(checked.path, this.materialZip(taskId), definition)) };
2621
3211
  }
2622
3212
  });
@@ -2646,30 +3236,53 @@ var TaskService = class {
2646
3236
  ok: false,
2647
3237
  error: "评测脚本不存在:" + script
2648
3238
  };
2649
- const evalWorkspace = (taskId === "neural-pendulum" ? this.stageHostWorkspace(workspaceId) : void 0) ?? checked.path;
3239
+ const hostView = taskId === "neural-pendulum" ? this.stageHostWorkspace(workspaceId) : void 0;
3240
+ const evalWorkspace = hostView ?? checked.path;
3241
+ const cleanupHostView = () => {
3242
+ if (hostView === void 0) return;
3243
+ try {
3244
+ fs.rmSync(hostView, {
3245
+ recursive: true,
3246
+ force: true
3247
+ });
3248
+ } catch {}
3249
+ };
3250
+ const baseline = this.baselineDir(workspaceId);
3251
+ const baselineEnv = fs.existsSync(baseline) ? { EVAL_BASELINE_DIR: baseline } : {};
3252
+ const job = this.jobs.start({
3253
+ kind: "eval",
3254
+ task: taskId,
3255
+ workspaceId,
3256
+ command: [
3257
+ "python3",
3258
+ "-X",
3259
+ "utf8",
3260
+ script,
3261
+ "--workspace",
3262
+ evalWorkspace
3263
+ ],
3264
+ cwd: packageRoot(),
3265
+ env: {
3266
+ PYTHONPATH: "",
3267
+ PYTHONHOME: "",
3268
+ EVAL_CIFAR_PROXY: this.proxyValue(this.guard.configuration.cifarProxyUrl),
3269
+ EVAL_CIFAR_URL: this.cifarUrl(this.guard.configuration),
3270
+ ...baselineEnv
3271
+ },
3272
+ onSuccess: cleanupHostView,
3273
+ onCancel: cleanupHostView,
3274
+ onFailure: cleanupHostView
3275
+ });
3276
+ if (job.status === "failed") {
3277
+ cleanupHostView();
3278
+ return {
3279
+ ok: false,
3280
+ error: job.error ?? "评测启动失败"
3281
+ };
3282
+ }
2650
3283
  return {
2651
3284
  ok: true,
2652
- jobId: this.jobs.start({
2653
- kind: "eval",
2654
- task: taskId,
2655
- workspaceId,
2656
- command: [
2657
- "python3",
2658
- "-X",
2659
- "utf8",
2660
- script,
2661
- "--workspace",
2662
- evalWorkspace
2663
- ],
2664
- cwd: packageRoot(),
2665
- env: {
2666
- PYTHONPATH: "",
2667
- PYTHONHOME: "",
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, "_"))
2671
- }
2672
- }).id
3285
+ jobId: job.id
2673
3286
  };
2674
3287
  }
2675
3288
  /** Add the optional shared-machine notice to a task prompt. */
@@ -2695,54 +3308,6 @@ var TaskService = class {
2695
3308
  if (stale) fs.copyFileSync(source, target);
2696
3309
  return target;
2697
3310
  }
2698
- /** Install the selected task pack into an empty workspace. */
2699
- install(workspaceId, taskId = TANK_TASK, promptOverride) {
2700
- const definition = TASK_DEFINITIONS[taskId];
2701
- if (definition === void 0) return {
2702
- ok: false,
2703
- workspaceId,
2704
- error: "未知任务:" + taskId
2705
- };
2706
- const checked = this.check(workspaceId);
2707
- if (!checked.ok) return {
2708
- ok: false,
2709
- error: checked.error,
2710
- workspaceId
2711
- };
2712
- if (!checked.empty) return {
2713
- ok: false,
2714
- workspaceId,
2715
- workspacePath: checked.path,
2716
- error: "工作区不是空的;未执行任何操作。请换一个空工作区,或按手动通道处理。"
2717
- };
2718
- if (!this.guard.active) return {
2719
- ok: false,
2720
- workspaceId,
2721
- workspacePath: checked.path,
2722
- error: "请先启用评测守护,再安装评测任务。"
2723
- };
2724
- try {
2725
- fs.copyFileSync(this.materialZip(taskId), path.join(checked.path, "workspace.zip"));
2726
- if (definition.needsSetup) this.extractMaterial(checked.path);
2727
- if (definition.prepare !== void 0) definition.prepare(checked.path);
2728
- if (definition.needsSetup) this.cleanupWorkspace(checked.path);
2729
- } catch (error) {
2730
- if (checked.empty) this.wipeInstallWorkspace(checked.path);
2731
- return {
2732
- ok: false,
2733
- workspaceId,
2734
- workspacePath: checked.path,
2735
- error: "任务准备失败:" + (error instanceof Error ? error.message : String(error))
2736
- };
2737
- }
2738
- const prompt = this.promptNotice(promptOverride ?? this.guard.promptFor(checked.path, this.materialZip(taskId), definition));
2739
- return {
2740
- ok: true,
2741
- workspaceId,
2742
- workspacePath: checked.path,
2743
- prompt
2744
- };
2745
- }
2746
3311
  /** The prompt for manual copying, without installing anything. */
2747
3312
  prompt(workspaceId, taskId = TANK_TASK) {
2748
3313
  const definition = TASK_DEFINITIONS[taskId];
@@ -2794,6 +3359,14 @@ function under(target, root) {
2794
3359
  const relative = path.relative(root, target);
2795
3360
  return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
2796
3361
  }
3362
+ /** Resolve symlinks where possible; keep the lexical path when it does not exist. */
3363
+ function realOrSelf(target) {
3364
+ try {
3365
+ return fs.realpathSync.native(target);
3366
+ } catch {
3367
+ return path.resolve(target);
3368
+ }
3369
+ }
2797
3370
  /**
2798
3371
  * Register every route this plugin owns.
2799
3372
  * @param ctx - host context.
@@ -2818,6 +3391,7 @@ function registerRoutes(ctx, guard, tasks) {
2818
3391
  fetch: async () => {
2819
3392
  return json({
2820
3393
  enabled: guard.active,
3394
+ ...guard.lastFailure === void 0 ? {} : { error: guard.lastFailure },
2821
3395
  running: guard.runningSessions(),
2822
3396
  tasks: [...TASKS],
2823
3397
  defaultBlacklist: guard.defaultBlacklist(),
@@ -2964,15 +3538,25 @@ function registerRoutes(ctx, guard, tasks) {
2964
3538
  }
2965
3539
  const target = typeof body.path === "string" ? path.resolve(body.path) : "";
2966
3540
  if (target === "") return json({ error: "path is required" }, 400);
2967
- const material = path.resolve(guardRoot(), "material");
2968
- const workspaces = child.get("workspaceRegistry")?.list?.() ?? [];
2969
- if (!(under(target, material) || workspaces.some((workspace) => under(target, workspace.path)))) return json({ error: "path is not revealable" }, 403);
2970
3541
  if (!fs.existsSync(target)) return json({ error: "path does not exist" }, 404);
3542
+ const real = realOrSelf(target);
3543
+ const material = realOrSelf(path.join(guardRoot(), "material"));
3544
+ const workspaces = child.get("workspaceRegistry")?.list?.() ?? [];
3545
+ if (!(under(real, material) || workspaces.some((workspace) => under(real, realOrSelf(workspace.path))))) return json({ error: "path is not revealable" }, 403);
2971
3546
  try {
2972
- spawn("xdg-open", [target], {
2973
- detached: true,
2974
- stdio: "ignore"
2975
- }).unref();
3547
+ await new Promise((resolve, reject) => {
3548
+ const childProcess = spawn("xdg-open", [real], {
3549
+ detached: true,
3550
+ stdio: "ignore"
3551
+ });
3552
+ childProcess.once("spawn", () => {
3553
+ childProcess.unref();
3554
+ resolve();
3555
+ });
3556
+ childProcess.once("error", (error) => {
3557
+ reject(error);
3558
+ });
3559
+ });
2976
3560
  } catch (error) {
2977
3561
  return json({ error: error instanceof Error ? error.message : String(error) }, 500);
2978
3562
  }
@@ -3008,6 +3592,7 @@ function resolve(guard, exec) {
3008
3592
  const run = guard.runForSession(exec.agent?.id);
3009
3593
  if (run === void 0) throw new Error("eval task run is not active for this session");
3010
3594
  if (run.task !== "neural-pendulum") throw new Error("this tool is only available for the neural-pendulum task");
3595
+ if (run.status !== "active") throw new Error("eval task run is not active (status: " + run.status + ")");
3011
3596
  return run;
3012
3597
  }
3013
3598
  /** Install the task tools into one agent's own scoped registry. */
@@ -3037,7 +3622,7 @@ function registerAgentSubmissionTools(agent, guard, tasks) {
3037
3622
  schema: OUTPUT_SCHEMA,
3038
3623
  render
3039
3624
  },
3040
- timeoutMs: 9e5,
3625
+ timeoutMs: 132e4,
3041
3626
  execute: async (_args, exec) => tasks.hostTest(resolve(guard, exec))
3042
3627
  };
3043
3628
  let registered = false;