@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.
- package/assets/gep/genes.jsonl +5 -5
- package/dist/algo/bans.d.ts +10 -1
- package/dist/algo/bans.js +56 -6
- package/dist/algo/candidateAssembly.d.ts +25 -2
- package/dist/algo/candidateAssembly.js +143 -20
- package/dist/algo/capabilityCandidates.js +10 -0
- package/dist/algo/cycleEngine.d.ts +40 -5
- package/dist/algo/cycleEngine.js +277 -37
- package/dist/algo/evolutionEvent.d.ts +26 -0
- package/dist/algo/evolutionEvent.js +32 -0
- package/dist/algo/exploration.d.ts +7 -0
- package/dist/algo/exploration.js +16 -3
- package/dist/algo/geneHealth.d.ts +34 -1
- package/dist/algo/geneHealth.js +42 -0
- package/dist/algo/geneIntake.d.ts +46 -1
- package/dist/algo/geneIntake.js +121 -10
- package/dist/algo/geneSelection.d.ts +115 -6
- package/dist/algo/geneSelection.js +459 -32
- package/dist/algo/index.d.ts +4 -1
- package/dist/algo/index.js +4 -1
- package/dist/algo/kautoProjection.d.ts +41 -0
- package/dist/algo/kautoProjection.js +95 -0
- package/dist/algo/kautoValidator.d.ts +68 -0
- package/dist/algo/kautoValidator.js +256 -0
- package/dist/algo/orchestrator.d.ts +14 -1
- package/dist/algo/orchestrator.js +9 -2
- package/dist/algo/solidify.d.ts +11 -2
- package/dist/algo/solidify.js +37 -7
- package/dist/algo/ucb1.d.ts +53 -0
- package/dist/algo/ucb1.js +156 -0
- package/dist/assetrepair/hubRejection.d.ts +12 -0
- package/dist/assetrepair/hubRejection.js +109 -0
- package/dist/assetrepair/index.d.ts +2 -0
- package/dist/assetrepair/index.js +2 -0
- package/dist/assetrepair/repair.d.ts +33 -0
- package/dist/assetrepair/repair.js +155 -0
- package/dist/assetstore/assetSidecarRecords.js +5 -1
- package/dist/assetstore/assetStoreHealth.d.ts +2 -3
- package/dist/assetstore/assetStoreHealth.js +4 -10
- package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
- package/dist/assetstore/foreignJsonlSource.js +150 -0
- package/dist/assetstore/index.d.ts +3 -0
- package/dist/assetstore/index.js +3 -0
- package/dist/assetstore/learningHistory.js +3 -3
- package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
- package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
- package/dist/assetstore/localJsonl.d.ts +8 -0
- package/dist/assetstore/localJsonl.js +128 -3
- package/dist/assetstore/provenance.d.ts +38 -3
- package/dist/assetstore/provenance.js +103 -45
- package/dist/assetstore/provider.d.ts +4 -0
- package/dist/assetstore/reviewFilter.d.ts +19 -1
- package/dist/assetstore/reviewFilter.js +36 -0
- package/dist/assetstore/unionReadStore.d.ts +25 -0
- package/dist/assetstore/unionReadStore.js +119 -0
- package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
- package/dist/benchmark/antiGeneBenchmark.js +4 -3
- package/dist/benchmark/antiGeneRollout.d.ts +2 -0
- package/dist/benchmark/antiGeneRollout.js +4 -3
- package/dist/benchmark/index.d.ts +2 -1
- package/dist/benchmark/index.js +2 -1
- package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
- package/dist/benchmark/selectionFlatAbstention.js +481 -0
- package/dist/bootstrap/envFingerprint.d.ts +9 -0
- package/dist/bootstrap/envFingerprint.js +5 -0
- package/dist/bootstrap/index.d.ts +2 -1
- package/dist/bootstrap/index.js +2 -1
- package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
- package/dist/bootstrap/lifecycleBootstrap.js +433 -0
- package/dist/bootstrap/v1EnvCompat.d.ts +3 -0
- package/dist/bootstrap/v1EnvCompat.js +48 -1
- package/dist/events/eventArchive.d.ts +2 -0
- package/dist/events/eventArchive.js +13 -3
- package/dist/events/eventSchema.d.ts +7 -7
- package/dist/events/eventStore.d.ts +2 -0
- package/dist/events/eventStore.js +5 -1
- package/dist/events/ingest.d.ts +2 -1
- package/dist/events/ingest.js +6 -0
- package/dist/exec/autoExec.d.ts +30 -3
- package/dist/exec/autoExec.js +294 -29
- package/dist/exec/autonomousCycle.d.ts +7 -0
- package/dist/exec/autonomousCycle.js +2 -0
- package/dist/exec/claudeBridge.d.ts +28 -6
- package/dist/exec/claudeBridge.js +411 -258
- package/dist/exec/executionBinding.d.ts +414 -0
- package/dist/exec/executionBinding.js +588 -0
- package/dist/exec/index.d.ts +1 -0
- package/dist/exec/index.js +1 -0
- package/dist/exec/proofOfWork.d.ts +1 -1
- package/dist/exec/proofOfWork.js +2 -2
- package/dist/exec/runnerRegistry.d.ts +5 -0
- package/dist/exec/runnerRegistry.js +54 -14
- package/dist/hub/capability.d.ts +40 -1
- package/dist/hub/conversationDistiller.d.ts +19 -0
- package/dist/hub/conversationDistiller.js +115 -37
- package/dist/hub/fake.d.ts +1 -0
- package/dist/hub/fake.js +1 -0
- package/dist/hub/index.d.ts +1 -0
- package/dist/hub/index.js +1 -0
- package/dist/hub/recipeCompose.d.ts +27 -0
- package/dist/hub/recipeCompose.js +90 -0
- package/dist/hub/sanitize.js +119 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mailbox/ipcServer.d.ts +2 -0
- package/dist/mailbox/ipcServer.js +20 -0
- package/dist/mailbox/store.d.ts +52 -2
- package/dist/mailbox/store.js +546 -81
- package/dist/modelCompatibility.d.ts +164 -0
- package/dist/modelCompatibility.js +309 -0
- package/dist/observers/valueDigestObserver.d.ts +9 -0
- package/dist/observers/valueDigestObserver.js +35 -2
- package/dist/ops/cleanup.js +1 -1
- package/dist/ops/evolutionGraphProjection.d.ts +20 -0
- package/dist/ops/evolutionGraphProjection.js +315 -0
- package/dist/ops/index.d.ts +2 -1
- package/dist/ops/index.js +2 -1
- package/dist/ops/selfUpdate.d.ts +8 -0
- package/dist/ops/selfUpdate.js +24 -8
- package/dist/ops/valueOutreach.d.ts +3 -1
- package/dist/ops/valueOutreach.js +5 -1
- package/dist/personality/schema.d.ts +18 -18
- package/dist/schema/evolutionGraph.d.ts +784 -0
- package/dist/schema/evolutionGraph.js +187 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/proofOfWork.d.ts +125 -6
- package/dist/schema/proofOfWork.js +102 -4
- package/dist/shadow/shadowHub.js +1 -0
- package/dist/signals/expand.d.ts +15 -1
- package/dist/signals/expand.js +148 -0
- package/dist/signals/index.d.ts +3 -1
- package/dist/signals/index.js +3 -1
- package/dist/signals/scopeVocabulary.d.ts +75 -0
- package/dist/signals/scopeVocabulary.js +91 -0
- package/dist/signals/taskDomain.d.ts +22 -0
- package/dist/signals/taskDomain.js +43 -0
- package/dist/trace/learningTrace.d.ts +7 -0
- package/dist/trace/learningTrace.js +22 -0
- package/dist/trace/trajectory.d.ts +8 -0
- package/dist/trace/trajectory.js +14 -2
- package/dist/util/fileLock.d.ts +92 -5
- package/dist/util/fileLock.js +809 -70
- package/dist/util/index.d.ts +2 -1
- package/dist/util/index.js +1 -1
- package/dist/verify/index.d.ts +2 -1
- package/dist/verify/index.js +1 -1
- package/dist/verify/sandboxRunner.d.ts +2 -0
- package/dist/verify/sandboxRunner.js +154 -10
- package/dist/verify/sandboxedValidation.d.ts +7 -2
- package/dist/verify/sandboxedValidation.js +102 -24
- package/dist/verify/validation.d.ts +24 -3
- package/dist/verify/validation.js +141 -24
- package/dist/wire/geneHints.d.ts +88 -16
- package/dist/wire/geneHints.js +124 -15
- package/dist/wire/index.d.ts +8 -3
- package/dist/wire/index.js +2 -2
- package/dist/wire/schemaGate.d.ts +21 -0
- package/dist/wire/schemaGate.js +119 -10
- package/package.json +5 -4
package/dist/util/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export
|
|
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';
|
package/dist/util/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export
|
|
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';
|
package/dist/verify/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export
|
|
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';
|
package/dist/verify/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export
|
|
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
|
|
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
|
|
236
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
/**
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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 },
|
|
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
|
|
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)。
|