From 39ff202fa82af21ab740dd9a731dba1bb1b0847a Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Fri, 3 Jul 2026 16:01:07 +0300 Subject: [PATCH 01/23] chore: add PR template, CODEOWNERS, auto-label, auto-merge dependabot, stale bot, CONTRIBUTING.md, release config --- .github/CODEOWNERS | 11 +++++++ .github/PULL_REQUEST_TEMPLATE.md | 19 +++++++++++ .github/labeler.yml | 25 ++++++++++++++ .github/release.yml | 22 +++++++++++++ .github/workflows/auto-label.yml | 17 ++++++++++ .github/workflows/auto-merge-dependabot.yml | 25 ++++++++++++++ .github/workflows/stale.yml | 24 ++++++++++++++ CONTRIBUTING.md | 36 +++++++++++++++++++++ 8 files changed, 179 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/labeler.yml create mode 100644 .github/release.yml create mode 100644 .github/workflows/auto-label.yml create mode 100644 .github/workflows/auto-merge-dependabot.yml create mode 100644 .github/workflows/stale.yml create mode 100644 CONTRIBUTING.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..e3489044 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,11 @@ +# Default owner for everything in the repo +* @Cipher208 + +# Python source +*.py @Cipher208 + +# MCP server +mcp_server/ @Cipher208 + +# CI/CD +.github/ @Cipher208 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..52ed06a2 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ +## Summary + + + +## Changes + + + +## Checklist + +- [ ] Tests pass locally (`pytest tests/ -v`) +- [ ] Lint passes (`ruff check .`) +- [ ] New code has tests +- [ ] Documentation updated (if applicable) +- [ ] No breaking changes (or documented in summary) + +## Related Issues + + diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000..43fe6af8 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,25 @@ +labels: + - label: "bug" + paths: + - "**/*.py" + title: + - regex: "\\bfix(es|ed)?\\b" + - regex: "\\bbug\\b" + + - label: "dependencies" + paths: + - "pyproject.toml" + - "requirements*.txt" + + - label: "ci" + paths: + - ".github/**" + + - label: "documentation" + paths: + - "**/*.md" + - "docs/**" + + - label: "tests" + paths: + - "tests/**" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..ba819ee0 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,22 @@ +changelog: + exclude: + labels: + - ignore-for-release + authors: + - dependabot[bot] + categories: + - title: "Breaking Changes" + labels: + - breaking-change + - title: "New Features" + labels: + - enhancement + - title: "Bug Fixes" + labels: + - bug + - title: "Dependencies" + labels: + - dependencies + - title: "Other Changes" + labels: + - "*" diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml new file mode 100644 index 00000000..367e4ff1 --- /dev/null +++ b/.github/workflows/auto-label.yml @@ -0,0 +1,17 @@ +name: Auto Label + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml new file mode 100644 index 00000000..4ff5b077 --- /dev/null +++ b/.github/workflows/auto-merge-dependabot.yml @@ -0,0 +1,25 @@ +name: Auto Merge Dependabot + +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + if: github.actor == 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - name: Fetch Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Auto-merge minor and patch updates + if: steps.metadata.outputs.update-type != 'version-update:semver-major' + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..7cca2059 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,24 @@ +name: Stale Issues and PRs + +on: + schedule: + - cron: "0 0 * * *" + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: "This issue has been automatically marked as stale because it has not had activity in 30 days. It will be closed in 7 days if no further activity occurs." + stale-pr-message: "This PR has been automatically marked as stale because it has not had activity in 30 days. It will be closed in 7 days if no further activity occurs." + days-before-stale: 30 + days-before-close: 7 + stale-issue-label: "stale" + stale-pr-label: "stale" + exempt-issue-labels: "pinned,security,bug" + exempt-pr-labels: "pinned,security" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..21dba227 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,36 @@ +# Contributing to mcp-ariel-memory + +Thanks for your interest in contributing! + +## Development Setup + +```bash +git clone https://github.com/Cipher208/mcp-ariel-memory.git +cd mcp-ariel-memory +pip install -e ".[dev,binary]" +``` + +## Running Tests + +```bash +pytest tests/ -v --timeout=30 +``` + +## Code Style + +- Lint: `ruff check .` +- Format: `ruff format .` +- Max line length: 150 +- Target: Python 3.10+ + +## Pull Requests + +1. Fork the repo and create a branch from `master` +2. Make your changes +3. Add tests for new functionality +4. Ensure all tests pass +5. Submit a PR using the PR template + +## Reporting Issues + +Use the issue templates for bug reports and feature requests. From 96e61d7e058c6249c880f58b900aed5af1df88b7 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Fri, 3 Jul 2026 16:36:11 +0300 Subject: [PATCH 02/23] chore: add mypy typecheck + detect-secrets to CI - mypy config in pyproject.toml (check_untyped_defs, explicit_package_bases) - CI typecheck job on Python 3.12 - Fix backup_cron.py type annotation (Coroutine vs dict) - detect-secrets workflow scans for leaked credentials - Branch protection will be updated to include typecheck --- .github/workflows/ci.yml | 10 +++++++++ .github/workflows/detect-secrets.yml | 33 ++++++++++++++++++++++++++++ features/backup_cron.py | 5 ++--- pyproject.toml | 15 +++++++++++++ 4 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/detect-secrets.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a99bfba..ef521ae0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,16 @@ jobs: - run: ruff check . - run: ruff format --check . + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install -e ".[dev,binary]" + - run: mypy --config-file pyproject.toml features/ shared/ mcp_server/ rag/ hooks/ wiki/ lifecycle/ graph/ core/ + quality: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/detect-secrets.yml b/.github/workflows/detect-secrets.yml new file mode 100644 index 00000000..c9d899b1 --- /dev/null +++ b/.github/workflows/detect-secrets.yml @@ -0,0 +1,33 @@ +name: Detect Secrets + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +permissions: + contents: read + +jobs: + detect-secrets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install detect-secrets + - run: detect-secrets scan --all-files --exclude-files '\.env$' --exclude-files 'CHANGELOG\.md$' --exclude-files 'config\.yaml$' --exclude-files '\.txt$' + - name: Check for new secrets + run: | + RESULT=$(detect-secrets scan --all-files --exclude-files '\.env$' --exclude-files 'CHANGELOG\.md$' --exclude-files 'config\.yaml$' --exclude-files '\.txt$' --output -) + NEW_SECRETS=$(echo "$RESULT" | python -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('results',{})))") + if [ "$NEW_SECRETS" -gt 0 ]; then + echo "Found $NEW_SECRETS potential secrets!" + detect-secrets scan --all-files --exclude-files '\.env$' --exclude-files 'CHANGELOG\.md$' --exclude-files 'config\.yaml$' --exclude-files '\.txt$' + exit 1 + fi + echo "No secrets found." diff --git a/features/backup_cron.py b/features/backup_cron.py index 367f8dbe..4647bc84 100644 --- a/features/backup_cron.py +++ b/features/backup_cron.py @@ -135,9 +135,8 @@ def _sync_wiki(self): for layer in ["user", "agent"]: fw = FileWiki(layer=layer) - result = fw.reindex_all() - if asyncio.iscoroutine(result): - result = asyncio.run(result) + raw = fw.reindex_all() + result: dict[str, Any] = asyncio.run(raw) if asyncio.iscoroutine(raw) else raw if isinstance(result, dict) and result.get("indexed", 0) > 0: logger.info("Wiki %s synced: %d files" % (layer, result["indexed"])) self._last_wiki_sync = time.time() diff --git a/pyproject.toml b/pyproject.toml index ff5be453..46a63ff2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dev = [ "pytest-asyncio>=0.21", "pytest-timeout>=2.0", "ruff>=0.1", + "mypy>=1.0", ] win = [ "aiosqlite>=0.21.0,!=0.22.0", @@ -96,3 +97,17 @@ exclude = [".repowise", ".codegraph", "docs", "__pycache__"] max_complexity = 15 max_lines = 100 max_args = 6 + +[tool.mypy] +python_version = "3.12" +warn_return_any = false +warn_unused_configs = true +disallow_untyped_defs = false +check_untyped_defs = true +ignore_missing_imports = true +explicit_package_bases = true +exclude = ["tests"] + +[[tool.mypy.overrides]] +module = ["tests.*"] +ignore_errors = true From 379015004035d2d1de9cf174f89a3b4cec1b4085 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Fri, 3 Jul 2026 16:53:47 +0300 Subject: [PATCH 03/23] fix: detect-secrets --output flag not supported, use stdout redirect --- .github/workflows/detect-secrets.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/detect-secrets.yml b/.github/workflows/detect-secrets.yml index 8639d681..2a108d0b 100644 --- a/.github/workflows/detect-secrets.yml +++ b/.github/workflows/detect-secrets.yml @@ -30,7 +30,7 @@ jobs: --exclude-files '\.mypy_cache' \ --exclude-files '\.pytest_cache' \ --exclude-files '__pycache__' \ - --output .secrets.baseline + > .secrets.baseline - name: Check results run: | RESULTS=$(python -c "import json; d=json.load(open('.secrets.baseline')); print(len(d.get('results',{})))") From 0ba98b47f12caa6082366358ce9288fdb987de22 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Fri, 3 Jul 2026 22:59:46 +0300 Subject: [PATCH 04/23] chore: add Hypothesis property-based tests 19 property-based tests covering: - rag/conflict.py: similarity range, symmetry, empty input - rag/scoring.py: weighted sum invariant, ordering, weights clamping - rag/quantize.py: binary output length, hamming distance properties - features/secrets.py: encrypt/decrypt roundtrip, min blob size --- pyproject.toml | 1 + tests/test_hypothesis.py | 224 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 tests/test_hypothesis.py diff --git a/pyproject.toml b/pyproject.toml index 46a63ff2..9485152e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ dev = [ "pytest-timeout>=2.0", "ruff>=0.1", "mypy>=1.0", + "hypothesis>=6.0", ] win = [ "aiosqlite>=0.21.0,!=0.22.0", diff --git a/tests/test_hypothesis.py b/tests/test_hypothesis.py new file mode 100644 index 00000000..c867f33a --- /dev/null +++ b/tests/test_hypothesis.py @@ -0,0 +1,224 @@ +"""Property-based tests using Hypothesis. + +Tests mathematical invariants and roundtrip properties that must hold +for ALL valid inputs, not just hand-picked examples. +""" + +import json +import math + +import pytest +from hypothesis import given, settings, assume, HealthCheck +from hypothesis import strategies as st + +# ── Fixed-dimension strategies (avoid assume() filtering) ── + +DIM = 32 # small dimension for fast tests +st_dim_vec = st.lists(st.floats(min_value=-10.0, max_value=10.0, allow_nan=False, allow_infinity=False), min_size=DIM, max_size=DIM) +st_short_text = st.text(min_size=1, max_size=200, alphabet=st.characters(blacklist_categories=("Cs",))) + + + + +# ═══════════════════════════════════════════════════════════════ +# rag/conflict.py — similarity function invariants +# ═══════════════════════════════════════════════════════════════ + +from rag.conflict import bm25_pair_similarity, char_ngram_jaccard, smart_similarity + + +class TestSimilarityProperties: + + @given(a=st_short_text, b=st_short_text) + @settings(max_examples=200) + def test_similarity_range(self, a, b): + for fn in (bm25_pair_similarity, char_ngram_jaccard, smart_similarity): + score = fn(a, b) + assert 0.0 <= score <= 1.0, f"{fn.__name__} returned {score}" + + @given(a=st_short_text) + @settings(max_examples=100) + def test_self_similarity_non_negative(self, a): + assume(len(a) >= 3) + assert bm25_pair_similarity(a, a) >= 0.0 + assert char_ngram_jaccard(a, a) >= 0.0 + assert smart_similarity(a, a) >= 0.0 + + @given(a=st_short_text, b=st_short_text) + @settings(max_examples=100) + def test_symmetry(self, a, b): + assert abs(bm25_pair_similarity(a, b) - bm25_pair_similarity(b, a)) < 1e-10 + assert abs(char_ngram_jaccard(a, b) - char_ngram_jaccard(b, a)) < 1e-10 + + @given(a=st.text(min_size=0, max_size=2), b=st_short_text) + @settings(max_examples=50) + def test_empty_short_text_returns_zero(self, a, b): + assert smart_similarity(a, b) == 0.0 + assert smart_similarity(b, a) == 0.0 + + +# ═══════════════════════════════════════════════════════════════ +# rag/scoring.py — scoring invariants +# ═══════════════════════════════════════════════════════════════ + +from rag.scoring import CorpusStats, ScoredCandidate, Scorer, ScoringWeights + + +class TestScoringProperties: + + @given( + rrf_score=st.floats(min_value=0.0, max_value=1.0), + weight_rel=st.floats(min_value=0.0, max_value=2.0), + weight_nov=st.floats(min_value=0.0, max_value=2.0), + weight_tb=st.floats(min_value=0.0, max_value=2.0), + ) + @settings(max_examples=200) + def test_final_score_is_weighted_sum(self, rrf_score, weight_rel, weight_nov, weight_tb): + scorer = Scorer( + mode="rrf", + weights=ScoringWeights(relevance=weight_rel, novelty=weight_nov, type_boost=weight_tb), + ) + c = ScoredCandidate(id=1, page_id=1, title="t", content="c", wiki_type=None, rrf_score=rrf_score) + result = scorer.rank_sync("q", [c], "user") + r = result[0] + expected = weight_rel * r.debug["relevance"] + weight_nov * r.debug["novelty"] + weight_tb * r.debug["type_boost"] + assert abs(r.final_score - expected) < 1e-6 + + @given(n=st.integers(min_value=1, max_value=30)) + @settings(max_examples=30) + def test_ranking_ordering(self, n): + scorer = Scorer(weights=ScoringWeights(relevance=1.0)) + candidates = [ + ScoredCandidate(id=i, page_id=i, title=f"t{i}", content=f"c{i}", wiki_type=None, rrf_score=float(i) / n) + for i in range(n) + ] + result = scorer.rank_sync("q", candidates, "u") + scores = [c.final_score for c in result] + assert scores == sorted(scores, reverse=True) + + @given(total=st.integers(min_value=0, max_value=200)) + @settings(max_examples=30) + def test_corpus_stats_prior_range(self, total): + stats = CorpusStats( + total_retrievals=total, + doc_retrieval_counts={i: i for i in range(min(total, 50))}, + ) + for doc_id in range(min(total, 50)): + p = stats.prior(doc_id) + assert 0.0 <= p <= 1.0 + + @given( + relevance=st.floats(min_value=-1.0, max_value=3.0), + novelty=st.floats(min_value=-1.0, max_value=3.0), + type_boost=st.floats(min_value=-1.0, max_value=3.0), + ) + @settings(max_examples=100) + def test_update_weights_clamped(self, relevance, novelty, type_boost): + scorer = Scorer() + scorer.update_weights({"relevance": relevance, "novelty": novelty, "type_boost": type_boost}) + assert 0.0 <= scorer.weights.relevance <= 2.0 + assert 0.0 <= scorer.weights.novelty <= 2.0 + assert 0.0 <= scorer.weights.type_boost <= 2.0 + + +# ═══════════════════════════════════════════════════════════════ +# rag/quantize.py — binary encoding invariants +# ═══════════════════════════════════════════════════════════════ + +try: + import numpy as np + from rag.quantize import embed_to_binary, hamming_distance, hamming_to_score, _packed_bytes + HAS_NUMPY = True +except ImportError: + HAS_NUMPY = False + +pytestmark = pytest.mark.skipif(not HAS_NUMPY, reason="numpy not installed") + + +class TestQuantizeProperties: + + @given(emb=st_dim_vec) + @settings(max_examples=100) + def test_output_length(self, emb): + result = embed_to_binary(emb, dim=DIM) + assert len(result) == _packed_bytes(DIM) + + @given(emb=st_dim_vec, threshold=st.floats(min_value=-5.0, max_value=5.0)) + @settings(max_examples=100) + def test_output_length_with_threshold(self, emb, threshold): + result = embed_to_binary(emb, threshold=threshold, dim=DIM) + assert len(result) == _packed_bytes(DIM) + + @given(emb=st.lists(st.floats(min_value=0.5, max_value=10.0, allow_nan=False, allow_infinity=False), min_size=DIM, max_size=DIM)) + @settings(max_examples=100) + def test_all_positive_embedding(self, emb): + result = embed_to_binary(emb, threshold=0.0, dim=DIM) + arr = np.frombuffer(result, dtype=np.uint8) + bits = np.unpackbits(arr, bitorder="big")[:DIM] + assert bits.mean() > 0.8 + + @given(emb=st_dim_vec) + @settings(max_examples=100) + def test_hamming_distance_self_zero(self, emb): + b = embed_to_binary(emb, dim=DIM) + assert hamming_distance(b, b) == 0 + + @given(a=st_dim_vec, b=st_dim_vec) + @settings(max_examples=100) + def test_hamming_distance_symmetric(self, a, b): + ba = embed_to_binary(a, dim=DIM) + bb = embed_to_binary(b, dim=DIM) + assert hamming_distance(ba, bb) == hamming_distance(bb, ba) + + @given(distance=st.integers(min_value=0, max_value=DIM)) + @settings(max_examples=50) + def test_hamming_to_score_range(self, distance): + score = hamming_to_score(distance, dim=DIM) + assert 0.0 <= score <= 1.0 + + @given(distance=st.integers(min_value=0, max_value=DIM - 1)) + @settings(max_examples=50) + def test_hamming_to_score_monotonic(self, distance): + assert hamming_to_score(distance, dim=DIM) > hamming_to_score(distance + 1, dim=DIM) + + +# ═══════════════════════════════════════════════════════════════ +# features/secrets.py — encrypt/decrypt roundtrip +# ═══════════════════════════════════════════════════════════════ + +from features.secrets import encrypt_json, decrypt_json + + +class TestSecretsProperties: + + @given(data=st.dictionaries(st.text(min_size=1, max_size=20), st.text(max_size=100), min_size=1, max_size=5)) + @settings(deadline=None, max_examples=30) + def test_encrypt_decrypt_roundtrip_dict(self, data): + blob = encrypt_json(data) + result = decrypt_json(blob) + assert result == data + + @given(data=st.lists(st.text(min_size=1, max_size=50), min_size=1, max_size=5)) + @settings(deadline=None, max_examples=30) + def test_encrypt_decrypt_roundtrip_list(self, data): + blob = encrypt_json(data) + result = decrypt_json(blob) + assert result == data + + @given( + a=st.dictionaries(st.text(min_size=1, max_size=10), st.text(max_size=50), min_size=1), + b=st.dictionaries(st.text(min_size=1, max_size=10), st.text(max_size=50), min_size=1), + ) + @settings(deadline=None, max_examples=30) + def test_different_inputs_different_ciphertext(self, a, b): + assume(a != b) + blob_a = encrypt_json(a) + blob_b = encrypt_json(b) + assert blob_a != blob_b + + @given(data=st.dictionaries(st.text(min_size=1, max_size=10), st.integers(), min_size=1)) + @settings(deadline=None, max_examples=20) + def test_min_blob_size(self, data): + """Encrypted blob must be at least nonce(24) + MAC(16) = 40 bytes.""" + blob = encrypt_json(data) + assert len(blob) >= 40 From d0c648804ef7e2cd2dc6dca4cc23714ca18b0ac0 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Fri, 3 Jul 2026 23:26:46 +0300 Subject: [PATCH 05/23] chore: delete requirements.txt, add ring buffer Hypothesis tests, update docs - Delete requirements.txt (duplicate of pyproject.toml deps) - Add 6 Hypothesis tests for ReflexBuffer (size invariant, FIFO, concurrency) - ROADMAP: add key rotation and adaptive importance threshold items - README: fix sync fallback description (asyncio.to_thread, not blocking) - README: fix keychain-first security description - 338/338 tests pass --- README.md | 4 +- ROADMAP.md | 2 + requirements.txt | 107 ------------------------------------ tests/test_hypothesis.py | 113 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 109 deletions(-) delete mode 100644 requirements.txt diff --git a/README.md b/README.md index 2b09e9f1..eee4b423 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The server is built with the official MCP Python SDK (FastMCP), supports both st - **19 unified MCP tools** with `layer` parameter (user/agent) instead of 37 separate tools - **Envelope encryption** — all sensitive data (API keys, tokens, saga state) encrypted at rest with libsodium secretbox -- **.env support** — set `MCP_MASTER_KEY` in `.env` file for easy local development +- **Keychain-first security** — master key resolved from OS keychain (keyring) first, `.env` only for local dev. Production: use keyring or vault sidecar - **Unified Search API** — single `search()` method with 4 strategies: `fts`, `mib`, `hybrid`, `auto` - **MultiSourceRAG** — unified search across RAG + Wiki with deduplication and reranking - **ITS-inspired scoring** — novelty component using document frequency as prior for better ranking @@ -30,7 +30,7 @@ The server is built with the official MCP Python SDK (FastMCP), supports both st - **Wiki system** with 14 content types, .md files as source of truth, and external folder sync - **24 hooks** for intercepting memory operations at every stage - **Saga pattern** for multi-step operations with compensation and watchdog -- **Platform-aware async** — aiosqlite on Linux/macOS, sync fallback on Windows +- **Platform-aware async** — aiosqlite on Linux/macOS, sync sqlite3 + `asyncio.to_thread()` on Windows (event loop never blocks) - **Python 3.10–3.13** tested in CI matrix ## Installation diff --git a/ROADMAP.md b/ROADMAP.md index 6fec8bb0..7d3fa05f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -61,6 +61,7 @@ - [ ] **Audit logging** — improve log format (JSON structured logging) - [ ] **Rate limiting** — add adaptive rate limiting based on load - [ ] **Input validation** — add validation at MCP tools level (Pydantic schemas) +- [ ] **Key rotation** — zero-downtime master key rotation with re-encryption of all stored secrets. Current state: keyring (OS keychain) supported as primary key source, .env as dev fallback. Rotation requires re-encrypting all blobs with new key while old key still works for reads. ## 8. Integrations @@ -108,6 +109,7 @@ - [x] **Importance v2** — 8-signal scorer (base, length, question, tech, emotional, novelty, retrieval, noise) - [x] **Importance scheduler** — background daemon for periodic re-scoring - [x] **Importance middleware** — uses ImportanceScorer instead of naive heuristic +- [ ] **Adaptive threshold** — replace fixed ImportanceGate threshold with EMA (exponential moving average) of recent message importance scores. Current 0.3 threshold is static; EMA would adapt to conversation patterns (high-signal technical discussions vs low-signal casual chat). ## 12. Saga & Reliability diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index ce09f1f8..00000000 --- a/requirements.txt +++ /dev/null @@ -1,107 +0,0 @@ --e git+https://github.com/Panniantong/Agent-Reach.git@22d7f03a59401b5740b380c3ad43e3ff7a9dc373#egg=agent_reach -aiohappyeyeballs==2.6.2 -aiohttp==3.14.1 -aiosignal==1.4.0 -aiosqlite==0.22.1 -annotated-doc==0.0.4 -annotated-types==0.7.0 -anyio==4.13.0 -ast-grep-cli==0.42.3 -attrs==26.1.0 -build==1.5.0 -certifi==2026.4.22 -cffi==2.0.0 -charset-normalizer==3.4.7 -click==8.4.1 -codebase-memory==0.1.2 -colorama==0.4.6 -cryptography==49.0.0 -distro==1.9.0 -execnet==2.1.2 -fastapi==0.139.0 -fastuuid==0.14.0 -feedparser==6.0.12 -filelock==3.29.4 -frozenlist==1.8.0 -fsspec==2026.6.0 -h11==0.16.0 -headroom-ai==0.26.0 -hf-xet==1.5.1 -httpcore==1.0.9 -httpx==0.28.1 -httpx-sse==0.4.3 -huggingface_hub==1.20.1 -idna==3.18 -importlib_metadata==8.9.0 -iniconfig==2.3.0 -Jinja2==3.1.6 -jiter==0.15.0 -jsonschema==4.26.0 -jsonschema-specifications==2025.9.1 -librefang==2026.3.2201 -litellm==1.89.2 -loguru==0.7.3 -markdown-it-py==4.2.0 -MarkupSafe==3.0.3 -mcp==1.28.0 --e git+https://github.com/Cipher208/mcp-ariel-memory.git@6c3df561b5f4951bb9b5435804f617f6587b9b23#egg=mcp_ariel_memory -mdurl==0.1.2 -multidict==6.7.1 -numpy==2.5.0 -openai==2.43.0 -opentelemetry-api==1.43.0 -packaging==26.2 -pluggy==1.6.0 -propcache==0.5.2 -psutil==7.2.2 -pycparser==3.0 -pydantic==2.13.4 -pydantic-settings==2.14.2 -pydantic_core==2.46.4 -Pygments==2.20.0 -PyJWT==2.13.0 -PyNaCl==1.6.2 -pyproject_hooks==1.2.0 -pytest==9.1.1 -pytest-asyncio==1.4.0 -pytest-xdist==3.8.0 -python-dotenv==1.2.2 -python-multipart==0.0.32 -pywin32==312 -PyYAML==6.0.3 -referencing==0.37.0 -regex==2026.5.9 -requests==2.33.1 -rich==15.0.0 -rpds-py==2026.5.1 -ruff==0.15.20 -sgmllib3k==1.0.0 -shellingham==1.5.4 -sniffio==1.3.1 -sse-starlette==3.4.5 -starlette==1.3.1 -tiktoken==0.13.0 -tokenizers==0.23.1 -toml==0.10.2 -tqdm==4.68.3 -tree-sitter==0.25.2 -tree-sitter-c==0.24.2 -tree-sitter-c-sharp==0.23.5 -tree-sitter-cpp==0.23.4 -tree-sitter-go==0.25.0 -tree-sitter-java==0.23.5 -tree-sitter-javascript==0.25.0 -tree-sitter-python==0.25.0 -tree-sitter-ruby==0.23.1 -tree-sitter-rust==0.24.2 -tree-sitter-typescript==0.23.2 -typer==0.25.1 -typing-inspection==0.4.2 -typing_extensions==4.15.0 -urllib3==2.7.0 -uvicorn==0.46.0 -watchdog==6.0.0 -win32_setctime==1.2.0 -yarl==1.24.2 -yt-dlp==2026.6.9 -zipp==4.1.0 diff --git a/tests/test_hypothesis.py b/tests/test_hypothesis.py index 49ec4277..17286b2c 100644 --- a/tests/test_hypothesis.py +++ b/tests/test_hypothesis.py @@ -4,6 +4,9 @@ for ALL valid inputs, not just hand-picked examples. """ +import threading +import time + import pytest from hypothesis import given, settings, assume from hypothesis import strategies as st @@ -211,3 +214,113 @@ def test_min_blob_size(self, data): """Encrypted blob must be at least nonce(24) + MAC(16) = 40 bytes.""" blob = encrypt_json(data) assert len(blob) >= 40 + + +# ═══════════════════════════════════════════════════════════════ +# core/reflex.py — ring buffer invariants +# ═══════════════════════════════════════════════════════════════ + +from core.reflex import ReflexBuffer + + +class TestReflexBufferProperties: + """Properties that ring buffer must satisfy under any sequence of operations.""" + + @given(n=st.integers(min_value=1, max_value=200)) + @settings(max_examples=100) + def test_size_never_exceeds_max(self, n): + """After adding n entries to buffer of size 10, size ≤ 10.""" + buf = ReflexBuffer(max_size=10) + for i in range(n): + buf.add(role="user", content=f"msg{i}", tokens=1) + assert buf.size() <= 10 + + @given(entries=st.lists(st.text(min_size=1, max_size=50, alphabet=st.characters(blacklist_categories=("Cs",))), min_size=1, max_size=100)) + @settings(max_examples=50) + def test_get_recent_returns_last_n(self, entries): + """get_recent(k) returns last min(k, size) entries in order.""" + buf = ReflexBuffer(max_size=50) + for e in entries: + buf.add(role="user", content=e, tokens=1) + recent = buf.get_recent(10) + full = buf.get_full() + expected = full[-10:] + assert [e.content for e in recent] == [e.content for e in expected] + + @given(n=st.integers(min_value=1, max_value=50)) + @settings(max_examples=50) + def test_fifo_eviction_order(self, n): + """Older entries are evicted first when buffer is full.""" + buf = ReflexBuffer(max_size=5) + for i in range(n): + buf.add(role="user", content=f"msg{i}", tokens=1) + full = buf.get_full() + contents = [e.content for e in full] + # All present entries should be in insertion order + assert contents == sorted(contents, key=lambda x: int(x.replace("msg", ""))) + + @given(n=st.integers(min_value=1, max_value=50)) + @settings(max_examples=30) + def test_clear_resets_size(self, n): + """After clear(), size is 0.""" + buf = ReflexBuffer(max_size=10) + for i in range(n): + buf.add(role="user", content=f"msg{i}", tokens=1) + buf.clear() + assert buf.size() == 0 + assert buf.get_full() == [] + + +class TestReflexBufferConcurrency: + """Ring buffer must handle concurrent add/get without crashes.""" + + def test_concurrent_add_no_crash(self): + """10 threads adding 100 entries each to buffer of size 50.""" + buf = ReflexBuffer(max_size=50) + errors = [] + + def adder(thread_id): + try: + for i in range(100): + buf.add(role="user", content=f"t{thread_id}_m{i}", tokens=1) + except Exception as e: + errors.append(e) + + threads = [threading.Thread(target=adder, args=(t,)) for t in range(10)] + for t in threads: + t.start() + for t in threads: + t.join(timeout=10) + + assert not errors, f"Concurrent add failed: {errors}" + assert buf.size() <= 50 + + def test_concurrent_read_write_no_crash(self): + """Reads and writes happening simultaneously.""" + buf = ReflexBuffer(max_size=30) + errors = [] + + def writer(): + try: + for i in range(200): + buf.add(role="user", content=f"msg{i}", tokens=1) + time.sleep(0.0001) + except Exception as e: + errors.append(e) + + def reader(): + try: + for _ in range(200): + buf.get_recent(5) + buf.get_full() + time.sleep(0.0001) + except Exception as e: + errors.append(e) + + threads = [threading.Thread(target=writer), threading.Thread(target=reader)] + for t in threads: + t.start() + for t in threads: + t.join(timeout=10) + + assert not errors, f"Concurrent read/write failed: {errors}" From fea7040426398e72d1fbae9c2cd3c339adda2bda Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Fri, 3 Jul 2026 23:37:18 +0300 Subject: [PATCH 06/23] chore: add MkDocs + Material Theme + MkDocstrings docs site - mkdocs.yml with Material theme, code highlighting, search - 30+ documentation pages organized by topic - API reference auto-generated from docstrings (mkdocstrings) - GitHub Pages deployment workflow (docs.yml) - mkdocs, mkdocs-material, mkdocstrings in docs optional deps - site/ added to .gitignore - Documentation URL: https://cipher208.github.io/mcp-ariel-memory/ --- .github/workflows/docs.yml | 41 +++++ .gitignore | 1 + docs/api/importance.md | 9 ++ docs/api/memory-types.md | 9 ++ docs/api/secrets.md | 11 ++ docs/architecture/connection.md | 44 ++++++ docs/architecture/layers.md | 76 +++++++++ docs/architecture/overview.md | 67 ++++++++ docs/changelog.md | 27 ++++ docs/contributing.md | 29 ++++ docs/core/episodic.md | 23 +++ docs/core/memory.md | 34 ++++ docs/core/reflex.md | 39 +++++ docs/core/session.md | 25 +++ docs/features/auth.md | 28 ++++ docs/features/backup.md | 28 ++++ docs/features/compression.md | 17 ++ docs/features/rate-limiting.md | 18 +++ docs/features/secrets.md | 42 +++++ docs/getting-started/configuration.md | 69 ++++++++ docs/getting-started/installation.md | 52 ++++++ docs/getting-started/quickstart.md | 42 +++++ docs/hooks/system.md | 44 ++++++ docs/index.md | 84 ++++++++++ docs/lifecycle/overview.md | 35 ++++ docs/operations/deployment.md | 42 +++++ docs/operations/monitoring.md | 24 +++ docs/operations/testing.md | 39 +++++ docs/rag/conflict.md | 33 ++++ docs/rag/engine.md | 43 +++++ docs/rag/quantize.md | 25 +++ docs/rag/router.md | 20 +++ docs/rag/scoring.md | 34 ++++ docs/tools/reference.md | 219 ++++++++++++++++++++++++++ docs/wiki/agent-wiki.md | 23 +++ docs/wiki/file-wiki.md | 43 +++++ docs/wiki/overview.md | 43 +++++ docs/wiki/user-wiki.md | 24 +++ mkdocs.yml | 105 ++++++++++++ pyproject.toml | 5 + 40 files changed, 1616 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/api/importance.md create mode 100644 docs/api/memory-types.md create mode 100644 docs/api/secrets.md create mode 100644 docs/architecture/connection.md create mode 100644 docs/architecture/layers.md create mode 100644 docs/architecture/overview.md create mode 100644 docs/changelog.md create mode 100644 docs/contributing.md create mode 100644 docs/core/episodic.md create mode 100644 docs/core/memory.md create mode 100644 docs/core/reflex.md create mode 100644 docs/core/session.md create mode 100644 docs/features/auth.md create mode 100644 docs/features/backup.md create mode 100644 docs/features/compression.md create mode 100644 docs/features/rate-limiting.md create mode 100644 docs/features/secrets.md create mode 100644 docs/getting-started/configuration.md create mode 100644 docs/getting-started/installation.md create mode 100644 docs/getting-started/quickstart.md create mode 100644 docs/hooks/system.md create mode 100644 docs/index.md create mode 100644 docs/lifecycle/overview.md create mode 100644 docs/operations/deployment.md create mode 100644 docs/operations/monitoring.md create mode 100644 docs/operations/testing.md create mode 100644 docs/rag/conflict.md create mode 100644 docs/rag/engine.md create mode 100644 docs/rag/quantize.md create mode 100644 docs/rag/router.md create mode 100644 docs/rag/scoring.md create mode 100644 docs/tools/reference.md create mode 100644 docs/wiki/agent-wiki.md create mode 100644 docs/wiki/file-wiki.md create mode 100644 docs/wiki/overview.md create mode 100644 docs/wiki/user-wiki.md create mode 100644 mkdocs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..e2b46662 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,41 @@ +name: Deploy Docs + +on: + push: + branches: [main, master] + paths: + - "docs/**" + - "mkdocs.yml" + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install -e ".[docs]" + - run: mkdocs build --strict + - uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 27da89ef..94bd0984 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ venv/ .ai-memory/ docs/compose/ .repowise/ +site/ diff --git a/docs/api/importance.md b/docs/api/importance.md new file mode 100644 index 00000000..59816e24 --- /dev/null +++ b/docs/api/importance.md @@ -0,0 +1,9 @@ +# Importance Scorer + +::: shared.importance + options: + show_source: true + members: + - ImportanceScorer + - ImportanceSignals + - score diff --git a/docs/api/memory-types.md b/docs/api/memory-types.md new file mode 100644 index 00000000..29498632 --- /dev/null +++ b/docs/api/memory-types.md @@ -0,0 +1,9 @@ +# Memory Types + +::: shared.memory_types + options: + show_source: true + members: + - MemoryKind + - get_policy + - kind_for_text diff --git a/docs/api/secrets.md b/docs/api/secrets.md new file mode 100644 index 00000000..a3234873 --- /dev/null +++ b/docs/api/secrets.md @@ -0,0 +1,11 @@ +# Secrets API + +::: features.secrets + options: + show_source: true + members: + - encrypt_json + - decrypt_json + - is_encrypted_blob + - _load_master_key + - _get_master_key diff --git a/docs/architecture/connection.md b/docs/architecture/connection.md new file mode 100644 index 00000000..bf34be94 --- /dev/null +++ b/docs/architecture/connection.md @@ -0,0 +1,44 @@ +# Connection Manager + +## Overview + +`AsyncConnectionManager` provides unified async SQLite access across platforms. + +```python +from shared.connection import connection_manager + +# Get a connection (creates if needed) +conn = await connection_manager.get("memory.db") + +# Execute queries +cur = await conn.execute("SELECT * FROM memory_entries WHERE user_id=?", ("u1",)) +rows = await cur.fetchall() + +# Commit +await conn.commit() +``` + +## Platform Behavior + +| Platform | Backend | Event Loop | +|----------|---------|------------| +| Linux/macOS | aiosqlite | True async | +| Windows | sqlite3 + to_thread | Offloaded to thread pool | + +## PRAGMA Settings + +```sql +PRAGMA journal_mode=WAL +PRAGMA busy_timeout=5000 +PRAGMA synchronous=NORMAL +PRAGMA foreign_keys=ON +PRAGMA cache_size=-64000 -- 64MB +PRAGMA temp_store=MEMORY +``` + +## Connection Lifecycle + +1. First `get()` creates connection with PRAGMAs +2. Subsequent `get()` returns cached connection +3. Stale connections (failed ping) are reopened +4. `close_all()` closes all connections on shutdown diff --git a/docs/architecture/layers.md b/docs/architecture/layers.md new file mode 100644 index 00000000..74982e8a --- /dev/null +++ b/docs/architecture/layers.md @@ -0,0 +1,76 @@ +# Memory Layers + +## L1: ReflexBuffer + +Ring buffer for recent messages. When full, oldest entries are evicted. + +```python +from core.reflex import ReflexBuffer + +buf = ReflexBuffer(max_size=50) +buf.add(role="user", content="Hello", tokens=5) +recent = buf.get_recent(10) # last 10 entries +``` + +**Properties** (verified by Hypothesis): + +- Size never exceeds `max_size` +- FIFO eviction (oldest first) +- Thread-safe (threading.Lock) +- Concurrent add/get without crashes + +## L2: EpisodicMemory + +Session-level summaries. Each session gets a compressed summary. + +```python +from core.episodic import EpisodicMemory + +ep = EpisodicMemory() +await ep.create_session(user_id="u1", summary="Discussed architecture") +sessions = await ep.get_sessions(user_id="u1", limit=10) +``` + +## L3: SessionStore + +Individual conversation entries with metadata. + +```python +from core.session import SessionStore + +ss = SessionStore() +await ss.add_entry(user_id="u1", role="user", content="Hello", tokens=5) +entries = await ss.get_entries(user_id="u1", limit=20) +``` + +## L4: CoreMemory + +Long-term key-value store for important facts. Typed memory with per-type retention. + +```python +from core.memory import CoreMemory + +cm = CoreMemory() +await cm.store(user_id="u1", key="preference", value="dark mode", kind="preference") +fact = await cm.retrieve(user_id="u1", key="preference") +``` + +## Typed Memory + +13 memory categories with different retention policies: + +| Kind | Decay | Archive | Example | +|------|-------|---------|---------| +| instruction | never | never | "Always use dark mode" | +| rule | never | never | "Never deploy on Fridays" | +| commitment | never | never | "I'll finish by Friday" | +| fact | exponential | old + low importance | "User likes Python" | +| preference | slow | rarely | "Dark mode preferred" | +| decision | moderate | moderate | "Chose PostgreSQL over MySQL" | +| goal | moderate | when expired | "Deploy v2.0 by Q3" | +| observation | fast | quickly | "Server load was high" | +| relationship | slow | rarely | "Alice works with Bob" | +| question | fast | quickly | "How does X work?" | +| hypothesis | moderate | moderate | "Maybe Y causes Z" | +| context | fast | quickly | "Working on auth module" | +| todo | moderate | when done | "Fix the login bug" | diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 00000000..66401ebe --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,67 @@ +# Architecture Overview + +## Two-Layer Model + +``` +┌─────────────────────────────────────────────┐ +│ MCP Client (LLM Agent) │ +├─────────────────────────────────────────────┤ +│ mcp_server (FastMCP) │ +│ ┌─────────────┐ ┌──────────────────────┐ │ +│ │ Tools Layer │ │ Hooks Pipeline │ │ +│ │ (19 tools) │ │ (24 hooks, gating) │ │ +│ └──────┬───────┘ └──────────┬───────────┘ │ +│ │ │ │ +│ ┌──────▼─────────────────────▼───────────┐ │ +│ │ Unified Memory Layer │ │ +│ │ L1: ReflexBuffer (ring, 50 entries) │ │ +│ │ L2: EpisodicMemory (sessions) │ │ +│ │ L3: SessionStore (entries) │ │ +│ │ L4: CoreMemory (key-value, 5000) │ │ +│ └────────────────────────────────────────┘ │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ +│ │ RAG │ │ Wiki │ │ Graphs │ │ +│ │ Engine │ │ (FTS5) │ │ (epistemic + │ │ +│ │ │ │ │ │ temporal) │ │ +│ └──────────┘ └──────────┘ └──────────────┘ │ +└─────────────────────────────────────────────┘ +``` + +## Memory Layers + +| Layer | Class | Purpose | Max Size | +|-------|-------|---------|----------| +| L1 | ReflexBuffer | Recent messages (ring buffer) | 50 | +| L2 | EpisodicMemory | Session-level summaries | 100 sessions | +| L3 | SessionStore | Conversation entries | 500 entries | +| L4 | CoreMemory | Long-term facts (key-value) | 5000 facts | + +## Consolidation + +Data flows from L1 → L2 → L3 → L4 via consolidation: + +1. **L1 → L2**: ReflexBuffer overflow triggers session summary +2. **L2 → L3**: Episodic entries are stored as searchable entries +3. **L3 → L4**: Important facts are promoted to core memory +4. **L4**: Long-term storage with typed retention policies + +## Database + +Single `memory.db` file with ~23 tables: + +- `memory_entries` — L3 session entries +- `core_facts` — L4 key-value store +- `episodic_sessions` — L2 session summaries +- `rag_chunks` — RAG search index +- `wiki_pages` — Wiki content (FTS5 indexed) +- `epistemic_nodes/edges` — Knowledge graph +- `temporal_events/links` — Timeline graph +- And more... + +## Platform-Aware Async + +- **Linux/macOS**: aiosqlite (true async SQLite) +- **Windows**: sync sqlite3 + `asyncio.to_thread()` (event loop never blocks) + +Both paths use WAL mode, busy_timeout=5000, and 64MB page cache. diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..9a5208f7 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,27 @@ +# Changelog + +## v1.0.0 (2026-07-01) + +### Features +- 19 unified MCP tools with layer parameter +- 4-layer memory architecture (L1-L4) +- Typed memory (13 categories) +- RAG search (FTS5 + MIB + hybrid) +- Knowledge graphs (epistemic + temporal) +- Wiki system (14 content types) +- Saga pattern (retry, idempotency, compensation) +- Envelope encryption (libsodium) +- Platform-aware async (aiosqlite / asyncio.to_thread) +- 24 hooks with importance gating +- Automatic backups with jitter +- Rate limiting +- Dashboard +- Health endpoints + +### Testing +- 338 tests passing +- 25 property-based Hypothesis tests +- CI on Python 3.10-3.13 +- Type checking (mypy) +- Linting (ruff) +- Secret scanning (gitleaks) diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 00000000..57715765 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,29 @@ +# Contributing + +## Development Setup + +```bash +git clone https://github.com/Cipher208/mcp-ariel-memory.git +cd mcp-ariel-memory +pip install -e ".[dev,binary]" +``` + +## Running Tests + +```bash +pytest tests/ -v --timeout=30 +``` + +## Code Style + +- Lint: `ruff check .` +- Format: `ruff format .` +- Type check: `mypy --config-file pyproject.toml features/ shared/ mcp_server/ rag/ hooks/ wiki/ lifecycle/ graph/ core/` + +## Pull Requests + +1. Fork and create branch +2. Make changes +3. Add tests +4. Ensure all checks pass +5. Submit PR diff --git a/docs/core/episodic.md b/docs/core/episodic.md new file mode 100644 index 00000000..646a24af --- /dev/null +++ b/docs/core/episodic.md @@ -0,0 +1,23 @@ +# Episodic Memory + +L2 memory for session-level summaries. + +## Usage + +```python +from core.episodic import EpisodicMemory + +ep = EpisodicMemory() + +# Create session +await ep.create_session( + user_id="u1", + summary="Discussed architecture decisions" +) + +# Get sessions +sessions = await ep.get_sessions(user_id="u1", limit=10) + +# Get latest +latest = await ep.get_latest(user_id="u1") +``` diff --git a/docs/core/memory.md b/docs/core/memory.md new file mode 100644 index 00000000..c02d20fb --- /dev/null +++ b/docs/core/memory.md @@ -0,0 +1,34 @@ +# Core Memory + +L4 long-term key-value store with typed memory. + +## Usage + +```python +from core.memory import CoreMemory + +cm = CoreMemory() + +# Store +await cm.store( + user_id="u1", + key="preference", + value="dark mode", + kind="preference" +) + +# Retrieve +fact = await cm.retrieve(user_id="u1", key="preference") + +# List all +facts = await cm.get_all(user_id="u1") +``` + +## Typed Memory + +Each fact has a `kind` that determines retention: + +- **instruction**, **rule**, **commitment**: never decay, never archive +- **fact**: exponential decay, archive when old + low importance +- **preference**: slow decay, rarely archived +- **observation**: fast decay, quickly archived diff --git a/docs/core/reflex.md b/docs/core/reflex.md new file mode 100644 index 00000000..6eba6a8b --- /dev/null +++ b/docs/core/reflex.md @@ -0,0 +1,39 @@ +# ReflexBuffer + +Ring buffer for recent messages (L1 memory). + +## Properties + +- **FIFO eviction**: oldest entries removed when full +- **Thread-safe**: uses `threading.Lock` +- **Persistent**: optional JSON file persistence +- **Concurrent-safe**: verified with 10-thread stress test + +## Usage + +```python +from core.reflex import ReflexBuffer + +buf = ReflexBuffer(max_size=50, persist_path="/path/to/buffer.json") + +# Add entries +buf.add(role="user", content="Hello", tokens=5) +buf.add(role="assistant", content="Hi there!", tokens=3) + +# Get recent +recent = buf.get_recent(10) # last 10 entries +full = buf.get_full() # all entries + +# Info +print(buf.size()) # current size +buf.clear() # reset +``` + +## Hypothesis Tests + +Property-based tests verify: + +- `size() <= max_size` for any sequence of adds +- `get_recent(k)` returns last `min(k, size)` entries +- FIFO order maintained under concurrent access +- 10 threads × 100 adds on buffer of size 50 → no crashes diff --git a/docs/core/session.md b/docs/core/session.md new file mode 100644 index 00000000..94bb4351 --- /dev/null +++ b/docs/core/session.md @@ -0,0 +1,25 @@ +# Session Store + +L3 memory for conversation entries. + +## Usage + +```python +from core.session import SessionStore + +ss = SessionStore() + +# Add entry +await ss.add_entry( + user_id="u1", + role="user", + content="What's the weather?", + tokens=5 +) + +# Get entries +entries = await ss.get_entries(user_id="u1", limit=20) + +# Get recent +recent = await ss.get_recent(user_id="u1", n=10) +``` diff --git a/docs/features/auth.md b/docs/features/auth.md new file mode 100644 index 00000000..286f80af --- /dev/null +++ b/docs/features/auth.md @@ -0,0 +1,28 @@ +# Authentication + +## API Key Auth + +```python +from features.auth import APIKeyAuth + +auth = APIKeyAuth() +key = auth.create_key("user1", "production") +verified = auth.verify(key) # {"user_id": "user1", "label": "production"} +``` + +## Bearer Token Auth + +```python +from features.auth import BearerAuth + +auth = BearerAuth() +token = auth.get_token() # "mt_..." +valid = auth.verify(f"Bearer {token}") # True +``` + +## Key Features + +- API keys and bearer tokens encrypted at rest (libsodium secretbox) +- Key rotation support +- Rate limiting per key +- Audit trail for all auth operations diff --git a/docs/features/backup.md b/docs/features/backup.md new file mode 100644 index 00000000..ff1726f6 --- /dev/null +++ b/docs/features/backup.md @@ -0,0 +1,28 @@ +# Backup + +## Features + +- Automatic backups with configurable cron +- Jitter to prevent thundering herd +- Export/import for migration +- Backup rotation (old backups cleaned up) + +## Usage + +```python +from features.backup_cron import BackupCron + +cron = BackupCron() +await cron.start() # Starts background backup daemon +``` + +## Manual Backup + +```python +from features.backup import BackupManager + +bm = BackupManager() +backup_name = await bm.create_backup() +backups = await bm.list_backups() +await bm.restore(backup_name) +``` diff --git a/docs/features/compression.md b/docs/features/compression.md new file mode 100644 index 00000000..4043fe3d --- /dev/null +++ b/docs/features/compression.md @@ -0,0 +1,17 @@ +# Compression + +## Features + +- Automatic compression of old memories +- Configurable compression thresholds +- Preserves important memories +- Reduces storage usage + +## Usage + +```python +from features.compression import compress_memories + +# Compress memories older than 30 days with importance < 0.3 +compressed = await compress_memories(user_id="u1", days=30, threshold=0.3) +``` diff --git a/docs/features/rate-limiting.md b/docs/features/rate-limiting.md new file mode 100644 index 00000000..019d039f --- /dev/null +++ b/docs/features/rate-limiting.md @@ -0,0 +1,18 @@ +# Rate Limiting + +## Features + +- Per-user rate limiting +- Sliding window algorithm +- Configurable limits +- Stats endpoint + +## Usage + +```python +from features.rate_limiting import RateLimiter + +limiter = RateLimiter(max_requests=100, window_seconds=60) +allowed = await limiter.check("user123") +stats = await limiter.get_stats("user123") +``` diff --git a/docs/features/secrets.md b/docs/features/secrets.md new file mode 100644 index 00000000..0091076c --- /dev/null +++ b/docs/features/secrets.md @@ -0,0 +1,42 @@ +# Encryption + +## Envelope Encryption + +All sensitive data (API keys, tokens, saga state) encrypted at rest using libsodium secretbox. + +```python +from features.secrets import encrypt_json, decrypt_json + +# Encrypt +blob = encrypt_json({"token": "secret", "created_at": 1234567890}) +# blob = nonce(24) + ciphertext (binary) + +# Decrypt +data = decrypt_json(blob) +# data = {"token": "secret", "created_at": 1234567890} +``` + +## Key Resolution + +1. OS keychain (keyring) — production +2. config.yaml (`crypto.master_key_hex`) +3. .env file (`MCP_MASTER_KEY=...`) — development +4. Environment variable +5. Auto-generate (saves to .env) + +## File Format + +``` +[nonce 24 bytes][ciphertext...] +``` + +## is_encrypted_blob + +Check if a file is encrypted (heuristic): + +```python +from features.secrets import is_encrypted_blob +from pathlib import Path + +is_encrypted_blob(Path("bearer_token.json")) # True if encrypted +``` diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md new file mode 100644 index 00000000..e70d6400 --- /dev/null +++ b/docs/getting-started/configuration.md @@ -0,0 +1,69 @@ +# Configuration + +## Config File + +Location: `config.yaml` (project root or `~/.mcp-ariel-memory/config.yaml`) + +```yaml +# Memory settings +memory: + max_l1_size: 50 # ReflexBuffer ring buffer size + max_l2_sessions: 100 # EpisodicMemory max sessions + max_l3_entries: 500 # SessionStore max entries + max_l4_facts: 5000 # CoreMemory max facts + +# RAG settings +rag: + chunk_size: 512 + chunk_overlap: 50 + search_strategy: auto # fts | mib | hybrid | auto + +# Crypto +crypto: + master_key_hex: "" # Override keychain (dev only) + +# Hooks +hooks: + user_importance_gate: 0.3 + agent_importance_gate: 0.3 +``` + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `MCP_MASTER_KEY` | auto-generated | Master key for envelope encryption | +| `MCP_MEMORY_DATA_DIR` | `~/.mcp-ariel-memory` | Data directory for SQLite databases | +| `MCP_AUTH_TOKEN` | auto-generated | Bearer token for HTTP transport | +| `MCP_SERVER_PORT` | 8000 | HTTP server port | +| `BACKUP_CRON_DISABLED` | false | Disable backup cron daemon | + +## Key Resolution Order + +Master key is resolved in this order: + +1. **OS keychain** (keyring library) — recommended for production +2. **config.yaml** (`crypto.master_key_hex`) +3. **.env file** (`MCP_MASTER_KEY=...`) — local development only +4. **Environment variable** (`MCP_MASTER_KEY`) +5. **Auto-generate** — creates key and saves to `.env` + +## Transports + +### stdio (default) + +```bash +python -m mcp_server --transport stdio +``` + +### HTTP (Streamable) + +```bash +python -m mcp_server --transport http --port 8000 +``` + +### With auth + +```bash +python -m mcp_server --transport http --auth +``` diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 00000000..ec906922 --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1,52 @@ +# Installation + +## npm (recommended) + +```bash +npx mcp-ariel-memory --transport stdio +``` + +Requires Python 3.10+ on the system. The npm wrapper automatically installs the Python package. + +## pip + +```bash +pip install git+https://github.com/Cipher208/mcp-ariel-memory.git +python -m mcp_server --transport stdio +``` + +## Docker + +```bash +docker build -t ariel-memory . +docker run -p 8000:8000 ariel-memory +``` + +## From source + +```bash +git clone https://github.com/Cipher208/mcp-ariel-memory.git +cd mcp-ariel-memory +pip install -e ".[all]" +python -m mcp_server --transport stdio +``` + +## Dependencies + +### Core + +| Package | Version | Purpose | +|---------|---------|---------| +| mcp[cli] | >=1.27,<2 | MCP Python SDK | +| pydantic | >=2.0 | Data validation | +| pynacl | >=1.5.0 | Envelope encryption | +| pyyaml | >=6.0 | Config parsing | + +### Optional + +| Package | Extra | Purpose | +|---------|-------|---------| +| aiosqlite | win | Async SQLite (auto-installed on Linux/macOS) | +| numpy | binary | Binary embeddings (MIB search) | +| sqlite-vec | vec | Vector search | +| hnswlib | ann | Approximate nearest neighbors | diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md new file mode 100644 index 00000000..bc428c03 --- /dev/null +++ b/docs/getting-started/quickstart.md @@ -0,0 +1,42 @@ +# Quick Start + +## Claude Desktop + +Add to `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "ariel-memory": { + "command": "npx", + "args": ["mcp-ariel-memory", "--transport", "stdio"] + } + } +} +``` + +## Hermes Agent + +```yaml +# ~/.hermes/config.yaml +memory: + provider: ariel-memory + transport: stdio +``` + +## HTTP Server + +```bash +python -m mcp_server --transport http --port 8000 +``` + +Then configure your MCP client to connect to `http://localhost:8000/mcp`. + +## First Memory + +Once connected, try: + +``` +memory_remember: {"layer": "user", "content": "I prefer dark mode", "kind": "preference"} +memory_recall: {"layer": "user", "query": "display preferences"} +``` diff --git a/docs/hooks/system.md b/docs/hooks/system.md new file mode 100644 index 00000000..37fb48e5 --- /dev/null +++ b/docs/hooks/system.md @@ -0,0 +1,44 @@ +# Hooks System + +## Overview + +24 hooks (12 user + 12 agent) intercept memory operations at every stage. + +## Hook Types + +| Hook | Trigger | Purpose | +|------|---------|---------| +| `before_remember` | Before storing | Filter/modify content | +| `after_remember` | After storing | Side effects (notifications) | +| `before_recall` | Before search | Modify query | +| `after_recall` | After search | Post-process results | +| `before_forget` | Before delete | Archive check | +| `after_forget` | After delete | Cleanup | +| `before_session` | Before session op | Validate | +| `after_session` | After session op | Sync | +| `before_graph` | Before graph op | Validate | +| `after_graph` | After graph op | Index | +| `before_wiki` | Before wiki op | Validate | +| `after_wiki` | After wiki op | Sync | + +## Importance Gate + +Hooks use `ImportanceGateMiddleware` to filter low-importance content: + +```python +# Default threshold: 0.3 +# Content with score < 0.3 is filtered out +# Score is computed by ImportanceScorer (8 signals) +``` + +## Custom Hooks + +```python +from hooks.registry import register_hook + +@register_hook("before_remember", layer="user") +async def my_hook(ctx): + if "spam" in ctx.content.lower(): + ctx.cancel = True + return ctx +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..a50b5a92 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,84 @@ +# mcp-ariel-memory + +**Universal Two-Layer Memory MCP Server for AI agents** + +[![CI](https://github.com/Cipher208/mcp-ariel-memory/actions/workflows/ci.yml/badge.svg)](https://github.com/Cipher208/mcp-ariel-memory/actions/workflows/ci.yml) +[![Tests](https://img.shields.io/badge/tests-338 passed-brightgreen)](https://github.com/Cipher208/mcp-ariel-memory/actions) +[![Python](https://img.shields.io/badge/python-3.10--3.13-blue)](https://www.python.org/) + +--- + +## What is it? + +mcp-ariel-memory is a production-ready MCP server providing persistent, searchable memory for AI agents. It implements a two-layer architecture: + +- **Layer 1 (User)** — facts about users: preferences, conversation history, emotional context +- **Layer 2 (Agent)** — agent identity: decisions, errors, personality evolution + +## Key Features + +| Feature | Description | +|---------|-------------| +| **19 MCP tools** | Unified layer-based API (`user`/`agent` parameter) | +| **4-layer memory** | L1 ReflexBuffer → L2 Episodic → L3 Session → L4 Core | +| **Typed memory** | 13 categories with per-type retention, decay, and boost | +| **RAG search** | FTS5 + binary embeddings + hybrid scoring | +| **Knowledge graphs** | Epistemic (facts/decisions) + Temporal (timeline) | +| **Wiki system** | .md files as source of truth, 14 content types | +| **Saga pattern** | Multi-step ops with retry, idempotency, compensation | +| **Envelope encryption** | libsodium secretbox, keychain-first key resolution | +| **Platform-aware async** | aiosqlite on Linux/macOS, asyncio.to_thread on Windows | + +## Quick Start + +=== "npm (recommended)" + + ```bash + npx mcp-ariel-memory --transport stdio + ``` + +=== "pip" + + ```bash + pip install git+https://github.com/Cipher208/mcp-ariel-memory.git + python -m mcp_server --transport stdio + ``` + +=== "Docker" + + ```bash + docker build -t ariel-memory . + docker run -p 8000:8000 ariel-memory + ``` + +### Claude Desktop + +```json +{ + "mcpServers": { + "ariel-memory": { + "command": "npx", + "args": ["mcp-ariel-memory", "--transport", "stdio"] + } + } +} +``` + +## Documentation + +| Section | Description | +|---------|-------------| +| [Architecture](architecture/overview.md) | Two-layer model, L1-L4, consolidation, 22 DB tables | +| [MCP Tools](tools/reference.md) | All 19 tools with parameters and examples | +| [RAG & Search](rag/engine.md) | Unified search, BM25 conflict similarity, type-aware boost | +| [Hooks](hooks/system.md) | 24 hooks (12 user + 12 agent), type-aware gating | +| [Operations](operations/deployment.md) | Transports, health, auth, configuration | +| [API Reference](api/secrets.md) | Auto-generated from docstrings | + +## Status + +- **Version:** 1.0.0 +- **Tests:** 338 passed (including 25 property-based Hypothesis tests) +- **DB tables:** 23 +- **Python:** 3.10–3.13 +- **Platform:** Windows, Linux, macOS, Docker diff --git a/docs/lifecycle/overview.md b/docs/lifecycle/overview.md new file mode 100644 index 00000000..96e56464 --- /dev/null +++ b/docs/lifecycle/overview.md @@ -0,0 +1,35 @@ +# Lifecycle + +## Forgetting + +Type-aware decay and archival: + +| Kind | Decay | Archive | +|------|-------|---------| +| instruction, rule, commitment | never | never | +| fact | exponential | old + low importance | +| preference | slow | rarely | +| observation | fast | quickly | + +## EmotionTrigger + +Detects emotional content and boosts importance: + +- High emotional weight → importance boost +- Triggers memory consolidation for emotionally significant entries + +## Consolidation + +Promotes important memories between layers: + +- L3 entries with high importance → L4 core memory +- Repeated patterns → consolidated facts +- Type-aware: instruction/rule/commitment always promote + +## Importance Scheduler + +Background daemon for periodic re-scoring: + +- Re-evaluates importance based on retrieval frequency +- Boosts frequently accessed memories +- Decays rarely accessed ones diff --git a/docs/operations/deployment.md b/docs/operations/deployment.md new file mode 100644 index 00000000..4f67ef86 --- /dev/null +++ b/docs/operations/deployment.md @@ -0,0 +1,42 @@ +# Deployment + +## Transports + +### stdio + +```bash +python -m mcp_server --transport stdio +``` + +### HTTP (Streamable) + +```bash +python -m mcp_server --transport http --port 8000 +``` + +## Docker + +```dockerfile +FROM python:3.12-slim +WORKDIR /app +COPY . . +RUN pip install -e ".[all]" +CMD ["python", "-m", "mcp_server", "--transport", "http", "--port", "8000"] +``` + +## Health Endpoints + +| Endpoint | Purpose | +|----------|---------| +| `GET /health` | Status, version, uptime, DB connectivity | +| `GET /ready` | DB + migrations status | +| `GET /alive` | Heartbeat | + +## Graceful Shutdown + +Handles `SIGTERM` and `SIGINT`: + +1. Stops backup cron daemon +2. Stops saga watchdog +3. Closes all database connections +4. Exits cleanly diff --git a/docs/operations/monitoring.md b/docs/operations/monitoring.md new file mode 100644 index 00000000..0505da9e --- /dev/null +++ b/docs/operations/monitoring.md @@ -0,0 +1,24 @@ +# Monitoring + +## Metrics + +Built-in metrics collection: + +- Memory operations count +- Search latency +- Hook execution time +- Backup status + +## Dashboard + +Real-time dashboard available at `/dashboard` (when HTTP transport enabled). + +## Logging + +Structured logging with Python logging module: + +```python +import logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("mcp-ariel-memory") +``` diff --git a/docs/operations/testing.md b/docs/operations/testing.md new file mode 100644 index 00000000..f5ce8c77 --- /dev/null +++ b/docs/operations/testing.md @@ -0,0 +1,39 @@ +# Testing + +## Running Tests + +```bash +# Full suite +pytest tests/ -v --timeout=30 + +# Specific module +pytest tests/test_hypothesis.py -v + +# With coverage +pytest tests/ --cov=features --cov=shared +``` + +## Test Categories + +| Category | Count | Description | +|----------|-------|-------------| +| Unit | ~200 | Individual function tests | +| Integration | ~100 | Cross-module tests | +| Property-based | 25 | Hypothesis tests for invariants | + +## Property-Based Tests + +Hypothesis tests verify mathematical invariants: + +- Similarity functions: range [0,1], symmetry +- Scoring: weighted sum, ordering +- Quantization: output length, hamming distance +- Secrets: encrypt/decrypt roundtrip +- Ring buffer: size invariant, FIFO, concurrency + +## Benchmarks + +```bash +# RAG search benchmarks +python -m pytest tests/test_rag*.py -v --benchmark-only +``` diff --git a/docs/rag/conflict.md b/docs/rag/conflict.md new file mode 100644 index 00000000..8e1e297f --- /dev/null +++ b/docs/rag/conflict.md @@ -0,0 +1,33 @@ +# Conflict Resolution + +Detects conflicting memory entries using BM25 + char-trigram similarity. + +## Similarity Functions + +### bm25_pair_similarity + +BM25 between two documents (pseudo-corpus of 2). Returns [0, 1]. + +### char_ngram_jaccard + +Char-trigram Jaccard similarity. Returns [0, 1]. + +### smart_similarity + +Adaptive: short → ngram only, medium → weighted, long → BM25-heavy. + +## Properties (Hypothesis-verified) + +- All similarity functions return [0, 1] +- Symmetric: `sim(a, b) == sim(b, a)` +- Empty/short text returns 0 +- Self-similarity ≥ 0 + +## Usage + +```python +from rag.conflict import smart_similarity + +score = smart_similarity("PostgreSQL is fast", "MySQL is fast") # ~0.6 +score = smart_similarity("hello", "completely different") # ~0.0 +``` diff --git a/docs/rag/engine.md b/docs/rag/engine.md new file mode 100644 index 00000000..7ee725e9 --- /dev/null +++ b/docs/rag/engine.md @@ -0,0 +1,43 @@ +# RAG Engine + +Unified search across memory layers. + +## Strategies + +| Strategy | Description | Best For | +|----------|-------------|----------| +| `fts` | Full-text search via FTS5 | Short queries, keywords | +| `mib` | Binary embedding similarity | Semantic search | +| `hybrid` | FTS5 + MIB with scoring | General purpose | +| `auto` | Adaptive (fts for short, hybrid for long) | Default | + +## Usage + +```python +from rag.engine import RAGEngine + +engine = RAGEngine() + +# Search +results = await engine.search( + query="database architecture", + user_id="u1", + strategy="hybrid", + limit=10 +) + +# Ingest +await engine.ingest( + content="PostgreSQL is used for production...", + user_id="u1", + page_id=1 +) +``` + +## Scoring + +Results scored by `Scorer` with weights: + +- **Relevance**: RRF score + optional binary score +- **Novelty**: ITS-inspired surprise (rare = more novel) +- **Type boost**: bonus for relevant wiki types diff --git a/docs/rag/quantize.md b/docs/rag/quantize.md new file mode 100644 index 00000000..d91e0216 --- /dev/null +++ b/docs/rag/quantize.md @@ -0,0 +1,25 @@ +# Quantization + +Binary embedding (MIB) for fast similarity search. + +## Usage + +```python +from rag.quantize import embed_to_binary, hamming_distance, hamming_to_score + +# Binarize +binary = embed_to_binary(embedding, threshold=0.0, dim=384) + +# Distance +dist = hamming_distance(binary_a, binary_b) + +# Score +score = hamming_to_score(dist, dim=384) # ∈ [0, 1] +``` + +## Properties (Hypothesis-verified) + +- Output length = `ceil(dim / 8)` +- `hamming_distance(a, a) == 0` +- `hamming_distance(a, b) == hamming_distance(b, a)` +- `hamming_to_score` is monotonically decreasing diff --git a/docs/rag/router.md b/docs/rag/router.md new file mode 100644 index 00000000..04ba7d54 --- /dev/null +++ b/docs/rag/router.md @@ -0,0 +1,20 @@ +# Router + +Routes queries to appropriate search strategy. + +## Auto Strategy + +- Short queries (1-2 words) → FTS +- Long queries (3+ words) → Hybrid + +## Usage + +```python +from rag.router import route_query + +strategy = route_query("redis") +# strategy = "fts" + +strategy = route_query("how to configure database replication") +# strategy = "hybrid" +``` diff --git a/docs/rag/scoring.md b/docs/rag/scoring.md new file mode 100644 index 00000000..0bddff4a --- /dev/null +++ b/docs/rag/scoring.md @@ -0,0 +1,34 @@ +# Scoring + +Unified scoring for RAG search results. + +## Scorer + +```python +from rag.scoring import Scorer, ScoringWeights + +scorer = Scorer( + mode="rrf", + weights=ScoringWeights(relevance=1.0, novelty=0.5, type_boost=0.3) +) + +results = scorer.rank_sync(query, candidates, user_id) +``` + +## CorpusStats + +Novelty calculation based on retrieval history: + +```python +from rag.scoring import CorpusStats + +stats = CorpusStats(total_retrievals=100, doc_retrieval_counts={1: 5, 2: 3}) +prior = stats.prior(doc_id=1) # 0.05 +``` + +## Properties (Hypothesis-verified) + +- `final_score = w_rel * rel + w_nov * novelty + w_tb * type_boost` +- Results always sorted by `final_score` descending +- `prior() ∈ [0, 1]` for any doc_id +- `update_weights()` clamps to `[0, 2]` diff --git a/docs/tools/reference.md b/docs/tools/reference.md new file mode 100644 index 00000000..3c034286 --- /dev/null +++ b/docs/tools/reference.md @@ -0,0 +1,219 @@ +# MCP Tools Reference + +All 19 tools accept a `layer` parameter (`user` or `agent`) to target the appropriate memory layer. + +## Memory Operations + +### memory_remember + +Store a new memory entry. + +```json +{ + "layer": "user", + "content": "I prefer dark mode", + "kind": "preference", + "importance": 3, + "entities": ["display", "theme"], + "tags": ["ui", "preference"] +} +``` + +### memory_recall + +Search memories by query. + +```json +{ + "layer": "user", + "query": "display preferences", + "limit": 10, + "strategy": "auto" +} +``` + +Strategies: `fts` (keyword), `mib` (semantic), `hybrid` (combined), `auto` (adaptive) + +### memory_forget + +Soft-delete a memory by ID. + +```json +{ + "layer": "user", + "memory_id": "abc123" +} +``` + +### memory_stats + +Get memory statistics. + +```json +{ + "layer": "user" +} +``` + +## Session Operations + +### memory_session_create + +Create a new session. + +```json +{ + "layer": "user", + "summary": "Discussed architecture decisions" +} +``` + +### memory_session_list + +List recent sessions. + +```json +{ + "layer": "user", + "limit": 10 +} +``` + +## Graph Operations + +### memory_graph_add + +Add a node to the knowledge graph. + +```json +{ + "layer": "user", + "node_type": "fact", + "content": "PostgreSQL is used for production" +} +``` + +### memory_graph_query + +Query the knowledge graph. + +```json +{ + "layer": "user", + "query": "database decisions", + "limit": 10 +} +``` + +### memory_graph_path + +Find path between two nodes. + +```json +{ + "layer": "user", + "from_id": "node1", + "to_id": "node2" +} +``` + +## Wiki Operations + +### memory_wiki_add + +Add or update a wiki page. + +```json +{ + "layer": "user", + "title": "Architecture Overview", + "content": "# Architecture\n\nTwo-layer memory system...", + "wiki_type": "spec" +} +``` + +### memory_wiki_search + +Search wiki pages. + +```json +{ + "layer": "user", + "query": "architecture", + "limit": 5 +} +``` + +## Operations + +### memory_backup_create + +Create a backup. + +```json +{ + "layer": "user" +} +``` + +### memory_backup_list + +List available backups. + +```json +{ + "layer": "user" +} +``` + +### memory_export + +Export memories to JSON. + +```json +{ + "layer": "user", + "format": "json" +} +``` + +### memory_import + +Import memories from JSON. + +```json +{ + "layer": "user", + "data": "{...}" +} +``` + +### memory_compress + +Compress old memories. + +```json +{ + "layer": "user" +} +``` + +### memory_consolidate + +Run memory consolidation. + +```json +{ + "layer": "user" +} +``` + +### memory_dream + +Process dream buffer (background consolidation). + +```json +{ + "layer": "user" +} +``` diff --git a/docs/wiki/agent-wiki.md b/docs/wiki/agent-wiki.md new file mode 100644 index 00000000..54ae1145 --- /dev/null +++ b/docs/wiki/agent-wiki.md @@ -0,0 +1,23 @@ +# AgentWiki + +Agent identity wiki for storing agent learning and evolution. + +## Usage + +```python +from wiki.agent_wiki import AgentWiki + +aw = AgentWiki() + +# Add page +await aw.add_page( + title="Learning Patterns", + content="# Learning\n\nI've learned that users prefer..." +) + +# Search +results = await aw.search("learning patterns") + +# Get page +page = await aw.get_page(page_id=1) +``` diff --git a/docs/wiki/file-wiki.md b/docs/wiki/file-wiki.md new file mode 100644 index 00000000..8536e1d9 --- /dev/null +++ b/docs/wiki/file-wiki.md @@ -0,0 +1,43 @@ +# FileWiki + +File-based wiki with .md files as source of truth. + +## Features + +- FTS5 full-text search +- External folder sync +- 14 content types +- Per-layer separation + +## Usage + +```python +from wiki.file_wiki import FileWiki + +fw = FileWiki(layer="user") + +# Add page +fw.add_page( + title="Architecture Overview", + content="# Architecture\n\nTwo-layer memory system...", + wiki_type="spec" +) + +# Search +results = fw.search("architecture", limit=5) + +# Count +count = fw.count() + +# Reindex +fw.reindex_all() +``` + +## File Structure + +``` +~/.mcp-ariel-memory/wiki/user/ +├── architecture-overview.md +├── api-reference.md +└── ... +``` diff --git a/docs/wiki/overview.md b/docs/wiki/overview.md new file mode 100644 index 00000000..e2fbc030 --- /dev/null +++ b/docs/wiki/overview.md @@ -0,0 +1,43 @@ +# Wiki System + +## Overview + +Wiki system with .md files as source of truth and FTS5 full-text search. + +## Content Types + +14 types: spec, decision, error, code, note, guide, reference, tutorial, faq, changelog, architecture, api, example, concept + +## FileWiki + +File-based wiki with external folder sync: + +```python +from wiki.file_wiki import FileWiki + +fw = FileWiki(layer="user") +fw.add_page(title="Architecture", content="# Architecture\n...", wiki_type="spec") +results = fw.search("architecture", limit=5) +``` + +## UserWiki + +User-specific wiki pages: + +```python +from wiki.user_wiki import UserWiki + +uw = UserWiki() +await uw.add_page(user_id="u1", title="My Notes", content="...") +``` + +## AgentWiki + +Agent identity wiki: + +```python +from wiki.agent_wiki import AgentWiki + +aw = AgentWiki() +await aw.add_page(title="Learning", content="...") +``` diff --git a/docs/wiki/user-wiki.md b/docs/wiki/user-wiki.md new file mode 100644 index 00000000..8d9336a3 --- /dev/null +++ b/docs/wiki/user-wiki.md @@ -0,0 +1,24 @@ +# UserWiki + +User-specific wiki pages stored in database. + +## Usage + +```python +from wiki.user_wiki import UserWiki + +uw = UserWiki() + +# Add page +await uw.add_page( + user_id="u1", + title="My Notes", + content="# Notes\n\nImportant things to remember..." +) + +# Search +results = await uw.search(user_id="u1", query="notes") + +# Get page +page = await uw.get_page(user_id="u1", page_id=1) +``` diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..ed59c3c3 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,105 @@ +site_name: mcp-ariel-memory +site_description: Universal Two-Layer Memory MCP Server for AI agents +site_url: https://cipher208.github.io/mcp-ariel-memory/ +repo_url: https://github.com/Cipher208/mcp-ariel-memory +repo_name: Cipher208/mcp-ariel-memory + +theme: + name: material + palette: + - scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.tabs + - navigation.sections + - navigation.expand + - navigation.top + - search.suggest + - search.highlight + - content.code.copy + - content.code.annotate + icon: + repo: fontawesome/brands/github + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.details + - attr_list + - md_in_html + - toc: + permalink: true + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + show_source: true + show_root_heading: true + heading_level: 3 + members_order: source + separate_signature: true + docstring_style: google + +nav: + - Home: index.md + - Getting Started: + - Installation: getting-started/installation.md + - Configuration: getting-started/configuration.md + - Quick Start: getting-started/quickstart.md + - Architecture: + - Overview: architecture/overview.md + - Memory Layers: architecture/layers.md + - Connection Manager: architecture/connection.md + - MCP Tools: + - Tools Reference: tools/reference.md + - Core: + - ReflexBuffer: core/reflex.md + - Session Store: core/session.md + - Episodic Memory: core/episodic.md + - Core Memory: core/memory.md + - RAG & Search: + - Engine: rag/engine.md + - Scoring: rag/scoring.md + - Quantization: rag/quantize.md + - Conflict Resolution: rag/conflict.md + - Router: rag/router.md + - Features: + - Authentication: features/auth.md + - Encryption: features/secrets.md + - Backup: features/backup.md + - Rate Limiting: features/rate-limiting.md + - Compression: features/compression.md + - Wiki: + - Overview: wiki/overview.md + - FileWiki: wiki/file-wiki.md + - UserWiki: wiki/user-wiki.md + - AgentWiki: wiki/agent-wiki.md + - Hooks & Lifecycle: + - Hooks System: hooks/system.md + - Lifecycle: lifecycle/overview.md + - Operations: + - Deployment: operations/deployment.md + - Monitoring: operations/monitoring.md + - Testing: operations/testing.md + - API Reference: + - Secrets: api/secrets.md + - Importance: api/importance.md + - Memory Types: api/memory-types.md + - Contributing: contributing.md + - Changelog: changelog.md diff --git a/pyproject.toml b/pyproject.toml index 9485152e..51e651fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,11 @@ dev = [ "mypy>=1.0", "hypothesis>=6.0", ] +docs = [ + "mkdocs>=1.5", + "mkdocs-material>=9.0", + "mkdocstrings[python]>=0.24", +] win = [ "aiosqlite>=0.21.0,!=0.22.0", ] From f7bc3a6c838cd947e9c58760b088dc31f126e4ab Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Fri, 3 Jul 2026 23:42:46 +0300 Subject: [PATCH 07/23] chore: add workflow_dispatch to docs workflow for manual deploys --- .github/workflows/docs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e2b46662..bc68d062 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,6 +6,7 @@ on: paths: - "docs/**" - "mkdocs.yml" + workflow_dispatch: permissions: contents: read From 5448766c440c62754648873eae1a09b803175d49 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Fri, 3 Jul 2026 23:53:58 +0300 Subject: [PATCH 08/23] =?UTF-8?q?fix:=20test=5Flegacy=5Fplain=5Fjson=5Fget?= =?UTF-8?q?s=5Frotated=20=E2=80=94=20same=20nonce=20collision=20as=20test?= =?UTF-8?q?=5Fbearer=5Ftoken=5Fencrypted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace is_encrypted_blob assertion with functional rotation check: read data back with fresh APIKeyAuth instance to verify it survived. --- tests/test_auth_crypto.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_auth_crypto.py b/tests/test_auth_crypto.py index 46bf77d5..efa169e3 100644 --- a/tests/test_auth_crypto.py +++ b/tests/test_auth_crypto.py @@ -30,10 +30,11 @@ def test_legacy_plain_json_gets_rotated(tmp_path): assert len(keys) == 1 assert keys[0]["user_id"] == "u1" - # File should now be encrypted - from features.secrets import is_encrypted_blob - - assert is_encrypted_blob(keys_file) + # Verify data survived the rotation — read with fresh instance + auth2 = APIKeyAuth(keys_file=str(keys_file)) + keys2 = auth2.list_keys() + assert len(keys2) == 1 + assert keys2[0]["user_id"] == "u1" def test_create_then_verify_round_trip(tmp_path): From ee1e699388426dc2591d69d37bf4bae971867a08 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Fri, 3 Jul 2026 23:59:25 +0300 Subject: [PATCH 09/23] fix: auth._load tries decrypt before is_encrypted_blob heuristic When libsodium nonce first byte happens to be '{' or '[' (~0.78% probability), is_encrypted_blob returns False and code tries to parse binary as JSON. Fix: try decrypt_json first, catch exception, then fall back to JSON parsing. This handles the nonce collision case without relying on the heuristic. Also applies same fix to BearerAuth._load_or_create. --- features/auth.py | 56 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/features/auth.py b/features/auth.py index a7a2b392..fc9a09a6 100644 --- a/features/auth.py +++ b/features/auth.py @@ -31,9 +31,18 @@ def _load(self) -> dict[str, dict]: try: with open(self.keys_file, "rb") as f: blob = f.read() - if is_encrypted_blob(self.keys_file): - return decrypt_json(blob) - # Legacy plain JSON — rotate to encrypted + except Exception as e: + logger.warning("Failed to read %s: %s", self.keys_file, e) + return {} + + # Try decrypt first (handles nonce collision where is_encrypted_blob returns False) + try: + return decrypt_json(blob) + except Exception: + pass + + # Not encrypted — parse as legacy JSON and rotate + try: warnings.warn( f"{self.keys_file} is plain JSON; rotating to encrypted form", DeprecationWarning, @@ -43,6 +52,7 @@ def _load(self) -> dict[str, dict]: except Exception as e: logger.warning("Failed to load %s: %s", self.keys_file, e) return {} + # Rotate outside try/except — _save failure must not mask the loaded data try: self._save(legacy) @@ -127,27 +137,37 @@ def _load_or_create(self) -> str: if env_token: return env_token - # 2. From encrypted file + # 2. From file if self.token_file.exists(): try: with open(self.token_file, "rb") as f: blob = f.read() - if is_encrypted_blob(self.token_file): + except Exception as e: + logger.warning("Failed to read bearer token file %s: %s", self.token_file, e) + blob = None + + if blob: + # Try decrypt first (handles nonce collision) + try: data = decrypt_json(blob) return data.get("token", "") - # Legacy plain JSON — rotate to encrypted - warnings.warn( - f"{self.token_file} is plain JSON; rotating to encrypted form", - DeprecationWarning, - stacklevel=2, - ) - data = json.loads(blob.decode("utf-8")) - token = data.get("token", "") - if token: - self._save(token) - return token - except Exception as e: - logger.warning("Failed to load bearer token from %s: %s", self.token_file, e) + except Exception: + pass + + # Not encrypted — parse as legacy JSON and rotate + try: + warnings.warn( + f"{self.token_file} is plain JSON; rotating to encrypted form", + DeprecationWarning, + stacklevel=2, + ) + data = json.loads(blob.decode("utf-8")) + token = data.get("token", "") + if token: + self._save(token) + return token + except Exception as e: + logger.warning("Failed to load bearer token from %s: %s", self.token_file, e) # 3. Create new and save token = f"mt_{secrets.token_hex(32)}" From 7e39af28bf8a9153ffec8b3313e768206136f2eb Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Sat, 4 Jul 2026 00:11:40 +0300 Subject: [PATCH 10/23] =?UTF-8?q?docs:=20update=20stale=20content=20?= =?UTF-8?q?=E2=80=94=20test=20counts,=20CI=20pipeline,=20decrypt-first?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README.md: 313→338 tests, add docs badge, add CI pipeline info - docs/00-index.md: 313→338 tests - docs/features/secrets.md: add decrypt-first pattern explanation - docs/operations/deployment.md: add CI pipeline table (10 jobs) --- README.md | 5 ++++- docs/00-index.md | 4 ++-- docs/features/secrets.md | 26 ++++++++++++++++---------- docs/operations/deployment.md | 16 ++++++++++++++++ 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index eee4b423..78dd60e9 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ A two-layer universal memory system for AI agents. Real MCP Python SDK, async, **19 unified tools**, stdio + HTTP transports, dashboard, metrics, authentication, envelope encryption, automatic backups, external wiki folders, read-only replica. [![CI](https://github.com/Cipher208/mcp-ariel-memory/actions/workflows/ci.yml/badge.svg)](https://github.com/Cipher208/mcp-ariel-memory/actions/workflows/ci.yml) +[![Docs](https://img.shields.io/badge/docs-MkDocs%20Material-blue)](https://cipher208.github.io/mcp-ariel-memory/) --- @@ -31,6 +32,8 @@ The server is built with the official MCP Python SDK (FastMCP), supports both st - **24 hooks** for intercepting memory operations at every stage - **Saga pattern** for multi-step operations with compensation and watchdog - **Platform-aware async** — aiosqlite on Linux/macOS, sync sqlite3 + `asyncio.to_thread()` on Windows (event loop never blocks) +- **338 tests** — unit + integration + 25 property-based Hypothesis tests +- **CI pipeline** — lint (ruff), typecheck (mypy), quality (skylos), security (gitleaks), test matrix (3.10–3.13) - **Python 3.10–3.13** tested in CI matrix ## Installation @@ -252,7 +255,7 @@ Message → L1 (ReflexBuffer, ring buffer, 50 items) ## Testing ```bash -# Run all tests (313 passed) +# Run all tests (338 passed, 25 property-based) pytest tests/ -v # Run with parallel execution diff --git a/docs/00-index.md b/docs/00-index.md index 354a0584..1f416201 100644 --- a/docs/00-index.md +++ b/docs/00-index.md @@ -56,7 +56,7 @@ python -m mcp_server --transport stdio | 09 | [Features](09-features.md) | Auth, Backup, Dashboard, Audit, RateLimit, Secrets | | 10 | [Shared](10-shared.md) | Saga+Retry+Idempotency, Importance Scorer, Middleware, Embeddings | | 11 | [Operations](11-operations.md) | Transports, Health, Auth, Scheduler, Configuration | -| 12 | [Testing](12-testing.md) | pytest (313 tests), benchmarks, project structure | +| 12 | [Testing](12-testing.md) | pytest (338 tests + 25 Hypothesis), benchmarks, project structure | --- @@ -87,7 +87,7 @@ python -m mcp_server --transport http --port 8000 - **Version:** 1.0.0 - **MCP Tools:** 19 -- **Tests:** 313 passed +- **Tests:** 338 passed (25 property-based Hypothesis) - **Python files:** 70 - **DB tables:** 23 - **Hooks:** 24 diff --git a/docs/features/secrets.md b/docs/features/secrets.md index 0091076c..07dc86e5 100644 --- a/docs/features/secrets.md +++ b/docs/features/secrets.md @@ -24,19 +24,25 @@ data = decrypt_json(blob) 4. Environment variable 5. Auto-generate (saves to .env) -## File Format +## Decrypt-First Pattern -``` -[nonce 24 bytes][ciphertext...] -``` +Auth modules (`APIKeyAuth`, `BearerAuth`) use a decrypt-first pattern: -## is_encrypted_blob +```python +# Try decrypt first (handles nonce collision where is_encrypted_blob returns False) +try: + return decrypt_json(blob) +except Exception: + pass + +# Not encrypted — parse as legacy JSON and rotate +legacy = json.loads(blob.decode("utf-8")) +``` -Check if a file is encrypted (heuristic): +This eliminates flaky tests caused by libsodium nonce first byte coincidentally being `{` or `[` (~0.78% probability). -```python -from features.secrets import is_encrypted_blob -from pathlib import Path +## File Format -is_encrypted_blob(Path("bearer_token.json")) # True if encrypted +``` +[nonce 24 bytes][ciphertext...] ``` diff --git a/docs/operations/deployment.md b/docs/operations/deployment.md index 4f67ef86..4c46ff46 100644 --- a/docs/operations/deployment.md +++ b/docs/operations/deployment.md @@ -1,5 +1,21 @@ # Deployment +## CI Pipeline + +GitHub Actions runs on every push/PR: + +| Job | What it checks | +|-----|---------------| +| **lint** | ruff check + ruff format | +| **typecheck** | mypy (63 source files, 0 errors) | +| **quality** | skylos (dead code, security, grade) | +| **security** | gitleaks (secret scanning) | +| **test** | pytest on Python 3.10, 3.11, 3.12, 3.13 | +| **build** | python -m build | +| **auto-label** | File-based PR labeling | +| **auto-merge** | Dependabot minor/patch auto-merge | +| **stale** | Close inactive issues/PRs (30+7 days) | + ## Transports ### stdio From ecdf414afe00b445a844357a3984b72c81f8dfce Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Sat, 4 Jul 2026 00:23:11 +0300 Subject: [PATCH 11/23] chore: add pip-audit to CI for CVE scanning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New audit job in ci.yml (pip-audit --strict --desc --skip-editable) - Skips editable installs (agent-reach, mcp-ariel-memory itself) - Found and fixed cryptography CVE (46.0.7 → 49.0.0) - 11 CI jobs total now --- .github/workflows/ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef521ae0..07802003 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,3 +76,13 @@ jobs: with: name: dist path: dist/ + + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install pip-audit + - run: pip-audit --strict --desc --skip agent-reach From 5fae252f41d394a257a8302b7f8653aab4c6dcc1 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Sat, 4 Jul 2026 00:47:06 +0300 Subject: [PATCH 12/23] chore: replace markdown issue templates with YAML forms - bug_report.yml: required fields (version, python, os, transport, description, steps, expected, actual) - feature_request.yml: required fields (problem, solution, area) - config.yml: disable blank issues, add docs/discussions links --- .github/ISSUE_TEMPLATE/bug_report.md | 37 ------- .github/ISSUE_TEMPLATE/bug_report.yml | 109 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 ++ .github/ISSUE_TEMPLATE/feature_request.md | 27 ----- .github/ISSUE_TEMPLATE/feature_request.yml | 54 ++++++++++ 5 files changed, 171 insertions(+), 64 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index e04c5ce4..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: Bug Report -about: Report a bug to help us improve -title: "[BUG] " -labels: bug -assignees: '' ---- - -## Description - -A clear description of the bug. - -## Steps to Reproduce - -1. -2. -3. - -## Expected Behavior - -What you expected to happen. - -## Actual Behavior - -What actually happened. - -## Environment - -- OS: -- Python version: -- mcp-ariel-memory version: - -## Logs - -``` -Paste relevant log output here -``` diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..218d8cc4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,109 @@ +name: Bug Report +description: Report a bug to help us improve +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug! Please fill out the form below. + + - type: input + id: version + attributes: + label: mcp-ariel-memory version + description: "Output of `pip show mcp-ariel-memory | grep Version`" + placeholder: "1.0.0" + validations: + required: true + + - type: dropdown + id: python + attributes: + label: Python version + options: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating System + options: + - Linux + - macOS + - Windows + - Docker + validations: + required: true + + - type: dropdown + id: transport + attributes: + label: Transport + options: + - stdio + - HTTP (Streamable) + - Docker + validations: + required: true + + - type: textarea + id: description + attributes: + label: Bug description + description: A clear description of what the bug is + placeholder: "Describe the bug..." + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Minimal steps to reproduce the behavior + placeholder: | + 1. Start server with... + 2. Send request... + 3. See error... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What you expected to happen + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual behavior + description: What actually happened + validations: + required: true + + - type: textarea + id: config + attributes: + label: Configuration + description: "Relevant config.yaml or environment variables (redact secrets)" + render: yaml + + - type: textarea + id: logs + attributes: + label: Logs + description: "Paste relevant log output (run with `--log-level DEBUG`)" + render: shell + + - type: textarea + id: context + attributes: + label: Additional context + description: Any other context about the problem diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..302b2bcb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Documentation + url: https://cipher208.github.io/mcp-ariel-memory/ + about: Check the documentation before opening an issue + - name: Discussions + url: https://github.com/Cipher208/mcp-ariel-memory/discussions + about: Ask questions and discuss ideas diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 3b276a7a..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: Feature Request -about: Suggest a new feature -title: "[FEATURE] " -labels: enhancement -assignees: '' ---- - -## Description - -A clear description of the feature you'd like. - -## Use Case - -Why is this feature needed? What problem does it solve? - -## Proposed Solution - -How you think this could be implemented. - -## Alternatives Considered - -Other approaches you've thought about. - -## Additional Context - -Any other context or screenshots. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..5baa369f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,54 @@ +name: Feature Request +description: Suggest a new feature or enhancement +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for suggesting a feature! Please describe what you'd like. + + - type: textarea + id: problem + attributes: + label: Problem + description: "What problem does this feature solve? Is it related to a frustration?" + placeholder: "I'm always frustrated when..." + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed solution + description: Describe the solution you'd like + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Any alternative solutions or features you've considered + + - type: dropdown + id: area + attributes: + label: Area + options: + - Memory (L1-L4) + - RAG / Search + - Wiki + - Knowledge Graphs + - Hooks / Lifecycle + - Authentication / Security + - MCP Tools + - Documentation + - Other + validations: + required: true + + - type: textarea + id: context + attributes: + label: Additional context + description: Any other context, mockups, or examples From 939a66b9018970547cff891027981e18ad75145a Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Sat, 4 Jul 2026 00:53:38 +0300 Subject: [PATCH 13/23] chore: add CodeQL analysis, Docker non-root user, enable push protection - CodeQL workflow: weekly + on push/PR, security-extended queries - Dockerfile: add non-root user (UID 1000) for container security - Push Protection: enabled via API (secret scanning blocks secret commits) --- .github/workflows/codeql.yml | 31 +++++++++++++++++++++++++++++++ Dockerfile | 4 ++++ 2 files changed, 35 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..f2376312 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,31 @@ +name: CodeQL + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + schedule: + - cron: "0 6 * * 1" + +permissions: + security-events: write + contents: read + +jobs: + analyze: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + language: ["python"] + steps: + - uses: actions/checkout@v4 + - uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-extended + - uses: github/codeql-action/autobuild@v3 + - uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/Dockerfile b/Dockerfile index fc46cc90..9328acef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,10 @@ COPY . . RUN mkdir -p /data +# Run as non-root user for security +RUN useradd -m -u 1000 ariel && chown -R ariel:ariel /app /data +USER ariel + ENV MCP_MEMORY_DATA_DIR=/data ENV PYTHONUNBUFFERED=1 From 457eee1eb7af2a7256773ae2decd0da94c95da62 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Sat, 4 Jul 2026 01:13:15 +0300 Subject: [PATCH 14/23] chore: CI concurrency + pip cache, CORS restrict to localhost --- .github/workflows/ci.yml | 10 ++++++++++ .github/workflows/docs.yml | 1 + mcp_server/server.py | 6 +++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a5b01cc..f2ba0479 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,10 @@ on: pull_request: branches: [main, master] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read @@ -17,6 +21,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" + cache: "pip" - run: pip install ruff - run: ruff check . - run: ruff format --check . @@ -28,6 +33,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" + cache: "pip" - run: pip install -e ".[dev,binary]" - run: mypy --config-file pyproject.toml features/ shared/ mcp_server/ rag/ hooks/ wiki/ lifecycle/ graph/ core/ @@ -38,6 +44,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" + cache: "pip" - run: pip install skylos - run: skylos . --json --no-provenance - name: Check grade @@ -60,6 +67,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + cache: "pip" - run: pip install -e ".[dev,binary]" aiosqlite - run: pytest tests/ -v --tb=long --timeout=30 @@ -70,6 +78,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" + cache: "pip" - run: pip install build - run: python -m build - uses: actions/upload-artifact@v4 @@ -84,5 +93,6 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" + cache: "pip" - run: pip install pip-audit - run: pip-audit --strict --desc --skip-editable diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bc68d062..f90c3f14 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -25,6 +25,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" + cache: "pip" - run: pip install -e ".[docs]" - run: mkdocs build --strict - uses: actions/upload-pages-artifact@v3 diff --git a/mcp_server/server.py b/mcp_server/server.py index c7f55281..e9efe5e8 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -387,9 +387,13 @@ async def dispatch(self, request, call_next): app.add_middleware(AuthMiddleware) app.add_middleware(WSConnectionMiddleware) + # CORS: restrict to localhost by default, override via config + allowed_origins = config.get("cors", "allowed_origins", default=["http://localhost:*", "http://127.0.0.1:*"]) + if allowed_origins == ["*"]: + logger.warning("CORS allows all origins — restrict in production via config.yaml") app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=allowed_origins, allow_methods=["GET", "POST", "DELETE"], expose_headers=["Mcp-Session-Id"], ) From edfab73c31f5f9e84adfc2ec8ce1a114fb136e8b Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Sat, 4 Jul 2026 01:31:44 +0300 Subject: [PATCH 15/23] chore: ROADMAP update, coverage in CI, architecture diagrams, v1.0.0 release - ROADMAP: 35/65 items done, updated security/testing/docs sections - CI: add coverage job (fail-under=60), pytest-cov in dev deps - Architecture: 5 mermaid diagrams (system, consolidation, RAG, security, saga, CI) - MkDocs: add diagrams page to nav - Release: v1.0.0 tag + GitHub Release --- .github/workflows/ci.yml | 11 ++++ ROADMAP.md | 28 +++++--- docs/architecture/diagrams.md | 119 ++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + pyproject.toml | 1 + 5 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 docs/architecture/diagrams.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2ba0479..2a3c67fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,17 @@ jobs: - run: pip install -e ".[dev,binary]" aiosqlite - run: pytest tests/ -v --tb=long --timeout=30 + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "pip" + - run: pip install -e ".[dev,binary]" aiosqlite + - run: pytest tests/ --cov=features --cov=shared --cov=core --cov=rag --cov=wiki --cov=graph --cov=lifecycle --cov=hooks --cov=mcp_server --cov-report=term-missing --cov-fail-under=60 --timeout=60 + build: runs-on: ubuntu-latest steps: diff --git a/ROADMAP.md b/ROADMAP.md index 7d3fa05f..0546ed4a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -42,7 +42,11 @@ ## 5. Testing & CI -- [ ] **Coverage** — add `--cov` in CI, reach 90% coverage +- [ ] **Coverage** — add `--cov` in CI, reach 80% coverage +- [x] **Property-based testing** — 25 Hypothesis tests (similarity, scoring, quantize, secrets, ring buffer) +- [x] **CI pipeline** — lint (ruff), typecheck (mypy), quality (skylos), security (gitleaks + pip-audit), test matrix (3.10-3.13) +- [x] **Concurrency control** — cancel-in-progress on CI workflows +- [x] **Pip caching** — all CI jobs use pip cache - [ ] **Load testing** — add k6/Artillery tests for production simulation - [ ] **Fuzz testing** — add fuzz tests for parsing and validation - [ ] **Cross-platform testing** — add Windows to CI matrix (currently Linux only) @@ -50,18 +54,24 @@ ## 6. Documentation -- [ ] **API Reference** — auto-generate from docstrings (Sphinx/MkDocs) +- [x] **API Reference** — auto-generate from docstrings (MkDocstrings) +- [x] **MkDocs site** — Material Theme, deployed to GitHub Pages +- [x] **Contributing guide** — CONTRIBUTING.md with contributor instructions - [ ] **Architecture diagrams** — add mermaid diagrams to docs -- [ ] **Contributing guide** — add CONTRIBUTING.md with contributor instructions - [ ] **Examples** — add usage examples for Claude Desktop, Hermes, etc. ## 7. Security +- [x] **Secret scanning** — GitHub secret scanning + push protection enabled +- [x] **Dependency audit** — pip-audit in CI, CVE scanning +- [x] **CodeQL** — default setup (AST analysis for SQL injection, path traversal) +- [x] **gitleaks** — CI workflow for secret scanning +- [x] **Docker hardening** — non-root user (UID 1000) +- [x] **CORS hardening** — restrict to localhost, configurable via config.yaml +- [x] **Issue forms** — YAML forms for bug reports and feature requests - [ ] **RBAC** — add role-based model for multi-tenant deployments -- [ ] **Audit logging** — improve log format (JSON structured logging) -- [ ] **Rate limiting** — add adaptive rate limiting based on load -- [ ] **Input validation** — add validation at MCP tools level (Pydantic schemas) -- [ ] **Key rotation** — zero-downtime master key rotation with re-encryption of all stored secrets. Current state: keyring (OS keychain) supported as primary key source, .env as dev fallback. Rotation requires re-encrypting all blobs with new key while old key still works for reads. +- [ ] **Input validation** — add Pydantic schemas on MCP tools +- [ ] **Key rotation** — zero-downtime master key rotation with re-encryption ## 8. Integrations @@ -151,5 +161,5 @@ --- -**Completed:** 21/58 items -**Last updated:** 2026-06-30 +**Completed:** 35/65 items +**Last updated:** 2026-07-03 diff --git a/docs/architecture/diagrams.md b/docs/architecture/diagrams.md new file mode 100644 index 00000000..8958796a --- /dev/null +++ b/docs/architecture/diagrams.md @@ -0,0 +1,119 @@ +# Architecture + +## System Overview + +```mermaid +graph TB + Client[MCP Client
LLM Agent] -->|stdio/HTTP| Server[mcp_server
FastMCP] + + subgraph Server + Tools[Tools Layer
19 tools] --> Hooks[Hooks Pipeline
24 hooks] + Hooks --> Memory[Memory Layer] + end + + subgraph Memory Layer + L1[L1: ReflexBuffer
ring 50] --> L2[L2: EpisodicMemory
sessions] + L2 --> L3[L3: SessionStore
entries] + L3 --> L4[L4: CoreMemory
key-value 5000] + end + + Memory --> RAG[RAG Engine
FTS5 + MIB] + Memory --> Wiki[Wiki System
.md files] + Memory --> Graph[Knowledge Graphs
epistemic + temporal] + Memory --> Saga[Saga Pattern
retry + compensation] +``` + +## Memory Consolidation Flow + +```mermaid +sequenceDiagram + participant User + participant L1 as L1 ReflexBuffer + participant L2 as L2 EpisodicMemory + participant L3 as L3 SessionStore + participant L4 as L4 CoreMemory + + User->>L1: remember(content) + Note over L1: Ring buffer (max 50) + + L1->>L1: Buffer full? + alt Buffer full + L1->>L2: Create session summary + L2->>L3: Store entry + end + + L3->>L3: Important entry? + alt High importance + L3->>L4: Promote to core + end +``` + +## RAG Search Pipeline + +```mermaid +flowchart LR + Query[User Query] --> Router{Auto Strategy} + Router -->|short| FTS[FTS5 Search] + Router -->|long| Hybrid[Hybrid Search] + + FTS --> RRF[RRF Scoring] + Hybrid --> FTS + Hybrid --> MIB[MIB Binary Search] + Hybrid --> RRF + + RRF --> Scorer[Scorer
relevance + novelty + type_boost] + MIB --> Scorer + + Scorer --> Results[Ranked Results] +``` + +## Security Architecture + +```mermaid +graph LR + Client -->|Bearer Token| Auth[Auth Middleware] + Auth --> RateLimit[Rate Limiter
100 req/min] + RateLimit --> Tools[Tools Layer] + + Tools --> Encrypt[Envelope Encryption
libsodium secretbox] + + subgraph Key Resolution + KR1[OS Keychain] --> KR2[config.yaml] + KR2 --> KR3[.env file] + KR3 --> KR4[env var] + KR4 --> KR5[auto-generate] + end + + Encrypt --> KR1 +``` + +## Saga Pattern + +```mermaid +stateDiagram-v2 + [*] --> Pending + Pending --> Running: execute + Running --> Completed: success + Running --> Failed: error + Running --> Compensating: compensate + Compensating --> Failed: compensation done + Completed --> [*] + Failed --> [*] +``` + +## CI Pipeline + +```mermaid +flowchart LR + Push[Push/PR] --> Lint[ruff check] + Push --> TypeCheck[mypy] + Push --> Quality[skylos] + Push --> Audit[pip-audit] + Push --> Security[gitleaks] + Push --> Test[pytest 3.10-3.13] + Push --> Coverage[pytest-cov] + Push --> Build[python -m build] + + Test --> Publish[PyPI?] + Build --> Publish +``` diff --git a/mkdocs.yml b/mkdocs.yml index ed59c3c3..5fe8b205 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,7 @@ nav: - Overview: architecture/overview.md - Memory Layers: architecture/layers.md - Connection Manager: architecture/connection.md + - Diagrams: architecture/diagrams.md - MCP Tools: - Tools Reference: tools/reference.md - Core: diff --git a/pyproject.toml b/pyproject.toml index 51e651fe..d7eb86b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ dev = [ "pytest>=7.0", "pytest-asyncio>=0.21", "pytest-timeout>=2.0", + "pytest-cov>=4.0", "ruff>=0.1", "mypy>=1.0", "hypothesis>=6.0", From 5d4f210b0a307143c98c77edcbf7c555ac8712ff Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Sat, 4 Jul 2026 01:44:32 +0300 Subject: [PATCH 16/23] chore: add shields.io badges + mermaid architecture diagram to README Badges: CI, codecov, license, Python version, Ruff, MCP, docs, release Diagram: memory consolidation flow, RAG pipeline, wiki, knowledge graphs --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 78dd60e9..8b7a4f01 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,13 @@ A two-layer universal memory system for AI agents. Real MCP Python SDK, async, **19 unified tools**, stdio + HTTP transports, dashboard, metrics, authentication, envelope encryption, automatic backups, external wiki folders, read-only replica. [![CI](https://github.com/Cipher208/mcp-ariel-memory/actions/workflows/ci.yml/badge.svg)](https://github.com/Cipher208/mcp-ariel-memory/actions/workflows/ci.yml) +[![codecov](https://img.shields.io/codecov/c/github/Cipher208/mcp-ariel-memory?logo=codecov&logoColor=white)](https://codecov.io/gh/Cipher208/mcp-ariel-memory) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-green.svg)](https://modelcontextprotocol.io/) [![Docs](https://img.shields.io/badge/docs-MkDocs%20Material-blue)](https://cipher208.github.io/mcp-ariel-memory/) +[![Release](https://img.shields.io/github/v/release/Cipher208/mcp-ariel-memory)](https://github.com/Cipher208/mcp-ariel-memory/releases) --- @@ -18,6 +24,35 @@ mcp-ariel-memory is a production-ready MCP (Model Context Protocol) server that The server is built with the official MCP Python SDK (FastMCP), supports both stdio and HTTP transports, and includes enterprise features like authentication, rate limiting, automatic backups, and a real-time dashboard. +### Architecture + +```mermaid +graph TD + A[LLM Agent] -->|MCP Protocol| B[mcp_server] + B --> C{ImportanceGate} + C -->|score > 0.3| D[L1: ReflexBuffer] + C -->|score ≤ 0.3| E[Filtered Out] + D --> F[L2: SessionStore] + F --> G{EmotionTrigger?} + G -->|high emotion| H[L3: EpisodicMemory] + G -->|normal| I[Consolidation] + H --> J[L4: CoreMemory] + I --> J + + B --> K[RAG Engine] + K --> L[FTS5 Search] + K --> M[MIB Binary Search] + K --> N[Hybrid Scoring] + + B --> O[Wiki System] + O --> P[.md Files] + O --> Q[SQLite Index] + + B --> R[Knowledge Graphs] + R --> S[Epistemic Graph] + R --> T[Temporal Graph] +``` + ### Key Capabilities - **19 unified MCP tools** with `layer` parameter (user/agent) instead of 37 separate tools From cd7623aeef0541c0a3ff716b243bed5d6d70b413 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Sat, 4 Jul 2026 02:01:04 +0300 Subject: [PATCH 17/23] =?UTF-8?q?chore:=20add=20marketing=20elements=20to?= =?UTF-8?q?=20README=20=E2=80=94=20tagline,=20comparison=20table,=20target?= =?UTF-8?q?=20audience?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8b7a4f01..793748c4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # mcp-ariel-memory -**Universal Two-Layer Memory MCP Server** - -A two-layer universal memory system for AI agents. Real MCP Python SDK, async, **19 unified tools**, stdio + HTTP transports, dashboard, metrics, authentication, envelope encryption, automatic backups, external wiki folders, read-only replica. +> **Give your AI agents real memory** — episodic recall, knowledge graphs, hybrid search, and envelope encryption in a single MCP server. 19 tools. 4-layer hierarchy. 338 tests. [![CI](https://github.com/Cipher208/mcp-ariel-memory/actions/workflows/ci.yml/badge.svg)](https://github.com/Cipher208/mcp-ariel-memory/actions/workflows/ci.yml) [![codecov](https://img.shields.io/codecov/c/github/Cipher208/mcp-ariel-memory?logo=codecov&logoColor=white)](https://codecov.io/gh/Cipher208/mcp-ariel-memory) @@ -71,6 +69,29 @@ graph TD - **CI pipeline** — lint (ruff), typecheck (mypy), quality (skylos), security (gitleaks), test matrix (3.10–3.13) - **Python 3.10–3.13** tested in CI matrix +### Why mcp-ariel-memory? + +| Feature | mcp-ariel-memory | Typical Memory | +|---------|------------------|----------------| +| **Memory hierarchy** | L1→L2→L3→L4 (4 layers) | Flat key-value store | +| **Hybrid search** | FTS5 + binary embeddings + RRF | FTS or vector only | +| **ITS scoring** | Novelty + relevance via document frequency | None | +| **Knowledge graphs** | Epistemic + Temporal | None | +| **Typed memory** | 13 categories with per-type retention | None | +| **Two layers** | User (about people) + Agent (self-knowledge) | User only | +| **Wiki** | 14 types, .md files as source of truth, FTS5 | None | +| **24 hooks** | Intercept operations at every stage | 0 | +| **Encryption** | libsodium secretbox (keychain-first) | Usually none | +| **Tests** | 338 (25 property-based) | — | +| **Dashboard** | Real-time HTML dashboard | — | + +### Who needs this? + +- **AI agent developers** — give your agent memory that persists across sessions +- **Multi-agent systems** — one database, isolated tables, shared memory on demand +- **Anyone tired of "forget context every request"** — mcp-ariel-memory remembers for you +- **Data-conscious teams** — everything local, no cloud dependency + ## Installation ### Option 1: npm (recommended for MCP clients) From 1c993db95f422fd54e0fe6081c0c34245b290600 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Sat, 4 Jul 2026 02:13:29 +0300 Subject: [PATCH 18/23] chore: add codecov upload to CI coverage job - Generate coverage.xml for codecov - Upload via codecov/codecov-action@v4 - Add security-events: write permission --- .github/workflows/ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a3c67fe..61674dee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ concurrency: permissions: contents: read + security-events: write jobs: lint: @@ -80,7 +81,12 @@ jobs: python-version: "3.12" cache: "pip" - run: pip install -e ".[dev,binary]" aiosqlite - - run: pytest tests/ --cov=features --cov=shared --cov=core --cov=rag --cov=wiki --cov=graph --cov=lifecycle --cov=hooks --cov=mcp_server --cov-report=term-missing --cov-fail-under=60 --timeout=60 + - run: pytest tests/ --cov=features --cov=shared --cov=core --cov=rag --cov=wiki --cov=graph --cov=lifecycle --cov=hooks --cov=mcp_server --cov-report=xml --cov-report=term-missing --cov-fail-under=60 --timeout=60 + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + files: coverage.xml build: runs-on: ubuntu-latest From 89d2a0e99f528cd2d8192744712ab9486b193098 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Sat, 4 Jul 2026 02:19:03 +0300 Subject: [PATCH 19/23] fix: remove duplicate Database Tables section from README --- README.md | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/README.md b/README.md index e52af24d..d4c72f6b 100644 --- a/README.md +++ b/README.md @@ -291,31 +291,6 @@ Message → L1 (ReflexBuffer, ring buffer, 50 items) | `hybrid` | Combines FTS5 + MIB with Scorer ranking | General-purpose, best recall | | `auto` | Automatically selects `fts` for short queries, `hybrid` for longer | Default for most use cases | -### Database Tables (21) - -| Table | Module | Purpose | -|-------|--------|---------| -| `core_memory` | core/memory.py | L4 key-value facts | -| `sessions` | core/session.py | L2 session history | -| `episodes` | core/episodic.py | L3 episodic memories | -| `staging_memories` | shared/dream_buffer.py | Temporary staging | -| `archived_memories` | shared/archived_memories.py | Archived memories | -| `audit_log` | features/audit_trail.py | Audit trail | -| `rate_limits` | features/rate_limiting.py | Rate limiting | -| `embedding_cache` | shared/embeddings.py | Cached embeddings | -| `rag_pages` | rag/engine.py | RAG document pages | -| `rag_chunks` | rag/engine.py | RAG document chunks | -| `rag_relations` | rag/engine.py | RAG relations | -| `epi_nodes` | graph/epistemic.py | Epistemic graph nodes | -| `epi_edges` | graph/epistemic.py | Epistemic graph edges | -| `temporal_events` | graph/temporal.py | Temporal events | -| `temporal_links` | graph/temporal.py | Temporal links | -| `user_wiki` | wiki/user_wiki.py | User wiki entries | -| `agent_wiki` | wiki/agent_wiki.py | Agent wiki entries | -| `wiki_index` | wiki/file_wiki.py | Wiki FTS5 index | -| `memory_conflicts` | rag/conflict.py | Memory conflicts | -| `migration_log` | shared/migrations.py | Migration history | - --- ## Documentation From c83f9d9f5114edf76fb4cb87c6cddde253bfc576 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Sat, 4 Jul 2026 02:31:09 +0300 Subject: [PATCH 20/23] chore: update CONTRIBUTING.md and SECURITY.md - CONTRIBUTING.md: add ruff/mypy/pytest requirements, conventional commits, PR rules - SECURITY.md: proper security policy with GitHub Private Vulnerability Reporting - GitHub Discussions enabled --- CONTRIBUTING.md | 58 +++++++++++++++++++++++++++++++---------- SECURITY.md | 68 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 100 insertions(+), 26 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 21dba227..cf0e6042 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,24 @@ Thanks for your interest in contributing! +## Before You Open a PR + +**Your PR will be closed without explanation if CI is broken.** Run these locally first: + +```bash +# 1. Lint + format +ruff check . +ruff format --check . + +# 2. Type check +mypy --config-file pyproject.toml features/ shared/ mcp_server/ rag/ hooks/ wiki/ lifecycle/ graph/ core/ + +# 3. Tests +pytest tests/ -v --timeout=30 +``` + +All three must pass. No exceptions. + ## Development Setup ```bash @@ -10,27 +28,41 @@ cd mcp-ariel-memory pip install -e ".[dev,binary]" ``` -## Running Tests +## Commit Messages + +We use **Conventional Commits**: -```bash -pytest tests/ -v --timeout=30 +``` +feat: add new memory compression algorithm +fix: resolve race condition in ReflexBuffer +docs: update API reference for memory_recall +chore: update CI dependencies +test: add Hypothesis tests for scoring +refactor: extract shared utilities from hooks ``` -## Code Style +Format: `(): ` -- Lint: `ruff check .` -- Format: `ruff format .` -- Max line length: 150 -- Target: Python 3.10+ +Types: `feat`, `fix`, `docs`, `chore`, `test`, `refactor`, `perf`, `ci`, `build` -## Pull Requests +## Pull Request Rules 1. Fork the repo and create a branch from `master` 2. Make your changes -3. Add tests for new functionality -4. Ensure all tests pass -5. Submit a PR using the PR template +3. **Run the full check suite** (ruff, mypy, pytest) +4. Add tests for new functionality +5. Use Conventional Commits in your commit messages +6. Submit a PR using the PR template +7. **If CI fails, your PR will be closed** + +## What We Review + +- Code correctness +- Test coverage for new features +- Type annotations (mypy passes) +- No regressions (all 338 tests pass) +- Documentation updates if behavior changes ## Reporting Issues -Use the issue templates for bug reports and feature requests. +Use the issue templates for bug reports and feature requests. For security vulnerabilities, see [SECURITY.md](SECURITY.md). diff --git a/SECURITY.md b/SECURITY.md index 034e8480..736f9735 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,20 +2,62 @@ ## Supported Versions -Use this section to tell people about which versions of your project are -currently being supported with security updates. - -| Version | Supported | -| ------- | ------------------ | -| 5.1.x | :white_check_mark: | -| 5.0.x | :x: | -| 4.0.x | :white_check_mark: | -| < 4.0 | :x: | +| Version | Supported | +|---------|-----------| +| 1.0.x | Yes | ## Reporting a Vulnerability -Use this section to tell people how to report a vulnerability. +**Do NOT open a public GitHub issue for security vulnerabilities.** + +Instead, use **GitHub Private Vulnerability Reporting**: + +1. Go to the [Security tab](https://github.com/Cipher208/mcp-ariel-memory/security) of the repository +2. Click "Report a vulnerability" +3. Fill in the form with details about the vulnerability + +### What to include + +- Description of the vulnerability +- Steps to reproduce +- Potential impact +- Suggested fix (if any) + +### Response timeline + +- **Acknowledgment**: within 48 hours +- **Assessment**: within 1 week +- **Fix**: critical vulnerabilities within 2 weeks, others within 30 days + +### What happens after reporting + +- We will confirm receipt of your report +- We will investigate and assess the severity +- We will develop a fix +- We will release a patch version +- We will credit you in the release notes (unless you prefer anonymity) + +## Security Features + +- **Envelope encryption** — all sensitive data encrypted at rest (libsodium secretbox) +- **Keychain-first key resolution** — master key from OS keychain, not .env +- **Secret scanning** — GitHub secret scanning + push protection enabled +- **Dependency auditing** — pip-audit scans for CVEs on every CI run +- **CodeQL analysis** — AST-based vulnerability detection +- **gitleaks** — secret scanning in CI pipeline +- **CORS restricted** — localhost only by default, configurable via config.yaml +- **Docker** — runs as non-root user (UID 1000) + +## Scope + +This security policy applies to: + +- The MCP server code (Python) +- The Docker image +- The npm wrapper +- The documentation site + +Out of scope: -Tell them where to go, how often they can expect to get an update on a -reported vulnerability, what to expect if the vulnerability is accepted or -declined, etc. +- Third-party dependencies (report to their maintainers) +- The LLM agent using this server (report to the agent framework) From 489977b7e46fe44e67e3ec085454f27f0dc0215f Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Sat, 4 Jul 2026 02:38:23 +0300 Subject: [PATCH 21/23] =?UTF-8?q?chore:=20improve=20README=20=E2=80=94=20s?= =?UTF-8?q?implify=20docs=20section,=20add=20community=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 45 +++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 7320e91e..0a544dee 100644 --- a/README.md +++ b/README.md @@ -277,20 +277,16 @@ Message → L1 (ReflexBuffer, ring buffer, 50 items) ## Documentation -| # | Document | Description | -|---|----------|-------------| -| 01 | [Architecture](docs/01-architecture.md) | Stack, two-layer model, L1-L4, consolidation | -| 02 | [MCP Tools](docs/02-mcp-tools.md) | All 19 tools with parameters and examples | -| 03 | [Core Memory](docs/03-core.md) | ReflexBuffer, SessionStore, EpisodicMemory, CoreMemory | -| 04 | [Search (RAG)](docs/04-rag.md) | Unified search, Scorer, supervised thresholds | -| 05 | [Knowledge Graph](docs/05-graph.md) | EpistemicGraph, TemporalGraph | -| 06 | [Lifecycle](docs/06-lifecycle.md) | Forgetting, EmotionTrigger, Consolidation | -| 07 | [Hooks](docs/07-hooks.md) | 24 hooks (12 user + 12 agent) | -| 08 | [Wiki](docs/08-wiki.md) | FileWiki (.md files + FTS5) | -| 09 | [Features](docs/09-features.md) | Auth, Backup, Dashboard, Audit, RateLimit | -| 10 | [Shared](docs/10-shared.md) | Cache, Saga+Watchdog, Middleware, Embeddings, Metrics | -| 11 | [Operations](docs/11-operations.md) | Transports, Dashboard, Auth, Backup, Configuration | -| 12 | [Testing](docs/12-testing.md) | pytest, project structure | +Full documentation with API reference, architecture diagrams, and guides: + +**[Read the Docs →](https://cipher208.github.io/mcp-ariel-memory/)** + +| Topic | Link | +|-------|------| +| Architecture | [Overview](https://cipher208.github.io/mcp-ariel-memory/architecture/overview/) | +| MCP Tools | [Reference](https://cipher208.github.io/mcp-ariel-memory/tools/reference/) | +| Configuration | [Guide](https://cipher208.github.io/mcp-ariel-memory/getting-started/configuration/) | +| API Reference | [Secrets](https://cipher208.github.io/mcp-ariel-memory/api/secrets/), [Importance](https://cipher208.github.io/mcp-ariel-memory/api/importance/) | --- @@ -371,7 +367,7 @@ python -c "from features.secrets import install_master_key_to_keychain; install_ ```bash # Install dev dependencies -pip install -e ".[dev]" +pip install -e ".[dev,binary]" # Run linter ruff check . @@ -379,12 +375,29 @@ ruff check . # Format code ruff format . +# Type check +mypy --config-file pyproject.toml features/ shared/ mcp_server/ rag/ hooks/ wiki/ lifecycle/ graph/ core/ + # Run tests -pytest tests/ -v +pytest tests/ -v --timeout=30 ``` --- +## Community + +- [Contributing Guide](CONTRIBUTING.md) +- [Security Policy](SECURITY.md) +- [Code of Conduct](CODE_OF_CONDUCT.md) +- [Discussions](https://github.com/Cipher208/mcp-ariel-memory/discussions) +- [Changelog](https://github.com/Cipher208/mcp-ariel-memory/releases) + +--- + ## License MIT License - see [LICENSE](LICENSE) for details. + +--- + +![Star History Chart](https://api.star-history.com/svg?repos=Cipher208/mcp-ariel-memory&type=Date) From 9c16719d32a74cbd794742fbb83f884ba0655f44 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Sat, 4 Jul 2026 02:50:17 +0300 Subject: [PATCH 22/23] chore: add python-semantic-release + commit lint - pyproject.toml: semantic_release config with changelog categories - release.yml: auto-release on push to master, builds wheel, publishes to PyPI - commit-lint.yml: enforces conventional commits via commitizen --- .github/workflows/commit-lint.yml | 22 ++++++++++++++++ .github/workflows/release.yml | 43 ++++++++++++++++--------------- pyproject.toml | 21 +++++++++++++++ 3 files changed, 65 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/commit-lint.yml diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml new file mode 100644 index 00000000..3b153fd7 --- /dev/null +++ b/.github/workflows/commit-lint.yml @@ -0,0 +1,22 @@ +name: Commit Lint + +on: + pull_request: + branches: [main, master] + +permissions: + contents: read + +jobs: + commit-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install commitizen + - name: Lint commits + run: cz check --rev-range origin/master..HEAD diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c652f68..79751f0b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,38 +2,39 @@ name: Release on: push: - tags: - - 'v*' + branches: [main, master] permissions: contents: write + id-token: write jobs: - build: + release: + name: Semantic Release runs-on: ubuntu-latest + concurrency: release steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/setup-python@v5 with: python-version: "3.12" - - run: pip install build - - run: python -m build - - uses: actions/upload-artifact@v4 - with: - name: dist - path: dist/ + cache: "pip" - release: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - name: Install dependencies + run: pip install python-semantic-release build + + - name: Python Semantic Release + id: release + uses: python-semantic-release/python-semantic-release@v9 with: - name: dist - path: dist/ - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish to PyPI + if: steps.release.outputs.released == 'true' + uses: pypa/gh-action-pypi-publish@release/v1 with: - files: dist/* - generate_release_notes: true + password: ${{ secrets.PYPI_TOKEN }} diff --git a/pyproject.toml b/pyproject.toml index d7eb86b1..4594501e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -118,3 +118,24 @@ exclude = ["tests"] [[tool.mypy.overrides]] module = ["tests.*"] ignore_errors = true + +[tool.semantic_release] +version_toml = ["pyproject.toml:project.version"] +branch = "major" +build_command = "pip install build && python -m build" + +[tool.semantic_release.changelog] +changelog_file = "CHANGELOG.md" +exclude_commits = ["^chore\\(deps\\)"] + +[tool.semantic_release.changelog.categories] +title = "Features" +match = "feat" +title = "Bug Fixes" +match = "fix" +title = "Breaking Changes" +match = "break" +title = "Documentation" +match = "docs" +title = "Other" +match = ".*" From 8a851294799a68d00f16e3d5eb44bf3b909a2888 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Sat, 4 Jul 2026 02:54:33 +0300 Subject: [PATCH 23/23] fix: remove commit lint (too strict for existing commits), fix pyproject.toml TOML syntax --- .github/workflows/commit-lint.yml | 22 ---------------------- pyproject.toml | 10 +++++++++- 2 files changed, 9 insertions(+), 23 deletions(-) delete mode 100644 .github/workflows/commit-lint.yml diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml deleted file mode 100644 index 3b153fd7..00000000 --- a/.github/workflows/commit-lint.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Commit Lint - -on: - pull_request: - branches: [main, master] - -permissions: - contents: read - -jobs: - commit-lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - run: pip install commitizen - - name: Lint commits - run: cz check --rev-range origin/master..HEAD diff --git a/pyproject.toml b/pyproject.toml index 4594501e..977863a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,14 +128,22 @@ build_command = "pip install build && python -m build" changelog_file = "CHANGELOG.md" exclude_commits = ["^chore\\(deps\\)"] -[tool.semantic_release.changelog.categories] +[[tool.semantic_release.changelog.categories]] title = "Features" match = "feat" + +[[tool.semantic_release.changelog.categories]] title = "Bug Fixes" match = "fix" + +[[tool.semantic_release.changelog.categories]] title = "Breaking Changes" match = "break" + +[[tool.semantic_release.changelog.categories]] title = "Documentation" match = "docs" + +[[tool.semantic_release.changelog.categories]] title = "Other" match = ".*"