@evomap/evolver 1.86.1 → 1.87.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/gep/genes.seed.json +44 -2
- package/index.js +67 -73
- package/package.json +3 -2
- package/skills/_meta/SKILL.md +41 -0
- package/skills/index.json +14 -0
- package/src/adapters/hookAdapter.js +17 -2
- package/src/adapters/scripts/evolver-session-start.js +2 -52
- package/src/adapters/scripts/evolver-signal-detect.js +0 -28
- 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 +1 -0
- 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 +7 -1
- 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/schemas/capsule.js +51 -1
- package/src/gep/selector.js +1 -1
- 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/index.js +226 -1
- package/src/proxy/router/messages_route.js +87 -9
- 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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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)
|
|
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 = {
|
|
314
|
+
module.exports = {
|
|
315
|
+
buildMessagesHandler,
|
|
316
|
+
DEFAULT_TIER_MODELS,
|
|
317
|
+
resolveTierModels,
|
|
318
|
+
parseClaudeId,
|
|
319
|
+
isIntraFamilyDowngrade,
|
|
320
|
+
};
|
package/src/proxy/server/http.js
CHANGED
|
@@ -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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
}
|