fix(memory): don't let update() metadata overwrite user_id/agent_id/run_id (#6278)

Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
Aditya Jethani
2026-07-21 12:44:22 +05:30
committed by GitHub
co-authored by kartik-mem0
parent fec2fe6a2c
commit c2bc28e589
2 changed files with 72 additions and 56 deletions
+52 -46
View File
@@ -418,6 +418,58 @@ async def test_async_update_memory_uses_utc_timestamps(mocker):
assert payload["updated_at"] is not None
_ATTACKER_UPDATE_METADATA = {
"user_id": "attacker_tenant",
"agent_id": "attacker_agent",
"run_id": "attacker_run",
"actor_id": "attacker_actor",
"category": "sports",
}
# Omits agent_id on purpose, so one payload covers both overwriting and injecting an identity field.
_EXISTING_UPDATE_PAYLOAD = {
"data": "old memory",
"user_id": "tenant_a",
"run_id": "run_a",
"actor_id": "actor_a",
}
def test_update_memory_metadata_cannot_change_identity_fields(mocker, caplog):
"""Regression (issues #4490, #6277): update() metadata must not overwrite or inject identity fields."""
memory = _build_memory_instance(mocker, Memory)
memory.vector_store.get.return_value = MagicMock(payload=dict(_EXISTING_UPDATE_PAYLOAD))
with caplog.at_level(logging.WARNING, logger="mem0.memory.main"):
memory._update_memory("memory-id", "new memory", {}, metadata=dict(_ATTACKER_UPDATE_METADATA))
payload = memory.vector_store.update.call_args.kwargs["payload"]
assert payload["user_id"] == "tenant_a"
assert payload["run_id"] == "run_a"
assert "agent_id" not in payload
assert payload["actor_id"] == "actor_a"
assert payload["category"] == "sports"
assert payload["data"] == "new memory"
assert "ignoring metadata['user_id']" in caplog.text
@pytest.mark.asyncio
async def test_async_update_memory_metadata_cannot_change_identity_fields(mocker):
"""Async counterpart of test_update_memory_metadata_cannot_change_identity_fields."""
memory = _build_memory_instance(mocker, AsyncMemory)
memory.vector_store.get.return_value = MagicMock(payload=dict(_EXISTING_UPDATE_PAYLOAD))
await memory._update_memory("memory-id", "new memory", {}, metadata=dict(_ATTACKER_UPDATE_METADATA))
payload = memory.vector_store.update.call_args.kwargs["payload"]
assert payload["user_id"] == "tenant_a"
assert payload["run_id"] == "run_a"
assert "agent_id" not in payload
assert payload["actor_id"] == "actor_a"
assert payload["category"] == "sports"
assert payload["data"] == "new memory"
def test_create_then_search_and_get_all_return_same_timestamps(mocker):
"""Reproduces issue #3720: created_at must be identical in search() and get_all()."""
memory = _build_memory_instance(mocker, Memory)
@@ -714,52 +766,6 @@ class TestMetadataNotMutated:
)
def test_update_preserves_actor_id_when_different_actor_updates(mocker):
"""actor_id must be preserved from the original memory even when the
updating caller passes a different actor_id in metadata (issue #4490)."""
memory = _build_memory_instance(mocker, Memory)
memory.vector_store.get.return_value = MagicMock(
payload={
"data": "I am player #1",
"user_id": "team",
"actor_id": "Alice",
"created_at": "2026-01-01T00:00:00+00:00",
}
)
memory._update_memory(
"mem-id", "Player #1 is a good person",
{"Player #1 is a good person": [0.1, 0.2, 0.3]},
metadata={"user_id": "team", "actor_id": "Bob"},
)
stored = memory.vector_store.update.call_args.kwargs["payload"]
assert stored["actor_id"] == "Alice"
@pytest.mark.asyncio
async def test_async_update_preserves_actor_id_when_different_actor_updates(mocker):
"""Async variant: actor_id must be preserved from the original memory (issue #4490)."""
memory = _build_memory_instance(mocker, AsyncMemory)
memory.vector_store.get.return_value = MagicMock(
payload={
"data": "I am player #1",
"user_id": "team",
"actor_id": "Alice",
"created_at": "2026-01-01T00:00:00+00:00",
}
)
await memory._update_memory(
"mem-id", "Player #1 is a good person",
{"Player #1 is a good person": [0.1, 0.2, 0.3]},
metadata={"user_id": "team", "actor_id": "Bob"},
)
stored = memory.vector_store.update.call_args.kwargs["payload"]
assert stored["actor_id"] == "Alice"
def _make_match(score, linked_memory_ids):
return SimpleNamespace(score=score, payload={"linked_memory_ids": linked_memory_ids})