dsh-eval-guard 0.1.2 → 0.1.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.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 huhu233
3
+ Copyright (c) 2026 huhu23333
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  - 禁止联网、子代理、Agent Team/跨 agent 通信、读取其它工作区。
7
7
  - 工具黑名单可配置;未知工具默认放行,但 DSH 内置与 dsh-ops 的工具会做参数路径审计。
8
8
  - 每个用户消息后的单轮有可配置的最长时长(默认 90 分钟);到点只终止当前轮次,并递归清理子代理与后台任务。
9
- - 设置页可安装“第 6 集 · 坦克大战”任务:空工作区 → 复制素材 → 新会话填入提示词(由用户发送)。
9
+ - 设置页可安装“第 6 集 · 坦克大战”任务:空工作区 → 放入打包好的 `workspace.zip` → 新会话填入提示词(由用户发送)。
10
10
 
11
11
  插件默认关闭;关闭时不做任何行为。所有 run 记录、usage、violations 写入
12
12
  $DSH_HOME/eval-guard/runs/<run-id>/,供独立的 ../tank-eval 评测程序读取。
@@ -20,7 +20,7 @@ $DSH_HOME/eval-guard/runs/<run-id>/,供独立的 ../tank-eval 评测程序读
20
20
  - 使用版本:commit `6bd738b`(2026-10-16)
21
21
  - 素材来源:该仓库的 `tank/workspace/`(MIT License)
22
22
 
23
- 本插件打包的 Linux 版 `workspace.zip` 在原素材基础上做了两处适配:把 `baseline.cpp` 编译为 Linux 可执行文件并移出源码;把 `clock.py` 改为单次 90 分钟版本。规则、引擎、本地裁判、模板等其余文件保持原样。
23
+ 本插件打包的 Linux 版 `workspace.zip` 在原素材基础上做了三处适配:把 `baseline.cpp` 编译为 Linux ELF 可执行文件并移出源码,文件名改为 `baseline`(不带 `.exe`);把 `clock.py` 改为单次 90 分钟版本;`README.md` 中对应的 baseline 示例同步改名。规则、引擎、本地裁判、模板等其余文件保持原样。安装任务时只把 `workspace.zip` 放入工作区,不再散装解压。
24
24
 
25
25
  ## 构建
26
26
 
@@ -34,7 +34,7 @@ node scripts/build-native.mjs
34
34
  ~~~bash
35
35
  scripts/install.sh web
36
36
  # 如果脚本找不到 dsh 命令:
37
- DSH_CHECKOUT=/home/huhu233/otherProject/deepseek-harness scripts/install.sh web
37
+ DSH_CHECKOUT=/path/to/deepseek-harness scripts/install.sh web
38
38
  # 然后重启 dsh web
39
39
  ~~~
40
40
 
Binary file
package/lib/index.js CHANGED
@@ -4,7 +4,7 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import os from "node:os";
6
6
  import { fileURLToPath } from "node:url";
7
- import { execFileSync, spawn } from "node:child_process";
7
+ import { spawn } from "node:child_process";
8
8
  //#region src/config.ts
9
9
  /**
10
10
  * Plugin configuration. The Settings page writes this document through the
@@ -337,10 +337,29 @@ function packageRoot() {
337
337
  function supervisorPath() {
338
338
  return path.join(packageRoot(), "native", "dsh-eval-supervisor");
339
339
  }
340
- /** The DSH home: $DSH_HOME or ~/.dsh. */
340
+ /** Launcher-provided facts; nothing here is a fixed install path. */
341
+ let runtimeHome;
342
+ let runtimeProfileDir;
343
+ /** Record the launcher's profile/home paths. */
344
+ function configureRuntimeContext(next) {
345
+ if (next.home !== void 0 && next.home !== "") runtimeHome = next.home;
346
+ if (next.profileDir !== void 0 && next.profileDir !== "") runtimeProfileDir = next.profileDir;
347
+ }
348
+ /** The DSH home: launcher profile context, then $DSH_HOME, then ~/.dsh. */
341
349
  function dshHome() {
350
+ if (runtimeHome !== void 0) return runtimeHome;
342
351
  return process.env.DSH_HOME && process.env.DSH_HOME !== "" ? process.env.DSH_HOME : path.join(os.homedir(), ".dsh");
343
352
  }
353
+ /** Candidate node_modules directories, launcher profile first and then profiles/*. */
354
+ function profileNodeModules() {
355
+ const out = [];
356
+ if (runtimeProfileDir !== void 0) out.push(path.join(runtimeProfileDir, "node_modules"));
357
+ const profiles = path.join(dshHome(), "profiles");
358
+ try {
359
+ for (const entry of fs.readdirSync(profiles, { withFileTypes: true })) if (entry.isDirectory()) out.push(path.join(profiles, entry.name, "node_modules"));
360
+ } catch {}
361
+ return out;
362
+ }
344
363
  /** Root for run records, audits, and sandbox policy files. */
345
364
  function guardRoot() {
346
365
  return path.join(dshHome(), "eval-guard");
@@ -381,14 +400,9 @@ function packageDir(name) {
381
400
  const require = createRequire(path.join(process.cwd(), "noop.js"));
382
401
  candidates.push(path.dirname(require.resolve(name + "/package.json")));
383
402
  } catch {}
384
- if (name === "@deepseek-ai/dsh-ptc-runtime-node") {
385
- candidates.push(path.join(process.cwd(), "packages", "ptc-runtime", "ptc-runtime-node"));
386
- candidates.push(path.join(dshHome(), "profiles", "web", "node_modules", name));
387
- }
388
- if (name === "@deepseek-ai/dsh-subprocess") {
389
- candidates.push(path.join(process.cwd(), "packages", "subprocess", "subprocess"));
390
- candidates.push(path.join(dshHome(), "profiles", "web", "node_modules", name));
391
- }
403
+ if (name === "@deepseek-ai/dsh-ptc-runtime-node") candidates.push(path.join(process.cwd(), "packages", "ptc-runtime", "ptc-runtime-node"));
404
+ if (name === "@deepseek-ai/dsh-subprocess") candidates.push(path.join(process.cwd(), "packages", "subprocess", "subprocess"));
405
+ for (const modules of profileNodeModules()) candidates.push(path.join(modules, name));
392
406
  for (const candidate of candidates) try {
393
407
  if (fs.statSync(path.join(candidate, "package.json")).isFile()) return fs.realpathSync.native(candidate);
394
408
  } catch {}
@@ -1421,7 +1435,7 @@ function registerRoutes(ctx, guard, tasks) {
1421
1435
  //#endregion
1422
1436
  //#region src/task.ts
1423
1437
  /**
1424
- * The tank task pack: workspace extraction, prompt construction, and the one
1438
+ * The tank task pack: packaged workspace.zip install, prompt construction, and the one
1425
1439
  * run record the evaluator later reads.
1426
1440
  * @module dsh-eval-guard/task
1427
1441
  */
@@ -1429,7 +1443,7 @@ function registerRoutes(ctx, guard, tasks) {
1429
1443
  const TASKS = [{
1430
1444
  id: TANK_TASK,
1431
1445
  label: TANK_TASK_LABEL,
1432
- description: "1v1 坦克大战 bot:编写 submission/bot.cpp,评测时与基准及其它提交打循环赛。"
1446
+ description: "1v1 坦克大战 bot:解压 workspace.zip 后编写 workspace/submission/bot.cpp,评测时与基准及其它提交打循环赛。"
1433
1447
  }];
1434
1448
  /** Format a blacklist for the prompt. */
1435
1449
  function blacklistBlock(blacklist) {
@@ -1455,19 +1469,23 @@ const TANK_DEFINITION = {
1455
1469
  "",
1456
1470
  "## 交付",
1457
1471
  "",
1458
- "- 把最终 bot 保存到工作区内的 submission/bot.cpp(只需要这一个文件)。",
1459
- "- 交付前检查并关闭所有后台任务;最多 " + String(options.minutes ?? 90) + " 分钟,到点会被强制终止。",
1472
+ "- 解压工作区里的 workspace.zip,把最终 bot 保存到解压后的 workspace/submission/bot.cpp(只需要这一个文件)。",
1460
1473
  "- 最后用几句话说明你的策略。",
1461
1474
  "",
1475
+ "## 时间规则",
1476
+ "",
1477
+ "- 本次是**连续 " + String(options.minutes ?? 90) + " 分钟的单次任务**,从对话开始时开始计时。",
1478
+ "- 交付前检查并关闭所有后台任务;到点会被强制终止。",
1479
+ "- clock.py 从解压那一刻开始计算,而实际计时从对话开始计算;它**只能作为参考**,请尽快解压以减小误差。实际时间以评测守护为准。",
1480
+ "",
1462
1481
  "## 本次评测禁止调用的工具",
1463
1482
  "",
1464
1483
  blacklistBlock(blacklist),
1465
1484
  "",
1466
1485
  "## 工作区内容",
1467
1486
  "",
1468
- "工作区根目录就是选手包:先读 README.md 和 RULES.md,规则引擎是 tank.hpp。",
1469
- "本地工具(arena.py、check、play、batch、vs 等)和起步模板都在工作区里,可直接使用。",
1470
- "README.md 里提到的上下半场属于原始赛制;clock.py 只是参考,本次是单次任务,以本提示词的时间规则为准。",
1487
+ "工作区里有一个 workspace.zip;解压后进入 workspace/ 目录,先读 README.md 和 RULES.md,规则引擎是 tank.hpp。",
1488
+ "本地工具(arena.py、check、play、batch、vs 等)和起步模板都在解压后的 workspace/ 里,可直接使用。",
1471
1489
  "",
1472
1490
  "开始吧。"
1473
1491
  ].join("\n");
@@ -1513,20 +1531,21 @@ var TaskService = class {
1513
1531
  entries
1514
1532
  };
1515
1533
  }
1516
- /** Extract the task pack to a material directory the manual channel can reveal. */
1517
- materialDir() {
1534
+ /** Copy the packaged workspace.zip to a stable material path for install/manual use. */
1535
+ materialZip() {
1518
1536
  const dir = path.join(guardRoot(), "material", "tank");
1519
- const marker = path.join(guardRoot(), "material", ".tank-complete");
1520
- if (fs.existsSync(marker) && fs.existsSync(path.join(dir, "RULES.md"))) return dir;
1521
- fs.rmSync(dir, {
1522
- recursive: true,
1523
- force: true
1524
- });
1525
1537
  fs.mkdirSync(dir, { recursive: true });
1526
- fs.mkdirSync(path.dirname(marker), { recursive: true });
1527
- this.extract(TANK_DEFINITION.asset, dir);
1528
- fs.writeFileSync(marker, "ok\n", "utf8");
1529
- return dir;
1538
+ const target = path.join(dir, "workspace.zip");
1539
+ const source = TANK_DEFINITION.asset;
1540
+ if (!fs.existsSync(source)) throw new Error("task asset is missing: " + source);
1541
+ let stale = true;
1542
+ try {
1543
+ stale = !fs.existsSync(target) || fs.statSync(target).size !== fs.statSync(source).size;
1544
+ } catch {
1545
+ stale = true;
1546
+ }
1547
+ if (stale) fs.copyFileSync(source, target);
1548
+ return target;
1530
1549
  }
1531
1550
  /** Install the task pack into an empty workspace. */
1532
1551
  install(workspaceId, promptOverride) {
@@ -1549,7 +1568,7 @@ var TaskService = class {
1549
1568
  error: "请先启用评测守护,再安装评测任务。"
1550
1569
  };
1551
1570
  try {
1552
- this.extract(TANK_DEFINITION.asset, checked.path);
1571
+ fs.copyFileSync(this.materialZip(), path.join(checked.path, "workspace.zip"));
1553
1572
  } catch (error) {
1554
1573
  return {
1555
1574
  ok: false,
@@ -1559,7 +1578,7 @@ var TaskService = class {
1559
1578
  };
1560
1579
  }
1561
1580
  const runId = this.newRunId();
1562
- const prompt = promptOverride ?? this.guard.promptFor(checked.path, this.materialDir());
1581
+ const prompt = promptOverride ?? this.guard.promptFor(checked.path, this.materialZip());
1563
1582
  const run = {
1564
1583
  runId,
1565
1584
  task: TANK_TASK,
@@ -1585,39 +1604,16 @@ var TaskService = class {
1585
1604
  /** The prompt for manual copying, without installing anything. */
1586
1605
  prompt(workspaceId) {
1587
1606
  const workspace = this.registry.get(workspaceId);
1588
- return this.guard.promptFor(workspace?.path ?? "", this.materialDir());
1607
+ return this.guard.promptFor(workspace?.path ?? "", this.materialZip());
1589
1608
  }
1590
1609
  /** Filesystem paths for the manual channel. */
1591
1610
  manualPaths(workspaceId) {
1592
1611
  const workspace = this.registry.get(workspaceId);
1593
1612
  return {
1594
- source: this.materialDir(),
1595
- prompt: this.guard.promptFor(workspace?.path ?? "", this.materialDir())
1613
+ source: this.materialZip(),
1614
+ prompt: this.guard.promptFor(workspace?.path ?? "", this.materialZip())
1596
1615
  };
1597
1616
  }
1598
- /** Extract a workspace.zip into a destination, stripping the workspace/ prefix. */
1599
- extract(zip, destination) {
1600
- if (!fs.existsSync(zip)) throw new Error("task asset is missing: " + zip);
1601
- fs.mkdirSync(path.join(guardRoot(), "tmp"), { recursive: true });
1602
- const temporary = fs.mkdtempSync(path.join(guardRoot(), "tmp", "extract-"));
1603
- try {
1604
- execFileSync("unzip", [
1605
- "-q",
1606
- "-o",
1607
- zip,
1608
- "-d",
1609
- temporary
1610
- ], { stdio: "ignore" });
1611
- const inner = path.join(temporary, "workspace");
1612
- if (!fs.existsSync(inner)) throw new Error("task asset has no workspace/ root");
1613
- fs.cpSync(inner, destination, { recursive: true });
1614
- } finally {
1615
- fs.rmSync(temporary, {
1616
- recursive: true,
1617
- force: true
1618
- });
1619
- }
1620
- }
1621
1617
  /** A unique run id. */
1622
1618
  newRunId() {
1623
1619
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "").slice(0, 14) + "-" + Math.random().toString(16).slice(2, 8);
@@ -1650,6 +1646,12 @@ function apply(ctx, config) {
1650
1646
  "sandboxPolicy",
1651
1647
  "workspaceRegistry"
1652
1648
  ], (child) => {
1649
+ const getService = ctx.get;
1650
+ const profile = typeof getService === "function" ? getService.call(ctx, "profileContext") : void 0;
1651
+ configureRuntimeContext({
1652
+ home: profile?.home,
1653
+ profileDir: profile?.dir
1654
+ });
1653
1655
  const services = {
1654
1656
  sessions: child.get("sessions"),
1655
1657
  agents: child.get("agents"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-eval-guard",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "Evaluation guard for DSH: workspace-only filesystem, no network/subagents, wall-clock limits, and task installation for the meow-ai-arena tank task.",
6
6
  "license": "MIT",