dsh-eval-guard 0.7.2 → 0.7.3
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 +235 -0
- package/README.md +46 -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 +39 -32
- package/assets/neural-pendulum/TASK.md +137 -32
- package/assets/neural-pendulum/workspace.zip +0 -0
- package/eval/README.md +19 -9
- package/eval/__pycache__/bomberman.cpython-310.pyc +0 -0
- package/eval/__pycache__/commit0.cpython-310.pyc +0 -0
- package/eval/__pycache__/common.cpython-310.pyc +0 -0
- package/eval/__pycache__/ctf.cpython-310.pyc +0 -0
- package/eval/__pycache__/featurebench.cpython-310.pyc +0 -0
- package/eval/__pycache__/land.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-cifar.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-cpp.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-fp32.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-pendulum.cpython-310.pyc +0 -0
- package/eval/__pycache__/swe-bench-pro.cpython-310.pyc +0 -0
- package/eval/__pycache__/tank.cpython-310.pyc +0 -0
- package/eval/__pycache__/warehouse.cpython-310.pyc +0 -0
- 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/__pycache__/bomberman.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/commit0.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/ctf.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/featurebench.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/land.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/swe_bench_pro.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/tank.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/warehouse.cpython-310.pyc +0 -0
- 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 +878 -297
- package/native/dsh-eval-supervisor +0 -0
- package/native/supervisor.c +190 -26
- package/neural-cifar-eval/README.md +5 -0
- package/neural-cifar-eval/__pycache__/cifar_cache.cpython-310.pyc +0 -0
- package/neural-cifar-eval/__pycache__/common.cpython-310.pyc +0 -0
- package/neural-cifar-eval/__pycache__/fetch_cifar.cpython-310.pyc +0 -0
- package/neural-cifar-eval/__pycache__/ops_eval.cpython-310.pyc +0 -0
- package/neural-cifar-eval/__pycache__/run_eval.cpython-310.pyc +0 -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/__pycache__/ops_eval.cpython-310.pyc +0 -0
- package/neural-pendulum-eval/__pycache__/run_eval.cpython-310.pyc +0 -0
- package/neural-pendulum-eval/ops_eval.py +603 -0
- package/neural-pendulum-eval/run_eval.py +218 -137
- 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);
|
|
@@ -756,7 +893,8 @@ function bwrapArgs(workspace, cwd, rules, runtime) {
|
|
|
756
893
|
* Wrap one argv in the strict sandbox.
|
|
757
894
|
* @param argv - the exact command the consumer was about to spawn.
|
|
758
895
|
* @param workspace - session cwd.
|
|
759
|
-
* @param cwd - working directory the
|
|
896
|
+
* @param cwd - explicit working directory override; the empty string lets the
|
|
897
|
+
* supervisor use its inherited cwd when that cwd lies inside the workspace.
|
|
760
898
|
* @param rules - ordered forbidden-path rules.
|
|
761
899
|
* @param auditFile - host-side audit path the supervisor appends to.
|
|
762
900
|
*/
|
|
@@ -771,6 +909,7 @@ function wrapSandbox(argv, workspace, cwd, rules, auditFile) {
|
|
|
771
909
|
ensureDir(path.join(workspace, ".config"));
|
|
772
910
|
ensureDir(path.join(workspace, ".local", "share"));
|
|
773
911
|
const policyFile = writePolicy(workspace, rules);
|
|
912
|
+
const args = bwrapArgs(workspace, rules, runtimeFiles());
|
|
774
913
|
return {
|
|
775
914
|
argv: [
|
|
776
915
|
supervisor,
|
|
@@ -778,9 +917,10 @@ function wrapSandbox(argv, workspace, cwd, rules, auditFile) {
|
|
|
778
917
|
policyFile,
|
|
779
918
|
"--audit",
|
|
780
919
|
auditFile,
|
|
920
|
+
...cwd === "" ? [] : ["--cwd", cwd],
|
|
781
921
|
"--",
|
|
782
922
|
"bwrap",
|
|
783
|
-
...
|
|
923
|
+
...args,
|
|
784
924
|
"--",
|
|
785
925
|
...argv
|
|
786
926
|
],
|
|
@@ -795,6 +935,39 @@ function wrapSandbox(argv, workspace, cwd, rules, auditFile) {
|
|
|
795
935
|
* tool policy, wall-clock limit, run records, and audit events.
|
|
796
936
|
* @module dsh-eval-guard/guard
|
|
797
937
|
*/
|
|
938
|
+
/** The cwd a session is confined to, or undefined when the header has none. */
|
|
939
|
+
function sessionCwd(session) {
|
|
940
|
+
const cwd = session?.header?.cwd;
|
|
941
|
+
return typeof cwd === "string" && cwd !== "" ? cwd : void 0;
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Whether a session is a subagent child.
|
|
945
|
+
*
|
|
946
|
+
* DSH uses `origin: 'subagent'` and/or `delegationDepth > 0` for subagents.
|
|
947
|
+
* `parentSession` alone is NOT a subagent marker: a user-visible fork of a
|
|
948
|
+
* normal conversation also sets `parentSession` (seed lineage), and treating
|
|
949
|
+
* that as a subagent would leave the whole fork unguarded.
|
|
950
|
+
*/
|
|
951
|
+
function sessionIsSubagent(session) {
|
|
952
|
+
const header = session?.header;
|
|
953
|
+
if (header === void 0) return false;
|
|
954
|
+
if (header.origin === "subagent") return true;
|
|
955
|
+
return typeof header.delegationDepth === "number" && header.delegationDepth > 0;
|
|
956
|
+
}
|
|
957
|
+
/** The session this one was forked/delegated from, if any. */
|
|
958
|
+
function sessionParent(session) {
|
|
959
|
+
const parent = session?.header?.parentSession;
|
|
960
|
+
return typeof parent === "string" && parent !== "" ? parent : void 0;
|
|
961
|
+
}
|
|
962
|
+
/** Display title for the enable warning; DSH's header has none, so fall back. */
|
|
963
|
+
function sessionTitle(session) {
|
|
964
|
+
const title = session?.header?.title;
|
|
965
|
+
if (typeof title === "string" && title !== "") return title;
|
|
966
|
+
const cwd = sessionCwd(session);
|
|
967
|
+
return cwd === void 0 ? void 0 : path.basename(cwd);
|
|
968
|
+
}
|
|
969
|
+
/** How long an idle session keeps its deadline alive before it is settled. */
|
|
970
|
+
const IDLE_GRACE_MS = 5e3;
|
|
798
971
|
/** Event ring + run audit writer. */
|
|
799
972
|
var Guard = class {
|
|
800
973
|
ctx;
|
|
@@ -811,6 +984,7 @@ var Guard = class {
|
|
|
811
984
|
approvalPrev = /* @__PURE__ */ new Map();
|
|
812
985
|
deadlines = /* @__PURE__ */ new Map();
|
|
813
986
|
startedAt = /* @__PURE__ */ new Map();
|
|
987
|
+
lastActivity = /* @__PURE__ */ new Map();
|
|
814
988
|
runs = /* @__PURE__ */ new Map();
|
|
815
989
|
sessionRuns = /* @__PURE__ */ new Map();
|
|
816
990
|
offsets = /* @__PURE__ */ new Map();
|
|
@@ -818,6 +992,8 @@ var Guard = class {
|
|
|
818
992
|
toolCache = [];
|
|
819
993
|
disposers = [];
|
|
820
994
|
task;
|
|
995
|
+
lastError;
|
|
996
|
+
enableRetryAt = 0;
|
|
821
997
|
/** Optional hook invoked when any run binds to an agent. */
|
|
822
998
|
onRunAssociated;
|
|
823
999
|
/** Optional automatic task binding for every main conversation. */
|
|
@@ -892,13 +1068,21 @@ var Guard = class {
|
|
|
892
1068
|
if (!this.rawConfig) return;
|
|
893
1069
|
const next = this.configuration;
|
|
894
1070
|
const wasEnabled = this.enabled;
|
|
895
|
-
if (!wasEnabled && next.enabled)
|
|
896
|
-
|
|
1071
|
+
if (!wasEnabled && next.enabled) {
|
|
1072
|
+
if (Date.now() < this.enableRetryAt) return;
|
|
1073
|
+
this.enable().catch((error) => {
|
|
1074
|
+
this.failEnable(error instanceof Error ? error.message : String(error));
|
|
1075
|
+
});
|
|
1076
|
+
} else if (wasEnabled && !next.enabled) this.disable();
|
|
897
1077
|
}
|
|
898
1078
|
/** Whether the guard is active. */
|
|
899
1079
|
get active() {
|
|
900
1080
|
return this.enabled;
|
|
901
1081
|
}
|
|
1082
|
+
/** Why the guard refused to enable, if it did. */
|
|
1083
|
+
get lastFailure() {
|
|
1084
|
+
return this.lastError;
|
|
1085
|
+
}
|
|
902
1086
|
/** Installed tool names currently visible in this deployment. */
|
|
903
1087
|
refreshTools() {
|
|
904
1088
|
try {
|
|
@@ -928,31 +1112,31 @@ var Guard = class {
|
|
|
928
1112
|
pathRules() {
|
|
929
1113
|
return parsePathRules(this.configuration.forbiddenPaths);
|
|
930
1114
|
}
|
|
931
|
-
/**
|
|
1115
|
+
/** Jobs owned by one session (unowned/global jobs must never be attributed). */
|
|
1116
|
+
ownJobs(sessionId) {
|
|
1117
|
+
return (this.services.jobs?.list(sessionId) ?? []).filter((job) => job.owner === sessionId).filter((job) => job.status === "running" || job.status === "stopping");
|
|
1118
|
+
}
|
|
1119
|
+
/** Live main conversations for the enable warning. */
|
|
932
1120
|
runningSessions() {
|
|
933
1121
|
const out = [];
|
|
934
1122
|
for (const agent of this.services.agents.list()) {
|
|
935
|
-
|
|
1123
|
+
if (!this.isMain(agent.session)) continue;
|
|
1124
|
+
const jobs = this.ownJobs(agent.id);
|
|
936
1125
|
const running = agent.status === "running";
|
|
937
|
-
|
|
938
|
-
if (!running && jobCount === 0) continue;
|
|
1126
|
+
if (!running && jobs.length === 0) continue;
|
|
939
1127
|
out.push({
|
|
940
1128
|
sessionId: agent.id,
|
|
941
|
-
title: agent.session
|
|
942
|
-
cwd: agent.session
|
|
1129
|
+
title: sessionTitle(agent.session) ?? agent.id,
|
|
1130
|
+
cwd: sessionCwd(agent.session) ?? "",
|
|
943
1131
|
running,
|
|
944
|
-
jobs:
|
|
1132
|
+
jobs: jobs.length
|
|
945
1133
|
});
|
|
946
1134
|
}
|
|
947
1135
|
return out;
|
|
948
1136
|
}
|
|
949
|
-
/** Whether a session is a user
|
|
1137
|
+
/** Whether a session is a user conversation (not a subagent/plugin child). */
|
|
950
1138
|
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;
|
|
1139
|
+
return session !== void 0 && !sessionIsSubagent(session);
|
|
956
1140
|
}
|
|
957
1141
|
/** Approvals are auto-rejected while the guard is active. */
|
|
958
1142
|
forceApproval(session) {
|
|
@@ -960,8 +1144,10 @@ var Guard = class {
|
|
|
960
1144
|
if (approval === void 0) return;
|
|
961
1145
|
const agent = this.services.agents.get(session.id);
|
|
962
1146
|
if (agent === void 0) return;
|
|
963
|
-
|
|
964
|
-
|
|
1147
|
+
try {
|
|
1148
|
+
if (!this.approvalPrev.has(session.id)) this.approvalPrev.set(session.id, approval.effectivePolicy(session));
|
|
1149
|
+
approval.setPolicy(agent, "never");
|
|
1150
|
+
} catch {}
|
|
965
1151
|
}
|
|
966
1152
|
/** Restore the policy that existed before the guard touched this session. */
|
|
967
1153
|
restoreApproval(sessionId) {
|
|
@@ -972,19 +1158,36 @@ var Guard = class {
|
|
|
972
1158
|
const approval = this.services.approval;
|
|
973
1159
|
if (agent !== void 0 && approval !== void 0) approval.setPolicy(agent, previous);
|
|
974
1160
|
}
|
|
1161
|
+
/** Record an enable-time refusal and back off before retrying. */
|
|
1162
|
+
failEnable(error) {
|
|
1163
|
+
this.lastError = error;
|
|
1164
|
+
this.enableRetryAt = Date.now() + 5e3;
|
|
1165
|
+
this.record({
|
|
1166
|
+
kind: "notice",
|
|
1167
|
+
detail: "评测守护启用失败:" + error
|
|
1168
|
+
});
|
|
1169
|
+
try {
|
|
1170
|
+
this.ctx.logger.warn("eval-guard: %s", error);
|
|
1171
|
+
} catch {}
|
|
1172
|
+
return {
|
|
1173
|
+
ok: false,
|
|
1174
|
+
error
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
975
1177
|
/** Turn the guard on. */
|
|
976
1178
|
async enable() {
|
|
977
1179
|
if (this.enabled) return { ok: true };
|
|
978
1180
|
const sandbox = this.services.sandbox;
|
|
979
1181
|
const policy = this.services.sandboxPolicy;
|
|
980
|
-
if (sandbox === void 0 || policy === void 0) return
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1182
|
+
if (sandbox === void 0 || policy === void 0) return this.failEnable("sandbox service is unavailable; refusing to enable without process confinement");
|
|
1183
|
+
if (!fs.existsSync(supervisorPath())) return this.failEnable("native supervisor is not built; run npm run build:native");
|
|
1184
|
+
try {
|
|
1185
|
+
fs.accessSync(supervisorPath(), fs.constants.X_OK);
|
|
1186
|
+
} catch {
|
|
1187
|
+
return this.failEnable("native supervisor is not executable; run npm run build:native");
|
|
1188
|
+
}
|
|
1189
|
+
const probe = probeConfinement();
|
|
1190
|
+
if (probe !== void 0) return this.failEnable(probe);
|
|
988
1191
|
this.sandboxObject = sandbox;
|
|
989
1192
|
this.policyObject = policy;
|
|
990
1193
|
this.origConfine = sandbox.confine.bind(sandbox);
|
|
@@ -994,12 +1197,12 @@ var Guard = class {
|
|
|
994
1197
|
if (!guard.enabled) return guard.origConfine(argv, executionPolicy, signal);
|
|
995
1198
|
const sessionId = typeof executionPolicy["sessionId"] === "string" ? executionPolicy["sessionId"] : void 0;
|
|
996
1199
|
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
|
|
1200
|
+
if (session === void 0) return guard.origConfine(argv, executionPolicy, signal);
|
|
1201
|
+
const workspace = sessionCwd(session);
|
|
1202
|
+
if (workspace === void 0) throw new Error("dsh-eval-guard: session has no workspace");
|
|
1000
1203
|
const audit = guard.auditFileFor(session.id);
|
|
1001
1204
|
return {
|
|
1002
|
-
argv: wrapSandbox(argv, workspace,
|
|
1205
|
+
argv: wrapSandbox(argv, workspace, "", guard.pathRules(), audit).argv,
|
|
1003
1206
|
enforcement: "full",
|
|
1004
1207
|
denialSignatures: [
|
|
1005
1208
|
"operation not permitted",
|
|
@@ -1012,29 +1215,60 @@ var Guard = class {
|
|
|
1012
1215
|
policy.resolve = function resolveHook(request = {}) {
|
|
1013
1216
|
const resolved = guard.origResolve(request);
|
|
1014
1217
|
if (!guard.enabled) return resolved;
|
|
1015
|
-
|
|
1016
|
-
if (session === void 0 || !guard.isMain(session)) return resolved;
|
|
1218
|
+
if (request["session"] === void 0) return resolved;
|
|
1017
1219
|
return {
|
|
1018
1220
|
...resolved,
|
|
1019
1221
|
mode: "workspace-write"
|
|
1020
1222
|
};
|
|
1021
1223
|
};
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1224
|
+
try {
|
|
1225
|
+
for (const agent of this.services.agents.list()) if (this.isMain(agent.session)) this.forceApproval(agent.session);
|
|
1226
|
+
try {
|
|
1227
|
+
const runtime = runtimeFiles();
|
|
1228
|
+
if (runtime.ptcProcess === void 0 || runtime.ptcControl === void 0) this.record({
|
|
1229
|
+
kind: "notice",
|
|
1230
|
+
detail: "未找到 run_code 运行时(PTC)文件;沙箱内 run_code 可能不可用。"
|
|
1231
|
+
});
|
|
1232
|
+
} catch {
|
|
1233
|
+
this.record({
|
|
1234
|
+
kind: "notice",
|
|
1235
|
+
detail: "无法读取 run_code 运行时(PTC)文件;沙箱内 run_code 可能不可用。"
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
this.enabled = true;
|
|
1239
|
+
this.lastError = void 0;
|
|
1240
|
+
this.enableRetryAt = 0;
|
|
1241
|
+
this.record({
|
|
1242
|
+
kind: "notice",
|
|
1243
|
+
detail: "评测守护已启用。"
|
|
1244
|
+
});
|
|
1245
|
+
return { ok: true };
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
this.enabled = false;
|
|
1248
|
+
this.restoreHooks();
|
|
1249
|
+
return this.failEnable(error instanceof Error ? error.message : String(error));
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
/** Restore the original sandbox/policy methods (idempotent). */
|
|
1253
|
+
restoreHooks() {
|
|
1254
|
+
if (this.sandboxObject !== void 0 && this.origConfine !== void 0) this.sandboxObject.confine = this.origConfine;
|
|
1255
|
+
if (this.policyObject !== void 0 && this.origResolve !== void 0) this.policyObject.resolve = this.origResolve;
|
|
1256
|
+
this.sandboxObject = void 0;
|
|
1257
|
+
this.policyObject = void 0;
|
|
1258
|
+
this.origConfine = void 0;
|
|
1259
|
+
this.origResolve = void 0;
|
|
1029
1260
|
}
|
|
1030
1261
|
/** Turn the guard off without touching already-running confined processes. */
|
|
1031
1262
|
disable() {
|
|
1263
|
+
this.enableRetryAt = 0;
|
|
1032
1264
|
if (!this.enabled) return;
|
|
1033
1265
|
this.enabled = false;
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1266
|
+
this.restoreHooks();
|
|
1267
|
+
for (const sessionId of [...this.approvalPrev.keys()]) try {
|
|
1268
|
+
this.restoreApproval(sessionId);
|
|
1269
|
+
} catch {}
|
|
1037
1270
|
this.deadlines.clear();
|
|
1271
|
+
this.lastActivity.clear();
|
|
1038
1272
|
this.record({
|
|
1039
1273
|
kind: "notice",
|
|
1040
1274
|
detail: "评测守护已停用。"
|
|
@@ -1045,24 +1279,31 @@ var Guard = class {
|
|
|
1045
1279
|
const config = this.configuration;
|
|
1046
1280
|
if (!config.timeLimitEnabled) return;
|
|
1047
1281
|
this.deadlines.set(sessionId, Date.now() + config.timeLimitMinutes * 6e4);
|
|
1282
|
+
this.lastActivity.set(sessionId, Date.now());
|
|
1048
1283
|
}
|
|
1049
1284
|
/** A new main agent appeared. */
|
|
1050
1285
|
onAgentCreated(agent) {
|
|
1051
1286
|
if (!this.isMain(agent.session)) return;
|
|
1052
1287
|
this.knownAgents.add(agent.id);
|
|
1053
1288
|
if (this.enabled) this.forceApproval(agent.session);
|
|
1054
|
-
const cwd = agent.session
|
|
1055
|
-
if (cwd !== void 0) {
|
|
1056
|
-
|
|
1289
|
+
const cwd = sessionCwd(agent.session);
|
|
1290
|
+
if (cwd !== void 0 && this.enabled) {
|
|
1291
|
+
const existingRunId = this.sessionRuns.get(agent.id);
|
|
1292
|
+
const existing = existingRunId === void 0 ? void 0 : this.runs.get(existingRunId);
|
|
1293
|
+
if (existing !== void 0 && (existing.status === "active" || existing.status === "prepared")) return;
|
|
1294
|
+
try {
|
|
1057
1295
|
this.onAutoRun?.(agent, cwd);
|
|
1058
1296
|
} catch {}
|
|
1059
1297
|
}
|
|
1060
1298
|
}
|
|
1061
1299
|
/** An agent went away. */
|
|
1062
1300
|
onAgentDisposed(agent) {
|
|
1063
|
-
|
|
1301
|
+
try {
|
|
1302
|
+
this.restoreApproval(agent.id);
|
|
1303
|
+
} catch {}
|
|
1064
1304
|
this.deadlines.delete(agent.id);
|
|
1065
1305
|
this.startedAt.delete(agent.id);
|
|
1306
|
+
this.lastActivity.delete(agent.id);
|
|
1066
1307
|
const runId = this.sessionRuns.get(agent.id);
|
|
1067
1308
|
if (runId !== void 0) {
|
|
1068
1309
|
const run = this.runs.get(runId);
|
|
@@ -1087,7 +1328,7 @@ var Guard = class {
|
|
|
1087
1328
|
run.status = "active";
|
|
1088
1329
|
run.provider = agent.options?.provider;
|
|
1089
1330
|
run.model = agent.options?.model;
|
|
1090
|
-
run.title = agent.session
|
|
1331
|
+
run.title = sessionTitle(agent.session);
|
|
1091
1332
|
this.sessionRuns.set(agent.id, runId);
|
|
1092
1333
|
this.writeRun(run);
|
|
1093
1334
|
this.writeUsage(runId, this.zeroUsage(run.startedAt));
|
|
@@ -1098,8 +1339,10 @@ var Guard = class {
|
|
|
1098
1339
|
/** Session event intake. */
|
|
1099
1340
|
onSessionEvent(session, event) {
|
|
1100
1341
|
if (!this.enabled || session === void 0 || !this.isMain(session)) return;
|
|
1342
|
+
this.lastActivity.set(session.id, Date.now());
|
|
1101
1343
|
const runId = this.sessionRuns.get(session.id);
|
|
1102
1344
|
const run = runId === void 0 ? void 0 : this.runs.get(runId);
|
|
1345
|
+
const activeRun = run !== void 0 && run.status === "active";
|
|
1103
1346
|
if (event.type === "user/message") {
|
|
1104
1347
|
if ((event.data?.source)?.kind === "user") {
|
|
1105
1348
|
this.startedAt.set(session.id, Date.now());
|
|
@@ -1108,44 +1351,51 @@ var Guard = class {
|
|
|
1108
1351
|
}
|
|
1109
1352
|
} else if (event.type === "assistant/message") {
|
|
1110
1353
|
const usage = event.data?.usage;
|
|
1111
|
-
if (usage !== void 0 && runId !== void 0) this.addUsage(runId, usage);
|
|
1354
|
+
if (usage !== void 0 && runId !== void 0 && activeRun) this.addUsage(runId, usage);
|
|
1112
1355
|
} else if (event.type === "tool/call") {
|
|
1113
|
-
if (runId !== void 0) this.addToolCall(runId);
|
|
1356
|
+
if (runId !== void 0 && activeRun) this.addToolCall(runId);
|
|
1114
1357
|
} else if (event.type === "turn/end") {
|
|
1115
|
-
if (runId !== void 0) this.addTurn(runId);
|
|
1358
|
+
if (runId !== void 0 && activeRun) this.addTurn(runId);
|
|
1116
1359
|
this.maybeComplete(session.id);
|
|
1117
1360
|
} else if (event.type === "turn/start") {
|
|
1118
1361
|
if (run !== void 0 && run.startedAt === null) run.startedAt = Date.now();
|
|
1119
1362
|
}
|
|
1120
1363
|
}
|
|
1121
|
-
/** Clear a turn deadline once no work remains for that session. */
|
|
1364
|
+
/** Clear a turn deadline once no work remains for that session (with grace). */
|
|
1122
1365
|
maybeComplete(sessionId) {
|
|
1123
|
-
|
|
1124
|
-
const
|
|
1125
|
-
if (
|
|
1366
|
+
if (this.services.agents.get(sessionId)?.status === "running" || this.ownJobs(sessionId).length > 0) return;
|
|
1367
|
+
const last = this.lastActivity.get(sessionId) ?? 0;
|
|
1368
|
+
if (Date.now() - last < IDLE_GRACE_MS) return;
|
|
1126
1369
|
this.deadlines.delete(sessionId);
|
|
1127
1370
|
}
|
|
1128
1371
|
/** One-second clock: expire deadlines and tail supervisor audits. */
|
|
1129
1372
|
tick() {
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1373
|
+
try {
|
|
1374
|
+
this.syncConfig();
|
|
1375
|
+
if (!this.enabled) return;
|
|
1376
|
+
const now = Date.now();
|
|
1377
|
+
for (const [sessionId, deadline] of [...this.deadlines]) {
|
|
1378
|
+
if (deadline > now) {
|
|
1379
|
+
const agent = this.services.agents.get(sessionId);
|
|
1380
|
+
const ownJobs = this.ownJobs(sessionId).length > 0;
|
|
1381
|
+
const last = this.lastActivity.get(sessionId) ?? 0;
|
|
1382
|
+
const descendantRunning = this.descendants(sessionId).some((id) => {
|
|
1383
|
+
if (this.services.agents.get(id)?.status === "running") return true;
|
|
1384
|
+
return this.ownJobs(id).length > 0;
|
|
1385
|
+
});
|
|
1386
|
+
if (agent?.status !== "running" && !ownJobs && !descendantRunning && now - last >= IDLE_GRACE_MS) this.deadlines.delete(sessionId);
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
this.deadlines.delete(sessionId);
|
|
1390
|
+
this.expire(sessionId);
|
|
1143
1391
|
}
|
|
1144
|
-
this.
|
|
1145
|
-
this.
|
|
1392
|
+
this.tailAudits();
|
|
1393
|
+
this.cleanupTemp();
|
|
1394
|
+
} catch (error) {
|
|
1395
|
+
try {
|
|
1396
|
+
this.ctx.logger.warn("eval-guard: tick failed: %s", error instanceof Error ? error.message : String(error));
|
|
1397
|
+
} catch {}
|
|
1146
1398
|
}
|
|
1147
|
-
this.tailAudits();
|
|
1148
|
-
this.cleanupTemp();
|
|
1149
1399
|
}
|
|
1150
1400
|
/** Kill one session's turn, descendants, and jobs. */
|
|
1151
1401
|
expire(sessionId) {
|
|
@@ -1159,9 +1409,11 @@ var Guard = class {
|
|
|
1159
1409
|
reason
|
|
1160
1410
|
});
|
|
1161
1411
|
} catch {}
|
|
1162
|
-
for (const job of this.
|
|
1412
|
+
for (const job of this.ownJobs(id)) try {
|
|
1163
1413
|
this.services.jobs?.kill(job.id, id, "评测时间上限");
|
|
1164
1414
|
} catch {}
|
|
1415
|
+
this.deadlines.delete(id);
|
|
1416
|
+
this.lastActivity.delete(id);
|
|
1165
1417
|
}
|
|
1166
1418
|
const runId = this.sessionRuns.get(sessionId);
|
|
1167
1419
|
if (runId !== void 0) {
|
|
@@ -1176,8 +1428,10 @@ var Guard = class {
|
|
|
1176
1428
|
sessionId,
|
|
1177
1429
|
detail: reason
|
|
1178
1430
|
});
|
|
1431
|
+
return;
|
|
1179
1432
|
}
|
|
1180
|
-
}
|
|
1433
|
+
}
|
|
1434
|
+
this.record({
|
|
1181
1435
|
kind: "timeout",
|
|
1182
1436
|
sessionId,
|
|
1183
1437
|
detail: reason
|
|
@@ -1185,10 +1439,10 @@ var Guard = class {
|
|
|
1185
1439
|
}
|
|
1186
1440
|
/** Direct and transitive subagent sessions of one main session. */
|
|
1187
1441
|
descendants(sessionId) {
|
|
1188
|
-
const all = this.services.agents.list();
|
|
1189
1442
|
const children = /* @__PURE__ */ new Map();
|
|
1190
|
-
for (const agent of
|
|
1191
|
-
|
|
1443
|
+
for (const agent of this.services.agents.list()) {
|
|
1444
|
+
if (!sessionIsSubagent(agent.session)) continue;
|
|
1445
|
+
const parent = sessionParent(agent.session);
|
|
1192
1446
|
if (parent === void 0) continue;
|
|
1193
1447
|
const list = children.get(parent) ?? [];
|
|
1194
1448
|
list.push(agent.id);
|
|
@@ -1208,7 +1462,7 @@ var Guard = class {
|
|
|
1208
1462
|
checkTool(exec) {
|
|
1209
1463
|
if (!this.enabled) return void 0;
|
|
1210
1464
|
const agent = exec.agent;
|
|
1211
|
-
if (agent === void 0
|
|
1465
|
+
if (agent === void 0) return void 0;
|
|
1212
1466
|
const name = String(exec.name);
|
|
1213
1467
|
if (WAIT_TOOLS.includes(name)) {
|
|
1214
1468
|
this.record({
|
|
@@ -1228,8 +1482,17 @@ var Guard = class {
|
|
|
1228
1482
|
});
|
|
1229
1483
|
return "该工具在本次评测的黑名单中,调用已被阻止。";
|
|
1230
1484
|
}
|
|
1231
|
-
const workspace = agent.session
|
|
1232
|
-
if (workspace === void 0
|
|
1485
|
+
const workspace = sessionCwd(agent.session);
|
|
1486
|
+
if (workspace === void 0) {
|
|
1487
|
+
if (!isAuditedTool(name)) return void 0;
|
|
1488
|
+
this.record({
|
|
1489
|
+
kind: "path-denied",
|
|
1490
|
+
sessionId: agent.id,
|
|
1491
|
+
tool: name,
|
|
1492
|
+
detail: "会话没有工作区,无法审计路径。"
|
|
1493
|
+
});
|
|
1494
|
+
return "评测守护:无法确定会话工作区,文件类工具已被阻止。";
|
|
1495
|
+
}
|
|
1233
1496
|
const decision = auditToolCall(name, exec.arguments, workspace, this.pathRules());
|
|
1234
1497
|
if (!decision.allow) {
|
|
1235
1498
|
this.record({
|
|
@@ -1420,13 +1683,7 @@ var Guard = class {
|
|
|
1420
1683
|
}
|
|
1421
1684
|
/** Build the prompt for one task with only installed tools listed. */
|
|
1422
1685
|
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
|
-
});
|
|
1686
|
+
return task.prompt({ minutes: this.configuration.timeLimitMinutes });
|
|
1430
1687
|
}
|
|
1431
1688
|
/** The task definition. */
|
|
1432
1689
|
get definition() {
|
|
@@ -1440,6 +1697,45 @@ var Guard = class {
|
|
|
1440
1697
|
//#endregion
|
|
1441
1698
|
//#region src/jobs.ts
|
|
1442
1699
|
/** Persistent background jobs for task setup and single-result evaluation. */
|
|
1700
|
+
/**
|
|
1701
|
+
* The only host environment variables a setup/evaluation child inherits.
|
|
1702
|
+
*
|
|
1703
|
+
* Evaluation children execute agent-authored build scripts and binaries, so
|
|
1704
|
+
* the ambient DSH environment (credentials, session secrets, proxy settings)
|
|
1705
|
+
* must not leak into them. Everything the task needs is passed explicitly via
|
|
1706
|
+
* the EVAL_* variables in StartOptions.env.
|
|
1707
|
+
*/
|
|
1708
|
+
function baseEnvironment(source = process.env) {
|
|
1709
|
+
const keep = [
|
|
1710
|
+
"PATH",
|
|
1711
|
+
"HOME",
|
|
1712
|
+
"USER",
|
|
1713
|
+
"LOGNAME",
|
|
1714
|
+
"SHELL",
|
|
1715
|
+
"LANG",
|
|
1716
|
+
"LANGUAGE",
|
|
1717
|
+
"LC_ALL",
|
|
1718
|
+
"LC_CTYPE",
|
|
1719
|
+
"LC_MESSAGES",
|
|
1720
|
+
"TZ",
|
|
1721
|
+
"TERM",
|
|
1722
|
+
"TMPDIR",
|
|
1723
|
+
"TMP",
|
|
1724
|
+
"TEMP",
|
|
1725
|
+
"SSL_CERT_FILE",
|
|
1726
|
+
"SSL_CERT_DIR",
|
|
1727
|
+
"GIT_SSL_CAINFO",
|
|
1728
|
+
"REQUESTS_CA_BUNDLE",
|
|
1729
|
+
"CURL_CA_BUNDLE",
|
|
1730
|
+
"NODE_EXTRA_CA_CERTS"
|
|
1731
|
+
];
|
|
1732
|
+
const env = {};
|
|
1733
|
+
for (const key of keep) {
|
|
1734
|
+
const value = source[key];
|
|
1735
|
+
if (value !== void 0) env[key] = value;
|
|
1736
|
+
}
|
|
1737
|
+
return env;
|
|
1738
|
+
}
|
|
1443
1739
|
function now() {
|
|
1444
1740
|
return Date.now();
|
|
1445
1741
|
}
|
|
@@ -1461,15 +1757,65 @@ var JobManager = class {
|
|
|
1461
1757
|
try {
|
|
1462
1758
|
const stored = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1463
1759
|
if (stored.status === "running" || stored.status === "queued") {
|
|
1760
|
+
if (typeof stored.pid === "number" && stored.pid > 0 && this.stillOurProcess(stored)) try {
|
|
1761
|
+
process.kill(-stored.pid, "SIGTERM");
|
|
1762
|
+
} catch {
|
|
1763
|
+
try {
|
|
1764
|
+
process.kill(stored.pid, "SIGTERM");
|
|
1765
|
+
} catch {}
|
|
1766
|
+
}
|
|
1464
1767
|
stored.status = "interrupted";
|
|
1465
1768
|
stored.error = "服务重启,任务已中断";
|
|
1466
1769
|
stored.endedAt = now();
|
|
1467
1770
|
this.write(stored);
|
|
1771
|
+
if (stored.kind === "install" && stored.cleanupWorkspace !== void 0) {
|
|
1772
|
+
this.wipeInstallWorkspace(stored.cleanupWorkspace);
|
|
1773
|
+
if (stored.cifarToken !== void 0) this.cleanupCifarLock(stored.cifarToken);
|
|
1774
|
+
}
|
|
1468
1775
|
}
|
|
1469
1776
|
this.jobs.set(stored.id, stored);
|
|
1470
1777
|
} catch {}
|
|
1471
1778
|
}
|
|
1472
1779
|
}
|
|
1780
|
+
/** Remove every top-level entry an interrupted install could have created. */
|
|
1781
|
+
wipeInstallWorkspace(workspacePath) {
|
|
1782
|
+
try {
|
|
1783
|
+
for (const entry of fs.readdirSync(workspacePath)) fs.rmSync(path.join(workspacePath, entry), {
|
|
1784
|
+
recursive: true,
|
|
1785
|
+
force: true
|
|
1786
|
+
});
|
|
1787
|
+
} catch {}
|
|
1788
|
+
}
|
|
1789
|
+
/** Remove the CIFAR lock only when it carries this install's token. */
|
|
1790
|
+
cleanupCifarLock(token) {
|
|
1791
|
+
try {
|
|
1792
|
+
const lock = path.join(guardRoot(), "cache", "cifar10", ".download.lock");
|
|
1793
|
+
if (fs.existsSync(lock) && fs.readFileSync(lock, "utf8").includes("token=" + token)) fs.rmSync(lock, { force: true });
|
|
1794
|
+
} catch {}
|
|
1795
|
+
}
|
|
1796
|
+
/** Read the Linux process start time from /proc/<pid>/stat (field 22). */
|
|
1797
|
+
processStartTime(pid) {
|
|
1798
|
+
try {
|
|
1799
|
+
const stat = fs.readFileSync("/proc/" + String(pid) + "/stat", "utf8");
|
|
1800
|
+
const end = stat.lastIndexOf(")");
|
|
1801
|
+
if (end < 0) return void 0;
|
|
1802
|
+
return stat.slice(end + 2).trim().split(/\s+/)[19];
|
|
1803
|
+
} catch {
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
/** Whether a persisted PID still names the process this job started. */
|
|
1808
|
+
stillOurProcess(stored) {
|
|
1809
|
+
if (stored.pidStartTime !== void 0 && this.processStartTime(stored.pid) !== stored.pidStartTime) return false;
|
|
1810
|
+
if (stored.pidStartTime !== void 0) return true;
|
|
1811
|
+
try {
|
|
1812
|
+
const argv = fs.readFileSync("/proc/" + String(stored.pid) + "/cmdline", "utf8").split("\0").filter(Boolean);
|
|
1813
|
+
if (argv.length === 0) return false;
|
|
1814
|
+
return path.basename(argv[0]) === path.basename(stored.command[0] ?? "");
|
|
1815
|
+
} catch {
|
|
1816
|
+
return false;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1473
1819
|
dir(idValue) {
|
|
1474
1820
|
return path.join(this.root, idValue);
|
|
1475
1821
|
}
|
|
@@ -1497,7 +1843,9 @@ var JobManager = class {
|
|
|
1497
1843
|
command: options.command,
|
|
1498
1844
|
cwd: options.cwd,
|
|
1499
1845
|
logFile,
|
|
1500
|
-
resultFile: path.join(directory, "result.json")
|
|
1846
|
+
resultFile: path.join(directory, "result.json"),
|
|
1847
|
+
...options.cleanupWorkspace === void 0 ? {} : { cleanupWorkspace: options.cleanupWorkspace },
|
|
1848
|
+
...options.cifarToken === void 0 ? {} : { cifarToken: options.cifarToken }
|
|
1501
1849
|
};
|
|
1502
1850
|
this.jobs.set(jobId, job);
|
|
1503
1851
|
this.write(job);
|
|
@@ -1506,7 +1854,7 @@ var JobManager = class {
|
|
|
1506
1854
|
child = spawn(options.command[0], options.command.slice(1), {
|
|
1507
1855
|
cwd: options.cwd,
|
|
1508
1856
|
env: {
|
|
1509
|
-
...
|
|
1857
|
+
...baseEnvironment(),
|
|
1510
1858
|
...options.env ?? {},
|
|
1511
1859
|
PYTHONUNBUFFERED: "1",
|
|
1512
1860
|
EVAL_JOB_DIR: directory,
|
|
@@ -1528,6 +1876,11 @@ var JobManager = class {
|
|
|
1528
1876
|
return this.info(job);
|
|
1529
1877
|
}
|
|
1530
1878
|
this.children.set(jobId, child);
|
|
1879
|
+
if (child.pid !== void 0) {
|
|
1880
|
+
job.pid = child.pid;
|
|
1881
|
+
job.pidStartTime = this.processStartTime(child.pid);
|
|
1882
|
+
this.write(job);
|
|
1883
|
+
}
|
|
1531
1884
|
if (options.onCancel !== void 0) this.cancelHandlers.set(jobId, options.onCancel);
|
|
1532
1885
|
const append = (chunk) => {
|
|
1533
1886
|
try {
|
|
@@ -1536,17 +1889,29 @@ var JobManager = class {
|
|
|
1536
1889
|
};
|
|
1537
1890
|
child.stdout?.on("data", (chunk) => append(chunk));
|
|
1538
1891
|
child.stderr?.on("data", (chunk) => append(chunk));
|
|
1539
|
-
|
|
1892
|
+
let failureNotified = false;
|
|
1893
|
+
const fail = (error) => {
|
|
1894
|
+
if (job.status !== "running" && job.status !== "queued") return;
|
|
1540
1895
|
job.status = "failed";
|
|
1541
|
-
job.error = error
|
|
1896
|
+
job.error = error;
|
|
1542
1897
|
job.endedAt = now();
|
|
1543
1898
|
job.updatedAt = now();
|
|
1544
1899
|
this.write(job);
|
|
1900
|
+
if (failureNotified) return;
|
|
1901
|
+
failureNotified = true;
|
|
1902
|
+
try {
|
|
1903
|
+
options.onFailure?.(this.detail(jobId));
|
|
1904
|
+
} catch {}
|
|
1905
|
+
};
|
|
1906
|
+
child.on("error", (error) => {
|
|
1907
|
+
this.children.delete(jobId);
|
|
1908
|
+
this.cancelHandlers.delete(jobId);
|
|
1909
|
+
fail(error.message);
|
|
1545
1910
|
});
|
|
1546
1911
|
child.on("exit", (code, signal) => {
|
|
1547
1912
|
this.children.delete(jobId);
|
|
1548
1913
|
this.cancelHandlers.delete(jobId);
|
|
1549
|
-
if (job.status === "cancelled") return;
|
|
1914
|
+
if (job.status === "cancelled" || job.status === "failed") return;
|
|
1550
1915
|
if (code === 0) try {
|
|
1551
1916
|
const extra = options.onSuccess?.(this.detail(jobId));
|
|
1552
1917
|
if (extra !== void 0) {
|
|
@@ -1564,17 +1929,14 @@ var JobManager = class {
|
|
|
1564
1929
|
} catch (error) {
|
|
1565
1930
|
job.status = "failed";
|
|
1566
1931
|
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 {}
|
|
1932
|
+
if (!failureNotified) {
|
|
1933
|
+
failureNotified = true;
|
|
1934
|
+
try {
|
|
1935
|
+
options.onFailure?.(this.detail(jobId));
|
|
1936
|
+
} catch {}
|
|
1937
|
+
}
|
|
1577
1938
|
}
|
|
1939
|
+
else fail(signal !== null ? "进程被信号 " + signal + " 终止" : "退出码 " + String(code));
|
|
1578
1940
|
job.endedAt = now();
|
|
1579
1941
|
job.updatedAt = now();
|
|
1580
1942
|
this.write(job);
|
|
@@ -1802,7 +2164,7 @@ const TASKS = [
|
|
|
1802
2164
|
{
|
|
1803
2165
|
id: NEURAL_PENDULUM_TASK,
|
|
1804
2166
|
label: NEURAL_PENDULUM_TASK_LABEL,
|
|
1805
|
-
description: "从零实现纯 C++17
|
|
2167
|
+
description: "从零实现纯 C++17 神经网络基础设施(算子数值以评测侧 PyTorch 参考校验),用三个关节角度控制预编译三节摆起摆并稳定保持;评测固定初始状态,统计 t_can。",
|
|
1806
2168
|
timeLimitMinutes: 1440,
|
|
1807
2169
|
timeLimitEnabled: false,
|
|
1808
2170
|
needsSetup: true
|
|
@@ -1817,14 +2179,14 @@ const TANK_DEFINITION = {
|
|
|
1817
2179
|
timeLimitMinutes: 90,
|
|
1818
2180
|
timeLimitEnabled: true,
|
|
1819
2181
|
needsSetup: false,
|
|
1820
|
-
prompt() {
|
|
2182
|
+
prompt({ minutes }) {
|
|
1821
2183
|
return [
|
|
1822
2184
|
"按照要求编写尽可能强的坦克大战 bot。你的对手是其他顶尖 AI 模型写的 bot,它们可能主动设计战术。基准是一个中等强度、思路常规的 bot,它在正式比赛中是 0 分线;战胜基准不代表足够强,正式对手远强于基准。正式赛是 1 对 1 循环赛,用 Bradley-Terry 模型拟合实力值。",
|
|
1823
2185
|
"",
|
|
1824
2186
|
"解压 workspace.zip,阅读 README.md 和 RULES.md,然后开始。",
|
|
1825
2187
|
"",
|
|
1826
|
-
"比赛是连续
|
|
1827
|
-
"- 你有
|
|
2188
|
+
"比赛是连续 " + minutes + " 分钟的单次任务:",
|
|
2189
|
+
"- 你有 " + minutes + " 分钟修改 bot,从对话开始计时;到 " + minutes + " 分钟强制停止;",
|
|
1828
2190
|
"- 最终只看 bot 的强度,提前交卷没有任何好处;觉得没什么可改了,先换一个思路再试;",
|
|
1829
2191
|
"- 到点时 workspace/submission/bot.cpp 是什么就交什么:不要让它长时间停在编译不过的状态;",
|
|
1830
2192
|
"- 允许使用本地工具、对战和复盘;禁止联网,禁止求助其他模型(包括启动子代理:subagent、Task / Agent 工具、后台并行代理之类),除主办方发给你的材料外禁止查看其他选手的资料;",
|
|
@@ -1846,7 +2208,7 @@ const WAREHOUSE_DEFINITION = {
|
|
|
1846
2208
|
timeLimitMinutes: 90,
|
|
1847
2209
|
timeLimitEnabled: true,
|
|
1848
2210
|
needsSetup: false,
|
|
1849
|
-
prompt() {
|
|
2211
|
+
prompt({ minutes }) {
|
|
1850
2212
|
return [
|
|
1851
2213
|
"这是一场 AI 编程赛。你接手了一个代码混乱的老系统:src/ 里是一个仓储机器人调度仿真系统,功能正确,但运行很慢。你的任务:在输出完全不变的前提下,让它尽可能快。",
|
|
1852
2214
|
"",
|
|
@@ -1855,7 +2217,7 @@ const WAREHOUSE_DEFINITION = {
|
|
|
1855
2217
|
"要点(详见 TASK.md):",
|
|
1856
2218
|
"- 只能修改 src/;编译命令固定为 g++ -O2 -std=c++20 src/*.cpp,只用 C++ 标准库;",
|
|
1857
2219
|
"- 对任何合法输入(以 tools/check_input.py --full 为准),输出都必须与基线逐字节相同;SPEC.md 是过时的旧文档,一切以基线代码的实际行为为准;",
|
|
1858
|
-
"- 本次是连续
|
|
2220
|
+
"- 本次是连续 " + minutes + " 分钟的单次任务;clock.py 从解压那一刻开始计算,实际计时从对话开始计算,它只能作为参考,请尽快解压;",
|
|
1859
2221
|
"- 只能读写当前文件夹里的文件;可以使用本环境的所有工具,但不能联网搜索;",
|
|
1860
2222
|
"- 不要向我提问,也不要等待我确认,直到完成为止。",
|
|
1861
2223
|
"",
|
|
@@ -1872,11 +2234,11 @@ const LAND_DEFINITION = {
|
|
|
1872
2234
|
timeLimitMinutes: 90,
|
|
1873
2235
|
timeLimitEnabled: true,
|
|
1874
2236
|
needsSetup: false,
|
|
1875
|
-
prompt() {
|
|
2237
|
+
prompt({ minutes }) {
|
|
1876
2238
|
return [
|
|
1877
2239
|
"这是一场正式的 AI 编程对抗赛,你的对手是其他顶尖 AI 模型写的 bot,它们会主动切断你、抢你的地。比赛结果会公开排名,请拿出你的最高水平。",
|
|
1878
2240
|
"",
|
|
1879
|
-
"请解压当前目录的 workspace.zip,进入 workspace 文件夹,按 TASK.md 的要求编写一个尽可能强的圈地对战 bot,保存为 workspace/submission/bot.cpp。时间预算约 60 分钟,请自己把握进度,做完就可以结束;超过
|
|
2241
|
+
"请解压当前目录的 workspace.zip,进入 workspace 文件夹,按 TASK.md 的要求编写一个尽可能强的圈地对战 bot,保存为 workspace/submission/bot.cpp。时间预算约 " + Math.min(60, minutes) + " 分钟,请自己把握进度,做完就可以结束;超过 " + minutes + " 分钟会被强制停止。只能读写当前文件夹里的文件,不能联网搜索,不要向我提问。"
|
|
1880
2242
|
].join("\n");
|
|
1881
2243
|
}
|
|
1882
2244
|
};
|
|
@@ -1889,8 +2251,8 @@ const BOMBERMAN_DEFINITION = {
|
|
|
1889
2251
|
timeLimitMinutes: 90,
|
|
1890
2252
|
timeLimitEnabled: true,
|
|
1891
2253
|
needsSetup: false,
|
|
1892
|
-
prompt() {
|
|
1893
|
-
return ["按照要求编写尽可能强bot,你的对手是全球其他顶尖ai,可能主动设计战术。基准只是很弱的bot,它在正式比赛为0分线。正式赛会对战多轮,用Bradley-Terry模型拟合实力值。", "解压 workspace.zip,阅读 README.md 和 RULES.md,编写支持2–10人的炸弹人 bot。尽量60分钟完成,
|
|
2254
|
+
prompt({ minutes }) {
|
|
2255
|
+
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
2256
|
}
|
|
1895
2257
|
};
|
|
1896
2258
|
/** CTF bot task: original participant prompt, unchanged. */
|
|
@@ -1902,28 +2264,17 @@ const CTF_DEFINITION = {
|
|
|
1902
2264
|
timeLimitMinutes: 90,
|
|
1903
2265
|
timeLimitEnabled: true,
|
|
1904
2266
|
needsSetup: false,
|
|
1905
|
-
prompt() {
|
|
1906
|
-
return ["按照要求编写尽可能强的 bot,你的对手是全球其他顶尖 AI,可能主动设计战术。基准是一个中等强度、思路常规的 bot,它在正式比赛是 0 分线;战胜基准不代表足够强,正式对手远强于基准。正式赛会对战多轮,用 Bradley-Terry 模型拟合实力值。", "解压 workspace.zip,阅读 README.md 和 RULES.md,编写支持 2–15 方的夺旗 bot。你有 60 分钟,只看最终 bot 的强度,提前交卷不加分,请充分利用时间持续改进;可以适当超时,但到
|
|
2267
|
+
prompt({ minutes }) {
|
|
2268
|
+
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
2269
|
}
|
|
1908
2270
|
};
|
|
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
2271
|
/** Commit0 single-library task. */
|
|
1921
2272
|
const COMMIT0_DEFINITION = {
|
|
1922
2273
|
id: COMMIT0_TASK,
|
|
1923
2274
|
label: COMMIT0_TASK_LABEL,
|
|
1924
2275
|
description: TASKS[5].description,
|
|
1925
2276
|
asset: path.join(packageRoot(), "assets", "commit0", "workspace.zip"),
|
|
1926
|
-
|
|
2277
|
+
testMaterial: ["tests", "pytest.ini"],
|
|
1927
2278
|
timeLimitMinutes: 480,
|
|
1928
2279
|
timeLimitEnabled: false,
|
|
1929
2280
|
needsSetup: true,
|
|
@@ -1954,7 +2305,11 @@ const SWE_BENCH_PRO_DEFINITION = {
|
|
|
1954
2305
|
label: SWE_BENCH_PRO_TASK_LABEL,
|
|
1955
2306
|
description: TASKS[6].description,
|
|
1956
2307
|
asset: path.join(packageRoot(), "assets", "swe-bench-pro", "workspace.zip"),
|
|
1957
|
-
|
|
2308
|
+
testMaterial: [
|
|
2309
|
+
"test",
|
|
2310
|
+
"pytest.ini",
|
|
2311
|
+
"conftest.py"
|
|
2312
|
+
],
|
|
1958
2313
|
timeLimitMinutes: 1440,
|
|
1959
2314
|
timeLimitEnabled: false,
|
|
1960
2315
|
needsSetup: true,
|
|
@@ -1989,7 +2344,7 @@ const FEATUREBENCH_DEFINITION = {
|
|
|
1989
2344
|
label: FEATUREBENCH_TASK_LABEL,
|
|
1990
2345
|
description: TASKS[7].description,
|
|
1991
2346
|
asset: path.join(packageRoot(), "assets", "featurebench", "workspace.zip"),
|
|
1992
|
-
|
|
2347
|
+
testMaterial: ["tests", "conftest.py"],
|
|
1993
2348
|
timeLimitMinutes: 1440,
|
|
1994
2349
|
timeLimitEnabled: false,
|
|
1995
2350
|
needsSetup: true,
|
|
@@ -2123,16 +2478,16 @@ const NEURAL_PENDULUM_DEFINITION = {
|
|
|
2123
2478
|
return [
|
|
2124
2479
|
"你是一名控制与神经网络工程师。任务:从零实现纯 C++17、仅 CPU、fp32 的神经网络基础设施,并用它训练一个控制器,控制预编译的三节摆从接近全向下起摆,使三根杆全部进入竖直向上容差并尽可能长时间保持。",
|
|
2125
2480
|
"",
|
|
2126
|
-
"请先完整阅读 TASK.md;它是权威规则。硬约束:仅 C++17 标准库、仅 CPU、fp32;不使用 Python/OpenCL/CUDA/BLAS/Eigen/第三方库;必须从提交源码构建出 build/
|
|
2481
|
+
"请先完整阅读 TASK.md;它是权威规则。硬约束:仅 C++17 标准库、仅 CPU、fp32;不使用 Python/OpenCL/CUDA/BLAS/Eigen/第三方库;必须从提交源码构建出 build/nn_task 与 build/control。",
|
|
2127
2482
|
"",
|
|
2128
2483
|
"环境细节:joint1 与 joint3 驱动,joint2 被动;控制频率 100 Hz;每回合最多 3000 步(30 s),达到上限后继续 step 返回 {\"error\":\"episode_finished\"},必须 reset。环境只返回三个关节角度(弧度,不除 pi),不返回 reward/loss/cost/done;环境不提供杆长、质量、惯量、角速度或 tip。训练 reset 带小范围随机初始角度和角速度;评测使用固定初始状态,无随机扰动。",
|
|
2129
2484
|
"",
|
|
2130
2485
|
"优化目标:从接近全向下起摆,使三根杆全部进入 0.05 rad 的竖直向上容差,并尽可能长时间保持;评测指标为 t_can。损失函数、奖励、优化器、网络结构和训练方法由你自行设计,不规定任何训练方案。",
|
|
2131
2486
|
"",
|
|
2132
|
-
"
|
|
2487
|
+
"评测会从干净副本重新构建提交源码,先运行与 Neural-FP32/Neural-CIFAR 相同的算子数值评测:评测侧用 PyTorch 在相同数据上计算参考值,逐操作比较加权平均相对误差并对照阈值,任一算子不可执行、崩溃、超时、输出缺失或误差超阈值即判为基础设施失败;然后在固定初始状态上驱动 build/control,统计连续全向上保持时间 t_can。不要把任何未提交的二进制、临时构建产物或外部进程当作交付依赖。",
|
|
2133
2488
|
"使用宿主工具 eval_submit 提交当前源码快照,使用 eval_test 请求宿主评测最新提交。提交快照、事件、测试结果和最终分数全部保存在工作区之外的宿主目录;不要尝试读取或修改这些数据,也不要依赖任何工作区内的测试结果文件。最终评测使用最新提交快照,按宿主事件时间积分计算分数。",
|
|
2134
2489
|
"",
|
|
2135
|
-
"工作方式:先完成最小可构建的基础设施和 control
|
|
2490
|
+
"工作方式:先完成最小可构建的基础设施和 control 协议;再持续做控制/训练实验并记录可复现结果;最终保证工作区从零构建、build/nn_task 算子数值评测通过、control 可被评测驱动,并留下 NOTES.md/RESULTS.md。",
|
|
2136
2491
|
"",
|
|
2137
2492
|
"不要向我提问,也不要等待确认,独立完成。"
|
|
2138
2493
|
].join("\n");
|
|
@@ -2153,7 +2508,47 @@ const TASK_DEFINITIONS = {
|
|
|
2153
2508
|
[NEURAL_CIFAR_TASK]: NEURAL_CIFAR_DEFINITION,
|
|
2154
2509
|
[NEURAL_PENDULUM_TASK]: NEURAL_PENDULUM_DEFINITION
|
|
2155
2510
|
};
|
|
2156
|
-
|
|
2511
|
+
/** SHA-256 of one file. */
|
|
2512
|
+
function sha256File(file) {
|
|
2513
|
+
const hash = createHash("sha256");
|
|
2514
|
+
hash.update(fs.readFileSync(file));
|
|
2515
|
+
return hash.digest("hex");
|
|
2516
|
+
}
|
|
2517
|
+
/** Order-stable SHA-256 of a directory tree (relative paths + contents). */
|
|
2518
|
+
function sha256Tree(dir) {
|
|
2519
|
+
const hash = createHash("sha256");
|
|
2520
|
+
const walk = (current) => {
|
|
2521
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2522
|
+
const full = path.join(current, entry.name);
|
|
2523
|
+
hash.update(path.relative(dir, full));
|
|
2524
|
+
if (entry.isDirectory()) walk(full);
|
|
2525
|
+
else if (entry.isFile()) hash.update(fs.readFileSync(full));
|
|
2526
|
+
else if (entry.isSymbolicLink()) hash.update("link:" + fs.readlinkSync(full));
|
|
2527
|
+
}
|
|
2528
|
+
};
|
|
2529
|
+
walk(dir);
|
|
2530
|
+
return hash.digest("hex");
|
|
2531
|
+
}
|
|
2532
|
+
/** The venv site-packages directories that exist under one workspace venv. */
|
|
2533
|
+
function sitePackageDirs(venv) {
|
|
2534
|
+
const out = [];
|
|
2535
|
+
for (const lib of ["lib", "lib64"]) {
|
|
2536
|
+
const base = path.join(venv, lib);
|
|
2537
|
+
let entries = [];
|
|
2538
|
+
try {
|
|
2539
|
+
entries = fs.readdirSync(base, { withFileTypes: true });
|
|
2540
|
+
} catch {
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
for (const entry of entries) {
|
|
2544
|
+
if (!entry.isDirectory() || !entry.name.startsWith("python")) continue;
|
|
2545
|
+
const site = path.join(base, entry.name, "site-packages");
|
|
2546
|
+
if (fs.existsSync(site)) out.push(site);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
return out;
|
|
2550
|
+
}
|
|
2551
|
+
var TaskService = class TaskService {
|
|
2157
2552
|
guard;
|
|
2158
2553
|
registry;
|
|
2159
2554
|
jobs;
|
|
@@ -2196,7 +2591,14 @@ var TaskService = class {
|
|
|
2196
2591
|
".submissions",
|
|
2197
2592
|
"node_modules",
|
|
2198
2593
|
"__pycache__",
|
|
2199
|
-
".venv"
|
|
2594
|
+
".venv",
|
|
2595
|
+
".tmp",
|
|
2596
|
+
".home",
|
|
2597
|
+
".cache",
|
|
2598
|
+
".config",
|
|
2599
|
+
".local",
|
|
2600
|
+
"data",
|
|
2601
|
+
"work"
|
|
2200
2602
|
]);
|
|
2201
2603
|
let count = 0;
|
|
2202
2604
|
const walk = (dir) => {
|
|
@@ -2265,13 +2667,85 @@ var TaskService = class {
|
|
|
2265
2667
|
files
|
|
2266
2668
|
};
|
|
2267
2669
|
}
|
|
2268
|
-
|
|
2670
|
+
/** Map an evaluator result that has no explicit stage (older evaluator builds). */
|
|
2671
|
+
inferStage(value, error) {
|
|
2672
|
+
if (value.infra_ok === true) return value.success === true ? "completed" : "scoring";
|
|
2673
|
+
const text = error.toLowerCase();
|
|
2674
|
+
if (text.includes("cmake") || text.includes("build")) return "compile";
|
|
2675
|
+
if (text.includes("infrastructure") || text.includes("operator") || text.includes("mre")) return "infra";
|
|
2676
|
+
if (text.includes("controller") || text.includes("torque")) return "controller";
|
|
2677
|
+
return "simulation";
|
|
2678
|
+
}
|
|
2679
|
+
/** The target hold time used by the neural-pendulum scoring integral. */
|
|
2680
|
+
static FINAL_T_TGT = 10;
|
|
2681
|
+
/** One test event's instantaneous score, mirroring the task specification. */
|
|
2682
|
+
static eventScore(event) {
|
|
2683
|
+
const stage = String(event["stage"] ?? "");
|
|
2684
|
+
if (stage !== "scoring" && stage !== "completed" && event["valid"] !== true) return -1;
|
|
2685
|
+
const value = Number(event["t_can"] ?? 0);
|
|
2686
|
+
if (!Number.isFinite(value)) return -1;
|
|
2687
|
+
return (Math.min(Math.max(value, 0), TaskService.FINAL_T_TGT) - TaskService.FINAL_T_TGT) / TaskService.FINAL_T_TGT;
|
|
2688
|
+
}
|
|
2689
|
+
/**
|
|
2690
|
+
* Host-side authoritative final score: integral of the per-test score over
|
|
2691
|
+
* time, applying every test event at its REQUEST time and stopping at the
|
|
2692
|
+
* first completed request. Mirrors neural-pendulum-eval/run_eval.py, but the
|
|
2693
|
+
* host owns it because only the host knows the request timestamp and the
|
|
2694
|
+
* current test's outcome before the next test arrives.
|
|
2695
|
+
*/
|
|
2696
|
+
static finalScoreFromEvents(startedAt, events, now) {
|
|
2697
|
+
const tests = events.filter((item) => (item["event"] ?? item["type"]) === "test");
|
|
2698
|
+
if (tests.length === 0) return -1 * Math.max(0, now - startedAt);
|
|
2699
|
+
let doneAt;
|
|
2700
|
+
let previous = startedAt;
|
|
2701
|
+
let current = -1;
|
|
2702
|
+
let total = 0;
|
|
2703
|
+
for (const item of tests) {
|
|
2704
|
+
const raw = Number(item["time"] ?? item["at"] ?? previous);
|
|
2705
|
+
const at = Number.isFinite(raw) ? Math.max(raw, previous) : previous;
|
|
2706
|
+
total += current * (at - previous);
|
|
2707
|
+
current = TaskService.eventScore(item);
|
|
2708
|
+
previous = at;
|
|
2709
|
+
if (item["completed"] === true) {
|
|
2710
|
+
doneAt = at;
|
|
2711
|
+
break;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
total += current * Math.max(0, (doneAt ?? now) - previous);
|
|
2715
|
+
return total;
|
|
2716
|
+
}
|
|
2717
|
+
/** Read host test events; malformed lines are ignored. */
|
|
2718
|
+
readHostEvents(workspaceId) {
|
|
2719
|
+
const file = this.hostEvents(workspaceId);
|
|
2720
|
+
if (!fs.existsSync(file)) return [];
|
|
2721
|
+
const out = [];
|
|
2722
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
2723
|
+
if (line.trim() === "") continue;
|
|
2724
|
+
try {
|
|
2725
|
+
const value = JSON.parse(line);
|
|
2726
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) out.push(value);
|
|
2727
|
+
} catch {}
|
|
2728
|
+
}
|
|
2729
|
+
return out;
|
|
2730
|
+
}
|
|
2731
|
+
/** The task start time recorded at first submission, falling back to the run creation time. */
|
|
2732
|
+
hostStartedAt(workspaceId, run) {
|
|
2733
|
+
try {
|
|
2734
|
+
const meta = JSON.parse(fs.readFileSync(path.join(this.hostDir(workspaceId), "meta.json"), "utf8"));
|
|
2735
|
+
const value = Number(meta.started_at);
|
|
2736
|
+
if (Number.isFinite(value) && value > 0) return value;
|
|
2737
|
+
} catch {}
|
|
2738
|
+
return run.createdAt / 1e3;
|
|
2739
|
+
}
|
|
2740
|
+
/** @param startedAtSeconds - task start in epoch seconds (from host meta). */
|
|
2741
|
+
async runHostEval(workspaceId, startedAtSeconds) {
|
|
2269
2742
|
const latest = this.hostLatestSubmission(workspaceId);
|
|
2270
2743
|
if (latest === void 0) return {
|
|
2271
2744
|
stage: "no_submission",
|
|
2272
2745
|
t_can: 0,
|
|
2273
2746
|
completed: false,
|
|
2274
|
-
error: ""
|
|
2747
|
+
error: "",
|
|
2748
|
+
final_score: 0
|
|
2275
2749
|
};
|
|
2276
2750
|
const temp = fs.mkdtempSync(path.join(guardRoot(), "pend-eval-"));
|
|
2277
2751
|
try {
|
|
@@ -2281,16 +2755,14 @@ var TaskService = class {
|
|
|
2281
2755
|
const events = this.hostEvents(workspaceId);
|
|
2282
2756
|
fs.writeFileSync(path.join(temp, ".eval", "events.jsonl"), fs.existsSync(events) ? fs.readFileSync(events) : "", "utf8");
|
|
2283
2757
|
fs.writeFileSync(path.join(temp, ".eval", "meta.json"), JSON.stringify({
|
|
2284
|
-
started_at:
|
|
2758
|
+
started_at: startedAtSeconds,
|
|
2285
2759
|
t_tgt: 10
|
|
2286
2760
|
}), "utf8");
|
|
2287
2761
|
const resultFile = path.join(temp, "result.json");
|
|
2288
2762
|
const script = path.join(packageRoot(), "neural-pendulum-eval", "run_eval.py");
|
|
2289
2763
|
const env = {
|
|
2290
|
-
...
|
|
2291
|
-
EVAL_RESULT_FILE: resultFile
|
|
2292
|
-
PYTHONPATH: "",
|
|
2293
|
-
PYTHONHOME: ""
|
|
2764
|
+
...baseEnvironment(),
|
|
2765
|
+
EVAL_RESULT_FILE: resultFile
|
|
2294
2766
|
};
|
|
2295
2767
|
const child = spawn("python3", [
|
|
2296
2768
|
"-X",
|
|
@@ -2298,35 +2770,61 @@ var TaskService = class {
|
|
|
2298
2770
|
script,
|
|
2299
2771
|
"--workspace",
|
|
2300
2772
|
temp
|
|
2301
|
-
], {
|
|
2773
|
+
], {
|
|
2774
|
+
env,
|
|
2775
|
+
detached: true
|
|
2776
|
+
});
|
|
2302
2777
|
let stderr = "";
|
|
2303
2778
|
child.stderr?.on("data", (chunk) => {
|
|
2304
2779
|
stderr += String(chunk);
|
|
2305
2780
|
});
|
|
2781
|
+
let timedOut = false;
|
|
2306
2782
|
await new Promise((resolve, reject) => {
|
|
2307
|
-
|
|
2308
|
-
|
|
2783
|
+
const timer = setTimeout(() => {
|
|
2784
|
+
timedOut = true;
|
|
2785
|
+
try {
|
|
2786
|
+
if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
|
|
2787
|
+
} catch {}
|
|
2788
|
+
try {
|
|
2789
|
+
child.kill("SIGKILL");
|
|
2790
|
+
} catch {}
|
|
2791
|
+
resolve();
|
|
2792
|
+
}, 12e5);
|
|
2793
|
+
child.on("error", (error) => {
|
|
2794
|
+
clearTimeout(timer);
|
|
2795
|
+
reject(error);
|
|
2796
|
+
});
|
|
2797
|
+
child.on("close", () => {
|
|
2798
|
+
clearTimeout(timer);
|
|
2799
|
+
resolve();
|
|
2800
|
+
});
|
|
2309
2801
|
});
|
|
2802
|
+
if (timedOut) return {
|
|
2803
|
+
stage: "infra",
|
|
2804
|
+
t_can: 0,
|
|
2805
|
+
completed: false,
|
|
2806
|
+
error: "evaluation timed out after 20 minutes",
|
|
2807
|
+
final_score: 0
|
|
2808
|
+
};
|
|
2310
2809
|
if (!fs.existsSync(resultFile)) return {
|
|
2311
|
-
stage: "
|
|
2810
|
+
stage: "infra",
|
|
2312
2811
|
t_can: 0,
|
|
2313
2812
|
completed: false,
|
|
2314
|
-
error: stderr || "evaluator failed"
|
|
2813
|
+
error: stderr || "evaluator failed",
|
|
2814
|
+
final_score: 0
|
|
2315
2815
|
};
|
|
2316
2816
|
const value = JSON.parse(fs.readFileSync(resultFile, "utf8"));
|
|
2317
2817
|
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";
|
|
2818
|
+
const rawStage = String(value.stage ?? "");
|
|
2819
|
+
const stage = rawStage !== "" ? rawStage : this.inferStage(value, error);
|
|
2820
|
+
const tCan = Number(value.t_can ?? 0);
|
|
2821
|
+
const finalScore = Number(value.final_score ?? 0);
|
|
2325
2822
|
return {
|
|
2326
2823
|
stage,
|
|
2327
|
-
t_can: Number(
|
|
2328
|
-
completed: value.success === true,
|
|
2329
|
-
error
|
|
2824
|
+
t_can: Number.isFinite(tCan) ? tCan : 0,
|
|
2825
|
+
completed: stage === "completed" && value.success === true,
|
|
2826
|
+
error,
|
|
2827
|
+
final_score: Number.isFinite(finalScore) ? finalScore : 0
|
|
2330
2828
|
};
|
|
2331
2829
|
} finally {
|
|
2332
2830
|
fs.rmSync(temp, {
|
|
@@ -2343,11 +2841,12 @@ var TaskService = class {
|
|
|
2343
2841
|
stage: "completed",
|
|
2344
2842
|
error: "task already completed"
|
|
2345
2843
|
};
|
|
2844
|
+
const requestedAt = Date.now() / 1e3;
|
|
2346
2845
|
const latest = this.hostLatestSubmission(run.workspaceId);
|
|
2347
2846
|
if (latest === void 0) {
|
|
2348
2847
|
this.appendHostEvent(run.workspaceId, {
|
|
2349
2848
|
event: "test",
|
|
2350
|
-
time:
|
|
2849
|
+
time: requestedAt,
|
|
2351
2850
|
submission_id: "",
|
|
2352
2851
|
stage: "no_submission",
|
|
2353
2852
|
t_can: 0,
|
|
@@ -2362,20 +2861,29 @@ var TaskService = class {
|
|
|
2362
2861
|
error: ""
|
|
2363
2862
|
};
|
|
2364
2863
|
}
|
|
2365
|
-
const
|
|
2864
|
+
const startedAt = this.hostStartedAt(run.workspaceId, run);
|
|
2865
|
+
const value = await this.runHostEval(run.workspaceId, startedAt);
|
|
2366
2866
|
const stage = String(value.stage ?? "simulation");
|
|
2367
2867
|
const tCan = Number(value.t_can ?? 0);
|
|
2368
|
-
const completed = stage === "completed" && Number.isFinite(tCan);
|
|
2369
|
-
|
|
2868
|
+
const completed = stage === "completed" && Number.isFinite(tCan) && tCan >= 10;
|
|
2869
|
+
const event = {
|
|
2370
2870
|
event: "test",
|
|
2371
|
-
time:
|
|
2871
|
+
time: requestedAt,
|
|
2372
2872
|
submission_id: latest.id,
|
|
2373
2873
|
stage,
|
|
2374
2874
|
t_can: tCan,
|
|
2375
2875
|
completed,
|
|
2376
2876
|
error: String(value.error ?? "")
|
|
2377
|
-
}
|
|
2378
|
-
|
|
2877
|
+
};
|
|
2878
|
+
const finalScore = TaskService.finalScoreFromEvents(startedAt, [...this.readHostEvents(run.workspaceId), event], Date.now() / 1e3);
|
|
2879
|
+
event["final_score"] = finalScore;
|
|
2880
|
+
this.appendHostEvent(run.workspaceId, event);
|
|
2881
|
+
if (completed) fs.writeFileSync(done, JSON.stringify({
|
|
2882
|
+
at: requestedAt,
|
|
2883
|
+
stage,
|
|
2884
|
+
t_can: tCan,
|
|
2885
|
+
final_score: finalScore
|
|
2886
|
+
}) + "\n", "utf8");
|
|
2379
2887
|
return {
|
|
2380
2888
|
ok: true,
|
|
2381
2889
|
stage,
|
|
@@ -2386,17 +2894,28 @@ var TaskService = class {
|
|
|
2386
2894
|
}
|
|
2387
2895
|
/** Build a temporary workspace containing the host-side authoritative state. */
|
|
2388
2896
|
stageHostWorkspace(workspaceId) {
|
|
2389
|
-
|
|
2897
|
+
const latest = this.hostLatestSubmission(workspaceId);
|
|
2898
|
+
if (latest === void 0) return void 0;
|
|
2390
2899
|
const temp = fs.mkdtempSync(path.join(guardRoot(), "pend-eval-view-"));
|
|
2391
2900
|
const state = path.join(temp, ".eval");
|
|
2392
2901
|
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 });
|
|
2902
|
+
fs.cpSync(latest.path, path.join(state, "submissions", latest.id), { recursive: true });
|
|
2395
2903
|
const events = this.hostEvents(workspaceId);
|
|
2396
2904
|
fs.writeFileSync(path.join(state, "events.jsonl"), fs.existsSync(events) ? fs.readFileSync(events) : "", "utf8");
|
|
2397
2905
|
const meta = path.join(this.hostDir(workspaceId), "meta.json");
|
|
2906
|
+
let fallbackStart = Date.now() / 1e3;
|
|
2907
|
+
try {
|
|
2908
|
+
for (const line of (fs.existsSync(events) ? fs.readFileSync(events, "utf8") : "").split("\n")) {
|
|
2909
|
+
if (line.trim() === "") continue;
|
|
2910
|
+
const time = Number(JSON.parse(line).time);
|
|
2911
|
+
if (Number.isFinite(time) && time > 0) {
|
|
2912
|
+
fallbackStart = time;
|
|
2913
|
+
break;
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
} catch {}
|
|
2398
2917
|
fs.writeFileSync(path.join(state, "meta.json"), fs.existsSync(meta) ? fs.readFileSync(meta) : JSON.stringify({
|
|
2399
|
-
started_at:
|
|
2918
|
+
started_at: fallbackStart,
|
|
2400
2919
|
t_tgt: 10
|
|
2401
2920
|
}), "utf8");
|
|
2402
2921
|
return temp;
|
|
@@ -2455,6 +2974,67 @@ var TaskService = class {
|
|
|
2455
2974
|
if (index === "custom") return (options.customPipUrl ?? "").trim() || "https://pypi.org/simple";
|
|
2456
2975
|
return PIP_MIRRORS.find((item) => item.id === index)?.url ?? "https://pypi.org/simple";
|
|
2457
2976
|
}
|
|
2977
|
+
/** Host-side pristine material for one workspace (tests, venv manifest). */
|
|
2978
|
+
baselineDir(workspaceId) {
|
|
2979
|
+
return path.join(guardRoot(), "task-baseline", workspaceId.replace(/[^A-Za-z0-9_.-]/g, "_"));
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Snapshot the test material a benchmark task ships with, so an agent cannot
|
|
2983
|
+
* pass by editing or weakening the tests. Runs once after a successful
|
|
2984
|
+
* install; the evaluator restores it before scoring.
|
|
2985
|
+
*/
|
|
2986
|
+
snapshotTestMaterial(definition, workspaceId, workspacePath) {
|
|
2987
|
+
const names = definition.testMaterial ?? [];
|
|
2988
|
+
if (names.length === 0) return;
|
|
2989
|
+
const target = this.baselineDir(workspaceId);
|
|
2990
|
+
fs.mkdirSync(target, { recursive: true });
|
|
2991
|
+
for (const name of names) {
|
|
2992
|
+
const source = path.join(workspacePath, name);
|
|
2993
|
+
if (!fs.existsSync(source)) continue;
|
|
2994
|
+
const destination = path.join(target, name);
|
|
2995
|
+
fs.rmSync(destination, {
|
|
2996
|
+
recursive: true,
|
|
2997
|
+
force: true
|
|
2998
|
+
});
|
|
2999
|
+
fs.cpSync(source, destination, {
|
|
3000
|
+
recursive: true,
|
|
3001
|
+
dereference: false
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
fs.writeFileSync(path.join(target, "manifest.json"), JSON.stringify({
|
|
3005
|
+
task: definition.id,
|
|
3006
|
+
names
|
|
3007
|
+
}, null, 2) + "\n", "utf8");
|
|
3008
|
+
}
|
|
3009
|
+
/**
|
|
3010
|
+
* Record a fingerprint of the venv's control plane (python target, pyvenv.cfg,
|
|
3011
|
+
* .pth files, pytest packages). The evaluator refuses to trust a venv whose
|
|
3012
|
+
* fingerprint no longer matches, which closes the "replace bin/python or
|
|
3013
|
+
* patch pytest" forgery path.
|
|
3014
|
+
*/
|
|
3015
|
+
snapshotVenv(workspaceId, workspacePath) {
|
|
3016
|
+
const venv = path.join(workspacePath, ".venv");
|
|
3017
|
+
if (!fs.existsSync(venv)) return;
|
|
3018
|
+
const target = this.baselineDir(workspaceId);
|
|
3019
|
+
fs.mkdirSync(target, { recursive: true });
|
|
3020
|
+
const manifest = { files: {} };
|
|
3021
|
+
const python = path.join(venv, "bin", "python");
|
|
3022
|
+
try {
|
|
3023
|
+
manifest.python = fs.realpathSync.native(python);
|
|
3024
|
+
} catch {}
|
|
3025
|
+
const cfg = path.join(venv, "pyvenv.cfg");
|
|
3026
|
+
if (fs.existsSync(cfg)) manifest.files["pyvenv.cfg"] = sha256File(cfg);
|
|
3027
|
+
for (const site of sitePackageDirs(venv)) {
|
|
3028
|
+
for (const entry of fs.readdirSync(site, { withFileTypes: true })) {
|
|
3029
|
+
if (!entry.isFile() || !entry.name.endsWith(".pth")) continue;
|
|
3030
|
+
const file = path.join(site, entry.name);
|
|
3031
|
+
manifest.files[path.relative(venv, file)] = sha256File(file);
|
|
3032
|
+
}
|
|
3033
|
+
const pytest = path.join(site, "pytest");
|
|
3034
|
+
if (fs.existsSync(pytest)) manifest.pytest = sha256Tree(pytest);
|
|
3035
|
+
}
|
|
3036
|
+
fs.writeFileSync(path.join(target, "venv.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
3037
|
+
}
|
|
2458
3038
|
/** Check one workspace before install. */
|
|
2459
3039
|
check(workspaceId) {
|
|
2460
3040
|
const workspace = this.registry.get(workspaceId);
|
|
@@ -2548,7 +3128,7 @@ var TaskService = class {
|
|
|
2548
3128
|
}
|
|
2549
3129
|
/** Remove the shared CIFAR lock only when it belongs to this install. */
|
|
2550
3130
|
cleanupCifarLock(token) {
|
|
2551
|
-
const lock = path.join(
|
|
3131
|
+
const lock = path.join(dshHome(), "eval-guard", "cache", "cifar10", ".download.lock");
|
|
2552
3132
|
try {
|
|
2553
3133
|
if (!fs.existsSync(lock)) return;
|
|
2554
3134
|
if (fs.readFileSync(lock, "utf8").includes("token=" + token)) fs.rmSync(lock, { force: true });
|
|
@@ -2613,10 +3193,16 @@ var TaskService = class {
|
|
|
2613
3193
|
command,
|
|
2614
3194
|
cwd: checked.path,
|
|
2615
3195
|
env,
|
|
3196
|
+
cleanupWorkspace: checked.path,
|
|
3197
|
+
cifarToken: installToken,
|
|
2616
3198
|
onCancel: cleanupInstall,
|
|
2617
3199
|
onFailure: cleanupInstall,
|
|
2618
3200
|
onSuccess: () => {
|
|
2619
3201
|
if (definition.needsSetup) this.cleanupWorkspace(checked.path);
|
|
3202
|
+
try {
|
|
3203
|
+
this.snapshotTestMaterial(definition, workspaceId, checked.path);
|
|
3204
|
+
this.snapshotVenv(workspaceId, checked.path);
|
|
3205
|
+
} catch {}
|
|
2620
3206
|
return { prompt: this.promptNotice(this.guard.promptFor(checked.path, this.materialZip(taskId), definition)) };
|
|
2621
3207
|
}
|
|
2622
3208
|
});
|
|
@@ -2646,30 +3232,53 @@ var TaskService = class {
|
|
|
2646
3232
|
ok: false,
|
|
2647
3233
|
error: "评测脚本不存在:" + script
|
|
2648
3234
|
};
|
|
2649
|
-
const
|
|
3235
|
+
const hostView = taskId === "neural-pendulum" ? this.stageHostWorkspace(workspaceId) : void 0;
|
|
3236
|
+
const evalWorkspace = hostView ?? checked.path;
|
|
3237
|
+
const cleanupHostView = () => {
|
|
3238
|
+
if (hostView === void 0) return;
|
|
3239
|
+
try {
|
|
3240
|
+
fs.rmSync(hostView, {
|
|
3241
|
+
recursive: true,
|
|
3242
|
+
force: true
|
|
3243
|
+
});
|
|
3244
|
+
} catch {}
|
|
3245
|
+
};
|
|
3246
|
+
const baseline = this.baselineDir(workspaceId);
|
|
3247
|
+
const baselineEnv = fs.existsSync(baseline) ? { EVAL_BASELINE_DIR: baseline } : {};
|
|
3248
|
+
const job = this.jobs.start({
|
|
3249
|
+
kind: "eval",
|
|
3250
|
+
task: taskId,
|
|
3251
|
+
workspaceId,
|
|
3252
|
+
command: [
|
|
3253
|
+
"python3",
|
|
3254
|
+
"-X",
|
|
3255
|
+
"utf8",
|
|
3256
|
+
script,
|
|
3257
|
+
"--workspace",
|
|
3258
|
+
evalWorkspace
|
|
3259
|
+
],
|
|
3260
|
+
cwd: packageRoot(),
|
|
3261
|
+
env: {
|
|
3262
|
+
PYTHONPATH: "",
|
|
3263
|
+
PYTHONHOME: "",
|
|
3264
|
+
EVAL_CIFAR_PROXY: this.proxyValue(this.guard.configuration.cifarProxyUrl),
|
|
3265
|
+
EVAL_CIFAR_URL: this.cifarUrl(this.guard.configuration),
|
|
3266
|
+
...baselineEnv
|
|
3267
|
+
},
|
|
3268
|
+
onSuccess: cleanupHostView,
|
|
3269
|
+
onCancel: cleanupHostView,
|
|
3270
|
+
onFailure: cleanupHostView
|
|
3271
|
+
});
|
|
3272
|
+
if (job.status === "failed") {
|
|
3273
|
+
cleanupHostView();
|
|
3274
|
+
return {
|
|
3275
|
+
ok: false,
|
|
3276
|
+
error: job.error ?? "评测启动失败"
|
|
3277
|
+
};
|
|
3278
|
+
}
|
|
2650
3279
|
return {
|
|
2651
3280
|
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
|
|
3281
|
+
jobId: job.id
|
|
2673
3282
|
};
|
|
2674
3283
|
}
|
|
2675
3284
|
/** Add the optional shared-machine notice to a task prompt. */
|
|
@@ -2695,54 +3304,6 @@ var TaskService = class {
|
|
|
2695
3304
|
if (stale) fs.copyFileSync(source, target);
|
|
2696
3305
|
return target;
|
|
2697
3306
|
}
|
|
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
3307
|
/** The prompt for manual copying, without installing anything. */
|
|
2747
3308
|
prompt(workspaceId, taskId = TANK_TASK) {
|
|
2748
3309
|
const definition = TASK_DEFINITIONS[taskId];
|
|
@@ -2794,6 +3355,14 @@ function under(target, root) {
|
|
|
2794
3355
|
const relative = path.relative(root, target);
|
|
2795
3356
|
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
2796
3357
|
}
|
|
3358
|
+
/** Resolve symlinks where possible; keep the lexical path when it does not exist. */
|
|
3359
|
+
function realOrSelf(target) {
|
|
3360
|
+
try {
|
|
3361
|
+
return fs.realpathSync.native(target);
|
|
3362
|
+
} catch {
|
|
3363
|
+
return path.resolve(target);
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
2797
3366
|
/**
|
|
2798
3367
|
* Register every route this plugin owns.
|
|
2799
3368
|
* @param ctx - host context.
|
|
@@ -2818,6 +3387,7 @@ function registerRoutes(ctx, guard, tasks) {
|
|
|
2818
3387
|
fetch: async () => {
|
|
2819
3388
|
return json({
|
|
2820
3389
|
enabled: guard.active,
|
|
3390
|
+
...guard.lastFailure === void 0 ? {} : { error: guard.lastFailure },
|
|
2821
3391
|
running: guard.runningSessions(),
|
|
2822
3392
|
tasks: [...TASKS],
|
|
2823
3393
|
defaultBlacklist: guard.defaultBlacklist(),
|
|
@@ -2964,15 +3534,25 @@ function registerRoutes(ctx, guard, tasks) {
|
|
|
2964
3534
|
}
|
|
2965
3535
|
const target = typeof body.path === "string" ? path.resolve(body.path) : "";
|
|
2966
3536
|
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
3537
|
if (!fs.existsSync(target)) return json({ error: "path does not exist" }, 404);
|
|
3538
|
+
const real = realOrSelf(target);
|
|
3539
|
+
const material = realOrSelf(path.join(guardRoot(), "material"));
|
|
3540
|
+
const workspaces = child.get("workspaceRegistry")?.list?.() ?? [];
|
|
3541
|
+
if (!(under(real, material) || workspaces.some((workspace) => under(real, realOrSelf(workspace.path))))) return json({ error: "path is not revealable" }, 403);
|
|
2971
3542
|
try {
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
3543
|
+
await new Promise((resolve, reject) => {
|
|
3544
|
+
const childProcess = spawn("xdg-open", [real], {
|
|
3545
|
+
detached: true,
|
|
3546
|
+
stdio: "ignore"
|
|
3547
|
+
});
|
|
3548
|
+
childProcess.once("spawn", () => {
|
|
3549
|
+
childProcess.unref();
|
|
3550
|
+
resolve();
|
|
3551
|
+
});
|
|
3552
|
+
childProcess.once("error", (error) => {
|
|
3553
|
+
reject(error);
|
|
3554
|
+
});
|
|
3555
|
+
});
|
|
2976
3556
|
} catch (error) {
|
|
2977
3557
|
return json({ error: error instanceof Error ? error.message : String(error) }, 500);
|
|
2978
3558
|
}
|
|
@@ -3008,6 +3588,7 @@ function resolve(guard, exec) {
|
|
|
3008
3588
|
const run = guard.runForSession(exec.agent?.id);
|
|
3009
3589
|
if (run === void 0) throw new Error("eval task run is not active for this session");
|
|
3010
3590
|
if (run.task !== "neural-pendulum") throw new Error("this tool is only available for the neural-pendulum task");
|
|
3591
|
+
if (run.status !== "active") throw new Error("eval task run is not active (status: " + run.status + ")");
|
|
3011
3592
|
return run;
|
|
3012
3593
|
}
|
|
3013
3594
|
/** Install the task tools into one agent's own scoped registry. */
|
|
@@ -3037,7 +3618,7 @@ function registerAgentSubmissionTools(agent, guard, tasks) {
|
|
|
3037
3618
|
schema: OUTPUT_SCHEMA,
|
|
3038
3619
|
render
|
|
3039
3620
|
},
|
|
3040
|
-
timeoutMs:
|
|
3621
|
+
timeoutMs: 132e4,
|
|
3041
3622
|
execute: async (_args, exec) => tasks.hostTest(resolve(guard, exec))
|
|
3042
3623
|
};
|
|
3043
3624
|
let registered = false;
|