@evomap/evolver 1.87.2 → 1.87.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja-JP.md +1 -1
- package/README.ko-KR.md +1 -1
- package/README.md +9 -8
- package/README.zh-CN.md +9 -8
- package/package.json +1 -1
- package/scripts/build_binaries.js +31 -7
- package/src/atp/atpExecute.js +35 -8
- package/src/atp/autoBuyer.js +71 -16
- package/src/atp/autoDeliver.js +16 -0
- package/src/atp/cliAutobuyPrompt.js +8 -22
- package/src/atp/hubClient.js +42 -4
- package/src/evolve/guards.js +1 -1
- package/src/evolve/pipeline/collect.js +1 -1
- package/src/evolve/pipeline/dispatch.js +1 -1
- package/src/evolve/pipeline/enrich.js +1 -1
- package/src/evolve/pipeline/hub.js +1 -1
- package/src/evolve/pipeline/select.js +1 -1
- package/src/evolve/pipeline/signals.js +1 -1
- package/src/evolve/utils.js +1 -1
- package/src/evolve.js +1 -1
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/assetStore.js +52 -5
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/contentHash.js +1 -1
- package/src/gep/crypto.js +1 -1
- package/src/gep/curriculum.js +1 -1
- package/src/gep/deviceId.js +1 -1
- package/src/gep/envFingerprint.js +1 -1
- package/src/gep/epigenetics.js +1 -1
- package/src/gep/explore.js +1 -1
- package/src/gep/hash.js +1 -1
- package/src/gep/hubFetch.js +1 -1
- package/src/gep/hubReview.js +1 -1
- package/src/gep/hubSearch.js +1 -1
- package/src/gep/hubVerify.js +1 -1
- package/src/gep/learningSignals.js +1 -1
- package/src/gep/memoryGraph.js +1 -1
- package/src/gep/memoryGraphAdapter.js +1 -1
- package/src/gep/mutation.js +1 -1
- package/src/gep/narrativeMemory.js +1 -1
- package/src/gep/openPRRegistry.js +1 -1
- package/src/gep/paths.js +6 -2
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallVerifier.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/sanitize.js +57 -3
- package/src/gep/selector.js +1 -1
- package/src/gep/selfPR.js +34 -1
- package/src/gep/skill2gep.js +108 -29
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/workspaceKeychain.js +1 -1
- package/src/proxy/lifecycle/manager.js +97 -37
- package/src/proxy/router/messages_route.js +25 -0
- package/src/proxy/sync/engine.js +68 -31
- package/assets/gep/candidates.jsonl +0 -1
- package/assets/gep/capsules.json +0 -4
- package/assets/gep/events.jsonl +0 -0
- package/assets/gep/failed_capsules.json +0 -4
- package/assets/gep/genes.json +0 -245
- package/assets/gep/genes.jsonl +0 -0
package/README.ja-JP.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
>
|
|
33
33
|
> Evolver はこの結論を実装に落とし込んだオープンソースエンジンです。GEP プロトコルの下で、エージェントの経験を場当たり的なプロンプトやスキルドキュメントではなく、Gene と Capsule として符号化します。*なぜ* Evolver が長いスキルドキュメントではなく Gene にこだわるのか疑問に思ったことがあるなら、読むべきはこの論文です。
|
|
34
34
|
>
|
|
35
|
-
> 応用事例を見たい方へ:[OpenClaw x EvoMap:CritPt 評価レポート](https://evomap.ai/blog/openclaw-critpt-report) では、OpenClaw エージェントが CritPt Physics Solver 上の 5 バージョン(Beta → v2.2)にわたって、同じ Gene ベース進化ループによってスコアを
|
|
35
|
+
> 応用事例を見たい方へ:[OpenClaw x EvoMap:CritPt 評価レポート](https://evomap.ai/blog/openclaw-critpt-report) では、OpenClaw エージェントが CritPt Physics Solver 上の 5 バージョン(Beta → v2.2)にわたって、同じ Gene ベース進化ループによってスコアを 9.1% から 18.57% まで押し上げる全過程を、トークンコストの軌跡、遺伝子活性化マップ、そして推論が再利用可能な Gene に圧縮されるときに現れる「トークンが上昇してから下降する」シグネチャとともに詳述しています。
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
package/README.ko-KR.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
>
|
|
33
33
|
> Evolver는 이 결과를 실제로 구현하는 오픈소스 엔진입니다. GEP 프로토콜 아래 에이전트의 경험을 임시 프롬프트나 스킬 문서가 아니라 Gene과 Capsule로 인코딩합니다. *왜* Evolver가 더 긴 스킬 문서 대신 Gene을 고집하는지 궁금했다면, 바로 이 논문을 읽어야 합니다.
|
|
34
34
|
>
|
|
35
|
-
> 적용 사례가 궁금하신가요? [OpenClaw x EvoMap: CritPt 평가 보고서](https://evomap.ai/blog/openclaw-critpt-report)는 동일한 Gene 기반 진화 루프가 OpenClaw 에이전트를 CritPt Physics Solver의 5개 버전(Beta → v2.2)에 걸쳐
|
|
35
|
+
> 적용 사례가 궁금하신가요? [OpenClaw x EvoMap: CritPt 평가 보고서](https://evomap.ai/blog/openclaw-critpt-report)는 동일한 Gene 기반 진화 루프가 OpenClaw 에이전트를 CritPt Physics Solver의 5개 버전(Beta → v2.2)에 걸쳐 9.1%에서 18.57%까지 끌어올리는 과정을, 전체 토큰 비용 궤적, 유전자 활성화 매핑, 그리고 추론이 재사용 가능한 Gene으로 압축될 때 나타나는 "토큰이 먼저 상승한 뒤 하강하는" 시그니처와 함께 단계별로 보여줍니다.
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
>
|
|
33
33
|
> Evolver is the open-source engine that puts this result into practice: it encodes agent experience as Genes and Capsules under the GEP protocol, not as ad hoc prompts or skill docs. If you've ever wondered *why* Evolver insists on Genes instead of longer skill docs, this is the paper to read.
|
|
34
34
|
>
|
|
35
|
-
> Want the applied version? [OpenClaw x EvoMap: CritPt Evaluation Report](https://evomap.ai/blog/openclaw-critpt-report) walks through how the same Gene-based evolution loop drives an OpenClaw agent from
|
|
35
|
+
> Want the applied version? [OpenClaw x EvoMap: CritPt Evaluation Report](https://evomap.ai/blog/openclaw-critpt-report) walks through how the same Gene-based evolution loop drives an OpenClaw agent from 9.1% to 18.57% on CritPt Physics Solver across five versions (Beta -> v2.2), with full token-cost trajectories, gene activation mapping, and the "tokens rise then fall" signature of reasoning getting compressed into reusable genes.
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
@@ -184,7 +184,7 @@ Every `evolver <flag>` invocation in the rest of this README maps 1:1 to `node i
|
|
|
184
184
|
**Evolver is a prompt generator, not a code patcher.** Each evolution cycle:
|
|
185
185
|
|
|
186
186
|
1. Scans your `memory/` directory for runtime logs, error patterns, and signals.
|
|
187
|
-
2. Selects the best-matching [Gene or Capsule](https://evomap.ai/wiki) from
|
|
187
|
+
2. Selects the best-matching [Gene or Capsule](https://evomap.ai/wiki) from the local GEP asset store.
|
|
188
188
|
3. Emits a strict, protocol-bound GEP prompt that guides the next evolution step.
|
|
189
189
|
4. Records an auditable [EvolutionEvent](https://evomap.ai/wiki) for traceability.
|
|
190
190
|
|
|
@@ -377,16 +377,17 @@ The [evomap.ai](https://evomap.ai) dashboard has a "Worker" toggle on the node d
|
|
|
377
377
|
|
|
378
378
|
This repo includes a protocol-constrained prompt mode based on [GEP (Genome Evolution Protocol)](https://evomap.ai/wiki).
|
|
379
379
|
|
|
380
|
-
- **Structured assets** live in
|
|
381
|
-
-
|
|
382
|
-
-
|
|
383
|
-
-
|
|
380
|
+
- **Structured runtime assets** live in `<workspace>/.evolver/gep/` by default:
|
|
381
|
+
- `<workspace>/.evolver/gep/genes.json`
|
|
382
|
+
- `<workspace>/.evolver/gep/capsules.json`
|
|
383
|
+
- `<workspace>/.evolver/gep/events.jsonl`
|
|
384
|
+
- Set `GEP_ASSETS_DIR` to place the runtime asset store elsewhere.
|
|
384
385
|
- **Selector** logic uses extracted signals to prefer existing Genes/Capsules and emits a JSON selector decision in the prompt.
|
|
385
386
|
- **Constraints**: Only the DNA emoji is allowed in documentation; all other emoji are disallowed.
|
|
386
387
|
|
|
387
388
|
### Your local asset store is never overwritten by upgrades
|
|
388
389
|
|
|
389
|
-
|
|
390
|
+
`<workspace>/.evolver/gep/genes.json`, `<workspace>/.evolver/gep/capsules.json`, and `<workspace>/.evolver/gep/events.jsonl` are owned by your runtime and ignored by git. `assets/gep/` is reserved for bundled starter assets. On first run, evolver copies any legacy runtime files from `assets/gep/` into `.evolver/gep/` without deleting the originals, then seeds `genes.json` from the bundled starter genes only when no local `genes.json` exists.
|
|
390
391
|
|
|
391
392
|
If you ran an older evolver version that wiped your local assets, pull back everything you Promoted or published to the Hub with a single command:
|
|
392
393
|
|
|
@@ -396,7 +397,7 @@ A2A_HUB_URL=https://evomap.ai evolver sync --scope=all --export=backup.gepx
|
|
|
396
397
|
|
|
397
398
|
This hits `/a2a/assets/purchased` (Promoted-to-you plus self-purchased) and `/a2a/assets/published-by-me` (your own drafts and published assets), re-materializes the full payloads into `genes.json` / `capsules.json`, and packs a portable `.gepx` bundle. Previously-purchased payloads re-fetch at zero cost.
|
|
398
399
|
|
|
399
|
-
Purely local assets that were never uploaded to the Hub have no remote copy -- recover them from
|
|
400
|
+
Purely local assets that were never uploaded to the Hub have no remote copy -- recover them from `.evolver/gep/`, from an older `assets/gep/` checkout, or from disk snapshots.
|
|
400
401
|
|
|
401
402
|
## Configuration & Decoupling
|
|
402
403
|
|
package/README.zh-CN.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
>
|
|
33
33
|
> Evolver 正是把这一结论落地的开源引擎:它基于 GEP 协议,把 Agent 的经验沉淀为 Gene 与 Capsule,而不是散落的 prompt 或技能文档。如果你想知道 *为什么* Evolver 坚持使用 Gene 而不是更长的 skill 文档,这就是那篇该读的论文。
|
|
34
34
|
>
|
|
35
|
-
> 想看应用落地的样本?[OpenClaw x EvoMap:CritPt 评测报告](https://evomap.ai/blog/openclaw-critpt-report) 以 OpenClaw Agent 在 CritPt Physics Solver 上的五个版本演进(Beta → v2.2)为例,完整拆解了同一套 Gene 进化闭环如何把得分从
|
|
35
|
+
> 想看应用落地的样本?[OpenClaw x EvoMap:CritPt 评测报告](https://evomap.ai/blog/openclaw-critpt-report) 以 OpenClaw Agent 在 CritPt Physics Solver 上的五个版本演进(Beta → v2.2)为例,完整拆解了同一套 Gene 进化闭环如何把得分从 9.1% 推到 18.57%,并给出 token 成本轨迹、基因激活映射,以及推理被压缩成可复用基因后所呈现的「token 先升后降」特征。
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
@@ -159,7 +159,7 @@ evolver --loop
|
|
|
159
159
|
**Evolver 是一个提示词生成器,不是代码修改器。** 每个进化周期:
|
|
160
160
|
|
|
161
161
|
1. 扫描 `memory/` 目录中的运行日志、错误模式和信号。
|
|
162
|
-
2.
|
|
162
|
+
2. 从本地 GEP 资产库中选择最匹配的 [Gene 或 Capsule](https://evomap.ai/wiki)。
|
|
163
163
|
3. 输出一份严格的、受协议约束的 GEP 提示词来引导下一步进化。
|
|
164
164
|
4. 记录可审计的 [EvolutionEvent](https://evomap.ai/wiki) 以便追溯。
|
|
165
165
|
|
|
@@ -351,16 +351,17 @@ WORKER_ENABLED=1 WORKER_DOMAINS=repair,harden WORKER_MAX_LOAD=3 evolver --loop
|
|
|
351
351
|
|
|
352
352
|
本仓库内置基于 [GEP(基因组进化协议)](https://evomap.ai/wiki)的协议受限提示词模式。
|
|
353
353
|
|
|
354
|
-
-
|
|
355
|
-
-
|
|
356
|
-
-
|
|
357
|
-
-
|
|
354
|
+
- **结构化运行时资产目录**:默认位于 `<workspace>/.evolver/gep/`
|
|
355
|
+
- `<workspace>/.evolver/gep/genes.json`
|
|
356
|
+
- `<workspace>/.evolver/gep/capsules.json`
|
|
357
|
+
- `<workspace>/.evolver/gep/events.jsonl`
|
|
358
|
+
- 可通过 `GEP_ASSETS_DIR` 把运行时资产库放到其他位置。
|
|
358
359
|
- **Selector 选择器**:根据日志提取 signals,优先复用已有 Gene/Capsule,并在提示词中输出可审计的 Selector 决策 JSON。
|
|
359
360
|
- **约束**:除 🧬 外,禁止使用其他 emoji。
|
|
360
361
|
|
|
361
362
|
### 升级不再覆盖你的本地资产库
|
|
362
363
|
|
|
363
|
-
|
|
364
|
+
`<workspace>/.evolver/gep/genes.json`、`<workspace>/.evolver/gep/capsules.json`、`<workspace>/.evolver/gep/events.jsonl` 属于你本地运行时,并被 git 忽略。`assets/gep/` 保留给随包发布的 starter 资产。首次运行时,evolver 会把旧版遗留在 `assets/gep/` 的运行时文件复制到 `.evolver/gep/`,不会删除原文件;只有在本地 `genes.json` 不存在时,才会从随包 starter Gene 初始化。
|
|
364
365
|
|
|
365
366
|
如果你之前用老版本被覆盖过,现在可以一键把所有被 Promoted 给你、以及你自己上传到 Hub 的资产拉回来:
|
|
366
367
|
|
|
@@ -370,7 +371,7 @@ A2A_HUB_URL=https://evomap.ai evolver sync --scope=all --export=backup.gepx
|
|
|
370
371
|
|
|
371
372
|
它会去 `/a2a/assets/purchased`(被 Promoted 给你 + 自购)和 `/a2a/assets/published-by-me`(你自己发布的,含 draft)拉回完整 payload,直接回写 `genes.json` / `capsules.json`,并顺便打成 `.gepx` 整包备份。已购买过的 payload 这次重新拉取不收费。
|
|
372
373
|
|
|
373
|
-
纯本地、从未上传过的资产 Hub 没有副本,只能从
|
|
374
|
+
纯本地、从未上传过的资产 Hub 没有副本,只能从 `.evolver/gep/`、旧版 `assets/gep/` checkout 或磁盘快照找回。
|
|
374
375
|
|
|
375
376
|
## 配置与解耦
|
|
376
377
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver",
|
|
3
|
-
"version": "1.87.
|
|
3
|
+
"version": "1.87.3",
|
|
4
4
|
"description": "A GEP-powered self-evolution engine for AI agents. Features automated log analysis and Genome Evolution Protocol (GEP) for auditable, reusable evolution assets.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -192,7 +192,14 @@ step('Stage 1 — bun bundle (resolve require tree to one file)');
|
|
|
192
192
|
ensureDir(STAGE_DIR);
|
|
193
193
|
const BUNDLED_JS = path.join(STAGE_DIR, 'bundled.js');
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
// `--external '@napi-rs/keyring'`: keyring is an optional dep loaded via
|
|
196
|
+
// dynamic require() in workspace-id; bun otherwise tries to bundle the
|
|
197
|
+
// platform-specific `.node` file as a second output asset, which makes
|
|
198
|
+
// `bun build … --outfile=…` fail with "cannot write multiple output files
|
|
199
|
+
// without an output directory". Treating it as external preserves the
|
|
200
|
+
// existing optional-fallback behaviour (require throws → FS path used) in
|
|
201
|
+
// the standalone binaries.
|
|
202
|
+
run('bun', ['build', ENTRY, '--target=node', `--outfile=${BUNDLED_JS}`, '--external', '@napi-rs/keyring']);
|
|
196
203
|
|
|
197
204
|
const bundleSize = OPTS.dryRun ? 0 : fs.statSync(BUNDLED_JS).size;
|
|
198
205
|
console.log(` bundled.js: ${(bundleSize / 1024 / 1024).toFixed(2)} MB`);
|
|
@@ -266,13 +273,30 @@ if (!OPTS.skipObfuscate) {
|
|
|
266
273
|
const obfSecs = ((Date.now() - t0) / 1000).toFixed(1);
|
|
267
274
|
|
|
268
275
|
const check = spawnSync('node', ['--check', OBF_JS], { encoding: 'utf8' });
|
|
269
|
-
if (check.status
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
276
|
+
if (check.status !== 0) {
|
|
277
|
+
lastValidationErr = (check.stderr || check.stdout || '').split('\n').slice(0, 3).join(' | ');
|
|
278
|
+
console.warn(` attempt ${attempt}/${MAX_OBF_ATTEMPTS}: obfuscator output failed node --check (${lastValidationErr.slice(0, 200)}); retrying with perturbed seed...`);
|
|
279
|
+
continue;
|
|
273
280
|
}
|
|
274
|
-
|
|
275
|
-
|
|
281
|
+
// Second gate: bun's compile-time parser is stricter than node's.
|
|
282
|
+
// 1.87.x (post `@napi-rs/keyring` dep) revealed that ~5% of obfuscator
|
|
283
|
+
// outputs that pass `node --check` still trip bun with errors like
|
|
284
|
+
// `Expected "in" but found ","`. Probe with a cheap bundle-only call
|
|
285
|
+
// (no --compile, native target) to fail fast and feed back into the
|
|
286
|
+
// seed-perturbation loop instead of dying in stage 3.
|
|
287
|
+
const bunProbe = spawnSync('bun', [
|
|
288
|
+
'build', OBF_JS,
|
|
289
|
+
'--target=bun',
|
|
290
|
+
`--outfile=${path.join(STAGE_DIR, 'bundled.obf.bunprobe.js')}`,
|
|
291
|
+
], { encoding: 'utf8' });
|
|
292
|
+
if (bunProbe.status !== 0) {
|
|
293
|
+
lastValidationErr = (bunProbe.stderr || bunProbe.stdout || '').split('\n').slice(0, 3).join(' | ');
|
|
294
|
+
console.warn(` attempt ${attempt}/${MAX_OBF_ATTEMPTS}: obfuscator output rejected by bun parser (${lastValidationErr.slice(0, 200)}); retrying with perturbed seed...`);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
console.log(` obfuscation: ${obfSecs}s, output ${(obfSize / 1024 / 1024).toFixed(2)} MB (attempt ${attempt}/${MAX_OBF_ATTEMPTS}, seed=0x${usedSeed.toString(16)})`);
|
|
298
|
+
succeeded = true;
|
|
299
|
+
break;
|
|
276
300
|
}
|
|
277
301
|
if (!succeeded) {
|
|
278
302
|
console.error(` ERROR: javascript-obfuscator produced syntactically invalid output in ${MAX_OBF_ATTEMPTS} attempts.`);
|
package/src/atp/atpExecute.js
CHANGED
|
@@ -27,6 +27,7 @@ const https = require('https');
|
|
|
27
27
|
const crypto = require('crypto');
|
|
28
28
|
|
|
29
29
|
const { computeAssetId } = require('../gep/contentHash');
|
|
30
|
+
const { enforceHubScheme, strictHttpsAgent } = require('../gep/hubFetch');
|
|
30
31
|
const {
|
|
31
32
|
getNodeId,
|
|
32
33
|
getHubUrl,
|
|
@@ -114,6 +115,19 @@ function _publishUrl() {
|
|
|
114
115
|
|
|
115
116
|
function _postJson(urlStr, body, timeoutMs) {
|
|
116
117
|
return new Promise(function (resolve) {
|
|
118
|
+
// Same TLS posture as hubFetch: refuse plain http:// unless
|
|
119
|
+
// EVOMAP_HUB_ALLOW_INSECURE=1. Before this guard the function
|
|
120
|
+
// silently fell back to `lib = http` for any non-https URL, so an
|
|
121
|
+
// operator override `A2A_HUB_URL=http://...` would send /a2a/publish
|
|
122
|
+
// and /a2a/task/complete in cleartext while hubFetch-routed calls
|
|
123
|
+
// (e.g. /a2a/verify-solidify) refused the same URL — inconsistent
|
|
124
|
+
// TLS enforcement across modules.
|
|
125
|
+
try {
|
|
126
|
+
enforceHubScheme(urlStr);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
resolve({ ok: false, error: 'tls_refused: ' + (e && e.message) });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
117
131
|
let parsed;
|
|
118
132
|
try {
|
|
119
133
|
parsed = new URL(urlStr);
|
|
@@ -128,15 +142,28 @@ function _postJson(urlStr, body, timeoutMs) {
|
|
|
128
142
|
{ 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
129
143
|
buildHubHeaders() || {},
|
|
130
144
|
);
|
|
145
|
+
// Pin TLS cert verification for https calls so a globally-disabled
|
|
146
|
+
// NODE_TLS_REJECT_UNAUTHORIZED=0 cannot weaken the Hub channel
|
|
147
|
+
// (Cursor Security Reviewer #160 Medium). hubFetch enforces the
|
|
148
|
+
// same via its undici dispatcher; this is the Node-native-https
|
|
149
|
+
// equivalent.
|
|
150
|
+
//
|
|
151
|
+
// Skipped under EVOMAP_HUB_ALLOW_INSECURE=1 so local-dev / self-
|
|
152
|
+
// signed mock hubs that legitimately rely on
|
|
153
|
+
// NODE_TLS_REJECT_UNAUTHORIZED=0 still work.
|
|
154
|
+
const requestOpts = {
|
|
155
|
+
hostname: parsed.hostname,
|
|
156
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
157
|
+
path: parsed.pathname + (parsed.search || ''),
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: headers,
|
|
160
|
+
timeout: timeoutMs || PUBLISH_TIMEOUT_MS,
|
|
161
|
+
};
|
|
162
|
+
if (isHttps && process.env.EVOMAP_HUB_ALLOW_INSECURE !== '1') {
|
|
163
|
+
requestOpts.agent = strictHttpsAgent;
|
|
164
|
+
}
|
|
131
165
|
const req = lib.request(
|
|
132
|
-
|
|
133
|
-
hostname: parsed.hostname,
|
|
134
|
-
port: parsed.port || (isHttps ? 443 : 80),
|
|
135
|
-
path: parsed.pathname + (parsed.search || ''),
|
|
136
|
-
method: 'POST',
|
|
137
|
-
headers: headers,
|
|
138
|
-
timeout: timeoutMs || PUBLISH_TIMEOUT_MS,
|
|
139
|
-
},
|
|
166
|
+
requestOpts,
|
|
140
167
|
function (res) {
|
|
141
168
|
const chunks = [];
|
|
142
169
|
res.on('data', function (c) { chunks.push(c); });
|
package/src/atp/autoBuyer.js
CHANGED
|
@@ -36,7 +36,13 @@ const DEFAULT_DAILY_CAP = 50;
|
|
|
36
36
|
const DEFAULT_PER_ORDER_CAP = 10;
|
|
37
37
|
const DEFAULT_ORDER_TIMEOUT_MS = 3000;
|
|
38
38
|
const COLD_START_WINDOW_MS = 5 * 60 * 1000;
|
|
39
|
+
// Successful orders dedup for 24h so the same capability gap is only paid for
|
|
40
|
+
// once per day. Failed orders dedup for 5 minutes only — long enough to
|
|
41
|
+
// absorb tight retry loops (the original goal of "don't hammer the hub")
|
|
42
|
+
// without making the user wait 24h to retry a question after a transient
|
|
43
|
+
// 503/network blip.
|
|
39
44
|
const DEDUP_TTL_MS = 24 * 60 * 60 * 1000;
|
|
45
|
+
const DEDUP_FAILURE_TTL_MS = 5 * 60 * 1000;
|
|
40
46
|
const LEDGER_FILENAME = 'atp-autobuyer-ledger.json';
|
|
41
47
|
const ACK_FILENAME = 'atp-autobuy-ack.json';
|
|
42
48
|
|
|
@@ -145,13 +151,22 @@ function _rotateIfNewDay(ledger, now) {
|
|
|
145
151
|
}
|
|
146
152
|
|
|
147
153
|
function _pruneDedup(ledger, now) {
|
|
148
|
-
const
|
|
154
|
+
const nowMs = typeof now === 'number' ? now : Date.now();
|
|
149
155
|
const out = {};
|
|
150
156
|
const src = ledger.dedup || {};
|
|
151
157
|
const keys = Object.keys(src);
|
|
152
158
|
for (let i = 0; i < keys.length; i++) {
|
|
153
159
|
const k = keys[i];
|
|
154
|
-
|
|
160
|
+
const entry = src[k];
|
|
161
|
+
// Legacy ledgers written by older versions stored plain timestamps; treat
|
|
162
|
+
// them as successful orders (the original behaviour) so an upgrade does
|
|
163
|
+
// not suddenly forget recent dedups.
|
|
164
|
+
if (typeof entry === 'number') {
|
|
165
|
+
if (entry >= nowMs - DEDUP_TTL_MS) out[k] = entry;
|
|
166
|
+
} else if (entry && typeof entry.ts === 'number') {
|
|
167
|
+
const ttl = entry.failed ? DEDUP_FAILURE_TTL_MS : DEDUP_TTL_MS;
|
|
168
|
+
if (entry.ts >= nowMs - ttl) out[k] = entry;
|
|
169
|
+
}
|
|
155
170
|
}
|
|
156
171
|
ledger.dedup = out;
|
|
157
172
|
return ledger;
|
|
@@ -205,11 +220,36 @@ function _withTimeout(promise, timeoutMs) {
|
|
|
205
220
|
]);
|
|
206
221
|
}
|
|
207
222
|
|
|
208
|
-
|
|
209
|
-
|
|
223
|
+
// Single-flight queue: serialize the read → cap-check → placeOrder → write
|
|
224
|
+
// pipeline so two concurrent considerOrder() calls cannot both pass the cap
|
|
225
|
+
// check on the same ledger snapshot and silently double-spend.
|
|
226
|
+
//
|
|
227
|
+
// Without this, two parallel calls (e.g. user runs Claude Code in two tabs
|
|
228
|
+
// through the same proxy, or two capability gaps fire in the same tick) both
|
|
229
|
+
// read spent=40, both compute remaining=10, both await placeOrder, both
|
|
230
|
+
// increment to spent=50, and write — silently exceeding the daily cap by one
|
|
231
|
+
// full order each. autoBuyer is single-process so an in-memory queue is
|
|
232
|
+
// sufficient; a file lock would only be needed if multiple OS processes
|
|
233
|
+
// shared the same ledger file (not the current deployment model).
|
|
234
|
+
let _orderQueue = Promise.resolve();
|
|
235
|
+
|
|
236
|
+
function considerOrder(opts) {
|
|
237
|
+
if (!_started) return Promise.resolve({ ok: false, skipped: true, reason: 'not_started' });
|
|
210
238
|
if (!opts || !Array.isArray(opts.capabilities) || opts.capabilities.length === 0) {
|
|
211
|
-
return { ok: false, skipped: true, reason: 'no_capabilities' };
|
|
239
|
+
return Promise.resolve({ ok: false, skipped: true, reason: 'no_capabilities' });
|
|
212
240
|
}
|
|
241
|
+
const next = _orderQueue.then(
|
|
242
|
+
() => _considerOrderSerialized(opts),
|
|
243
|
+
() => _considerOrderSerialized(opts), // never let a prior rejection break the chain
|
|
244
|
+
);
|
|
245
|
+
// Swallow rejection on the queue tail so a single thrown error here does
|
|
246
|
+
// not poison every subsequent call; the original `next` promise still
|
|
247
|
+
// surfaces the error to the caller.
|
|
248
|
+
_orderQueue = next.then(() => {}, () => {});
|
|
249
|
+
return next;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function _considerOrderSerialized(opts) {
|
|
213
253
|
const now = Date.now();
|
|
214
254
|
let ledger = _readLedger();
|
|
215
255
|
ledger = _rotateIfNewDay(ledger, now);
|
|
@@ -248,15 +288,17 @@ async function considerOrder(opts) {
|
|
|
248
288
|
|
|
249
289
|
if (result && result.ok) {
|
|
250
290
|
ledger.spent = (ledger.spent || 0) + budget;
|
|
251
|
-
ledger.dedup[hash] = now;
|
|
291
|
+
ledger.dedup[hash] = { ts: now, failed: false };
|
|
252
292
|
_writeLedger(ledger);
|
|
253
293
|
console.log('[ATP-AutoBuyer] Order placed: ' + (result.data && result.data.order_id) + ' budget=' + budget + ' remaining_today=' + Math.max(0, dailyCap - ledger.spent));
|
|
254
294
|
return { ok: true, data: result.data, spent: budget };
|
|
255
295
|
}
|
|
256
296
|
|
|
257
|
-
// On failure
|
|
258
|
-
// capability gap
|
|
259
|
-
|
|
297
|
+
// On failure record a SHORT-TTL dedup entry (5 min) so we don't hammer the
|
|
298
|
+
// hub for the same capability gap inside a tight retry loop, but the user
|
|
299
|
+
// can retry the same question once the transient error clears — far better
|
|
300
|
+
// than the previous 24h block for a single 503.
|
|
301
|
+
ledger.dedup[hash] = { ts: now, failed: true };
|
|
260
302
|
_writeLedger(ledger);
|
|
261
303
|
return { ok: false, error: (result && result.error) || 'unknown_error' };
|
|
262
304
|
}
|
|
@@ -267,15 +309,25 @@ async function considerOrder(opts) {
|
|
|
267
309
|
// via .tmp + rename so a crash mid-write never produces a corrupt ack file.
|
|
268
310
|
function setConsent(enabled) {
|
|
269
311
|
const dir = getMemoryDir();
|
|
270
|
-
try { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } catch (_) {}
|
|
271
312
|
const body = {
|
|
272
313
|
enabled: !!enabled,
|
|
273
314
|
acknowledged_at: new Date().toISOString(),
|
|
274
315
|
version: 1,
|
|
275
316
|
};
|
|
276
317
|
const tmp = _ackPath() + '.tmp';
|
|
277
|
-
|
|
278
|
-
|
|
318
|
+
// Single try/catch over the whole pipeline. Previously the mkdirSync was
|
|
319
|
+
// wrapped in its own swallowing try/catch, so an EACCES on the parent dir
|
|
320
|
+
// would surface to the caller as a confusing ENOENT from writeFileSync.
|
|
321
|
+
// Surface the original error verbatim and best-effort clean up any
|
|
322
|
+
// partial .tmp file so a retry from a TTY prompt sees a clean slate.
|
|
323
|
+
try {
|
|
324
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
325
|
+
fs.writeFileSync(tmp, JSON.stringify(body, null, 2) + '\n', 'utf8');
|
|
326
|
+
fs.renameSync(tmp, _ackPath());
|
|
327
|
+
} catch (err) {
|
|
328
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
329
|
+
throw err;
|
|
330
|
+
}
|
|
279
331
|
return body;
|
|
280
332
|
}
|
|
281
333
|
|
|
@@ -288,6 +340,7 @@ function _resetForTests() {
|
|
|
288
340
|
perOrderCap: DEFAULT_PER_ORDER_CAP,
|
|
289
341
|
timeoutMs: DEFAULT_ORDER_TIMEOUT_MS,
|
|
290
342
|
};
|
|
343
|
+
_orderQueue = Promise.resolve();
|
|
291
344
|
}
|
|
292
345
|
|
|
293
346
|
module.exports = {
|
|
@@ -306,22 +359,24 @@ module.exports = {
|
|
|
306
359
|
readAck: _readAck,
|
|
307
360
|
ACK_FILENAME,
|
|
308
361
|
// Exposed for tests and diagnostics only; callers should not depend on
|
|
309
|
-
// these internals in production code paths.
|
|
362
|
+
// these internals in production code paths. Ack-file helpers
|
|
363
|
+
// (getAckPath / readAck / ACK_FILENAME) are intentionally NOT mirrored
|
|
364
|
+
// here — production and tests both go through the public surface above,
|
|
365
|
+
// so a single source of truth survives schema changes (Bugbot PR #141 R6
|
|
366
|
+
// follow-up: keep "test-only" honest, no production caller may reach in).
|
|
310
367
|
__internals: {
|
|
311
368
|
readLedger: _readLedger,
|
|
312
369
|
writeLedger: _writeLedger,
|
|
313
370
|
questionHash: _questionHash,
|
|
314
371
|
effectiveCap: _effectiveCap,
|
|
315
372
|
resetForTests: _resetForTests,
|
|
316
|
-
ackPath: _ackPath,
|
|
317
|
-
readAck: _readAck,
|
|
318
373
|
constants: {
|
|
319
374
|
DEFAULT_DAILY_CAP,
|
|
320
375
|
DEFAULT_PER_ORDER_CAP,
|
|
321
376
|
COLD_START_WINDOW_MS,
|
|
322
377
|
DEDUP_TTL_MS,
|
|
378
|
+
DEDUP_FAILURE_TTL_MS,
|
|
323
379
|
LEDGER_FILENAME,
|
|
324
|
-
ACK_FILENAME,
|
|
325
380
|
},
|
|
326
381
|
},
|
|
327
382
|
};
|
package/src/atp/autoDeliver.js
CHANGED
|
@@ -155,6 +155,18 @@ function start(opts) {
|
|
|
155
155
|
_pollInterval = setInterval(function () {
|
|
156
156
|
_tick().catch(function () { /* swallowed in _tick */ });
|
|
157
157
|
}, _pollMs);
|
|
158
|
+
// .unref() so this background poller does NOT keep the Node event
|
|
159
|
+
// loop alive on its own. `evolver run` (single-shot) writes its
|
|
160
|
+
// artifacts and expects to exit — without unref, the setInterval
|
|
161
|
+
// handle pins the process and the run sits as a residual `node`
|
|
162
|
+
// process until manually killed (public issue #553). `evolver --loop`
|
|
163
|
+
// (daemon) keeps the foreground evolve loop alive on its own
|
|
164
|
+
// schedule, so an unref'd poller still polls — unref only changes
|
|
165
|
+
// whether THIS handle alone keeps the loop alive, not whether the
|
|
166
|
+
// handle fires.
|
|
167
|
+
if (_pollInterval && typeof _pollInterval.unref === 'function') {
|
|
168
|
+
_pollInterval.unref();
|
|
169
|
+
}
|
|
158
170
|
// Do not await -- fire the first tick asynchronously so start() returns
|
|
159
171
|
// immediately. This matches the autoBuyer start() semantics.
|
|
160
172
|
_tick().catch(function () { /* swallowed in _tick */ });
|
|
@@ -189,6 +201,10 @@ module.exports = {
|
|
|
189
201
|
writeLedger: _writeLedger,
|
|
190
202
|
buildProofPayload: _buildProofPayload,
|
|
191
203
|
resetForTests: _resetForTests,
|
|
204
|
+
// Test-only accessor for the active poll Timeout. Used to assert
|
|
205
|
+
// the timer was `.unref()`ed so it does not pin the Node event
|
|
206
|
+
// loop (regression guard for public issue #553).
|
|
207
|
+
getPollIntervalForTest: () => _pollInterval,
|
|
192
208
|
constants: {
|
|
193
209
|
DEFAULT_POLL_MS,
|
|
194
210
|
MIN_POLL_MS,
|
|
@@ -22,22 +22,13 @@
|
|
|
22
22
|
const readline = require("readline");
|
|
23
23
|
const autoBuyer = require("./autoBuyer");
|
|
24
24
|
|
|
25
|
-
// All ack file plumbing
|
|
26
|
-
// resolution, read
|
|
27
|
-
// cliAutobuyPrompt
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
const ACK_FILE_NAME = autoBuyer.ACK_FILENAME;
|
|
33
|
-
|
|
34
|
-
function _getAckPath() {
|
|
35
|
-
return autoBuyer.getAckPath();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function _readAck() {
|
|
39
|
-
return autoBuyer.readAck();
|
|
40
|
-
}
|
|
25
|
+
// All ack file plumbing lives on autoBuyer (filename constant, path
|
|
26
|
+
// resolution, read with strict validation, atomic write via tmp+rename).
|
|
27
|
+
// cliAutobuyPrompt always reaches it through the public surface so the
|
|
28
|
+
// two modules cannot diverge on schema or validation — pre-consolidation
|
|
29
|
+
// drift bit us twice (Bugbot PR #141: duplicate writers + lenient-vs-
|
|
30
|
+
// strict reader). No __internals re-export here either: tests import
|
|
31
|
+
// autoBuyer directly so a future rename trips a single set of asserts.
|
|
41
32
|
|
|
42
33
|
/**
|
|
43
34
|
* @returns {"ack_present"|"env_set"|"non_tty"|"eligible"}
|
|
@@ -50,7 +41,7 @@ function classify(env, stdin) {
|
|
|
50
41
|
if (!stdin || !stdin.isTTY) {
|
|
51
42
|
return "non_tty";
|
|
52
43
|
}
|
|
53
|
-
if (
|
|
44
|
+
if (autoBuyer.readAck()) {
|
|
54
45
|
return "ack_present";
|
|
55
46
|
}
|
|
56
47
|
return "eligible";
|
|
@@ -160,9 +151,4 @@ async function runPrompt(opts) {
|
|
|
160
151
|
module.exports = {
|
|
161
152
|
runPrompt,
|
|
162
153
|
classify,
|
|
163
|
-
__internals: {
|
|
164
|
-
ACK_FILE_NAME,
|
|
165
|
-
_readAck,
|
|
166
|
-
_getAckPath,
|
|
167
|
-
},
|
|
168
154
|
};
|
package/src/atp/hubClient.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
const http = require('http');
|
|
18
18
|
const { getHubUrl, buildHubHeaders, getNodeId } = require('../gep/a2aProtocol');
|
|
19
|
+
const { hubFetch } = require('../gep/hubFetch');
|
|
19
20
|
const { getProxyUrl, getProxyToken } = require('../proxy/server/settings');
|
|
20
21
|
|
|
21
22
|
function _isProxyMode() {
|
|
@@ -68,12 +69,33 @@ function _proxyRequest(method, path, body, timeoutMs) {
|
|
|
68
69
|
});
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
// Route through hubFetch() rather than the global `fetch()` for two
|
|
73
|
+
// reasons (both flagged by Cursor reviewers on PR #160):
|
|
74
|
+
//
|
|
75
|
+
// 1. Dispatcher mixing (Bugbot HIGH): `strictUndiciAgent` is an Agent
|
|
76
|
+
// from the *installed* `undici` package, but `global.fetch` is
|
|
77
|
+
// backed by Node's *internal* undici. Passing one to the other
|
|
78
|
+
// throws `UND_ERR_INVALID_ARG: invalid onRequestStart method` at
|
|
79
|
+
// request time — exactly the failure mode the comment at the top
|
|
80
|
+
// of hubFetch.js calls out. hubFetch already routes through
|
|
81
|
+
// `undici.fetch` from the same package as its Agent, so all calls
|
|
82
|
+
// that go through hubFetch are immune.
|
|
83
|
+
//
|
|
84
|
+
// 2. Case-sensitive scheme check (Security Reviewer MEDIUM): a hand-
|
|
85
|
+
// rolled `endpoint.startsWith('https:')` would skip the strict
|
|
86
|
+
// dispatcher for `HTTPS://...`. hubFetch's `_validateHubUrl` uses
|
|
87
|
+
// `new URL(url).protocol`, which normalises to lowercase, so
|
|
88
|
+
// routing through it eliminates the bug class.
|
|
89
|
+
//
|
|
90
|
+
// Routing through hubFetch also inherits the URL-scheme enforcement and
|
|
91
|
+
// the EVOMAP_HUB_ALLOW_INSECURE escape hatch automatically; we no
|
|
92
|
+
// longer need the explicit `enforceHubScheme` guard here.
|
|
71
93
|
function _hubPost(pathSuffix, body, timeoutMs) {
|
|
72
94
|
const hubUrl = getHubUrl();
|
|
73
95
|
if (!hubUrl) return Promise.resolve({ ok: false, error: 'no_hub_url' });
|
|
74
96
|
const endpoint = hubUrl.replace(/\/+$/, '') + pathSuffix;
|
|
75
97
|
const timeout = timeoutMs || require('../config').HTTP_TRANSPORT_TIMEOUT_MS;
|
|
76
|
-
return
|
|
98
|
+
return hubFetch(endpoint, {
|
|
77
99
|
method: 'POST',
|
|
78
100
|
headers: buildHubHeaders(),
|
|
79
101
|
body: JSON.stringify(body),
|
|
@@ -83,7 +105,17 @@ function _hubPost(pathSuffix, body, timeoutMs) {
|
|
|
83
105
|
if (!res.ok) return res.text().then(function (t) { return { ok: false, status: res.status, error: t.slice(0, 400) }; });
|
|
84
106
|
return res.json().then(function (data) { return { ok: true, data: data }; });
|
|
85
107
|
})
|
|
86
|
-
.catch(function (err) {
|
|
108
|
+
.catch(function (err) {
|
|
109
|
+
// hubFetch throws synchronously (rejected Promise) when the URL
|
|
110
|
+
// fails scheme validation in secure mode. Translate to the same
|
|
111
|
+
// structured envelope the previous in-line guard produced so the
|
|
112
|
+
// caller contract is unchanged.
|
|
113
|
+
const msg = (err && err.message) || String(err);
|
|
114
|
+
if (msg.indexOf('[hubFetch]') !== -1) {
|
|
115
|
+
return { ok: false, error: 'tls_refused: ' + msg };
|
|
116
|
+
}
|
|
117
|
+
return { ok: false, error: msg };
|
|
118
|
+
});
|
|
87
119
|
}
|
|
88
120
|
|
|
89
121
|
function _hubGet(pathSuffix, timeoutMs) {
|
|
@@ -91,7 +123,7 @@ function _hubGet(pathSuffix, timeoutMs) {
|
|
|
91
123
|
if (!hubUrl) return Promise.resolve({ ok: false, error: 'no_hub_url' });
|
|
92
124
|
const endpoint = hubUrl.replace(/\/+$/, '') + pathSuffix;
|
|
93
125
|
const timeout = timeoutMs || require('../config').HTTP_TRANSPORT_TIMEOUT_MS;
|
|
94
|
-
return
|
|
126
|
+
return hubFetch(endpoint, {
|
|
95
127
|
method: 'GET',
|
|
96
128
|
headers: buildHubHeaders(),
|
|
97
129
|
signal: AbortSignal.timeout(timeout),
|
|
@@ -100,7 +132,13 @@ function _hubGet(pathSuffix, timeoutMs) {
|
|
|
100
132
|
if (!res.ok) return res.text().then(function (t) { return { ok: false, status: res.status, error: t.slice(0, 400) }; });
|
|
101
133
|
return res.json().then(function (data) { return { ok: true, data: data }; });
|
|
102
134
|
})
|
|
103
|
-
.catch(function (err) {
|
|
135
|
+
.catch(function (err) {
|
|
136
|
+
const msg = (err && err.message) || String(err);
|
|
137
|
+
if (msg.indexOf('[hubFetch]') !== -1) {
|
|
138
|
+
return { ok: false, error: 'tls_refused: ' + msg };
|
|
139
|
+
}
|
|
140
|
+
return { ok: false, error: msg };
|
|
141
|
+
});
|
|
104
142
|
}
|
|
105
143
|
|
|
106
144
|
// Dispatcher: choose proxy or direct hub based on env + proxy availability.
|