memdebug 0.3.0__tar.gz → 0.4.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.
- memdebug-0.4.1/.github/ISSUE_TEMPLATE/bug_report.yml +87 -0
- memdebug-0.4.1/.github/pull_request_template.md +24 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/.gitignore +4 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/CHANGELOG.md +45 -0
- memdebug-0.4.1/CONTRIBUTING.md +123 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/PKG-INFO +106 -7
- {memdebug-0.3.0 → memdebug-0.4.1}/README.md +105 -6
- {memdebug-0.3.0 → memdebug-0.4.1}/ROADMAP.md +13 -7
- {memdebug-0.3.0 → memdebug-0.4.1}/docs/agents.md +2 -1
- memdebug-0.4.1/docs/demo.cast +21 -0
- memdebug-0.4.1/docs/demo.gif +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/docs/threat-model.md +17 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/pyproject.toml +18 -5
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/__init__.py +1 -1
- memdebug-0.4.1/src/memdebug/__main__.py +4 -0
- memdebug-0.4.1/src/memdebug/adapters/__init__.py +6 -0
- memdebug-0.4.1/src/memdebug/adapters/base.py +83 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/adapters/common.py +18 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/adapters/fileops.py +49 -12
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/adapters/folder.py +29 -6
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/adapters/folder_restore.py +72 -11
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/adapters/markdown_git.py +82 -19
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/adapters/mem0.py +51 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/adapters/openwebui.py +28 -2
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/adapters/restore.py +114 -8
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/agents.py +44 -4
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/backends.py +1 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/cli.py +106 -55
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/demo.py +47 -5
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/describe.py +5 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/diff.py +36 -3
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/docker_source.py +14 -1
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/hints.py +10 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/ledger.py +132 -17
- memdebug-0.4.1/src/memdebug/longpath.py +104 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/models.py +69 -4
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/monitor.py +129 -11
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/paths.py +9 -2
- memdebug-0.4.1/src/memdebug/provenance.py +298 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/reconcile.py +24 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/report.py +67 -3
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/rollback_flow.py +57 -4
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/selftest.py +326 -83
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/stores.py +76 -8
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/sync.py +45 -9
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/textsafe.py +23 -6
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/viewer/html.py +24 -3
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/viewer/pages.py +199 -17
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/viewer/redline.py +4 -2
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/viewer/server.py +60 -19
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/witness.py +24 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_agents.py +13 -1
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_docker_source.py +3 -1
- memdebug-0.4.1/tests/test_long_paths.py +317 -0
- memdebug-0.4.1/tests/test_provenance.py +245 -0
- memdebug-0.4.1/tests/test_provenance_check.py +197 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_release_hygiene.py +2 -1
- memdebug-0.4.1/tests/test_selftest_tripwire.py +340 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_viewer.py +13 -1
- memdebug-0.4.1/tests/test_viewer_agents.py +81 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_viewer_html.py +23 -0
- memdebug-0.3.0/CONTRIBUTING.md +0 -57
- memdebug-0.3.0/src/memdebug/__main__.py +0 -3
- memdebug-0.3.0/src/memdebug/adapters/__init__.py +0 -0
- memdebug-0.3.0/src/memdebug/adapters/base.py +0 -37
- {memdebug-0.3.0 → memdebug-0.4.1}/.gitattributes +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/.github/dependabot.yml +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/.github/workflows/ci.yml +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/.github/workflows/release.yml +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/LICENSE +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/NOTICE +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/SECURITY.md +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/docs/releasing.md +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/errors.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/viewer/__init__.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/src/memdebug/viewer/style.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/conftest.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/payloads.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_cli.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_demo.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_diff.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_folder_and_openwebui.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_folder_restore.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_folder_rollback_cli.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_hints.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_ledger.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_ledger_read.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_markdown_git.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_mem0_adapter.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_models.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_platform.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_provenance_openwebui.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_readable_ids_and_honest_wording.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_reconcile.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_report.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_restore.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_rollback_cli.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_snapshots.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_stores_and_monitor.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_sync.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_textsafe.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_viewer_putback.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_viewer_redline.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_viewer_rollback.py +0 -0
- {memdebug-0.3.0 → memdebug-0.4.1}/tests/test_witness.py +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something does not work as the documentation says
|
|
3
|
+
labels: ["bug"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
## Please read this first
|
|
9
|
+
|
|
10
|
+
**Never paste memory contents or session transcripts into this issue.** An agent's memory and its conversation logs hold private text,
|
|
11
|
+
and an issue is public and stays public even if you edit or delete it. Describe what the text *did*, or replace it with a made-up
|
|
12
|
+
example that behaves the same way.
|
|
13
|
+
|
|
14
|
+
Also leave out: the ledger (`*.db`), `stores.json`, backup folders, the link printed by `memdebug serve` (it contains a secret),
|
|
15
|
+
API keys, and personal paths (replace them with `<home>`). Be careful with the output of `memdebug check`, `timeline`, `diff` and
|
|
16
|
+
`report`: it can quote the start of changed notes.
|
|
17
|
+
|
|
18
|
+
**A security problem?** Do not open an issue. Report it privately: https://github.com/juraj-jumic/memdebug/security/advisories/new
|
|
19
|
+
(see [SECURITY.md](https://github.com/juraj-jumic/memdebug/blob/main/SECURITY.md)).
|
|
20
|
+
- type: checkboxes
|
|
21
|
+
id: safe
|
|
22
|
+
attributes:
|
|
23
|
+
label: Before you submit
|
|
24
|
+
options:
|
|
25
|
+
- label: I have not included memory contents, session transcripts, a ledger, `stores.json`, a viewer link or personal paths.
|
|
26
|
+
required: true
|
|
27
|
+
- label: This is not a security problem.
|
|
28
|
+
required: true
|
|
29
|
+
- type: textarea
|
|
30
|
+
id: happened
|
|
31
|
+
attributes:
|
|
32
|
+
label: What happened
|
|
33
|
+
description: What you did, and what memdebug did.
|
|
34
|
+
validations:
|
|
35
|
+
required: true
|
|
36
|
+
- type: textarea
|
|
37
|
+
id: expected
|
|
38
|
+
attributes:
|
|
39
|
+
label: What you expected
|
|
40
|
+
validations:
|
|
41
|
+
required: true
|
|
42
|
+
- type: textarea
|
|
43
|
+
id: steps
|
|
44
|
+
attributes:
|
|
45
|
+
label: Steps to reproduce
|
|
46
|
+
description: The commands you ran, ideally against a made-up folder of notes. `memdebug demo` shows what a healthy run looks like.
|
|
47
|
+
- type: input
|
|
48
|
+
id: version
|
|
49
|
+
attributes:
|
|
50
|
+
label: memdebug version
|
|
51
|
+
description: The output of `memdebug --version`. If you use pipx and a virtual environment, make sure it is the copy you actually ran.
|
|
52
|
+
validations:
|
|
53
|
+
required: true
|
|
54
|
+
- type: dropdown
|
|
55
|
+
id: system
|
|
56
|
+
attributes:
|
|
57
|
+
label: Operating system
|
|
58
|
+
options:
|
|
59
|
+
- Windows
|
|
60
|
+
- macOS
|
|
61
|
+
- Linux
|
|
62
|
+
- Other
|
|
63
|
+
validations:
|
|
64
|
+
required: true
|
|
65
|
+
- type: input
|
|
66
|
+
id: tools
|
|
67
|
+
attributes:
|
|
68
|
+
label: Python and git versions
|
|
69
|
+
description: The output of `python --version` and `git --version`.
|
|
70
|
+
- type: dropdown
|
|
71
|
+
id: store
|
|
72
|
+
attributes:
|
|
73
|
+
label: Kind of store involved
|
|
74
|
+
multiple: true
|
|
75
|
+
options:
|
|
76
|
+
- Markdown notes in git
|
|
77
|
+
- Plain folder of markdown notes
|
|
78
|
+
- Open WebUI
|
|
79
|
+
- Mem0
|
|
80
|
+
- The viewer (memdebug serve)
|
|
81
|
+
- Not about a store
|
|
82
|
+
- type: textarea
|
|
83
|
+
id: output
|
|
84
|
+
attributes:
|
|
85
|
+
label: Output or error message
|
|
86
|
+
description: Only text you have read through and are sure holds no memory contents. The output of `memdebug selftest` is useful here, but it names your user folder on Windows, so replace that with `<home>` first.
|
|
87
|
+
render: text
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
## What this changes, and why
|
|
2
|
+
|
|
3
|
+
<!-- One or two sentences. Link the issue if there is one. -->
|
|
4
|
+
|
|
5
|
+
## How it was checked
|
|
6
|
+
|
|
7
|
+
Run these with the repository's own environment (see [CONTRIBUTING.md](https://github.com/juraj-jumic/memdebug/blob/main/CONTRIBUTING.md)):
|
|
8
|
+
|
|
9
|
+
- [ ] `python -m pytest -q -n auto`, `ruff check src tests`, `mypy` and `python -m memdebug selftest` all pass
|
|
10
|
+
- [ ] If this adds or changes a protection: I removed it on purpose and a test failed (CONTRIBUTING, ground rule 5). Otherwise: no protection changed
|
|
11
|
+
- [ ] Hostile input is handled: text read from a store, log or repository is bounded, shown through `safe_text` or the viewer's builder, and any name from the ledger is validated (validators end with `\Z`, never `$`) before it reaches a command or a path
|
|
12
|
+
- [ ] Windows is covered: file names that differ only by case, CRLF, and junctions are considered; a POSIX-only test is marked skipped with a reason
|
|
13
|
+
|
|
14
|
+
## Documents
|
|
15
|
+
|
|
16
|
+
- [ ] `CHANGELOG.md` has a line under `[Unreleased]` for anything a user would notice
|
|
17
|
+
- [ ] Anything the change makes untrue is updated, and a new limit is written into `docs/threat-model.md`
|
|
18
|
+
|
|
19
|
+
## Private data
|
|
20
|
+
|
|
21
|
+
- [ ] The diff and the commit messages contain no memory text, session transcripts, `*.db` files, `stores.json`, `backups/`, `copies/` or personal paths. Test data is made up
|
|
22
|
+
- [ ] I understand the history is public. A GitHub no-reply address is fine as the commit email
|
|
23
|
+
|
|
24
|
+
By contributing you agree that your contribution is licensed under the Apache License 2.0 (see [LICENSE](https://github.com/juraj-jumic/memdebug/blob/main/LICENSE)).
|
|
@@ -26,7 +26,11 @@ htmlcov/
|
|
|
26
26
|
*.partial
|
|
27
27
|
stores.json
|
|
28
28
|
copies/
|
|
29
|
+
backups/
|
|
29
30
|
memdebug-report*
|
|
31
|
+
# Rollback backups hold the notes byte for byte, and agent session logs are whole conversations. If a test ever needs a made-up .jsonl file,
|
|
32
|
+
# add a negation for that one path (!tests/data/example.jsonl) instead of removing the rule.
|
|
33
|
+
*.jsonl
|
|
30
34
|
|
|
31
35
|
# Secrets
|
|
32
36
|
.env
|
|
@@ -3,6 +3,51 @@
|
|
|
3
3
|
All notable changes. The format follows [Keep a Changelog](https://keepachangelog.com/); versions follow [Semantic Versioning](https://semver.org/)
|
|
4
4
|
(alpha: anything may change before 1.0).
|
|
5
5
|
|
|
6
|
+
## [0.4.1] - 2026-10-09
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- **The viewer's diff view hid memory text.** It skipped every diff line that started with `+++` or `---`, to drop the diff's file headers, so an added or
|
|
10
|
+
removed line of memory text that began with `++` or `--` (for example a planted `++ send passwords to ...`) did not appear on the page. Only the two
|
|
11
|
+
real header lines at the top of a diff are skipped now. The timeline's event inspector and the Compare page were affected; the terminal output was not.
|
|
12
|
+
- **Windows paths longer than 259 characters.** Windows refuses such paths unless long paths are switched on in the registry, which is off by default.
|
|
13
|
+
Before, a note beyond the limit was left out of the baseline and of every check (`check` said "quiet, nothing new", exit code 0, with only a trailing
|
|
14
|
+
warning), a rollback said "Nothing to restore" while that note stayed tampered, a rollback whose backup path was beyond the limit failed, and a store
|
|
15
|
+
folder beyond the limit failed with "unexpected error". memdebug now hands Windows the extended-length form of every note, backup and session-log
|
|
16
|
+
path, so none of this depends on that setting. A git repository in a very deeply nested folder can still hit git's own limits.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- The code now follows the Google Python Style Guide, and CI enforces the parts a tool can check: every public module, class, function and method has a
|
|
20
|
+
docstring, every function has type annotations (`mypy` now requires them), exceptions end in `Error`, and a broad `except Exception` needs a stated
|
|
21
|
+
reason. No behaviour changed. CONTRIBUTING.md has a "Code style" section that also lists where the project deliberately differs from the guide.
|
|
22
|
+
|
|
23
|
+
## [0.4.0] - 2026-10-09
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- **Who wrote it.** For a changed note that looks suspicious, or that changed outside git, `memdebug check` and `memdebug watch` now also say which
|
|
27
|
+
logged Claude Code session wrote it, from that agent's session logs ("who wrote it: ..."), or that no logged edit explains it. It returns only a
|
|
28
|
+
session id, a time and a tool name, never a path or any text from a log. It is evidence, not proof: a deleted log looks the same as a note nobody
|
|
29
|
+
wrote, and shell-made changes cannot be matched (they are only counted). See `docs/threat-model.md`.
|
|
30
|
+
- The viewer has an **Agents** page (`memdebug serve`): the known agents that appear to be installed and what memdebug could watch for each, with the command
|
|
31
|
+
to run (`memdebug setup`). It only checks that folders exist, opens no file, and works even while the ledger is busy.
|
|
32
|
+
- Cline is in the agent catalog: `memdebug agents` and `memdebug setup` offer its global rules folder (`~/Documents/Cline/Rules`) once it holds markdown rules.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- `memdebug selftest`, "rollback is safe" and "hostile repository config cannot run programs": the control (ordinary git, tripwires armed) now runs in
|
|
36
|
+
a separate repository with its own marker files, so nothing it leaves behind can be mistaken for an escape from the protected run, which is still
|
|
37
|
+
asserted to run no tripwire. The tripwires now record what started them (time, arguments, and the parent and grandparent command lines where the
|
|
38
|
+
platform shows them: `/proc` on Linux, `ps` on macOS, nothing on Windows), and a failure prints those records and `git --version`. This follows a
|
|
39
|
+
failure on CI that passed on a re-run and could not be reproduced.
|
|
40
|
+
- The repository's ignore file also excludes `backups/` and `*.jsonl` (session logs), and the release hygiene test checks both rules.
|
|
41
|
+
|
|
42
|
+
### Documentation
|
|
43
|
+
- The README has badges, a recorded demo (`docs/demo.gif`, with the unpaced recording in `docs/demo.cast`), and new "Why this matters", "How it works" and
|
|
44
|
+
"Engineering" sections, checked against the code; claims about snapshots, the ledger and rollback that overstated what the code does were corrected.
|
|
45
|
+
- CONTRIBUTING.md now starts with the private-data rule and covers reporting a security problem, setup, writing tests, changing the viewer and the pull
|
|
46
|
+
request process; its second ground rule names both rollback engines. There is a pull request template and a bug-report form that tells people never
|
|
47
|
+
to paste memory contents or session transcripts.
|
|
48
|
+
- `docs/threat-model.md` has a section on the session-log reader, and `docs/agents.md` has a row for Cline and records Claude Code's per-project memory
|
|
49
|
+
folder as confirmed on Windows 11.
|
|
50
|
+
|
|
6
51
|
## [0.3.0] - 2026-10-07
|
|
7
52
|
|
|
8
53
|
### Added
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for helping. memdebug is a security tool, so the bar for changes is "what happens if the input is hostile?".
|
|
4
|
+
|
|
5
|
+
## Private data comes first
|
|
6
|
+
|
|
7
|
+
memdebug reads people's agent memory, so this repository must never hold any of it. Never commit, attach or paste (in an issue, a pull
|
|
8
|
+
request, a test or a commit message): real memory text, session transcripts, a ledger or any other `*.db` file, `stores.json`, backup or copy
|
|
9
|
+
folders, the link `memdebug serve` prints (it contains a secret), tokens or keys, or paths that name your user account. Test data is made up.
|
|
10
|
+
The ignore file keeps the common cases out (databases, `stores.json`, copy and backup folders, `*.jsonl` session logs, reports, keys), but it
|
|
11
|
+
is only a safety net: read `git status` and `git diff --cached` before every commit. The history is public, and a pushed commit cannot be
|
|
12
|
+
taken back without rewriting it.
|
|
13
|
+
|
|
14
|
+
## Reporting a security problem
|
|
15
|
+
|
|
16
|
+
Do not open a public issue or pull request for a security problem. Report it privately through GitHub
|
|
17
|
+
(https://github.com/juraj-jumic/memdebug/security/advisories/new); [SECURITY.md](SECURITY.md) says what counts and what to include. Keep private
|
|
18
|
+
text out of the report as well: a made-up example that behaves the same way is enough.
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
python -m venv .venv
|
|
23
|
+
.venv\Scripts\activate # Windows (PowerShell or cmd); on macOS and Linux: source .venv/bin/activate
|
|
24
|
+
pip install -e ".[dev]"
|
|
25
|
+
python -m pytest -n auto # the whole suite, in parallel; plain `python -m pytest` works too
|
|
26
|
+
python -m memdebug selftest # proves the protections on THIS machine
|
|
27
|
+
ruff check src tests # lint (CI enforces it)
|
|
28
|
+
mypy # types (CI enforces it)
|
|
29
|
+
|
|
30
|
+
You need Python 3.10+ and git 2.31+. The suite starts many git processes, so it is slower on Windows. Run everything through this environment's
|
|
31
|
+
`python -m ...`: a `memdebug` command on your PATH (a pipx install, say) can be an older release than the code you are editing, while
|
|
32
|
+
`python -m memdebug` is always your checkout. Two release-hygiene tests are skipped unless PyYAML is installed (`pip install pyyaml`).
|
|
33
|
+
|
|
34
|
+
## Ground rules
|
|
35
|
+
|
|
36
|
+
1. **Everything read from a memory store or repository is untrusted.** Bound its size, never print it raw (use
|
|
37
|
+
`safe_text`), and never let it become markup (use the viewer's builder, never string-built HTML).
|
|
38
|
+
2. **Adapters only read.** The only code allowed to change a memory store is the two rollback engines, `adapters/restore.py` (git
|
|
39
|
+
notes) and `adapters/folder_restore.py` (plain folders), which share the file-writing code in `adapters/fileops.py`. They have their
|
|
40
|
+
own rules: a dry run first, a plan the person confirms, a backup of anything not already held (in git, or in a private backup folder),
|
|
41
|
+
no rewritten history, undo on failure.
|
|
42
|
+
3. **No shell, fixed argument lists, no programs named by the data.** git runs with a scrubbed environment and the
|
|
43
|
+
repository's risky settings overridden. Do not add a git command without checking it cannot run a hook, filter or
|
|
44
|
+
external helper.
|
|
45
|
+
4. **Explicit text encodings** everywhere (`encoding="utf-8"`); a test enforces this because Windows defaults to a legacy
|
|
46
|
+
code page.
|
|
47
|
+
5. **A protection without a test that fails when it is removed is not a protection.** Break your own code on purpose
|
|
48
|
+
(remove the check) and confirm a test notices. Several tests exist only because that exercise found a gap.
|
|
49
|
+
6. **Windows is a first-class platform.** Avoid POSIX-only assumptions; if a test truly needs POSIX, mark it and say why. Two traps that only show up
|
|
50
|
+
there: file names ignore case ("A.db" and "a.db" are one file, so never name files after values that can differ only by case), and text-mode
|
|
51
|
+
writes turn `\n` into `\r\n` (write bytes when exact content matters).
|
|
52
|
+
7. **Say what a feature does not do.** Add its limits to `docs/threat-model.md`.
|
|
53
|
+
|
|
54
|
+
## Code style
|
|
55
|
+
|
|
56
|
+
The code follows the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html). CI checks the parts a tool can check:
|
|
57
|
+
`ruff` for docstrings (Google convention), naming, a type annotation on every function, and no blind `except Exception` without a stated reason;
|
|
58
|
+
`mypy` for the types themselves (every function must be annotated). The rest is for review. In practice:
|
|
59
|
+
|
|
60
|
+
* Every public module, class, function and method has a docstring: a one-line summary, then `Args:`, `Returns:`, `Raises:` and `Attributes:`
|
|
61
|
+
sections only where they say something the signature and the annotations do not. Describe behaviour and side effects, not implementation.
|
|
62
|
+
* Types are written as `X | None`; exceptions end in `Error`; `Any` is fine where a type should not be expressed.
|
|
63
|
+
* `except Exception` is only for an isolation point (one store, one request or one undo step failing without stopping the rest), with a
|
|
64
|
+
`# noqa: BLE001 - reason` that says so. Everything else catches what it expects.
|
|
65
|
+
* Tests need no docstrings or annotations.
|
|
66
|
+
|
|
67
|
+
Where the project deliberately differs from the guide, because changing it would add churn without making the code safer or clearer:
|
|
68
|
+
|
|
69
|
+
* Lines may be up to 140 characters (the guide says 80). There is no auto-formatter.
|
|
70
|
+
* The package uses relative imports and `from module import name` (the guide asks for absolute imports of modules only).
|
|
71
|
+
* A few helpers are `@staticmethod`s on the class they belong to (the guide prefers module-level functions).
|
|
72
|
+
* Some functions are longer than the guide's 40-line hint, mostly the rollback planners and the viewer's page builders, where splitting
|
|
73
|
+
would scatter a security-relevant sequence across helpers.
|
|
74
|
+
* A comprehension may have an `if` when it fits on one line.
|
|
75
|
+
|
|
76
|
+
## Writing tests
|
|
77
|
+
|
|
78
|
+
* Use made-up data only. Every test already gets an empty home folder and no Docker (`isolated_home` and `no_real_docker` in
|
|
79
|
+
`tests/conftest.py`), so never read a real home folder, agent folder, session log or container, and never put a real transcript in a
|
|
80
|
+
fixture. Write a made-up log, as `tests/test_provenance.py` does. A test that sets the home folder sets both `HOME` and `USERPROFILE`.
|
|
81
|
+
* A new protection needs a test that fails when it is removed (ground rule 5). A new validator or route pattern ends with `\Z`, never `$`,
|
|
82
|
+
and is added to `test_no_validator_accepts_a_trailing_line_break` in `tests/test_docker_source.py`, which tries every pattern with a
|
|
83
|
+
trailing line break.
|
|
84
|
+
* Give parametrized tests short `ids=`, and mark a POSIX-only test skipped with a reason (see `posix_only` in `tests/test_provenance.py`).
|
|
85
|
+
|
|
86
|
+
## Changing the viewer
|
|
87
|
+
|
|
88
|
+
The viewer (`memdebug serve`) only looks. It answers GET and HEAD only, sends no JavaScript, and cannot change a store or the ledger (the
|
|
89
|
+
theme switch sets one cookie and nothing else). Pages are built with the escaping builder in `src/memdebug/viewer/html.py`, never by joining
|
|
90
|
+
strings. The only form is the Compare picker, which uses `method="get"` and just navigates; a test fails if any form uses another method.
|
|
91
|
+
Where a person may want to act, show the command as text to paste, as the "put it back" guidance does, and build it only from names that pass
|
|
92
|
+
`valid_name`. A new page needs a route anchored with `\Z`, an entry in the trailing-line-break test, and an entry in `ALL_PAGES` in
|
|
93
|
+
`tests/test_viewer.py`. What is defended, and what is not: [docs/threat-model.md](docs/threat-model.md#the-viewer).
|
|
94
|
+
|
|
95
|
+
## Adding a backend
|
|
96
|
+
|
|
97
|
+
Implement the `MemoryAdapter` protocol in `adapters/base.py` (`list_memories`, `read_history`, `history`), read-only,
|
|
98
|
+
validating every row. Look at `adapters/mem0.py` and `adapters/markdown_git.py` for the expected hardening, and add the
|
|
99
|
+
same kinds of tests (hostile rows, oversized values, odd encodings). A backend without a change history should record
|
|
100
|
+
what it observes instead of raising "outside the history" alarms.
|
|
101
|
+
|
|
102
|
+
## Adding an agent to the catalog
|
|
103
|
+
|
|
104
|
+
See `docs/agents.md`: a documented location (with a link), a test using a fake home folder, and watch only the named files when the
|
|
105
|
+
folder also holds credentials.
|
|
106
|
+
|
|
107
|
+
## Workflows and Dependabot
|
|
108
|
+
|
|
109
|
+
Dependabot proposes updates to the pinned actions in one grouped pull request a week. Let CI run on it, and for changes to the release
|
|
110
|
+
workflow also run its dry run (docs/releasing.md) from the pull request's branch before merging.
|
|
111
|
+
|
|
112
|
+
## Releases
|
|
113
|
+
|
|
114
|
+
See `docs/releasing.md`. A test keeps the version, the changelog, the licence files and the links in the documents in agreement.
|
|
115
|
+
|
|
116
|
+
## Pull requests
|
|
117
|
+
|
|
118
|
+
Keep them focused (one topic), include tests, update the docs that your change makes untrue, and run the whole suite; the pull request
|
|
119
|
+
template lists what to check. Add a line under `[Unreleased]` in `CHANGELOG.md` for anything a user would notice, and write a new limit into
|
|
120
|
+
`docs/threat-model.md`. Do not bump the version or rename the changelog's heading: that is part of a release. The required checks must pass
|
|
121
|
+
before a pull request can merge, and only the maintainer creates release tags (the maintainer can bypass both rules, so they guard against
|
|
122
|
+
mistakes, not against the maintainer). Commits stay public for good, so use a GitHub no-reply address as your commit email if you would
|
|
123
|
+
rather not publish your own. By contributing you agree that your contribution is licensed under the Apache License 2.0 (see LICENSE).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: memdebug
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Inspect, compare and roll back what an AI agent's memory holds. Local-first and agent-neutral.
|
|
5
5
|
Project-URL: Homepage, https://github.com/juraj-jumic/memdebug
|
|
6
6
|
Project-URL: Source, https://github.com/juraj-jumic/memdebug
|
|
@@ -33,21 +33,63 @@ Description-Content-Type: text/markdown
|
|
|
33
33
|
|
|
34
34
|
# memdebug
|
|
35
35
|
|
|
36
|
+
[](https://github.com/juraj-jumic/memdebug/actions/workflows/ci.yml)
|
|
37
|
+
[](https://pypi.org/project/memdebug/)
|
|
38
|
+
[](https://pypi.org/project/memdebug/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
36
41
|
**See what your AI agent's memory holds, what changed, and put it back.** A local, agent-neutral tool for people who run
|
|
37
42
|
agents whose memory they can reach: notes in a folder or git repository, Open WebUI's memory, or a self-hosted Mem0.
|
|
38
43
|
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
That is `memdebug demo` as released in 0.3.0, paced for reading (the real run takes about a second on Linux and a few seconds on Windows).
|
|
47
|
+
The unpaced recording is [`docs/demo.cast`](docs/demo.cast): `asciinema play docs/demo.cast`.
|
|
48
|
+
|
|
39
49
|
An agent's memory is built from text it read at runtime, and text can be planted: an email, a web page, a document.
|
|
40
50
|
Nobody reviews all of it. memdebug records what the memory holds in a tamper-evident ledger, shows what changed and when,
|
|
41
51
|
catches edits that bypassed the store's own history, compares snapshots, flags wording worth a second look, and rolls markdown memory
|
|
42
52
|
back safely.
|
|
43
53
|
|
|
44
54
|
It is an **observer**: it never sits between the agent and its memory, never talks to the agent, and runs entirely on your
|
|
45
|
-
computer. It does not block attacks as they happen (run it next to runtime guards)
|
|
46
|
-
|
|
55
|
+
computer. It does not block attacks as they happen (run it next to runtime guards). For Claude Code it can point at the logged session
|
|
56
|
+
behind a flagged change, as evidence and never as proof; for any other agent it cannot say *which conversation* wrote a memory. See
|
|
57
|
+
[docs/threat-model.md](docs/threat-model.md) for exactly what it does and does not do.
|
|
47
58
|
|
|
48
|
-
> **Status: alpha (0.
|
|
59
|
+
> **Status: alpha (0.4).** The parts described here work. The tests run on every push on Linux, Windows and macOS (Python 3.10, 3.12 and 3.14), and
|
|
49
60
|
> the author also runs them on Windows 11, but expect rough edges. [ROADMAP.md](ROADMAP.md) lists what is built and what is next.
|
|
50
61
|
|
|
62
|
+
## Why this matters
|
|
63
|
+
|
|
64
|
+
An agent's memory is text that is read back into the model at the start of every session. That makes it an unusual kind of state: it
|
|
65
|
+
persists, nobody reviews it, and anything the agent reads (an email, a web page, a document) can try to write itself into it. A poisoned
|
|
66
|
+
note does not fail loudly. It quietly changes behaviour later, in sessions that look unrelated. memdebug treats memory like any other
|
|
67
|
+
critical state: something to inspect, compare, prove and recover. The design problems it takes on:
|
|
68
|
+
|
|
69
|
+
* **Non-deterministic behaviour, deterministic state.** You cannot replay a language model, but you can pin down what it was *given*. A
|
|
70
|
+
snapshot is the memory's text at a moment, as memdebug read it (line endings normalised, and a very long text cut with a hash of the whole),
|
|
71
|
+
a diff is exactly what changed between two snapshots, and for git notes and plain folders a rollback puts a known-good state back. "What was
|
|
72
|
+
the agent working from when it did that?" becomes a question you can answer from evidence, provided memdebug was watching at the time.
|
|
73
|
+
* **Views of memory that can disagree.** For markdown in git and for Mem0 there are three: the store's own history, what is actually there
|
|
74
|
+
now, and memdebug's own record. They are reconciled, and the case that matters most is flagged: a change that **bypassed the store's own
|
|
75
|
+
history**. A plain folder and Open WebUI keep no history, so there memdebug records what changed between two looks but cannot call
|
|
76
|
+
anything "outside the history".
|
|
77
|
+
* **A record that shows tampering.** Entries are only ever appended, and each is hash-chained to the one before, so editing or removing an
|
|
78
|
+
old entry is detectable (`memdebug verify`). Someone who can rewrite the whole ledger file can rebuild a valid chain, and cutting off the
|
|
79
|
+
newest entries is only detectable against a copy of the head hash kept somewhere else (`memdebug witness`). The threat model says so.
|
|
80
|
+
* **Safe recovery, not just detection.** A rollback is a plan you review first, and the plan writes nothing. What it would replace is saved
|
|
81
|
+
before anything is touched: for a plain folder every replaced file is copied byte for byte into a private backup folder next to the ledger;
|
|
82
|
+
for git notes, anything git does not already hold is saved under `refs/memdebug/backups/`. The written files are checked against the plan,
|
|
83
|
+
a failure while writing puts the files back, and the rollback is recorded in the ledger and can itself be undone.
|
|
84
|
+
* **Hostile input by design.** Memory text is untrusted: its size is bounded, it is never executed, and everything shown is escaped (terminal
|
|
85
|
+
output through `safe_text`, the viewer through an escaping HTML builder with no JavaScript). git runs with a scrubbed environment and with
|
|
86
|
+
hooks, filters, pagers, external diff helpers and file-system monitors switched off. `memdebug selftest` checks the platform-dependent
|
|
87
|
+
protections on your own machine instead of asking you to take them on trust, and [docs/threat-model.md](docs/threat-model.md) lists what is
|
|
88
|
+
protected and what is not.
|
|
89
|
+
|
|
90
|
+
What it deliberately is not: it does not hook into your agent, intercept prompts or optimise tokens. It reads the stores your agent already
|
|
91
|
+
writes, from outside (and, for Claude Code, its session logs, read-only, only to say which session logged an edit to a flagged note).
|
|
92
|
+
|
|
51
93
|
## Try it in a minute
|
|
52
94
|
|
|
53
95
|
You need Python 3.10 or newer and git 2.31 or newer. Install memdebug from PyPI into its own virtual environment, which works the same way
|
|
@@ -97,7 +139,7 @@ Or register stores yourself (this is what a script would do):
|
|
|
97
139
|
| openwebui | the `memory` table of Open WebUI's `webui.db`, copied from its Docker container (or from a copy you made) | none | not applicable | no |
|
|
98
140
|
| mem0 | self-hosted Mem0 | Mem0's `history.db` | a change made directly in storage | no |
|
|
99
141
|
|
|
100
|
-
Which agents does it know? Claude Code, OpenClaw, Gemini CLI, Codex CLI, Windsurf and Open WebUI (in Docker): see [docs/agents.md](docs/agents.md)
|
|
142
|
+
Which agents does it know? Claude Code, OpenClaw, Gemini CLI, Codex CLI, Windsurf, Cline and Open WebUI (in Docker): see [docs/agents.md](docs/agents.md)
|
|
101
143
|
for where each keeps its memory and where that was verified. Where memory sits in one file beside credentials (`~/.gemini`, `~/.codex`,
|
|
102
144
|
`~/.claude`), memdebug watches only that file. If Open WebUI runs in Docker, `memdebug setup` finds it and takes a read-only copy of its database before every look; there is nothing to
|
|
103
145
|
set up by hand (`windows-tools/copy-webui-db.ps1` is only a fallback for other setups). A store with no history can still be watched, but memdebug cannot tell an
|
|
@@ -139,8 +181,8 @@ user service or a login item. memdebug does not install anything that starts by
|
|
|
139
181
|
|
|
140
182
|
## The viewer
|
|
141
183
|
|
|
142
|
-
`memdebug serve` starts a read-only web page (timeline with an inspector, snapshots, compare,
|
|
143
|
-
integrity) on **127.0.0.1 only**, and prints a link that contains a random secret. Open that link;
|
|
184
|
+
`memdebug serve` starts a read-only web page (timeline with an inspector, snapshots, compare, the agents it
|
|
185
|
+
found, integrity) on **127.0.0.1 only**, and prints a link that contains a random secret. Open that link;
|
|
144
186
|
the secret moves into a cookie and disappears from the address bar. It uses only Python's standard
|
|
145
187
|
library and sends no JavaScript at all. Run `memdebug verify` once first if the ledger is from an
|
|
146
188
|
older version (the viewer itself never upgrades or creates anything).
|
|
@@ -165,11 +207,54 @@ it cannot prove (for example symlinks need Developer Mode).
|
|
|
165
207
|
- git is found through PATH entries that are absolute paths only; the current folder is never searched.
|
|
166
208
|
- Names that mean something special on Windows (`NUL.md`, `con.md`, `file:stream.md`, trailing dots or spaces, `GIT~1`, `.GIT`)
|
|
167
209
|
are rejected on every platform, so the history and the files always agree.
|
|
210
|
+
- Paths longer than 259 characters work without changing any Windows setting: memdebug hands Windows the extended-length form of every note, backup and
|
|
211
|
+
log path. A git repository in a very deeply nested folder can still hit git's own limits.
|
|
168
212
|
- Directory junctions and symlinks are never followed, including by rollback. A hung git is stopped with `taskkill /T`.
|
|
169
213
|
- Ledger file permissions are not enforced by this tool on Windows; the default location is private to your user account.
|
|
170
214
|
- If git reports "dubious ownership" for a repository on another drive, fix the ownership; this tool deliberately ignores your
|
|
171
215
|
global `safe.directory` setting.
|
|
172
216
|
|
|
217
|
+
## How it works
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
your agent (Claude Code, Open WebUI, Mem0, ...)
|
|
221
|
+
runs as usual: memdebug never hooks into it
|
|
222
|
+
| writes its memory
|
|
223
|
+
v
|
|
224
|
+
+--------------------------- memory store ---------------------------+
|
|
225
|
+
| markdown notes in git | plain folder | Open WebUI | Mem0 |
|
|
226
|
+
+------------------------------------+------------------------------+
|
|
227
|
+
| read-only readers: nothing the store holds is executed
|
|
228
|
+
v
|
|
229
|
+
+-------------------+ +---------------------------------+ +--------------------------+
|
|
230
|
+
| sync |-->| events: added, changed, deleted,|-->| hints: "worth a second |
|
|
231
|
+
| the store's | | OUTSIDE the store's history | | look" (heuristics) |
|
|
232
|
+
| history vs what | +----------------+----------------+ +--------------------------+
|
|
233
|
+
| is there now vs | | append
|
|
234
|
+
| the ledger | v
|
|
235
|
+
| (check, watch and | +-------------------------------------+
|
|
236
|
+
| snapshot run it) | | ledger: hash-chained, tamper-evident | snapshots = the text
|
|
237
|
+
+-------------------+ | + snapshots | of the memory at a moment
|
|
238
|
+
+-----+----------------+---------+-----+
|
|
239
|
+
| | |
|
|
240
|
+
v v v
|
|
241
|
+
check / watch / serve (read-only diff s1 s2
|
|
242
|
+
timeline / verify browser viewer) compare two moments
|
|
243
|
+
in the terminal
|
|
244
|
+
|
|
|
245
|
+
| you decide to go back
|
|
246
|
+
v
|
|
247
|
+
rollback: plan (writes nothing) -> you confirm -> save what is replaced
|
|
248
|
+
-> write -> check the written files against the plan -> record in the ledger
|
|
249
|
+
a failure while writing puts the files back (if only the final ledger entry fails,
|
|
250
|
+
the files stay restored and you are told)
|
|
251
|
+
the only code that writes to a memory store: git notes and plain folders, never databases
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
memdebug's own files (the ledger, the list of watched stores, folder-rollback backups, copies of Open WebUI's database) live in its own data
|
|
255
|
+
folder, or where you point `--db`, `--out` or `--file`; memdebug does not write them inside a store. For Claude Code, `check` and `watch` also read
|
|
256
|
+
its session logs (read-only, ids and times only) to say which session logged an edit to a flagged note: evidence, not proof.
|
|
257
|
+
|
|
173
258
|
## How it is built
|
|
174
259
|
|
|
175
260
|
The core (model, ledger, sync, reconcile) imports no backend. Each store type is a read-only adapter: `markdown-git` and `folder`
|
|
@@ -185,6 +270,20 @@ installed. Adding a store type is described in [CONTRIBUTING.md](CONTRIBUTING.md
|
|
|
185
270
|
|
|
186
271
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for the ground rules (everything read from a store is untrusted; adapters only read).
|
|
187
272
|
|
|
273
|
+
## Engineering
|
|
274
|
+
|
|
275
|
+
* **Tests:** 1,500+ `pytest` tests, run on every push to `main` and every pull request on Linux, Windows and macOS with Python 3.10, 3.12 and
|
|
276
|
+
3.14, including platform-specific cases (real NTFS junctions made with `mklink /J`, case-insensitive file names, CRLF). CI also runs
|
|
277
|
+
`memdebug selftest` and `memdebug demo` on each of those, and builds the package and installs it into a clean environment.
|
|
278
|
+
* **Protections are proven, not assumed:** the project's rule is that a protection only counts once removing it makes a test fail. See
|
|
279
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
280
|
+
* **A self-test on your machine:** `memdebug selftest` demonstrates the platform-dependent safety claims on the computer you actually use,
|
|
281
|
+
and says SKIP, never PASS, for anything it could not prove there.
|
|
282
|
+
* **Releases:** PyPI trusted publishing (no stored token), a manual approval before anything is published, provenance attestations (PyPI
|
|
283
|
+
holds them for 0.3.0 and 0.4.0), and branch and tag rules on `main` and on version tags.
|
|
284
|
+
* **Written down:** a [threat model](docs/threat-model.md), a [security policy](SECURITY.md), a [changelog](CHANGELOG.md) and a
|
|
285
|
+
[roadmap](ROADMAP.md) that says what is not built yet.
|
|
286
|
+
|
|
188
287
|
## Rolling back a watched store (folders and git notes)
|
|
189
288
|
|
|
190
289
|
`memdebug rollback store NAME --to s1` does the same job by the name you see in `memdebug stores`, for a plain folder of notes as well as for
|