fix(mem0): skip platform key lookup in OSS mode
Conflict resolution on top of main also routes MEM0_MODE / MEM0_HOST / MEM0_AGENT_ID / MEM0_USER_ID through get_secret: identity and host are .env values like the key, and a raw environ read hands a secondary profile the default profile's mem0 account. (cherry picked from commit 7a5863ebafdb193d8ec2f89a7aca309774d6d69a)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user