amu-mem0 0.1.0__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.
- amu_mem0-0.1.0/.github/workflows/ci.yml +26 -0
- amu_mem0-0.1.0/.github/workflows/publish.yml +41 -0
- amu_mem0-0.1.0/.gitignore +7 -0
- amu_mem0-0.1.0/EVIDENCE.md +36 -0
- amu_mem0-0.1.0/LICENSE +21 -0
- amu_mem0-0.1.0/Makefile +18 -0
- amu_mem0-0.1.0/PKG-INFO +260 -0
- amu_mem0-0.1.0/README.md +236 -0
- amu_mem0-0.1.0/bench/run_bench.py +495 -0
- amu_mem0-0.1.0/examples/quickstart.py +27 -0
- amu_mem0-0.1.0/pyproject.toml +49 -0
- amu_mem0-0.1.0/results/BENCH.md +43 -0
- amu_mem0-0.1.0/results/bench.json +236 -0
- amu_mem0-0.1.0/src/amu_mem0/__init__.py +36 -0
- amu_mem0-0.1.0/src/amu_mem0/gate.py +307 -0
- amu_mem0-0.1.0/src/amu_mem0/lineage.py +142 -0
- amu_mem0-0.1.0/src/amu_mem0/testing.py +60 -0
- amu_mem0-0.1.0/submissions/mem0-issue.md +63 -0
- amu_mem0-0.1.0/submissions/release-v0.1.0.md +21 -0
- amu_mem0-0.1.0/tests/__init__.py +0 -0
- amu_mem0-0.1.0/tests/conftest.py +29 -0
- amu_mem0-0.1.0/tests/test_gate.py +237 -0
- amu_mem0-0.1.0/uv.lock +1705 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
18
|
+
env:
|
|
19
|
+
MEM0_TELEMETRY: "False"
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: astral-sh/setup-uv@v6
|
|
23
|
+
- run: uv sync --python ${{ matrix.python }}
|
|
24
|
+
- run: uv run ruff check .
|
|
25
|
+
- run: uv run ruff format --check .
|
|
26
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
# Trusted publishing (OIDC): no API token is stored in this repo.
|
|
4
|
+
# One-time setup on each index: add a pending trusted publisher for
|
|
5
|
+
# repo sangaraju1988/amu-mem0, workflow publish.yml, and the environment
|
|
6
|
+
# named below (testpypi on test.pypi.org, pypi on pypi.org).
|
|
7
|
+
#
|
|
8
|
+
# TestPyPI: run manually (Actions > Publish > Run workflow).
|
|
9
|
+
# PyPI: runs only when a GitHub Release is published.
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
release:
|
|
14
|
+
types: [published]
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
testpypi:
|
|
18
|
+
if: github.event_name == 'workflow_dispatch'
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
environment: testpypi
|
|
21
|
+
permissions:
|
|
22
|
+
id-token: write
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: astral-sh/setup-uv@v6
|
|
26
|
+
- run: uv build
|
|
27
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
28
|
+
with:
|
|
29
|
+
repository-url: https://test.pypi.org/legacy/
|
|
30
|
+
|
|
31
|
+
pypi:
|
|
32
|
+
if: github.event_name == 'release'
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
environment: pypi
|
|
35
|
+
permissions:
|
|
36
|
+
id-token: write
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v4
|
|
39
|
+
- uses: astral-sh/setup-uv@v6
|
|
40
|
+
- run: uv build
|
|
41
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Evidence log
|
|
2
|
+
|
|
3
|
+
All times UTC. Each entry links to a public, timestamped record.
|
|
4
|
+
|
|
5
|
+
## Timeline
|
|
6
|
+
|
|
7
|
+
| When (UTC) | What | Link |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| 2026-11-15T04:03:59Z | Package and benchmark pushed (commit `1a3dae0`) | https://github.com/sangaraju1988/amu-mem0/commit/1a3dae0 |
|
|
10
|
+
| 2026-11-15T04:04:45Z | CI green on Python 3.10–3.13 | https://github.com/sangaraju1988/amu-mem0/actions/runs/43675135907 |
|
|
11
|
+
| 2026-11-15T04:53:37Z | Publish workflow pushed (commit `1a2d9ed`), CI green | https://github.com/sangaraju1988/amu-mem0/commit/1a2d9ed |
|
|
12
|
+
| 2026-11-15T04:54:56Z | Feature request opened on mem0 | https://github.com/mem0ai/mem0/issues/7927 |
|
|
13
|
+
| pending | TestPyPI upload | |
|
|
14
|
+
| pending | v0.1.0 tag and GitHub Release | |
|
|
15
|
+
| pending | PyPI release | https://pypi.org/project/amu-mem0/ |
|
|
16
|
+
|
|
17
|
+
## Benchmark
|
|
18
|
+
|
|
19
|
+
From `results/BENCH.md`, generated by `make bench` at commit `8783e3b` (synthetic multi-team setup, deterministic embedder, 420 memories; not production estimates):
|
|
20
|
+
|
|
21
|
+
| Condition | Leaked / returned | Queries with a leak | Recall@10 (entitled) |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| Plain mem0 | 857 / 2400 | 71.2% | 0.650 |
|
|
24
|
+
| Content-only filter (own labels) | 563 / 2400 (all derived) | 62.9% | 0.816 |
|
|
25
|
+
| amu-mem0 gate | 0 / 2164 | 0.0% | 0.999 |
|
|
26
|
+
|
|
27
|
+
Lineage coverage sweep (gate): queries with a leak at 100% / 90% / 75% / 50% of derivation edges recorded = 0.0% / 11.7% / 22.9% / 35.4%.
|
|
28
|
+
|
|
29
|
+
## To capture as PDF later
|
|
30
|
+
|
|
31
|
+
- [ ] mem0ai/mem0#7927 issue page, including any maintainer comments and labels
|
|
32
|
+
- [ ] Any PR page that follows, with reviewers (only if a maintainer asks for one)
|
|
33
|
+
- [ ] The merge commit, if anything is merged upstream
|
|
34
|
+
- [ ] GitHub Release v0.1.0 notes
|
|
35
|
+
- [ ] PyPI project page for amu-mem0 0.1.0
|
|
36
|
+
- [ ] This repo's CI run for the release commit
|
amu_mem0-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Venkata Sangaraju
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
amu_mem0-0.1.0/Makefile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.PHONY: install test lint format bench
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
uv sync
|
|
5
|
+
|
|
6
|
+
test:
|
|
7
|
+
MEM0_TELEMETRY=False uv run pytest -q
|
|
8
|
+
|
|
9
|
+
lint:
|
|
10
|
+
uv run ruff check .
|
|
11
|
+
uv run ruff format --check .
|
|
12
|
+
|
|
13
|
+
format:
|
|
14
|
+
uv run ruff check --fix .
|
|
15
|
+
uv run ruff format .
|
|
16
|
+
|
|
17
|
+
bench:
|
|
18
|
+
MEM0_TELEMETRY=False uv run python bench/run_bench.py
|
amu_mem0-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: amu-mem0
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Derivation-gated reads for mem0: a memory is returned only if the requester is entitled to every source it was derived from.
|
|
5
|
+
Project-URL: Paper, https://doi.org/10.1109/ACCESS.2026.3730363
|
|
6
|
+
Project-URL: amu-governance, https://github.com/sangaraju1988/amu-governance
|
|
7
|
+
Author: Venkata Sangaraju
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: access-control,agent-memory,data-lineage,mem0,privacy
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: amu-governance<0.2,>=0.1.2
|
|
22
|
+
Requires-Dist: mem0ai<3,>=2.2
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# amu-mem0
|
|
26
|
+
|
|
27
|
+
Derivation-gated reads for [mem0](https://github.com/mem0ai/mem0). A memory is
|
|
28
|
+
returned only if the requester is entitled to every sensitive label anywhere in
|
|
29
|
+
its lineage, including the sources of the memories it was derived from.
|
|
30
|
+
|
|
31
|
+
mem0 scopes memories by identity (`user_id`, `agent_id`, `run_id`). That
|
|
32
|
+
answers *whose* memory it is, not *what it was derived from*. When several
|
|
33
|
+
teams share one agent's memory, a summary written from restricted records looks
|
|
34
|
+
like any other memory and is returned to anyone who can search that scope.
|
|
35
|
+
`amu-mem0` records where each memory came from and checks it on every read.
|
|
36
|
+
|
|
37
|
+
The check is the rule from Sangaraju & Vissa, *Lineage-Aware Memory Governance*,
|
|
38
|
+
IEEE Access 14 (2026), [doi:10.1109/ACCESS.2026.3730363](https://doi.org/10.1109/ACCESS.2026.3730363):
|
|
39
|
+
serve a stored result only if `S(a) ⊆ P(d)`, where `S(a)` is the set of sensitive labels in
|
|
40
|
+
its derivation and `P(d)` is the set of labels the requester holds. The decision is made by
|
|
41
|
+
[`amu-governance`](https://github.com/sangaraju1988/amu-governance), not reimplemented here.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install amu-mem0 # not yet published; for now: pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Requires Python 3.10+, `mem0ai>=2.2,<3`, `amu-governance>=0.1.2,<0.2`. No other
|
|
50
|
+
dependencies.
|
|
51
|
+
|
|
52
|
+
## Quickstart
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from mem0 import Memory
|
|
56
|
+
from amu_mem0 import GatedMemory
|
|
57
|
+
|
|
58
|
+
memory = GatedMemory(Memory()) # wrap your existing instance
|
|
59
|
+
|
|
60
|
+
payroll = memory.add(
|
|
61
|
+
"Q3 payroll total is 4.2M",
|
|
62
|
+
agent_id="analyst",
|
|
63
|
+
sources={"hris": ["hr.salary"]}, # raw source -> its labels
|
|
64
|
+
)["results"][0]["id"]
|
|
65
|
+
memory.add("Payroll cost grew 8% QoQ", agent_id="analyst", derived_from=[payroll])
|
|
66
|
+
|
|
67
|
+
memory.search("payroll", filters={"agent_id": "analyst"}, entitlements={"mkt.campaign"})
|
|
68
|
+
# {'results': [], 'amu_denied': [{'id': ..., 'reason': 'unentitled', 'unentitled_labels': ['hr.salary']}, ...]}
|
|
69
|
+
|
|
70
|
+
memory.search("payroll", filters={"agent_id": "analyst"}, entitlements={"hr.salary"})
|
|
71
|
+
# both memories
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`examples/quickstart.py` is the same code running fully offline. It prints:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
marketing sees: []
|
|
78
|
+
marketing denied: [('unentitled', ['hr.salary']), ('unentitled', ['hr.salary'])]
|
|
79
|
+
finance sees: ['Payroll cost grew 8% QoQ', 'Q3 payroll total is 4.2M']
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## How it works
|
|
83
|
+
|
|
84
|
+
- **Writes.** `add(..., sources=..., derived_from=...)` stores lineage under the
|
|
85
|
+
reserved metadata key `amu`. Here `sources` maps each raw source to its labels,
|
|
86
|
+
and `derived_from` lists the ids of mem0 memories the new one was written from.
|
|
87
|
+
The transitive closure (`closed`) is computed once, at write time, from the
|
|
88
|
+
parents' stored closures. A summary of a summary of a summary therefore carries
|
|
89
|
+
every label beneath it, and a memory with mixed sources carries the union.
|
|
90
|
+
- **Reads.** `search`, `get` and `get_all` require `entitlements=` and keep a
|
|
91
|
+
result only if every sensitive label in its closed lineage is in that set.
|
|
92
|
+
`search` and `get_all` return mem0's normal shape plus `amu_denied`, which gives
|
|
93
|
+
the id, a reason code and the unentitled labels for every removed candidate.
|
|
94
|
+
Denials are also logged on the `amu_mem0` logger, with ids and label names only,
|
|
95
|
+
never memory text.
|
|
96
|
+
- **Labels.** By default every label gates a read. Pass
|
|
97
|
+
`GatedMemory(memory, sensitive_labels={...})` to gate only on a registry
|
|
98
|
+
(the paper's semantics). Labels outside the registry are then ignored.
|
|
99
|
+
- **Entitlement changes** take effect on the next read. Verdicts are never
|
|
100
|
+
stored; only labels are.
|
|
101
|
+
- **Recall.** Filtering after retrieval would starve `top_k`, so the gate
|
|
102
|
+
over-fetches. It doubles the fetch until it has `top_k` allowed results, the
|
|
103
|
+
fetch reaches `max_fetch` (default 1000), or a larger fetch adds no new
|
|
104
|
+
candidates.
|
|
105
|
+
|
|
106
|
+
### Fail-closed defaults
|
|
107
|
+
|
|
108
|
+
| Situation | Default | Reason code |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| Memory has no `amu` metadata (e.g. written with plain mem0) | denied | `missing_lineage` |
|
|
111
|
+
| `add()` with neither `sources` nor `derived_from` | stored, then denied | `incomplete_lineage` |
|
|
112
|
+
| A `derived_from` id is missing, or the parent has no or incomplete lineage | stored, then denied | `incomplete_lineage` |
|
|
113
|
+
| `amu` metadata is malformed | denied | `malformed_lineage` |
|
|
114
|
+
| `add(infer=True)` | raises `InferNotAllowedError` | n/a |
|
|
115
|
+
|
|
116
|
+
`on_missing_lineage="allow"` or `"allow_with_warning"` relaxes the first four
|
|
117
|
+
rows. A label already known to be forbidden still denies. `sources={}`
|
|
118
|
+
explicitly declares "no restricted inputs".
|
|
119
|
+
|
|
120
|
+
`infer=True` is rejected because mem0's extraction step shows the LLM up to 10
|
|
121
|
+
existing memories from the same scope. The facts it writes can draw on any of
|
|
122
|
+
them, and the caller cannot declare which. With `GatedMemory(allow_infer=True)`
|
|
123
|
+
such memories are stored but marked incomplete, so they stay hidden under the
|
|
124
|
+
default policy.
|
|
125
|
+
|
|
126
|
+
## Benchmark
|
|
127
|
+
|
|
128
|
+
> **Synthetic multi-team setup, deterministic embedder, 420 memories.** These
|
|
129
|
+
> results describe this benchmark only. They are not estimates of leak rates in
|
|
130
|
+
> production.
|
|
131
|
+
|
|
132
|
+
`make bench` runs a deterministic benchmark: fixed seed, offline hashed
|
|
133
|
+
bag-of-words embedder, local Qdrant, no LLM. Two teams share one mem0 scope
|
|
134
|
+
containing 420 memories: 180 raw records and 240 agent-written summaries,
|
|
135
|
+
aggregates, and 2- and 3-hop summaries. It runs 240 queries at `top_k=10`.
|
|
136
|
+
Full results, definitions, versions and the commit hash are in
|
|
137
|
+
[`results/BENCH.md`](results/BENCH.md).
|
|
138
|
+
|
|
139
|
+
Run at commit `8783e3b`, 2026-11-15 (synthetic multi-team setup, deterministic
|
|
140
|
+
embedder, 420 memories):
|
|
141
|
+
|
|
142
|
+
| Condition | Leaked / returned | Leak rate | Queries with a leak | Recall@10 for entitled users | p50 / p95 search ms |
|
|
143
|
+
|---|---|---|---|---|---|
|
|
144
|
+
| Plain mem0 | 857 / 2400 | 35.7% | 71.2% | 0.650 | 2.05 / 2.18 |
|
|
145
|
+
| Content-only filter (each memory's own labels) | 563 / 2400 | 23.5% | 62.9% | 0.816 | 2.91 / 3.03 |
|
|
146
|
+
| amu-mem0 gate | 0 / 2164 | 0.0% | 0.0% | 0.999 | 3.07 / 12.57 |
|
|
147
|
+
|
|
148
|
+
- **Content-only filtering misses derived memories.** It removes raw restricted
|
|
149
|
+
records, but none of what was derived from them: all 563 of its leaks are
|
|
150
|
+
derived memories.
|
|
151
|
+
- **Recall@10, exactly.** For each query, mem0 is searched with `top_k=500` and
|
|
152
|
+
the results are kept, in mem0's order, only if the requester is truly entitled
|
|
153
|
+
to them. The first min(10, n) of those form the ideal set. A returned memory is
|
|
154
|
+
a hit if it is in that entitled list with a score, in the same `top_k=500`
|
|
155
|
+
call, at least that of the 10th ideal item, so ties at rank 10 are not
|
|
156
|
+
penalised. Recall@10 = min(hits, |ideal|) / |ideal|, averaged over all 240
|
|
157
|
+
queries (every query has at least one entitled memory).
|
|
158
|
+
- **Plain mem0's lower recall is displacement.** It always returns 10 results,
|
|
159
|
+
and restricted memories take slots entitled ones would otherwise fill. In 170
|
|
160
|
+
of the 171 queries where it misses entitled memories, every miss is matched by
|
|
161
|
+
a restricted memory in its top 10. The remainder reflects mem0's ranking
|
|
162
|
+
depending on `top_k`.
|
|
163
|
+
- **The denominators differ: 2,164 vs 2,400.** The gate returns fewer than 10
|
|
164
|
+
results in 34 queries (236 empty slots). In all 34 it returned every entitled
|
|
165
|
+
memory mem0 surfaces for that query at any depth, because fewer than 10
|
|
166
|
+
entitled memories pass mem0's own score threshold. Leak rates are per returned
|
|
167
|
+
item, so each uses its own denominator.
|
|
168
|
+
- **Latency is tail overhead from over-fetch, at small scale.** When most top
|
|
169
|
+
candidates are restricted, the gate issues several underlying searches. That
|
|
170
|
+
shows up in p95, not p50. These are in-process timings on 420 memories with
|
|
171
|
+
local Qdrant. Latency was not measured at production scale, with a remote
|
|
172
|
+
vector store, or with real embeddings.
|
|
173
|
+
- **Comparison with the paper.** The content-only leak rate falls inside the
|
|
174
|
+
paper's 18.8–25.5% range for naive memory. That is not a replication: the
|
|
175
|
+
workload, store and leak definition all differ.
|
|
176
|
+
|
|
177
|
+
**Lineage coverage sweep (gate only; synthetic multi-team setup, deterministic
|
|
178
|
+
embedder, 420 memories).** Each `derived_from` edge is recorded with
|
|
179
|
+
probability c, and dropped edges are silent.
|
|
180
|
+
|
|
181
|
+
| Derivation edges recorded | Queries with a leak | Leak rate | Recall@10 |
|
|
182
|
+
|---|---|---|---|
|
|
183
|
+
| 100% | 0.0% | 0.0% | 0.999 |
|
|
184
|
+
| 90.2% | 11.7% | 1.4% | 0.995 |
|
|
185
|
+
| 75.9% | 22.9% | 4.8% | 0.983 |
|
|
186
|
+
| 49.9% | 35.4% | 11.2% | 0.949 |
|
|
187
|
+
|
|
188
|
+
The paper recommends at least 90% lineage completeness, based on 0% measured
|
|
189
|
+
leakage at 75–90% on its workloads. This workload is different: it drops whole
|
|
190
|
+
derivation edges in a multi-hop graph, where the paper dropped columns within a
|
|
191
|
+
flat result. Here some leakage remains at every coverage level below 100%.
|
|
192
|
+
|
|
193
|
+
## Threat model
|
|
194
|
+
|
|
195
|
+
**In scope.** A requester using the same mem0 scope as other teams receives a
|
|
196
|
+
memory derived, directly or through other memories, from a source whose label
|
|
197
|
+
they do not hold. Callers are trusted to pass the requester's real
|
|
198
|
+
entitlements and to declare lineage truthfully.
|
|
199
|
+
|
|
200
|
+
**Out of scope.**
|
|
201
|
+
- Calls made on the wrapped `Memory` directly (`gated.memory.search(...)`,
|
|
202
|
+
`history()`, `update()`). These bypass the gate.
|
|
203
|
+
- Anyone who can write mem0 metadata or the vector store directly. They can
|
|
204
|
+
forge or strip `amu` lineage. The gate is an application-level filter, not
|
|
205
|
+
storage-level enforcement. (For storage-level enforcement on Postgres, see
|
|
206
|
+
[amu-pgvector](https://github.com/sangaraju1988/amu-pgvector).)
|
|
207
|
+
- Content that encodes restricted information without naming its source
|
|
208
|
+
(inference through undeclared derived features, threat T4 in the paper), and
|
|
209
|
+
aggregation across many permitted reads (T6).
|
|
210
|
+
- mem0's SQLite history and message log, entity store and graph memory.
|
|
211
|
+
|
|
212
|
+
## Limitations
|
|
213
|
+
|
|
214
|
+
- **Zero leakage depends on lineage coverage.** The guarantee holds only if
|
|
215
|
+
every derivation is declared. An undeclared edge is invisible to the gate.
|
|
216
|
+
In the synthetic benchmark, with 90% of derivation edges recorded, 11.7% of
|
|
217
|
+
queries still returned at least one leaked memory. The paper recommends at
|
|
218
|
+
least 90% lineage completeness, and notes that deeper derivations raise the
|
|
219
|
+
requirement. Multi-hop agent memory is exactly that case. See the coverage
|
|
220
|
+
sweep in `results/BENCH.md`.
|
|
221
|
+
- **Derived-feature inference remains open.** A memory whose text paraphrases
|
|
222
|
+
restricted data without a declared source is not caught.
|
|
223
|
+
- **`infer=True` is unsupported** for gated data (see above).
|
|
224
|
+
- **Only the sync OSS `Memory` is supported.** `AsyncMemory` and the hosted
|
|
225
|
+
`MemoryClient` are not wrapped or tested.
|
|
226
|
+
- **`update()` is not gated or re-closed.** A child's closure is computed when
|
|
227
|
+
the child is written. Changing a parent's lineage later does not update its
|
|
228
|
+
children. Deleting a parent leaves children with their recorded closure.
|
|
229
|
+
- **Search cost grows with filtering.** When most top candidates are denied,
|
|
230
|
+
the gate issues several underlying searches. This was measured only as
|
|
231
|
+
small-scale tail overhead (p95 in the benchmark), not at production scale.
|
|
232
|
+
- Tested against mem0ai 2.2.1 with local Qdrant. Other vector stores are
|
|
233
|
+
expected to work because only metadata round-tripping is required, but they
|
|
234
|
+
are untested.
|
|
235
|
+
|
|
236
|
+
## Upstream discussion
|
|
237
|
+
|
|
238
|
+
The behaviour this package addresses is described for mem0's maintainers in
|
|
239
|
+
[mem0ai/mem0#7927](https://github.com/mem0ai/mem0/issues/7927). The issue
|
|
240
|
+
proposes no change to mem0's core; it asks whether a docs link would be welcome.
|
|
241
|
+
|
|
242
|
+
## Development
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
make install # uv sync
|
|
246
|
+
make test # pytest, against a real offline mem0 Memory (nothing mocked)
|
|
247
|
+
make lint
|
|
248
|
+
make bench
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Citation
|
|
252
|
+
|
|
253
|
+
If you use this, please cite the paper:
|
|
254
|
+
|
|
255
|
+
> V. Sangaraju and S. Vissa, "Lineage-Aware Memory Governance: A Derivation-Gated
|
|
256
|
+
> Framework for Privacy-Preserving Column-Level Access Control in Enterprise AI
|
|
257
|
+
> Agents," *IEEE Access*, vol. 14, pp. 139683–139693, 2026,
|
|
258
|
+
> doi:10.1109/ACCESS.2026.3730363.
|
|
259
|
+
|
|
260
|
+
MIT licensed. Not affiliated with or endorsed by Mem0.
|
amu_mem0-0.1.0/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# amu-mem0
|
|
2
|
+
|
|
3
|
+
Derivation-gated reads for [mem0](https://github.com/mem0ai/mem0). A memory is
|
|
4
|
+
returned only if the requester is entitled to every sensitive label anywhere in
|
|
5
|
+
its lineage, including the sources of the memories it was derived from.
|
|
6
|
+
|
|
7
|
+
mem0 scopes memories by identity (`user_id`, `agent_id`, `run_id`). That
|
|
8
|
+
answers *whose* memory it is, not *what it was derived from*. When several
|
|
9
|
+
teams share one agent's memory, a summary written from restricted records looks
|
|
10
|
+
like any other memory and is returned to anyone who can search that scope.
|
|
11
|
+
`amu-mem0` records where each memory came from and checks it on every read.
|
|
12
|
+
|
|
13
|
+
The check is the rule from Sangaraju & Vissa, *Lineage-Aware Memory Governance*,
|
|
14
|
+
IEEE Access 14 (2026), [doi:10.1109/ACCESS.2026.3730363](https://doi.org/10.1109/ACCESS.2026.3730363):
|
|
15
|
+
serve a stored result only if `S(a) ⊆ P(d)`, where `S(a)` is the set of sensitive labels in
|
|
16
|
+
its derivation and `P(d)` is the set of labels the requester holds. The decision is made by
|
|
17
|
+
[`amu-governance`](https://github.com/sangaraju1988/amu-governance), not reimplemented here.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install amu-mem0 # not yet published; for now: pip install -e .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Requires Python 3.10+, `mem0ai>=2.2,<3`, `amu-governance>=0.1.2,<0.2`. No other
|
|
26
|
+
dependencies.
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from mem0 import Memory
|
|
32
|
+
from amu_mem0 import GatedMemory
|
|
33
|
+
|
|
34
|
+
memory = GatedMemory(Memory()) # wrap your existing instance
|
|
35
|
+
|
|
36
|
+
payroll = memory.add(
|
|
37
|
+
"Q3 payroll total is 4.2M",
|
|
38
|
+
agent_id="analyst",
|
|
39
|
+
sources={"hris": ["hr.salary"]}, # raw source -> its labels
|
|
40
|
+
)["results"][0]["id"]
|
|
41
|
+
memory.add("Payroll cost grew 8% QoQ", agent_id="analyst", derived_from=[payroll])
|
|
42
|
+
|
|
43
|
+
memory.search("payroll", filters={"agent_id": "analyst"}, entitlements={"mkt.campaign"})
|
|
44
|
+
# {'results': [], 'amu_denied': [{'id': ..., 'reason': 'unentitled', 'unentitled_labels': ['hr.salary']}, ...]}
|
|
45
|
+
|
|
46
|
+
memory.search("payroll", filters={"agent_id": "analyst"}, entitlements={"hr.salary"})
|
|
47
|
+
# both memories
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`examples/quickstart.py` is the same code running fully offline. It prints:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
marketing sees: []
|
|
54
|
+
marketing denied: [('unentitled', ['hr.salary']), ('unentitled', ['hr.salary'])]
|
|
55
|
+
finance sees: ['Payroll cost grew 8% QoQ', 'Q3 payroll total is 4.2M']
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## How it works
|
|
59
|
+
|
|
60
|
+
- **Writes.** `add(..., sources=..., derived_from=...)` stores lineage under the
|
|
61
|
+
reserved metadata key `amu`. Here `sources` maps each raw source to its labels,
|
|
62
|
+
and `derived_from` lists the ids of mem0 memories the new one was written from.
|
|
63
|
+
The transitive closure (`closed`) is computed once, at write time, from the
|
|
64
|
+
parents' stored closures. A summary of a summary of a summary therefore carries
|
|
65
|
+
every label beneath it, and a memory with mixed sources carries the union.
|
|
66
|
+
- **Reads.** `search`, `get` and `get_all` require `entitlements=` and keep a
|
|
67
|
+
result only if every sensitive label in its closed lineage is in that set.
|
|
68
|
+
`search` and `get_all` return mem0's normal shape plus `amu_denied`, which gives
|
|
69
|
+
the id, a reason code and the unentitled labels for every removed candidate.
|
|
70
|
+
Denials are also logged on the `amu_mem0` logger, with ids and label names only,
|
|
71
|
+
never memory text.
|
|
72
|
+
- **Labels.** By default every label gates a read. Pass
|
|
73
|
+
`GatedMemory(memory, sensitive_labels={...})` to gate only on a registry
|
|
74
|
+
(the paper's semantics). Labels outside the registry are then ignored.
|
|
75
|
+
- **Entitlement changes** take effect on the next read. Verdicts are never
|
|
76
|
+
stored; only labels are.
|
|
77
|
+
- **Recall.** Filtering after retrieval would starve `top_k`, so the gate
|
|
78
|
+
over-fetches. It doubles the fetch until it has `top_k` allowed results, the
|
|
79
|
+
fetch reaches `max_fetch` (default 1000), or a larger fetch adds no new
|
|
80
|
+
candidates.
|
|
81
|
+
|
|
82
|
+
### Fail-closed defaults
|
|
83
|
+
|
|
84
|
+
| Situation | Default | Reason code |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| Memory has no `amu` metadata (e.g. written with plain mem0) | denied | `missing_lineage` |
|
|
87
|
+
| `add()` with neither `sources` nor `derived_from` | stored, then denied | `incomplete_lineage` |
|
|
88
|
+
| A `derived_from` id is missing, or the parent has no or incomplete lineage | stored, then denied | `incomplete_lineage` |
|
|
89
|
+
| `amu` metadata is malformed | denied | `malformed_lineage` |
|
|
90
|
+
| `add(infer=True)` | raises `InferNotAllowedError` | n/a |
|
|
91
|
+
|
|
92
|
+
`on_missing_lineage="allow"` or `"allow_with_warning"` relaxes the first four
|
|
93
|
+
rows. A label already known to be forbidden still denies. `sources={}`
|
|
94
|
+
explicitly declares "no restricted inputs".
|
|
95
|
+
|
|
96
|
+
`infer=True` is rejected because mem0's extraction step shows the LLM up to 10
|
|
97
|
+
existing memories from the same scope. The facts it writes can draw on any of
|
|
98
|
+
them, and the caller cannot declare which. With `GatedMemory(allow_infer=True)`
|
|
99
|
+
such memories are stored but marked incomplete, so they stay hidden under the
|
|
100
|
+
default policy.
|
|
101
|
+
|
|
102
|
+
## Benchmark
|
|
103
|
+
|
|
104
|
+
> **Synthetic multi-team setup, deterministic embedder, 420 memories.** These
|
|
105
|
+
> results describe this benchmark only. They are not estimates of leak rates in
|
|
106
|
+
> production.
|
|
107
|
+
|
|
108
|
+
`make bench` runs a deterministic benchmark: fixed seed, offline hashed
|
|
109
|
+
bag-of-words embedder, local Qdrant, no LLM. Two teams share one mem0 scope
|
|
110
|
+
containing 420 memories: 180 raw records and 240 agent-written summaries,
|
|
111
|
+
aggregates, and 2- and 3-hop summaries. It runs 240 queries at `top_k=10`.
|
|
112
|
+
Full results, definitions, versions and the commit hash are in
|
|
113
|
+
[`results/BENCH.md`](results/BENCH.md).
|
|
114
|
+
|
|
115
|
+
Run at commit `8783e3b`, 2026-11-15 (synthetic multi-team setup, deterministic
|
|
116
|
+
embedder, 420 memories):
|
|
117
|
+
|
|
118
|
+
| Condition | Leaked / returned | Leak rate | Queries with a leak | Recall@10 for entitled users | p50 / p95 search ms |
|
|
119
|
+
|---|---|---|---|---|---|
|
|
120
|
+
| Plain mem0 | 857 / 2400 | 35.7% | 71.2% | 0.650 | 2.05 / 2.18 |
|
|
121
|
+
| Content-only filter (each memory's own labels) | 563 / 2400 | 23.5% | 62.9% | 0.816 | 2.91 / 3.03 |
|
|
122
|
+
| amu-mem0 gate | 0 / 2164 | 0.0% | 0.0% | 0.999 | 3.07 / 12.57 |
|
|
123
|
+
|
|
124
|
+
- **Content-only filtering misses derived memories.** It removes raw restricted
|
|
125
|
+
records, but none of what was derived from them: all 563 of its leaks are
|
|
126
|
+
derived memories.
|
|
127
|
+
- **Recall@10, exactly.** For each query, mem0 is searched with `top_k=500` and
|
|
128
|
+
the results are kept, in mem0's order, only if the requester is truly entitled
|
|
129
|
+
to them. The first min(10, n) of those form the ideal set. A returned memory is
|
|
130
|
+
a hit if it is in that entitled list with a score, in the same `top_k=500`
|
|
131
|
+
call, at least that of the 10th ideal item, so ties at rank 10 are not
|
|
132
|
+
penalised. Recall@10 = min(hits, |ideal|) / |ideal|, averaged over all 240
|
|
133
|
+
queries (every query has at least one entitled memory).
|
|
134
|
+
- **Plain mem0's lower recall is displacement.** It always returns 10 results,
|
|
135
|
+
and restricted memories take slots entitled ones would otherwise fill. In 170
|
|
136
|
+
of the 171 queries where it misses entitled memories, every miss is matched by
|
|
137
|
+
a restricted memory in its top 10. The remainder reflects mem0's ranking
|
|
138
|
+
depending on `top_k`.
|
|
139
|
+
- **The denominators differ: 2,164 vs 2,400.** The gate returns fewer than 10
|
|
140
|
+
results in 34 queries (236 empty slots). In all 34 it returned every entitled
|
|
141
|
+
memory mem0 surfaces for that query at any depth, because fewer than 10
|
|
142
|
+
entitled memories pass mem0's own score threshold. Leak rates are per returned
|
|
143
|
+
item, so each uses its own denominator.
|
|
144
|
+
- **Latency is tail overhead from over-fetch, at small scale.** When most top
|
|
145
|
+
candidates are restricted, the gate issues several underlying searches. That
|
|
146
|
+
shows up in p95, not p50. These are in-process timings on 420 memories with
|
|
147
|
+
local Qdrant. Latency was not measured at production scale, with a remote
|
|
148
|
+
vector store, or with real embeddings.
|
|
149
|
+
- **Comparison with the paper.** The content-only leak rate falls inside the
|
|
150
|
+
paper's 18.8–25.5% range for naive memory. That is not a replication: the
|
|
151
|
+
workload, store and leak definition all differ.
|
|
152
|
+
|
|
153
|
+
**Lineage coverage sweep (gate only; synthetic multi-team setup, deterministic
|
|
154
|
+
embedder, 420 memories).** Each `derived_from` edge is recorded with
|
|
155
|
+
probability c, and dropped edges are silent.
|
|
156
|
+
|
|
157
|
+
| Derivation edges recorded | Queries with a leak | Leak rate | Recall@10 |
|
|
158
|
+
|---|---|---|---|
|
|
159
|
+
| 100% | 0.0% | 0.0% | 0.999 |
|
|
160
|
+
| 90.2% | 11.7% | 1.4% | 0.995 |
|
|
161
|
+
| 75.9% | 22.9% | 4.8% | 0.983 |
|
|
162
|
+
| 49.9% | 35.4% | 11.2% | 0.949 |
|
|
163
|
+
|
|
164
|
+
The paper recommends at least 90% lineage completeness, based on 0% measured
|
|
165
|
+
leakage at 75–90% on its workloads. This workload is different: it drops whole
|
|
166
|
+
derivation edges in a multi-hop graph, where the paper dropped columns within a
|
|
167
|
+
flat result. Here some leakage remains at every coverage level below 100%.
|
|
168
|
+
|
|
169
|
+
## Threat model
|
|
170
|
+
|
|
171
|
+
**In scope.** A requester using the same mem0 scope as other teams receives a
|
|
172
|
+
memory derived, directly or through other memories, from a source whose label
|
|
173
|
+
they do not hold. Callers are trusted to pass the requester's real
|
|
174
|
+
entitlements and to declare lineage truthfully.
|
|
175
|
+
|
|
176
|
+
**Out of scope.**
|
|
177
|
+
- Calls made on the wrapped `Memory` directly (`gated.memory.search(...)`,
|
|
178
|
+
`history()`, `update()`). These bypass the gate.
|
|
179
|
+
- Anyone who can write mem0 metadata or the vector store directly. They can
|
|
180
|
+
forge or strip `amu` lineage. The gate is an application-level filter, not
|
|
181
|
+
storage-level enforcement. (For storage-level enforcement on Postgres, see
|
|
182
|
+
[amu-pgvector](https://github.com/sangaraju1988/amu-pgvector).)
|
|
183
|
+
- Content that encodes restricted information without naming its source
|
|
184
|
+
(inference through undeclared derived features, threat T4 in the paper), and
|
|
185
|
+
aggregation across many permitted reads (T6).
|
|
186
|
+
- mem0's SQLite history and message log, entity store and graph memory.
|
|
187
|
+
|
|
188
|
+
## Limitations
|
|
189
|
+
|
|
190
|
+
- **Zero leakage depends on lineage coverage.** The guarantee holds only if
|
|
191
|
+
every derivation is declared. An undeclared edge is invisible to the gate.
|
|
192
|
+
In the synthetic benchmark, with 90% of derivation edges recorded, 11.7% of
|
|
193
|
+
queries still returned at least one leaked memory. The paper recommends at
|
|
194
|
+
least 90% lineage completeness, and notes that deeper derivations raise the
|
|
195
|
+
requirement. Multi-hop agent memory is exactly that case. See the coverage
|
|
196
|
+
sweep in `results/BENCH.md`.
|
|
197
|
+
- **Derived-feature inference remains open.** A memory whose text paraphrases
|
|
198
|
+
restricted data without a declared source is not caught.
|
|
199
|
+
- **`infer=True` is unsupported** for gated data (see above).
|
|
200
|
+
- **Only the sync OSS `Memory` is supported.** `AsyncMemory` and the hosted
|
|
201
|
+
`MemoryClient` are not wrapped or tested.
|
|
202
|
+
- **`update()` is not gated or re-closed.** A child's closure is computed when
|
|
203
|
+
the child is written. Changing a parent's lineage later does not update its
|
|
204
|
+
children. Deleting a parent leaves children with their recorded closure.
|
|
205
|
+
- **Search cost grows with filtering.** When most top candidates are denied,
|
|
206
|
+
the gate issues several underlying searches. This was measured only as
|
|
207
|
+
small-scale tail overhead (p95 in the benchmark), not at production scale.
|
|
208
|
+
- Tested against mem0ai 2.2.1 with local Qdrant. Other vector stores are
|
|
209
|
+
expected to work because only metadata round-tripping is required, but they
|
|
210
|
+
are untested.
|
|
211
|
+
|
|
212
|
+
## Upstream discussion
|
|
213
|
+
|
|
214
|
+
The behaviour this package addresses is described for mem0's maintainers in
|
|
215
|
+
[mem0ai/mem0#7927](https://github.com/mem0ai/mem0/issues/7927). The issue
|
|
216
|
+
proposes no change to mem0's core; it asks whether a docs link would be welcome.
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
make install # uv sync
|
|
222
|
+
make test # pytest, against a real offline mem0 Memory (nothing mocked)
|
|
223
|
+
make lint
|
|
224
|
+
make bench
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Citation
|
|
228
|
+
|
|
229
|
+
If you use this, please cite the paper:
|
|
230
|
+
|
|
231
|
+
> V. Sangaraju and S. Vissa, "Lineage-Aware Memory Governance: A Derivation-Gated
|
|
232
|
+
> Framework for Privacy-Preserving Column-Level Access Control in Enterprise AI
|
|
233
|
+
> Agents," *IEEE Access*, vol. 14, pp. 139683–139693, 2026,
|
|
234
|
+
> doi:10.1109/ACCESS.2026.3730363.
|
|
235
|
+
|
|
236
|
+
MIT licensed. Not affiliated with or endorsed by Mem0.
|