@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.
- package/index.js +234 -31
- package/package.json +1 -1
- 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/forceUpdate.js +499 -119
- 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 +1154 -0
- 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/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/issueReporter.js +86 -0
- package/src/gep/learningSignals.js +1 -1
- package/src/gep/memoryGraph.js +1 -1
- package/src/gep/memoryGraphAdapter.js +1 -1
- package/src/gep/mutation.js +1 -1
- package/src/gep/narrativeMemory.js +1 -1
- package/src/gep/openPRRegistry.js +1 -1
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- 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/sanitize.js +20 -4
- package/src/gep/savingsCore.js +1 -1
- package/src/gep/selector.js +1 -1
- package/src/gep/signals.js +33 -9
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/tokenSavings.js +1 -1
- package/src/gep/workspaceKeychain.js +1 -1
- package/src/proxy/extensions/traceControl.js +1 -1
- package/src/proxy/index.js +54 -4
- package/src/proxy/inject.js +1 -1
- package/src/proxy/lifecycle/manager.js +266 -27
- package/src/proxy/server/routes.js +10 -0
- package/src/proxy/sync/inbound.js +5 -4
- package/src/proxy/sync/outbound.js +4 -3
- package/src/proxy/trace/extractor.js +1 -1
- 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.
|
|
468
|
-
|
|
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
|
-
|
|
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' &&
|
|
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
|
-
'
|
|
486
|
-
'
|
|
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 (
|
|
492
|
-
this.
|
|
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
|
|
584
|
-
|
|
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
|
-
|
|
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
|
-
|
|
599
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
776
|
-
//
|
|
777
|
-
|
|
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
|
|
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
|
-
|
|
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: ${
|
|
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}: ${
|
|
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);
|