@evomap/evolver-proxy 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 (82) hide show
  1. package/README.md +49 -0
  2. package/dist/bin/envFile.d.ts +10 -0
  3. package/dist/bin/envFile.js +68 -0
  4. package/dist/bin/evolver-llm-proxy.d.ts +2 -0
  5. package/dist/bin/evolver-llm-proxy.js +111 -0
  6. package/dist/bin/evolver-proxy.d.ts +83 -0
  7. package/dist/bin/evolver-proxy.js +511 -0
  8. package/dist/bin/proxySettings.d.ts +15 -0
  9. package/dist/bin/proxySettings.js +84 -0
  10. package/dist/bin/proxyStorePath.d.ts +1 -0
  11. package/dist/bin/proxyStorePath.js +16 -0
  12. package/dist/daemon/atpConsent.d.ts +13 -0
  13. package/dist/daemon/atpConsent.js +60 -0
  14. package/dist/daemon/ipcConfig.d.ts +1 -0
  15. package/dist/daemon/ipcConfig.js +13 -0
  16. package/dist/daemon/proxyDaemon.d.ts +191 -0
  17. package/dist/daemon/proxyDaemon.js +1015 -0
  18. package/dist/daemon/selectHub.d.ts +16 -0
  19. package/dist/daemon/selectHub.js +30 -0
  20. package/dist/index.d.ts +8 -0
  21. package/dist/index.js +8 -0
  22. package/dist/lifecycle/deployGuard.d.ts +46 -0
  23. package/dist/lifecycle/deployGuard.js +53 -0
  24. package/dist/lifecycle/legacyNodeId.d.ts +96 -0
  25. package/dist/lifecycle/legacyNodeId.js +163 -0
  26. package/dist/lifecycle/manager.d.ts +106 -0
  27. package/dist/lifecycle/manager.js +390 -0
  28. package/dist/llm/bodyCapture.d.ts +31 -0
  29. package/dist/llm/bodyCapture.js +293 -0
  30. package/dist/llm/index.d.ts +3 -0
  31. package/dist/llm/index.js +3 -0
  32. package/dist/llm/server.d.ts +35 -0
  33. package/dist/llm/server.js +359 -0
  34. package/dist/llm/traceBackfill.d.ts +53 -0
  35. package/dist/llm/traceBackfill.js +525 -0
  36. package/dist/llm/traceConfig.d.ts +7 -0
  37. package/dist/llm/traceConfig.js +44 -0
  38. package/dist/llm/traceControl.d.ts +16 -0
  39. package/dist/llm/traceControl.js +85 -0
  40. package/dist/llm/traceEnvelope.d.ts +75 -0
  41. package/dist/llm/traceEnvelope.js +286 -0
  42. package/dist/llm/traceSink.d.ts +61 -0
  43. package/dist/llm/traceSink.js +278 -0
  44. package/dist/llm/traceUploadPayload.d.ts +14 -0
  45. package/dist/llm/traceUploadPayload.js +27 -0
  46. package/dist/llm/upstream.d.ts +68 -0
  47. package/dist/llm/upstream.js +491 -0
  48. package/dist/private/adapterLoader.d.ts +46 -0
  49. package/dist/private/adapterLoader.js +62 -0
  50. package/dist/private/privateRuntimeSmokeOptions.d.ts +15 -0
  51. package/dist/private/privateRuntimeSmokeOptions.js +132 -0
  52. package/dist/router/cachePassthrough.d.ts +3 -0
  53. package/dist/router/cachePassthrough.js +13 -0
  54. package/dist/router/features.d.ts +9 -0
  55. package/dist/router/features.js +52 -0
  56. package/dist/router/index.d.ts +6 -0
  57. package/dist/router/index.js +6 -0
  58. package/dist/router/messagesRoute.d.ts +138 -0
  59. package/dist/router/messagesRoute.js +753 -0
  60. package/dist/router/modelRouter.d.ts +49 -0
  61. package/dist/router/modelRouter.js +66 -0
  62. package/dist/router/providerRoutes.d.ts +42 -0
  63. package/dist/router/providerRoutes.js +1579 -0
  64. package/dist/router/sseScan.d.ts +56 -0
  65. package/dist/router/sseScan.js +543 -0
  66. package/dist/selfUpdate/executor.d.ts +90 -0
  67. package/dist/selfUpdate/executor.js +179 -0
  68. package/dist/selfUpdate/failureCodes.d.ts +33 -0
  69. package/dist/selfUpdate/failureCodes.js +84 -0
  70. package/dist/selfUpdate/index.d.ts +5 -0
  71. package/dist/selfUpdate/index.js +5 -0
  72. package/dist/selfUpdate/lastUpdate.d.ts +43 -0
  73. package/dist/selfUpdate/lastUpdate.js +195 -0
  74. package/dist/selfUpdate/policy.d.ts +3 -0
  75. package/dist/selfUpdate/policy.js +9 -0
  76. package/dist/selfUpdate/releaseBinary.d.ts +56 -0
  77. package/dist/selfUpdate/releaseBinary.js +498 -0
  78. package/dist/selfUpdate/version.d.ts +2 -0
  79. package/dist/selfUpdate/version.js +14 -0
  80. package/dist/sync/engine.d.ts +65 -0
  81. package/dist/sync/engine.js +461 -0
  82. package/package.json +41 -0
@@ -0,0 +1,359 @@
1
+ // The local HTTP shell that puts the /v1/messages handler on the wire (the listen half v1 kept in
2
+ // proxy/server/http.js). Binding model: loopback only, bearer-token self-auth (token mediation — the client
3
+ // exports ANTHROPIC_BASE_URL=http://127.0.0.1:<port> and ANTHROPIC_AUTH_TOKEN=<proxy token>; the upstream
4
+ // factory strips that header and substitutes real credentials). Loopback is NOT a trust boundary on a shared
5
+ // host (other local users, container neighbors, postinstall scripts), hence the mandatory token.
6
+ import { createServer } from 'node:http';
7
+ import { timingSafeEqual } from 'node:crypto';
8
+ export const DEFAULT_LLM_PORT = 19821; // one above the mailbox IPC default — the two daemons co-exist
9
+ const MAX_PORT_ATTEMPTS = 100;
10
+ /** /v1/messages bodies legitimately reach tens of MiB (long contexts); the 1 MiB IPC-style cap would break
11
+ * real clients. Still bounded — an unauthenticated local writer must not be able to balloon memory. */
12
+ export const DEFAULT_LLM_MAX_BODY_BYTES = 32 * 1024 * 1024;
13
+ const SENSITIVE_QUERY_PARAMS = new Set([
14
+ 'key',
15
+ 'api_key',
16
+ 'apikey',
17
+ 'access_token',
18
+ 'token',
19
+ 'authorization',
20
+ 'auth',
21
+ 'client_secret',
22
+ 'refresh_token',
23
+ 'id_token',
24
+ 'signature',
25
+ 'sig',
26
+ ]);
27
+ function resolveMaxBodyBytes(opts, env) {
28
+ if (typeof opts.maxBodyBytes === 'number' && opts.maxBodyBytes > 0)
29
+ return Math.floor(opts.maxBodyBytes);
30
+ const raw = Number(env['EVOLVER_LLM_MAX_BODY_BYTES']);
31
+ if (Number.isFinite(raw) && raw > 0)
32
+ return Math.floor(raw);
33
+ return DEFAULT_LLM_MAX_BODY_BYTES;
34
+ }
35
+ function safeExtraHeaders(extraHeaders = {}) {
36
+ const out = {};
37
+ for (const [name, value] of Object.entries(extraHeaders)) {
38
+ if (value === undefined || value === null)
39
+ continue;
40
+ const lower = name.toLowerCase();
41
+ if (lower === 'content-length' || lower === 'content-type' || lower === 'transfer-encoding' || lower === 'connection')
42
+ continue;
43
+ if (/[\r\n]/.test(value))
44
+ continue;
45
+ out[name] = value;
46
+ }
47
+ return out;
48
+ }
49
+ function sendJson(res, status, body, extraHeaders = {}) {
50
+ const payload = JSON.stringify(body);
51
+ res.writeHead(status, {
52
+ ...safeExtraHeaders(extraHeaders),
53
+ 'Content-Type': 'application/json',
54
+ 'Content-Length': Buffer.byteLength(payload),
55
+ });
56
+ res.end(payload);
57
+ }
58
+ function readBody(req, maxBytes) {
59
+ return new Promise((resolve, reject) => {
60
+ const declared = Number(req.headers['content-length']);
61
+ if (Number.isFinite(declared) && declared > maxBytes) {
62
+ reject(Object.assign(new Error('Request body too large'), { statusCode: 413 }));
63
+ return;
64
+ }
65
+ const chunks = [];
66
+ let received = 0;
67
+ let settled = false;
68
+ const fail = (err) => {
69
+ if (settled)
70
+ return;
71
+ settled = true;
72
+ try {
73
+ req.destroy();
74
+ }
75
+ catch { /* already gone */ }
76
+ reject(err);
77
+ };
78
+ req.on('data', (c) => {
79
+ if (settled)
80
+ return;
81
+ received += c.length;
82
+ if (received > maxBytes) {
83
+ fail(Object.assign(new Error('Request body too large'), { statusCode: 413 }));
84
+ return;
85
+ }
86
+ chunks.push(c);
87
+ });
88
+ req.on('end', () => {
89
+ if (settled)
90
+ return;
91
+ settled = true;
92
+ const raw = Buffer.concat(chunks).toString();
93
+ if (!raw) {
94
+ resolve({});
95
+ return;
96
+ }
97
+ try {
98
+ const parsed = JSON.parse(raw);
99
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
100
+ resolve(parsed);
101
+ else
102
+ reject(Object.assign(new Error('Body must be a JSON object'), { statusCode: 400 }));
103
+ }
104
+ catch {
105
+ reject(Object.assign(new Error('Invalid JSON body'), { statusCode: 400 }));
106
+ }
107
+ });
108
+ req.on('error', fail);
109
+ });
110
+ }
111
+ function lowerHeaders(req) {
112
+ const out = {};
113
+ for (const [k, v] of Object.entries(req.headers))
114
+ out[k.toLowerCase()] = Array.isArray(v) ? v.join(', ') : v;
115
+ return out;
116
+ }
117
+ function redactedRequestTarget(rawUrl, actualPort) {
118
+ try {
119
+ const url = new URL(rawUrl ?? '/', `http://127.0.0.1:${actualPort ?? 0}`);
120
+ const query = new URLSearchParams();
121
+ for (const [name, value] of url.searchParams.entries()) {
122
+ query.append(name, SENSITIVE_QUERY_PARAMS.has(name.toLowerCase()) ? 'REDACTED' : value);
123
+ }
124
+ const qs = query.toString();
125
+ return `${url.pathname}${qs ? `?${qs}` : ''}`;
126
+ }
127
+ catch {
128
+ return '?';
129
+ }
130
+ }
131
+ function routeTable(opts) {
132
+ return {
133
+ 'POST /v1/messages': opts.handler,
134
+ ...(opts.routes ?? {}),
135
+ };
136
+ }
137
+ function matchRoute(routes, method, pathname) {
138
+ const direct = routes[`${method} ${pathname}`];
139
+ if (direct)
140
+ return { handler: direct, params: {} };
141
+ const actual = pathname.split('/').filter(Boolean);
142
+ for (const [key, handler] of Object.entries(routes)) {
143
+ const [routeMethod, pattern] = key.split(' ', 2);
144
+ if (routeMethod !== method || !pattern)
145
+ continue;
146
+ const expected = pattern.split('/').filter(Boolean);
147
+ if (expected.length !== actual.length)
148
+ continue;
149
+ const params = {};
150
+ let ok = true;
151
+ for (let i = 0; i < expected.length; i++) {
152
+ const part = expected[i];
153
+ const got = actual[i];
154
+ if (part.startsWith(':')) {
155
+ params[part.slice(1)] = got;
156
+ }
157
+ else if (part !== got) {
158
+ ok = false;
159
+ break;
160
+ }
161
+ }
162
+ if (ok)
163
+ return { handler, params };
164
+ }
165
+ return null;
166
+ }
167
+ export class LlmProxyServer {
168
+ opts;
169
+ server;
170
+ actualPort = null;
171
+ log;
172
+ env;
173
+ maxBodyBytes;
174
+ constructor(opts) {
175
+ this.opts = opts;
176
+ if (!opts.token)
177
+ throw new Error('LlmProxyServer requires a non-empty token');
178
+ this.log = opts.logger ?? console;
179
+ this.env = opts.env ?? process.env;
180
+ this.maxBodyBytes = resolveMaxBodyBytes(opts, this.env);
181
+ }
182
+ authed(req) {
183
+ const header = req.headers['authorization'] ?? '';
184
+ const provided = typeof header === 'string' && header.startsWith('Bearer ') ? header.slice(7) : '';
185
+ const a = Buffer.from(provided, 'utf8');
186
+ const b = Buffer.from(this.opts.token, 'utf8');
187
+ return a.length === b.length && timingSafeEqual(a, b);
188
+ }
189
+ async start() {
190
+ if (this.server)
191
+ throw new Error('LlmProxyServer already started');
192
+ const host = this.opts.host ?? '127.0.0.1';
193
+ const basePort = this.opts.port ?? Number(this.env['EVOLVER_LLM_PORT'] || DEFAULT_LLM_PORT);
194
+ const server = createServer((req, res) => { void this.handle(req, res); });
195
+ const tryListen = (port) => new Promise((resolve, reject) => {
196
+ server.once('error', (err) => {
197
+ if (err.code === 'EADDRINUSE')
198
+ resolve(false);
199
+ else
200
+ reject(err);
201
+ });
202
+ server.listen(port, host, () => resolve(true));
203
+ });
204
+ let port = basePort;
205
+ for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {
206
+ if (await tryListen(port)) {
207
+ const addr = server.address();
208
+ this.actualPort = typeof addr === 'object' && addr ? addr.port : port;
209
+ this.server = server;
210
+ const url = `http://${host}:${this.actualPort}`;
211
+ this.log.log?.(`[evolver-llm-proxy] listening on ${url}`);
212
+ return { port: this.actualPort, url };
213
+ }
214
+ if (basePort === 0)
215
+ break; // kernel-assigned can't collide; a failure here is real
216
+ port++;
217
+ }
218
+ throw new Error(`LlmProxyServer: no free port after ${MAX_PORT_ATTEMPTS} attempts from ${basePort}`);
219
+ }
220
+ async stop() {
221
+ if (!this.server)
222
+ return;
223
+ const s = this.server;
224
+ this.server = undefined;
225
+ // close() alone waits for keep-alive sockets (undici/fetch pools them), which can park shutdown
226
+ // indefinitely; drop them so stop() always completes.
227
+ s.closeAllConnections();
228
+ await new Promise((resolve) => { s.close(() => resolve()); });
229
+ }
230
+ async handle(req, res) {
231
+ try {
232
+ const url = new URL(req.url ?? '/', `http://127.0.0.1:${this.actualPort}`);
233
+ if (req.method === 'GET' && url.pathname === '/healthz') {
234
+ sendJson(res, 200, { ok: true });
235
+ return;
236
+ }
237
+ if (!this.authed(req)) {
238
+ sendJson(res, 401, { error: 'Unauthorized' });
239
+ return;
240
+ }
241
+ const method = req.method ?? 'GET';
242
+ const matched = matchRoute(routeTable(this.opts), method, url.pathname);
243
+ if (!matched) {
244
+ sendJson(res, 404, { error: 'Not found', path: url.pathname });
245
+ return;
246
+ }
247
+ const body = method === 'POST' || method === 'PUT' || method === 'PATCH'
248
+ ? await readBody(req, this.maxBodyBytes)
249
+ : {};
250
+ const request = {
251
+ body,
252
+ headers: lowerHeaders(req),
253
+ params: matched.params,
254
+ query: Object.fromEntries(url.searchParams.entries()),
255
+ };
256
+ const result = await matched.handler(request);
257
+ if (result.stream) {
258
+ await this.relayStream(res, result);
259
+ return;
260
+ }
261
+ sendJson(res, result.status || 200, result.body ?? {}, result.headers);
262
+ }
263
+ catch (err) {
264
+ const sc = err.statusCode;
265
+ const status = typeof sc === 'number' ? sc : 500;
266
+ this.log.warn?.(`[evolver-llm-proxy] ${req.method ?? '?'} ${redactedRequestTarget(req.url, this.actualPort)} → ${status}: ${err instanceof Error ? err.message : String(err)}`);
267
+ if (res.headersSent) {
268
+ try {
269
+ res.end();
270
+ }
271
+ catch { /* socket already gone */ }
272
+ }
273
+ else {
274
+ sendJson(res, status, { error: err instanceof Error ? err.message : 'Internal error' });
275
+ }
276
+ }
277
+ }
278
+ // SSE / passthrough relay (port of v1 ProxyHttpServer._streamResponse). The client closing mid-stream is
279
+ // routine (Ctrl-C, network blip); without watching `close`, awaiting `drain` on a destroyed socket hangs
280
+ // this coroutine forever AND pins the upstream fetch body open — a real upstream socket leak. Web
281
+ // ReadableStream upstreams are read via an explicit reader so cancellation propagates; async iterables
282
+ // (incl. the trace tee) fall back to for-await.
283
+ async relayStream(res, result) {
284
+ const headers = {
285
+ 'Content-Type': 'text/event-stream',
286
+ 'Cache-Control': 'no-cache',
287
+ Connection: 'keep-alive',
288
+ ...(result.headers ?? {}),
289
+ };
290
+ res.writeHead(result.status || 200, headers);
291
+ const stream = result.stream;
292
+ const reader = stream && typeof stream.getReader === 'function'
293
+ ? stream.getReader()
294
+ : null;
295
+ let clientGone = false;
296
+ const onClose = () => {
297
+ clientGone = true;
298
+ if (reader) {
299
+ reader.cancel().catch(() => { });
300
+ }
301
+ else if (stream && typeof stream.return === 'function') {
302
+ void stream.return(undefined).catch(() => undefined);
303
+ }
304
+ };
305
+ res.once('close', onClose);
306
+ const awaitBackpressure = () => new Promise((resolve) => {
307
+ let settled = false;
308
+ const onDrain = () => {
309
+ if (settled)
310
+ return;
311
+ settled = true;
312
+ res.off('close', onCloseInner);
313
+ resolve();
314
+ };
315
+ const onCloseInner = () => {
316
+ if (settled)
317
+ return;
318
+ settled = true;
319
+ res.off('drain', onDrain);
320
+ resolve();
321
+ };
322
+ res.once('drain', onDrain);
323
+ res.once('close', onCloseInner);
324
+ });
325
+ const write = (chunk) => res.write(typeof chunk === 'string' || chunk instanceof Uint8Array ? chunk : String(chunk));
326
+ try {
327
+ if (reader) {
328
+ for (;;) {
329
+ const { value, done } = await reader.read();
330
+ if (done || clientGone)
331
+ break;
332
+ if (!write(value)) {
333
+ await awaitBackpressure();
334
+ if (clientGone)
335
+ break;
336
+ }
337
+ }
338
+ }
339
+ else if (stream && typeof stream[Symbol.asyncIterator] === 'function') {
340
+ for await (const chunk of stream) {
341
+ if (clientGone)
342
+ break;
343
+ if (!write(chunk)) {
344
+ await awaitBackpressure();
345
+ if (clientGone)
346
+ break;
347
+ }
348
+ }
349
+ }
350
+ }
351
+ finally {
352
+ res.off('close', onClose);
353
+ try {
354
+ res.end();
355
+ }
356
+ catch { /* socket may already be destroyed */ }
357
+ }
358
+ }
359
+ }
@@ -0,0 +1,53 @@
1
+ import { mailbox } from '@evomap/evolver-core';
2
+ import type { HubDecryptableTraceEnvelope } from './traceEnvelope.js';
3
+ import { type ProxyTraceUploadPayload } from './traceUploadPayload.js';
4
+ export interface TraceUploadMailboxStore {
5
+ send(e: mailbox.Envelope): {
6
+ receiptId: string;
7
+ stored: boolean;
8
+ };
9
+ countPending?(handler?: string, runtimeNamespace?: string): number;
10
+ getState?(key: string): string | undefined;
11
+ setState?(key: string, value: string): void;
12
+ hasMessageWithIdempotencyKey?(idempotencyKey: string): boolean;
13
+ hasMessageWithPayload?(type: string, payload: unknown): boolean;
14
+ }
15
+ export interface TraceBackfillStats {
16
+ scanned: number;
17
+ queued: number;
18
+ duplicates: number;
19
+ skipped: number;
20
+ files: number;
21
+ reasons: Record<string, number>;
22
+ }
23
+ type TraceDecryptStateStore = Pick<TraceUploadMailboxStore, 'getState'>;
24
+ export declare function isHubDecryptableTraceEnvelope(record: unknown): record is HubDecryptableTraceEnvelope;
25
+ export declare function traceUploadBlockedReason(record: HubDecryptableTraceEnvelope): string | undefined;
26
+ export declare function normalizeProxyTraceOutboundPayload(payload: unknown, env?: NodeJS.ProcessEnv, store?: TraceDecryptStateStore): {
27
+ ok: true;
28
+ payload: ProxyTraceUploadPayload;
29
+ } | {
30
+ ok: false;
31
+ reason: string;
32
+ };
33
+ export declare function traceUploadIdempotencyKey(record: HubDecryptableTraceEnvelope): string;
34
+ export declare function enqueueTraceEnvelope(store: TraceUploadMailboxStore, record: HubDecryptableTraceEnvelope, opts?: {
35
+ now?: number;
36
+ runtimeNamespace?: string;
37
+ sourceAgent?: string;
38
+ targetAgent?: string;
39
+ env?: NodeJS.ProcessEnv;
40
+ }): {
41
+ queued: boolean;
42
+ duplicate: boolean;
43
+ };
44
+ export declare function backfillProxyTraceUploads(opts: {
45
+ dir: string;
46
+ store: TraceUploadMailboxStore;
47
+ env?: NodeJS.ProcessEnv;
48
+ now?: () => number;
49
+ runtimeNamespace?: string;
50
+ sourceAgent?: string;
51
+ targetAgent?: string;
52
+ }): TraceBackfillStats;
53
+ export {};