memgres 0.2.0__tar.gz → 0.2.1__tar.gz

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.
Files changed (36) hide show
  1. {memgres-0.2.0 → memgres-0.2.1}/PKG-INFO +6 -4
  2. {memgres-0.2.0 → memgres-0.2.1}/README.md +5 -3
  3. {memgres-0.2.0 → memgres-0.2.1}/memgres/qdrant_backend.py +31 -3
  4. {memgres-0.2.0 → memgres-0.2.1}/memgres.egg-info/PKG-INFO +6 -4
  5. {memgres-0.2.0 → memgres-0.2.1}/memgres.egg-info/SOURCES.txt +1 -0
  6. {memgres-0.2.0 → memgres-0.2.1}/pyproject.toml +1 -1
  7. memgres-0.2.1/tests/test_qdrant_ca.py +72 -0
  8. {memgres-0.2.0 → memgres-0.2.1}/tests/test_qdrant_integration.py +10 -0
  9. {memgres-0.2.0 → memgres-0.2.1}/LICENSE +0 -0
  10. {memgres-0.2.0 → memgres-0.2.1}/memgres/__init__.py +0 -0
  11. {memgres-0.2.0 → memgres-0.2.1}/memgres/blame.py +0 -0
  12. {memgres-0.2.0 → memgres-0.2.1}/memgres/config.py +0 -0
  13. {memgres-0.2.0 → memgres-0.2.1}/memgres/diffing.py +0 -0
  14. {memgres-0.2.0 → memgres-0.2.1}/memgres/embeddings.py +0 -0
  15. {memgres-0.2.0 → memgres-0.2.1}/memgres/identity.py +0 -0
  16. {memgres-0.2.0 → memgres-0.2.1}/memgres/mcp_server.py +0 -0
  17. {memgres-0.2.0 → memgres-0.2.1}/memgres/migrations/0001_core.sql +0 -0
  18. {memgres-0.2.0 → memgres-0.2.1}/memgres/migrations/0002_identity.sql +0 -0
  19. {memgres-0.2.0 → memgres-0.2.1}/memgres/schema.py +0 -0
  20. {memgres-0.2.0 → memgres-0.2.1}/memgres/search.py +0 -0
  21. {memgres-0.2.0 → memgres-0.2.1}/memgres/server.py +0 -0
  22. {memgres-0.2.0 → memgres-0.2.1}/memgres/store.py +0 -0
  23. {memgres-0.2.0 → memgres-0.2.1}/memgres.egg-info/dependency_links.txt +0 -0
  24. {memgres-0.2.0 → memgres-0.2.1}/memgres.egg-info/entry_points.txt +0 -0
  25. {memgres-0.2.0 → memgres-0.2.1}/memgres.egg-info/requires.txt +0 -0
  26. {memgres-0.2.0 → memgres-0.2.1}/memgres.egg-info/top_level.txt +0 -0
  27. {memgres-0.2.0 → memgres-0.2.1}/setup.cfg +0 -0
  28. {memgres-0.2.0 → memgres-0.2.1}/tests/test_blame_integration.py +0 -0
  29. {memgres-0.2.0 → memgres-0.2.1}/tests/test_config.py +0 -0
  30. {memgres-0.2.0 → memgres-0.2.1}/tests/test_diffing.py +0 -0
  31. {memgres-0.2.0 → memgres-0.2.1}/tests/test_embeddings.py +0 -0
  32. {memgres-0.2.0 → memgres-0.2.1}/tests/test_identity_integration.py +0 -0
  33. {memgres-0.2.0 → memgres-0.2.1}/tests/test_search_integration.py +0 -0
  34. {memgres-0.2.0 → memgres-0.2.1}/tests/test_security_integration.py +0 -0
  35. {memgres-0.2.0 → memgres-0.2.1}/tests/test_server_integration.py +0 -0
  36. {memgres-0.2.0 → memgres-0.2.1}/tests/test_store_integration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memgres
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Drop-in memory for AI agents: one Postgres, lexical + semantic recall, diff-versioned history, GDPR-erasable.
5
5
  Author: mozgsml
6
6
  License-Expression: MIT
@@ -282,8 +282,10 @@ Either way the model gets tools `memory_write`, `memory_recall`, `memory_get`,
282
282
  X"* / *"what do you know about Y?"* and it calls them. (For semantic recall add the
283
283
  embedding env vars — see [docs/BACKENDS.md](docs/BACKENDS.md).)
284
284
 
285
- **Isolation — pin the identity in the client config; the agent never handles the
286
- token** (so the model spends nothing echoing a secret and can't switch user):
285
+ ### Isolation — pin the identity in the client config
286
+
287
+ The agent **never handles the token** (so the model spends nothing echoing a
288
+ secret and can't switch user):
287
289
 
288
290
  - **stdio**: set `MEMGRES_KEY_MODE=open` + `MEMGRES_TOKEN=<mgk_…>` in the client's
289
291
  `env` block (above).
@@ -305,7 +307,7 @@ argument for the model to supply — force it either way with
305
307
  library after the model produces text (see the examples above). Use this when you
306
308
  control the agent loop and decide when to write/recall.
307
309
 
308
- ## Tokens & auth
310
+ ## Multi-tenant, tokens & isolation
309
311
 
310
312
  There is **no token for single-user / local use** — leave everything default
311
313
  (`MEMGRES_KEY_MODE=single`) and it just works. Two token concepts exist, unrelated:
@@ -247,8 +247,10 @@ Either way the model gets tools `memory_write`, `memory_recall`, `memory_get`,
247
247
  X"* / *"what do you know about Y?"* and it calls them. (For semantic recall add the
248
248
  embedding env vars — see [docs/BACKENDS.md](docs/BACKENDS.md).)
249
249
 
250
- **Isolation — pin the identity in the client config; the agent never handles the
251
- token** (so the model spends nothing echoing a secret and can't switch user):
250
+ ### Isolation — pin the identity in the client config
251
+
252
+ The agent **never handles the token** (so the model spends nothing echoing a
253
+ secret and can't switch user):
252
254
 
253
255
  - **stdio**: set `MEMGRES_KEY_MODE=open` + `MEMGRES_TOKEN=<mgk_…>` in the client's
254
256
  `env` block (above).
@@ -270,7 +272,7 @@ argument for the model to supply — force it either way with
270
272
  library after the model produces text (see the examples above). Use this when you
271
273
  control the agent loop and decide when to write/recall.
272
274
 
273
- ## Tokens & auth
275
+ ## Multi-tenant, tokens & isolation
274
276
 
275
277
  There is **no token for single-user / local use** — leave everything default
276
278
  (`MEMGRES_KEY_MODE=single`) and it just works. Two token concepts exist, unrelated:
@@ -13,7 +13,13 @@ So tag/tree/TTL changes never need to touch Qdrant — only a body change re-emb
13
13
  (upsert) and `forget` deletes the point. Config for the connection:
14
14
 
15
15
  QDRANT_URL (default http://localhost:6333) · QDRANT_API_KEY ·
16
- MEMGRES_QDRANT_COLLECTION (default "memgres")
16
+ MEMGRES_QDRANT_COLLECTION (default "memgres") ·
17
+ MEMGRES_QDRANT_CA (path to a CA/self-signed cert to trust for an https URL)
18
+
19
+ MEMGRES_QDRANT_CA is only needed when Qdrant serves TLS with a certificate the
20
+ system trust store doesn't already know (a self-signed or private-CA deployment):
21
+ point it at the PEM to verify against. Leave it unset for plain http or a
22
+ publicly-trusted cert.
17
23
 
18
24
  Needs the `[qdrant]` extra (qdrant-client).
19
25
  """
@@ -26,14 +32,19 @@ from typing import List, Optional, Sequence, Tuple
26
32
 
27
33
  class QdrantIndex:
28
34
  def __init__(self, dim: int, url: Optional[str] = None,
29
- api_key: Optional[str] = None, collection: Optional[str] = None):
35
+ api_key: Optional[str] = None, collection: Optional[str] = None,
36
+ ca_cert: Optional[str] = None):
30
37
  from qdrant_client import QdrantClient
31
38
 
32
39
  self.dim = dim
33
40
  self.collection = collection or os.environ.get("MEMGRES_QDRANT_COLLECTION", "memgres")
34
41
  url = url or os.environ.get("QDRANT_URL", "http://localhost:6333")
35
42
  api_key = api_key or os.environ.get("QDRANT_API_KEY") or None
36
- self.client = QdrantClient(url=url, api_key=api_key)
43
+ ca_cert = ca_cert or os.environ.get("MEMGRES_QDRANT_CA") or None
44
+ # A self-signed / private-CA https endpoint isn't in the system trust store;
45
+ # `verify=<pem>` makes the underlying httpx client trust exactly that cert.
46
+ extra = {"verify": ca_cert} if ca_cert else {}
47
+ self.client = QdrantClient(url=url, api_key=api_key, **extra)
37
48
  self._ensure()
38
49
 
39
50
  def _ensure(self) -> None:
@@ -44,6 +55,7 @@ class QdrantIndex:
44
55
  self.collection,
45
56
  vectors_config=VectorParams(size=self.dim, distance=Distance.COSINE),
46
57
  )
58
+ self._ensure_namespace_index()
47
59
  return
48
60
  info = self.client.get_collection(self.collection)
49
61
  size = info.config.params.vectors.size
@@ -53,6 +65,22 @@ class QdrantIndex:
53
65
  f"Qdrant collection '{self.collection}' has dim {size}, model emits "
54
66
  f"{self.dim} — re-embed into a fresh collection, don't mix models."
55
67
  )
68
+ self._ensure_namespace_index()
69
+
70
+ def _ensure_namespace_index(self) -> None:
71
+ """Keyword payload index on `namespace` so tenant-filtered ANN search
72
+ stays fast as the collection grows (isolation is enforced by the query
73
+ filter regardless; this is purely the speed of that filter). Idempotent:
74
+ check the existing payload schema rather than catch a re-create error."""
75
+ from qdrant_client.models import PayloadSchemaType
76
+
77
+ info = self.client.get_collection(self.collection)
78
+ if "namespace" in (info.payload_schema or {}):
79
+ return
80
+ self.client.create_payload_index(
81
+ self.collection, field_name="namespace",
82
+ field_schema=PayloadSchemaType.KEYWORD,
83
+ )
56
84
 
57
85
  def upsert(self, id: str, vector: Sequence[float], namespace: str) -> None:
58
86
  from qdrant_client.models import PointStruct
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memgres
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Drop-in memory for AI agents: one Postgres, lexical + semantic recall, diff-versioned history, GDPR-erasable.
5
5
  Author: mozgsml
6
6
  License-Expression: MIT
@@ -282,8 +282,10 @@ Either way the model gets tools `memory_write`, `memory_recall`, `memory_get`,
282
282
  X"* / *"what do you know about Y?"* and it calls them. (For semantic recall add the
283
283
  embedding env vars — see [docs/BACKENDS.md](docs/BACKENDS.md).)
284
284
 
285
- **Isolation — pin the identity in the client config; the agent never handles the
286
- token** (so the model spends nothing echoing a secret and can't switch user):
285
+ ### Isolation — pin the identity in the client config
286
+
287
+ The agent **never handles the token** (so the model spends nothing echoing a
288
+ secret and can't switch user):
287
289
 
288
290
  - **stdio**: set `MEMGRES_KEY_MODE=open` + `MEMGRES_TOKEN=<mgk_…>` in the client's
289
291
  `env` block (above).
@@ -305,7 +307,7 @@ argument for the model to supply — force it either way with
305
307
  library after the model produces text (see the examples above). Use this when you
306
308
  control the agent loop and decide when to write/recall.
307
309
 
308
- ## Tokens & auth
310
+ ## Multi-tenant, tokens & isolation
309
311
 
310
312
  There is **no token for single-user / local use** — leave everything default
311
313
  (`MEMGRES_KEY_MODE=single`) and it just works. Two token concepts exist, unrelated:
@@ -26,6 +26,7 @@ tests/test_config.py
26
26
  tests/test_diffing.py
27
27
  tests/test_embeddings.py
28
28
  tests/test_identity_integration.py
29
+ tests/test_qdrant_ca.py
29
30
  tests/test_qdrant_integration.py
30
31
  tests/test_search_integration.py
31
32
  tests/test_security_integration.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "memgres"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Drop-in memory for AI agents: one Postgres, lexical + semantic recall, diff-versioned history, GDPR-erasable."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1,72 @@
1
+ """Unit test: MEMGRES_QDRANT_CA is forwarded to the Qdrant client as `verify=`.
2
+
3
+ No live Qdrant needed — a fake client records the kwargs it was built with and
4
+ stubs the calls `_ensure()` makes, so this runs anywhere (unlike the integration
5
+ tests that need a real server). Guards the self-signed / private-CA TLS path.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
12
+
13
+ import pytest # noqa: E402
14
+
15
+
16
+ class _FakeCollectionInfo:
17
+ def __init__(self, dim):
18
+ class _V: # info.config.params.vectors.size
19
+ size = dim
20
+ class _P:
21
+ vectors = _V()
22
+ class _C:
23
+ params = _P()
24
+ self.config = _C()
25
+ self.payload_schema = {"namespace": object()} # pretend the index exists
26
+
27
+
28
+ class _FakeClient:
29
+ last_kwargs = None
30
+
31
+ def __init__(self, **kwargs):
32
+ _FakeClient.last_kwargs = kwargs
33
+
34
+ def collection_exists(self, _c):
35
+ return True
36
+
37
+ def get_collection(self, _c):
38
+ return _FakeCollectionInfo(dim=8)
39
+
40
+
41
+ @pytest.fixture
42
+ def fake_qdrant(monkeypatch):
43
+ import qdrant_client
44
+ monkeypatch.setattr(qdrant_client, "QdrantClient", _FakeClient)
45
+ _FakeClient.last_kwargs = None
46
+ yield
47
+
48
+
49
+ def test_ca_cert_forwarded_as_verify(fake_qdrant, monkeypatch):
50
+ monkeypatch.setenv("MEMGRES_QDRANT_CA", "/etc/ssl/my-ca.pem")
51
+ from memgres.qdrant_backend import QdrantIndex
52
+ QdrantIndex(dim=8, url="https://qdrant.example:6333")
53
+ assert _FakeClient.last_kwargs.get("verify") == "/etc/ssl/my-ca.pem"
54
+
55
+
56
+ def test_no_ca_means_no_verify_kwarg(fake_qdrant, monkeypatch):
57
+ monkeypatch.delenv("MEMGRES_QDRANT_CA", raising=False)
58
+ from memgres.qdrant_backend import QdrantIndex
59
+ QdrantIndex(dim=8, url="http://qdrant.example:6333")
60
+ # default trust store path: don't pin `verify` at all
61
+ assert "verify" not in _FakeClient.last_kwargs
62
+
63
+
64
+ def test_explicit_ca_arg_beats_env(fake_qdrant, monkeypatch):
65
+ monkeypatch.setenv("MEMGRES_QDRANT_CA", "/env/ca.pem")
66
+ from memgres.qdrant_backend import QdrantIndex
67
+ QdrantIndex(dim=8, url="https://qdrant.example:6333", ca_cert="/arg/ca.pem")
68
+ assert _FakeClient.last_kwargs.get("verify") == "/arg/ca.pem"
69
+
70
+
71
+ if __name__ == "__main__":
72
+ sys.exit(pytest.main([__file__, "-v"]))
@@ -96,6 +96,16 @@ def test_no_pgvector_column_when_qdrant(store):
96
96
  assert "embedding" not in cols
97
97
 
98
98
 
99
+ def test_namespace_payload_index_created(store):
100
+ # tenant-scoped ANN search filters on `namespace`; a keyword payload index
101
+ # keeps that filter fast as the collection grows (isolation holds either way)
102
+ from qdrant_client import QdrantClient
103
+ qc = QdrantClient(url=QURL)
104
+ schema = qc.get_collection(COLL).payload_schema or {}
105
+ assert "namespace" in schema
106
+ assert str(schema["namespace"].data_type).lower().endswith("keyword")
107
+
108
+
99
109
  def test_semantic_recall_via_qdrant(store):
100
110
  store.write(body="I love apple pie\n", path="food.apple", tags=["fruit"])
101
111
  store.write(body="banana bread recipe\n", path="food.banana", tags=["fruit"])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes