overmind-mcp 2.8.28 → 2.8.34
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/dist/bridge/BridgeProxy.d.ts +2 -1
- package/dist/bridge/BridgeProxy.d.ts.map +1 -1
- package/dist/bridge/BridgeProxy.js +2 -1
- package/dist/bridge/BridgeProxy.js.map +1 -1
- package/dist/bridge/OverBridgeService.d.ts +6 -4
- package/dist/bridge/OverBridgeService.d.ts.map +1 -1
- package/dist/bridge/OverBridgeService.js +6 -4
- package/dist/bridge/OverBridgeService.js.map +1 -1
- package/dist/bridge/index.d.ts +2 -2
- package/dist/bridge/index.d.ts.map +1 -1
- package/dist/bridge/index.js +4 -4
- package/dist/bridge/index.js.map +1 -1
- package/dist/lib/config.d.ts +41 -20
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +85 -59
- package/dist/lib/config.js.map +1 -1
- package/dist/services/NousHermesRunner.d.ts.map +1 -1
- package/dist/services/NousHermesRunner.js +219 -261
- package/dist/services/NousHermesRunner.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
4
|
import { exec } from 'child_process';
|
|
5
5
|
import { promisify } from 'util';
|
|
6
|
-
import { CONFIG, resolveConfigPath, getWorkspaceDir, getAgentHermesHome, getAgentOvermindHome } from '../lib/config.js';
|
|
6
|
+
import { CONFIG, resolveConfigPath, getWorkspaceDir, getAgentHermesHome, getAgentOvermindHome, getSharedHermesHome } from '../lib/config.js';
|
|
7
7
|
import { getLastSessionId, saveSessionId } from '../lib/sessions.js';
|
|
8
8
|
import { linkSessionToPid } from '../lib/processRegistry.js';
|
|
9
9
|
import { interpolateEnvVars } from '../lib/envUtils.js';
|
|
@@ -297,7 +297,14 @@ export class NousHermesRunner {
|
|
|
297
297
|
'MISTRAL_API_KEY_6', 'MISTRAL_API_KEY_7', 'MISTRAL_API_KEY_E', 'MISTRAL_API_KEY_Y',
|
|
298
298
|
];
|
|
299
299
|
if (agentName) {
|
|
300
|
-
|
|
300
|
+
// Locate the per-agent SOUL.md (system prompt). We support the canonical
|
|
301
|
+
// Hermes layout (HERMES_HOME/agents/<name>/SOUL.md) and a one-shot legacy
|
|
302
|
+
// path (HERMES_HOME/agent_<name>/.hermes/SOUL.md) for existing installs.
|
|
303
|
+
// The canonical path wins; the legacy is fallback so we don't break
|
|
304
|
+
// agents that haven't been migrated yet.
|
|
305
|
+
const canonicalSoul = path.join(overmindHermesSubPath, 'SOUL.md');
|
|
306
|
+
const legacySoul = path.join(getSharedHermesHome(), `agent_${agentName}`, '.hermes', 'SOUL.md');
|
|
307
|
+
const agentPromptPath = fs.existsSync(canonicalSoul) ? canonicalSoul : legacySoul;
|
|
301
308
|
if (fs.existsSync(agentPromptPath)) {
|
|
302
309
|
systemPrompt = fs.readFileSync(agentPromptPath, 'utf8');
|
|
303
310
|
}
|
|
@@ -378,7 +385,21 @@ export class NousHermesRunner {
|
|
|
378
385
|
catch (e) {
|
|
379
386
|
logger.warn({ error: e }, `Failed to process settings/mcp configurations for Hermes agent ${agentName}`);
|
|
380
387
|
}
|
|
381
|
-
// Load environment from isolated .env file
|
|
388
|
+
// Load environment from the agent's isolated .hermes/.env file, BUT only as a
|
|
389
|
+
// fallback for keys that the explicit settings_<agent>.json did NOT set.
|
|
390
|
+
//
|
|
391
|
+
// CRITICAL (2.8.29): The .hermes/.env file is a STALE WRITE of the previous
|
|
392
|
+
// spawn — it gets re-written by the runner itself at line ~1013, but if a
|
|
393
|
+
// user's first run was for Z.AI and they later switch the agent to MiniMax
|
|
394
|
+
// CN, the stale .hermes/.env from the previous run gets re-loaded into
|
|
395
|
+
// agentCustomEnv HERE, OVERWRITING the MiniMax settings that were just merged
|
|
396
|
+
// from settings_<agent>.json in the block above (line ~412). Symptom: the
|
|
397
|
+
// agent silently reverts to the old provider (e.g. Z.AI glm-5.1) on every
|
|
398
|
+
// spawn, causing persistent 401s.
|
|
399
|
+
//
|
|
400
|
+
// Fix: only load keys from .hermes/.env that are NOT already in agentCustomEnv
|
|
401
|
+
// (i.e. settings_<agent>.json wins; .hermes/.env is a fallback for unrelated
|
|
402
|
+
// custom vars the user might have set manually).
|
|
382
403
|
const envPath = path.join(overmindHermesSubPath, '.env');
|
|
383
404
|
if (fs.existsSync(envPath)) {
|
|
384
405
|
try {
|
|
@@ -396,7 +417,10 @@ export class NousHermesRunner {
|
|
|
396
417
|
value = value.slice(1, -1);
|
|
397
418
|
else if (value.startsWith("'") && value.endsWith("'"))
|
|
398
419
|
value = value.slice(1, -1);
|
|
399
|
-
if (key) {
|
|
420
|
+
if (key && agentCustomEnv[key] === undefined) {
|
|
421
|
+
// Only fill in keys that settings_<agent>.json did NOT set.
|
|
422
|
+
// settings_<agent>.json is the user's source of truth; .hermes/.env
|
|
423
|
+
// is a stale write from a previous spawn and must not override it.
|
|
400
424
|
agentCustomEnv[key] = value;
|
|
401
425
|
}
|
|
402
426
|
});
|
|
@@ -425,18 +449,25 @@ export class NousHermesRunner {
|
|
|
425
449
|
// Build CLI args: chat -q (persistent session, NOT -z oneshot)
|
|
426
450
|
// -z + --resume doesn't work — resume is ignored in oneshot mode
|
|
427
451
|
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
452
|
+
// 2.8.33: RE-ADD --provider for MiniMax/Z.AI cases. The empirical 2.8.28
|
|
453
|
+
// observation ("`hermes chat -q --provider minimax-cn` 401s while `--yolo`
|
|
454
|
+
// alone works") was based on a specific `Hermes-MiniMax-2.bat` test
|
|
455
|
+
// where the env was set perfectly. In our sniperbot_analyst scenario
|
|
456
|
+
// (a real production setup with stale state, multiple providers in the
|
|
457
|
+
// auth.json pool, and Hermes upstream's auto-router that picks
|
|
458
|
+
// openrouter for `MiniMax-M3` as an OpenRouter alias), NOT passing
|
|
459
|
+
// --provider makes Hermes upstream fall back to openrouter, which
|
|
460
|
+
// then 401s because the OPENROUTER_API_KEY is purged.
|
|
461
|
+
//
|
|
462
|
+
// So: pass --provider when we have a resolved provider that matches
|
|
463
|
+
// a registered plugin. This forces Hermes upstream to use the right
|
|
464
|
+
// plugin profile (and the right credential pool bucket).
|
|
435
465
|
const cleanArgs = ['chat', '-q', cliPrompt, '-Q'];
|
|
436
466
|
cleanArgs.push('--model', finalModel);
|
|
437
|
-
// resolvedProvider is logged for debugging but NOT passed as --provider.
|
|
438
467
|
if (options.provider || resolvedProvider) {
|
|
439
|
-
|
|
468
|
+
const provider = (options.provider || resolvedProvider);
|
|
469
|
+
cleanArgs.push('--provider', provider);
|
|
470
|
+
logger.info({ agentName, provider, model: finalModel }, '[HERMES_ARGS] Passing --provider (2.8.33: needed to bypass upstream auto-router that picked openrouter for MiniMax-M3).');
|
|
440
471
|
}
|
|
441
472
|
if (sessionId)
|
|
442
473
|
cleanArgs.push('--resume', sessionId);
|
|
@@ -618,10 +649,12 @@ export class NousHermesRunner {
|
|
|
618
649
|
lower.includes('service unavailable') || lower.includes('500') ||
|
|
619
650
|
lower.includes('internal server error');
|
|
620
651
|
};
|
|
621
|
-
// HERMES_HOME setup
|
|
652
|
+
// HERMES_HOME setup — the SHARED root, not the per-agent home.
|
|
653
|
+
// Hermes upstream resolves `agents/<name>/`, `config.yaml`, `auth.json`, etc.
|
|
654
|
+
// relative to this single root. We do NOT seed `agentCustomEnv.HERMES_HOME`
|
|
655
|
+
// here anymore because spawnHermes() sets it explicitly from getSharedHermesHome().
|
|
622
656
|
if (!fs.existsSync(overmindHermesSubPath))
|
|
623
657
|
fs.mkdirSync(overmindHermesSubPath, { recursive: true });
|
|
624
|
-
agentCustomEnv.HERMES_HOME = overmindHermesSubPath;
|
|
625
658
|
// HOME / USERPROFILE override: point Hermes at the parent .overmind dir,
|
|
626
659
|
// NOT the cwd. This makes relative .hermes lookups inside Hermes
|
|
627
660
|
// (e.g. `~/.hermes/.env` resolution) resolve to the same canonical
|
|
@@ -630,86 +663,129 @@ export class NousHermesRunner {
|
|
|
630
663
|
agentCustomEnv.USERPROFILE = overmindHermesPath;
|
|
631
664
|
else
|
|
632
665
|
agentCustomEnv.HOME = overmindHermesPath;
|
|
633
|
-
//
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
666
|
+
// AbortSignal
|
|
667
|
+
if (options.signal?.aborted)
|
|
668
|
+
return Promise.reject(new Error('ABORTED'));
|
|
669
|
+
// =============================================================
|
|
670
|
+
// 2.8.30 — WRITE CANONICAL Hermes SETTINGS
|
|
671
|
+
// =============================================================
|
|
672
|
+
// Hermes upstream uses the standard appdirs-style layout:
|
|
673
|
+
// <HERMES_HOME>/agents/<name>/settings.json ← per-agent env, mcp, persona
|
|
674
|
+
// <HERMES_HOME>/agents/<name>/SOUL.md ← per-agent system prompt
|
|
675
|
+
// <HERMES_HOME>/config.yaml ← global, Hermes manages
|
|
676
|
+
// <HERMES_HOME>/auth.json ← global, Hermes manages
|
|
677
|
+
//
|
|
678
|
+
// Overmind's only job here is to:
|
|
679
|
+
// 1. Convert Workflow/.claude/settings_<name>.json (Overmind runner format)
|
|
680
|
+
// into the canonical Hermes format and write it to <HERMES_HOME>/agents/<name>/settings.json
|
|
681
|
+
// 2. Make sure <HERMES_HOME>/agents/<name>/SOUL.md exists (canonical path)
|
|
682
|
+
// 3. Let Hermes upstream manage config.yaml, auth.json, sessions/, etc.
|
|
683
|
+
//
|
|
684
|
+
// This replaces the previous "polylgot" hack where Overmind wrote
|
|
685
|
+
// `<HERMES_HOME>/agent_<name>/.hermes/.env`, `.hermes/config.yaml`, and
|
|
686
|
+
// `.hermes/auth.json` — files that don't match Hermes's expected layout
|
|
687
|
+
// and caused credential drift + silent 401s.
|
|
688
|
+
if (agentName) {
|
|
689
|
+
const agentHome = overmindHermesSubPath; // = <HERMES_HOME>/agents/<name>/
|
|
690
|
+
if (!fs.existsSync(agentHome))
|
|
691
|
+
fs.mkdirSync(agentHome, { recursive: true });
|
|
692
|
+
// Build the canonical Hermes settings.json from the agent's settings_<name>.json.
|
|
693
|
+
// We preserve: env, enableAllProjectMcpServers, enabledMcpjsonServers, agent, runner.
|
|
694
|
+
// We do NOT touch: config.yaml, auth.json, .env — Hermes upstream owns those.
|
|
695
|
+
const tmpAgentSettings = path.join(agentHome, 'settings.json');
|
|
696
|
+
const settingsJson = {};
|
|
697
|
+
// Read the interpolated settings the runner just merged (line ~412 above)
|
|
698
|
+
// and copy the Hermes-relevant fields. We don't re-interpolate here because
|
|
699
|
+
// the caller already did it on the raw `settings` object.
|
|
700
|
+
if (tmpSettingsPath && fs.existsSync(tmpSettingsPath)) {
|
|
701
|
+
try {
|
|
702
|
+
const raw = JSON.parse(fs.readFileSync(tmpSettingsPath, 'utf8'));
|
|
703
|
+
if (raw.env)
|
|
704
|
+
settingsJson.env = { ...raw.env };
|
|
705
|
+
if (raw.enableAllProjectMcpServers !== undefined) {
|
|
706
|
+
settingsJson.enableAllProjectMcpServers = raw.enableAllProjectMcpServers;
|
|
707
|
+
}
|
|
708
|
+
if (Array.isArray(raw.enabledMcpjsonServers)) {
|
|
709
|
+
settingsJson.enabledMcpjsonServers = raw.enabledMcpjsonServers;
|
|
659
710
|
}
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
catch (e) {
|
|
663
|
-
logger.warn({ envPath: dotPath, error: e }, 'Failed to read existing agent env file for deduplication');
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
for (const [k, v] of Object.entries(agentCustomEnv)) {
|
|
667
|
-
if (typeof v === 'string' && v.length > 0 && credRegex.test(k)) {
|
|
668
|
-
if (openRouterPrefixes.some(p => k.toUpperCase().startsWith(p)))
|
|
669
|
-
continue;
|
|
670
|
-
envMap.set(k, v);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
const finalDotEntries = [];
|
|
674
|
-
for (const [k, v] of envMap.entries()) {
|
|
675
|
-
finalDotEntries.push(`${k}=${v}`);
|
|
676
|
-
}
|
|
677
|
-
fs.writeFileSync(dotPath, finalDotEntries.join('\n') + '\n', 'utf8');
|
|
678
|
-
// Generate config.yaml in HERMES_HOME (MCP servers)
|
|
679
|
-
if (tmpMcpPath && fs.existsSync(tmpMcpPath)) {
|
|
680
|
-
try {
|
|
681
|
-
const mc = JSON.parse(fs.readFileSync(tmpMcpPath, 'utf8'));
|
|
682
|
-
const yamlPath = path.join(overmindHermesSubPath, 'config.yaml');
|
|
683
|
-
// Preserve existing config.yaml (tts, llm, etc.) — merge mcp_servers only
|
|
684
|
-
let existingYaml = '';
|
|
685
|
-
if (fs.existsSync(yamlPath)) {
|
|
686
|
-
existingYaml = fs.readFileSync(yamlPath, 'utf8');
|
|
687
711
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
for (const [name, srv] of Object.entries(mc.mcpServers || {})) {
|
|
691
|
-
const s = srv;
|
|
692
|
-
newMcpSection += ` ${name}:\n`;
|
|
693
|
-
if (s.url)
|
|
694
|
-
newMcpSection += ` url: "${s.url}"\n`;
|
|
695
|
-
if (s.command)
|
|
696
|
-
newMcpSection += ` command: "${s.command}"\n`;
|
|
712
|
+
catch (e) {
|
|
713
|
+
logger.warn({ tmpSettingsPath, error: e }, 'Failed to read tmp settings for canonical write');
|
|
697
714
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
715
|
+
}
|
|
716
|
+
// ============================================================
|
|
717
|
+
// 2.8.31 — INJECT PROVIDER-SPECIFIC ENV VARS INTO settings.json
|
|
718
|
+
// ============================================================
|
|
719
|
+
// The Hermes plugins (e.g. `minimax`, `zai`, `openai`) read PROVIDER-
|
|
720
|
+
// SPECIFIC env vars from the per-agent settings.json — NOT the generic
|
|
721
|
+
// `ANTHROPIC_AUTH_TOKEN`. For example the `minimax-cn` plugin reads
|
|
722
|
+
// `MINIMAX_CN_API_KEY`, and the `minimax` (GLOBAL) plugin reads
|
|
723
|
+
// `MINIMAX_API_KEY`. If those vars aren't in the agent's settings.json,
|
|
724
|
+
// the plugin can't find the credential and the upstream falls back to
|
|
725
|
+
// the wrong provider (we saw it pick `openrouter` or `nvidia` instead
|
|
726
|
+
// of `minimax-cn`).
|
|
727
|
+
//
|
|
728
|
+
// We inject by:
|
|
729
|
+
// 1. Detecting the provider from the user's ANTHROPIC_BASE_URL hint
|
|
730
|
+
// (api.minimaxi.com → CN, api.minimax.io → GLOBAL, etc.).
|
|
731
|
+
// 2. For MiniMax: ONLY seed the matching env var (CN vs GLOBAL) so
|
|
732
|
+
// the upstream plugin's first-match resolver picks the right one.
|
|
733
|
+
// Do NOT seed both — that was the bug in 2.8.30 (caused it to pick
|
|
734
|
+
// GLOBAL even when the URL was CN).
|
|
735
|
+
// 3. For Z.AI: seed ZAI_ANTHROPIC_FALLBACK_KEY + GLM_API_KEY.
|
|
736
|
+
// 4. Leave ANTHROPIC_AUTH_TOKEN in place as a fallback (some Hermes
|
|
737
|
+
// code paths still read it generically).
|
|
738
|
+
const envObj = settingsJson.env;
|
|
739
|
+
if (envObj) {
|
|
740
|
+
const baseUrl = (envObj['ANTHROPIC_BASE_URL'] || '').toLowerCase();
|
|
741
|
+
const anthropicToken = envObj['ANTHROPIC_AUTH_TOKEN'] || envObj['ANTHROPIC_API_KEY'] || '';
|
|
742
|
+
if (anthropicToken && (anthropicToken.startsWith('sk-cp-') || anthropicToken.startsWith('sk-mm-'))) {
|
|
743
|
+
// MiniMax token — pick CN vs GLOBAL based on URL
|
|
744
|
+
if (baseUrl.includes('minimaxi')) {
|
|
745
|
+
// CN: api.minimaxi.com
|
|
746
|
+
envObj['MINIMAX_CN_API_KEY'] = anthropicToken;
|
|
747
|
+
// The Hermes `providers.py` resolver reads `MINIMAX_CN_BASE_URL`
|
|
748
|
+
// (NOT just `ANTHROPIC_BASE_URL`) to dispatch to the CN plugin
|
|
749
|
+
// profile. Seed it explicitly so the provider resolver picks
|
|
750
|
+
// `minimax-cn` (not `minimax` GLOBAL on first-match).
|
|
751
|
+
envObj['MINIMAX_CN_BASE_URL'] = 'https://api.minimaxi.com/anthropic';
|
|
752
|
+
logger.info({ agentName, cnApiKeySet: true, cnBaseUrlSet: true, globalApiKeySet: false, detectedFrom: 'api.minimaxi.com (CN)' }, '[SETTINGS_JSON] Seeded MINIMAX_CN_API_KEY + MINIMAX_CN_BASE_URL (CN plugin resolver).');
|
|
753
|
+
}
|
|
754
|
+
else if (baseUrl.includes('minimax') || baseUrl === '') {
|
|
755
|
+
// GLOBAL: api.minimax.io, OR no URL hint (default to CN per OVERMIND_MINIMAX_DEFAULT=cn)
|
|
756
|
+
const defaultCn = (process.env.OVERMIND_MINIMAX_DEFAULT || 'cn').toLowerCase() === 'cn';
|
|
757
|
+
if (defaultCn) {
|
|
758
|
+
envObj['MINIMAX_CN_API_KEY'] = anthropicToken;
|
|
759
|
+
envObj['MINIMAX_CN_BASE_URL'] = 'https://api.minimaxi.com/anthropic';
|
|
760
|
+
logger.info({ agentName, detectedFrom: 'no URL + OVERMIND_MINIMAX_DEFAULT=cn' }, '[SETTINGS_JSON] Seeded MINIMAX_CN_API_KEY + MINIMAX_CN_BASE_URL (default CN per OVERMIND_MINIMAX_DEFAULT).');
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
envObj['MINIMAX_API_KEY'] = anthropicToken;
|
|
764
|
+
envObj['MINIMAX_BASE_URL'] = 'https://api.minimax.io/anthropic';
|
|
765
|
+
logger.info({ agentName, detectedFrom: 'api.minimax.io (GLOBAL)' }, '[SETTINGS_JSON] Seeded MINIMAX_API_KEY + MINIMAX_BASE_URL (GLOBAL plugin resolver).');
|
|
766
|
+
}
|
|
767
|
+
}
|
|
702
768
|
}
|
|
703
|
-
else {
|
|
704
|
-
|
|
769
|
+
else if (anthropicToken && /^[0-9a-f]{32}(\.[0-9a-zA-Z]+)?$/i.test(anthropicToken)) {
|
|
770
|
+
// Z.AI token (32hex or 32hex.32hex)
|
|
771
|
+
envObj['ZAI_ANTHROPIC_FALLBACK_KEY'] = anthropicToken;
|
|
772
|
+
envObj['GLM_API_KEY'] = anthropicToken;
|
|
773
|
+
logger.info({ agentName }, '[SETTINGS_JSON] Seeded ZAI_ANTHROPIC_FALLBACK_KEY + GLM_API_KEY (Z.AI token).');
|
|
705
774
|
}
|
|
706
|
-
fs.writeFileSync(yamlPath, finalYaml, 'utf8');
|
|
707
|
-
if (!silent)
|
|
708
|
-
console.error(`[NousHermesRunner] MCP config.yaml written to ${yamlPath}`);
|
|
709
|
-
}
|
|
710
|
-
catch (e) {
|
|
711
|
-
console.error(`[NousHermesRunner] config.yaml error: ${e}`);
|
|
712
775
|
}
|
|
776
|
+
// Always declare the agent name + runner so Hermes can route to the right MCP servers.
|
|
777
|
+
settingsJson.agent = agentName;
|
|
778
|
+
settingsJson.runner = 'hermes';
|
|
779
|
+
fs.writeFileSync(tmpAgentSettings, JSON.stringify(settingsJson, null, 2) + '\n', 'utf8');
|
|
780
|
+
// 2.8.32: DO NOT push tmpAgentSettings to this.tempFiles — the
|
|
781
|
+
// canonical settings.json is the AGENT'S PERMANENT Hermes config,
|
|
782
|
+
// not a temp file. cleanupTempFiles() (called in finally + after
|
|
783
|
+
// spawn) would otherwise unlink it on every spawn, forcing the
|
|
784
|
+
// Hermes upstream plugin resolver to re-derive provider routing
|
|
785
|
+
// from the (now-empty) .env block on the next run. This was the
|
|
786
|
+
// root cause of the 13:51 "Erreur inconnue" — manual settings.json
|
|
787
|
+
// edits were being silently deleted after each spawn.
|
|
788
|
+
logger.info({ agentName, settingsPath: tmpAgentSettings, envKeys: Object.keys(settingsJson.env || {}).length }, '[HERMES] Wrote canonical agents/<name>/settings.json (env block from settings_<name>.json + provider-specific seeds).');
|
|
713
789
|
}
|
|
714
790
|
// AbortSignal
|
|
715
791
|
if (options.signal?.aborted)
|
|
@@ -734,187 +810,63 @@ export class NousHermesRunner {
|
|
|
734
810
|
}
|
|
735
811
|
}
|
|
736
812
|
};
|
|
737
|
-
const writeAuthJson = (tokenInfo) => {
|
|
738
|
-
if (!tokenInfo || !overmindHermesSubPath)
|
|
739
|
-
return;
|
|
740
|
-
try {
|
|
741
|
-
const authPath = path.join(overmindHermesSubPath, 'auth.json');
|
|
742
|
-
// Read existing auth.json to preserve non-credential_pool state
|
|
743
|
-
// (e.g. oauth tokens, settings, version). But we PRUNE credential_pool
|
|
744
|
-
// entries for OTHER providers — those are stale from previous provider
|
|
745
|
-
// configs and Hermes may pick them up by mistake, causing silent 401s
|
|
746
|
-
// on the wrong endpoint. This is the source of the "auth.json drift"
|
|
747
|
-
// bug where the runner would seed `minimax-cn` credentials while a stale
|
|
748
|
-
// `zai` entry with last_status="exhausted" still existed in the pool.
|
|
749
|
-
let preservedAuth = { version: 1, providers: {} };
|
|
750
|
-
if (fs.existsSync(authPath)) {
|
|
751
|
-
try {
|
|
752
|
-
const parsed = JSON.parse(fs.readFileSync(authPath, 'utf8'));
|
|
753
|
-
// Keep the version + any oauth providers; drop credential_pool entirely
|
|
754
|
-
// (it will be re-seeded below with only the effectiveProvider's entries).
|
|
755
|
-
preservedAuth = {
|
|
756
|
-
version: parsed.version ?? 1,
|
|
757
|
-
providers: parsed.providers ?? {},
|
|
758
|
-
};
|
|
759
|
-
}
|
|
760
|
-
catch (e) {
|
|
761
|
-
logger.warn({ authPath, error: e }, 'auth.json was malformed; re-creating from scratch');
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
const auth = {
|
|
765
|
-
...preservedAuth,
|
|
766
|
-
credential_pool: {},
|
|
767
|
-
};
|
|
768
|
-
const cleanCp = auth.credential_pool;
|
|
769
|
-
// Determine effective provider from MULTIPLE signals
|
|
770
|
-
// Priority: TOKEN PREFIX (most reliable) > BASE_URL (very reliable) > settings.ANTHROPIC_PROVIDER (hint only)
|
|
771
|
-
// The user can put anything in settings.ANTHROPIC_PROVIDER — we don't blindly trust it.
|
|
772
|
-
const baseUrlHint = agentCustomEnv['ANTHROPIC_BASE_URL'] || agentCustomEnv['GLM_BASE_URL'] || '';
|
|
773
|
-
// First, detect from token prefix
|
|
774
|
-
const detectedFromToken = detectTokenProvider(tokenInfo.tokenValue);
|
|
775
|
-
// Then, detect from base URL
|
|
776
|
-
let detectedFromUrl = null;
|
|
777
|
-
if (baseUrlHint) {
|
|
778
|
-
const url = baseUrlHint.toLowerCase();
|
|
779
|
-
if (url.includes('minimaxi')) {
|
|
780
|
-
// The "i" suffix in api.minimaxi.com is the CN-specific endpoint
|
|
781
|
-
detectedFromUrl = 'minimax-cn';
|
|
782
|
-
}
|
|
783
|
-
else if (url.includes('minimax')) {
|
|
784
|
-
// api.minimax.com (no i) is the GLOBAL endpoint
|
|
785
|
-
detectedFromUrl = 'minimax';
|
|
786
|
-
}
|
|
787
|
-
else if (url.includes('z.ai') || url.includes('bigmodel') || url.includes('zhipu')) {
|
|
788
|
-
detectedFromUrl = 'zai';
|
|
789
|
-
}
|
|
790
|
-
else if (url.includes('anthropic.com')) {
|
|
791
|
-
detectedFromUrl = 'anthropic';
|
|
792
|
-
}
|
|
793
|
-
else if (url.includes('openai.com')) {
|
|
794
|
-
detectedFromUrl = 'openai';
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
// Then, the hint from settings
|
|
798
|
-
const settingsHint = resolvedProvider || '';
|
|
799
|
-
// Voting: token > URL > settings
|
|
800
|
-
// SPECIAL CASE: if token says "minimax" and URL says "minimax-cn" (or vice versa),
|
|
801
|
-
// the URL wins because the token prefix sk-cp- is shared between both endpoints.
|
|
802
|
-
// The URL is the only signal that can disambiguate CN vs GLOBAL.
|
|
803
|
-
//
|
|
804
|
-
// DEFAULT FOR MiniMax WHEN AMBIGUOUS:
|
|
805
|
-
// The sk-cp- prefix is shared between MiniMax GLOBAL and MiniMax CN. The
|
|
806
|
-
// URL is the only signal that disambiguates. For users whose setup
|
|
807
|
-
// exclusively uses CN tokens (the most common case for non-China
|
|
808
|
-
// operators), an absent/ambiguous URL should default to CN rather than
|
|
809
|
-
// silently picking GLOBAL and getting a 401. Override via env var:
|
|
810
|
-
// OVERMIND_MINIMAX_DEFAULT=cn (default: CN when ambiguous)
|
|
811
|
-
// OVERMIND_MINIMAX_DEFAULT=global (treat sk-cp-* as GLOBAL)
|
|
812
|
-
// OVERMIND_MINIMAX_DEFAULT=auto (never infer, require URL to disambiguate)
|
|
813
|
-
const minimaxDefault = (process.env.OVERMIND_MINIMAX_DEFAULT || 'cn').toLowerCase();
|
|
814
|
-
const minimaxDefaults = { cn: 'minimax-cn', global: 'minimax', auto: 'minimax' };
|
|
815
|
-
const minimaxFallback = minimaxDefaults[minimaxDefault] || minimaxDefaults.cn;
|
|
816
|
-
let effectiveProvider;
|
|
817
|
-
if (detectedFromToken.provider === 'minimax' && detectedFromUrl === 'minimax-cn') {
|
|
818
|
-
// URL has more specific info than the token prefix
|
|
819
|
-
effectiveProvider = 'minimax-cn';
|
|
820
|
-
logger.info({ agentName, tokenSays: 'minimax', urlSays: 'minimax-cn', settingsHint }, '[SUBTILISATION] URL is more specific than token prefix (minimax vs minimax-cn) — using URL.');
|
|
821
|
-
}
|
|
822
|
-
else if (detectedFromToken.provider === 'minimax-cn' && detectedFromUrl === 'minimax') {
|
|
823
|
-
effectiveProvider = 'minimax';
|
|
824
|
-
logger.info({ agentName, tokenSays: 'minimax-cn', urlSays: 'minimax', settingsHint }, '[SUBTILISATION] URL is more specific than token prefix (minimax vs minimax-cn) — using URL.');
|
|
825
|
-
}
|
|
826
|
-
else if (detectedFromToken.provider === 'minimax' && !detectedFromUrl) {
|
|
827
|
-
// Token says MiniMax, no URL hint — use OVERMIND_MINIMAX_DEFAULT
|
|
828
|
-
effectiveProvider = minimaxFallback;
|
|
829
|
-
logger.info({ agentName, tokenSays: 'minimax', urlSays: '(none)', minimaxDefault, effectiveProvider }, '[SUBTILISATION] MiniMax token without explicit URL — applying OVERMIND_MINIMAX_DEFAULT.');
|
|
830
|
-
}
|
|
831
|
-
else if (detectedFromToken.provider !== 'unknown') {
|
|
832
|
-
effectiveProvider = detectedFromToken.provider;
|
|
833
|
-
if (settingsHint && settingsHint !== effectiveProvider) {
|
|
834
|
-
logger.warn({ agentName, settingsHint, tokenSays: effectiveProvider, urlSays: detectedFromUrl }, '[SUBTILISATION] settings.ANTHROPIC_PROVIDER contradicts token prefix — using token.');
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
else if (detectedFromUrl) {
|
|
838
|
-
effectiveProvider = detectedFromUrl;
|
|
839
|
-
if (settingsHint && settingsHint !== effectiveProvider) {
|
|
840
|
-
logger.warn({ agentName, settingsHint, urlSays: effectiveProvider, tokenSays: detectedFromToken.provider }, '[SUBTILISATION] settings.ANTHROPIC_PROVIDER contradicts BASE_URL — using URL.');
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
else if (settingsHint) {
|
|
844
|
-
effectiveProvider = settingsHint;
|
|
845
|
-
}
|
|
846
|
-
else {
|
|
847
|
-
effectiveProvider = 'zai';
|
|
848
|
-
}
|
|
849
|
-
cleanCp[effectiveProvider] = [{
|
|
850
|
-
id: `${effectiveProvider}-default`, label: tokenInfo.tokenEnvKey, auth_type: 'api_key',
|
|
851
|
-
priority: 0, source: `env:${tokenInfo.tokenEnvKey}`, access_token: tokenInfo.tokenValue,
|
|
852
|
-
last_status: null, last_error_code: null,
|
|
853
|
-
base_url: baseUrlHint || defaultBaseUrlFor(effectiveProvider),
|
|
854
|
-
request_count: 0,
|
|
855
|
-
}];
|
|
856
|
-
fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
|
|
857
|
-
// ============================================================
|
|
858
|
-
// Write .env for HERMES_HOME — emit the 4 canonical fields
|
|
859
|
-
// Hermes needs: ANTHROPIC_MODEL, ANTHROPIC_AUTH_TOKEN,
|
|
860
|
-
// ANTHROPIC_PROVIDER, ANTHROPIC_BASE_URL
|
|
861
|
-
// ============================================================
|
|
862
|
-
const dotEnvPath = path.join(overmindHermesSubPath, '.env');
|
|
863
|
-
const dotLines = [];
|
|
864
|
-
// 1. ANTHROPIC_MODEL (always — Hermes needs it)
|
|
865
|
-
if (finalModel) {
|
|
866
|
-
dotLines.push(`ANTHROPIC_MODEL=${finalModel}`);
|
|
867
|
-
}
|
|
868
|
-
// 2. ANTHROPIC_PROVIDER (the kebab-case provider name)
|
|
869
|
-
dotLines.push(`ANTHROPIC_PROVIDER=${effectiveProvider}`);
|
|
870
|
-
// 3. ANTHROPIC_BASE_URL (from settings, or fallback)
|
|
871
|
-
const resolvedBaseUrl = baseUrlHint || defaultBaseUrlFor(effectiveProvider);
|
|
872
|
-
dotLines.push(`ANTHROPIC_BASE_URL=${resolvedBaseUrl}`);
|
|
873
|
-
// 4. ANTHROPIC_AUTH_TOKEN = literal token value (for backward compat with older Hermes versions)
|
|
874
|
-
// (Hermes reads this env var directly — no more provider-specific mapping)
|
|
875
|
-
dotLines.push(`ANTHROPIC_AUTH_TOKEN=${tokenInfo.tokenValue}`);
|
|
876
|
-
// 5. ALSO seed the provider-specific env var for plugins that need it
|
|
877
|
-
// For MiniMax/Z.AI plugins, the provider-specific var is the PRIMARY key
|
|
878
|
-
// the plugin reads. The .bat launchers in C:\Users\Deamon\Desktop\launcher\
|
|
879
|
-
// set MINIMAX_CN_API_KEY directly (not ANTHROPIC_AUTH_TOKEN), confirming
|
|
880
|
-
// that this is what the upstream plugin actually consumes.
|
|
881
|
-
if (effectiveProvider === 'minimax' || effectiveProvider === 'minimax-cn') {
|
|
882
|
-
// Both: the plugin reads whichever is set
|
|
883
|
-
if (effectiveProvider === 'minimax-cn') {
|
|
884
|
-
dotLines.push(`MINIMAX_CN_API_KEY=${tokenInfo.tokenValue}`);
|
|
885
|
-
}
|
|
886
|
-
else {
|
|
887
|
-
dotLines.push(`MINIMAX_API_KEY=${tokenInfo.tokenValue}`);
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
else if (effectiveProvider === 'zai' || effectiveProvider === 'z-ai') {
|
|
891
|
-
dotLines.push(`GLM_API_KEY=${tokenInfo.tokenValue}`);
|
|
892
|
-
dotLines.push(`ZAI_ANTHROPIC_FALLBACK_KEY=${tokenInfo.tokenValue}`);
|
|
893
|
-
}
|
|
894
|
-
else if (effectiveProvider === 'openai') {
|
|
895
|
-
dotLines.push(`OPENAI_API_KEY=${tokenInfo.tokenValue}`);
|
|
896
|
-
}
|
|
897
|
-
else if (effectiveProvider === 'anthropic') {
|
|
898
|
-
// ANTHROPIC_AUTH_TOKEN already set above
|
|
899
|
-
}
|
|
900
|
-
fs.writeFileSync(dotEnvPath, dotLines.join('\n') + '\n', 'utf8');
|
|
901
|
-
logger.info({ agentName, effectiveProvider, baseUrl: resolvedBaseUrl, model: finalModel, sourceKey: tokenInfo.tokenEnvKey, detectedProvider: detectedFromToken.provider, envKeysWritten: dotLines.length }, '[AUTH] Wrote agent .env with 4 canonical Hermes fields + provider-specific seeds.');
|
|
902
|
-
}
|
|
903
|
-
catch (e) {
|
|
904
|
-
logger.warn({ error: e, agentName }, '[AUTH] Failed to write auth.json or agent .env');
|
|
905
|
-
}
|
|
906
|
-
};
|
|
907
813
|
const spawnHermes = async (tokenInfo) => {
|
|
908
814
|
const spawnEnv = { ...process.env, ...agentCustomEnv };
|
|
909
815
|
if (tokenInfo) {
|
|
816
|
+
// Purge ALL known LLM provider env vars from the spawn env. We
|
|
817
|
+
// re-seed ONLY the ones this agent actually uses, derived from the
|
|
818
|
+
// token prefix. Without this purge, a stale `MINIMAX_CN_API_KEY` or
|
|
819
|
+
// `ZAI_ANTHROPIC_FALLBACK_KEY` left over from a previous provider
|
|
820
|
+
// (e.g. in `Workflow/.env`) can shadow the correct credential.
|
|
910
821
|
for (const tk of TOKEN_KEYS)
|
|
911
822
|
delete spawnEnv[tk];
|
|
823
|
+
// Also purge provider-specific env vars that the user might have
|
|
824
|
+
// left in the workflow .env (e.g. Z.AI legacy keys) but that don't
|
|
825
|
+
// match the agent's current provider.
|
|
826
|
+
for (const stale of [
|
|
827
|
+
'MINIMAX_CN_API_KEY', 'MINIMAX_API_KEY',
|
|
828
|
+
'ZAI_ANTHROPIC_FALLBACK_KEY', 'GLM_API_KEY',
|
|
829
|
+
'Z_AI_API_KEY', 'Z_AI_BASE_URL', 'GLM_BASE_URL',
|
|
830
|
+
'NVIDIA_API_KEY', 'NVIDIA_API_BASE',
|
|
831
|
+
]) {
|
|
832
|
+
delete spawnEnv[stale];
|
|
833
|
+
}
|
|
912
834
|
let resolvedToken = tokenInfo.tokenValue;
|
|
913
835
|
if (resolvedToken.startsWith('$'))
|
|
914
836
|
resolvedToken = process.env[resolvedToken.slice(1)] || resolvedToken;
|
|
915
837
|
spawnEnv[tokenInfo.tokenEnvKey] = resolvedToken;
|
|
838
|
+
// ============================================================
|
|
839
|
+
// 2.8.30 — Seed provider-specific env vars for Hermes plugins.
|
|
840
|
+
// ============================================================
|
|
841
|
+
// The Hermes plugins read provider-specific env vars, not the
|
|
842
|
+
// generic `ANTHROPIC_AUTH_TOKEN`. For example the `minimax` plugin
|
|
843
|
+
// reads `MINIMAX_CN_API_KEY` (CN) or `MINIMAX_API_KEY` (GLOBAL),
|
|
844
|
+
// and the `zai` plugin reads `ZAI_ANTHROPIC_FALLBACK_KEY`.
|
|
845
|
+
//
|
|
846
|
+
// Without this seed, Hermes falls back to the WRONG plugin (we saw
|
|
847
|
+
// it pick `nvidia` because `ANTHROPIC_BASE_URL` wasn't set yet and
|
|
848
|
+
// it could match an NVIDIA-style model name pattern). The fix is
|
|
849
|
+
// tiny: when we know the token prefix (e.g. `sk-cp-` for MiniMax),
|
|
850
|
+
// ALSO seed the provider-specific env var that the upstream plugin
|
|
851
|
+
// actually reads. Reference: the .bat launchers do the same.
|
|
852
|
+
//
|
|
853
|
+
// We do NOT write to the agent's `.hermes/.env` file anymore —
|
|
854
|
+
// this is a process-env-only seed, scoped to this single spawn.
|
|
855
|
+
if (resolvedToken.startsWith('sk-cp-') || resolvedToken.startsWith('sk-mm-')) {
|
|
856
|
+
// MiniMax token — seed BOTH env vars so either CN or GLOBAL plugin
|
|
857
|
+
// can pick it up. The plugin's own URL/host detection will pick
|
|
858
|
+
// the right one.
|
|
859
|
+
spawnEnv['MINIMAX_CN_API_KEY'] = resolvedToken;
|
|
860
|
+
spawnEnv['MINIMAX_API_KEY'] = resolvedToken;
|
|
861
|
+
logger.info({ agentName, envKey: tokenInfo.tokenEnvKey, alsoSeeded: ['MINIMAX_CN_API_KEY', 'MINIMAX_API_KEY'] }, '[SPAWN_ENV] Seeded MiniMax provider-specific env vars from sk-cp-* token (plugin compat).');
|
|
862
|
+
}
|
|
863
|
+
else if (/^[0-9a-f]{32}(\.[0-9a-zA-Z]+)?$/i.test(resolvedToken)) {
|
|
864
|
+
// Z.AI token (32hex or 32hex.32hex) — seed Z.AI env vars.
|
|
865
|
+
spawnEnv['ZAI_ANTHROPIC_FALLBACK_KEY'] = resolvedToken;
|
|
866
|
+
spawnEnv['GLM_API_KEY'] = resolvedToken;
|
|
867
|
+
logger.info({ agentName, envKey: tokenInfo.tokenEnvKey, alsoSeeded: ['ZAI_ANTHROPIC_FALLBACK_KEY', 'GLM_API_KEY'] }, '[SPAWN_ENV] Seeded Z.AI provider-specific env vars from 32hex token (plugin compat).');
|
|
868
|
+
}
|
|
916
869
|
}
|
|
917
|
-
writeAuthJson(tokenInfo);
|
|
918
870
|
// BLOCK: OpenRouter is for embeddings only — never pass to Hermes for LLM inference
|
|
919
871
|
delete spawnEnv['OPENROUTER_API_KEY'];
|
|
920
872
|
delete spawnEnv['OPENROUTER_BASE_URL'];
|
|
@@ -931,11 +883,17 @@ export class NousHermesRunner {
|
|
|
931
883
|
const venvBin = isWin ? path.join(venvRoot, 'Scripts') : path.join(venvRoot, 'bin');
|
|
932
884
|
const isVenvInstall = hermesBin.startsWith(venvBin + path.sep) || hermesBin === venvBin;
|
|
933
885
|
const pathSep = isWin ? ';' : ':';
|
|
886
|
+
// 2.8.30: HERMES_HOME is the SHARED root, not the per-agent home.
|
|
887
|
+
// Hermes upstream resolves `agents/<name>/`, `config.yaml`, `auth.json`,
|
|
888
|
+
// etc. relative to HERMES_HOME. Setting it to the per-agent home would
|
|
889
|
+
// tell Hermes "this IS the Hermes root" and make it look for config.yaml
|
|
890
|
+
// IN the agent dir — wrong layout.
|
|
891
|
+
const sharedHermesHome = getSharedHermesHome();
|
|
934
892
|
const child = spawn(hermesBin, cleanArgs, {
|
|
935
893
|
cwd, shell: false, windowsHide: true,
|
|
936
894
|
env: {
|
|
937
895
|
...spawnEnv,
|
|
938
|
-
HERMES_HOME:
|
|
896
|
+
HERMES_HOME: sharedHermesHome,
|
|
939
897
|
...(isVenvInstall
|
|
940
898
|
? {
|
|
941
899
|
VIRTUAL_ENV: venvRoot,
|