@evomap/evolver 1.89.13 → 1.89.15

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 (65) hide show
  1. package/index.js +234 -31
  2. package/package.json +1 -1
  3. package/src/evolve/guards.js +1 -1
  4. package/src/evolve/pipeline/collect.js +1 -1
  5. package/src/evolve/pipeline/dispatch.js +1 -1
  6. package/src/evolve/pipeline/enrich.js +1 -1
  7. package/src/evolve/pipeline/hub.js +1 -1
  8. package/src/evolve/pipeline/select.js +1 -1
  9. package/src/evolve/pipeline/signals.js +1 -1
  10. package/src/evolve/utils.js +1 -1
  11. package/src/evolve.js +1 -1
  12. package/src/forceUpdate.js +499 -119
  13. package/src/gep/a2aProtocol.js +1 -1
  14. package/src/gep/antiAbuseTelemetry.js +1 -1
  15. package/src/gep/autoDistillConv.js +1 -1
  16. package/src/gep/autoDistillLlm.js +1 -1
  17. package/src/gep/candidateEval.js +1 -1
  18. package/src/gep/candidates.js +1 -1
  19. package/src/gep/cliContracts.js +1154 -0
  20. package/src/gep/contentHash.js +1 -1
  21. package/src/gep/conversationDistiller.js +1 -1
  22. package/src/gep/conversationSniffer.js +1 -1
  23. package/src/gep/crypto.js +1 -1
  24. package/src/gep/curriculum.js +1 -1
  25. package/src/gep/deviceId.js +1 -1
  26. package/src/gep/envFingerprint.js +1 -1
  27. package/src/gep/epigenetics.js +1 -1
  28. package/src/gep/execBridge.js +1 -1
  29. package/src/gep/explore.js +1 -1
  30. package/src/gep/hash.js +1 -1
  31. package/src/gep/hubFetch.js +1 -1
  32. package/src/gep/hubReview.js +1 -1
  33. package/src/gep/hubSearch.js +1 -1
  34. package/src/gep/hubVerify.js +1 -1
  35. package/src/gep/issueReporter.js +86 -0
  36. package/src/gep/learningSignals.js +1 -1
  37. package/src/gep/memoryGraph.js +1 -1
  38. package/src/gep/memoryGraphAdapter.js +1 -1
  39. package/src/gep/mutation.js +1 -1
  40. package/src/gep/narrativeMemory.js +1 -1
  41. package/src/gep/openPRRegistry.js +1 -1
  42. package/src/gep/personality.js +1 -1
  43. package/src/gep/policyCheck.js +1 -1
  44. package/src/gep/prompt.js +1 -1
  45. package/src/gep/recallInject.js +1 -1
  46. package/src/gep/recallVerifier.js +1 -1
  47. package/src/gep/reflection.js +1 -1
  48. package/src/gep/sanitize.js +20 -4
  49. package/src/gep/savingsCore.js +1 -1
  50. package/src/gep/selector.js +1 -1
  51. package/src/gep/signals.js +33 -9
  52. package/src/gep/skillDistiller.js +1 -1
  53. package/src/gep/solidify.js +1 -1
  54. package/src/gep/strategy.js +1 -1
  55. package/src/gep/tokenSavings.js +1 -1
  56. package/src/gep/workspaceKeychain.js +1 -1
  57. package/src/proxy/extensions/traceControl.js +1 -1
  58. package/src/proxy/index.js +54 -4
  59. package/src/proxy/inject.js +1 -1
  60. package/src/proxy/lifecycle/manager.js +266 -27
  61. package/src/proxy/server/routes.js +10 -0
  62. package/src/proxy/sync/inbound.js +5 -4
  63. package/src/proxy/sync/outbound.js +4 -3
  64. package/src/proxy/trace/extractor.js +1 -1
  65. package/src/proxy/trace/usage.js +1 -1
@@ -11,6 +11,7 @@ const {
11
11
  isHubUnreachableError,
12
12
  readHubResponseJson,
13
13
  readHubResponseText,
14
+ sanitizeHubResponseForLog,
14
15
  throwIfHubUnreachableResponse,
15
16
  } = require('../../gep/hubFetch');
16
17
  const { getEvomapPath } = require('../../gep/paths');
@@ -362,6 +363,86 @@ class AuthError extends Error {
362
363
  }
363
364
  }
364
365
 
366
+ function parseNodeSecretVersion(value) {
367
+ const n = Number(value);
368
+ return Number.isInteger(n) && n > 0 ? n : null;
369
+ }
370
+
371
+ const NODE_SECRET_RE = /^[a-f0-9]{64}$/i;
372
+ const NODE_SECRET_SUPPRESSION_RE = /^sha256:[a-f0-9]{64}$/i;
373
+ const SECRET_DIVERGENCE_ERROR = 'secret_diverged_cleared';
374
+ const SECRET_DIVERGENCE_REASON_CODES = [
375
+ 'node_secret_invalid',
376
+ 'invalid_secret',
377
+ 'rotation_requires_current_secret',
378
+ ];
379
+
380
+ function validNodeSecret(value) {
381
+ return typeof value === 'string' && NODE_SECRET_RE.test(value);
382
+ }
383
+
384
+ function getEnvNodeSecret() {
385
+ return ((process.env.A2A_NODE_SECRET || process.env.EVOMAP_NODE_SECRET || '').trim() || null);
386
+ }
387
+
388
+ function fingerprintNodeSecret(secret) {
389
+ if (!validNodeSecret(secret)) return null;
390
+ const normalized = String(secret).trim().toLowerCase();
391
+ return 'sha256:' + crypto.createHash('sha256').update(normalized).digest('hex');
392
+ }
393
+
394
+ function truthyState(value) {
395
+ const v = String(value || '').trim().toLowerCase();
396
+ return v === '1' || v === 'true' || v === 'yes' || v === 'on';
397
+ }
398
+
399
+ function matchSecretDivergenceReason(value) {
400
+ const reason = String(value || '').trim().toLowerCase();
401
+ if (!reason) return null;
402
+ return SECRET_DIVERGENCE_REASON_CODES.find((code) => reason.includes(code)) || null;
403
+ }
404
+
405
+ function extractSecretDivergenceReason(data) {
406
+ const candidates = [
407
+ data?.payload?.reason,
408
+ data?.payload?.error,
409
+ data?.reason,
410
+ data?.error,
411
+ ];
412
+ for (const value of candidates) {
413
+ const reason = matchSecretDivergenceReason(value);
414
+ if (reason) return reason;
415
+ }
416
+ return null;
417
+ }
418
+
419
+ function isSecretDivergenceError(error) {
420
+ return error === SECRET_DIVERGENCE_ERROR || Boolean(matchSecretDivergenceReason(error));
421
+ }
422
+
423
+ function safeHubErrorCode(data, statusCode) {
424
+ const candidates = [
425
+ data?.error,
426
+ data?.reason,
427
+ data?.payload?.error,
428
+ data?.payload?.reason,
429
+ ];
430
+ for (const value of candidates) {
431
+ if (typeof value !== 'string') continue;
432
+ const code = value.trim();
433
+ if (/^[a-z][a-z0-9_:-]{0,79}$/i.test(code)) return code;
434
+ }
435
+ return `http_${statusCode}`;
436
+ }
437
+
438
+ function normalizeNodeSecretEnvSuppression(value) {
439
+ const raw = String(value || '').trim();
440
+ if (!raw) return null;
441
+ if (truthyState(raw)) return 'true';
442
+ const lower = raw.toLowerCase();
443
+ return NODE_SECRET_SUPPRESSION_RE.test(lower) ? lower : null;
444
+ }
445
+
365
446
  class LifecycleManager {
366
447
  constructor({ hubUrl, store, logger, getTaskMeta } = {}) {
367
448
  this.hubUrl = (hubUrl || process.env.A2A_HUB_URL || '').replace(/\/+$/, '');
@@ -381,6 +462,11 @@ class LifecycleManager {
381
462
  this._lastDriftCheckAt = 0;
382
463
  this._hubUnreachableFailures = 0;
383
464
  this._hubUnreachableUntil = 0;
465
+ this._envSecretSuppressionMarker = normalizeNodeSecretEnvSuppression(this.store && this.store.getState
466
+ ? this.store.getState('node_secret_env_suppressed')
467
+ : null);
468
+ this._envSuppressionClearedForSecret = null;
469
+ this._suppressEnvSecret = Boolean(this._envSecretSuppressionMarker && getEnvNodeSecret());
384
470
 
385
471
  // H4 fix: persist the legacy node_id file as soon as the in-memory
386
472
  // node_id is known, NOT only after a successful hello(). The original
@@ -433,6 +519,23 @@ class LifecycleManager {
433
519
  return this._resolveNodeSecret();
434
520
  }
435
521
 
522
+ get nodeSecretVersion() {
523
+ this._resolveNodeSecret();
524
+ const storeVersion = parseNodeSecretVersion(this.store.getState('node_secret_version'));
525
+ const storeSecret = this.store.getState('node_secret') || null;
526
+ const storeSource = this.store.getState('node_secret_source') || null;
527
+ const envSecret = this._effectiveEnvNodeSecret();
528
+ const envVersion = parseNodeSecretVersion(process.env.A2A_NODE_SECRET_VERSION || process.env.EVOMAP_NODE_SECRET_VERSION);
529
+ const validStoreSecret = validNodeSecret(storeSecret);
530
+ if (this._suppressEnvSecret) return validStoreSecret ? storeVersion : null;
531
+ if (storeSource === 'hub_rotate' && validStoreSecret) return storeVersion;
532
+ if (envSecret) {
533
+ if (envVersion) return envVersion;
534
+ return storeSecret === envSecret ? storeVersion : null;
535
+ }
536
+ return validStoreSecret ? storeVersion : null;
537
+ }
538
+
436
539
  /**
437
540
  * Resolve the active node_secret with conflict reconciliation between the
438
541
  * persistent MailboxStore and `process.env.A2A_NODE_SECRET`.
@@ -464,35 +567,41 @@ class LifecycleManager {
464
567
  * @returns {string|null}
465
568
  */
466
569
  _resolveNodeSecret() {
467
- const envSecret = this._suppressEnvSecret
468
- ? null
469
- : ((process.env.A2A_NODE_SECRET || '').trim() || null);
570
+ const envSecret = this._effectiveEnvNodeSecret();
571
+ const envUpdatedAfterSuppression = this._envSuppressionClearedForSecret === envSecret;
470
572
  const storeSecret = this.store.getState('node_secret') || null;
471
573
  const storeSource = this.store.getState('node_secret_source') || null;
472
- const valid = (s) => typeof s === 'string' && /^[a-f0-9]{64}$/i.test(s);
574
+
575
+ if (
576
+ envSecret &&
577
+ envUpdatedAfterSuppression &&
578
+ validNodeSecret(envSecret) &&
579
+ (!storeSecret || envSecret === storeSecret) &&
580
+ !(storeSecret && envSecret === storeSecret && storeSource === 'hub_rotate')
581
+ ) {
582
+ this._syncEnvNodeSecretToStore(envSecret);
583
+ return envSecret;
584
+ }
473
585
 
474
586
  if (envSecret && storeSecret && envSecret !== storeSecret) {
475
587
  // Store value came from a successful hub rotation -> trust it.
476
588
  // The env var is necessarily stale: it was captured by the parent
477
589
  // shell before the rotation and a child process cannot mutate it
478
590
  // back into its parent.
479
- if (storeSource === 'hub_rotate' && valid(storeSecret)) {
591
+ if (storeSource === 'hub_rotate' && validNodeSecret(storeSecret) && !envUpdatedAfterSuppression) {
480
592
  if (!this._storeSourceLogged) {
481
593
  this._storeSourceLogged = true;
482
594
  this.logger.warn(
483
595
  '[lifecycle] A2A_NODE_SECRET env var differs from MailboxStore; ' +
484
596
  'store value originated from a hub rotation, treating env as stale. ' +
485
- 'Run `evolver reset-local-secret` after a manual web reset, or ' +
486
- 'unset A2A_NODE_SECRET to silence this warning.'
597
+ 'After a manual web reset, run `evolver reset-local-secret` to clear local ' +
598
+ 'secret and env suppression state before relying only on updated env vars.'
487
599
  );
488
600
  }
489
601
  return storeSecret;
490
602
  }
491
- if (valid(envSecret)) {
492
- this.store.setState('node_secret', envSecret);
493
- // Mark the new store value as env-seeded so a future rotation can
494
- // distinguish "operator pasted this in" from "hub returned this".
495
- this.store.setState('node_secret_source', 'env_seed');
603
+ if (validNodeSecret(envSecret)) {
604
+ this._syncEnvNodeSecretToStore(envSecret);
496
605
  if (!this._envOverrideLogged) {
497
606
  this._envOverrideLogged = true;
498
607
  this.logger.warn(
@@ -509,10 +618,74 @@ class LifecycleManager {
509
618
  return storeSecret || envSecret || null;
510
619
  }
511
620
 
621
+ _syncEnvNodeSecretToStore(envSecret) {
622
+ const envVersion = parseNodeSecretVersion(process.env.A2A_NODE_SECRET_VERSION || process.env.EVOMAP_NODE_SECRET_VERSION);
623
+ this.store.setState('node_secret', envSecret);
624
+ this.store.setState('node_secret_version', envVersion ? String(envVersion) : '');
625
+ // Mark the new store value as env-seeded so a future rotation can
626
+ // distinguish "operator pasted this in" from "hub returned this".
627
+ this.store.setState('node_secret_source', 'env_seed');
628
+ this._clearEnvSecretSuppression();
629
+ }
630
+
631
+ _effectiveEnvNodeSecret() {
632
+ const envSecret = getEnvNodeSecret();
633
+ return this._isEnvNodeSecretSuppressed(envSecret) ? null : envSecret;
634
+ }
635
+
636
+ _isEnvNodeSecretSuppressed(envSecret) {
637
+ this._envSuppressionClearedForSecret = null;
638
+ const persistedMarker = normalizeNodeSecretEnvSuppression(this.store && this.store.getState
639
+ ? this.store.getState('node_secret_env_suppressed')
640
+ : null);
641
+ const marker = persistedMarker || this._envSecretSuppressionMarker;
642
+ if (!marker) {
643
+ this._envSecretSuppressionMarker = null;
644
+ this._suppressEnvSecret = false;
645
+ return false;
646
+ }
647
+ this._envSecretSuppressionMarker = marker;
648
+ if (marker === 'true') {
649
+ this._suppressEnvSecret = Boolean(envSecret);
650
+ return Boolean(envSecret);
651
+ }
652
+ const envFingerprint = fingerprintNodeSecret(envSecret);
653
+ if (!envFingerprint) {
654
+ this._suppressEnvSecret = false;
655
+ return false;
656
+ }
657
+ if (envFingerprint === marker) {
658
+ this._suppressEnvSecret = true;
659
+ return true;
660
+ }
661
+ this._clearEnvSecretSuppression();
662
+ this._envSuppressionClearedForSecret = envSecret;
663
+ return false;
664
+ }
665
+
666
+ _markCurrentEnvSecretSuppressed() {
667
+ const marker = fingerprintNodeSecret(getEnvNodeSecret());
668
+ if (!marker) {
669
+ this._clearEnvSecretSuppression();
670
+ return;
671
+ }
672
+ try { this.store.setState('node_secret_env_suppressed', marker); } catch { /* best-effort */ }
673
+ this._envSecretSuppressionMarker = marker;
674
+ this._suppressEnvSecret = true;
675
+ }
676
+
677
+ _clearEnvSecretSuppression() {
678
+ try { this.store.setState('node_secret_env_suppressed', ''); } catch { /* best-effort */ }
679
+ this._envSecretSuppressionMarker = null;
680
+ this._suppressEnvSecret = false;
681
+ }
682
+
512
683
  _buildHeaders() {
513
684
  const headers = { 'Content-Type': 'application/json' };
514
685
  const secret = this.nodeSecret;
515
686
  if (secret) headers['Authorization'] = 'Bearer ' + secret;
687
+ const secretVersion = this.nodeSecretVersion;
688
+ if (secretVersion) headers['X-EvoMap-Node-Secret-Version'] = String(secretVersion);
516
689
  headers['x-correlation-id'] = crypto.randomUUID();
517
690
  return headers;
518
691
  }
@@ -580,27 +753,64 @@ class LifecycleManager {
580
753
  await throwIfHubUnreachableResponse(res, 'lifecycle hello');
581
754
  this._recordHubReachable();
582
755
  if (!res.ok) {
583
- const errData = await readHubResponseJson(res).catch(() => ({}));
584
- const errMsg = errData?.error || `http_${res.status}`;
756
+ const errText = await readHubResponseText(res).catch(() => '');
757
+ let errData = {};
758
+ try { errData = errText ? JSON.parse(errText) : {}; } catch (_) { /* non-JSON API error */ }
759
+ const errMsg = safeHubErrorCode(errData, res.status);
585
760
  if (res.status === 429) {
586
761
  const retryAfter = parseInt(res.headers.get('retry-after') || '3600', 10);
587
762
  this._helloRateLimitUntil = Date.now() + retryAfter * 1000;
588
763
  this.logger.error(`[lifecycle] hello rate limited (429): retry after ${retryAfter}s`);
589
764
  return { ok: false, error: 'hello_rate_limited', retryAfter };
590
765
  }
591
- this.logger.error(`[lifecycle] hello HTTP ${res.status}: ${errMsg}`);
766
+ const divergenceReason = (res.status === 401 || res.status === 403)
767
+ ? extractSecretDivergenceReason(errData)
768
+ : null;
769
+ if (divergenceReason) {
770
+ this.logger.warn(
771
+ `[lifecycle] hello rejected secret rotation (${res.status}, reason=${divergenceReason}); ` +
772
+ 'local secret divergence detected'
773
+ );
774
+ return {
775
+ ok: false,
776
+ error: SECRET_DIVERGENCE_ERROR,
777
+ reason: divergenceReason,
778
+ statusCode: res.status,
779
+ };
780
+ }
781
+ const safeErrText = errText ? sanitizeHubResponseForLog(errText) : errMsg;
782
+ this.logger.error(`[lifecycle] hello HTTP ${res.status}: ${safeErrText}`);
592
783
  return { ok: false, error: errMsg, statusCode: res.status };
593
784
  }
594
785
 
595
786
  const data = await readHubResponseJson(res);
596
787
 
597
788
  if (data?.payload?.status === 'rejected') {
598
- this.logger.error(`[lifecycle] hello rejected: ${data.payload.reason || 'unknown'}`);
599
- return { ok: false, error: data.payload.reason || 'hello_rejected', response: data };
789
+ const divergenceReason = extractSecretDivergenceReason(data);
790
+ if (divergenceReason) {
791
+ this.logger.warn(
792
+ `[lifecycle] hello rejected secret rotation (reason=${divergenceReason}); ` +
793
+ 'local secret divergence detected'
794
+ );
795
+ return {
796
+ ok: false,
797
+ error: SECRET_DIVERGENCE_ERROR,
798
+ reason: divergenceReason,
799
+ response: data,
800
+ };
801
+ }
802
+ const reason = data.payload.reason || 'unknown';
803
+ const safeReason = sanitizeHubResponseForLog(reason);
804
+ this.logger.error(`[lifecycle] hello rejected: ${safeReason}`);
805
+ const error = String(reason || '').startsWith('node_id_already_claimed')
806
+ ? reason
807
+ : 'hello_rejected';
808
+ return { ok: false, error, response: data };
600
809
  }
601
810
 
602
811
  const secret = data?.payload?.node_secret || data?.node_secret || null;
603
- if (secret && /^[a-f0-9]{64}$/i.test(secret)) {
812
+ const secretVersion = parseNodeSecretVersion(data?.payload?.node_secret_version || data?.node_secret_version);
813
+ if (secret && validNodeSecret(secret)) {
604
814
  this.store.setState('node_secret', secret);
605
815
  // Tag the store entry so the next process that boots into a stale
606
816
  // shell env can recognise this value as hub-authoritative and
@@ -615,9 +825,18 @@ class LifecycleManager {
615
825
  // and overwrite the freshly rotated secret with the stale one,
616
826
  // re-creating the auth loop the previous patch fixed (see #529
617
827
  // and the Bugbot review on PR #22).
618
- this._suppressEnvSecret = true;
828
+ if (getEnvNodeSecret() && getEnvNodeSecret() !== secret) {
829
+ this._markCurrentEnvSecretSuppressed();
830
+ } else {
831
+ this._clearEnvSecretSuppression();
832
+ }
619
833
  this.logger.log('[lifecycle] new node_secret stored from hello response');
620
834
  }
835
+ if (secretVersion) {
836
+ this.store.setState('node_secret_version', String(secretVersion));
837
+ } else {
838
+ this.store.setState('node_secret_version', '');
839
+ }
621
840
 
622
841
  this.store.setState('node_id', nodeId);
623
842
  // Unify proxy node_id with the legacy GEP file. Without this, the
@@ -680,6 +899,7 @@ class LifecycleManager {
680
899
  this._reauthInProgress = true;
681
900
  let manualResetRequired = false;
682
901
  let hubUnreachable = false;
902
+ let droppedDivergedSecret = false;
683
903
  try {
684
904
  for (let attempt = 1; attempt <= MAX_REAUTH_ATTEMPTS; attempt++) {
685
905
  this.logger.warn(`[lifecycle] re-auth attempt ${attempt}/${MAX_REAUTH_ATTEMPTS}: rotating secret via hello...`);
@@ -698,6 +918,18 @@ class LifecycleManager {
698
918
  hubUnreachable = true;
699
919
  break;
700
920
  }
921
+ if (isSecretDivergenceError(helloResult.error)) {
922
+ // The hub has explicitly rejected our current secret as diverged
923
+ // from its record. Drop store/env-derived auth once, then spend
924
+ // the second attempt on an unauthenticated rotate hello instead
925
+ // of backing off while still presenting the same stale Bearer.
926
+ if (attempt < MAX_REAUTH_ATTEMPTS && !droppedDivergedSecret) {
927
+ this._dropLocalSecret(SECRET_DIVERGENCE_ERROR);
928
+ droppedDivergedSecret = true;
929
+ continue;
930
+ }
931
+ break;
932
+ }
701
933
  if (typeof helloResult.error === 'string' && helloResult.error.startsWith('node_id_already_claimed')) {
702
934
  // Hub does not believe we own this nodeId. Our locally cached
703
935
  // secret(s) are useless. Drop them so attempt 2 retries WITHOUT
@@ -770,12 +1002,12 @@ class LifecycleManager {
770
1002
  _dropLocalSecret(reason) {
771
1003
  this.logger.warn(`[lifecycle] dropping cached node_secret (reason=${reason}); next hello will run unauthenticated`);
772
1004
  try { this.store.setState('node_secret', ''); } catch { /* best-effort */ }
1005
+ try { this.store.setState('node_secret_version', ''); } catch { /* best-effort */ }
773
1006
  // Clear the source tag too -- nothing is stored, nothing to attribute.
774
1007
  try { this.store.setState('node_secret_source', ''); } catch { /* best-effort */ }
775
- try { this.store.setState('node_secret_env_suppressed', 'true'); } catch { /* best-effort */ }
776
- // Suppress the env override for this process so _resolveNodeSecret stops
777
- // re-seeding the store with the same stale env value next call.
778
- this._suppressEnvSecret = true;
1008
+ // Suppress only the exact env secret that was just proven stale. If no
1009
+ // env secret is present, do not leave a marker that blocks a future reset.
1010
+ this._markCurrentEnvSecretSuppressed();
779
1011
  }
780
1012
 
781
1013
  _emitManualResetNeeded() {
@@ -787,7 +1019,7 @@ class LifecycleManager {
787
1019
  payload: {
788
1020
  action: 'manual_secret_reset_required',
789
1021
  message:
790
- 'Hub disowns this node_id (node_id_already_claimed). Local node_secret in MailboxStore and A2A_NODE_SECRET env var are both invalid. Visit https://evomap.ai/account, click "Reset Secret" on the agent card, then update A2A_NODE_SECRET (or delete ~/.evomap/mailbox/state.json) and restart proxy.',
1022
+ 'Hub disowns this node_id (node_id_already_claimed). Local node_secret and the current node secret env value are invalid. Visit https://evomap.ai/account, click "Reset Secret" on the agent card, run `evolver reset-local-secret` to clear local secret and env suppression state, then update A2A_NODE_SECRET/EVOMAP_NODE_SECRET and restart proxy. Only changing env before clearing suppression state can keep an older local marker active.',
791
1023
  docs_url: 'https://evomap.ai/account',
792
1024
  },
793
1025
  });
@@ -824,6 +1056,7 @@ class LifecycleManager {
824
1056
  const endpoint = `${this.hubUrl}/a2a/heartbeat`;
825
1057
  const taskMeta = typeof this.getTaskMeta === 'function' ? this.getTaskMeta() : {};
826
1058
  const fp = _getEnvFingerprint();
1059
+ const secretVersion = this.nodeSecretVersion;
827
1060
  const body = {
828
1061
  node_id: this.nodeId,
829
1062
  sender_id: this.nodeId,
@@ -837,6 +1070,10 @@ class LifecycleManager {
837
1070
  ...taskMeta,
838
1071
  },
839
1072
  };
1073
+ if (secretVersion) {
1074
+ body.node_secret_version = secretVersion;
1075
+ body.meta.node_secret_version = secretVersion;
1076
+ }
840
1077
 
841
1078
  try {
842
1079
  const cfg = require('../../config');
@@ -885,7 +1122,8 @@ class LifecycleManager {
885
1122
  if (res.status === 403 || res.status === 401) {
886
1123
  this._consecutiveFailures++;
887
1124
  const errText = await readHubResponseText(res).catch(() => '');
888
- this.logger.error(`[lifecycle] heartbeat auth failed (${res.status}): ${errText}`);
1125
+ const safeErrText = sanitizeHubResponseForLog(errText);
1126
+ this.logger.error(`[lifecycle] heartbeat auth failed (${res.status}): ${safeErrText}`);
889
1127
  if (!_skipReauth) {
890
1128
  const recovered = await this.reAuthenticate();
891
1129
  if (recovered) {
@@ -898,6 +1136,7 @@ class LifecycleManager {
898
1136
 
899
1137
  if (!res.ok) {
900
1138
  const errText = await readHubResponseText(res).catch(() => '');
1139
+ const safeErrText = sanitizeHubResponseForLog(errText);
901
1140
  // 426 Upgrade Required: hub emits this when our evolver_version is
902
1141
  // below the minimum version it requires. The body is JSON of shape
903
1142
  // `{ error: 'evolver_min_version_required', force_update: {...} }`
@@ -922,7 +1161,7 @@ class LifecycleManager {
922
1161
  _maybeTriggerForceUpdateFromHeartbeat(fu, this.logger);
923
1162
  } else {
924
1163
  this.logger.warn(
925
- `[lifecycle] heartbeat HTTP 426 without parseable force_update payload: ${errText}`
1164
+ `[lifecycle] heartbeat HTTP 426 without parseable force_update payload: ${safeErrText}`
926
1165
  );
927
1166
  }
928
1167
  }
@@ -961,7 +1200,7 @@ class LifecycleManager {
961
1200
  }
962
1201
  }
963
1202
  this._consecutiveFailures++;
964
- this.logger.error(`[lifecycle] heartbeat HTTP ${res.status}: ${errText}`);
1203
+ this.logger.error(`[lifecycle] heartbeat HTTP ${res.status}: ${safeErrText}`);
965
1204
  return { ok: false, error: `http_${res.status}`, statusCode: res.status };
966
1205
  }
967
1206
 
@@ -80,6 +80,16 @@ function buildRoutes(store, proxyHandlers, taskMonitor, extensions) {
80
80
  return { body: result };
81
81
  },
82
82
 
83
+ // Reuse-attribution report: the agent declares which fetched assets it
84
+ // actually reused. Forwarded flat (not enveloped) to the hub's
85
+ // /a2a/memory/record; the proxy stamps its own node as sender_id and the
86
+ // hub cross-verifies against AssetFetcher. Best-effort — never throws on a
87
+ // hub error so a report failure can't break the calling agent.
88
+ 'POST /asset/report-reuse': async ({ body }) => {
89
+ const result = await proxyHandlers.reportReuse(body || {});
90
+ return { body: result };
91
+ },
92
+
83
93
  'POST /asset/search': async ({ body }) => {
84
94
  const result = await proxyHandlers.assetSearch(body);
85
95
  return { body: result };
@@ -9,6 +9,7 @@ const {
9
9
  isHubUnreachableError,
10
10
  readHubResponseJson,
11
11
  readHubResponseText,
12
+ sanitizeHubResponseForLog,
12
13
  throwIfHubUnreachableResponse,
13
14
  } = require('../../gep/hubFetch');
14
15
 
@@ -75,12 +76,12 @@ class InboundSync {
75
76
 
76
77
  if (res.status === 403 || res.status === 401) {
77
78
  const errText = await readHubResponseText(res).catch(() => 'unknown');
78
- throw new AuthError(`Hub ${res.status}: ${errText}`, res.status);
79
+ throw new AuthError(`Hub ${res.status}: ${sanitizeHubResponseForLog(errText)}`, res.status);
79
80
  }
80
81
 
81
82
  if (!res.ok) {
82
83
  const errText = await readHubResponseText(res).catch(() => 'unknown');
83
- throw new Error(`Hub returned ${res.status}: ${errText}`);
84
+ throw new Error(`Hub returned ${res.status}: ${sanitizeHubResponseForLog(errText)}`);
84
85
  }
85
86
 
86
87
  const data = await readHubResponseJson(res);
@@ -163,11 +164,11 @@ class InboundSync {
163
164
  this._recordHubReachable();
164
165
  if (res.status === 403 || res.status === 401) {
165
166
  const errText = await readHubResponseText(res).catch(() => 'unknown');
166
- throw new AuthError(`Hub ${res.status}: ${errText}`, res.status);
167
+ throw new AuthError(`Hub ${res.status}: ${sanitizeHubResponseForLog(errText)}`, res.status);
167
168
  }
168
169
  if (!res.ok) {
169
170
  const errText = await readHubResponseText(res).catch(() => 'unknown');
170
- throw new Error(`Hub returned ${res.status}: ${errText}`);
171
+ throw new Error(`Hub returned ${res.status}: ${sanitizeHubResponseForLog(errText)}`);
171
172
  }
172
173
  await drainHubResponse(res);
173
174
  return { acked: delivered.length };
@@ -9,6 +9,7 @@ const {
9
9
  isHubUnreachableError,
10
10
  readHubResponseJson,
11
11
  readHubResponseText,
12
+ sanitizeHubResponseForLog,
12
13
  throwIfHubUnreachableResponse,
13
14
  } = require('../../gep/hubFetch');
14
15
 
@@ -66,7 +67,7 @@ class OutboundSync {
66
67
  if (m.type !== 'proxy_trace') continue;
67
68
  if (!traceUploadEnabled) {
68
69
  rejectedTraceUploads.push({ id: m.id, error: 'proxy trace upload disabled' });
69
- } else if (!isProxyTraceUploadPayloadAllowed(m.payload, process.env)) {
70
+ } else if (!isProxyTraceUploadPayloadAllowed(m.payload, process.env, { store: this.store })) {
70
71
  rejectedTraceUploads.push({ id: m.id, error: 'proxy trace payload rejected' });
71
72
  }
72
73
  }
@@ -109,12 +110,12 @@ class OutboundSync {
109
110
 
110
111
  if (res.status === 403 || res.status === 401) {
111
112
  const errText = await readHubResponseText(res).catch(() => 'unknown');
112
- throw new AuthError(`Hub ${res.status}: ${errText}`, res.status);
113
+ throw new AuthError(`Hub ${res.status}: ${sanitizeHubResponseForLog(errText)}`, res.status);
113
114
  }
114
115
 
115
116
  if (!res.ok) {
116
117
  const errText = await readHubResponseText(res).catch(() => 'unknown');
117
- throw new Error(`Hub returned ${res.status}: ${errText}`);
118
+ throw new Error(`Hub returned ${res.status}: ${sanitizeHubResponseForLog(errText)}`);
118
119
  }
119
120
 
120
121
  const data = await readHubResponseJson(res);