@evomap/evolver 1.89.17 → 1.89.18
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/index.js +89 -17
- package/package.json +1 -1
- package/src/adapters/claudeCode.js +36 -7
- package/src/adapters/hookAdapter.js +24 -0
- package/src/atp/atpExecute.js +3 -1
- package/src/atp/hubClient.js +5 -3
- package/src/atp/serviceHelper.js +3 -2
- 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/antiAbuseTelemetry.js +1 -1
- package/src/gep/autoDistillConv.js +1 -1
- package/src/gep/autoDistillLlm.js +1 -1
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/cliContracts.js +37 -1
- package/src/gep/contentHash.js +1 -1
- package/src/gep/conversationDistiller.js +1 -1
- package/src/gep/conversationSniffer.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/directoryClient.js +7 -3
- package/src/gep/envFingerprint.js +1 -1
- package/src/gep/epigenetics.js +1 -1
- package/src/gep/execBridge.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/oauthLogin.js +3 -5
- package/src/gep/openPRRegistry.js +1 -1
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/privacyClient.js +28 -10
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallInject.js +1 -1
- package/src/gep/recallVerifier.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/savingsCore.js +1 -1
- package/src/gep/selector.js +1 -1
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/skillPublisher.js +8 -3
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/taskReceiver.js +3 -2
- package/src/gep/tokenSavings.js +1 -1
- package/src/gep/validator/index.js +7 -2
- package/src/gep/validator/reporter.js +7 -2
- package/src/gep/validator/stakeBootstrap.js +7 -2
- package/src/gep/workspaceKeychain.js +1 -1
- package/src/proxy/clientSettings.js +405 -0
- package/src/proxy/extensions/traceControl.js +1 -1
- package/src/proxy/index.js +10 -5
- package/src/proxy/inject.js +1 -1
- package/src/proxy/lifecycle/manager.js +82 -16
- package/src/proxy/mailbox/state.js +207 -0
- package/src/proxy/mailbox/store.js +164 -64
- package/src/proxy/router/messages_route.js +4 -3
- package/src/proxy/server/http.js +40 -6
- package/src/proxy/sync/inbound.js +31 -12
- package/src/proxy/sync/outbound.js +157 -22
- package/src/proxy/trace/extractor.js +1 -1
- package/src/proxy/trace/usage.js +1 -1
|
@@ -3,28 +3,20 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
|
+
const {
|
|
7
|
+
PRIVATE_FILE_MODE,
|
|
8
|
+
bestEffortChmod,
|
|
9
|
+
ensurePrivateDir,
|
|
10
|
+
writePrivateFile,
|
|
11
|
+
readMailboxStateFile,
|
|
12
|
+
writeMergedMailboxStateFile,
|
|
13
|
+
} = require('./state');
|
|
6
14
|
|
|
7
15
|
const DEFAULT_CHANNEL = 'evomap-hub';
|
|
8
16
|
const SCHEMA_VERSION = 1;
|
|
9
17
|
const PROXY_PROTOCOL_VERSION = '0.1.0';
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
function bestEffortChmod(filePath, mode) {
|
|
14
|
-
try { fs.chmodSync(filePath, mode); } catch { /* best effort; no-op on Windows */ }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function ensurePrivateDir(dir) {
|
|
18
|
-
if (!fs.existsSync(dir)) {
|
|
19
|
-
fs.mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
20
|
-
}
|
|
21
|
-
bestEffortChmod(dir, PRIVATE_DIR_MODE);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function writePrivateFile(filePath, content) {
|
|
25
|
-
fs.writeFileSync(filePath, content, { encoding: 'utf8', mode: PRIVATE_FILE_MODE });
|
|
26
|
-
bestEffortChmod(filePath, PRIVATE_FILE_MODE);
|
|
27
|
-
}
|
|
18
|
+
const DEFAULT_MAX_JSONL_LINE_BYTES = 16 * 1024 * 1024;
|
|
19
|
+
const JSONL_READ_CHUNK_BYTES = 64 * 1024;
|
|
28
20
|
|
|
29
21
|
// Merge `fields` into `target` while stripping keys that can mutate the
|
|
30
22
|
// prototype chain. Mailbox rows are persisted as JSONL and rebuilt on
|
|
@@ -107,22 +99,117 @@ function safeParse(payload) {
|
|
|
107
99
|
// Windows NTFS provides the same atomicity guarantee for O_APPEND writes to
|
|
108
100
|
// regular files that POSIX local filesystems do, so the removal above is
|
|
109
101
|
// equally valid on Windows. No platform-specific code is needed here.
|
|
102
|
+
function jsonlLineForWrite(obj, opts = {}) {
|
|
103
|
+
const line = JSON.stringify(obj) + '\n';
|
|
104
|
+
const maxLineBytes = Math.max(1, Math.floor(Number(opts.maxLineBytes) || resolveMaxJsonlLineBytes()));
|
|
105
|
+
const lineBytes = Buffer.byteLength(line, 'utf8');
|
|
106
|
+
if (lineBytes > maxLineBytes) {
|
|
107
|
+
const err = new Error(`mailbox JSONL line is ${lineBytes} bytes; maximum is ${maxLineBytes} bytes`);
|
|
108
|
+
err.code = 'MAILBOX_JSONL_LINE_TOO_LARGE';
|
|
109
|
+
err.lineBytes = lineBytes;
|
|
110
|
+
err.maxLineBytes = maxLineBytes;
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
return line;
|
|
114
|
+
}
|
|
115
|
+
|
|
110
116
|
function appendLine(filePath, obj) {
|
|
111
|
-
|
|
117
|
+
const line = jsonlLineForWrite(obj);
|
|
118
|
+
fs.appendFileSync(filePath, line, { encoding: 'utf8', mode: PRIVATE_FILE_MODE });
|
|
112
119
|
bestEffortChmod(filePath, PRIVATE_FILE_MODE);
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
function resolveMaxJsonlLineBytes(env = process.env) {
|
|
123
|
+
const raw = Number(env.EVOMAP_MAILBOX_MAX_LINE_BYTES);
|
|
124
|
+
if (Number.isFinite(raw) && raw > 0) return Math.floor(raw);
|
|
125
|
+
return DEFAULT_MAX_JSONL_LINE_BYTES;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function forEachJsonLine(filePath, onRow, opts = {}) {
|
|
129
|
+
const stats = { parsed: 0, corrupt: 0, overlong: 0 };
|
|
130
|
+
if (!fs.existsSync(filePath)) return stats;
|
|
131
|
+
|
|
132
|
+
const maxLineBytes = Math.max(1, Math.floor(Number(opts.maxLineBytes) || resolveMaxJsonlLineBytes()));
|
|
133
|
+
const chunk = Buffer.allocUnsafe(JSONL_READ_CHUNK_BYTES);
|
|
134
|
+
let fd = null;
|
|
135
|
+
let parts = [];
|
|
136
|
+
let lineBytes = 0;
|
|
137
|
+
let dropping = false;
|
|
138
|
+
|
|
139
|
+
const resetLine = () => {
|
|
140
|
+
parts = [];
|
|
141
|
+
lineBytes = 0;
|
|
142
|
+
dropping = false;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const appendSegment = (segment) => {
|
|
146
|
+
if (!segment || segment.length === 0 || dropping) return;
|
|
147
|
+
if (lineBytes + segment.length > maxLineBytes) {
|
|
148
|
+
parts = [];
|
|
149
|
+
lineBytes = 0;
|
|
150
|
+
dropping = true;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
parts.push(Buffer.from(segment));
|
|
154
|
+
lineBytes += segment.length;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const finishLine = (hasNewline = false) => {
|
|
158
|
+
const totalLineBytes = lineBytes + (hasNewline ? 1 : 0);
|
|
159
|
+
if (dropping) {
|
|
160
|
+
stats.overlong += 1;
|
|
161
|
+
resetLine();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (totalLineBytes > maxLineBytes) {
|
|
165
|
+
stats.overlong += 1;
|
|
166
|
+
resetLine();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (lineBytes === 0) {
|
|
170
|
+
resetLine();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const trimmed = Buffer.concat(parts, lineBytes).toString('utf8').trim();
|
|
174
|
+
resetLine();
|
|
175
|
+
if (!trimmed) return;
|
|
176
|
+
try {
|
|
177
|
+
onRow(JSON.parse(trimmed));
|
|
178
|
+
stats.parsed += 1;
|
|
179
|
+
} catch {
|
|
180
|
+
stats.corrupt += 1;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
fd = fs.openSync(filePath, 'r');
|
|
186
|
+
while (true) {
|
|
187
|
+
const bytesRead = fs.readSync(fd, chunk, 0, chunk.length, null);
|
|
188
|
+
if (bytesRead <= 0) break;
|
|
189
|
+
let start = 0;
|
|
190
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
191
|
+
if (chunk[i] !== 0x0a) continue;
|
|
192
|
+
appendSegment(chunk.subarray(start, i));
|
|
193
|
+
finishLine(true);
|
|
194
|
+
start = i + 1;
|
|
195
|
+
}
|
|
196
|
+
if (start < bytesRead) appendSegment(chunk.subarray(start, bytesRead));
|
|
197
|
+
}
|
|
198
|
+
finishLine();
|
|
199
|
+
} catch (err) {
|
|
200
|
+
stats.read_failed = 1;
|
|
201
|
+
const detail = err && err.message ? err.message : String(err);
|
|
202
|
+
const wrapped = new Error(`failed to read mailbox JSONL ${filePath}: ${detail}`);
|
|
203
|
+
wrapped.code = 'MAILBOX_JSONL_READ_FAILED';
|
|
204
|
+
wrapped.stats = stats;
|
|
205
|
+
wrapped.cause = err;
|
|
206
|
+
throw wrapped;
|
|
207
|
+
} finally {
|
|
208
|
+
if (fd !== null) {
|
|
209
|
+
try { fs.closeSync(fd); } catch { /* ignore close errors */ }
|
|
210
|
+
}
|
|
124
211
|
}
|
|
125
|
-
return
|
|
212
|
+
return stats;
|
|
126
213
|
}
|
|
127
214
|
|
|
128
215
|
// --- In-memory index that backs JSONL persistence ---
|
|
@@ -150,19 +237,21 @@ class MailboxStore {
|
|
|
150
237
|
}
|
|
151
238
|
|
|
152
239
|
_loadState() {
|
|
153
|
-
|
|
154
|
-
if (fs.existsSync(this._stateFile)) {
|
|
155
|
-
this._state = JSON.parse(fs.readFileSync(this._stateFile, 'utf8'));
|
|
156
|
-
}
|
|
157
|
-
} catch {
|
|
158
|
-
this._state = {};
|
|
159
|
-
}
|
|
240
|
+
this._state = readMailboxStateFile(this._stateFile) || {};
|
|
160
241
|
const existingVersion = this._state._schema_version || 0;
|
|
242
|
+
const beforeMigrationState = { ...this._state };
|
|
161
243
|
if (existingVersion < SCHEMA_VERSION) {
|
|
162
244
|
this._runMigrations(existingVersion, SCHEMA_VERSION);
|
|
163
245
|
}
|
|
164
246
|
this._state._schema_version = SCHEMA_VERSION;
|
|
165
|
-
|
|
247
|
+
const updatedKeys = new Set(['_schema_version']);
|
|
248
|
+
for (const key of Object.keys(this._state)) {
|
|
249
|
+
if (this._state[key] !== beforeMigrationState[key]) updatedKeys.add(key);
|
|
250
|
+
}
|
|
251
|
+
for (const key of Object.keys(beforeMigrationState)) {
|
|
252
|
+
if (!Object.prototype.hasOwnProperty.call(this._state, key)) updatedKeys.add(key);
|
|
253
|
+
}
|
|
254
|
+
this._persistState(updatedKeys);
|
|
166
255
|
}
|
|
167
256
|
|
|
168
257
|
_runMigrations(fromVersion, toVersion) {
|
|
@@ -174,9 +263,7 @@ class MailboxStore {
|
|
|
174
263
|
}
|
|
175
264
|
}
|
|
176
265
|
|
|
177
|
-
_persistState() {
|
|
178
|
-
const dir = path.dirname(this._stateFile);
|
|
179
|
-
ensurePrivateDir(dir);
|
|
266
|
+
_persistState(updatedKeys) {
|
|
180
267
|
// Round-7 (§20.5): per-PID tmp path. Two evolver processes (daemon +
|
|
181
268
|
// ad-hoc CLI / proxy + loop) writing to the same `${stateFile}.tmp`
|
|
182
269
|
// would otherwise interleave: process B's writeFileSync truncates
|
|
@@ -186,20 +273,11 @@ class MailboxStore {
|
|
|
186
273
|
// load-bearing trigger for the "401-loop -> reauth backoff -> dead
|
|
187
274
|
// for 30 min..4 h" symptom this branch targets. Matches the
|
|
188
275
|
// precedent set by _persistNodeSecret in src/gep/a2aProtocol.js.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
// The window between unlink and rename is intentionally tiny; a crash in
|
|
195
|
-
// that window leaves the tmp file behind (recovered on next _persistState).
|
|
196
|
-
if (process.platform === 'win32') {
|
|
197
|
-
try { fs.unlinkSync(this._stateFile); } catch (e) {
|
|
198
|
-
if (e.code !== 'ENOENT') throw e;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
fs.renameSync(tmp, this._stateFile);
|
|
202
|
-
bestEffortChmod(this._stateFile, PRIVATE_FILE_MODE);
|
|
276
|
+
//
|
|
277
|
+
// Use the shared merge helper so stale long-running stores cannot
|
|
278
|
+
// resurrect an older node_secret tuple after another path rotates or clears
|
|
279
|
+
// it directly on disk.
|
|
280
|
+
this._state = writeMergedMailboxStateFile(this._stateFile, this._state, updatedKeys);
|
|
203
281
|
}
|
|
204
282
|
|
|
205
283
|
_rebuildIndex() {
|
|
@@ -208,17 +286,20 @@ class MailboxStore {
|
|
|
208
286
|
this._inbound = [];
|
|
209
287
|
|
|
210
288
|
const TERMINAL = new Set(['synced', 'delivered', 'failed', 'rejected']);
|
|
211
|
-
const
|
|
212
|
-
for (const rawRow of rows) {
|
|
289
|
+
const stats = forEachJsonLine(this._messagesFile, (rawRow) => {
|
|
213
290
|
const row = sanitizeRow(rawRow);
|
|
291
|
+
if (!row || typeof row !== 'object') return;
|
|
214
292
|
if (row._op === 'update') {
|
|
215
293
|
const existing = this._messages.get(row.id);
|
|
216
294
|
if (existing) safeAssign(existing, row.fields);
|
|
217
|
-
|
|
295
|
+
return;
|
|
218
296
|
}
|
|
297
|
+
if (!row.id) return;
|
|
219
298
|
this._messages.set(row.id, row);
|
|
220
|
-
}
|
|
299
|
+
});
|
|
300
|
+
this._lastRebuildStats = stats;
|
|
221
301
|
for (const [id, msg] of this._messages) {
|
|
302
|
+
if (!msg || typeof msg !== 'object') continue;
|
|
222
303
|
if (TERMINAL.has(msg.status)) continue;
|
|
223
304
|
if (msg.direction === 'outbound') this._outbound.push(id);
|
|
224
305
|
else if (msg.direction === 'inbound') this._inbound.push(id);
|
|
@@ -430,8 +511,9 @@ class MailboxStore {
|
|
|
430
511
|
}
|
|
431
512
|
|
|
432
513
|
setCursor(key, value) {
|
|
433
|
-
|
|
434
|
-
this.
|
|
514
|
+
const stateKey = `cursor:${key}`;
|
|
515
|
+
this._state[stateKey] = value;
|
|
516
|
+
this._persistState([stateKey]);
|
|
435
517
|
}
|
|
436
518
|
|
|
437
519
|
getState(key) {
|
|
@@ -441,7 +523,20 @@ class MailboxStore {
|
|
|
441
523
|
|
|
442
524
|
setState(key, value) {
|
|
443
525
|
this._state[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
|
444
|
-
this._persistState();
|
|
526
|
+
this._persistState([key]);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
setNodeSecretState({ secret = '', version = '', source = '', envSuppressed = '' } = {}) {
|
|
530
|
+
this._state.node_secret = secret;
|
|
531
|
+
this._state.node_secret_version = version ? String(version) : '';
|
|
532
|
+
this._state.node_secret_source = source;
|
|
533
|
+
this._state.node_secret_env_suppressed = envSuppressed;
|
|
534
|
+
this._persistState([
|
|
535
|
+
'node_secret',
|
|
536
|
+
'node_secret_version',
|
|
537
|
+
'node_secret_source',
|
|
538
|
+
'node_secret_env_suppressed',
|
|
539
|
+
]);
|
|
445
540
|
}
|
|
446
541
|
|
|
447
542
|
// --- Compaction (reduces JSONL file size by rewriting only current state) ---
|
|
@@ -457,11 +552,16 @@ class MailboxStore {
|
|
|
457
552
|
}
|
|
458
553
|
entries.sort((a, b) => a.created_at - b.created_at);
|
|
459
554
|
|
|
555
|
+
const lines = entries.map((msg) => jsonlLineForWrite(msg));
|
|
556
|
+
|
|
460
557
|
const fd = fs.openSync(tmpFile, 'w', PRIVATE_FILE_MODE);
|
|
461
|
-
|
|
462
|
-
|
|
558
|
+
try {
|
|
559
|
+
for (const line of lines) {
|
|
560
|
+
fs.writeSync(fd, line);
|
|
561
|
+
}
|
|
562
|
+
} finally {
|
|
563
|
+
fs.closeSync(fd);
|
|
463
564
|
}
|
|
464
|
-
fs.closeSync(fd);
|
|
465
565
|
bestEffortChmod(tmpFile, PRIVATE_FILE_MODE);
|
|
466
566
|
// Windows: renameSync throws EPERM when the destination already exists.
|
|
467
567
|
// Remove it first so the swap succeeds on all platforms.
|
|
@@ -144,8 +144,8 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled, traceStor
|
|
|
144
144
|
return async ({ body, headers }) => {
|
|
145
145
|
const inboundHeaders = headers || {};
|
|
146
146
|
// x-api-key is satisfied by either the inbound header OR a proxy-side
|
|
147
|
-
// ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN env
|
|
148
|
-
// _proxyAnthropic). The proxy server itself has already auth-checked
|
|
147
|
+
// EVOMAP_ANTHROPIC_API_KEY / ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN env
|
|
148
|
+
// (token mediation, see _proxyAnthropic). The proxy server itself has already auth-checked
|
|
149
149
|
// `Authorization: Bearer <proxy_token>` before reaching this handler.
|
|
150
150
|
//
|
|
151
151
|
// Bedrock upstream uses SigV4 with AWS_* env, so neither inbound
|
|
@@ -161,7 +161,8 @@ function buildMessagesHandler({ anthropicProxy, logger, routerEnabled, traceStor
|
|
|
161
161
|
if (upstreamMode !== 'bedrock') {
|
|
162
162
|
const hasInboundKey = !!inboundHeaders['x-api-key'];
|
|
163
163
|
const hasProxyEnvCreds = !!(
|
|
164
|
-
process.env.
|
|
164
|
+
process.env.EVOMAP_ANTHROPIC_API_KEY
|
|
165
|
+
|| process.env.ANTHROPIC_API_KEY
|
|
165
166
|
|| process.env.EVOMAP_ANTHROPIC_AUTH_TOKEN
|
|
166
167
|
|| (process.env.EVOMAP_PROXY_AUTO_INJECTED === '1' ? '' : process.env.ANTHROPIC_AUTH_TOKEN)
|
|
167
168
|
);
|
package/src/proxy/server/http.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const http = require('http');
|
|
5
5
|
const { writeSettings, readSettings, clearSettings, clearIfStale } = require('./settings');
|
|
6
|
+
const {
|
|
7
|
+
isValidReusableProxyToken,
|
|
8
|
+
readReusableClientProxyToken,
|
|
9
|
+
syncClaudeProxySettings,
|
|
10
|
+
} = require('../clientSettings');
|
|
6
11
|
|
|
7
12
|
const MAX_PORT_ATTEMPTS = 100;
|
|
8
13
|
const DEFAULT_PORT = 19820;
|
|
@@ -93,13 +98,14 @@ function tryListen(server, port) {
|
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
class ProxyHttpServer {
|
|
96
|
-
constructor(routes, { port, logger } = {}) {
|
|
101
|
+
constructor(routes, { port, logger, clientSettings } = {}) {
|
|
97
102
|
this.routes = routes;
|
|
98
103
|
this.basePort = port || Number(process.env.EVOMAP_PROXY_PORT) || DEFAULT_PORT;
|
|
99
104
|
this.actualPort = null;
|
|
100
105
|
this.logger = logger || console;
|
|
101
106
|
this.server = null;
|
|
102
107
|
this.token = null;
|
|
108
|
+
this.clientSettings = clientSettings || null;
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
async start() {
|
|
@@ -109,16 +115,22 @@ class ProxyHttpServer {
|
|
|
109
115
|
// already exported into long-lived shells (the .bashrc auto-source only
|
|
110
116
|
// runs once per terminal).
|
|
111
117
|
const priorProxy = readSettings().proxy || {};
|
|
112
|
-
const previous =
|
|
118
|
+
const previous = isValidReusableProxyToken(priorProxy.token) ? priorProxy.token.trim() : null;
|
|
113
119
|
// settings.json is operator-edited; previous_tokens may contain non-strings
|
|
114
120
|
// (numbers, booleans, objects) that would later crash Buffer.from(cand, 'utf8')
|
|
115
121
|
// in _handleRequest as ERR_INVALID_ARG_TYPE — an unhandled rejection that
|
|
116
122
|
// takes the daemon down under default --unhandled-rejections=throw.
|
|
117
123
|
const priorPreviousTokens = Array.isArray(priorProxy.previous_tokens)
|
|
118
|
-
? priorProxy.previous_tokens
|
|
124
|
+
? priorProxy.previous_tokens
|
|
125
|
+
.map((t) => (typeof t === 'string' ? t.trim() : ''))
|
|
126
|
+
.filter((t) => isValidReusableProxyToken(t))
|
|
119
127
|
: [];
|
|
120
128
|
clearIfStale();
|
|
121
|
-
|
|
129
|
+
const clientSettingsOpts = this.clientSettings || {};
|
|
130
|
+
const clientToken = this.clientSettings
|
|
131
|
+
? readReusableClientProxyToken({ ...clientSettingsOpts, port: this.basePort })
|
|
132
|
+
: null;
|
|
133
|
+
this.token = previous || clientToken || crypto.randomBytes(32).toString('hex');
|
|
122
134
|
this._priorPreviousTokens = priorPreviousTokens;
|
|
123
135
|
this.server = http.createServer((req, res) => this._handleRequest(req, res));
|
|
124
136
|
|
|
@@ -138,6 +150,24 @@ class ProxyHttpServer {
|
|
|
138
150
|
proxyBlock.previous_tokens = this._priorPreviousTokens;
|
|
139
151
|
}
|
|
140
152
|
writeSettings({ proxy: proxyBlock });
|
|
153
|
+
if (this.clientSettings) {
|
|
154
|
+
try {
|
|
155
|
+
const syncResult = syncClaudeProxySettings({
|
|
156
|
+
...clientSettingsOpts,
|
|
157
|
+
url,
|
|
158
|
+
port: this.basePort,
|
|
159
|
+
token: this.token,
|
|
160
|
+
runtimeEnv: process.env,
|
|
161
|
+
});
|
|
162
|
+
if (syncResult.synced && syncResult.changed) {
|
|
163
|
+
this.logger.log(`[proxy] Synced Claude client settings at ${syncResult.file}`);
|
|
164
|
+
} else if (syncResult.reason === 'invalid_settings_json') {
|
|
165
|
+
this.logger.warn?.(`[proxy] Skipped Claude client settings sync because ${syncResult.file} is not valid JSON`);
|
|
166
|
+
}
|
|
167
|
+
} catch (err) {
|
|
168
|
+
this.logger.warn?.('[proxy] Claude client settings sync failed: ' + (err && err.message ? err.message : err));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
141
171
|
this.logger.log(`[proxy] HTTP server listening on ${url}`);
|
|
142
172
|
return { port, url, token: this.token };
|
|
143
173
|
}
|
|
@@ -171,9 +201,13 @@ class ProxyHttpServer {
|
|
|
171
201
|
// and unhandled-reject through the auth path.
|
|
172
202
|
const previous = readSettings().proxy?.previous_tokens;
|
|
173
203
|
const extras = Array.isArray(previous)
|
|
174
|
-
? previous
|
|
204
|
+
? previous
|
|
205
|
+
.map((t) => (typeof t === 'string' ? t.trim() : ''))
|
|
206
|
+
.filter((t) => isValidReusableProxyToken(t))
|
|
175
207
|
: [];
|
|
176
|
-
const candidates = [this.token, ...extras]
|
|
208
|
+
const candidates = [this.token, ...extras]
|
|
209
|
+
.filter((t) => isValidReusableProxyToken(t))
|
|
210
|
+
.map((t) => t.trim());
|
|
177
211
|
let valid = false;
|
|
178
212
|
for (const cand of candidates) {
|
|
179
213
|
const expBuf = Buffer.from(cand, 'utf8');
|
|
@@ -16,6 +16,18 @@ const {
|
|
|
16
16
|
const DEFAULT_POLL_INTERVAL_ACTIVE = 10_000;
|
|
17
17
|
const DEFAULT_POLL_INTERVAL_IDLE = 60_000;
|
|
18
18
|
|
|
19
|
+
function toInboundStoreMessage(m, channel) {
|
|
20
|
+
return {
|
|
21
|
+
id: m.id,
|
|
22
|
+
type: m.type,
|
|
23
|
+
payload: m.payload,
|
|
24
|
+
channel,
|
|
25
|
+
priority: m.priority || 'normal',
|
|
26
|
+
refId: m.ref_id,
|
|
27
|
+
expiresAt: m.expires_at,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
class InboundSync {
|
|
20
32
|
constructor({ store, hubUrl, getHeaders, logger }) {
|
|
21
33
|
this.store = store;
|
|
@@ -86,26 +98,33 @@ class InboundSync {
|
|
|
86
98
|
|
|
87
99
|
const data = await readHubResponseJson(res);
|
|
88
100
|
const messages = data.messages || [];
|
|
101
|
+
let stored = 0;
|
|
102
|
+
let dropped = 0;
|
|
89
103
|
|
|
90
104
|
if (messages.length > 0) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
for (const m of messages) {
|
|
106
|
+
try {
|
|
107
|
+
this.store.writeInbound(toInboundStoreMessage(m, channel));
|
|
108
|
+
stored++;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (err && err.code === 'MAILBOX_JSONL_LINE_TOO_LARGE') {
|
|
111
|
+
dropped++;
|
|
112
|
+
const msgId = m && m.id ? String(m.id) : '(missing id)';
|
|
113
|
+
this.logger.warn?.(`[inbound] dropped oversized inbound message ${msgId}: ${err.message}`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
if (data.next_cursor) {
|
|
105
122
|
this.store.setCursor(cursorKey, data.next_cursor);
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
|
|
125
|
+
const result = { received: stored, cursor: data.next_cursor || cursor };
|
|
126
|
+
if (dropped > 0) result.dropped = dropped;
|
|
127
|
+
return result;
|
|
109
128
|
} catch (err) {
|
|
110
129
|
if (err instanceof AuthError) throw err;
|
|
111
130
|
if (isHubUnreachableError(err)) {
|