@evomap/evolver 1.86.0 → 1.87.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 (51) hide show
  1. package/assets/gep/genes.seed.json +44 -2
  2. package/package.json +3 -2
  3. package/src/adapters/scripts/_memoryFiltering.js +35 -0
  4. package/src/adapters/scripts/evolver-session-start.js +8 -5
  5. package/src/adapters/scripts/evolver-signal-detect.js +24 -1
  6. package/src/evolve/guards.js +1 -1
  7. package/src/evolve/pipeline/collect.js +1 -1
  8. package/src/evolve/pipeline/dispatch.js +1 -1
  9. package/src/evolve/pipeline/enrich.js +1 -1
  10. package/src/evolve/pipeline/hub.js +1 -1
  11. package/src/evolve/pipeline/select.js +1 -1
  12. package/src/evolve/pipeline/signals.js +1 -1
  13. package/src/evolve/utils.js +1 -1
  14. package/src/evolve.js +1 -1
  15. package/src/gep/a2aProtocol.js +1 -1
  16. package/src/gep/assetStore.js +1 -0
  17. package/src/gep/candidateEval.js +1 -1
  18. package/src/gep/candidates.js +1 -1
  19. package/src/gep/contentHash.js +1 -1
  20. package/src/gep/crypto.js +1 -1
  21. package/src/gep/curriculum.js +1 -1
  22. package/src/gep/deviceId.js +1 -1
  23. package/src/gep/envFingerprint.js +1 -1
  24. package/src/gep/epigenetics.js +1 -1
  25. package/src/gep/explore.js +1 -1
  26. package/src/gep/hash.js +1 -1
  27. package/src/gep/hubFetch.js +1 -1
  28. package/src/gep/hubReview.js +1 -1
  29. package/src/gep/hubSearch.js +1 -1
  30. package/src/gep/hubVerify.js +1 -1
  31. package/src/gep/learningSignals.js +1 -1
  32. package/src/gep/memoryGraph.js +1 -1
  33. package/src/gep/memoryGraphAdapter.js +1 -1
  34. package/src/gep/mutation.js +1 -1
  35. package/src/gep/narrativeMemory.js +1 -1
  36. package/src/gep/openPRRegistry.js +1 -1
  37. package/src/gep/paths.js +7 -1
  38. package/src/gep/personality.js +1 -1
  39. package/src/gep/policyCheck.js +1 -1
  40. package/src/gep/prompt.js +1 -1
  41. package/src/gep/recallVerifier.js +1 -1
  42. package/src/gep/reflection.js +1 -1
  43. package/src/gep/schemas/capsule.js +51 -1
  44. package/src/gep/selector.js +1 -1
  45. package/src/gep/skillDistiller.js +1 -1
  46. package/src/gep/solidify.js +1 -1
  47. package/src/gep/strategy.js +1 -1
  48. package/src/gep/workspaceKeychain.js +1 -1
  49. package/src/proxy/index.js +226 -1
  50. package/src/proxy/router/messages_route.js +87 -9
  51. package/src/proxy/server/http.js +50 -13
@@ -19,12 +19,53 @@ const { pickForTurn } = require('./model_router');
19
19
  const { rewriteModel } = require('./cache_passthrough');
20
20
  const { extractFeatures } = require('./features');
21
21
 
22
+ // Bedrock-resolvable global.* aliases as of 2026-05-25:
23
+ // - opus-4-7 : bare alias OK
24
+ // - haiku-4-5 : ONLY the dated form resolves; bare alias 400s
25
+ // ("ValidationException: invalid model identifier")
26
+ // - sonnet-4-7 : not yet on Bedrock — sonnet-4-6 is the current global.*
27
+ // sonnet alias. The no-downgrade guard still blocks an
28
+ // inbound sonnet-4-7 → sonnet-4-6 rewrite, so callers
29
+ // pinned to 4-7 stay on 4-7.
22
30
  const DEFAULT_TIER_MODELS = Object.freeze({
23
- cheap: 'claude-haiku-4-5',
24
- mid: 'claude-sonnet-4-6',
25
- expensive: 'claude-opus-4-7',
31
+ cheap: 'global.anthropic.claude-haiku-4-5-20251001-v1:0',
32
+ mid: 'global.anthropic.claude-sonnet-4-6',
33
+ expensive: 'global.anthropic.claude-opus-4-7',
26
34
  });
27
35
 
36
+ // No-downgrade guard: when the router rewrites a request to a different model
37
+ // inside the same Claude family (opus / sonnet / haiku), the chosen generation
38
+ // must be >= the original. This catches the 2026-05-25 /compact incident where
39
+ // EVOMAP_MODEL_EXPENSIVE was misconfigured to opus-4-1 while users sent
40
+ // opus-4-7 — every planning turn silently rewrote 4-7 → 4-1, hit Bedrock 5xx
41
+ // on the older endpoint, and stalled the user behind retries. Cross-family
42
+ // rewrites (opus → haiku for cheap tier) are the router's core function and
43
+ // stay allowed; this guard only blocks intra-family generational downgrades.
44
+ //
45
+ // Parsers below handle two ID shapes the proxy actually sees:
46
+ // - global.anthropic.claude-{family}-{major}-{minor}
47
+ // - us.anthropic.claude-{family}-{major}-{minor}-YYYYMMDD-v1:0 (Bedrock dated)
48
+ // Anything else (third-party, opaque alias) → null on both fields → guard
49
+ // returns false (allow). We only block when we can prove the comparison.
50
+ function parseClaudeId(modelId) {
51
+ if (typeof modelId !== 'string') return null;
52
+ const m = modelId.match(/claude-(opus|sonnet|haiku)-(\d+)-(\d+)/i);
53
+ if (!m) return null;
54
+ const major = Number(m[2]);
55
+ const minor = Number(m[3]);
56
+ if (!Number.isFinite(major) || !Number.isFinite(minor)) return null;
57
+ return { family: m[1].toLowerCase(), major, minor };
58
+ }
59
+
60
+ function isIntraFamilyDowngrade(chosen, original) {
61
+ const c = parseClaudeId(chosen);
62
+ const o = parseClaudeId(original);
63
+ if (!c || !o) return false;
64
+ if (c.family !== o.family) return false;
65
+ if (c.major !== o.major) return c.major < o.major;
66
+ return c.minor < o.minor;
67
+ }
68
+
28
69
  function resolveTierModels() {
29
70
  return {
30
71
  cheap: process.env.EVOMAP_MODEL_CHEAP || DEFAULT_TIER_MODELS.cheap,
@@ -51,10 +92,23 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled } = {}) {
51
92
  // ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN env (token mediation, see
52
93
  // _proxyAnthropic). The proxy server itself has already auth-checked
53
94
  // `Authorization: Bearer <proxy_token>` before reaching this handler.
54
- const hasInboundKey = !!inboundHeaders['x-api-key'];
55
- const hasProxyEnvCreds = !!(process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN);
56
- if (!hasInboundKey && !hasProxyEnvCreds) {
57
- throw Object.assign(new Error('x-api-key required'), { statusCode: 401 });
95
+ //
96
+ // Bedrock upstream uses SigV4 with AWS_* env, so neither inbound
97
+ // x-api-key nor ANTHROPIC_* env are meaningful. Skip the check —
98
+ // the real proxy gate is still the Bearer proxy_token enforced
99
+ // upstream of this handler in ProxyHttpServer.
100
+ // Read EVOMAP_UPSTREAM exactly once per request and thread it through to
101
+ // the proxy callable via opts.upstreamMode. Reading it here AND in the
102
+ // dispatch closure would let a mid-request env hot-swap make the two
103
+ // decisions disagree (e.g. gate skipped on the assumption of bedrock,
104
+ // but the request still hits _proxyAnthropic with no credentials).
105
+ const upstreamMode = (process.env.EVOMAP_UPSTREAM || 'anthropic').toLowerCase();
106
+ if (upstreamMode !== 'bedrock') {
107
+ const hasInboundKey = !!inboundHeaders['x-api-key'];
108
+ const hasProxyEnvCreds = !!(process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN);
109
+ if (!hasInboundKey && !hasProxyEnvCreds) {
110
+ throw Object.assign(new Error('x-api-key required'), { statusCode: 401 });
111
+ }
58
112
  }
59
113
 
60
114
  const originalModel = body && typeof body.model === 'string' ? body.model : null;
@@ -74,7 +128,23 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled } = {}) {
74
128
  decisionTier = decision.tier;
75
129
  decisionReason = decision.reason;
76
130
  const tierModel = resolveTierModels()[decision.tier];
77
- if (tierModel) chosenModel = tierModel;
131
+ if (tierModel) {
132
+ if (isIntraFamilyDowngrade(tierModel, originalModel)) {
133
+ // Intra-family downgrade detected (e.g. opus-4-7 -> opus-4-1).
134
+ // Refuse the rewrite, keep the user's original model, and log a
135
+ // structured fallback so misconfigured tier env vars are visible
136
+ // in telemetry instead of manifesting as latency / 5xx stalls.
137
+ fallback = 'downgrade_blocked';
138
+ log.warn?.(JSON.stringify({
139
+ event: 'router_fallback',
140
+ reason: 'downgrade_blocked',
141
+ original_model: originalModel,
142
+ would_have_been: tierModel,
143
+ }));
144
+ } else {
145
+ chosenModel = tierModel;
146
+ }
147
+ }
78
148
  } catch (err) {
79
149
  fallback = 'classifier_error';
80
150
  log.warn?.(JSON.stringify({
@@ -120,6 +190,7 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled } = {}) {
120
190
 
121
191
  const upstream = await anthropicProxy('/v1/messages', outboundBody, {
122
192
  inboundHeaders,
193
+ upstreamMode,
123
194
  });
124
195
 
125
196
  if (upstream.stream) {
@@ -177,6 +248,7 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled } = {}) {
177
248
  const retryBody = rewriteModel(body, originalModel);
178
249
  finalUpstream = await anthropicProxy('/v1/messages', retryBody, {
179
250
  inboundHeaders,
251
+ upstreamMode,
180
252
  });
181
253
  } catch (err) {
182
254
  // Replay the drained first response so the caller still sees the
@@ -239,4 +311,10 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled } = {}) {
239
311
  };
240
312
  }
241
313
 
242
- module.exports = { buildMessagesHandler, DEFAULT_TIER_MODELS, resolveTierModels };
314
+ module.exports = {
315
+ buildMessagesHandler,
316
+ DEFAULT_TIER_MODELS,
317
+ resolveTierModels,
318
+ parseClaudeId,
319
+ isIntraFamilyDowngrade,
320
+ };
@@ -94,8 +94,23 @@ class ProxyHttpServer {
94
94
  }
95
95
 
96
96
  async start() {
97
+ // Capture the prior token before clearIfStale wipes it. Daemon restarts
98
+ // routinely fall into the stale branch (the previous PID is gone), and
99
+ // rotating `proxy.token` on every restart invalidates ANTHROPIC_AUTH_TOKEN
100
+ // already exported into long-lived shells (the .bashrc auto-source only
101
+ // runs once per terminal).
102
+ const priorProxy = readSettings().proxy || {};
103
+ const previous = typeof priorProxy.token === 'string' ? priorProxy.token : null;
104
+ // settings.json is operator-edited; previous_tokens may contain non-strings
105
+ // (numbers, booleans, objects) that would later crash Buffer.from(cand, 'utf8')
106
+ // in _handleRequest as ERR_INVALID_ARG_TYPE — an unhandled rejection that
107
+ // takes the daemon down under default --unhandled-rejections=throw.
108
+ const priorPreviousTokens = Array.isArray(priorProxy.previous_tokens)
109
+ ? priorProxy.previous_tokens.filter((t) => typeof t === 'string' && t.length > 0)
110
+ : [];
97
111
  clearIfStale();
98
- this.token = crypto.randomBytes(32).toString('hex');
112
+ this.token = previous || crypto.randomBytes(32).toString('hex');
113
+ this._priorPreviousTokens = priorPreviousTokens;
99
114
  this.server = http.createServer((req, res) => this._handleRequest(req, res));
100
115
 
101
116
  let port = this.basePort;
@@ -104,14 +119,16 @@ class ProxyHttpServer {
104
119
  if (ok) {
105
120
  this.actualPort = port;
106
121
  const url = `http://127.0.0.1:${port}`;
107
- writeSettings({
108
- proxy: {
109
- url,
110
- pid: process.pid,
111
- started_at: new Date().toISOString(),
112
- token: this.token,
113
- },
114
- });
122
+ const proxyBlock = {
123
+ url,
124
+ pid: process.pid,
125
+ started_at: new Date().toISOString(),
126
+ token: this.token,
127
+ };
128
+ if (this._priorPreviousTokens && this._priorPreviousTokens.length) {
129
+ proxyBlock.previous_tokens = this._priorPreviousTokens;
130
+ }
131
+ writeSettings({ proxy: proxyBlock });
115
132
  this.logger.log(`[proxy] HTTP server listening on ${url}`);
116
133
  return { port, url, token: this.token };
117
134
  }
@@ -131,11 +148,31 @@ class ProxyHttpServer {
131
148
  async _handleRequest(req, res) {
132
149
  const authHeader = req.headers['authorization'] || '';
133
150
  const provided = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
134
- const expBuf = Buffer.from(this.token || '', 'utf8');
135
151
  const provBuf = Buffer.from(provided, 'utf8');
136
- const valid = this.token &&
137
- provBuf.length === expBuf.length &&
138
- crypto.timingSafeEqual(provBuf, expBuf);
152
+ // Primary token plus any grace tokens from settings.json::proxy.previous_tokens.
153
+ // The grace list is the recovery path for the rare case where settings.json was
154
+ // wiped externally (logout, manual rm) while long-lived CC sessions still hold
155
+ // the pre-wipe token in their fork-time env. Operator writes the lost token into
156
+ // previous_tokens; once those sessions close, the operator can clear the array.
157
+ // Reading directly from settings.json (instead of an env shim) keeps the
158
+ // single source of truth on disk — no python bridge in the daemon hook.
159
+ // Defense in depth: even though start() filters non-strings before persisting,
160
+ // settings.json can be hand-edited between requests, so re-validate every read.
161
+ // A non-string slipping into Buffer.from below would throw ERR_INVALID_ARG_TYPE
162
+ // and unhandled-reject through the auth path.
163
+ const previous = readSettings().proxy?.previous_tokens;
164
+ const extras = Array.isArray(previous)
165
+ ? previous.filter((t) => typeof t === 'string' && t.length > 0)
166
+ : [];
167
+ const candidates = [this.token, ...extras].filter((t) => typeof t === 'string' && t.length > 0);
168
+ let valid = false;
169
+ for (const cand of candidates) {
170
+ const expBuf = Buffer.from(cand, 'utf8');
171
+ if (provBuf.length === expBuf.length && crypto.timingSafeEqual(provBuf, expBuf)) {
172
+ valid = true;
173
+ break;
174
+ }
175
+ }
139
176
  if (!valid) {
140
177
  return sendJson(res, 401, { error: 'Unauthorized' });
141
178
  }