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.
- package/ISSUES.md +239 -0
- package/README.md +48 -18
- package/assets/neural-cifar/workspace.zip +0 -0
- package/assets/neural-cpp/workspace.zip +0 -0
- package/assets/neural-fp32/workspace.zip +0 -0
- package/assets/neural-pendulum/SPEC.md +43 -35
- package/assets/neural-pendulum/TASK.md +139 -33
- package/assets/neural-pendulum/rl-feasibility/README.md +60 -0
- package/assets/neural-pendulum/rl-feasibility/RESULTS.md +370 -0
- package/assets/neural-pendulum/rl-feasibility/evaluate.py +107 -0
- package/assets/neural-pendulum/rl-feasibility/finalize_run.py +168 -0
- package/assets/neural-pendulum/rl-feasibility/models.py +124 -0
- package/assets/neural-pendulum/rl-feasibility/obs.py +205 -0
- package/assets/neural-pendulum/rl-feasibility/plot_selected.py +55 -0
- package/assets/neural-pendulum/rl-feasibility/reference_controller.py +112 -0
- package/assets/neural-pendulum/rl-feasibility/results/curves_selected.png +0 -0
- package/assets/neural-pendulum/rl-feasibility/results/reference_controller.json +417 -0
- package/assets/neural-pendulum/rl-feasibility/results/summary.json +1599 -0
- package/assets/neural-pendulum/rl-feasibility/results/table.md +39 -0
- package/assets/neural-pendulum/rl-feasibility/sim.py +324 -0
- package/assets/neural-pendulum/rl-feasibility/summarize.py +175 -0
- package/assets/neural-pendulum/rl-feasibility/task.py +342 -0
- package/assets/neural-pendulum/rl-feasibility/test_contract.py +188 -0
- package/assets/neural-pendulum/rl-feasibility/train_ppo.py +403 -0
- package/assets/neural-pendulum/rl-feasibility/train_sac.py +406 -0
- package/assets/neural-pendulum/sim.cpp +13 -2
- package/assets/neural-pendulum/sim_eval +0 -0
- package/assets/neural-pendulum/workspace.zip +0 -0
- package/eval/README.md +19 -9
- package/eval/bomberman.py +5 -3
- package/eval/commit0.py +10 -3
- package/eval/common.py +169 -5
- package/eval/ctf.py +4 -2
- package/eval/featurebench.py +12 -8
- package/eval/land.py +4 -2
- package/eval/legacy/bomberman.py +5 -2
- package/eval/legacy/ctf.py +5 -2
- package/eval/legacy/featurebench.py +55 -15
- package/eval/legacy/land.py +6 -3
- package/eval/legacy/swe_bench_pro.py +57 -16
- package/eval/legacy/tank.py +2 -2
- package/eval/legacy/warehouse.py +4 -4
- package/eval/neural-cpp.py +9 -6
- package/eval/neural-fp32.py +17 -5
- package/eval/swe-bench-pro.py +11 -7
- package/eval/tank.py +11 -2
- package/eval/warehouse.py +17 -1
- package/lib/client/index.js +56 -29
- package/lib/client/index.js.map +1 -1
- package/lib/index.js +883 -298
- package/native/dsh-eval-supervisor +0 -0
- package/native/supervisor.c +232 -36
- package/neural-cifar-eval/README.md +5 -0
- package/neural-cifar-eval/common.py +13 -0
- package/neural-cifar-eval/ops_eval.py +17 -5
- package/neural-cifar-eval/run_eval.py +42 -32
- package/neural-pendulum-eval/README.md +62 -0
- package/neural-pendulum-eval/ops_eval.py +603 -0
- package/neural-pendulum-eval/run_eval.py +218 -137
- package/neural-pendulum-eval/sim_eval +0 -0
- package/package.json +7 -11
- package/assets/neural-pendulum/hidden/test_infra.cpp +0 -631
- 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
|
-
/**
|
|
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
|
-
/**
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
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
|
-
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
/**
|
|
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")
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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 (!
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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,
|
|
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
|
-
}
|
|
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
|
|
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
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
"
|
|
732
|
-
|
|
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
|
|
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
|
-
...
|
|
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)
|
|
896
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
1125
|
+
if (!this.isMain(agent.session)) continue;
|
|
1126
|
+
const jobs = this.ownJobs(agent.id);
|
|
936
1127
|
const running = agent.status === "running";
|
|
937
|
-
|
|
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
|
|
942
|
-
cwd: agent.session
|
|
1131
|
+
title: sessionTitle(agent.session) ?? agent.id,
|
|
1132
|
+
cwd: sessionCwd(agent.session) ?? "",
|
|
943
1133
|
running,
|
|
944
|
-
jobs:
|
|
1134
|
+
jobs: jobs.length
|
|
945
1135
|
});
|
|
946
1136
|
}
|
|
947
1137
|
return out;
|
|
948
1138
|
}
|
|
949
|
-
/** Whether a session is a user
|
|
1139
|
+
/** Whether a session is a user conversation (not a subagent/plugin child). */
|
|
950
1140
|
isMain(session) {
|
|
951
|
-
|
|
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
|
-
|
|
964
|
-
|
|
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
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
|
998
|
-
const workspace =
|
|
999
|
-
if (workspace === void 0
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
|
1055
|
-
if (cwd !== void 0) {
|
|
1056
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1124
|
-
const
|
|
1125
|
-
if (
|
|
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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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.
|
|
1145
|
-
this.
|
|
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.
|
|
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
|
-
}
|
|
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
|
|
1191
|
-
|
|
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
|
|
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
|
|
1232
|
-
if (workspace === 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
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
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
|
|
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
|
-
"比赛是连续
|
|
1827
|
-
"- 你有
|
|
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
|
-
"- 本次是连续
|
|
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 分钟,请自己把握进度,做完就可以结束;超过
|
|
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分钟完成,
|
|
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 的强度,提前交卷不加分,请充分利用时间持续改进;可以适当超时,但到
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
...
|
|
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
|
-
], {
|
|
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
|
-
|
|
2308
|
-
|
|
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: "
|
|
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
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
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(
|
|
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:
|
|
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
|
|
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
|
-
|
|
2872
|
+
const completed = stage === "completed" && Number.isFinite(tCan) && tCan >= 10;
|
|
2873
|
+
const event = {
|
|
2370
2874
|
event: "test",
|
|
2371
|
-
time:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
|
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:
|
|
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
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
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:
|
|
3625
|
+
timeoutMs: 132e4,
|
|
3041
3626
|
execute: async (_args, exec) => tasks.hostTest(resolve(guard, exec))
|
|
3042
3627
|
};
|
|
3043
3628
|
let registered = false;
|