@evomap/evolver-core 2.0.0-beta.0

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 (373) hide show
  1. package/dist/algo/antiDistill.d.ts +35 -0
  2. package/dist/algo/antiDistill.js +167 -0
  3. package/dist/algo/bans.d.ts +6 -0
  4. package/dist/algo/bans.js +33 -0
  5. package/dist/algo/candidateAssembly.d.ts +65 -0
  6. package/dist/algo/candidateAssembly.js +196 -0
  7. package/dist/algo/capabilityCandidates.d.ts +38 -0
  8. package/dist/algo/capabilityCandidates.js +131 -0
  9. package/dist/algo/confidence.d.ts +106 -0
  10. package/dist/algo/confidence.js +196 -0
  11. package/dist/algo/cycleEngine.d.ts +143 -0
  12. package/dist/algo/cycleEngine.js +417 -0
  13. package/dist/algo/cycleFailureClassifier.d.ts +52 -0
  14. package/dist/algo/cycleFailureClassifier.js +104 -0
  15. package/dist/algo/epigenetics.d.ts +14 -0
  16. package/dist/algo/epigenetics.js +41 -0
  17. package/dist/algo/evolutionEvent.d.ts +23 -0
  18. package/dist/algo/evolutionEvent.js +24 -0
  19. package/dist/algo/exploration.d.ts +34 -0
  20. package/dist/algo/exploration.js +55 -0
  21. package/dist/algo/geneHealth.d.ts +24 -0
  22. package/dist/algo/geneHealth.js +16 -0
  23. package/dist/algo/geneIntake.d.ts +37 -0
  24. package/dist/algo/geneIntake.js +100 -0
  25. package/dist/algo/genePromotion.d.ts +48 -0
  26. package/dist/algo/genePromotion.js +53 -0
  27. package/dist/algo/geneSelection.d.ts +128 -0
  28. package/dist/algo/geneSelection.js +222 -0
  29. package/dist/algo/index.d.ts +18 -0
  30. package/dist/algo/index.js +18 -0
  31. package/dist/algo/mutation.d.ts +12 -0
  32. package/dist/algo/mutation.js +22 -0
  33. package/dist/algo/orchestrator.d.ts +65 -0
  34. package/dist/algo/orchestrator.js +63 -0
  35. package/dist/algo/solidify.d.ts +43 -0
  36. package/dist/algo/solidify.js +63 -0
  37. package/dist/algo/strategyPresets.d.ts +32 -0
  38. package/dist/algo/strategyPresets.js +61 -0
  39. package/dist/assetstore/index.d.ts +8 -0
  40. package/dist/assetstore/index.js +8 -0
  41. package/dist/assetstore/learningHistory.d.ts +25 -0
  42. package/dist/assetstore/learningHistory.js +38 -0
  43. package/dist/assetstore/localJsonl.d.ts +34 -0
  44. package/dist/assetstore/localJsonl.js +182 -0
  45. package/dist/assetstore/pendingSignals.d.ts +22 -0
  46. package/dist/assetstore/pendingSignals.js +171 -0
  47. package/dist/assetstore/provenance.d.ts +39 -0
  48. package/dist/assetstore/provenance.js +109 -0
  49. package/dist/assetstore/provider.d.ts +48 -0
  50. package/dist/assetstore/provider.js +36 -0
  51. package/dist/assetstore/remoteStub.d.ts +32 -0
  52. package/dist/assetstore/remoteStub.js +36 -0
  53. package/dist/assetstore/reviewFilter.d.ts +20 -0
  54. package/dist/assetstore/reviewFilter.js +44 -0
  55. package/dist/assetstore/reviewLedger.d.ts +55 -0
  56. package/dist/assetstore/reviewLedger.js +121 -0
  57. package/dist/benchmark/antiGeneBenchmark.d.ts +79 -0
  58. package/dist/benchmark/antiGeneBenchmark.js +241 -0
  59. package/dist/benchmark/antiGeneRollout.d.ts +80 -0
  60. package/dist/benchmark/antiGeneRollout.js +256 -0
  61. package/dist/benchmark/benchmark.d.ts +32 -0
  62. package/dist/benchmark/benchmark.js +26 -0
  63. package/dist/benchmark/evolutionThesisSolver.d.ts +25 -0
  64. package/dist/benchmark/evolutionThesisSolver.js +41 -0
  65. package/dist/benchmark/index.d.ts +5 -0
  66. package/dist/benchmark/index.js +5 -0
  67. package/dist/benchmark/thesis.d.ts +85 -0
  68. package/dist/benchmark/thesis.js +150 -0
  69. package/dist/bootstrap/envFingerprint.d.ts +45 -0
  70. package/dist/bootstrap/envFingerprint.js +72 -0
  71. package/dist/bootstrap/index.d.ts +2 -0
  72. package/dist/bootstrap/index.js +2 -0
  73. package/dist/bootstrap/selfEvolve.d.ts +36 -0
  74. package/dist/bootstrap/selfEvolve.js +26 -0
  75. package/dist/cycle/cycleTimeline.d.ts +35 -0
  76. package/dist/cycle/cycleTimeline.js +74 -0
  77. package/dist/cycle/index.d.ts +2 -0
  78. package/dist/cycle/index.js +2 -0
  79. package/dist/cycle/stateMachine.d.ts +6 -0
  80. package/dist/cycle/stateMachine.js +25 -0
  81. package/dist/daemon/cycleContext.d.ts +12 -0
  82. package/dist/daemon/cycleContext.js +13 -0
  83. package/dist/daemon/drain.d.ts +11 -0
  84. package/dist/daemon/drain.js +16 -0
  85. package/dist/daemon/guards.d.ts +87 -0
  86. package/dist/daemon/guards.js +142 -0
  87. package/dist/daemon/idempotency.d.ts +11 -0
  88. package/dist/daemon/idempotency.js +36 -0
  89. package/dist/daemon/idleScheduler.d.ts +55 -0
  90. package/dist/daemon/idleScheduler.js +181 -0
  91. package/dist/daemon/index.d.ts +8 -0
  92. package/dist/daemon/index.js +8 -0
  93. package/dist/daemon/stepRunner.d.ts +12 -0
  94. package/dist/daemon/stepRunner.js +31 -0
  95. package/dist/daemon/unhandledRejectionWindow.d.ts +21 -0
  96. package/dist/daemon/unhandledRejectionWindow.js +52 -0
  97. package/dist/daemon/watchdog.d.ts +11 -0
  98. package/dist/daemon/watchdog.js +14 -0
  99. package/dist/events/eventSchema.d.ts +132 -0
  100. package/dist/events/eventSchema.js +30 -0
  101. package/dist/events/eventStore.d.ts +32 -0
  102. package/dist/events/eventStore.js +137 -0
  103. package/dist/events/ingest.d.ts +34 -0
  104. package/dist/events/ingest.js +61 -0
  105. package/dist/events/paths.d.ts +22 -0
  106. package/dist/events/paths.js +44 -0
  107. package/dist/events/projectors.d.ts +9 -0
  108. package/dist/events/projectors.js +11 -0
  109. package/dist/events/public.d.ts +14 -0
  110. package/dist/events/public.js +8 -0
  111. package/dist/events/replayer.d.ts +26 -0
  112. package/dist/events/replayer.js +67 -0
  113. package/dist/events/reports.d.ts +90 -0
  114. package/dist/events/reports.js +244 -0
  115. package/dist/events/retention.d.ts +70 -0
  116. package/dist/events/retention.js +177 -0
  117. package/dist/events/sink.d.ts +5 -0
  118. package/dist/events/sink.js +1 -0
  119. package/dist/exec/autoExec.d.ts +137 -0
  120. package/dist/exec/autoExec.js +221 -0
  121. package/dist/exec/autonomousCycle.d.ts +48 -0
  122. package/dist/exec/autonomousCycle.js +78 -0
  123. package/dist/exec/claudeBridge.d.ts +112 -0
  124. package/dist/exec/claudeBridge.js +219 -0
  125. package/dist/exec/index.d.ts +9 -0
  126. package/dist/exec/index.js +11 -0
  127. package/dist/exec/openPrRegistry.d.ts +60 -0
  128. package/dist/exec/openPrRegistry.js +130 -0
  129. package/dist/exec/policy/blastRadius.d.ts +31 -0
  130. package/dist/exec/policy/blastRadius.js +67 -0
  131. package/dist/exec/policy/constraints.d.ts +26 -0
  132. package/dist/exec/policy/constraints.js +62 -0
  133. package/dist/exec/policy/destructive.d.ts +45 -0
  134. package/dist/exec/policy/destructive.js +108 -0
  135. package/dist/exec/policy/failureMode.d.ts +33 -0
  136. package/dist/exec/policy/failureMode.js +49 -0
  137. package/dist/exec/policy/index.d.ts +24 -0
  138. package/dist/exec/policy/index.js +30 -0
  139. package/dist/exec/policy/protectedPaths.d.ts +17 -0
  140. package/dist/exec/policy/protectedPaths.js +65 -0
  141. package/dist/exec/policyCheck.d.ts +2 -0
  142. package/dist/exec/policyCheck.js +7 -0
  143. package/dist/exec/prompt.d.ts +34 -0
  144. package/dist/exec/prompt.js +108 -0
  145. package/dist/exec/proofOfWork.d.ts +9 -0
  146. package/dist/exec/proofOfWork.js +11 -0
  147. package/dist/exec/runnerRegistry.d.ts +123 -0
  148. package/dist/exec/runnerRegistry.js +237 -0
  149. package/dist/exec/selfPr.d.ts +83 -0
  150. package/dist/exec/selfPr.js +104 -0
  151. package/dist/exec/selfPrObfuscation.d.ts +21 -0
  152. package/dist/exec/selfPrObfuscation.js +85 -0
  153. package/dist/hooks/hooks.d.ts +117 -0
  154. package/dist/hooks/hooks.js +89 -0
  155. package/dist/hooks/index.d.ts +1 -0
  156. package/dist/hooks/index.js +1 -0
  157. package/dist/hub/assetCallLog.d.ts +43 -0
  158. package/dist/hub/assetCallLog.js +72 -0
  159. package/dist/hub/bindings.d.ts +28 -0
  160. package/dist/hub/bindings.js +90 -0
  161. package/dist/hub/capability.d.ts +278 -0
  162. package/dist/hub/capability.js +1 -0
  163. package/dist/hub/conversationDistiller.d.ts +88 -0
  164. package/dist/hub/conversationDistiller.js +264 -0
  165. package/dist/hub/fake.d.ts +45 -0
  166. package/dist/hub/fake.js +69 -0
  167. package/dist/hub/hubReview.d.ts +91 -0
  168. package/dist/hub/hubReview.js +106 -0
  169. package/dist/hub/index.d.ts +10 -0
  170. package/dist/hub/index.js +10 -0
  171. package/dist/hub/ingest.d.ts +15 -0
  172. package/dist/hub/ingest.js +15 -0
  173. package/dist/hub/questionGenerator.d.ts +59 -0
  174. package/dist/hub/questionGenerator.js +406 -0
  175. package/dist/hub/reuseDecision.d.ts +87 -0
  176. package/dist/hub/reuseDecision.js +127 -0
  177. package/dist/hub/sanitize.d.ts +52 -0
  178. package/dist/hub/sanitize.js +321 -0
  179. package/dist/index.d.ts +27 -0
  180. package/dist/index.js +27 -0
  181. package/dist/mailbox/catalog.d.ts +17 -0
  182. package/dist/mailbox/catalog.js +53 -0
  183. package/dist/mailbox/daemon.d.ts +48 -0
  184. package/dist/mailbox/daemon.js +61 -0
  185. package/dist/mailbox/dispatch.d.ts +46 -0
  186. package/dist/mailbox/dispatch.js +62 -0
  187. package/dist/mailbox/envelope.d.ts +43 -0
  188. package/dist/mailbox/envelope.js +52 -0
  189. package/dist/mailbox/index.d.ts +8 -0
  190. package/dist/mailbox/index.js +8 -0
  191. package/dist/mailbox/ipcServer.d.ts +46 -0
  192. package/dist/mailbox/ipcServer.js +168 -0
  193. package/dist/mailbox/requestReply.d.ts +40 -0
  194. package/dist/mailbox/requestReply.js +77 -0
  195. package/dist/mailbox/store.d.ts +74 -0
  196. package/dist/mailbox/store.js +308 -0
  197. package/dist/mailbox/wake.d.ts +45 -0
  198. package/dist/mailbox/wake.js +60 -0
  199. package/dist/material/boundary.d.ts +7 -0
  200. package/dist/material/boundary.js +14 -0
  201. package/dist/material/consumer.d.ts +21 -0
  202. package/dist/material/consumer.js +52 -0
  203. package/dist/material/emit.d.ts +55 -0
  204. package/dist/material/emit.js +52 -0
  205. package/dist/material/factory.d.ts +27 -0
  206. package/dist/material/factory.js +25 -0
  207. package/dist/material/index.d.ts +8 -0
  208. package/dist/material/index.js +8 -0
  209. package/dist/material/materialStore.d.ts +24 -0
  210. package/dist/material/materialStore.js +76 -0
  211. package/dist/material/sampling.d.ts +21 -0
  212. package/dist/material/sampling.js +39 -0
  213. package/dist/material/sources.d.ts +25 -0
  214. package/dist/material/sources.js +33 -0
  215. package/dist/material/watermark.d.ts +21 -0
  216. package/dist/material/watermark.js +76 -0
  217. package/dist/observers/cursorRewriteObserver.d.ts +46 -0
  218. package/dist/observers/cursorRewriteObserver.js +103 -0
  219. package/dist/observers/distillObserver.d.ts +31 -0
  220. package/dist/observers/distillObserver.js +93 -0
  221. package/dist/observers/index.d.ts +5 -0
  222. package/dist/observers/index.js +5 -0
  223. package/dist/observers/observerBus.d.ts +44 -0
  224. package/dist/observers/observerBus.js +73 -0
  225. package/dist/observers/reflectionObserver.d.ts +31 -0
  226. package/dist/observers/reflectionObserver.js +124 -0
  227. package/dist/observers/valueDigestObserver.d.ts +50 -0
  228. package/dist/observers/valueDigestObserver.js +70 -0
  229. package/dist/ops/cleanup.d.ts +23 -0
  230. package/dist/ops/cleanup.js +68 -0
  231. package/dist/ops/health.d.ts +37 -0
  232. package/dist/ops/health.js +96 -0
  233. package/dist/ops/index.d.ts +7 -0
  234. package/dist/ops/index.js +7 -0
  235. package/dist/ops/recall.d.ts +65 -0
  236. package/dist/ops/recall.js +152 -0
  237. package/dist/ops/reuseOutcomes.d.ts +68 -0
  238. package/dist/ops/reuseOutcomes.js +126 -0
  239. package/dist/ops/savingsCore.d.ts +47 -0
  240. package/dist/ops/savingsCore.js +112 -0
  241. package/dist/ops/selfUpdate.d.ts +106 -0
  242. package/dist/ops/selfUpdate.js +294 -0
  243. package/dist/ops/valueLedger.d.ts +205 -0
  244. package/dist/ops/valueLedger.js +238 -0
  245. package/dist/ops/valueOutreach.d.ts +70 -0
  246. package/dist/ops/valueOutreach.js +244 -0
  247. package/dist/personality/drift.d.ts +45 -0
  248. package/dist/personality/drift.js +106 -0
  249. package/dist/personality/events.d.ts +25 -0
  250. package/dist/personality/events.js +37 -0
  251. package/dist/personality/evolveOps.d.ts +20 -0
  252. package/dist/personality/evolveOps.js +93 -0
  253. package/dist/personality/index.d.ts +11 -0
  254. package/dist/personality/index.js +11 -0
  255. package/dist/personality/mutate.d.ts +21 -0
  256. package/dist/personality/mutate.js +39 -0
  257. package/dist/personality/pivot.d.ts +23 -0
  258. package/dist/personality/pivot.js +22 -0
  259. package/dist/personality/prompt.d.ts +6 -0
  260. package/dist/personality/prompt.js +62 -0
  261. package/dist/personality/riskGate.d.ts +42 -0
  262. package/dist/personality/riskGate.js +80 -0
  263. package/dist/personality/schema.d.ts +255 -0
  264. package/dist/personality/schema.js +119 -0
  265. package/dist/personality/select.d.ts +46 -0
  266. package/dist/personality/select.js +71 -0
  267. package/dist/personality/stats.d.ts +46 -0
  268. package/dist/personality/stats.js +82 -0
  269. package/dist/personality/store.d.ts +35 -0
  270. package/dist/personality/store.js +92 -0
  271. package/dist/schema/common.d.ts +28 -0
  272. package/dist/schema/common.js +17 -0
  273. package/dist/schema/index.d.ts +5 -0
  274. package/dist/schema/index.js +5 -0
  275. package/dist/schema/material.d.ts +201 -0
  276. package/dist/schema/material.js +50 -0
  277. package/dist/schema/problem.d.ts +160 -0
  278. package/dist/schema/problem.js +45 -0
  279. package/dist/schema/proofOfWork.d.ts +97 -0
  280. package/dist/schema/proofOfWork.js +9 -0
  281. package/dist/schema/signal.d.ts +41 -0
  282. package/dist/schema/signal.js +19 -0
  283. package/dist/shadow/index.d.ts +6 -0
  284. package/dist/shadow/index.js +6 -0
  285. package/dist/shadow/jsonlSink.d.ts +17 -0
  286. package/dist/shadow/jsonlSink.js +27 -0
  287. package/dist/shadow/redact.d.ts +11 -0
  288. package/dist/shadow/redact.js +18 -0
  289. package/dist/shadow/shadowHub.d.ts +12 -0
  290. package/dist/shadow/shadowHub.js +42 -0
  291. package/dist/shadow/shadowMailbox.d.ts +17 -0
  292. package/dist/shadow/shadowMailbox.js +25 -0
  293. package/dist/shadow/shadowStore.d.ts +18 -0
  294. package/dist/shadow/shadowStore.js +55 -0
  295. package/dist/shadow/sink.d.ts +37 -0
  296. package/dist/shadow/sink.js +18 -0
  297. package/dist/signals/cycleHistoryFromEvents.d.ts +11 -0
  298. package/dist/signals/cycleHistoryFromEvents.js +96 -0
  299. package/dist/signals/expand.d.ts +34 -0
  300. package/dist/signals/expand.js +125 -0
  301. package/dist/signals/extractor.d.ts +25 -0
  302. package/dist/signals/extractor.js +57 -0
  303. package/dist/signals/index.d.ts +6 -0
  304. package/dist/signals/index.js +6 -0
  305. package/dist/signals/metaSignals.d.ts +57 -0
  306. package/dist/signals/metaSignals.js +158 -0
  307. package/dist/signals/signalGate.d.ts +15 -0
  308. package/dist/signals/signalGate.js +40 -0
  309. package/dist/signals/traceSignals.d.ts +44 -0
  310. package/dist/signals/traceSignals.js +123 -0
  311. package/dist/signatures/classifier.d.ts +16 -0
  312. package/dist/signatures/classifier.js +29 -0
  313. package/dist/signatures/index.d.ts +2 -0
  314. package/dist/signatures/index.js +2 -0
  315. package/dist/signatures/signatures.d.ts +22 -0
  316. package/dist/signatures/signatures.js +42 -0
  317. package/dist/strategy/experiment.d.ts +48 -0
  318. package/dist/strategy/experiment.js +63 -0
  319. package/dist/strategy/index.d.ts +2 -0
  320. package/dist/strategy/index.js +2 -0
  321. package/dist/strategy/strategyPoint.d.ts +30 -0
  322. package/dist/strategy/strategyPoint.js +23 -0
  323. package/dist/trace/index.d.ts +2 -0
  324. package/dist/trace/index.js +2 -0
  325. package/dist/trace/trajectory.d.ts +82 -0
  326. package/dist/trace/trajectory.js +117 -0
  327. package/dist/trace/trajectoryExport.d.ts +237 -0
  328. package/dist/trace/trajectoryExport.js +1818 -0
  329. package/dist/trigger/budget.d.ts +16 -0
  330. package/dist/trigger/budget.js +20 -0
  331. package/dist/trigger/engine.d.ts +12 -0
  332. package/dist/trigger/engine.js +23 -0
  333. package/dist/trigger/index.d.ts +6 -0
  334. package/dist/trigger/index.js +6 -0
  335. package/dist/trigger/problemBuilder.d.ts +24 -0
  336. package/dist/trigger/problemBuilder.js +32 -0
  337. package/dist/trigger/trigger.d.ts +22 -0
  338. package/dist/trigger/trigger.js +27 -0
  339. package/dist/trigger/valueModel.d.ts +26 -0
  340. package/dist/trigger/valueModel.js +25 -0
  341. package/dist/trigger/wfq.d.ts +13 -0
  342. package/dist/trigger/wfq.js +25 -0
  343. package/dist/util/fileLock.d.ts +24 -0
  344. package/dist/util/fileLock.js +227 -0
  345. package/dist/util/index.d.ts +2 -0
  346. package/dist/util/index.js +2 -0
  347. package/dist/util/version.d.ts +12 -0
  348. package/dist/util/version.js +51 -0
  349. package/dist/verify/index.d.ts +4 -0
  350. package/dist/verify/index.js +4 -0
  351. package/dist/verify/rollback.d.ts +22 -0
  352. package/dist/verify/rollback.js +33 -0
  353. package/dist/verify/sandboxRunner.d.ts +39 -0
  354. package/dist/verify/sandboxRunner.js +116 -0
  355. package/dist/verify/sandboxedValidation.d.ts +35 -0
  356. package/dist/verify/sandboxedValidation.js +56 -0
  357. package/dist/verify/validation.d.ts +41 -0
  358. package/dist/verify/validation.js +85 -0
  359. package/dist/wire/canonicalize.golden.d.ts +23 -0
  360. package/dist/wire/canonicalize.golden.js +46 -0
  361. package/dist/wire/geneHints.d.ts +64 -0
  362. package/dist/wire/geneHints.js +100 -0
  363. package/dist/wire/index.d.ts +91 -0
  364. package/dist/wire/index.js +3 -0
  365. package/dist/wire/schemaGate.d.ts +11 -0
  366. package/dist/wire/schemaGate.js +49 -0
  367. package/dist/workflow/dsl.d.ts +54 -0
  368. package/dist/workflow/dsl.js +15 -0
  369. package/dist/workflow/engine.d.ts +30 -0
  370. package/dist/workflow/engine.js +94 -0
  371. package/dist/workflow/index.d.ts +2 -0
  372. package/dist/workflow/index.js +2 -0
  373. package/package.json +29 -0
@@ -0,0 +1,33 @@
1
+ import { resolve, relative, isAbsolute, sep } from 'node:path';
2
+ function isUnder(root, p) {
3
+ const r = resolve(root);
4
+ const ap = resolve(p);
5
+ const rel = relative(r, ap);
6
+ return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel));
7
+ }
8
+ /**
9
+ * 规划回滚(M4A-4, 批注#20). 三条铁律, 全部满足才回滚:
10
+ * - opt-in: optIn=false 时一律不回滚.
11
+ * - 只动 evolver-owned: 路径必须在 ownedRoots 之内(绝不 reset 主仓 working tree / 仓外文件).
12
+ * - 不删 untracked: 未跟踪文件不回滚(避免破坏用户未提交工作).
13
+ */
14
+ export function planRollback(targets, cfg) {
15
+ const willRollback = [];
16
+ const skipped = [];
17
+ for (const t of targets) {
18
+ if (!cfg.optIn) {
19
+ skipped.push({ target: t, reason: 'rollback 未 opt-in' });
20
+ continue;
21
+ }
22
+ if (!cfg.ownedRoots.some((root) => isUnder(root, t.path))) {
23
+ skipped.push({ target: t, reason: '非 evolver-owned 路径' });
24
+ continue;
25
+ }
26
+ if (!t.tracked) {
27
+ skipped.push({ target: t, reason: 'untracked 文件, 不回滚' });
28
+ continue;
29
+ }
30
+ willRollback.push(t);
31
+ }
32
+ return { willRollback, skipped };
33
+ }
@@ -0,0 +1,39 @@
1
+ import { type CommandRunner } from './validation.js';
2
+ export interface SandboxOptions {
3
+ /** Per-command timeout (ms); clamped to 120s. Default 60s. */
4
+ timeoutMs?: number;
5
+ /** Working directory; default = a fresh temp dir, wiped after the command. */
6
+ cwd?: string;
7
+ /** Env keys that reach the child (default = a minimal safe set). */
8
+ envAllowKeys?: readonly string[];
9
+ /**
10
+ * Run the command in a fresh network namespace (no network) — a validation command shouldn't need the net,
11
+ * and cutting it blocks exfiltration (#26). Uses `unshare -r -n` (unprivileged user+net namespace). FAIL-SAFE:
12
+ * if unshare/userns is unavailable, the command is DENIED rather than run with network. Default off.
13
+ */
14
+ noNetwork?: boolean;
15
+ /**
16
+ * Hide the user's credential directories from the command via a mount namespace (#26 FS half): a validation
17
+ * command runs in a fresh temp cwd and has no business reading ~/.evomap (node_secret), ~/.ssh, ~/.aws, etc.
18
+ * — an empty tmpfs is mounted over each (inside the ns only; the host is untouched). FAIL-SAFE: denied if
19
+ * unprivileged mount namespaces are unavailable. Binaries live outside these dirs, so the command still runs.
20
+ */
21
+ hideHomeSecrets?: boolean;
22
+ /** Injected unshare-availability probe (test seam). Default: a real `unshare -r -m -n true` check (cached). */
23
+ unshareCheck?: () => boolean;
24
+ }
25
+ /** Wrap an (executable,args) in unprivileged namespaces per the requested isolation (pure — testable). */
26
+ export declare function isolationCommand(bin: string, args: readonly string[], opts: {
27
+ noNetwork?: boolean;
28
+ hideHomeSecrets?: boolean;
29
+ }): {
30
+ cmd: string;
31
+ args: string[];
32
+ };
33
+ /** Whether unprivileged user+mount+net namespaces (`unshare -r -m -n`) work here (cached) — covers both isolation modes. */
34
+ export declare function unshareNetAvailable(): boolean;
35
+ /**
36
+ * Build a hardened CommandRunner. The executable allowlist is the ValidationPlan's job (runValidation);
37
+ * this only hardens how an allowed command runs.
38
+ */
39
+ export declare function makeSandboxRunner(opts?: SandboxOptions): CommandRunner;
@@ -0,0 +1,116 @@
1
+ // Hardened sandbox CommandRunner (ported from v1 src/gep/validator/sandboxExecutor.js). Fills the
2
+ // CommandRunner seam in validation.ts so the validator role can actually EXECUTE validation commands
3
+ // (e.g. `node test.js`) to verify an evolution — safely, without trusting the caller. The executable
4
+ // allowlist stays data-driven in the ValidationPlan (runValidation checks it before calling this); this
5
+ // module hardens EXECUTION: blocked node eval-flags, shell-metachar rejection, a fresh wiped temp cwd,
6
+ // a scrubbed env (no secrets leak in), and a SIGKILL timeout. Output is folded + truncated.
7
+ import { spawn, spawnSync } from 'node:child_process';
8
+ import { mkdtempSync, rmSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { isNodeExecutable, nodeFlagViolation, SHELL_METACHARS } from './validation.js';
12
+ const DEFAULT_TIMEOUT_MS = 60_000;
13
+ const MAX_TIMEOUT_MS = 120_000;
14
+ const MAX_OUTPUT_CHARS = 4000;
15
+ // Env scrub: only these keys reach the child, so node-secrets/tokens in the parent env cannot leak into validation.
16
+ const DEFAULT_ENV_ALLOW = ['PATH', 'HOME', 'LANG', 'LC_ALL', 'TMPDIR', 'SYSTEMROOT'];
17
+ /** Credential dirs hidden under $HOME when hideHomeSecrets is set (dirs only — never where binaries live). */
18
+ const SECRET_HOME_DIRS = ['.evomap', '.ssh', '.aws', '.gnupg', '.docker', '.kube'];
19
+ // Fixed setup run inside the mount ns: tmpfs over each existing secret dir, then exec the command. The command
20
+ // 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 "$@"`;
22
+ /** Wrap an (executable,args) in unprivileged namespaces per the requested isolation (pure — testable). */
23
+ export function isolationCommand(bin, args, opts) {
24
+ const flags = ['-r'];
25
+ if (opts.hideHomeSecrets)
26
+ flags.push('-m');
27
+ if (opts.noNetwork)
28
+ flags.push('-n');
29
+ if (!opts.noNetwork && !opts.hideHomeSecrets)
30
+ return { cmd: bin, args: [...args] };
31
+ const base = [...flags, '--fork', '--kill-child', '--'];
32
+ // 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] };
36
+ }
37
+ let unshareCache;
38
+ /** Whether unprivileged user+mount+net namespaces (`unshare -r -m -n`) work here (cached) — covers both isolation modes. */
39
+ export function unshareNetAvailable() {
40
+ if (unshareCache === undefined) {
41
+ try {
42
+ unshareCache = spawnSync('unshare', ['-r', '-m', '-n', 'true'], { timeout: 5000 }).status === 0;
43
+ }
44
+ catch {
45
+ unshareCache = false;
46
+ }
47
+ }
48
+ return unshareCache;
49
+ }
50
+ function parse(cmd) {
51
+ const tokens = cmd.trim().split(/\s+/).filter(Boolean);
52
+ return { executable: tokens[0] ?? '', args: tokens.slice(1) };
53
+ }
54
+ function scrubEnv(allow) {
55
+ const out = {};
56
+ for (const k of allow) {
57
+ const v = process.env[k];
58
+ if (v !== undefined)
59
+ out[k] = v;
60
+ }
61
+ return out;
62
+ }
63
+ /**
64
+ * Build a hardened CommandRunner. The executable allowlist is the ValidationPlan's job (runValidation);
65
+ * this only hardens how an allowed command runs.
66
+ */
67
+ export function makeSandboxRunner(opts = {}) {
68
+ const timeout = Math.min(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
69
+ 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);
73
+ if (!executable)
74
+ return deny('empty command', 1);
75
+ if (SHELL_METACHARS.test(cmd))
76
+ return deny('rejected: shell metacharacter (no shell is provided; split compound commands)');
77
+ const badFlag = nodeFlagViolation(executable, args);
78
+ if (badFlag)
79
+ return deny(`rejected: blocked node flag ${badFlag}`);
80
+ // 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');
83
+ }
84
+ const ownTemp = !opts.cwd;
85
+ let cwd;
86
+ try {
87
+ cwd = opts.cwd ?? mkdtempSync(join(tmpdir(), 'evo-sbx-'));
88
+ }
89
+ catch (e) {
90
+ return deny(`temp dir failed: ${e instanceof Error ? e.message : 'err'}`, 1);
91
+ }
92
+ const cleanup = () => { if (ownTemp) {
93
+ try {
94
+ rmSync(cwd, { recursive: true, force: true });
95
+ }
96
+ catch { /* best effort */ }
97
+ } };
98
+ // Resolve 'node' to the running binary so spawn(shell:false) works on Windows too (where bare 'node' would ENOENT).
99
+ const bin = isNodeExecutable(executable) ? process.execPath : executable;
100
+ 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'] });
103
+ let out = '';
104
+ const cap = (d) => { if (out.length < MAX_OUTPUT_CHARS)
105
+ out += String(d); };
106
+ child.stdout?.on('data', cap);
107
+ 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) }); });
110
+ }
111
+ catch (e) {
112
+ cleanup();
113
+ resolve({ exitCode: 127, stdout: `[sandbox] spawn failed: ${e instanceof Error ? e.message : 'err'}` });
114
+ }
115
+ });
116
+ }
@@ -0,0 +1,35 @@
1
+ import { type ValidationResult } from './validation.js';
2
+ export interface SandboxedValidationSkippedCommand {
3
+ cmd: string;
4
+ script: string;
5
+ reason: 'missing_script';
6
+ }
7
+ export interface SandboxedValidationResult {
8
+ passed: boolean;
9
+ /** Convenience score for cycle outcomes: 0.95 pass / 0.2 fail (matches the prior inline hook). */
10
+ score: number;
11
+ results: ValidationResult[];
12
+ /** Declared repo-relative validation scripts that were absent from this checkout. */
13
+ skipped: SandboxedValidationSkippedCommand[];
14
+ /**
15
+ * Whether OS-namespace isolation (no-network + hidden home secrets) was applied. TRUE only where unprivileged
16
+ * namespaces exist (Linux today). FALSE on Windows/macOS, where commands still get the non-namespace hardening
17
+ * but CAN reach the network and read home files — the caller should surface this so the weaker guarantee on
18
+ * those platforms is not silent.
19
+ */
20
+ isolated: boolean;
21
+ }
22
+ export interface SandboxedValidationOptions {
23
+ /** Per-command timeout (ms), forwarded to the sandbox runner. */
24
+ timeoutMs?: number;
25
+ /** Test seam: override the unprivileged-namespace availability probe. */
26
+ unshareCheck?: () => boolean;
27
+ }
28
+ /**
29
+ * Run validation commands in the hardened sandbox. Isolation (no-network + hidden home secrets) is requested only
30
+ * when unprivileged namespaces are available; elsewhere (Windows/macOS) it degrades to the non-namespace hardening
31
+ * rather than denying every command. The allowlist is derived from the declared commands' own executables, so this
32
+ * runs exactly the commands the caller asked for — just hardened. An EMPTY command set passes (nothing to verify),
33
+ * preserving prior behavior; note this differs from runValidation's own "no commands = not verified" stance.
34
+ */
35
+ export declare function runSandboxedValidation(cmds: readonly string[], cwd: string, opts?: SandboxedValidationOptions): Promise<SandboxedValidationResult>;
@@ -0,0 +1,56 @@
1
+ // The trusted external verifier (wires the hardened sandbox runner to the validation plan). A cycle's validate
2
+ // step should run its validation commands through HERE rather than a bare spawn: each command runs with shell-
3
+ // metachar rejection, node-eval-flag blocking, a scrubbed env, a SIGKILL timeout, and — where unprivileged
4
+ // namespaces exist — no network + hidden home secrets, so a validation command cannot phone home / exfiltrate to
5
+ // game the pass/fail. This is the "verifier outside the repo, untrusted caller" half the safety model relies on.
6
+ import { existsSync } from 'node:fs';
7
+ import { isAbsolute, resolve } from 'node:path';
8
+ import { makeSandboxRunner, unshareNetAvailable } from './sandboxRunner.js';
9
+ import { runValidation, validationScriptPath } from './validation.js';
10
+ /**
11
+ * Run validation commands in the hardened sandbox. Isolation (no-network + hidden home secrets) is requested only
12
+ * when unprivileged namespaces are available; elsewhere (Windows/macOS) it degrades to the non-namespace hardening
13
+ * rather than denying every command. The allowlist is derived from the declared commands' own executables, so this
14
+ * runs exactly the commands the caller asked for — just hardened. An EMPTY command set passes (nothing to verify),
15
+ * preserving prior behavior; note this differs from runValidation's own "no commands = not verified" stance.
16
+ */
17
+ export async function runSandboxedValidation(cmds, cwd, opts = {}) {
18
+ const isolated = (opts.unshareCheck ?? unshareNetAvailable)();
19
+ // ONLY a truly empty command set is a vacuous pass (nothing to verify). A non-empty set with blank entries is a
20
+ // malformed plan, NOT a pass: blanks are kept (trimmed to '') so they fall outside the allowlist and fail, rather
21
+ // than being silently dropped into a pass (Bugbot).
22
+ if (cmds.length === 0)
23
+ return { passed: true, score: 0.95, results: [], skipped: [], isolated };
24
+ const runner = makeSandboxRunner({
25
+ cwd,
26
+ noNetwork: isolated,
27
+ hideHomeSecrets: isolated,
28
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
29
+ ...(opts.unshareCheck ? { unshareCheck: opts.unshareCheck } : {}),
30
+ });
31
+ const list = cmds.map((c) => String(c ?? '').trim());
32
+ // Validation plans may reference repo-relative scripts that do not exist in this checkout; skip only those.
33
+ const skipped = [];
34
+ const runnable = [];
35
+ for (const cmd of list) {
36
+ const script = validationScriptPath(cmd);
37
+ if (script && !isAbsolute(script) && !existsSync(resolve(cwd, script))) {
38
+ skipped.push({ cmd, script, reason: 'missing_script' });
39
+ console.warn(`[Validation] Skipping validation command (script not in repoRoot): ${script}`);
40
+ continue;
41
+ }
42
+ runnable.push(cmd);
43
+ }
44
+ if (runnable.length === 0)
45
+ return { passed: false, score: 0.2, results: [], skipped, isolated };
46
+ const allowlist = [...new Set(runnable.map((c) => c.split(/\s+/)[0] ?? '').filter(Boolean))];
47
+ const { results, passed } = await runValidation({ commands: runnable.map((cmd) => ({ cmd })), allowlist }, runner);
48
+ const complete = skipped.length === 0;
49
+ return {
50
+ passed: passed && complete,
51
+ score: passed && complete ? 0.95 : 0.2,
52
+ results,
53
+ skipped,
54
+ isolated,
55
+ };
56
+ }
@@ -0,0 +1,41 @@
1
+ export interface ValidationCommand {
2
+ cmd: string;
3
+ label?: string;
4
+ }
5
+ /** 校验计划. allowlist **由用户项目声明**(批注#21), 不再内置 ['node']. */
6
+ export interface ValidationPlan {
7
+ commands: readonly ValidationCommand[];
8
+ allowlist: readonly string[];
9
+ }
10
+ export interface ValidationResult {
11
+ label: string;
12
+ cmd: string;
13
+ allowed: boolean;
14
+ exitCode: number | null;
15
+ stdoutSummary: string;
16
+ passed: boolean;
17
+ }
18
+ export interface RunOutput {
19
+ exitCode: number;
20
+ stdout: string;
21
+ }
22
+ export type CommandRunner = (cmd: string) => Promise<RunOutput> | RunOutput;
23
+ export declare const SHELL_METACHARS: RegExp;
24
+ export declare const BLOCKED_NODE_FLAGS: Set<string>;
25
+ export declare function normalizeExecutableName(executable: string): string;
26
+ export declare function isNodeExecutable(executable: string): boolean;
27
+ export declare function nodeFlagViolation(executable: string, args: readonly string[]): string | null;
28
+ /** 命令首 token(可执行名)是否在项目声明的 allowlist 内. */
29
+ 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. */
31
+ export declare function validationScriptPath(cmd: string): string | null;
32
+ /** stdout 摘要 = 首行 + 末行(去 canary.js 隐形约定: 不找魔法字符串, 只留可读摘要). */
33
+ export declare function summarizeStdout(stdout: string): string;
34
+ /**
35
+ * 跑校验(M4A-4). 判价值 = **exit code + stdout 摘要**, 不依赖 canary.js 魔法字符串(批注#22).
36
+ * 不在 allowlist 的命令 auto-deny(不执行, allowed=false), 守无人值守安全.
37
+ */
38
+ export declare function runValidation(plan: ValidationPlan, run: CommandRunner): Promise<{
39
+ results: ValidationResult[];
40
+ passed: boolean;
41
+ }>;
@@ -0,0 +1,85 @@
1
+ // With shell:false these are passed literally (harmless), but their presence means an injection attempt
2
+ // or a command that wrongly assumes a shell (e.g. `a && b`) — reject loudly so it is split, not silently mis-run.
3
+ export const SHELL_METACHARS = /[;&|`$<>\n\r]/;
4
+ // Node flags that turn `node` into an arbitrary-code evaluator or preload hook. A legit validation command is
5
+ // `node <script> [args]`; these flags must be denied by the sandbox and must never be hidden by missing-script skip.
6
+ export const BLOCKED_NODE_FLAGS = new Set([
7
+ '-e', '--eval', '-p', '--print', '-i', '--interactive', '-r', '--require',
8
+ '--loader', '--experimental-loader', '--import', '--env-file',
9
+ // Inspector: opens an UNAUTHENTICATED Node debug port (a remote-code-execution channel) for the command's
10
+ // lifetime — never needed by a light `node <script>` validation. (`=value` forms are covered by the
11
+ // split('=') in nodeFlagViolation below.) Hardens both the distill light-validation filter and the sandbox.
12
+ '--inspect', '--inspect-brk', '--inspect-port', '--inspect-publish-uid',
13
+ // Watch: keeps the process alive past its work → hangs validation until the sandbox SIGKILLs it (DoS).
14
+ '--watch', '--watch-path', '--watch-preserve-output',
15
+ // Module-resolution override: can redirect imports/requires to attacker-chosen code paths.
16
+ '--conditions', '-C',
17
+ ]);
18
+ export function normalizeExecutableName(executable) {
19
+ const trimmed = String(executable ?? '').trim();
20
+ const unquoted = trimmed.replace(/^["']|["']$/g, '');
21
+ const base = unquoted.split(/[\\/]/).pop() ?? unquoted;
22
+ return base.toLowerCase().replace(/\.exe$/, '');
23
+ }
24
+ export function isNodeExecutable(executable) {
25
+ return normalizeExecutableName(executable) === 'node';
26
+ }
27
+ export function nodeFlagViolation(executable, args) {
28
+ if (!isNodeExecutable(executable))
29
+ return null;
30
+ for (const a of args) {
31
+ const flag = a.split('=')[0] ?? a;
32
+ if (BLOCKED_NODE_FLAGS.has(flag))
33
+ return flag;
34
+ }
35
+ return null;
36
+ }
37
+ /** 命令首 token(可执行名)是否在项目声明的 allowlist 内. */
38
+ export function isAllowed(cmd, allowlist) {
39
+ const head = cmd.trim().split(/\s+/)[0] ?? '';
40
+ return allowlist.includes(head);
41
+ }
42
+ /** Extract the local script targeted by `node <script> ...`, or null for non-script node commands. */
43
+ export function validationScriptPath(cmd) {
44
+ const tokens = String(cmd ?? '').trim().split(/\s+/);
45
+ const executable = tokens[0] ?? '';
46
+ if (tokens.length < 2 || !isNodeExecutable(executable))
47
+ return null;
48
+ if (SHELL_METACHARS.test(cmd) || nodeFlagViolation(executable, tokens.slice(1)))
49
+ return null;
50
+ for (let i = 1; i < tokens.length; i += 1) {
51
+ const token = tokens[i];
52
+ if (token.startsWith('-'))
53
+ continue;
54
+ return /\.(?:c|m)?js$/.test(token) ? token : null;
55
+ }
56
+ return null;
57
+ }
58
+ /** stdout 摘要 = 首行 + 末行(去 canary.js 隐形约定: 不找魔法字符串, 只留可读摘要). */
59
+ export function summarizeStdout(stdout) {
60
+ const lines = stdout.split('\n').map((l) => l.trim()).filter(Boolean);
61
+ if (lines.length === 0)
62
+ return '';
63
+ if (lines.length <= 2)
64
+ return lines.join(' | ');
65
+ return `${lines[0]} … ${lines[lines.length - 1]}`;
66
+ }
67
+ /**
68
+ * 跑校验(M4A-4). 判价值 = **exit code + stdout 摘要**, 不依赖 canary.js 魔法字符串(批注#22).
69
+ * 不在 allowlist 的命令 auto-deny(不执行, allowed=false), 守无人值守安全.
70
+ */
71
+ export async function runValidation(plan, run) {
72
+ const results = [];
73
+ for (const c of plan.commands) {
74
+ const label = c.label ?? c.cmd;
75
+ if (!isAllowed(c.cmd, plan.allowlist)) {
76
+ results.push({ label, cmd: c.cmd, allowed: false, exitCode: null, stdoutSummary: '', passed: false });
77
+ continue;
78
+ }
79
+ const out = await run(c.cmd);
80
+ results.push({ label, cmd: c.cmd, allowed: true, exitCode: out.exitCode, stdoutSummary: summarizeStdout(out.stdout), passed: out.exitCode === 0 });
81
+ }
82
+ // 全部 allowed 的命令都 exit 0 才算通过; 有命令被 deny 视为未通过(校验不完整)
83
+ const passed = results.length > 0 && results.every((r) => r.allowed && r.passed);
84
+ return { results, passed };
85
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * canonicalize 黄金向量 (M3-0). asset_id = SHA256(canonicalize(asset 去 asset_id)) 是跨 Go/JS/Rust
3
+ * 逐字节一致的命门; 任何实现必须复现下列 canonical 字节, 否则互相 reject 资产 (硬化 A5).
4
+ *
5
+ * 三陷阱(必须照 JS 语义实现, 非各语言原生):
6
+ * 1. number = JS Number→String 最短往返 (1.0→"1", 1e21→"1e+21", -0→"0", 非有限→"null").
7
+ * 2. string = JSON.stringify, **不转义 HTML** (`<`/`>`/`&`/`/` 原样). Go 必须 json Encoder.SetEscapeHTML(false).
8
+ * 3. object 键排序 = **UTF-16 码元序** (JS Array.sort 默认), 非 UTF-8 字节/码点序.
9
+ * 非 BMP 字符(emoji)按 surrogate 首单元 0xD800–0xDBFF 排, 排在 U+E000–U+FFFF 之前 — 见 key-order-divergent.
10
+ */
11
+ export interface CanonicalVector {
12
+ name: string;
13
+ input: unknown;
14
+ canonical: string;
15
+ }
16
+ export declare const CANONICAL_VECTORS: readonly CanonicalVector[];
17
+ /** asset_id 黄金: 固定资产 → 固定 sha256. excludeFields 默认 ['asset_id']. */
18
+ export interface AssetIdVector {
19
+ name: string;
20
+ asset: Record<string, unknown>;
21
+ assetId: string;
22
+ }
23
+ export declare const ASSET_ID_VECTORS: readonly AssetIdVector[];
@@ -0,0 +1,46 @@
1
+ /**
2
+ * canonicalize 黄金向量 (M3-0). asset_id = SHA256(canonicalize(asset 去 asset_id)) 是跨 Go/JS/Rust
3
+ * 逐字节一致的命门; 任何实现必须复现下列 canonical 字节, 否则互相 reject 资产 (硬化 A5).
4
+ *
5
+ * 三陷阱(必须照 JS 语义实现, 非各语言原生):
6
+ * 1. number = JS Number→String 最短往返 (1.0→"1", 1e21→"1e+21", -0→"0", 非有限→"null").
7
+ * 2. string = JSON.stringify, **不转义 HTML** (`<`/`>`/`&`/`/` 原样). Go 必须 json Encoder.SetEscapeHTML(false).
8
+ * 3. object 键排序 = **UTF-16 码元序** (JS Array.sort 默认), 非 UTF-8 字节/码点序.
9
+ * 非 BMP 字符(emoji)按 surrogate 首单元 0xD800–0xDBFF 排, 排在 U+E000–U+FFFF 之前 — 见 key-order-divergent.
10
+ */
11
+ export const CANONICAL_VECTORS = [
12
+ { name: 'null', input: null, canonical: 'null' },
13
+ { name: 'bool-true', input: true, canonical: 'true' },
14
+ { name: 'int', input: 42, canonical: '42' },
15
+ { name: 'float', input: 0.1, canonical: '0.1' },
16
+ { name: 'float-trailing-zero', input: 1.0, canonical: '1' }, // 陷阱1: 无 ".0"
17
+ { name: 'big-exp', input: 1e21, canonical: '1e+21' }, // 陷阱1: 指数形式
18
+ { name: 'neg-zero', input: -0, canonical: '0' }, // 陷阱1: -0→"0"
19
+ { name: 'str-basic', input: 'hello', canonical: '"hello"' },
20
+ { name: 'str-html', input: 'a<b>&"/', canonical: '"a<b>&\\"/"' }, // 陷阱2: HTML 不转义, 仅 \" 转义
21
+ { name: 'str-unicode', input: '认证 \u{1F9EC}', canonical: '"认证 \u{1F9EC}"' },
22
+ { name: 'empty-array', input: [], canonical: '[]' },
23
+ { name: 'empty-object', input: {}, canonical: '{}' },
24
+ { name: 'nested', input: { b: 2, a: [1, { z: 'x', a: null }] }, canonical: '{"a":[1,{"a":null,"z":"x"}],"b":2}' },
25
+ // 陷阱3: 键 UTF-16 序 — 'a'(0x61) < emoji(surrogate 0xD83E) < U+F8FF(0xF8FF). 码点序会把 emoji 排最后.
26
+ { name: 'key-order-divergent', input: { a: 3, '\u{1F9EC}': 2, '': 1 }, canonical: '{"a":3,"\u{1F9EC}":2,"":1}' },
27
+ ];
28
+ export const ASSET_ID_VECTORS = [
29
+ {
30
+ name: 'gene',
31
+ asset: {
32
+ type: 'Gene', schema_version: '1.11.0', id: 'gene-1', category: 'repair',
33
+ signals_match: ['auth', '401'], strategy: ['s1'], constraints: { max_files: 3, forbidden_paths: [] },
34
+ validation: ['v'], asset_id: 'IGNORED',
35
+ },
36
+ assetId: 'sha256:402ea1911984bd7b24ffa3faf18c5679cdb5f0c5215700cbdd1519705f9b9453',
37
+ },
38
+ {
39
+ name: 'capsule',
40
+ asset: {
41
+ type: 'Capsule', schema_version: '1.11.0', id: 'cap-1', trigger: ['t'], gene: 'gene-1', summary: 's',
42
+ confidence: 0.9, blast_radius: { files: 1, lines: 10 }, outcome: { status: 'success', score: 0.8 }, asset_id: 'IGNORED',
43
+ },
44
+ assetId: 'sha256:fa352d440ace8505f4e06d6cf9d215c9b03b9799cd34f0afb6eec18a5878ee60',
45
+ },
46
+ ];
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Gene routing / tool hints — OPTIONAL EvoX-side annotations a Gene MAY carry to bias the model-tier
3
+ * router (routing_hint) or restrict tools (tool_policy). Ported from v1 src/gep/schemas/gene.js (PR #93,
4
+ * "wire routing_hint and tool_policy into Gene schema").
5
+ *
6
+ * v2-delta (feature-gate): these fields are NOT yet in @evomap/gep-sdk's gene.schema.json
7
+ * (additionalProperties:false), so a Gene carrying them is not a schema-valid gep-sdk asset until a
8
+ * gep-sdk gene-schema bump — exactly how Capsule's proof_of_work / resolution_status were v2-delta before
9
+ * the 1.11.0 bump made them first-class. That invalidity is surfaced ONLY by the advisory validateWire
10
+ * (wire/schemaGate) — at intake on the hint-stripped core, and via the evolver_gep_build pre-publish
11
+ * preview — plus the hub's own schema check on receipt; there is NO enforced local egress gate (the
12
+ * publish/egress path is sanitize-only and does not call validateWire). Until a bump the hints ride along
13
+ * LOCALLY (intake pool + v1→v2 import fidelity); nothing in this repo wires them to a runtime consumer yet
14
+ * (the proxy model router's `gene_hint` seam is the eventual tier consumer — see
15
+ * packages/evolver-proxy/src/router/modelRouter.ts).
16
+ *
17
+ * Enum strings are matched case-sensitively by the consuming router / tool-gate — keep them EXACT. The
18
+ * router tier set (proxy/modelRouter.ts `Tier`) is the consumer-side mirror of ROUTING_TIERS; the two MUST
19
+ * agree. Normalization is deliberately lossy: unknown enum members are dropped (not coerced to a default),
20
+ * mirroring the consumer's exhaustive match where a stray value is simply ignored rather than mis-routed.
21
+ */
22
+ export declare const ROUTING_TIERS: readonly ["cheap", "mid", "expensive"];
23
+ export type RoutingTier = (typeof ROUTING_TIERS)[number];
24
+ export declare const REASONING_LEVELS: readonly ["off", "low", "medium", "high"];
25
+ export type ReasoningLevel = (typeof REASONING_LEVELS)[number];
26
+ export declare const TOOL_POLICY_SEVERITIES: readonly ["warn", "block"];
27
+ export type ToolPolicySeverity = (typeof TOOL_POLICY_SEVERITIES)[number];
28
+ export interface GeneRoutingHint {
29
+ tier?: RoutingTier;
30
+ reasoning_level?: ReasoningLevel;
31
+ }
32
+ export interface GeneToolPolicy {
33
+ allow_only?: string[];
34
+ deny?: string[];
35
+ severity?: ToolPolicySeverity;
36
+ }
37
+ /**
38
+ * The v2-delta hint field names, in one place — the intake gate strips these before the gep-sdk schema check.
39
+ * IMPORTANT: when a gep-sdk gene-schema bump makes one of these first-class, REMOVE it from this list in the
40
+ * same change. Otherwise intakeGene keeps stripping a now-validated field before validateWire (fail-open — the
41
+ * field's new schema constraints go unchecked at intake) while asset_id still commits it.
42
+ */
43
+ export declare const GENE_HINT_FIELDS: readonly ["routing_hint", "tool_policy"];
44
+ /**
45
+ * Normalize an arbitrary routing_hint fragment to a strict { tier?, reasoning_level? } object, or null.
46
+ * Unknown enum values are dropped (a stray tier would fail the consumer's exhaustive match and route as if
47
+ * no hint existed, so we mirror that here rather than emit JSON that misleads). Empty result → null
48
+ * ("no opinion — let the router take its default path").
49
+ */
50
+ export declare function normalizeRoutingHint(raw: unknown): GeneRoutingHint | null;
51
+ /**
52
+ * Normalize an arbitrary tool_policy fragment. allow_only / deny are TOOL-NAME lists: only non-empty STRING
53
+ * entries are kept — non-string entries (objects, numbers, null, booleans) are DROPPED, not String()-coerced.
54
+ * This is a deliberate tightening over v1 (which did `.map(String)` and leaked garbage like "[object Object]"
55
+ * into a tool-name list); it mirrors the case-sensitive typeguards used for tier/severity here, and never
56
+ * drops a LEGITIMATE restriction — a partly-garbage list keeps its valid names, and an all-garbage list
57
+ * collapses exactly like an all-empty one. A list is kept only if something survives: a raw
58
+ * { allow_only: ['', ''] } must NOT leak through as allow_only:[], which a consuming tool-gate reads as
59
+ * "allow zero tools" and would block every tool call when the gene only meant to carry a deny list. severity
60
+ * defaults to 'warn' when a list is present; the whole object collapses to null when neither list survives.
61
+ */
62
+ export declare function normalizeToolPolicy(raw: unknown): GeneToolPolicy | null;
63
+ /** Strip the v2-delta hint fields from a gene-shaped object (used to validate the gep-sdk-known core). */
64
+ export declare function stripGeneHints(gene: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Gene routing / tool hints — OPTIONAL EvoX-side annotations a Gene MAY carry to bias the model-tier
3
+ * router (routing_hint) or restrict tools (tool_policy). Ported from v1 src/gep/schemas/gene.js (PR #93,
4
+ * "wire routing_hint and tool_policy into Gene schema").
5
+ *
6
+ * v2-delta (feature-gate): these fields are NOT yet in @evomap/gep-sdk's gene.schema.json
7
+ * (additionalProperties:false), so a Gene carrying them is not a schema-valid gep-sdk asset until a
8
+ * gep-sdk gene-schema bump — exactly how Capsule's proof_of_work / resolution_status were v2-delta before
9
+ * the 1.11.0 bump made them first-class. That invalidity is surfaced ONLY by the advisory validateWire
10
+ * (wire/schemaGate) — at intake on the hint-stripped core, and via the evolver_gep_build pre-publish
11
+ * preview — plus the hub's own schema check on receipt; there is NO enforced local egress gate (the
12
+ * publish/egress path is sanitize-only and does not call validateWire). Until a bump the hints ride along
13
+ * LOCALLY (intake pool + v1→v2 import fidelity); nothing in this repo wires them to a runtime consumer yet
14
+ * (the proxy model router's `gene_hint` seam is the eventual tier consumer — see
15
+ * packages/evolver-proxy/src/router/modelRouter.ts).
16
+ *
17
+ * Enum strings are matched case-sensitively by the consuming router / tool-gate — keep them EXACT. The
18
+ * router tier set (proxy/modelRouter.ts `Tier`) is the consumer-side mirror of ROUTING_TIERS; the two MUST
19
+ * agree. Normalization is deliberately lossy: unknown enum members are dropped (not coerced to a default),
20
+ * mirroring the consumer's exhaustive match where a stray value is simply ignored rather than mis-routed.
21
+ */
22
+ export const ROUTING_TIERS = ['cheap', 'mid', 'expensive'];
23
+ export const REASONING_LEVELS = ['off', 'low', 'medium', 'high'];
24
+ export const TOOL_POLICY_SEVERITIES = ['warn', 'block'];
25
+ /**
26
+ * The v2-delta hint field names, in one place — the intake gate strips these before the gep-sdk schema check.
27
+ * IMPORTANT: when a gep-sdk gene-schema bump makes one of these first-class, REMOVE it from this list in the
28
+ * same change. Otherwise intakeGene keeps stripping a now-validated field before validateWire (fail-open — the
29
+ * field's new schema constraints go unchecked at intake) while asset_id still commits it.
30
+ */
31
+ export const GENE_HINT_FIELDS = ['routing_hint', 'tool_policy'];
32
+ function includesEnum(set, v) {
33
+ return typeof v === 'string' && set.includes(v);
34
+ }
35
+ /**
36
+ * Normalize an arbitrary routing_hint fragment to a strict { tier?, reasoning_level? } object, or null.
37
+ * Unknown enum values are dropped (a stray tier would fail the consumer's exhaustive match and route as if
38
+ * no hint existed, so we mirror that here rather than emit JSON that misleads). Empty result → null
39
+ * ("no opinion — let the router take its default path").
40
+ */
41
+ export function normalizeRoutingHint(raw) {
42
+ if (!raw || typeof raw !== 'object')
43
+ return null;
44
+ const r = raw;
45
+ const out = {};
46
+ const tier = r['tier'];
47
+ if (includesEnum(ROUTING_TIERS, tier))
48
+ out.tier = tier;
49
+ const reasoning = r['reasoning_level'];
50
+ if (includesEnum(REASONING_LEVELS, reasoning))
51
+ out.reasoning_level = reasoning;
52
+ return Object.keys(out).length > 0 ? out : null;
53
+ }
54
+ /**
55
+ * Normalize an arbitrary tool_policy fragment. allow_only / deny are TOOL-NAME lists: only non-empty STRING
56
+ * entries are kept — non-string entries (objects, numbers, null, booleans) are DROPPED, not String()-coerced.
57
+ * This is a deliberate tightening over v1 (which did `.map(String)` and leaked garbage like "[object Object]"
58
+ * into a tool-name list); it mirrors the case-sensitive typeguards used for tier/severity here, and never
59
+ * drops a LEGITIMATE restriction — a partly-garbage list keeps its valid names, and an all-garbage list
60
+ * collapses exactly like an all-empty one. A list is kept only if something survives: a raw
61
+ * { allow_only: ['', ''] } must NOT leak through as allow_only:[], which a consuming tool-gate reads as
62
+ * "allow zero tools" and would block every tool call when the gene only meant to carry a deny list. severity
63
+ * defaults to 'warn' when a list is present; the whole object collapses to null when neither list survives.
64
+ */
65
+ export function normalizeToolPolicy(raw) {
66
+ if (!raw || typeof raw !== 'object')
67
+ return null;
68
+ const r = raw;
69
+ const out = {};
70
+ const allow = cleanToolList(r['allow_only']);
71
+ if (allow)
72
+ out.allow_only = allow;
73
+ const deny = cleanToolList(r['deny']);
74
+ if (deny)
75
+ out.deny = deny;
76
+ if (out.allow_only === undefined && out.deny === undefined)
77
+ return null;
78
+ const severity = r['severity'];
79
+ out.severity = includesEnum(TOOL_POLICY_SEVERITIES, severity) ? severity : 'warn';
80
+ return out;
81
+ }
82
+ /**
83
+ * A tool-name list survives only as its non-empty string members. Non-strings are dropped (not coerced) —
84
+ * see normalizeToolPolicy. Returns undefined when nothing survives, so the caller omits the field entirely.
85
+ */
86
+ function cleanToolList(raw) {
87
+ if (!Array.isArray(raw))
88
+ return undefined;
89
+ const cleaned = raw.filter((x) => typeof x === 'string' && x.length > 0);
90
+ return cleaned.length > 0 ? cleaned : undefined;
91
+ }
92
+ /** Strip the v2-delta hint fields from a gene-shaped object (used to validate the gep-sdk-known core). */
93
+ export function stripGeneHints(gene) {
94
+ const hidden = GENE_HINT_FIELDS;
95
+ const out = {};
96
+ for (const [k, v] of Object.entries(gene))
97
+ if (!hidden.includes(k))
98
+ out[k] = v;
99
+ return out;
100
+ }