@evomap/evolver-core 2.0.0-beta.2 → 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 (240) 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 +171 -26
  6. package/dist/algo/capabilityCandidates.js +10 -0
  7. package/dist/algo/conversationSniffer.js +25 -8
  8. package/dist/algo/cycleEngine.d.ts +63 -5
  9. package/dist/algo/cycleEngine.js +320 -42
  10. package/dist/algo/cycleFailureClassifier.d.ts +1 -1
  11. package/dist/algo/evolutionEvent.d.ts +26 -0
  12. package/dist/algo/evolutionEvent.js +32 -0
  13. package/dist/algo/exploration.d.ts +7 -0
  14. package/dist/algo/exploration.js +16 -3
  15. package/dist/algo/geneHealth.d.ts +36 -3
  16. package/dist/algo/geneHealth.js +47 -4
  17. package/dist/algo/geneIntake.d.ts +46 -1
  18. package/dist/algo/geneIntake.js +121 -10
  19. package/dist/algo/geneSelection.d.ts +126 -6
  20. package/dist/algo/geneSelection.js +479 -36
  21. package/dist/algo/index.d.ts +6 -1
  22. package/dist/algo/index.js +6 -1
  23. package/dist/algo/kautoProjection.d.ts +41 -0
  24. package/dist/algo/kautoProjection.js +95 -0
  25. package/dist/algo/kautoValidator.d.ts +68 -0
  26. package/dist/algo/kautoValidator.js +256 -0
  27. package/dist/algo/memoryGraph.d.ts +62 -0
  28. package/dist/algo/memoryGraph.js +86 -0
  29. package/dist/algo/orchestrator.d.ts +17 -1
  30. package/dist/algo/orchestrator.js +30 -4
  31. package/dist/algo/publishEligibility.d.ts +34 -0
  32. package/dist/algo/publishEligibility.js +52 -0
  33. package/dist/algo/solidify.d.ts +11 -2
  34. package/dist/algo/solidify.js +37 -7
  35. package/dist/algo/ucb1.d.ts +53 -0
  36. package/dist/algo/ucb1.js +156 -0
  37. package/dist/assetrepair/hubRejection.d.ts +12 -0
  38. package/dist/assetrepair/hubRejection.js +109 -0
  39. package/dist/assetrepair/index.d.ts +2 -0
  40. package/dist/assetrepair/index.js +2 -0
  41. package/dist/assetrepair/repair.d.ts +33 -0
  42. package/dist/assetrepair/repair.js +155 -0
  43. package/dist/assetstore/assetSidecarRecords.d.ts +28 -0
  44. package/dist/assetstore/assetSidecarRecords.js +384 -0
  45. package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
  46. package/dist/assetstore/assetSidecarRecovery.js +288 -0
  47. package/dist/assetstore/assetStoreHealth.d.ts +81 -0
  48. package/dist/assetstore/assetStoreHealth.js +319 -0
  49. package/dist/assetstore/assetStoreLayout.d.ts +2 -0
  50. package/dist/assetstore/assetStoreLayout.js +6 -0
  51. package/dist/assetstore/assetStoreStorage.d.ts +42 -0
  52. package/dist/assetstore/assetStoreStorage.js +336 -0
  53. package/dist/assetstore/assetSyncLedger.d.ts +91 -1
  54. package/dist/assetstore/assetSyncLedger.js +718 -59
  55. package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
  56. package/dist/assetstore/foreignJsonlSource.js +150 -0
  57. package/dist/assetstore/index.d.ts +5 -0
  58. package/dist/assetstore/index.js +5 -0
  59. package/dist/assetstore/learningHistory.js +3 -3
  60. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  61. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  62. package/dist/assetstore/localJsonl.d.ts +12 -1
  63. package/dist/assetstore/localJsonl.js +251 -42
  64. package/dist/assetstore/provenance.d.ts +93 -4
  65. package/dist/assetstore/provenance.js +368 -84
  66. package/dist/assetstore/provider.d.ts +63 -0
  67. package/dist/assetstore/provider.js +97 -6
  68. package/dist/assetstore/reviewFilter.d.ts +19 -1
  69. package/dist/assetstore/reviewFilter.js +39 -1
  70. package/dist/assetstore/reviewLedger.d.ts +8 -2
  71. package/dist/assetstore/reviewLedger.js +71 -45
  72. package/dist/assetstore/unionReadStore.d.ts +25 -0
  73. package/dist/assetstore/unionReadStore.js +119 -0
  74. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  75. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  76. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  77. package/dist/benchmark/antiGeneRollout.js +4 -3
  78. package/dist/benchmark/index.d.ts +3 -1
  79. package/dist/benchmark/index.js +3 -1
  80. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  81. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  82. package/dist/benchmark/triggerShift.d.ts +62 -0
  83. package/dist/benchmark/triggerShift.js +106 -0
  84. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  85. package/dist/bootstrap/envFingerprint.js +5 -0
  86. package/dist/bootstrap/index.d.ts +3 -1
  87. package/dist/bootstrap/index.js +3 -1
  88. package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
  89. package/dist/bootstrap/lifecycleBootstrap.js +433 -0
  90. package/dist/bootstrap/v1EnvCompat.d.ts +113 -0
  91. package/dist/bootstrap/v1EnvCompat.js +303 -0
  92. package/dist/events/eventArchive.d.ts +2 -0
  93. package/dist/events/eventArchive.js +13 -3
  94. package/dist/events/eventSchema.d.ts +7 -7
  95. package/dist/events/eventStore.d.ts +2 -0
  96. package/dist/events/eventStore.js +5 -1
  97. package/dist/events/ingest.d.ts +2 -1
  98. package/dist/events/ingest.js +15 -0
  99. package/dist/events/paths.d.ts +12 -10
  100. package/dist/events/paths.js +24 -20
  101. package/dist/events/public.d.ts +2 -2
  102. package/dist/events/public.js +2 -2
  103. package/dist/events/reports.d.ts +2 -0
  104. package/dist/events/reports.js +4 -0
  105. package/dist/exec/autoExec.d.ts +68 -4
  106. package/dist/exec/autoExec.js +392 -33
  107. package/dist/exec/autonomousCycle.d.ts +31 -4
  108. package/dist/exec/autonomousCycle.js +71 -13
  109. package/dist/exec/claudeBridge.d.ts +80 -15
  110. package/dist/exec/claudeBridge.js +845 -76
  111. package/dist/exec/executionBinding.d.ts +414 -0
  112. package/dist/exec/executionBinding.js +588 -0
  113. package/dist/exec/index.d.ts +1 -0
  114. package/dist/exec/index.js +1 -0
  115. package/dist/exec/openPrRegistry.d.ts +8 -2
  116. package/dist/exec/openPrRegistry.js +32 -22
  117. package/dist/exec/prompt.js +14 -1
  118. package/dist/exec/proofOfWork.d.ts +1 -1
  119. package/dist/exec/proofOfWork.js +2 -2
  120. package/dist/exec/runnerRegistry.d.ts +153 -36
  121. package/dist/exec/runnerRegistry.js +848 -65
  122. package/dist/exec/selfPr.js +1 -7
  123. package/dist/feedback/envelope.d.ts +61 -0
  124. package/dist/feedback/envelope.js +168 -0
  125. package/dist/feedback/index.d.ts +1 -0
  126. package/dist/feedback/index.js +1 -0
  127. package/dist/hooks/hooks.js +1 -0
  128. package/dist/hub/assetCallLog.d.ts +35 -1
  129. package/dist/hub/assetCallLog.js +124 -1
  130. package/dist/hub/bindings.d.ts +8 -1
  131. package/dist/hub/bindings.js +29 -8
  132. package/dist/hub/capability.d.ts +130 -4
  133. package/dist/hub/conversationDistiller.d.ts +19 -0
  134. package/dist/hub/conversationDistiller.js +115 -37
  135. package/dist/hub/fake.d.ts +3 -2
  136. package/dist/hub/fake.js +2 -1
  137. package/dist/hub/index.d.ts +1 -0
  138. package/dist/hub/index.js +1 -0
  139. package/dist/hub/questionGenerator.d.ts +5 -1
  140. package/dist/hub/questionGenerator.js +8 -6
  141. package/dist/hub/recipeCompose.d.ts +27 -0
  142. package/dist/hub/recipeCompose.js +90 -0
  143. package/dist/hub/sanitize.js +122 -7
  144. package/dist/index.d.ts +6 -1
  145. package/dist/index.js +7 -1
  146. package/dist/issueReporter/index.d.ts +156 -0
  147. package/dist/issueReporter/index.js +1688 -0
  148. package/dist/mailbox/dispatch.d.ts +1 -1
  149. package/dist/mailbox/dispatch.js +22 -6
  150. package/dist/mailbox/envelope.d.ts +7 -1
  151. package/dist/mailbox/envelope.js +9 -2
  152. package/dist/mailbox/ipcServer.d.ts +12 -2
  153. package/dist/mailbox/ipcServer.js +183 -13
  154. package/dist/mailbox/store.d.ts +89 -3
  155. package/dist/mailbox/store.js +895 -41
  156. package/dist/modelCompatibility.d.ts +164 -0
  157. package/dist/modelCompatibility.js +309 -0
  158. package/dist/observers/valueDigestObserver.d.ts +9 -0
  159. package/dist/observers/valueDigestObserver.js +35 -2
  160. package/dist/ops/cleanup.js +1 -1
  161. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  162. package/dist/ops/evolutionGraphProjection.js +315 -0
  163. package/dist/ops/index.d.ts +2 -1
  164. package/dist/ops/index.js +2 -1
  165. package/dist/ops/savingsCore.js +1 -2
  166. package/dist/ops/selfUpdate.d.ts +18 -1
  167. package/dist/ops/selfUpdate.js +88 -23
  168. package/dist/ops/valueOutreach.d.ts +3 -1
  169. package/dist/ops/valueOutreach.js +5 -1
  170. package/dist/personality/schema.d.ts +24 -24
  171. package/dist/schema/evolutionGraph.d.ts +784 -0
  172. package/dist/schema/evolutionGraph.js +187 -0
  173. package/dist/schema/index.d.ts +1 -0
  174. package/dist/schema/index.js +1 -0
  175. package/dist/schema/proofOfWork.d.ts +125 -6
  176. package/dist/schema/proofOfWork.js +102 -4
  177. package/dist/schema/signal.d.ts +3 -3
  178. package/dist/schema/signal.js +1 -1
  179. package/dist/shadow/shadowHub.js +1 -0
  180. package/dist/signals/curriculum.d.ts +55 -0
  181. package/dist/signals/curriculum.js +202 -0
  182. package/dist/signals/cycleHistoryFromEvents.js +17 -8
  183. package/dist/signals/expand.d.ts +15 -1
  184. package/dist/signals/expand.js +169 -1
  185. package/dist/signals/extractor.d.ts +2 -2
  186. package/dist/signals/extractor.js +31 -6
  187. package/dist/signals/index.d.ts +4 -1
  188. package/dist/signals/index.js +4 -1
  189. package/dist/signals/metaSignals.d.ts +4 -0
  190. package/dist/signals/metaSignals.js +42 -0
  191. package/dist/signals/scopeVocabulary.d.ts +75 -0
  192. package/dist/signals/scopeVocabulary.js +91 -0
  193. package/dist/signals/signalGate.js +1 -1
  194. package/dist/signals/taskDomain.d.ts +22 -0
  195. package/dist/signals/taskDomain.js +43 -0
  196. package/dist/strategy/constraintAblation.d.ts +64 -0
  197. package/dist/strategy/constraintAblation.js +2820 -0
  198. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  199. package/dist/strategy/constraintAblationPredicates.js +339 -0
  200. package/dist/strategy/index.d.ts +2 -1
  201. package/dist/strategy/index.js +2 -1
  202. package/dist/trace/index.d.ts +3 -1
  203. package/dist/trace/index.js +3 -1
  204. package/dist/trace/learningTrace.d.ts +216 -0
  205. package/dist/trace/learningTrace.js +298 -0
  206. package/dist/trace/proxyTurns.d.ts +31 -0
  207. package/dist/trace/proxyTurns.js +137 -0
  208. package/dist/trace/trajectory.d.ts +8 -0
  209. package/dist/trace/trajectory.js +14 -2
  210. package/dist/util/fetchPort.d.ts +1 -0
  211. package/dist/util/fetchPort.js +11 -0
  212. package/dist/util/fileLock.d.ts +113 -7
  213. package/dist/util/fileLock.js +1035 -80
  214. package/dist/util/index.d.ts +3 -1
  215. package/dist/util/index.js +2 -1
  216. package/dist/verify/index.d.ts +2 -1
  217. package/dist/verify/index.js +1 -1
  218. package/dist/verify/sandboxRunner.d.ts +30 -0
  219. package/dist/verify/sandboxRunner.js +370 -27
  220. package/dist/verify/sandboxedValidation.d.ts +16 -2
  221. package/dist/verify/sandboxedValidation.js +200 -22
  222. package/dist/verify/validation.d.ts +35 -4
  223. package/dist/verify/validation.js +164 -16
  224. package/dist/wire/geneHints.d.ts +88 -16
  225. package/dist/wire/geneHints.js +124 -15
  226. package/dist/wire/index.d.ts +8 -3
  227. package/dist/wire/index.js +2 -2
  228. package/dist/wire/schemaGate.d.ts +21 -0
  229. package/dist/wire/schemaGate.js +119 -10
  230. package/dist/workflow/dsl.d.ts +24 -3
  231. package/dist/workflow/dsl.js +4 -0
  232. package/dist/workflow/engine.d.ts +5 -1
  233. package/dist/workflow/engine.js +3 -0
  234. package/dist/workflow/index.d.ts +3 -1
  235. package/dist/workflow/index.js +3 -1
  236. package/dist/workflow/runtime.d.ts +110 -0
  237. package/dist/workflow/runtime.js +1298 -0
  238. package/dist/workflow/stateStore.d.ts +172 -0
  239. package/dist/workflow/stateStore.js +1044 -0
  240. package/package.json +12 -4
@@ -1,2 +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';
3
+ export * from './fetchPort.js';
2
4
  export * from './version.js';
@@ -1,2 +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
+ export * from './fetchPort.js';
2
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';
@@ -19,13 +19,43 @@ export interface SandboxOptions {
19
19
  * unprivileged mount namespaces are unavailable. Binaries live outside these dirs, so the command still runs.
20
20
  */
21
21
  hideHomeSecrets?: boolean;
22
+ /** Remount the inherited filesystem read-only inside the mount namespace, leaving only an isolated /tmp writable. */
23
+ readOnlyFilesystem?: boolean;
24
+ /** Host scratch directory exposed as the sandbox's only writable filesystem. */
25
+ writableTmpDir?: string;
26
+ /** Read-only checkout root remounted at its original absolute path after HOME and /tmp are hidden. */
27
+ readOnlyRoot?: string;
28
+ /** Put the full command tree in a bounded cgroup v2. Required by verified Skill execution. */
29
+ resourceLimits?: boolean;
22
30
  /** Injected unshare-availability probe (test seam). Default: a real `unshare -r -m -n true` check (cached). */
23
31
  unshareCheck?: () => boolean;
32
+ /** Injected cgroup allocator (test seam). */
33
+ resourceGroupFactory?: () => SandboxResourceGroup | null;
34
+ /** Cooperative cancellation for the current validation command. */
35
+ signal?: AbortSignal;
24
36
  }
37
+ export interface SandboxResourceGroup {
38
+ procsFile: string;
39
+ cleanup: () => void;
40
+ }
41
+ /** Configure an already-created cgroup. Kept separate so limit values are testable without resource exhaustion. */
42
+ export declare function configureSandboxResourceGroup(path: string): boolean;
43
+ /** Allocate a delegated cgroup v2 for one validation command. Returns null unless every limit is enforceable. */
44
+ export declare function createSandboxResourceGroup(): SandboxResourceGroup | null;
45
+ /** Join the cgroup before exec, so attacker-controlled code never runs outside the aggregate resource budget. */
46
+ export declare function resourceLimitedCommand(cmd: string, args: readonly string[], procsFile: string): {
47
+ cmd: string;
48
+ args: string[];
49
+ };
50
+ export declare function sandboxResourceLimitsAvailable(): boolean;
25
51
  /** Wrap an (executable,args) in unprivileged namespaces per the requested isolation (pure — testable). */
26
52
  export declare function isolationCommand(bin: string, args: readonly string[], opts: {
27
53
  noNetwork?: boolean;
28
54
  hideHomeSecrets?: boolean;
55
+ readOnlyFilesystem?: boolean;
56
+ writableTmpDir?: string;
57
+ readOnlyRoot?: string;
58
+ cwd?: string;
29
59
  }): {
30
60
  cmd: string;
31
61
  args: string[];
@@ -5,41 +5,207 @@
5
5
  // module hardens EXECUTION: blocked node eval-flags, shell-metachar rejection, a fresh wiped temp cwd,
6
6
  // a scrubbed env (no secrets leak in), and a SIGKILL timeout. Output is folded + truncated.
7
7
  import { spawn, spawnSync } from 'node:child_process';
8
- import { mkdtempSync, rmSync } from 'node:fs';
8
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, rmdirSync, writeFileSync } from 'node:fs';
9
9
  import { tmpdir } from 'node:os';
10
- import { join } from 'node:path';
11
- import { isNodeExecutable, nodeFlagViolation, SHELL_METACHARS } from './validation.js';
10
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
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;
17
+ const CGROUP_ROOT = '/sys/fs/cgroup';
18
+ const TRUSTED_SYSTEM_PATH = '/usr/sbin:/usr/bin:/sbin:/bin';
19
+ const SYSTEM_SH = '/bin/sh';
20
+ const SYSTEM_UNSHARE = ['/usr/bin/unshare', '/bin/unshare'].find((path) => existsSync(path)) ?? '/usr/bin/unshare';
21
+ const RESOURCE_LIMITS = {
22
+ memoryBytes: 1024 * 1024 * 1024,
23
+ processes: 64,
24
+ cpuQuota: '100000 100000',
25
+ scratchBytes: 256 * 1024 * 1024,
26
+ };
15
27
  // Env scrub: only these keys reach the child, so node-secrets/tokens in the parent env cannot leak into validation.
16
28
  const DEFAULT_ENV_ALLOW = ['PATH', 'HOME', 'LANG', 'LC_ALL', 'TMPDIR', 'SYSTEMROOT'];
17
29
  /** Credential dirs hidden under $HOME when hideHomeSecrets is set (dirs only — never where binaries live). */
18
30
  const SECRET_HOME_DIRS = ['.evomap', '.ssh', '.aws', '.gnupg', '.docker', '.kube'];
19
31
  // Fixed setup run inside the mount ns: tmpfs over each existing secret dir, then exec the command. The command
20
32
  // is passed as POSITIONAL args (`exec "$@"`), never interpolated into the script — no shell-injection vector.
21
- const HIDE_SECRETS_SCRIPT = `for d in ${SECRET_HOME_DIRS.map((d) => `"$HOME/${d}"`).join(' ')}; do [ -d "$d" ] && mount -t tmpfs none "$d" 2>/dev/null; done; exec "$@"`;
33
+ const HIDE_SECRETS_SETUP = `PATH=${TRUSTED_SYSTEM_PATH}; export PATH; for d in ${SECRET_HOME_DIRS.map((d) => `"$HOME/${d}"`).join(' ')}; do if [ -d "$d" ]; then mount -t tmpfs none "$d" 2>/dev/null || exit 126; fi; done`;
34
+ const READ_ONLY_FILESYSTEM_SETUP = [
35
+ `PATH=${TRUSTED_SYSTEM_PATH}; export PATH`,
36
+ 'session_tmp="$1"; shift',
37
+ 'validation_root="$1"; shift',
38
+ 'validation_cwd="$1"; shift',
39
+ 'host_bin="$1"; shift',
40
+ '[ -n "$HOME" ] && [ "$HOME" != / ] && [ -d "$HOME" ] || exit 126',
41
+ 'case "$validation_cwd/" in "$validation_root/"*) ;; *) exit 126 ;; esac',
42
+ 'case "$HOME/" in "$validation_root/"*) exit 126 ;; esac',
43
+ `mount -t tmpfs -o size=${RESOURCE_LIMITS.scratchBytes},nr_inodes=65536 none "$session_tmp" 2>/dev/null || exit 126`,
44
+ 'mkdir -p "$session_tmp/source" "$session_tmp/home" || exit 126',
45
+ 'mkdir -p "$session_tmp/dev" || exit 126',
46
+ ': > "$session_tmp/node" || exit 126',
47
+ 'mount --bind "$validation_root" "$session_tmp/source" 2>/dev/null || exit 126',
48
+ 'mount --bind "$host_bin" "$session_tmp/node" 2>/dev/null || exit 126',
49
+ 'for device in null zero random urandom; do : > "$session_tmp/dev/$device" || exit 126; mount --bind "/dev/$device" "$session_tmp/dev/$device" 2>/dev/null || exit 126; done',
50
+ 'mount -t tmpfs none "$HOME" 2>/dev/null || exit 126',
51
+ '[ ! -d /run ] || mount -t tmpfs none /run 2>/dev/null || exit 126',
52
+ 'mount -t tmpfs none /tmp 2>/dev/null || exit 126',
53
+ 'mkdir -p "$validation_root" || exit 126',
54
+ 'mount --bind "$session_tmp/source" "$validation_root" 2>/dev/null || exit 126',
55
+ 'mounts="$(findmnt -rn -o TARGET -R /)" || exit 126',
56
+ '[ -n "$mounts" ] || exit 126',
57
+ 'printf "%s\\n" "$mounts" | while IFS= read -r target; do mount -o remount,bind,ro "$target" 2>/dev/null || exit 126; done || exit 126',
58
+ 'mount -o remount,bind,rw,exec "$session_tmp" 2>/dev/null || exit 126',
59
+ 'mount -t tmpfs -o mode=755,size=65536,nr_inodes=64 none /dev 2>/dev/null || exit 126',
60
+ 'for device in null zero random urandom; do : > "/dev/$device" || exit 126; mount --bind "$session_tmp/dev/$device" "/dev/$device" 2>/dev/null || exit 126; done',
61
+ 'mount -o remount,ro /dev 2>/dev/null || exit 126',
62
+ 'mount --rbind "$session_tmp" /tmp 2>/dev/null || exit 126',
63
+ 'cd "$validation_cwd" || exit 126',
64
+ 'export HOME=/tmp/home TMPDIR=/tmp TMP=/tmp TEMP=/tmp',
65
+ ].join('; ');
66
+ const DROP_NAMESPACE_PRIVILEGES = [
67
+ 'supervisor_pid=$$',
68
+ 'exec 3<&0',
69
+ 'setpriv --no-new-privs --securebits=+noroot,+noroot_locked --bounding-set=-all --inh-caps=-all --ambient-caps=-all -- /tmp/node "$@" </dev/null 3<&- & workload_pid=$!',
70
+ // The pipe is owned by the parent Node process. Kernel EOF is therefore a reliable parent-death signal,
71
+ // including SIGKILL, while unshare --kill-child propagates supervisor death through the PID namespace.
72
+ '(IFS= read -r _ <&3 || kill -KILL "$supervisor_pid") & watchdog_pid=$!',
73
+ 'exec 3<&-',
74
+ 'wait "$workload_pid"; status=$?',
75
+ 'kill "$watchdog_pid" 2>/dev/null || true',
76
+ 'wait "$watchdog_pid" 2>/dev/null || true',
77
+ 'exit "$status"',
78
+ ].join('; ');
79
+ function pathIsWithin(root, target) {
80
+ const rel = relative(root, target);
81
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
82
+ }
83
+ function currentCgroupPath() {
84
+ const entry = readFileSync('/proc/self/cgroup', 'utf8')
85
+ .split(/\r?\n/)
86
+ .find((line) => line.startsWith('0::'));
87
+ if (!entry)
88
+ return null;
89
+ const relativePath = entry.slice(3).replace(/^\/+/, '');
90
+ const path = resolve(CGROUP_ROOT, relativePath);
91
+ return pathIsWithin(CGROUP_ROOT, path) ? path : null;
92
+ }
93
+ /** Configure an already-created cgroup. Kept separate so limit values are testable without resource exhaustion. */
94
+ export function configureSandboxResourceGroup(path) {
95
+ try {
96
+ const required = ['cgroup.procs', 'cgroup.kill', 'memory.max', 'memory.swap.max', 'memory.oom.group', 'pids.max', 'cpu.max'];
97
+ if (!required.every((name) => existsSync(join(path, name))))
98
+ return false;
99
+ writeFileSync(join(path, 'memory.max'), String(RESOURCE_LIMITS.memoryBytes));
100
+ writeFileSync(join(path, 'memory.swap.max'), '0');
101
+ writeFileSync(join(path, 'memory.oom.group'), '1');
102
+ writeFileSync(join(path, 'pids.max'), String(RESOURCE_LIMITS.processes));
103
+ writeFileSync(join(path, 'cpu.max'), RESOURCE_LIMITS.cpuQuota);
104
+ return true;
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
110
+ /** Allocate a delegated cgroup v2 for one validation command. Returns null unless every limit is enforceable. */
111
+ export function createSandboxResourceGroup() {
112
+ if (process.platform !== 'linux' || !existsSync(join(CGROUP_ROOT, 'cgroup.controllers')))
113
+ return null;
114
+ let path = null;
115
+ try {
116
+ const parent = currentCgroupPath();
117
+ if (!parent)
118
+ return null;
119
+ path = join(parent, `evolver-validation-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
120
+ mkdirSync(path, { mode: 0o700 });
121
+ if (!configureSandboxResourceGroup(path))
122
+ throw new Error('required cgroup v2 controllers are not delegated');
123
+ let cleaned = false;
124
+ return {
125
+ procsFile: join(path, 'cgroup.procs'),
126
+ cleanup: () => {
127
+ if (cleaned)
128
+ return;
129
+ cleaned = true;
130
+ try {
131
+ writeFileSync(join(path, 'cgroup.kill'), '1');
132
+ }
133
+ catch { /* already empty or removed */ }
134
+ try {
135
+ rmdirSync(path);
136
+ }
137
+ catch { /* kernel may release the empty group asynchronously */ }
138
+ },
139
+ };
140
+ }
141
+ catch {
142
+ if (path) {
143
+ try {
144
+ writeFileSync(join(path, 'cgroup.kill'), '1');
145
+ }
146
+ catch { /* best effort */ }
147
+ try {
148
+ rmdirSync(path);
149
+ }
150
+ catch { /* best effort */ }
151
+ }
152
+ return null;
153
+ }
154
+ }
155
+ /** Join the cgroup before exec, so attacker-controlled code never runs outside the aggregate resource budget. */
156
+ export function resourceLimitedCommand(cmd, args, procsFile) {
157
+ const setup = 'printf "%s" "$$" > "$1" || exit 126; shift; exec "$@"';
158
+ return { cmd: SYSTEM_SH, args: ['-c', setup, 'sh', procsFile, cmd, ...args] };
159
+ }
160
+ export function sandboxResourceLimitsAvailable() {
161
+ const group = createSandboxResourceGroup();
162
+ if (!group)
163
+ return false;
164
+ group.cleanup();
165
+ return true;
166
+ }
22
167
  /** Wrap an (executable,args) in unprivileged namespaces per the requested isolation (pure — testable). */
23
168
  export function isolationCommand(bin, args, opts) {
169
+ if (opts.readOnlyFilesystem && !opts.writableTmpDir) {
170
+ throw new Error('read-only filesystem isolation requires writableTmpDir');
171
+ }
172
+ if (opts.readOnlyFilesystem && (!opts.readOnlyRoot || !opts.cwd)) {
173
+ throw new Error('read-only filesystem isolation requires readOnlyRoot and cwd');
174
+ }
24
175
  const flags = ['-r'];
25
- if (opts.hideHomeSecrets)
176
+ if (opts.hideHomeSecrets || opts.readOnlyFilesystem)
26
177
  flags.push('-m');
27
178
  if (opts.noNetwork)
28
179
  flags.push('-n');
29
- if (!opts.noNetwork && !opts.hideHomeSecrets)
180
+ if (opts.readOnlyFilesystem)
181
+ flags.push('-p', '-i', '-u');
182
+ if (!opts.noNetwork && !opts.hideHomeSecrets && !opts.readOnlyFilesystem)
30
183
  return { cmd: bin, args: [...args] };
31
- const base = [...flags, '--fork', '--kill-child', '--'];
184
+ const base = [...flags, '--fork', '--kill-child', ...(opts.readOnlyFilesystem ? ['--mount-proc'] : []), '--'];
185
+ const setup = [
186
+ ...(opts.hideHomeSecrets && !opts.readOnlyFilesystem ? [HIDE_SECRETS_SETUP] : []),
187
+ ...(opts.readOnlyFilesystem ? [READ_ONLY_FILESYSTEM_SETUP] : []),
188
+ opts.readOnlyFilesystem ? DROP_NAMESPACE_PRIVILEGES : 'exec "$@"',
189
+ ].join('; ');
32
190
  // Mount setup needs a launcher (sh runs the FIXED script then exec "$@"); net-only needs no shell.
33
- return opts.hideHomeSecrets
34
- ? { cmd: 'unshare', args: [...base, 'sh', '-c', HIDE_SECRETS_SCRIPT, 'sh', bin, ...args] }
35
- : { cmd: 'unshare', args: [...base, bin, ...args] };
191
+ return opts.hideHomeSecrets || opts.readOnlyFilesystem
192
+ ? {
193
+ cmd: SYSTEM_UNSHARE,
194
+ args: [...base, SYSTEM_SH, '-c', setup, 'sh', ...(opts.readOnlyFilesystem
195
+ ? [opts.writableTmpDir, opts.readOnlyRoot, opts.cwd]
196
+ : []), bin, ...args],
197
+ }
198
+ : { cmd: SYSTEM_UNSHARE, args: [...base, bin, ...args] };
36
199
  }
37
200
  let unshareCache;
38
201
  /** Whether unprivileged user+mount+net namespaces (`unshare -r -m -n`) work here (cached) — covers both isolation modes. */
39
202
  export function unshareNetAvailable() {
40
203
  if (unshareCache === undefined) {
41
204
  try {
42
- unshareCache = spawnSync('unshare', ['-r', '-m', '-n', 'true'], { timeout: 5000 }).status === 0;
205
+ unshareCache = spawnSync(SYSTEM_UNSHARE, ['-r', '-m', '-n', 'true'], {
206
+ env: { PATH: TRUSTED_SYSTEM_PATH },
207
+ timeout: 5000,
208
+ }).status === 0;
43
209
  }
44
210
  catch {
45
211
  unshareCache = false;
@@ -48,7 +214,9 @@ export function unshareNetAvailable() {
48
214
  return unshareCache;
49
215
  }
50
216
  function parse(cmd) {
51
- const tokens = cmd.trim().split(/\s+/).filter(Boolean);
217
+ const tokens = tokenizeValidationCommand(cmd);
218
+ if (!tokens)
219
+ return null;
52
220
  return { executable: tokens[0] ?? '', args: tokens.slice(1) };
53
221
  }
54
222
  function scrubEnv(allow) {
@@ -60,6 +228,72 @@ function scrubEnv(allow) {
60
228
  }
61
229
  return out;
62
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
+ }
63
297
  /**
64
298
  * Build a hardened CommandRunner. The executable allowlist is the ValidationPlan's job (runValidation);
65
299
  * this only hardens how an allowed command runs.
@@ -67,50 +301,159 @@ function scrubEnv(allow) {
67
301
  export function makeSandboxRunner(opts = {}) {
68
302
  const timeout = Math.min(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
69
303
  const envAllow = opts.envAllowKeys ?? DEFAULT_ENV_ALLOW;
70
- return (cmd) => new Promise((resolve) => {
71
- const deny = (msg, code = 126) => resolve({ exitCode: code, stdout: `[sandbox] ${msg}` });
72
- 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;
73
326
  if (!executable)
74
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
+ }
75
331
  if (SHELL_METACHARS.test(cmd))
76
332
  return deny('rejected: shell metacharacter (no shell is provided; split compound commands)');
77
333
  const badFlag = nodeFlagViolation(executable, args);
78
334
  if (badFlag)
79
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
+ }
80
344
  // Isolation (#26): fail-safe — if we can't actually create the namespaces, refuse rather than run un-isolated.
81
- if ((opts.noNetwork || opts.hideHomeSecrets) && !(opts.unshareCheck ?? unshareNetAvailable)()) {
82
- return deny('rejected: isolation (noNetwork/hideHomeSecrets) requested but unprivileged namespaces (unshare -r -m -n) are unavailable here');
345
+ if ((opts.noNetwork || opts.hideHomeSecrets || opts.readOnlyFilesystem) && !(opts.unshareCheck ?? unshareNetAvailable)()) {
346
+ return deny('rejected: requested namespace isolation is unavailable');
83
347
  }
348
+ const resourceGroup = opts.resourceLimits
349
+ ? (opts.resourceGroupFactory ?? createSandboxResourceGroup)()
350
+ : null;
351
+ if (opts.resourceLimits && !resourceGroup)
352
+ return deny('rejected: cgroup v2 resource limits are unavailable');
84
353
  const ownTemp = !opts.cwd;
85
354
  let cwd;
86
355
  try {
87
356
  cwd = opts.cwd ?? mkdtempSync(join(tmpdir(), 'evo-sbx-'));
88
357
  }
89
358
  catch (e) {
359
+ resourceGroup?.cleanup();
90
360
  return deny(`temp dir failed: ${e instanceof Error ? e.message : 'err'}`, 1);
91
361
  }
92
- const cleanup = () => { if (ownTemp) {
93
- try {
94
- rmSync(cwd, { recursive: true, force: true });
362
+ let cleaned = false;
363
+ const cleanup = () => {
364
+ if (cleaned)
365
+ return;
366
+ cleaned = true;
367
+ if (timeoutTimer !== undefined)
368
+ clearTimeout(timeoutTimer);
369
+ removeAbortListener();
370
+ resourceGroup?.cleanup();
371
+ if (ownTemp) {
372
+ try {
373
+ rmSync(cwd, { recursive: true, force: true });
374
+ }
375
+ catch { /* best effort */ }
95
376
  }
96
- catch { /* best effort */ }
97
- } };
377
+ };
98
378
  // Resolve 'node' to the running binary so spawn(shell:false) works on Windows too (where bare 'node' would ENOENT).
99
379
  const bin = isNodeExecutable(executable) ? process.execPath : executable;
100
380
  try {
101
- const { cmd: spawnCmd, args: spawnArgs } = isolationCommand(bin, args, { noNetwork: opts.noNetwork, hideHomeSecrets: opts.hideHomeSecrets });
102
- const child = spawn(spawnCmd, spawnArgs, { shell: false, cwd, env: scrubEnv(envAllow), timeout, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
381
+ const { cmd: spawnCmd, args: spawnArgs } = isolationCommand(bin, args, {
382
+ noNetwork: opts.noNetwork,
383
+ hideHomeSecrets: opts.hideHomeSecrets,
384
+ readOnlyFilesystem: opts.readOnlyFilesystem,
385
+ writableTmpDir: opts.writableTmpDir,
386
+ readOnlyRoot: opts.readOnlyRoot ?? cwd,
387
+ cwd,
388
+ });
389
+ const env = scrubEnv(envAllow);
390
+ if (opts.noNetwork || opts.hideHomeSecrets || opts.readOnlyFilesystem || opts.resourceLimits) {
391
+ env['PATH'] = TRUSTED_SYSTEM_PATH;
392
+ }
393
+ if (opts.readOnlyFilesystem) {
394
+ Object.assign(env, { TMPDIR: '/tmp', TMP: '/tmp', TEMP: '/tmp' });
395
+ }
396
+ const limited = resourceGroup ? resourceLimitedCommand(spawnCmd, spawnArgs, resourceGroup.procsFile) : { cmd: spawnCmd, args: spawnArgs };
397
+ const child = spawn(limited.cmd, limited.args, {
398
+ shell: false,
399
+ cwd,
400
+ env,
401
+ // POSIX 依靠 detached 进程组执行整树终止;Windows 的 taskkill 直接沿父子树追踪,保持启动器附着可
402
+ // 避免兜底终止时产生孤儿进程。
403
+ detached: process.platform !== 'win32',
404
+ windowsHide: true,
405
+ stdio: [opts.readOnlyFilesystem ? 'pipe' : 'ignore', 'pipe', 'pipe'],
406
+ });
407
+ let childClosed = false;
103
408
  let out = '';
104
409
  const cap = (d) => { if (out.length < MAX_OUTPUT_CHARS)
105
410
  out += String(d); };
106
411
  child.stdout?.on('data', cap);
107
412
  child.stderr?.on('data', cap);
108
- child.on('error', (err) => { cleanup(); resolve({ exitCode: 127, stdout: `[sandbox] spawn error: ${err.message}` }); });
109
- 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?.();
110
453
  }
111
454
  catch (e) {
112
455
  cleanup();
113
- 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'}` });
114
457
  }
115
458
  });
116
459
  }
@@ -1,15 +1,19 @@
1
+ import { type SandboxResourceGroup } from './sandboxRunner.js';
1
2
  import { type ValidationResult } from './validation.js';
3
+ export type SandboxedValidationSkipReason = 'missing_script' | 'script_outside_root' | 'script_symlink' | 'script_unresolvable';
2
4
  export interface SandboxedValidationSkippedCommand {
3
5
  cmd: string;
4
6
  script: string;
5
- reason: 'missing_script';
7
+ reason: SandboxedValidationSkipReason;
6
8
  }
7
9
  export interface SandboxedValidationResult {
8
10
  passed: boolean;
11
+ /** 是否因宿主取消而终止;取消永远不是成功验证。 */
12
+ cancelled?: boolean;
9
13
  /** Convenience score for cycle outcomes: 0.95 pass / 0.2 fail (matches the prior inline hook). */
10
14
  score: number;
11
15
  results: ValidationResult[];
12
- /** 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. */
13
17
  skipped: SandboxedValidationSkippedCommand[];
14
18
  /**
15
19
  * Whether OS-namespace isolation (no-network + hidden home secrets) was applied. TRUE only where unprivileged
@@ -19,11 +23,21 @@ export interface SandboxedValidationResult {
19
23
  */
20
24
  isolated: boolean;
21
25
  }
26
+ export declare function readOnlyFilesystemIsolationAvailable(): boolean;
27
+ export declare function readOnlyIsolationAvailable(): boolean;
22
28
  export interface SandboxedValidationOptions {
23
29
  /** Per-command timeout (ms), forwarded to the sandbox runner. */
24
30
  timeoutMs?: number;
31
+ /** Cooperative cancellation forwarded to the validation process. */
32
+ signal?: AbortSignal;
25
33
  /** Test seam: override the unprivileged-namespace availability probe. */
26
34
  unshareCheck?: () => boolean;
35
+ /** Refuse before spawning when network/home namespace isolation is unavailable. */
36
+ requireIsolation?: boolean;
37
+ /** Checkout root to preserve read-only at its original absolute path. */
38
+ readOnlyRoot?: string;
39
+ /** Injected cgroup allocator (test seam). */
40
+ resourceGroupFactory?: () => SandboxResourceGroup | null;
27
41
  }
28
42
  /**
29
43
  * Run validation commands in the hardened sandbox. Isolation (no-network + hidden home secrets) is requested only