Compare commits

..
Author SHA1 Message Date
harshgupta-mem0 884e056853 fix(deps): upgrade litellm to >=1.88.1 and python-dotenv to 1.2.2 (CVE-2026-28684)
litellm 1.83.7 hard-pinned python-dotenv==1.0.1 (vulnerable). litellm 1.88.1
relaxed this to python-dotenv>=1.0.0,<2.0, unblocking the upgrade to 1.2.2.

Updates pyproject.toml constraint and manually patches poetry.lock entries
with verified PyPI hashes. The lock file content-hash will need a full
poetry lock regeneration on Python 3.10-3.13 (litellm does not yet support
Python 3.14, which is the local system Python).

Resolves Dependabot alert #760.
2026-06-12 20:08:58 +05:30
harshgupta-mem0 fbf6b8c0db fix(@mem0/community): upgrade @langchain/community to ^1.1.18 (CVE-2026-27795, CVE-2026-26019)
Bumps @langchain/community from ^0.3.36 to ^1.1.18 and @langchain/core from
^0.3.42 to ^1.1.27 to resolve two SSRF CVEs in RecursiveUrlLoader.

Resolves Dependabot alerts #496 and #538.
2026-06-12 19:48:13 +05:30
1d56f85705 fix(cli): resolve Windows environment compatibility issues in python CLI tests (#5464)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-12 19:40:26 +05:30
Hrushikesh YadavandGitHub ced852033b fix: return 400 instead of 502 for invalid search filters (#5482) 2026-06-12 19:35:24 +05:30
Yufeng HeandGitHub b9ad8fa8b2 fix(openclaw): skip runtime setup during metadata registration (#5383) 2026-06-12 19:32:59 +05:30
e3f5ce7b41 fix: use valid S3 entity index names (#5416)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-12 19:22:32 +05:30
14 changed files with 147 additions and 21 deletions
+6 -3
View File
@@ -32,12 +32,13 @@ def _run(args: list[str], home_dir: str | None = None) -> subprocess.CompletedPr
if key.startswith("MEM0_"):
del env[key]
env.pop("FORCE_COLOR", None)
env["PYTHONIOENCODING"] = "utf-8"
if home_dir:
env["HOME"] = home_dir
result = subprocess.run(
[sys.executable, "-m", "mem0_cli", *args],
capture_output=True,
text=True,
encoding="utf-8",
env=env,
timeout=15,
)
@@ -99,12 +100,13 @@ class TestArgvPreprocessing:
result = subprocess.run(
[sys.executable, "-m", "mem0_cli", "init", "--agent"],
capture_output=True,
text=True,
encoding="utf-8",
env={
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
"HOME": clean_home,
"MEM0_BASE_URL": "http://127.0.0.1:1", # blackhole
"FORCE_COLOR": "0",
"PYTHONIOENCODING": "utf-8",
},
timeout=15,
)
@@ -133,12 +135,13 @@ class TestJsonEnvelopeParity:
result = subprocess.run(
[sys.executable, "-m", "mem0_cli", "init", "--agent", "--json"],
capture_output=True,
text=True,
encoding="utf-8",
env={
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
"HOME": clean_home,
"MEM0_BASE_URL": "http://127.0.0.1:1",
"FORCE_COLOR": "0",
"PYTHONIOENCODING": "utf-8",
},
timeout=15,
)
+2 -1
View File
@@ -49,6 +49,7 @@ def _run(
if key.startswith("MEM0_"):
del env[key]
env.pop("FORCE_COLOR", None)
env["PYTHONIOENCODING"] = "utf-8"
if home_dir:
env["HOME"] = home_dir
if env_override:
@@ -56,7 +57,7 @@ def _run(
result = subprocess.run(
[sys.executable, "-m", "mem0_cli", *args],
capture_output=True,
text=True,
encoding="utf-8",
env=env,
)
return subprocess.CompletedProcess(
+2 -1
View File
@@ -67,7 +67,8 @@ class TestConfig:
from mem0_cli.config import CONFIG_FILE
mode = os.stat(CONFIG_FILE).st_mode & 0o777
assert mode == 0o600
if os.name != "nt":
assert mode == 0o600
def test_defaults_save_and_load(self, isolate_config):
config = Mem0Config()
+43 -2
View File
@@ -2,8 +2,8 @@
* Regression tests for per-agent memory isolation helpers and
* message filtering logic.
*/
import { describe, it, expect } from "vitest";
import {
import { describe, it, expect, vi } from "vitest";
import memoryPlugin, {
extractAgentId,
effectiveUserId,
agentUserId,
@@ -16,6 +16,47 @@ import {
filterMessagesForExtraction,
} from "./index.ts";
function createPluginApi(registrationMode?: string) {
return {
pluginConfig: {
mode: "platform",
apiKey: "test-api-key",
userId: "alice",
},
registrationMode,
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
resolvePath: vi.fn((p: string) => p),
registerTool: vi.fn(),
on: vi.fn(),
registerCli: vi.fn(),
registerCommand: vi.fn(),
registerService: vi.fn(),
registerMemoryCapability: vi.fn(),
};
}
describe("plugin registration modes", () => {
it("keeps cli-metadata registration free of runtime side effects", () => {
const api = createPluginApi("cli-metadata");
memoryPlugin.register(api as any);
expect(api.registerCli).toHaveBeenCalledTimes(1);
expect(api.registerService).not.toHaveBeenCalled();
expect(api.registerMemoryCapability).not.toHaveBeenCalled();
expect(api.registerTool).not.toHaveBeenCalled();
expect(api.on).not.toHaveBeenCalled();
expect(api.logger.info).not.toHaveBeenCalledWith(
expect.stringContaining("openclaw-mem0: registered"),
);
});
});
// ---------------------------------------------------------------------------
// extractAgentId
// ---------------------------------------------------------------------------
+16
View File
@@ -107,6 +107,7 @@ const memoryPlugin = definePluginEntry({
baseUrl: pluginAuth.baseUrl,
};
const cfg = mem0ConfigSchema.parse(api.pluginConfig, fileConfig);
const isMetadataRegistration = api.registrationMode === "cli-metadata";
// Telemetry context bound to this plugin instance's config
const telemetryCtx = {
@@ -122,6 +123,21 @@ const memoryPlugin = definePluginEntry({
}
};
if (isMetadataRegistration) {
registerCliCommands(
api,
null as any,
null as any,
cfg,
() => cfg.userId,
(id: string) => `${cfg.userId}:agent:${id}`,
() => ({ user_id: cfg.userId, top_k: cfg.topK }),
() => undefined,
(cmd: string) => _captureEvent(`openclaw.cli.${cmd}`, { command: cmd }),
);
return;
}
if (cfg.needsSetup) {
api.logger.warn(
"openclaw-mem0: API key not configured. Memory features are disabled.\n" +
+1
View File
@@ -26,6 +26,7 @@ declare module "openclaw/plugin-sdk" {
export interface OpenClawPluginApi {
pluginConfig: Record<string, unknown>;
registrationMode?: "full" | "cli-metadata" | string;
logger: {
info(msg: string): void;
warn(msg: string): void;
+2 -2
View File
@@ -75,8 +75,8 @@
"typescript": "5.5.4"
},
"dependencies": {
"@langchain/community": "^0.3.36",
"@langchain/core": "^0.3.42",
"@langchain/community": "^1.1.18",
"@langchain/core": "^1.1.27",
"axios": "^1.16.0",
"mem0ai": "^2.1.8",
"uuid": "^11.1.1",
+7 -2
View File
@@ -341,6 +341,11 @@ def _build_session_scope(filters):
return "&".join(parts)
def _entity_collection_name(provider: str, collection_name: str) -> str:
separator = "-" if provider == "s3_vectors" else "_"
return f"{collection_name}{separator}entities"
setup_config()
logger = logging.getLogger(__name__)
@@ -417,7 +422,7 @@ class Memory(MemoryBase):
"""Lazily initialize entity store on first use."""
if self._entity_store is None:
entity_config = _safe_deepcopy_config(self.config.vector_store.config)
entity_collection = f"{self.collection_name}_entities"
entity_collection = _entity_collection_name(self.config.vector_store.provider, self.collection_name)
# Set collection name on the cloned config
if hasattr(entity_config, 'collection_name'):
entity_config.collection_name = entity_collection
@@ -1889,7 +1894,7 @@ class AsyncMemory(MemoryBase):
"""Lazily initialize entity store on first use."""
if self._entity_store is None:
entity_config = _safe_deepcopy_config(self.config.vector_store.config)
entity_collection = f"{self.collection_name}_entities"
entity_collection = _entity_collection_name(self.config.vector_store.provider, self.collection_name)
if hasattr(entity_config, 'collection_name'):
entity_config.collection_name = entity_collection
elif isinstance(entity_config, dict):
Generated
+9 -9
View File
@@ -3494,15 +3494,15 @@ pytest = ["pytest (>=7.0.0)", "rich (>=13.9.4,<14.0.0)"]
[[package]]
name = "litellm"
version = "1.83.7"
version = "1.88.1"
description = "Library to easily interface with LLM API providers"
optional = true
python-versions = "<4.0,>=3.9"
python-versions = "<3.14,>=3.10"
groups = ["main"]
markers = "extra == \"llms\""
files = [
{file = "litellm-1.83.7-py3-none-any.whl", hash = "sha256:5784a1d9a9a4a8acd6ca1e347003a5e2e1b3c749b4d41e7da4904577adade111"},
{file = "litellm-1.83.7.tar.gz", hash = "sha256:e2f2cb99df2e2b2eab63f1354faa45c88dd7c8d40c18eb648afb1b349c689633"},
{file = "litellm-1.88.1-py3-none-any.whl", hash = "sha256:369b84e57d9426582ddc35e731956ddb6618cda97cc44e4e4d2dfa75982a6e3a"},
{file = "litellm-1.88.1.tar.gz", hash = "sha256:89c6b74cc7912d6365793006ff951c0450fe847625008dfe49de8a7dc4529aa5"},
]
[package.dependencies]
@@ -3515,7 +3515,7 @@ jinja2 = "3.1.6"
jsonschema = "4.23.0"
openai = "2.30.0"
pydantic = "2.12.5"
python-dotenv = "1.0.1"
python-dotenv = "1.2.2"
tiktoken = "0.12.0"
tokenizers = "0.22.2"
@@ -6866,15 +6866,15 @@ six = ">=1.5"
[[package]]
name = "python-dotenv"
version = "1.0.1"
version = "1.2.2"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = true
python-versions = ">=3.8"
python-versions = ">=3.10"
groups = ["main"]
markers = "extra == \"vector-stores\" or extra == \"llms\" or extra == \"extras\""
files = [
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
{file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"},
{file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"},
]
[package.extras]
+1 -1
View File
@@ -55,7 +55,7 @@ vector_stores = [
llms = [
"groq>=0.3.0",
"together>=0.2.10",
"litellm>=1.83.7",
"litellm>=1.88.1",
"openai>=1.90.0",
"ollama>=0.3.0",
"vertexai>=0.1.0",
+4
View File
@@ -451,6 +451,10 @@ def search_memories(search_req: SearchRequest, _auth=Depends(verify_auth)):
if search_req.explain is not None:
params["explain"] = search_req.explain
return get_memory_instance().search(query=search_req.query, filters=filters, **params)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except HTTPException:
raise
except Exception:
raise upstream_error()
+9
View File
@@ -6,6 +6,7 @@ import pytest
from mem0 import Memory
from mem0.configs.base import MemoryConfig
from mem0.memory.main import _entity_collection_name
from mem0.memory.utils import normalize_facts
@@ -37,6 +38,14 @@ def test_create_memory(memory_client):
assert result["results"][0]["memory"] == data
def test_entity_collection_name_uses_dash_for_s3_vectors():
assert _entity_collection_name("s3_vectors", "test-index") == "test-index-entities"
def test_entity_collection_name_keeps_underscore_for_other_stores():
assert _entity_collection_name("qdrant", "mem0") == "mem0_entities"
def test_get_memory(memory_client):
data = "Name is John Doe."
memory_client.add([{"role": "user", "content": data}], user_id="test_user")
+19
View File
@@ -682,3 +682,22 @@ class TestSearchEntityIdMapping:
_, kwargs = mock_memory.search.call_args
assert kwargs["filters"]["user_id"] == "u1"
assert kwargs["filters"]["category"] == "food"
class TestSearchValidationErrors:
"""Verify that ValueError from Memory.search() returns 400, not 502."""
def test_empty_filters_returns_400(self, client, mock_memory):
mock_memory.search.side_effect = ValueError(
"filters must contain at least one of: user_id, agent_id, run_id"
)
resp = client.post("/search", json={"query": "food", "filters": {}})
assert resp.status_code == 400
assert "filters must contain" in resp.json()["detail"]
def test_no_identifiers_returns_400(self, client, mock_memory):
mock_memory.search.side_effect = ValueError(
"filters must contain at least one of: user_id, agent_id, run_id"
)
resp = client.post("/search", json={"query": "food"})
assert resp.status_code == 400
+26
View File
@@ -110,6 +110,32 @@ def test_memory_initialization_with_config(mock_boto_client, mock_llm, mock_embe
pytest.fail("Memory initialization failed")
def test_memory_entity_store_uses_s3_valid_index_name(mock_boto_client, mock_llm, mock_embedder):
mock_boto_client.get_vector_bucket.return_value = {}
mock_boto_client.get_index.return_value = {}
config = {
"vector_store": {
"provider": "s3_vectors",
"config": {
"vector_bucket_name": BUCKET_NAME,
"collection_name": INDEX_NAME,
"embedding_model_dims": EMBEDDING_DIMS,
"distance_metric": "cosine",
"region_name": REGION,
},
}
}
memory = Memory.from_config(config)
assert memory.entity_store is not None
index_names = [call.kwargs["indexName"] for call in mock_boto_client.get_index.call_args_list]
assert INDEX_NAME in index_names
assert f"{INDEX_NAME}-entities" in index_names
assert f"{INDEX_NAME}_entities" not in index_names
def test_insert(mock_boto_client):
"""Test inserting vectors."""
store = S3Vectors(