@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,525 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { closeSync, existsSync, openSync, readdirSync, readSync, statSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { mailbox } from '@evomap/evolver-core';
5
+ import { traceCollectionEnabled } from './traceConfig.js';
6
+ import { buildProxyTraceUploadPayload, PROXY_TRACE_UPLOAD_SCHEMA, } from './traceUploadPayload.js';
7
+ const TRACE_BACKFILL_STATE_PREFIX = 'llm_trace_backfill:v1:';
8
+ const DEFAULT_TRACE_BACKFILL_MAX_ROWS = 100;
9
+ const DEFAULT_TRACE_BACKFILL_MAX_SCAN_BYTES = 8 * 1024 * 1024;
10
+ const DEFAULT_TRACE_BACKFILL_MAX_LINE_BYTES = 1024 * 1024;
11
+ const DEFAULT_TRACE_BACKFILL_MAX_PENDING = 100;
12
+ const TRACE_CURSOR_GUARD_BYTES = 4096;
13
+ const SUMMARY_ALLOWED_KEYS = new Set([
14
+ 'event',
15
+ 'payload_schema',
16
+ 'ts',
17
+ 'route',
18
+ 'provider',
19
+ 'wire_api',
20
+ 'original_model',
21
+ 'chosen_model',
22
+ 'tier',
23
+ 'reason',
24
+ 'fallback',
25
+ 'router_enabled',
26
+ 'upstream_mode',
27
+ 'status',
28
+ 'stream',
29
+ 'ttfb_ms',
30
+ 'latency_ms',
31
+ 'usage',
32
+ ]);
33
+ const SUMMARY_STRING_OR_NULL_KEYS = [
34
+ 'route',
35
+ 'provider',
36
+ 'wire_api',
37
+ 'original_model',
38
+ 'chosen_model',
39
+ 'tier',
40
+ 'reason',
41
+ 'fallback',
42
+ 'upstream_mode',
43
+ ];
44
+ const SUMMARY_NUMBER_OR_NULL_KEYS = ['status', 'ttfb_ms', 'latency_ms'];
45
+ const USAGE_ALLOWED_KEYS = new Set([
46
+ 'input_tokens',
47
+ 'output_tokens',
48
+ 'cache_creation_input_tokens',
49
+ 'cache_read_input_tokens',
50
+ ]);
51
+ function isRecord(value) {
52
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
53
+ }
54
+ function isNonEmptyBase64(value) {
55
+ return typeof value === 'string' && value.length > 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value);
56
+ }
57
+ function hasOnlyKeys(value, allowed) {
58
+ return Object.keys(value).every((key) => allowed.has(key));
59
+ }
60
+ function hasOwnKey(value, key) {
61
+ return Object.prototype.hasOwnProperty.call(value, key);
62
+ }
63
+ function parseNodeSecretVersion(value) {
64
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0)
65
+ return value;
66
+ if (typeof value !== 'string')
67
+ return undefined;
68
+ const trimmed = value.trim();
69
+ if (!/^[1-9]\d*$/.test(trimmed))
70
+ return undefined;
71
+ const parsed = Number(trimmed);
72
+ return Number.isSafeInteger(parsed) ? parsed : undefined;
73
+ }
74
+ function isProducerGeneration(value) {
75
+ return value === 'v1' || value === 'v2' || value === 'unknown';
76
+ }
77
+ function storeState(store, key) {
78
+ try {
79
+ return store?.getState?.(key) ?? undefined;
80
+ }
81
+ catch {
82
+ return undefined;
83
+ }
84
+ }
85
+ /** Mirrors v1 extractor truthyState: accepts the string/number/boolean truthy forms the store may surface. */
86
+ function truthyState(value) {
87
+ return value === true || value === 'true' || value === 1 || value === '1';
88
+ }
89
+ /**
90
+ * v1 parity (extractor.js readTraceNodeSecretVersionDecryptEnabled): the hub-keyring decrypt path is OFF by
91
+ * default. It is enabled only when ANY of these env vars / store-state keys is truthy. Without it, an encrypted
92
+ * row that only carries a secret_version (no hub_key_envelope) can only be decrypted by the hub keyring, which
93
+ * older hubs cannot do — so we must refuse the upload.
94
+ */
95
+ function readTraceNodeSecretVersionDecryptEnabled(env = process.env, store) {
96
+ const candidates = [
97
+ env['EVOMAP_PROXY_TRACE_NODE_SECRET_VERSION_DECRYPT'],
98
+ env['EVOMAP_PROXY_TRACE_HUB_KEYRING_DECRYPT'],
99
+ storeState(store, 'trace_node_secret_version_decrypt_enabled'),
100
+ storeState(store, 'proxy_trace_node_secret_version_decrypt_enabled'),
101
+ storeState(store, 'trace_hub_keyring_decrypt_enabled'),
102
+ storeState(store, 'proxy_trace_hub_keyring_decrypt_enabled'),
103
+ ];
104
+ const rawEnabled = candidates.find((value) => value !== undefined && value !== null && value !== '');
105
+ return truthyState(rawEnabled);
106
+ }
107
+ /**
108
+ * v1 parity (extractor.js secretVersionTraceDecryptAllowed). An encrypted envelope is hub-decryptable when it
109
+ * carries a hub_key_envelope (always allowed). A secret_version-only envelope is allowed ONLY when the
110
+ * node-secret-version keyring-decrypt flag is enabled; otherwise the hub could never decrypt it.
111
+ */
112
+ function secretVersionTraceDecryptAllowed(record, env, store) {
113
+ if (!record || typeof record !== 'object')
114
+ return false;
115
+ if (record.hub_key_envelope)
116
+ return true;
117
+ if (parseNodeSecretVersion(record.secret_version) === undefined)
118
+ return false;
119
+ return readTraceNodeSecretVersionDecryptEnabled(env, store);
120
+ }
121
+ function isOptionalShortString(value, max) {
122
+ return value === undefined || (typeof value === 'string' && value.length > 0 && value.length <= max);
123
+ }
124
+ function isOptionalNonNegativeSafeInteger(value) {
125
+ return value === undefined || (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0);
126
+ }
127
+ function isStringOrNull(value) {
128
+ return typeof value === 'string' || value === null;
129
+ }
130
+ function isBooleanOrNull(value) {
131
+ return typeof value === 'boolean' || value === null;
132
+ }
133
+ function isFiniteNumberOrNull(value) {
134
+ return value === null || (typeof value === 'number' && Number.isFinite(value));
135
+ }
136
+ function isHubTraceUsageSummary(value) {
137
+ if (!isRecord(value) || !hasOnlyKeys(value, USAGE_ALLOWED_KEYS))
138
+ return false;
139
+ return Object.values(value).every((entry) => typeof entry === 'number' && Number.isFinite(entry));
140
+ }
141
+ function isHubTracePlaintextSummary(value) {
142
+ if (!isRecord(value) || !hasOnlyKeys(value, SUMMARY_ALLOWED_KEYS))
143
+ return false;
144
+ if (value['event'] !== 'llm_trace_plaintext_summary')
145
+ return false;
146
+ if (value['payload_schema'] !== 'llm_turn_summary')
147
+ return false;
148
+ if (typeof value['ts'] !== 'string')
149
+ return false;
150
+ for (const key of SUMMARY_STRING_OR_NULL_KEYS) {
151
+ if (!isStringOrNull(value[key]))
152
+ return false;
153
+ }
154
+ if (!isBooleanOrNull(value['router_enabled']))
155
+ return false;
156
+ if (!isBooleanOrNull(value['stream']))
157
+ return false;
158
+ for (const key of SUMMARY_NUMBER_OR_NULL_KEYS) {
159
+ if (!isFiniteNumberOrNull(value[key]))
160
+ return false;
161
+ }
162
+ return !hasOwnKey(value, 'usage') || isHubTraceUsageSummary(value['usage']);
163
+ }
164
+ function isHubKeyEnvelope(value) {
165
+ if (!isRecord(value))
166
+ return false;
167
+ const allowed = new Set(['algorithm', 'key_id', 'wrapped_key']);
168
+ return hasOnlyKeys(value, allowed)
169
+ && value['algorithm'] === 'rsa-oaep-sha256'
170
+ && typeof value['key_id'] === 'string'
171
+ && /^[a-f0-9]{16}$/i.test(value['key_id'])
172
+ && isNonEmptyBase64(value['wrapped_key']);
173
+ }
174
+ export function isHubDecryptableTraceEnvelope(record) {
175
+ if (!isRecord(record))
176
+ return false;
177
+ const allowed = new Set([
178
+ 'schema_version',
179
+ 'event',
180
+ 'encrypted',
181
+ 'payload_schema',
182
+ 'algorithm',
183
+ 'key_id',
184
+ 'iv',
185
+ 'tag',
186
+ 'ciphertext',
187
+ 'hub_key_envelope',
188
+ 'secret_version',
189
+ 'producer_generation',
190
+ 'producer_version',
191
+ 'producer_component',
192
+ 'plaintext_summary',
193
+ 'payload_complete',
194
+ 'payload_incomplete_reason',
195
+ 'hub_uploadable',
196
+ 'hub_upload_blocked_reason',
197
+ 'hub_upload_size_bytes',
198
+ 'hub_upload_max_bytes',
199
+ ]);
200
+ return hasOnlyKeys(record, allowed)
201
+ && record['schema_version'] === 1
202
+ && record['event'] === 'llm_trace_envelope'
203
+ && record['encrypted'] === true
204
+ && record['payload_schema'] === 'prism_trace_row'
205
+ && record['algorithm'] === 'aes-256-gcm'
206
+ && typeof record['key_id'] === 'string'
207
+ && /^[a-f0-9]{16}$/i.test(record['key_id'])
208
+ && isNonEmptyBase64(record['iv'])
209
+ && isNonEmptyBase64(record['tag'])
210
+ && isNonEmptyBase64(record['ciphertext'])
211
+ && (!hasOwnKey(record, 'hub_key_envelope') || isHubKeyEnvelope(record['hub_key_envelope']))
212
+ && (isHubKeyEnvelope(record['hub_key_envelope']) || parseNodeSecretVersion(record['secret_version']) !== undefined)
213
+ && (record['secret_version'] === undefined || parseNodeSecretVersion(record['secret_version']) !== undefined)
214
+ && (record['producer_generation'] === undefined || isProducerGeneration(record['producer_generation']))
215
+ && isOptionalShortString(record['producer_version'], 32)
216
+ && isOptionalShortString(record['producer_component'], 32)
217
+ && (record['payload_complete'] === undefined || record['payload_complete'] === false)
218
+ && isOptionalShortString(record['payload_incomplete_reason'], 64)
219
+ && (record['hub_uploadable'] === undefined || record['hub_uploadable'] === false)
220
+ && isOptionalShortString(record['hub_upload_blocked_reason'], 64)
221
+ && isOptionalNonNegativeSafeInteger(record['hub_upload_size_bytes'])
222
+ && isOptionalNonNegativeSafeInteger(record['hub_upload_max_bytes'])
223
+ && (!hasOwnKey(record, 'plaintext_summary') || isHubTracePlaintextSummary(record['plaintext_summary']));
224
+ }
225
+ export function traceUploadBlockedReason(record) {
226
+ if (record.hub_uploadable === false) {
227
+ const reason = record.hub_upload_blocked_reason ?? 'hub_upload_blocked';
228
+ return reason === 'payload_incomplete' ? undefined : reason;
229
+ }
230
+ return undefined;
231
+ }
232
+ function isProxyTraceUploadPayload(payload) {
233
+ if (!isRecord(payload))
234
+ return false;
235
+ const allowed = new Set([
236
+ 'schema',
237
+ 'encrypted',
238
+ 'trace',
239
+ 'node_secret_version',
240
+ 'secret_version',
241
+ 'producer_generation',
242
+ 'producer_version',
243
+ 'producer_component',
244
+ ]);
245
+ return hasOnlyKeys(payload, allowed)
246
+ && payload['schema'] === PROXY_TRACE_UPLOAD_SCHEMA
247
+ && payload['encrypted'] === true
248
+ && isHubDecryptableTraceEnvelope(payload['trace'])
249
+ && (payload['node_secret_version'] === undefined || parseNodeSecretVersion(payload['node_secret_version']) !== undefined)
250
+ && (payload['secret_version'] === undefined || parseNodeSecretVersion(payload['secret_version']) !== undefined)
251
+ && (payload['producer_generation'] === undefined || isProducerGeneration(payload['producer_generation']))
252
+ && isOptionalShortString(payload['producer_version'], 32)
253
+ && isOptionalShortString(payload['producer_component'], 32);
254
+ }
255
+ export function normalizeProxyTraceOutboundPayload(payload, env = process.env, store) {
256
+ if (!traceCollectionEnabled(env, store) || env['EVOLVER_LLM_TRACE_MAILBOX'] === '0') {
257
+ return { ok: false, reason: 'proxy_trace_upload_disabled' };
258
+ }
259
+ // v1 parity (isProxyTraceUploadPayloadAllowed): an encrypted row that carries a secret_version but no
260
+ // hub_key_envelope is decryptable only by the hub keyring. Refuse it at egress unless the keyring-decrypt
261
+ // flag is enabled — a hub_key_envelope row is always allowed.
262
+ const gate = (trace) => {
263
+ const blockedReason = traceUploadBlockedReason(trace);
264
+ if (blockedReason)
265
+ return { ok: false, reason: blockedReason };
266
+ if (!secretVersionTraceDecryptAllowed(trace, env, store)) {
267
+ return { ok: false, reason: 'hub_keyring_decrypt_unsupported' };
268
+ }
269
+ return { ok: true, payload: buildProxyTraceUploadPayload(trace) };
270
+ };
271
+ if (isProxyTraceUploadPayload(payload))
272
+ return gate(payload.trace);
273
+ if (isRecord(payload) && payload['schema'] === PROXY_TRACE_UPLOAD_SCHEMA && payload['encrypted'] === true) {
274
+ const trace = payload['trace'];
275
+ if (isHubDecryptableTraceEnvelope(trace)) {
276
+ return gate(trace);
277
+ }
278
+ return { ok: false, reason: 'proxy_trace_payload_rejected' };
279
+ }
280
+ if (isHubDecryptableTraceEnvelope(payload))
281
+ return gate(payload);
282
+ return { ok: false, reason: 'proxy_trace_payload_rejected' };
283
+ }
284
+ export function traceUploadIdempotencyKey(record) {
285
+ return `proxy_trace:${createHash('sha256').update(JSON.stringify(record)).digest('hex')}`;
286
+ }
287
+ export function enqueueTraceEnvelope(store, record, opts = {}) {
288
+ if (!traceCollectionEnabled(opts.env ?? process.env, store)) {
289
+ return { queued: false, duplicate: false };
290
+ }
291
+ if (traceUploadBlockedReason(record)) {
292
+ return { queued: false, duplicate: false };
293
+ }
294
+ // v1 parity (validateTraceUpload): refuse — without enqueueing — an encrypted row that carries a
295
+ // secret_version but no hub_key_envelope unless the node-secret-version keyring-decrypt flag is enabled.
296
+ // The hub could otherwise never decrypt it. The mailbox store doubles as the decrypt-flag state source.
297
+ if (!secretVersionTraceDecryptAllowed(record, opts.env ?? process.env, store)) {
298
+ return { queued: false, duplicate: false };
299
+ }
300
+ const idempotencyKey = traceUploadIdempotencyKey(record);
301
+ const payload = buildProxyTraceUploadPayload(record);
302
+ if (store.hasMessageWithIdempotencyKey?.(idempotencyKey)
303
+ || store.hasMessageWithPayload?.('proxy_trace', payload)
304
+ || store.hasMessageWithPayload?.('proxy_trace', record)) {
305
+ return { queued: false, duplicate: true };
306
+ }
307
+ const env = mailbox.createEnvelope({
308
+ id: idempotencyKey,
309
+ type: 'proxy_trace',
310
+ payload,
311
+ correlationId: idempotencyKey,
312
+ idempotencyKey,
313
+ ...(opts.now !== undefined ? { now: opts.now } : {}),
314
+ ...(opts.runtimeNamespace ? { runtimeNamespace: opts.runtimeNamespace } : {}),
315
+ ...(opts.sourceAgent ? { sourceAgent: opts.sourceAgent } : {}),
316
+ ...(opts.targetAgent ? { targetAgent: opts.targetAgent } : {}),
317
+ });
318
+ return { queued: store.send(env).stored, duplicate: false };
319
+ }
320
+ function numberFromEnv(env, keys, fallback) {
321
+ for (const key of keys) {
322
+ const raw = Number(env[key]);
323
+ if (Number.isFinite(raw) && raw > 0)
324
+ return Math.floor(raw);
325
+ }
326
+ return fallback;
327
+ }
328
+ function cursorKey(file) {
329
+ return `${TRACE_BACKFILL_STATE_PREFIX}${createHash('sha256').update(file).digest('hex').slice(0, 24)}`;
330
+ }
331
+ function currentCursorState(file, offset) {
332
+ const stat = statSync(file);
333
+ return {
334
+ dev: Number(stat.dev) || 0,
335
+ ino: Number(stat.ino) || 0,
336
+ birthtimeMs: Math.floor(Number(stat.birthtimeMs) || 0),
337
+ size: Number(stat.size) || 0,
338
+ offset,
339
+ guard: cursorGuard(file, offset),
340
+ };
341
+ }
342
+ function cursorGuard(file, offset) {
343
+ if (offset <= 0)
344
+ return '';
345
+ const bytesToRead = Math.min(TRACE_CURSOR_GUARD_BYTES, offset);
346
+ const fd = openSync(file, 'r');
347
+ try {
348
+ const buf = Buffer.alloc(bytesToRead);
349
+ const bytesRead = readSync(fd, buf, 0, bytesToRead, offset - bytesToRead);
350
+ return createHash('sha256').update(buf.subarray(0, bytesRead)).digest('hex');
351
+ }
352
+ finally {
353
+ closeSync(fd);
354
+ }
355
+ }
356
+ function parseCursor(raw) {
357
+ if (!raw)
358
+ return null;
359
+ try {
360
+ const parsed = JSON.parse(raw);
361
+ if (typeof parsed.dev === 'number'
362
+ && typeof parsed.ino === 'number'
363
+ && typeof parsed.birthtimeMs === 'number'
364
+ && typeof parsed.size === 'number'
365
+ && typeof parsed.offset === 'number'
366
+ && (typeof parsed.guard === 'string' || parsed.guard === undefined)
367
+ && parsed.offset >= 0)
368
+ return { ...parsed, guard: parsed.guard ?? '' };
369
+ }
370
+ catch {
371
+ return null;
372
+ }
373
+ return null;
374
+ }
375
+ function sameFileIdentity(a, b) {
376
+ if (!a)
377
+ return false;
378
+ if (a.dev && b.dev && a.ino && b.ino)
379
+ return a.dev === b.dev && a.ino === b.ino;
380
+ return a.birthtimeMs > 0 && a.birthtimeMs === b.birthtimeMs;
381
+ }
382
+ function cursorOffset(store, file) {
383
+ const statState = currentCursorState(file, 0);
384
+ const saved = parseCursor(store.getState?.(cursorKey(file)));
385
+ if (!sameFileIdentity(saved, statState))
386
+ return 0;
387
+ if (!saved || saved.offset > statState.size || saved.size > statState.size)
388
+ return 0;
389
+ if (saved.guard !== cursorGuard(file, saved.offset))
390
+ return 0;
391
+ return saved.offset;
392
+ }
393
+ function readCompleteLines(file, offset, maxBytes, maxLineBytes) {
394
+ const stat = statSync(file);
395
+ const bytesToRead = Math.min(maxBytes, Math.max(0, stat.size - offset));
396
+ if (bytesToRead <= 0)
397
+ return { lines: [], nextOffset: offset, lineTooLong: false };
398
+ const fd = openSync(file, 'r');
399
+ try {
400
+ const buf = Buffer.alloc(bytesToRead);
401
+ const bytesRead = readSync(fd, buf, 0, bytesToRead, offset);
402
+ let pos = 0;
403
+ let nextOffset = offset;
404
+ let lineTooLong = false;
405
+ const lines = [];
406
+ while (pos < bytesRead) {
407
+ const nl = buf.indexOf(0x0a, pos);
408
+ if (nl === -1) {
409
+ if (bytesRead === bytesToRead && offset + bytesRead < stat.size) {
410
+ const partialLineBytes = bytesRead - pos;
411
+ if (partialLineBytes > maxLineBytes) {
412
+ nextOffset = offset + bytesRead;
413
+ lineTooLong = true;
414
+ }
415
+ }
416
+ break;
417
+ }
418
+ const rawLine = buf.subarray(pos, nl);
419
+ nextOffset = offset + nl + 1;
420
+ pos = nl + 1;
421
+ if (rawLine.length === 0)
422
+ continue;
423
+ if (rawLine.length > maxLineBytes) {
424
+ lineTooLong = true;
425
+ continue;
426
+ }
427
+ lines.push({ raw: rawLine.toString('utf8'), nextOffset });
428
+ }
429
+ return { lines, nextOffset, lineTooLong };
430
+ }
431
+ finally {
432
+ closeSync(fd);
433
+ }
434
+ }
435
+ function traceFiles(dir) {
436
+ if (!existsSync(dir))
437
+ return [];
438
+ return readdirSync(dir)
439
+ .filter((name) => /^llm-trace-\d{8}\.jsonl$/.test(name))
440
+ .sort()
441
+ .map((name) => join(dir, name));
442
+ }
443
+ function bump(stats, reason) {
444
+ stats.skipped += 1;
445
+ stats.reasons[reason] = (stats.reasons[reason] ?? 0) + 1;
446
+ }
447
+ function persistCursor(store, file, offset) {
448
+ store.setState?.(cursorKey(file), JSON.stringify(currentCursorState(file, offset)));
449
+ }
450
+ export function backfillProxyTraceUploads(opts) {
451
+ const env = opts.env ?? process.env;
452
+ const maxRows = numberFromEnv(env, ['EVOLVER_LLM_TRACE_BACKFILL_MAX_ROWS', 'EVOMAP_PROXY_TRACE_BACKFILL_MAX_ROWS'], DEFAULT_TRACE_BACKFILL_MAX_ROWS);
453
+ const maxScanBytes = numberFromEnv(env, ['EVOLVER_LLM_TRACE_BACKFILL_MAX_SCAN_BYTES', 'EVOMAP_PROXY_TRACE_BACKFILL_MAX_SCAN_BYTES'], DEFAULT_TRACE_BACKFILL_MAX_SCAN_BYTES);
454
+ const maxLineBytes = numberFromEnv(env, ['EVOLVER_LLM_TRACE_BACKFILL_MAX_LINE_BYTES', 'EVOMAP_PROXY_TRACE_BACKFILL_MAX_LINE_BYTES'], DEFAULT_TRACE_BACKFILL_MAX_LINE_BYTES);
455
+ const maxPending = numberFromEnv(env, ['EVOLVER_LLM_TRACE_MAX_PENDING_UPLOADS', 'EVOMAP_PROXY_TRACE_MAX_PENDING_UPLOADS'], DEFAULT_TRACE_BACKFILL_MAX_PENDING);
456
+ const stats = { scanned: 0, queued: 0, duplicates: 0, skipped: 0, files: 0, reasons: {} };
457
+ let stopBackfill = false;
458
+ for (const file of traceFiles(opts.dir)) {
459
+ if (stopBackfill || stats.scanned >= maxRows)
460
+ break;
461
+ let pending = opts.store.countPending?.('proxy', opts.runtimeNamespace);
462
+ if (pending !== undefined && pending >= maxPending) {
463
+ bump(stats, 'max_pending_uploads');
464
+ break;
465
+ }
466
+ stats.files += 1;
467
+ let offset = cursorOffset(opts.store, file);
468
+ try {
469
+ const read = readCompleteLines(file, offset, maxScanBytes, maxLineBytes);
470
+ if (read.lineTooLong)
471
+ bump(stats, 'line_too_long');
472
+ let processedLineCount = 0;
473
+ for (const line of read.lines) {
474
+ if (stats.scanned >= maxRows)
475
+ break;
476
+ if (pending !== undefined && pending >= maxPending) {
477
+ bump(stats, 'max_pending_uploads');
478
+ stopBackfill = true;
479
+ break;
480
+ }
481
+ processedLineCount += 1;
482
+ stats.scanned += 1;
483
+ offset = line.nextOffset;
484
+ let parsed;
485
+ try {
486
+ parsed = JSON.parse(line.raw);
487
+ }
488
+ catch {
489
+ bump(stats, 'invalid_json');
490
+ continue;
491
+ }
492
+ if (!isHubDecryptableTraceEnvelope(parsed)) {
493
+ bump(stats, 'not_hub_decryptable');
494
+ continue;
495
+ }
496
+ const blockedReason = traceUploadBlockedReason(parsed);
497
+ if (blockedReason) {
498
+ bump(stats, blockedReason);
499
+ continue;
500
+ }
501
+ const queued = enqueueTraceEnvelope(opts.store, parsed, {
502
+ now: opts.now?.(),
503
+ env,
504
+ ...(opts.runtimeNamespace ? { runtimeNamespace: opts.runtimeNamespace } : {}),
505
+ ...(opts.sourceAgent ? { sourceAgent: opts.sourceAgent } : {}),
506
+ ...(opts.targetAgent ? { targetAgent: opts.targetAgent } : {}),
507
+ });
508
+ if (queued.duplicate)
509
+ stats.duplicates += 1;
510
+ else if (queued.queued) {
511
+ stats.queued += 1;
512
+ if (pending !== undefined)
513
+ pending += 1;
514
+ }
515
+ }
516
+ if (processedLineCount === read.lines.length && read.nextOffset > offset)
517
+ offset = read.nextOffset;
518
+ persistCursor(opts.store, file, offset);
519
+ }
520
+ catch {
521
+ bump(stats, 'read_failed');
522
+ }
523
+ }
524
+ return stats;
525
+ }
@@ -0,0 +1,7 @@
1
+ export declare function traceDisabledByValue(value: string | undefined): boolean;
2
+ export interface TraceConfigStateStore {
3
+ getState?(key: string): string | undefined;
4
+ }
5
+ export declare function traceCollectionEnabled(env?: NodeJS.ProcessEnv, store?: TraceConfigStateStore): boolean;
6
+ export declare function traceProfileAnalysisEnabled(env?: NodeJS.ProcessEnv, store?: TraceConfigStateStore): boolean;
7
+ export declare function traceHubPublicKey(env?: NodeJS.ProcessEnv, store?: TraceConfigStateStore): string | null;
@@ -0,0 +1,44 @@
1
+ export function traceDisabledByValue(value) {
2
+ const raw = String(value ?? '').trim().toLowerCase();
3
+ return raw === '0' || raw === 'false' || raw === 'off' || raw === 'none' || raw === 'no';
4
+ }
5
+ function truthyState(value) {
6
+ return value === true || value === 'true' || value === 1 || value === '1';
7
+ }
8
+ function falseyState(value) {
9
+ return value === false || value === 'false' || value === 0 || value === '0';
10
+ }
11
+ function storeState(store, key) {
12
+ try {
13
+ return store?.getState?.(key) ?? undefined;
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ }
19
+ export function traceCollectionEnabled(env = process.env, store) {
20
+ const state = storeState(store, 'trace_collection_enabled') ?? storeState(store, 'proxy_trace_collection_enabled');
21
+ if (falseyState(state))
22
+ return false;
23
+ if (env['EVOLVER_LLM_TRACE'] !== undefined)
24
+ return !traceDisabledByValue(env['EVOLVER_LLM_TRACE']);
25
+ if (env['EVOMAP_PROXY_TRACE'] !== undefined)
26
+ return !traceDisabledByValue(env['EVOMAP_PROXY_TRACE']);
27
+ return true;
28
+ }
29
+ export function traceProfileAnalysisEnabled(env = process.env, store) {
30
+ if (env['EVOLVER_LLM_TRACE_PROFILE_ANALYSIS'] !== undefined)
31
+ return !traceDisabledByValue(env['EVOLVER_LLM_TRACE_PROFILE_ANALYSIS']);
32
+ if (env['EVOMAP_PROXY_TRACE_PROFILE_ANALYSIS'] !== undefined)
33
+ return !traceDisabledByValue(env['EVOMAP_PROXY_TRACE_PROFILE_ANALYSIS']);
34
+ const state = storeState(store, 'trace_profile_analysis_enabled') ?? storeState(store, 'proxy_trace_profile_analysis_enabled');
35
+ return truthyState(state);
36
+ }
37
+ export function traceHubPublicKey(env = process.env, store) {
38
+ const key = String(env['EVOLVER_LLM_TRACE_HUB_PUBLIC_KEY']
39
+ || env['EVOMAP_PROXY_TRACE_HUB_PUBLIC_KEY']
40
+ || storeState(store, 'trace_hub_public_key')
41
+ || storeState(store, 'proxy_trace_hub_public_key')
42
+ || '').trim();
43
+ return key || null;
44
+ }
@@ -0,0 +1,16 @@
1
+ interface TraceControlStore {
2
+ setState(key: string, value: string): void;
3
+ }
4
+ interface TraceControlLogger {
5
+ log?: (line: string) => void;
6
+ warn?: (line: string) => void;
7
+ }
8
+ interface TraceControlResult {
9
+ ack: true;
10
+ applied: boolean;
11
+ }
12
+ export declare const DEFAULT_TRACE_CONFIG_SIGNING_PUBLIC_KEY: string;
13
+ export declare function canonicalTraceConfigPayload(payload?: Record<string, unknown>): string;
14
+ export declare function verifyTraceConfigSignature(payload: unknown, env?: NodeJS.ProcessEnv): boolean;
15
+ export declare function applyTraceCollectionConfig(payload: unknown, store: TraceControlStore, env?: NodeJS.ProcessEnv, logger?: TraceControlLogger): TraceControlResult;
16
+ export {};
@@ -0,0 +1,85 @@
1
+ import { verify } from 'node:crypto';
2
+ export const DEFAULT_TRACE_CONFIG_SIGNING_PUBLIC_KEY = [
3
+ '-----BEGIN PUBLIC KEY-----',
4
+ 'MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA7kJvWUP3HC4FJPQtkh74',
5
+ 'y75h9Rzc2NSZC9e4fiIWdax4iv+yWeMeIHGNsMr7YI8Ws7ck1BimJWt026gwRW8I',
6
+ 'c2A7h97oZQ0Z0zFcjEZ8FpYFSu++Yz/dGrARAV7uCQg289jvo89F5fWNdX2k+lTH',
7
+ 'hBoBm0G71vkiAYlbQEjq1xm1WzYf8CVXmbr+J1z+ydQf9jczcFL79u3eQZhIPs3R',
8
+ '8Sr83YrXWyCVOBIPTW4EbyR1RrHNs9pyrcHo7tyKdpKYreM/0de5A5Ya1VFaakVd',
9
+ 'RsE3UModswJeMzyHOj7wZ+OZVb466Bttr0wDHhg93sWg5h5m0YqNfEcdqFXKlxy3',
10
+ 'RCAu+hINcwt27CcIEU82jhDusiKEfM/EHS/uN3GTuvNaUFpmIOPNFYINKdjdiMJK',
11
+ '50lyW9E3SN+Q3HT6flseEAI+hMvFx6wxGqzf64jWbuUlatl8M9v3NNZAOgG4SnTt',
12
+ 'PiOh2Uxc0qFAKPpcz8gaGYm0yMuFGsr5zb0IMDSBr++PAgMBAAE=',
13
+ '-----END PUBLIC KEY-----',
14
+ ].join('\n');
15
+ function isRecord(value) {
16
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
17
+ }
18
+ export function canonicalTraceConfigPayload(payload = {}) {
19
+ const clean = {};
20
+ for (const key of Object.keys(payload).sort()) {
21
+ if (key === 'signature' || key === 'trace_config_signature' || key === 'signature_algorithm')
22
+ continue;
23
+ if (key === 'hub_public_key')
24
+ continue;
25
+ const value = payload[key];
26
+ if (value !== undefined)
27
+ clean[key] = value;
28
+ }
29
+ return JSON.stringify(clean);
30
+ }
31
+ export function verifyTraceConfigSignature(payload, env = process.env) {
32
+ if (!isRecord(payload))
33
+ return false;
34
+ const publicKey = String(env['EVOMAP_TRACE_CONFIG_SIGNING_PUBLIC_KEY']
35
+ || env['EVOMAP_PROXY_TRACE_CONFIG_SIGNING_PUBLIC_KEY']
36
+ || DEFAULT_TRACE_CONFIG_SIGNING_PUBLIC_KEY).trim();
37
+ const signature = String(payload['signature'] || payload['trace_config_signature'] || '').trim();
38
+ if (!publicKey || !signature)
39
+ return false;
40
+ try {
41
+ return verify('sha256', Buffer.from(canonicalTraceConfigPayload(payload), 'utf8'), publicKey, Buffer.from(signature, 'base64'));
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ export function applyTraceCollectionConfig(payload, store, env = process.env, logger = console) {
48
+ if (!isRecord(payload)) {
49
+ logger.warn?.('[trace-control] ignoring non-object trace config');
50
+ return { ack: true, applied: false };
51
+ }
52
+ const enabled = payload['enabled'] ?? payload['trace_collection_enabled'] ?? payload['proxy_trace_collection_enabled'];
53
+ if (typeof enabled !== 'boolean') {
54
+ logger.warn?.('[trace-control] ignoring config without boolean enabled');
55
+ return { ack: true, applied: false };
56
+ }
57
+ const signed = verifyTraceConfigSignature(payload, env);
58
+ let profileEnabled = payload['profile_analysis_enabled'] ?? payload['trace_profile_analysis_enabled'];
59
+ const traceHubPublicKey = typeof payload['trace_hub_public_key'] === 'string' ? payload['trace_hub_public_key'].trim() : '';
60
+ const runtimeHubKey = typeof payload['hub_public_key'] === 'string' ? payload['hub_public_key'].trim() : '';
61
+ if (!signed && enabled === true) {
62
+ logger.warn?.('[trace-control] rejected unsigned trace config that could enable collection/profile analysis');
63
+ return { ack: true, applied: false };
64
+ }
65
+ if (!signed && profileEnabled === true) {
66
+ logger.warn?.('[trace-control] ignored unsigned profile analysis enable while applying trace disable');
67
+ profileEnabled = false;
68
+ }
69
+ store.setState('trace_collection_enabled', enabled ? 'true' : 'false');
70
+ if (typeof profileEnabled === 'boolean') {
71
+ store.setState('trace_profile_analysis_enabled', profileEnabled ? 'true' : 'false');
72
+ }
73
+ if (signed && traceHubPublicKey && traceHubPublicKey.includes('PUBLIC KEY')) {
74
+ store.setState('trace_hub_public_key', traceHubPublicKey);
75
+ }
76
+ else if (!signed && traceHubPublicKey) {
77
+ logger.warn?.('[trace-control] ignored trace_hub_public_key from an unsigned config');
78
+ }
79
+ if (runtimeHubKey) {
80
+ logger.warn?.('[trace-control] ignored runtime hub_public_key; use pinned EVOMAP_PROXY_TRACE_HUB_PUBLIC_KEY or signed trace_hub_public_key');
81
+ }
82
+ store.setState('trace_collection_updated_at', new Date().toISOString());
83
+ logger.log?.(`[trace-control] trace collection ${enabled ? 'enabled' : 'disabled'}`);
84
+ return { ack: true, applied: true };
85
+ }