overmind-mcp 2.8.27 → 2.8.30

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.
@@ -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';
@@ -148,11 +148,20 @@ async function findHermesBinary() {
148
148
  return 'hermes';
149
149
  }
150
150
  /**
151
- * NousHermesRunner — Runner polyglote pour Hermes Agent.
152
- * • Providers : OpenAI, MiniMax, Zhipu/GLM, Mistral, NVIDIA NIM, OpenRouter (fallback)
153
- * Lit settings/agents/.mcp depuis .claude/ comme les autres runners
154
- * Interpolation $VAR et ${VAR} sur tout settings + mcp config (via envUtils)
155
- * Isolation : .overmind/hermes/agent_<name>/ (HERMES_HOME)
151
+ * NousHermesRunner — Runner polyglote pour Hermes Agent (Overmind 2.8.27+).
152
+ *
153
+ * Providers : OpenAI, MiniMax GLOBAL/CN, Zhipu/GLM, Mistral, NVIDIA NIM, OpenRouter (embeddings only)
154
+ * Lit settings_<agent>.json + .mcp.<agent>.json depuis .claude/ comme les autres runners
155
+ * Interpolation $VAR et ${VAR} sur tout settings + mcp config (via envUtils, regex fix 2.8.25)
156
+ * • Subtilisation 3-pass (Pass 1: settings-explicit, Pass A: prefer provider-specific,
157
+ * Pass B: re-map generic key, Pass C: rare fallback) — see hermesTokenResolver.ts
158
+ * • CN/GLOBAL disambiguation for sk-cp-* via ANTHROPIC_BASE_URL (URL wins)
159
+ * • OVERMIND_MINIMAX_DEFAULT=cn|global|auto for setups where all MiniMax tokens are CN
160
+ * • HERMES_HOME resolved via getAgentHermesHome() — deterministic across cwd (2.8.27+)
161
+ * Priority: OVERMIND_AGENT_HOME > legacy workspace > $HOME/.overmind/hermes/agent_<name>/
162
+ * • auth.json credential_pool is PRUNED every run (keep version+oauth, drop stale creds)
163
+ * to prevent Hermes from picking an exhausted bucket from a previous provider config
164
+ * • HOME/USERPROFILE propagated to spawned Hermes so ~/.hermes lookups resolve canonically
156
165
  */
157
166
  export class NousHermesRunner {
158
167
  timeoutMs;
@@ -288,7 +297,14 @@ export class NousHermesRunner {
288
297
  'MISTRAL_API_KEY_6', 'MISTRAL_API_KEY_7', 'MISTRAL_API_KEY_E', 'MISTRAL_API_KEY_Y',
289
298
  ];
290
299
  if (agentName) {
291
- const agentPromptPath = path.join(overmindHermesSubPath, 'SOUL.md');
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;
292
308
  if (fs.existsSync(agentPromptPath)) {
293
309
  systemPrompt = fs.readFileSync(agentPromptPath, 'utf8');
294
310
  }
@@ -369,7 +385,21 @@ export class NousHermesRunner {
369
385
  catch (e) {
370
386
  logger.warn({ error: e }, `Failed to process settings/mcp configurations for Hermes agent ${agentName}`);
371
387
  }
372
- // Load environment from isolated .env file (to allow overrides)
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).
373
403
  const envPath = path.join(overmindHermesSubPath, '.env');
374
404
  if (fs.existsSync(envPath)) {
375
405
  try {
@@ -387,7 +417,10 @@ export class NousHermesRunner {
387
417
  value = value.slice(1, -1);
388
418
  else if (value.startsWith("'") && value.endsWith("'"))
389
419
  value = value.slice(1, -1);
390
- 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.
391
424
  agentCustomEnv[key] = value;
392
425
  }
393
426
  });
@@ -415,10 +448,19 @@ export class NousHermesRunner {
415
448
  const cliPrompt = finalPrompt.length > 7000 ? finalPrompt.substring(0, 7000) : finalPrompt;
416
449
  // Build CLI args: chat -q (persistent session, NOT -z oneshot)
417
450
  // -z + --resume doesn't work — resume is ignored in oneshot mode
451
+ //
452
+ // DO NOT pass --provider explicitly. We learned empirically (Hermes-MiniMax-2.bat
453
+ // works while `hermes chat -q --provider minimax-cn` 401s) that letting Hermes
454
+ // auto-detect the provider from MINIMAX_CN_API_KEY / ZAI_ANTHROPIC_FALLBACK_KEY
455
+ // / etc. in the env gives correct results, while the explicit --provider flag
456
+ // activates a buggy code path that sends an auth header the upstream rejects.
457
+ // The ANTHROPIC_MODEL + ANTHROPIC_BASE_URL + provider-specific env var are
458
+ // enough for Hermes to pick the right plugin on its own.
418
459
  const cleanArgs = ['chat', '-q', cliPrompt, '-Q'];
419
460
  cleanArgs.push('--model', finalModel);
461
+ // resolvedProvider is logged for debugging but NOT passed as --provider.
420
462
  if (options.provider || resolvedProvider) {
421
- cleanArgs.push('--provider', options.provider || resolvedProvider);
463
+ logger.info({ agentName, resolvedProvider: options.provider || resolvedProvider, hint: 'omitted --provider; letting Hermes auto-detect from env' }, '[HERMES_ARGS] Not passing --provider (auto-detect from MINIMAX_CN_API_KEY et al. is more reliable).');
422
464
  }
423
465
  if (sessionId)
424
466
  cleanArgs.push('--resume', sessionId);
@@ -600,10 +642,12 @@ export class NousHermesRunner {
600
642
  lower.includes('service unavailable') || lower.includes('500') ||
601
643
  lower.includes('internal server error');
602
644
  };
603
- // HERMES_HOME setup
645
+ // HERMES_HOME setup — the SHARED root, not the per-agent home.
646
+ // Hermes upstream resolves `agents/<name>/`, `config.yaml`, `auth.json`, etc.
647
+ // relative to this single root. We do NOT seed `agentCustomEnv.HERMES_HOME`
648
+ // here anymore because spawnHermes() sets it explicitly from getSharedHermesHome().
604
649
  if (!fs.existsSync(overmindHermesSubPath))
605
650
  fs.mkdirSync(overmindHermesSubPath, { recursive: true });
606
- agentCustomEnv.HERMES_HOME = overmindHermesSubPath;
607
651
  // HOME / USERPROFILE override: point Hermes at the parent .overmind dir,
608
652
  // NOT the cwd. This makes relative .hermes lookups inside Hermes
609
653
  // (e.g. `~/.hermes/.env` resolution) resolve to the same canonical
@@ -612,86 +656,62 @@ export class NousHermesRunner {
612
656
  agentCustomEnv.USERPROFILE = overmindHermesPath;
613
657
  else
614
658
  agentCustomEnv.HOME = overmindHermesPath;
615
- // Write .env to HERMES_HOME (credential auto-discovery) - Cleaned to prevent duplicates
616
- // EXCLUDE all OpenRouter keys — OpenRouter is managed internally by Overmind, Hermes must never see it
617
- const credRegex = /(?:api_key|auth_token|base_url|endpoint|url)$/i;
618
- const openRouterPrefixes = ['OPENROUTER', 'OVERMIND_EMBEDDING'];
619
- const envMap = new Map();
620
- const dotPath = path.join(overmindHermesSubPath, '.env');
621
- if (fs.existsSync(dotPath)) {
622
- try {
623
- const existing = fs.readFileSync(dotPath, 'utf8');
624
- existing.split('\n').forEach((line) => {
625
- const trimmed = line.trim();
626
- if (!trimmed || trimmed.startsWith('#'))
627
- return;
628
- const eqIdx = trimmed.indexOf('=');
629
- if (eqIdx === -1)
630
- return;
631
- const k = trimmed.slice(0, eqIdx).trim();
632
- let v = trimmed.slice(eqIdx + 1).trim();
633
- if (v.startsWith('"') && v.endsWith('"'))
634
- v = v.slice(1, -1);
635
- else if (v.startsWith("'") && v.endsWith("'"))
636
- v = v.slice(1, -1);
637
- if (k) {
638
- if (openRouterPrefixes.some(p => k.toUpperCase().startsWith(p)))
639
- return;
640
- envMap.set(k, v);
659
+ // AbortSignal
660
+ if (options.signal?.aborted)
661
+ return Promise.reject(new Error('ABORTED'));
662
+ // =============================================================
663
+ // 2.8.30 WRITE CANONICAL Hermes SETTINGS
664
+ // =============================================================
665
+ // Hermes upstream uses the standard appdirs-style layout:
666
+ // <HERMES_HOME>/agents/<name>/settings.json ← per-agent env, mcp, persona
667
+ // <HERMES_HOME>/agents/<name>/SOUL.md ← per-agent system prompt
668
+ // <HERMES_HOME>/config.yaml ← global, Hermes manages
669
+ // <HERMES_HOME>/auth.json ← global, Hermes manages
670
+ //
671
+ // Overmind's only job here is to:
672
+ // 1. Convert Workflow/.claude/settings_<name>.json (Overmind runner format)
673
+ // into the canonical Hermes format and write it to <HERMES_HOME>/agents/<name>/settings.json
674
+ // 2. Make sure <HERMES_HOME>/agents/<name>/SOUL.md exists (canonical path)
675
+ // 3. Let Hermes upstream manage config.yaml, auth.json, sessions/, etc.
676
+ //
677
+ // This replaces the previous "polylgot" hack where Overmind wrote
678
+ // `<HERMES_HOME>/agent_<name>/.hermes/.env`, `.hermes/config.yaml`, and
679
+ // `.hermes/auth.json` — files that don't match Hermes's expected layout
680
+ // and caused credential drift + silent 401s.
681
+ if (agentName) {
682
+ const agentHome = overmindHermesSubPath; // = <HERMES_HOME>/agents/<name>/
683
+ if (!fs.existsSync(agentHome))
684
+ fs.mkdirSync(agentHome, { recursive: true });
685
+ // Build the canonical Hermes settings.json from the agent's settings_<name>.json.
686
+ // We preserve: env, enableAllProjectMcpServers, enabledMcpjsonServers, agent, runner.
687
+ // We do NOT touch: config.yaml, auth.json, .env — Hermes upstream owns those.
688
+ const tmpAgentSettings = path.join(agentHome, 'settings.json');
689
+ const settingsJson = {};
690
+ // Read the interpolated settings the runner just merged (line ~412 above)
691
+ // and copy the Hermes-relevant fields. We don't re-interpolate here because
692
+ // the caller already did it on the raw `settings` object.
693
+ if (tmpSettingsPath && fs.existsSync(tmpSettingsPath)) {
694
+ try {
695
+ const raw = JSON.parse(fs.readFileSync(tmpSettingsPath, 'utf8'));
696
+ if (raw.env)
697
+ settingsJson.env = raw.env;
698
+ if (raw.enableAllProjectMcpServers !== undefined) {
699
+ settingsJson.enableAllProjectMcpServers = raw.enableAllProjectMcpServers;
700
+ }
701
+ if (Array.isArray(raw.enabledMcpjsonServers)) {
702
+ settingsJson.enabledMcpjsonServers = raw.enabledMcpjsonServers;
641
703
  }
642
- });
643
- }
644
- catch (e) {
645
- logger.warn({ envPath: dotPath, error: e }, 'Failed to read existing agent env file for deduplication');
646
- }
647
- }
648
- for (const [k, v] of Object.entries(agentCustomEnv)) {
649
- if (typeof v === 'string' && v.length > 0 && credRegex.test(k)) {
650
- if (openRouterPrefixes.some(p => k.toUpperCase().startsWith(p)))
651
- continue;
652
- envMap.set(k, v);
653
- }
654
- }
655
- const finalDotEntries = [];
656
- for (const [k, v] of envMap.entries()) {
657
- finalDotEntries.push(`${k}=${v}`);
658
- }
659
- fs.writeFileSync(dotPath, finalDotEntries.join('\n') + '\n', 'utf8');
660
- // Generate config.yaml in HERMES_HOME (MCP servers)
661
- if (tmpMcpPath && fs.existsSync(tmpMcpPath)) {
662
- try {
663
- const mc = JSON.parse(fs.readFileSync(tmpMcpPath, 'utf8'));
664
- const yamlPath = path.join(overmindHermesSubPath, 'config.yaml');
665
- // Preserve existing config.yaml (tts, llm, etc.) — merge mcp_servers only
666
- let existingYaml = '';
667
- if (fs.existsSync(yamlPath)) {
668
- existingYaml = fs.readFileSync(yamlPath, 'utf8');
669
- }
670
- // Build new mcp_servers section
671
- let newMcpSection = 'mcp_servers:\n';
672
- for (const [name, srv] of Object.entries(mc.mcpServers || {})) {
673
- const s = srv;
674
- newMcpSection += ` ${name}:\n`;
675
- if (s.url)
676
- newMcpSection += ` url: "${s.url}"\n`;
677
- if (s.command)
678
- newMcpSection += ` command: "${s.command}"\n`;
679
- }
680
- // Merge: replace mcp_servers block in existing yaml or append
681
- let finalYaml;
682
- if (existingYaml.includes('mcp_servers:')) {
683
- finalYaml = existingYaml.replace(/mcp_servers:\n([\s\S]*?)(?=\n\w|\n$|$)/, newMcpSection.trimEnd() + '\n');
684
704
  }
685
- else {
686
- finalYaml = existingYaml.trimEnd() + '\n' + newMcpSection;
705
+ catch (e) {
706
+ logger.warn({ tmpSettingsPath, error: e }, 'Failed to read tmp settings for canonical write');
687
707
  }
688
- fs.writeFileSync(yamlPath, finalYaml, 'utf8');
689
- if (!silent)
690
- console.error(`[NousHermesRunner] MCP config.yaml written to ${yamlPath}`);
691
- }
692
- catch (e) {
693
- console.error(`[NousHermesRunner] config.yaml error: ${e}`);
694
708
  }
709
+ // Always declare the agent name + runner so Hermes can route to the right MCP servers.
710
+ settingsJson.agent = agentName;
711
+ settingsJson.runner = 'hermes';
712
+ fs.writeFileSync(tmpAgentSettings, JSON.stringify(settingsJson, null, 2) + '\n', 'utf8');
713
+ this.tempFiles.push(tmpAgentSettings);
714
+ logger.info({ agentName, settingsPath: tmpAgentSettings, envKeys: Object.keys(settingsJson.env || {}).length }, '[HERMES] Wrote canonical agents/<name>/settings.json (env block from settings_<name>.json).');
695
715
  }
696
716
  // AbortSignal
697
717
  if (options.signal?.aborted)
@@ -716,176 +736,6 @@ export class NousHermesRunner {
716
736
  }
717
737
  }
718
738
  };
719
- const writeAuthJson = (tokenInfo) => {
720
- if (!tokenInfo || !overmindHermesSubPath)
721
- return;
722
- try {
723
- const authPath = path.join(overmindHermesSubPath, 'auth.json');
724
- // Read existing auth.json to preserve non-credential_pool state
725
- // (e.g. oauth tokens, settings, version). But we PRUNE credential_pool
726
- // entries for OTHER providers — those are stale from previous provider
727
- // configs and Hermes may pick them up by mistake, causing silent 401s
728
- // on the wrong endpoint. This is the source of the "auth.json drift"
729
- // bug where the runner would seed `minimax-cn` credentials while a stale
730
- // `zai` entry with last_status="exhausted" still existed in the pool.
731
- let preservedAuth = { version: 1, providers: {} };
732
- if (fs.existsSync(authPath)) {
733
- try {
734
- const parsed = JSON.parse(fs.readFileSync(authPath, 'utf8'));
735
- // Keep the version + any oauth providers; drop credential_pool entirely
736
- // (it will be re-seeded below with only the effectiveProvider's entries).
737
- preservedAuth = {
738
- version: parsed.version ?? 1,
739
- providers: parsed.providers ?? {},
740
- };
741
- }
742
- catch (e) {
743
- logger.warn({ authPath, error: e }, 'auth.json was malformed; re-creating from scratch');
744
- }
745
- }
746
- const auth = {
747
- ...preservedAuth,
748
- credential_pool: {},
749
- };
750
- const cleanCp = auth.credential_pool;
751
- // Determine effective provider from MULTIPLE signals
752
- // Priority: TOKEN PREFIX (most reliable) > BASE_URL (very reliable) > settings.ANTHROPIC_PROVIDER (hint only)
753
- // The user can put anything in settings.ANTHROPIC_PROVIDER — we don't blindly trust it.
754
- const baseUrlHint = agentCustomEnv['ANTHROPIC_BASE_URL'] || agentCustomEnv['GLM_BASE_URL'] || '';
755
- // First, detect from token prefix
756
- const detectedFromToken = detectTokenProvider(tokenInfo.tokenValue);
757
- // Then, detect from base URL
758
- let detectedFromUrl = null;
759
- if (baseUrlHint) {
760
- const url = baseUrlHint.toLowerCase();
761
- if (url.includes('minimaxi')) {
762
- // The "i" suffix in api.minimaxi.com is the CN-specific endpoint
763
- detectedFromUrl = 'minimax-cn';
764
- }
765
- else if (url.includes('minimax')) {
766
- // api.minimax.com (no i) is the GLOBAL endpoint
767
- detectedFromUrl = 'minimax';
768
- }
769
- else if (url.includes('z.ai') || url.includes('bigmodel') || url.includes('zhipu')) {
770
- detectedFromUrl = 'zai';
771
- }
772
- else if (url.includes('anthropic.com')) {
773
- detectedFromUrl = 'anthropic';
774
- }
775
- else if (url.includes('openai.com')) {
776
- detectedFromUrl = 'openai';
777
- }
778
- }
779
- // Then, the hint from settings
780
- const settingsHint = resolvedProvider || '';
781
- // Voting: token > URL > settings
782
- // SPECIAL CASE: if token says "minimax" and URL says "minimax-cn" (or vice versa),
783
- // the URL wins because the token prefix sk-cp- is shared between both endpoints.
784
- // The URL is the only signal that can disambiguate CN vs GLOBAL.
785
- //
786
- // DEFAULT FOR MiniMax WHEN AMBIGUOUS:
787
- // The sk-cp- prefix is shared between MiniMax GLOBAL and MiniMax CN. The
788
- // URL is the only signal that disambiguates. For users whose setup
789
- // exclusively uses CN tokens (the most common case for non-China
790
- // operators), an absent/ambiguous URL should default to CN rather than
791
- // silently picking GLOBAL and getting a 401. Override via env var:
792
- // OVERMIND_MINIMAX_DEFAULT=cn (default: CN when ambiguous)
793
- // OVERMIND_MINIMAX_DEFAULT=global (treat sk-cp-* as GLOBAL)
794
- // OVERMIND_MINIMAX_DEFAULT=auto (never infer, require URL to disambiguate)
795
- const minimaxDefault = (process.env.OVERMIND_MINIMAX_DEFAULT || 'cn').toLowerCase();
796
- const minimaxDefaults = { cn: 'minimax-cn', global: 'minimax', auto: 'minimax' };
797
- const minimaxFallback = minimaxDefaults[minimaxDefault] || minimaxDefaults.cn;
798
- let effectiveProvider;
799
- if (detectedFromToken.provider === 'minimax' && detectedFromUrl === 'minimax-cn') {
800
- // URL has more specific info than the token prefix
801
- effectiveProvider = 'minimax-cn';
802
- logger.info({ agentName, tokenSays: 'minimax', urlSays: 'minimax-cn', settingsHint }, '[SUBTILISATION] URL is more specific than token prefix (minimax vs minimax-cn) — using URL.');
803
- }
804
- else if (detectedFromToken.provider === 'minimax-cn' && detectedFromUrl === 'minimax') {
805
- effectiveProvider = 'minimax';
806
- logger.info({ agentName, tokenSays: 'minimax-cn', urlSays: 'minimax', settingsHint }, '[SUBTILISATION] URL is more specific than token prefix (minimax vs minimax-cn) — using URL.');
807
- }
808
- else if (detectedFromToken.provider === 'minimax' && !detectedFromUrl) {
809
- // Token says MiniMax, no URL hint — use OVERMIND_MINIMAX_DEFAULT
810
- effectiveProvider = minimaxFallback;
811
- logger.info({ agentName, tokenSays: 'minimax', urlSays: '(none)', minimaxDefault, effectiveProvider }, '[SUBTILISATION] MiniMax token without explicit URL — applying OVERMIND_MINIMAX_DEFAULT.');
812
- }
813
- else if (detectedFromToken.provider !== 'unknown') {
814
- effectiveProvider = detectedFromToken.provider;
815
- if (settingsHint && settingsHint !== effectiveProvider) {
816
- logger.warn({ agentName, settingsHint, tokenSays: effectiveProvider, urlSays: detectedFromUrl }, '[SUBTILISATION] settings.ANTHROPIC_PROVIDER contradicts token prefix — using token.');
817
- }
818
- }
819
- else if (detectedFromUrl) {
820
- effectiveProvider = detectedFromUrl;
821
- if (settingsHint && settingsHint !== effectiveProvider) {
822
- logger.warn({ agentName, settingsHint, urlSays: effectiveProvider, tokenSays: detectedFromToken.provider }, '[SUBTILISATION] settings.ANTHROPIC_PROVIDER contradicts BASE_URL — using URL.');
823
- }
824
- }
825
- else if (settingsHint) {
826
- effectiveProvider = settingsHint;
827
- }
828
- else {
829
- effectiveProvider = 'zai';
830
- }
831
- cleanCp[effectiveProvider] = [{
832
- id: `${effectiveProvider}-default`, label: tokenInfo.tokenEnvKey, auth_type: 'api_key',
833
- priority: 0, source: `env:${tokenInfo.tokenEnvKey}`, access_token: tokenInfo.tokenValue,
834
- last_status: null, last_error_code: null,
835
- base_url: baseUrlHint || defaultBaseUrlFor(effectiveProvider),
836
- request_count: 0,
837
- }];
838
- fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
839
- // ============================================================
840
- // Write .env for HERMES_HOME — emit the 4 canonical fields
841
- // Hermes needs: ANTHROPIC_MODEL, ANTHROPIC_AUTH_TOKEN,
842
- // ANTHROPIC_PROVIDER, ANTHROPIC_BASE_URL
843
- // ============================================================
844
- const dotEnvPath = path.join(overmindHermesSubPath, '.env');
845
- const dotLines = [];
846
- // 1. ANTHROPIC_MODEL (always — Hermes needs it)
847
- if (finalModel) {
848
- dotLines.push(`ANTHROPIC_MODEL=${finalModel}`);
849
- }
850
- // 2. ANTHROPIC_PROVIDER (the kebab-case provider name)
851
- dotLines.push(`ANTHROPIC_PROVIDER=${effectiveProvider}`);
852
- // 3. ANTHROPIC_BASE_URL (from settings, or fallback)
853
- const resolvedBaseUrl = baseUrlHint || defaultBaseUrlFor(effectiveProvider);
854
- dotLines.push(`ANTHROPIC_BASE_URL=${resolvedBaseUrl}`);
855
- // 4. ANTHROPIC_AUTH_TOKEN = literal token value (for backward compat with older Hermes versions)
856
- // (Hermes reads this env var directly — no more provider-specific mapping)
857
- dotLines.push(`ANTHROPIC_AUTH_TOKEN=${tokenInfo.tokenValue}`);
858
- // 5. ALSO seed the provider-specific env var for plugins that need it
859
- // For MiniMax/Z.AI plugins, the provider-specific var is the PRIMARY key
860
- // the plugin reads. The .bat launchers in C:\Users\Deamon\Desktop\launcher\
861
- // set MINIMAX_CN_API_KEY directly (not ANTHROPIC_AUTH_TOKEN), confirming
862
- // that this is what the upstream plugin actually consumes.
863
- if (effectiveProvider === 'minimax' || effectiveProvider === 'minimax-cn') {
864
- // Both: the plugin reads whichever is set
865
- if (effectiveProvider === 'minimax-cn') {
866
- dotLines.push(`MINIMAX_CN_API_KEY=${tokenInfo.tokenValue}`);
867
- }
868
- else {
869
- dotLines.push(`MINIMAX_API_KEY=${tokenInfo.tokenValue}`);
870
- }
871
- }
872
- else if (effectiveProvider === 'zai' || effectiveProvider === 'z-ai') {
873
- dotLines.push(`GLM_API_KEY=${tokenInfo.tokenValue}`);
874
- dotLines.push(`ZAI_ANTHROPIC_FALLBACK_KEY=${tokenInfo.tokenValue}`);
875
- }
876
- else if (effectiveProvider === 'openai') {
877
- dotLines.push(`OPENAI_API_KEY=${tokenInfo.tokenValue}`);
878
- }
879
- else if (effectiveProvider === 'anthropic') {
880
- // ANTHROPIC_AUTH_TOKEN already set above
881
- }
882
- fs.writeFileSync(dotEnvPath, dotLines.join('\n') + '\n', 'utf8');
883
- 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.');
884
- }
885
- catch (e) {
886
- logger.warn({ error: e, agentName }, '[AUTH] Failed to write auth.json or agent .env');
887
- }
888
- };
889
739
  const spawnHermes = async (tokenInfo) => {
890
740
  const spawnEnv = { ...process.env, ...agentCustomEnv };
891
741
  if (tokenInfo) {
@@ -896,7 +746,6 @@ export class NousHermesRunner {
896
746
  resolvedToken = process.env[resolvedToken.slice(1)] || resolvedToken;
897
747
  spawnEnv[tokenInfo.tokenEnvKey] = resolvedToken;
898
748
  }
899
- writeAuthJson(tokenInfo);
900
749
  // BLOCK: OpenRouter is for embeddings only — never pass to Hermes for LLM inference
901
750
  delete spawnEnv['OPENROUTER_API_KEY'];
902
751
  delete spawnEnv['OPENROUTER_BASE_URL'];
@@ -913,11 +762,17 @@ export class NousHermesRunner {
913
762
  const venvBin = isWin ? path.join(venvRoot, 'Scripts') : path.join(venvRoot, 'bin');
914
763
  const isVenvInstall = hermesBin.startsWith(venvBin + path.sep) || hermesBin === venvBin;
915
764
  const pathSep = isWin ? ';' : ':';
765
+ // 2.8.30: HERMES_HOME is the SHARED root, not the per-agent home.
766
+ // Hermes upstream resolves `agents/<name>/`, `config.yaml`, `auth.json`,
767
+ // etc. relative to HERMES_HOME. Setting it to the per-agent home would
768
+ // tell Hermes "this IS the Hermes root" and make it look for config.yaml
769
+ // IN the agent dir — wrong layout.
770
+ const sharedHermesHome = getSharedHermesHome();
916
771
  const child = spawn(hermesBin, cleanArgs, {
917
772
  cwd, shell: false, windowsHide: true,
918
773
  env: {
919
774
  ...spawnEnv,
920
- HERMES_HOME: overmindHermesSubPath,
775
+ HERMES_HOME: sharedHermesHome,
921
776
  ...(isVenvInstall
922
777
  ? {
923
778
  VIRTUAL_ENV: venvRoot,