@evomap/evolver-core 2.0.0-beta.19 → 2.0.0-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/assets/gep/genes.jsonl +5 -5
  2. package/dist/algo/bans.d.ts +10 -1
  3. package/dist/algo/bans.js +56 -6
  4. package/dist/algo/candidateAssembly.d.ts +25 -2
  5. package/dist/algo/candidateAssembly.js +143 -20
  6. package/dist/algo/capabilityCandidates.js +10 -0
  7. package/dist/algo/cycleEngine.d.ts +40 -5
  8. package/dist/algo/cycleEngine.js +277 -37
  9. package/dist/algo/evolutionEvent.d.ts +26 -0
  10. package/dist/algo/evolutionEvent.js +32 -0
  11. package/dist/algo/exploration.d.ts +7 -0
  12. package/dist/algo/exploration.js +16 -3
  13. package/dist/algo/geneHealth.d.ts +34 -1
  14. package/dist/algo/geneHealth.js +42 -0
  15. package/dist/algo/geneIntake.d.ts +46 -1
  16. package/dist/algo/geneIntake.js +121 -10
  17. package/dist/algo/geneSelection.d.ts +115 -6
  18. package/dist/algo/geneSelection.js +459 -32
  19. package/dist/algo/index.d.ts +4 -1
  20. package/dist/algo/index.js +4 -1
  21. package/dist/algo/kautoProjection.d.ts +41 -0
  22. package/dist/algo/kautoProjection.js +95 -0
  23. package/dist/algo/kautoValidator.d.ts +68 -0
  24. package/dist/algo/kautoValidator.js +256 -0
  25. package/dist/algo/orchestrator.d.ts +14 -1
  26. package/dist/algo/orchestrator.js +9 -2
  27. package/dist/algo/solidify.d.ts +11 -2
  28. package/dist/algo/solidify.js +37 -7
  29. package/dist/algo/ucb1.d.ts +53 -0
  30. package/dist/algo/ucb1.js +156 -0
  31. package/dist/assetrepair/hubRejection.d.ts +12 -0
  32. package/dist/assetrepair/hubRejection.js +109 -0
  33. package/dist/assetrepair/index.d.ts +2 -0
  34. package/dist/assetrepair/index.js +2 -0
  35. package/dist/assetrepair/repair.d.ts +33 -0
  36. package/dist/assetrepair/repair.js +155 -0
  37. package/dist/assetstore/assetSidecarRecords.js +5 -1
  38. package/dist/assetstore/assetStoreHealth.d.ts +2 -3
  39. package/dist/assetstore/assetStoreHealth.js +4 -10
  40. package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
  41. package/dist/assetstore/foreignJsonlSource.js +150 -0
  42. package/dist/assetstore/index.d.ts +3 -0
  43. package/dist/assetstore/index.js +3 -0
  44. package/dist/assetstore/learningHistory.js +3 -3
  45. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  46. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  47. package/dist/assetstore/localJsonl.d.ts +8 -0
  48. package/dist/assetstore/localJsonl.js +128 -3
  49. package/dist/assetstore/provenance.d.ts +38 -3
  50. package/dist/assetstore/provenance.js +103 -45
  51. package/dist/assetstore/provider.d.ts +4 -0
  52. package/dist/assetstore/reviewFilter.d.ts +19 -1
  53. package/dist/assetstore/reviewFilter.js +36 -0
  54. package/dist/assetstore/unionReadStore.d.ts +25 -0
  55. package/dist/assetstore/unionReadStore.js +119 -0
  56. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  57. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  58. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  59. package/dist/benchmark/antiGeneRollout.js +4 -3
  60. package/dist/benchmark/index.d.ts +2 -1
  61. package/dist/benchmark/index.js +2 -1
  62. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  63. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  64. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  65. package/dist/bootstrap/envFingerprint.js +5 -0
  66. package/dist/bootstrap/index.d.ts +2 -1
  67. package/dist/bootstrap/index.js +2 -1
  68. package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
  69. package/dist/bootstrap/lifecycleBootstrap.js +433 -0
  70. package/dist/bootstrap/v1EnvCompat.d.ts +3 -0
  71. package/dist/bootstrap/v1EnvCompat.js +48 -1
  72. package/dist/events/eventArchive.d.ts +2 -0
  73. package/dist/events/eventArchive.js +13 -3
  74. package/dist/events/eventSchema.d.ts +7 -7
  75. package/dist/events/eventStore.d.ts +2 -0
  76. package/dist/events/eventStore.js +5 -1
  77. package/dist/events/ingest.d.ts +2 -1
  78. package/dist/events/ingest.js +6 -0
  79. package/dist/exec/autoExec.d.ts +30 -3
  80. package/dist/exec/autoExec.js +294 -29
  81. package/dist/exec/autonomousCycle.d.ts +7 -0
  82. package/dist/exec/autonomousCycle.js +2 -0
  83. package/dist/exec/claudeBridge.d.ts +28 -6
  84. package/dist/exec/claudeBridge.js +411 -258
  85. package/dist/exec/executionBinding.d.ts +414 -0
  86. package/dist/exec/executionBinding.js +588 -0
  87. package/dist/exec/index.d.ts +1 -0
  88. package/dist/exec/index.js +1 -0
  89. package/dist/exec/proofOfWork.d.ts +1 -1
  90. package/dist/exec/proofOfWork.js +2 -2
  91. package/dist/exec/runnerRegistry.d.ts +5 -0
  92. package/dist/exec/runnerRegistry.js +54 -14
  93. package/dist/hub/capability.d.ts +40 -1
  94. package/dist/hub/conversationDistiller.d.ts +19 -0
  95. package/dist/hub/conversationDistiller.js +115 -37
  96. package/dist/hub/fake.d.ts +1 -0
  97. package/dist/hub/fake.js +1 -0
  98. package/dist/hub/index.d.ts +1 -0
  99. package/dist/hub/index.js +1 -0
  100. package/dist/hub/recipeCompose.d.ts +27 -0
  101. package/dist/hub/recipeCompose.js +90 -0
  102. package/dist/hub/sanitize.js +119 -5
  103. package/dist/index.d.ts +2 -0
  104. package/dist/index.js +2 -0
  105. package/dist/mailbox/ipcServer.d.ts +2 -0
  106. package/dist/mailbox/ipcServer.js +20 -0
  107. package/dist/mailbox/store.d.ts +52 -2
  108. package/dist/mailbox/store.js +546 -81
  109. package/dist/modelCompatibility.d.ts +164 -0
  110. package/dist/modelCompatibility.js +309 -0
  111. package/dist/observers/valueDigestObserver.d.ts +9 -0
  112. package/dist/observers/valueDigestObserver.js +35 -2
  113. package/dist/ops/cleanup.js +1 -1
  114. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  115. package/dist/ops/evolutionGraphProjection.js +315 -0
  116. package/dist/ops/index.d.ts +2 -1
  117. package/dist/ops/index.js +2 -1
  118. package/dist/ops/selfUpdate.d.ts +8 -0
  119. package/dist/ops/selfUpdate.js +24 -8
  120. package/dist/ops/valueOutreach.d.ts +3 -1
  121. package/dist/ops/valueOutreach.js +5 -1
  122. package/dist/personality/schema.d.ts +18 -18
  123. package/dist/schema/evolutionGraph.d.ts +784 -0
  124. package/dist/schema/evolutionGraph.js +187 -0
  125. package/dist/schema/index.d.ts +1 -0
  126. package/dist/schema/index.js +1 -0
  127. package/dist/schema/proofOfWork.d.ts +125 -6
  128. package/dist/schema/proofOfWork.js +102 -4
  129. package/dist/shadow/shadowHub.js +1 -0
  130. package/dist/signals/expand.d.ts +15 -1
  131. package/dist/signals/expand.js +148 -0
  132. package/dist/signals/index.d.ts +3 -1
  133. package/dist/signals/index.js +3 -1
  134. package/dist/signals/scopeVocabulary.d.ts +75 -0
  135. package/dist/signals/scopeVocabulary.js +91 -0
  136. package/dist/signals/taskDomain.d.ts +22 -0
  137. package/dist/signals/taskDomain.js +43 -0
  138. package/dist/trace/learningTrace.d.ts +7 -0
  139. package/dist/trace/learningTrace.js +22 -0
  140. package/dist/trace/trajectory.d.ts +8 -0
  141. package/dist/trace/trajectory.js +14 -2
  142. package/dist/util/fileLock.d.ts +92 -5
  143. package/dist/util/fileLock.js +809 -70
  144. package/dist/util/index.d.ts +2 -1
  145. package/dist/util/index.js +1 -1
  146. package/dist/verify/index.d.ts +2 -1
  147. package/dist/verify/index.js +1 -1
  148. package/dist/verify/sandboxRunner.d.ts +2 -0
  149. package/dist/verify/sandboxRunner.js +154 -10
  150. package/dist/verify/sandboxedValidation.d.ts +7 -2
  151. package/dist/verify/sandboxedValidation.js +102 -24
  152. package/dist/verify/validation.d.ts +24 -3
  153. package/dist/verify/validation.js +141 -24
  154. package/dist/wire/geneHints.d.ts +88 -16
  155. package/dist/wire/geneHints.js +124 -15
  156. package/dist/wire/index.d.ts +8 -3
  157. package/dist/wire/index.js +2 -2
  158. package/dist/wire/schemaGate.d.ts +21 -0
  159. package/dist/wire/schemaGate.js +119 -10
  160. package/package.json +5 -4
@@ -1,3 +1,4 @@
1
- export * from './fileLock.js';
1
+ export { acquireLock, attachLockGuardianToProcess, clearLockGuardianForProcess, inspectFileLockOwnerProcess, LockReleaseError, LockTimeoutError, MAX_LOCK_OWNER_BYTES, releaseLock, retainLockGuardianForProcess, readFileLockProcessStartIdentity, sameFileLockProcessStartIdentity, parseFileLockProcessStartIdentity, syncSleep, transferLockOwnershipToProcess, UnsafeLockPathError, } from './fileLock.js';
2
+ export type { AcquireLockOptions, AttachLockGuardianResult, ClearLockGuardianResult, FileLockOwnerRecord, FileLockOwnerProcessStatus, FileLockProcessStartIdentity, ReleaseLockReason, ReleaseLockResult, RetainLockGuardianResult, TransferLockOwnershipReason, TransferLockOwnershipResult, UnsafeLockPathReason, } from './fileLock.js';
2
3
  export * from './fetchPort.js';
3
4
  export * from './version.js';
@@ -1,3 +1,3 @@
1
- export * from './fileLock.js';
1
+ export { acquireLock, attachLockGuardianToProcess, clearLockGuardianForProcess, inspectFileLockOwnerProcess, LockReleaseError, LockTimeoutError, MAX_LOCK_OWNER_BYTES, releaseLock, retainLockGuardianForProcess, readFileLockProcessStartIdentity, sameFileLockProcessStartIdentity, parseFileLockProcessStartIdentity, syncSleep, transferLockOwnershipToProcess, UnsafeLockPathError, } from './fileLock.js';
2
2
  export * from './fetchPort.js';
3
3
  export * from './version.js';
@@ -1,4 +1,5 @@
1
- export * from './validation.js';
1
+ export { BLOCKED_NODE_FLAGS, SHELL_METACHARS, isAllowed, isNodeExecutable, isValidationCommandAllowed, nodeFlagViolation, normalizeExecutableName, runValidation, summarizeStdout, validationScriptPath, } from './validation.js';
2
+ export type { CommandRunner, RunOutput, ValidationCommand, ValidationPlan, ValidationResult, } from './validation.js';
2
3
  export * from './rollback.js';
3
4
  export * from './sandboxRunner.js';
4
5
  export * from './sandboxedValidation.js';
@@ -1,4 +1,4 @@
1
- export * from './validation.js';
1
+ export { BLOCKED_NODE_FLAGS, SHELL_METACHARS, isAllowed, isNodeExecutable, isValidationCommandAllowed, nodeFlagViolation, normalizeExecutableName, runValidation, summarizeStdout, validationScriptPath, } from './validation.js';
2
2
  export * from './rollback.js';
3
3
  export * from './sandboxRunner.js';
4
4
  export * from './sandboxedValidation.js';
@@ -31,6 +31,8 @@ export interface SandboxOptions {
31
31
  unshareCheck?: () => boolean;
32
32
  /** Injected cgroup allocator (test seam). */
33
33
  resourceGroupFactory?: () => SandboxResourceGroup | null;
34
+ /** Cooperative cancellation for the current validation command. */
35
+ signal?: AbortSignal;
34
36
  }
35
37
  export interface SandboxResourceGroup {
36
38
  procsFile: string;
@@ -8,10 +8,12 @@ import { spawn, spawnSync } from 'node:child_process';
8
8
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, rmdirSync, writeFileSync } from 'node:fs';
9
9
  import { tmpdir } from 'node:os';
10
10
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
11
- import { isNodeExecutable, nodeFlagViolation, SHELL_METACHARS } from './validation.js';
11
+ import { classifyNodeValidationInvocation, isNodeExecutable, nodeFlagViolation, SHELL_METACHARS, tokenizeValidationCommand, } from './validation.js';
12
12
  const DEFAULT_TIMEOUT_MS = 60_000;
13
13
  const MAX_TIMEOUT_MS = 120_000;
14
14
  const MAX_OUTPUT_CHARS = 4000;
15
+ const PROCESS_TREE_KILL_TIMEOUT_MS = 5_000;
16
+ const PROCESS_TREE_EXIT_WAIT_MS = 5_000;
15
17
  const CGROUP_ROOT = '/sys/fs/cgroup';
16
18
  const TRUSTED_SYSTEM_PATH = '/usr/sbin:/usr/bin:/sbin:/bin';
17
19
  const SYSTEM_SH = '/bin/sh';
@@ -212,7 +214,9 @@ export function unshareNetAvailable() {
212
214
  return unshareCache;
213
215
  }
214
216
  function parse(cmd) {
215
- const tokens = cmd.trim().split(/\s+/).filter(Boolean);
217
+ const tokens = tokenizeValidationCommand(cmd);
218
+ if (!tokens)
219
+ return null;
216
220
  return { executable: tokens[0] ?? '', args: tokens.slice(1) };
217
221
  }
218
222
  function scrubEnv(allow) {
@@ -224,6 +228,72 @@ function scrubEnv(allow) {
224
228
  }
225
229
  return out;
226
230
  }
231
+ /**
232
+ * 终止完整的验证进程树,而不只是启动器。POSIX 启动器会独立成组,
233
+ * 可以安全地按进程组寻址;Windows 没有可移植的 Node API,因此使用受信任的
234
+ * System32 taskkill,并在该工具不可用时保留直接子进程兜底。
235
+ */
236
+ function terminateProcessTree(child) {
237
+ const pid = child.pid;
238
+ if (pid === undefined)
239
+ return;
240
+ if (process.platform === 'win32') {
241
+ const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
242
+ const taskkill = systemRoot && isAbsolute(systemRoot)
243
+ ? join(systemRoot, 'System32', 'taskkill.exe')
244
+ : undefined;
245
+ if (taskkill && existsSync(taskkill)) {
246
+ try {
247
+ const result = spawnSync(taskkill, ['/PID', String(pid), '/T', '/F'], {
248
+ shell: false,
249
+ windowsHide: true,
250
+ stdio: 'ignore',
251
+ timeout: PROCESS_TREE_KILL_TIMEOUT_MS,
252
+ });
253
+ if (result.status === 0)
254
+ return;
255
+ }
256
+ catch {
257
+ // 回退到下面的直接子进程终止;命令结果仍保持 fail-closed。
258
+ }
259
+ }
260
+ try {
261
+ child.kill('SIGKILL');
262
+ }
263
+ catch { /* 尽力终止;最终失败状态仍由 close/error 决定 */ }
264
+ return;
265
+ }
266
+ // 负 PID 指向 detached 进程组;若子进程未能成为组长,下面的直接终止仍是兜底。
267
+ try {
268
+ process.kill(-pid, 'SIGKILL');
269
+ }
270
+ catch { /* 进程组不存在或已退出 */ }
271
+ try {
272
+ if (!child.killed)
273
+ child.kill('SIGKILL');
274
+ }
275
+ catch { /* 尽力终止 */ }
276
+ }
277
+ function processTreeExists(pid) {
278
+ try {
279
+ process.kill(process.platform === 'win32' ? pid : -pid, 0);
280
+ return true;
281
+ }
282
+ catch (error) {
283
+ // EPERM 说明进程仍存在但当前用户暂时没有查询权限;不能把它当成已退出,
284
+ // 否则取消结果可能在后代仍运行时提前返回。
285
+ return error?.code === 'EPERM';
286
+ }
287
+ }
288
+ /** 取消后短暂等待,确保后代进程不会在 runner 返回成功解析后继续存活。 */
289
+ async function waitForProcessTreeExit(pid) {
290
+ if (pid === undefined)
291
+ return;
292
+ const deadline = Date.now() + PROCESS_TREE_EXIT_WAIT_MS;
293
+ while (Date.now() < deadline && processTreeExists(pid)) {
294
+ await new Promise((resolve) => setTimeout(resolve, 25));
295
+ }
296
+ }
227
297
  /**
228
298
  * Build a hardened CommandRunner. The executable allowlist is the ValidationPlan's job (runValidation);
229
299
  * this only hardens how an allowed command runs.
@@ -231,16 +301,46 @@ function scrubEnv(allow) {
231
301
  export function makeSandboxRunner(opts = {}) {
232
302
  const timeout = Math.min(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
233
303
  const envAllow = opts.envAllowKeys ?? DEFAULT_ENV_ALLOW;
234
- return (cmd) => new Promise((resolve) => {
235
- const deny = (msg, code = 126) => resolve({ exitCode: code, stdout: `[sandbox] ${msg}` });
236
- const { executable, args } = parse(cmd);
304
+ return (cmd, signal) => new Promise((resolve) => {
305
+ const activeSignal = signal ?? opts.signal;
306
+ let settled = false;
307
+ let aborted = false;
308
+ let timedOut = false;
309
+ let terminationStarted = false;
310
+ let timeoutTimer;
311
+ let removeAbortListener = () => { };
312
+ const finish = (result) => {
313
+ if (settled)
314
+ return;
315
+ settled = true;
316
+ removeAbortListener();
317
+ resolve(result);
318
+ };
319
+ const deny = (msg, code = 126) => finish({ exitCode: code, stdout: `[sandbox] ${msg}` });
320
+ if (activeSignal?.aborted)
321
+ return deny('cancelled', 130);
322
+ const parsed = parse(cmd);
323
+ if (!parsed)
324
+ return deny('invalid command syntax', 1);
325
+ const { executable, args } = parsed;
237
326
  if (!executable)
238
327
  return deny('empty command', 1);
328
+ if (isNodeExecutable(executable) && (args.length === 0 || (args.length === 1 && args[0] === '--'))) {
329
+ return deny('rejected: node script is required');
330
+ }
239
331
  if (SHELL_METACHARS.test(cmd))
240
332
  return deny('rejected: shell metacharacter (no shell is provided; split compound commands)');
241
333
  const badFlag = nodeFlagViolation(executable, args);
242
334
  if (badFlag)
243
335
  return deny(`rejected: blocked node flag ${badFlag}`);
336
+ if (isNodeExecutable(executable)) {
337
+ const invocation = classifyNodeValidationInvocation(executable, args);
338
+ if (invocation.kind === 'invalid') {
339
+ return invocation.option
340
+ ? deny(`rejected: unsupported node option ${invocation.option}`)
341
+ : deny('rejected: node script is required');
342
+ }
343
+ }
244
344
  // Isolation (#26): fail-safe — if we can't actually create the namespaces, refuse rather than run un-isolated.
245
345
  if ((opts.noNetwork || opts.hideHomeSecrets || opts.readOnlyFilesystem) && !(opts.unshareCheck ?? unshareNetAvailable)()) {
246
346
  return deny('rejected: requested namespace isolation is unavailable');
@@ -264,6 +364,9 @@ export function makeSandboxRunner(opts = {}) {
264
364
  if (cleaned)
265
365
  return;
266
366
  cleaned = true;
367
+ if (timeoutTimer !== undefined)
368
+ clearTimeout(timeoutTimer);
369
+ removeAbortListener();
267
370
  resourceGroup?.cleanup();
268
371
  if (ownTemp) {
269
372
  try {
@@ -295,21 +398,62 @@ export function makeSandboxRunner(opts = {}) {
295
398
  shell: false,
296
399
  cwd,
297
400
  env,
298
- timeout,
299
- killSignal: 'SIGKILL',
401
+ // POSIX 依靠 detached 进程组执行整树终止;Windows 的 taskkill 直接沿父子树追踪,保持启动器附着可
402
+ // 避免兜底终止时产生孤儿进程。
403
+ detached: process.platform !== 'win32',
404
+ windowsHide: true,
300
405
  stdio: [opts.readOnlyFilesystem ? 'pipe' : 'ignore', 'pipe', 'pipe'],
301
406
  });
407
+ let childClosed = false;
302
408
  let out = '';
303
409
  const cap = (d) => { if (out.length < MAX_OUTPUT_CHARS)
304
410
  out += String(d); };
305
411
  child.stdout?.on('data', cap);
306
412
  child.stderr?.on('data', cap);
307
- child.on('error', (err) => { cleanup(); resolve({ exitCode: 127, stdout: `[sandbox] spawn error: ${err.message}` }); });
308
- child.on('close', (code, signal) => { cleanup(); resolve({ exitCode: code ?? (signal ? 137 : 1), stdout: out.slice(0, MAX_OUTPUT_CHARS) }); });
413
+ const complete = async (result) => {
414
+ if (terminationStarted)
415
+ await waitForProcessTreeExit(child.pid);
416
+ cleanup();
417
+ finish(result);
418
+ };
419
+ // 先注册生命周期处理器,再暴露 AbortSignal 监听器,堵住快速命令退出后 runner 尚未观察 close
420
+ // 事件、调用方却已经 abort 的竞态窗口。
421
+ child.on('error', (err) => {
422
+ childClosed = true;
423
+ void complete({ exitCode: aborted ? 130 : timedOut ? 137 : 127, stdout: `[sandbox] spawn error: ${err.message}` });
424
+ });
425
+ child.on('close', (code, childSignal) => {
426
+ childClosed = true;
427
+ void complete({
428
+ exitCode: aborted ? 130 : timedOut ? 137 : code ?? (childSignal ? 137 : 1),
429
+ stdout: out.slice(0, MAX_OUTPUT_CHARS),
430
+ });
431
+ });
432
+ const terminate = (reason) => {
433
+ // close 事件等待继承的 stdio 句柄时,exitCode 可能已经设置。必须持续启用整树终止直到观察到
434
+ // close,否则快速退出的启动器可能留下仍在运行的后代进程。
435
+ if (terminationStarted || childClosed)
436
+ return;
437
+ terminationStarted = true;
438
+ aborted = reason === 'abort';
439
+ timedOut = reason === 'timeout';
440
+ resourceGroup?.cleanup();
441
+ terminateProcessTree(child);
442
+ };
443
+ const onAbort = () => terminate('abort');
444
+ if (activeSignal) {
445
+ removeAbortListener = () => activeSignal.removeEventListener('abort', onAbort);
446
+ if (activeSignal.aborted)
447
+ onAbort();
448
+ else
449
+ activeSignal.addEventListener('abort', onAbort, { once: true });
450
+ }
451
+ timeoutTimer = timeout > 0 ? setTimeout(() => terminate('timeout'), timeout) : undefined;
452
+ timeoutTimer?.unref?.();
309
453
  }
310
454
  catch (e) {
311
455
  cleanup();
312
- resolve({ exitCode: 127, stdout: `[sandbox] spawn failed: ${e instanceof Error ? e.message : 'err'}` });
456
+ finish({ exitCode: 127, stdout: `[sandbox] spawn failed: ${e instanceof Error ? e.message : 'err'}` });
313
457
  }
314
458
  });
315
459
  }
@@ -1,16 +1,19 @@
1
1
  import { type SandboxResourceGroup } from './sandboxRunner.js';
2
2
  import { type ValidationResult } from './validation.js';
3
+ export type SandboxedValidationSkipReason = 'missing_script' | 'script_outside_root' | 'script_symlink' | 'script_unresolvable';
3
4
  export interface SandboxedValidationSkippedCommand {
4
5
  cmd: string;
5
6
  script: string;
6
- reason: 'missing_script';
7
+ reason: SandboxedValidationSkipReason;
7
8
  }
8
9
  export interface SandboxedValidationResult {
9
10
  passed: boolean;
11
+ /** 是否因宿主取消而终止;取消永远不是成功验证。 */
12
+ cancelled?: boolean;
10
13
  /** Convenience score for cycle outcomes: 0.95 pass / 0.2 fail (matches the prior inline hook). */
11
14
  score: number;
12
15
  results: ValidationResult[];
13
- /** Declared repo-relative validation scripts that were absent from this checkout. */
16
+ /** Validation commands not executed because their script was missing or failed the path safety gate. */
14
17
  skipped: SandboxedValidationSkippedCommand[];
15
18
  /**
16
19
  * Whether OS-namespace isolation (no-network + hidden home secrets) was applied. TRUE only where unprivileged
@@ -25,6 +28,8 @@ export declare function readOnlyIsolationAvailable(): boolean;
25
28
  export interface SandboxedValidationOptions {
26
29
  /** Per-command timeout (ms), forwarded to the sandbox runner. */
27
30
  timeoutMs?: number;
31
+ /** Cooperative cancellation forwarded to the validation process. */
32
+ signal?: AbortSignal;
28
33
  /** Test seam: override the unprivileged-namespace availability probe. */
29
34
  unshareCheck?: () => boolean;
30
35
  /** Refuse before spawning when network/home namespace isolation is unavailable. */
@@ -4,10 +4,10 @@
4
4
  // namespaces exist — no network + hidden home secrets, so a validation command cannot phone home / exfiltrate to
5
5
  // game the pass/fail. This is the "verifier outside the repo, untrusted caller" half the safety model relies on.
6
6
  import { spawnSync } from 'node:child_process';
7
- import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync } from 'node:fs';
7
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, realpathSync, rmSync } from 'node:fs';
8
8
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
9
9
  import { isolationCommand, makeSandboxRunner, sandboxResourceLimitsAvailable, unshareNetAvailable, } from './sandboxRunner.js';
10
- import { runValidation, validationScriptPath } from './validation.js';
10
+ import { runValidation, tokenizeValidationCommand, validationScriptPath, } from './validation.js';
11
11
  let readOnlyIsolationAvailableCache;
12
12
  let readOnlyFilesystemIsolationAvailableCache;
13
13
  export function readOnlyFilesystemIsolationAvailable() {
@@ -69,6 +69,53 @@ function validationReadOnlyRoot(cwd) {
69
69
  current = parent;
70
70
  }
71
71
  }
72
+ function skippedCommand(cmd, script, reason) {
73
+ return { cmd, script, reason };
74
+ }
75
+ /**
76
+ * Validate the script target before it reaches the runner. The runner's mount namespace is read-only, not a chroot,
77
+ * so lexical containment alone is insufficient: a path inside the checkout may resolve through a symlink outside it.
78
+ * Symlink script entries are rejected outright, and the canonical target is checked immediately before execution.
79
+ */
80
+ function classifyValidationScript(cmd, script, validationCwd, readOnlyRoot) {
81
+ if (isAbsolute(script)) {
82
+ return { ok: false, skipped: skippedCommand(cmd, script, 'script_outside_root') };
83
+ }
84
+ let candidate;
85
+ try {
86
+ candidate = resolve(validationCwd, script);
87
+ }
88
+ catch {
89
+ return { ok: false, skipped: skippedCommand(cmd, script, 'script_unresolvable') };
90
+ }
91
+ if (!pathIsWithin(readOnlyRoot, candidate)) {
92
+ return { ok: false, skipped: skippedCommand(cmd, script, 'script_outside_root') };
93
+ }
94
+ let metadata;
95
+ try {
96
+ metadata = lstatSync(candidate);
97
+ }
98
+ catch {
99
+ return { ok: false, skipped: skippedCommand(cmd, script, 'missing_script') };
100
+ }
101
+ if (metadata.isSymbolicLink()) {
102
+ return { ok: false, skipped: skippedCommand(cmd, script, 'script_symlink') };
103
+ }
104
+ if (!metadata.isFile()) {
105
+ return { ok: false, skipped: skippedCommand(cmd, script, 'script_unresolvable') };
106
+ }
107
+ let canonicalScript;
108
+ try {
109
+ canonicalScript = realpathSync(candidate);
110
+ }
111
+ catch {
112
+ return { ok: false, skipped: skippedCommand(cmd, script, 'script_unresolvable') };
113
+ }
114
+ if (!pathIsWithin(readOnlyRoot, canonicalScript)) {
115
+ return { ok: false, skipped: skippedCommand(cmd, script, 'script_outside_root') };
116
+ }
117
+ return { ok: true };
118
+ }
72
119
  /**
73
120
  * Run validation commands in the hardened sandbox. Isolation (no-network + hidden home secrets) is requested only
74
121
  * when unprivileged namespaces are available; elsewhere (Windows/macOS) it degrades to the non-namespace hardening
@@ -77,45 +124,56 @@ function validationReadOnlyRoot(cwd) {
77
124
  * preserving prior behavior; note this differs from runValidation's own "no commands = not verified" stance.
78
125
  */
79
126
  export async function runSandboxedValidation(cmds, cwd, opts = {}) {
127
+ if (opts.signal?.aborted)
128
+ return { passed: false, cancelled: true, score: 0.2, results: [], skipped: [], isolated: false };
80
129
  const isolationCheck = opts.unshareCheck ?? (opts.requireIsolation ? readOnlyIsolationAvailable : unshareNetAvailable);
81
130
  const isolated = isolationCheck();
82
131
  if (opts.requireIsolation && !isolated) {
83
- return { passed: false, score: 0.2, results: [], skipped: [], isolated: false };
132
+ return { passed: false, cancelled: opts.signal?.aborted === true, score: 0.2, results: [], skipped: [], isolated: false };
84
133
  }
85
134
  let validationCwd = resolve(cwd);
86
135
  let readOnlyRoot = opts.readOnlyRoot ? resolve(opts.readOnlyRoot) : validationReadOnlyRoot(validationCwd);
87
- if (opts.requireIsolation && isolated) {
88
- try {
89
- validationCwd = realpathSync(validationCwd);
90
- readOnlyRoot = realpathSync(readOnlyRoot);
91
- }
92
- catch {
93
- return { passed: false, score: 0.2, results: [], skipped: [], isolated };
94
- }
95
- if (dirname(readOnlyRoot) === readOnlyRoot || !pathIsWithin(readOnlyRoot, validationCwd)) {
96
- return { passed: false, score: 0.2, results: [], skipped: [], isolated };
97
- }
136
+ // Canonicalize the validation root in every mode. Namespace isolation is an additional boundary, not a substitute
137
+ // for refusing an absolute, escaping, or symlinked script path; this also keeps the Windows/macOS path fail closed.
138
+ try {
139
+ validationCwd = realpathSync(validationCwd);
140
+ readOnlyRoot = realpathSync(readOnlyRoot);
141
+ }
142
+ catch {
143
+ return { passed: false, cancelled: opts.signal?.aborted === true, score: 0.2, results: [], skipped: [], isolated };
144
+ }
145
+ if (dirname(readOnlyRoot) === readOnlyRoot || !pathIsWithin(readOnlyRoot, validationCwd)) {
146
+ return { passed: false, cancelled: opts.signal?.aborted === true, score: 0.2, results: [], skipped: [], isolated };
98
147
  }
99
148
  // ONLY a truly empty command set is a vacuous pass (nothing to verify). A non-empty set with blank entries is a
100
149
  // malformed plan, NOT a pass: blanks are kept (trimmed to '') so they fall outside the allowlist and fail, rather
101
150
  // than being silently dropped into a pass (Bugbot).
102
151
  if (cmds.length === 0)
103
- return { passed: true, score: 0.95, results: [], skipped: [], isolated };
152
+ return { passed: true, cancelled: false, score: 0.95, results: [], skipped: [], isolated };
104
153
  const list = cmds.map((c) => String(c ?? '').trim());
105
- // Validation plans may reference repo-relative scripts that do not exist in this checkout; skip only those.
154
+ // Validation plans may reference repo-relative scripts that do not exist in this checkout; skip those, but never
155
+ // execute a script that is absolute, escapes the root, resolves through a symlink, or is not a regular file.
106
156
  const skipped = [];
107
157
  const runnable = [];
108
158
  for (const cmd of list) {
109
159
  const script = validationScriptPath(cmd);
110
- if (script && !isAbsolute(script) && !existsSync(resolve(validationCwd, script))) {
111
- skipped.push({ cmd, script, reason: 'missing_script' });
112
- console.warn(`[Validation] Skipping validation command (script not in repoRoot): ${script}`);
113
- continue;
160
+ if (script) {
161
+ const classification = classifyValidationScript(cmd, script, validationCwd, readOnlyRoot);
162
+ if (!classification.ok) {
163
+ skipped.push(classification.skipped);
164
+ if (classification.skipped.reason === 'missing_script') {
165
+ console.warn(`[Validation] Skipping validation command (script not in repoRoot): ${script}`);
166
+ }
167
+ else {
168
+ console.warn(`[Validation] Rejecting validation command (${classification.skipped.reason}): ${script}`);
169
+ }
170
+ continue;
171
+ }
114
172
  }
115
173
  runnable.push(cmd);
116
174
  }
117
175
  if (runnable.length === 0)
118
- return { passed: false, score: 0.2, results: [], skipped, isolated };
176
+ return { passed: false, cancelled: opts.signal?.aborted === true, score: 0.2, results: [], skipped, isolated };
119
177
  const scratch = isolated && opts.requireIsolation
120
178
  ? mkdtempSync('/var/tmp/evolver-validation-')
121
179
  : undefined;
@@ -129,21 +187,41 @@ export async function runSandboxedValidation(cmds, cwd, opts = {}) {
129
187
  readOnlyRoot,
130
188
  ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
131
189
  ...(opts.resourceGroupFactory ? { resourceGroupFactory: opts.resourceGroupFactory } : {}),
190
+ ...(opts.signal ? { signal: opts.signal } : {}),
132
191
  unshareCheck: () => isolated,
133
192
  });
134
- const allowlist = [...new Set(runnable.map((c) => c.split(/\s+/)[0] ?? '').filter(Boolean))];
193
+ // Re-check each script immediately before its spawn. The initial pass supplies precise skipped reasons, while
194
+ // this second gate closes the degraded-mode window where an earlier validator could replace a later path.
195
+ const guardedRunner = async (cmd, signal) => {
196
+ const script = validationScriptPath(cmd);
197
+ if (script) {
198
+ const classification = classifyValidationScript(cmd, script, validationCwd, readOnlyRoot);
199
+ if (!classification.ok) {
200
+ return {
201
+ exitCode: 126,
202
+ stdout: `[sandbox] rejected: ${classification.skipped.reason}: ${script}`,
203
+ };
204
+ }
205
+ }
206
+ return runner(cmd, signal);
207
+ };
208
+ const allowlist = [...new Set(runnable
209
+ .map((c) => tokenizeValidationCommand(c)?.[0] ?? '')
210
+ .filter(Boolean))];
135
211
  let results;
136
212
  let passed;
213
+ let cancelled;
137
214
  try {
138
- ({ results, passed } = await runValidation({ commands: runnable.map((cmd) => ({ cmd })), allowlist }, runner));
215
+ ({ results, passed, cancelled } = await runValidation({ commands: runnable.map((cmd) => ({ cmd })), allowlist }, guardedRunner, opts.signal));
139
216
  }
140
217
  finally {
141
218
  if (scratch)
142
219
  rmSync(scratch, { recursive: true, force: true });
143
220
  }
144
- const complete = skipped.length === 0;
221
+ const complete = skipped.length === 0 && !cancelled;
145
222
  return {
146
223
  passed: passed && complete,
224
+ cancelled,
147
225
  score: passed && complete ? 0.95 : 0.2,
148
226
  results,
149
227
  skipped,
@@ -19,15 +19,35 @@ export interface RunOutput {
19
19
  exitCode: number;
20
20
  stdout: string;
21
21
  }
22
- export type CommandRunner = (cmd: string) => Promise<RunOutput> | RunOutput;
22
+ export type CommandRunner = (cmd: string, signal?: AbortSignal) => Promise<RunOutput> | RunOutput;
23
23
  export declare const SHELL_METACHARS: RegExp;
24
+ /**
25
+ * Parse one validation command without invoking a shell. Quotes group whitespace and backslash only escapes
26
+ * whitespace, quotes, or another backslash; no variable expansion or command substitution is performed.
27
+ * A null result means the command has an unterminated quote and must be rejected.
28
+ */
29
+ export declare function tokenizeValidationCommand(command: string): string[] | null;
24
30
  export declare const BLOCKED_NODE_FLAGS: Set<string>;
25
31
  export declare function normalizeExecutableName(executable: string): string;
26
32
  export declare function isNodeExecutable(executable: string): boolean;
27
33
  export declare function nodeFlagViolation(executable: string, args: readonly string[]): string | null;
34
+ export type NodeValidationInvocation = {
35
+ kind: 'script';
36
+ script: string;
37
+ } | {
38
+ kind: 'safe_no_script';
39
+ } | {
40
+ kind: 'invalid';
41
+ option?: string;
42
+ };
43
+ /**
44
+ * 将 Node 命令限制为明确的脚本位置,防止带参数的选项吞掉下一个 token 后绕过脚本路径检查。
45
+ * 验证只需要普通脚本、语法检查,以及兼容旧 Gene 的 version/help 身份检查;其他前置选项默认拒绝。
46
+ */
47
+ export declare function classifyNodeValidationInvocation(executable: string, args: readonly string[]): NodeValidationInvocation;
28
48
  /** 命令首 token(可执行名)是否在项目声明的 allowlist 内. */
29
49
  export declare function isAllowed(cmd: string, allowlist: readonly string[]): boolean;
30
- /** Extract the local script targeted by `node <script> ...`, or null for non-script node commands. */
50
+ /** Extract the unambiguous script targeted by `node <script> ...`, or null for non-script/unsupported commands. */
31
51
  export declare function validationScriptPath(cmd: string): string | null;
32
52
  /** stdout 摘要 = 首行 + 末行(去 canary.js 隐形约定: 不找魔法字符串, 只留可读摘要). */
33
53
  export declare function summarizeStdout(stdout: string): string;
@@ -35,9 +55,10 @@ export declare function summarizeStdout(stdout: string): string;
35
55
  * 跑校验(M4A-4). 判价值 = **exit code + stdout 摘要**, 不依赖 canary.js 魔法字符串(批注#22).
36
56
  * 不在 allowlist 的命令 auto-deny(不执行, allowed=false), 守无人值守安全.
37
57
  */
38
- export declare function runValidation(plan: ValidationPlan, run: CommandRunner): Promise<{
58
+ export declare function runValidation(plan: ValidationPlan, run: CommandRunner, signal?: AbortSignal): Promise<{
39
59
  results: ValidationResult[];
40
60
  passed: boolean;
61
+ cancelled: boolean;
41
62
  }>;
42
63
  /**
43
64
  * 检查验证命令是否安全可作为 Gene.validation 条目使用(忠实移植 v1 policyCheck.isValidationCommandAllowed)。