diff --git a/plugins/memory/mem0/__init__.py b/plugins/memory/mem0/__init__.py index cfe97916d6..acbaadd388 100644 --- a/plugins/memory/mem0/__init__.py +++ b/plugins/memory/mem0/__init__.py @@ -12,7 +12,6 @@ from __future__ import annotations import atexit import json import logging -import os import threading import time from contextlib import suppress @@ -79,11 +78,22 @@ def _load_config() -> dict: Layering avoids a silent failure when the JSON file exists but lacks fields like ``api_key`` that the user set in ``.env``.""" from hermes_constants import get_hermes_home - config = {"mode": os.environ.get("MEM0_MODE", "platform"), "api_key": get_secret("MEM0_API_KEY", ""), "host": os.environ.get("MEM0_HOST", ""), "agent_id": os.environ.get("MEM0_AGENT_ID", "hermes"), "oss": {}} - if os.environ.get("MEM0_USER_ID"): # only when explicitly configured, so initialize() can fall back to the gateway-native id - config["user_id"] = os.environ["MEM0_USER_ID"] + # Identity (user/agent id), host and mode are .env values like the key: read them through the + # profile scope too, or a secondary profile's memories land in the default profile's account. + config = {"mode": get_secret("MEM0_MODE", "") or "platform", "host": get_secret("MEM0_HOST", "") or "", + "agent_id": get_secret("MEM0_AGENT_ID", "") or "hermes", "oss": {}} + if user_id := get_secret("MEM0_USER_ID", ""): # only when explicitly configured, so initialize() can fall back to the gateway-native id + config["user_id"] = user_id file_cfg = _read_mem0_json(get_hermes_home() / "mem0.json") config.update({k: v for k, v in file_cfg.items() if v is not None and v != ""}) + # MEM0_API_KEY authenticates the Platform and self-hosted HTTP backends; pure OSS mode builds its + # backend from the local ``oss`` config and has no platform credential to resolve. Decide after + # mem0.json overrode the env fallback so a scope-less multiplex caller can load an OSS config + # without weakening fail-closed reads for credentialed modes. + if config.get("mode", "platform") == "oss": + config.setdefault("api_key", "") + elif not config.get("api_key"): + config["api_key"] = get_secret("MEM0_API_KEY", "") return config diff --git a/tests/plugins/memory/test_mem0_v3.py b/tests/plugins/memory/test_mem0_v3.py index 870d8d49dd..c1a1160143 100644 --- a/tests/plugins/memory/test_mem0_v3.py +++ b/tests/plugins/memory/test_mem0_v3.py @@ -5,6 +5,7 @@ import threading import time import pytest +from agent import secret_scope import plugins.memory.mem0 as mem0_plugin from plugins.memory.mem0 import Mem0MemoryProvider @@ -309,6 +310,59 @@ class TestMem0V3Config: class TestMem0ModeSwitch: + def test_oss_mode_initializes_without_unscoped_platform_key( + self, monkeypatch, tmp_path + ): + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + monkeypatch.delenv("MEM0_API_KEY", raising=False) + (tmp_path / "mem0.json").write_text( + json.dumps( + { + "mode": "oss", + "oss": {"vector_store": {"provider": "qdrant"}}, + } + ) + ) + + token = secret_scope.set_secret_scope(None) + secret_scope.set_multiplex_active(True) + try: + provider = Mem0MemoryProvider() + provider._create_backend = lambda: None # type: ignore[method-assign] + provider.initialize("test") + available = provider.is_available() + finally: + secret_scope.set_multiplex_active(False) + secret_scope.reset_secret_scope(token) + + assert provider._mode == "oss" + assert provider._api_key == "" + assert available is True + + def test_platform_config_still_fails_closed_without_profile_scope( + self, monkeypatch, tmp_path + ): + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + monkeypatch.delenv("MEM0_API_KEY", raising=False) + + token = secret_scope.set_secret_scope(None) + secret_scope.set_multiplex_active(True) + try: + with pytest.raises(secret_scope.UnscopedSecretError): + Mem0MemoryProvider().is_available() + finally: + secret_scope.set_multiplex_active(False) + secret_scope.reset_secret_scope(token) + + def test_file_api_key_still_overrides_environment(self, monkeypatch, tmp_path): + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + monkeypatch.setenv("MEM0_API_KEY", "env-key") + (tmp_path / "mem0.json").write_text( + json.dumps({"api_key": "file-key"}) + ) + + assert mem0_plugin._load_config()["api_key"] == "file-key" + def test_default_mode_is_platform(self, monkeypatch, tmp_path): monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setenv("MEM0_API_KEY", "test-key") @@ -453,5 +507,3 @@ class TestSelfHostedConfig: def test_load_config_reads_mem0_host_env(self, monkeypatch): monkeypatch.setenv("MEM0_HOST", "http://localhost:8888") assert mem0_plugin._load_config()["host"] == "http://localhost:8888" - -