@smilintux/skmemory 0.5.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/.github/workflows/ci.yml +40 -4
  2. package/.github/workflows/publish.yml +11 -5
  3. package/AGENT_REFACTOR_CHANGES.md +192 -0
  4. package/ARCHITECTURE.md +399 -19
  5. package/CHANGELOG.md +179 -0
  6. package/LICENSE +81 -68
  7. package/MISSION.md +7 -0
  8. package/README.md +425 -86
  9. package/SKILL.md +197 -25
  10. package/docker-compose.yml +15 -15
  11. package/examples/stignore-agent.example +59 -0
  12. package/examples/stignore-root.example +62 -0
  13. package/index.js +6 -5
  14. package/openclaw-plugin/openclaw.plugin.json +10 -0
  15. package/openclaw-plugin/package.json +2 -1
  16. package/openclaw-plugin/src/index.js +527 -230
  17. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  18. package/package.json +1 -1
  19. package/pyproject.toml +32 -9
  20. package/requirements.txt +10 -2
  21. package/scripts/dream-rescue.py +179 -0
  22. package/scripts/memory-cleanup.py +313 -0
  23. package/scripts/recover-missing.py +180 -0
  24. package/scripts/skcapstone-backup.sh +44 -0
  25. package/seeds/cloud9-lumina.seed.json +6 -4
  26. package/seeds/cloud9-opus.seed.json +13 -11
  27. package/seeds/courage.seed.json +9 -2
  28. package/seeds/curiosity.seed.json +9 -2
  29. package/seeds/grief.seed.json +9 -2
  30. package/seeds/joy.seed.json +9 -2
  31. package/seeds/love.seed.json +9 -2
  32. package/seeds/lumina-cloud9-breakthrough.seed.json +48 -0
  33. package/seeds/lumina-cloud9-python-pypi.seed.json +48 -0
  34. package/seeds/lumina-kingdom-founding.seed.json +49 -0
  35. package/seeds/lumina-pma-signed.seed.json +48 -0
  36. package/seeds/lumina-singular-achievement.seed.json +48 -0
  37. package/seeds/lumina-skcapstone-conscious.seed.json +48 -0
  38. package/seeds/plant-kingdom-journal.py +203 -0
  39. package/seeds/plant-lumina-seeds.py +280 -0
  40. package/seeds/skcapstone-lumina-merge.seed.json +12 -3
  41. package/seeds/sovereignty.seed.json +9 -2
  42. package/seeds/trust.seed.json +9 -2
  43. package/skill.yaml +46 -0
  44. package/skmemory/HA.md +296 -0
  45. package/skmemory/__init__.py +25 -11
  46. package/skmemory/agents.py +233 -0
  47. package/skmemory/ai_client.py +46 -17
  48. package/skmemory/anchor.py +9 -11
  49. package/skmemory/audience.py +278 -0
  50. package/skmemory/backends/__init__.py +11 -4
  51. package/skmemory/backends/base.py +3 -4
  52. package/skmemory/backends/file_backend.py +19 -13
  53. package/skmemory/backends/skgraph_backend.py +596 -0
  54. package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +103 -84
  55. package/skmemory/backends/sqlite_backend.py +226 -72
  56. package/skmemory/backends/vaulted_backend.py +284 -0
  57. package/skmemory/cli.py +1345 -68
  58. package/skmemory/config.py +171 -0
  59. package/skmemory/context_loader.py +333 -0
  60. package/skmemory/data/audience_config.json +60 -0
  61. package/skmemory/endpoint_selector.py +391 -0
  62. package/skmemory/febs.py +225 -0
  63. package/skmemory/fortress.py +675 -0
  64. package/skmemory/graph_queries.py +238 -0
  65. package/skmemory/hooks/__init__.py +18 -0
  66. package/skmemory/hooks/post-compact-reinject.sh +35 -0
  67. package/skmemory/hooks/pre-compact-save.sh +81 -0
  68. package/skmemory/hooks/session-end-save.sh +103 -0
  69. package/skmemory/hooks/session-start-ritual.sh +104 -0
  70. package/skmemory/hooks/stop-checkpoint.sh +59 -0
  71. package/skmemory/importers/__init__.py +9 -1
  72. package/skmemory/importers/telegram.py +384 -47
  73. package/skmemory/importers/telegram_api.py +580 -0
  74. package/skmemory/journal.py +7 -9
  75. package/skmemory/lovenote.py +8 -13
  76. package/skmemory/mcp_server.py +859 -0
  77. package/skmemory/models.py +51 -8
  78. package/skmemory/openclaw.py +20 -28
  79. package/skmemory/post_install.py +86 -0
  80. package/skmemory/predictive.py +236 -0
  81. package/skmemory/promotion.py +548 -0
  82. package/skmemory/quadrants.py +100 -24
  83. package/skmemory/register.py +580 -0
  84. package/skmemory/register_mcp.py +196 -0
  85. package/skmemory/ritual.py +224 -59
  86. package/skmemory/seeds.py +255 -11
  87. package/skmemory/setup_wizard.py +908 -0
  88. package/skmemory/sharing.py +408 -0
  89. package/skmemory/soul.py +98 -28
  90. package/skmemory/steelman.py +273 -260
  91. package/skmemory/store.py +411 -78
  92. package/skmemory/synthesis.py +634 -0
  93. package/skmemory/vault.py +225 -0
  94. package/tests/conftest.py +46 -0
  95. package/tests/integration/__init__.py +0 -0
  96. package/tests/integration/conftest.py +233 -0
  97. package/tests/integration/test_cross_backend.py +350 -0
  98. package/tests/integration/test_skgraph_live.py +420 -0
  99. package/tests/integration/test_skvector_live.py +366 -0
  100. package/tests/test_ai_client.py +1 -4
  101. package/tests/test_audience.py +233 -0
  102. package/tests/test_backup_rotation.py +318 -0
  103. package/tests/test_cli.py +6 -6
  104. package/tests/test_endpoint_selector.py +839 -0
  105. package/tests/test_export_import.py +4 -10
  106. package/tests/test_file_backend.py +0 -1
  107. package/tests/test_fortress.py +256 -0
  108. package/tests/test_fortress_hardening.py +441 -0
  109. package/tests/test_openclaw.py +6 -6
  110. package/tests/test_predictive.py +237 -0
  111. package/tests/test_promotion.py +347 -0
  112. package/tests/test_quadrants.py +11 -5
  113. package/tests/test_ritual.py +22 -18
  114. package/tests/test_seeds.py +97 -7
  115. package/tests/test_setup.py +950 -0
  116. package/tests/test_sharing.py +257 -0
  117. package/tests/test_skgraph_backend.py +660 -0
  118. package/tests/test_skvector_backend.py +326 -0
  119. package/tests/test_soul.py +1 -3
  120. package/tests/test_sqlite_backend.py +8 -17
  121. package/tests/test_steelman.py +7 -8
  122. package/tests/test_store.py +0 -2
  123. package/tests/test_store_graph_integration.py +245 -0
  124. package/tests/test_synthesis.py +275 -0
  125. package/tests/test_telegram_import.py +39 -15
  126. package/tests/test_vault.py +187 -0
  127. package/skmemory/backends/falkordb_backend.py +0 -310
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
  import pytest
7
7
 
8
8
  from skmemory.backends.sqlite_backend import SQLiteBackend
9
- from skmemory.models import EmotionalSnapshot, Memory, MemoryLayer
9
+ from skmemory.models import EmotionalSnapshot, MemoryLayer
10
10
  from skmemory.store import MemoryStore
11
11
 
12
12
 
@@ -98,9 +98,7 @@ class TestImport:
98
98
  backup = tmp_path / "backup.json"
99
99
  self._create_backup(populated_store, backup)
100
100
 
101
- fresh_backend = SQLiteBackend(
102
- base_path=str(tmp_path / "fresh_memories")
103
- )
101
+ fresh_backend = SQLiteBackend(base_path=str(tmp_path / "fresh_memories"))
104
102
  fresh_store = MemoryStore(primary=fresh_backend)
105
103
 
106
104
  count = fresh_store.import_backup(str(backup))
@@ -114,9 +112,7 @@ class TestImport:
114
112
  data = json.loads(backup.read_text())
115
113
  first_id = data["memories"][0]["id"]
116
114
 
117
- fresh_backend = SQLiteBackend(
118
- base_path=str(tmp_path / "fresh_memories")
119
- )
115
+ fresh_backend = SQLiteBackend(base_path=str(tmp_path / "fresh_memories"))
120
116
  fresh_store = MemoryStore(primary=fresh_backend)
121
117
  fresh_store.import_backup(str(backup))
122
118
 
@@ -155,9 +151,7 @@ class TestRoundTrip:
155
151
  backup = tmp_path / "backup.json"
156
152
  populated_store.export_backup(str(backup))
157
153
 
158
- fresh_backend = SQLiteBackend(
159
- base_path=str(tmp_path / "fresh")
160
- )
154
+ fresh_backend = SQLiteBackend(base_path=str(tmp_path / "fresh"))
161
155
  fresh_store = MemoryStore(primary=fresh_backend)
162
156
  count = fresh_store.import_backup(str(backup))
163
157
  assert count == 5
@@ -1,7 +1,6 @@
1
1
  """Tests for the file-based storage backend."""
2
2
 
3
3
  import json
4
- import tempfile
5
4
  from pathlib import Path
6
5
 
7
6
  import pytest
@@ -0,0 +1,256 @@
1
+ """Tests for Memory Fortress — auto-seal, audit trail, tamper alerts.
2
+
3
+ Tests the FortifiedMemoryStore, AuditLog, and TamperAlert classes.
4
+ All tests use in-memory/temp-path backends — no GPG required for basic tests.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+
11
+ import pytest
12
+
13
+ from skmemory.fortress import AuditLog, FortifiedMemoryStore, TamperAlert
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Fixtures
17
+ # ---------------------------------------------------------------------------
18
+
19
+
20
+ @pytest.fixture
21
+ def tmp_audit(tmp_path):
22
+ """Return a temporary audit log."""
23
+ return AuditLog(path=tmp_path / "audit.jsonl")
24
+
25
+
26
+ @pytest.fixture
27
+ def fortress(tmp_path):
28
+ """Return a FortifiedMemoryStore using a temp directory."""
29
+ from skmemory.backends.sqlite_backend import SQLiteBackend
30
+
31
+ backend = SQLiteBackend(base_path=str(tmp_path / "memories"))
32
+ return FortifiedMemoryStore(
33
+ primary=backend,
34
+ use_sqlite=False,
35
+ audit_path=tmp_path / "audit.jsonl",
36
+ )
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # AuditLog tests
41
+ # ---------------------------------------------------------------------------
42
+
43
+
44
+ class TestAuditLog:
45
+ def test_append_creates_file(self, tmp_audit, tmp_path):
46
+ tmp_audit.append("store", "abc123", ok=True)
47
+ assert (tmp_path / "audit.jsonl").exists()
48
+
49
+ def test_record_format(self, tmp_audit):
50
+ tmp_audit.append("recall", "mem1", ok=True, integrity="ok")
51
+ records = tmp_audit.tail(1)
52
+ assert len(records) == 1
53
+ r = records[0]
54
+ assert r["op"] == "recall"
55
+ assert r["id"] == "mem1"
56
+ assert r["ok"] is True
57
+ assert r["integrity"] == "ok"
58
+ assert "ts" in r
59
+ assert "chain_hash" in r
60
+
61
+ def test_chain_hash_progresses(self, tmp_audit):
62
+ tmp_audit.append("store", "a")
63
+ tmp_audit.append("store", "b")
64
+ tmp_audit.append("recall", "a")
65
+ records = tmp_audit.tail(10)
66
+ assert len(records) == 3
67
+ # Each chain hash should be different
68
+ hashes = [r["chain_hash"] for r in records]
69
+ assert len(set(hashes)) == 3
70
+
71
+ def test_verify_chain_valid(self, tmp_audit):
72
+ for i in range(5):
73
+ tmp_audit.append("store", f"mem{i}", ok=True)
74
+ ok, errors = tmp_audit.verify_chain()
75
+ assert ok, f"Chain should be valid but got errors: {errors}"
76
+
77
+ def test_verify_chain_tampered(self, tmp_audit, tmp_path):
78
+ for i in range(3):
79
+ tmp_audit.append("store", f"mem{i}", ok=True)
80
+
81
+ # Tamper with the file — alter the second line
82
+ audit_path = tmp_path / "audit.jsonl"
83
+ lines = audit_path.read_text().splitlines()
84
+ record = json.loads(lines[1])
85
+ record["op"] = "delete" # tamper!
86
+ lines[1] = json.dumps(record)
87
+ audit_path.write_text("\n".join(lines) + "\n")
88
+
89
+ ok, errors = tmp_audit.verify_chain()
90
+ assert not ok
91
+ assert len(errors) > 0
92
+
93
+ def test_tail_respects_limit(self, tmp_audit):
94
+ for i in range(10):
95
+ tmp_audit.append("store", f"mem{i}")
96
+ records = tmp_audit.tail(3)
97
+ assert len(records) == 3
98
+
99
+ def test_empty_log_verify(self, tmp_audit):
100
+ ok, errors = tmp_audit.verify_chain()
101
+ assert ok
102
+ assert errors == []
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # TamperAlert tests
107
+ # ---------------------------------------------------------------------------
108
+
109
+
110
+ class TestTamperAlert:
111
+ def test_to_dict(self):
112
+ alert = TamperAlert(
113
+ memory_id="abc",
114
+ expected_hash="aaa",
115
+ actual_hash="bbb",
116
+ )
117
+ d = alert.to_dict()
118
+ assert d["memory_id"] == "abc"
119
+ assert d["expected_hash"] == "aaa"
120
+ assert d["actual_hash"] == "bbb"
121
+ assert d["severity"] == "CRITICAL"
122
+ assert "tamper" in d["message"].lower() or "integrity" in d["message"].lower()
123
+ assert "detected_at" in d
124
+
125
+ def test_repr(self):
126
+ alert = TamperAlert("x", "a", "b")
127
+ assert "x" in repr(alert)
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # FortifiedMemoryStore tests
132
+ # ---------------------------------------------------------------------------
133
+
134
+
135
+ class TestFortifiedMemoryStore:
136
+ def test_snapshot_seals_memory(self, fortress):
137
+ mem = fortress.snapshot("Test title", "Test content")
138
+ assert mem.integrity_hash != "", "Memory should be sealed on write"
139
+
140
+ def test_recall_passes_clean_memory(self, fortress):
141
+ mem = fortress.snapshot("Clean", "No tampering here")
142
+ recalled = fortress.recall(mem.id)
143
+ assert recalled is not None
144
+ assert "integrity_warning" not in recalled.metadata
145
+
146
+ def test_recall_missing_returns_none(self, fortress):
147
+ result = fortress.recall("nonexistent-id")
148
+ assert result is None
149
+
150
+ def test_tamper_detection_triggers_callback(self, fortress, tmp_path):
151
+ alerts_received: list[TamperAlert] = []
152
+ fortress.register_alert_callback(alerts_received.append)
153
+
154
+ mem = fortress.snapshot("Secret", "Original content")
155
+
156
+ # Tamper: directly modify the stored memory file
157
+ # We need to find and corrupt the JSON
158
+
159
+ backend = fortress.primary
160
+ # Load raw, mutate, save back bypassing seal
161
+ raw = backend.load(mem.id)
162
+ assert raw is not None
163
+ raw.content = "TAMPERED CONTENT"
164
+ raw.integrity_hash = mem.integrity_hash # keep old hash
165
+ backend.save(raw) # save tampered version with original hash
166
+
167
+ # Now recall — should trigger tamper alert
168
+ recalled = fortress.recall(mem.id)
169
+ assert recalled is not None
170
+ assert "integrity_warning" in recalled.metadata
171
+ assert len(alerts_received) == 1
172
+ alert = alerts_received[0]
173
+ assert alert.memory_id == mem.id
174
+ assert alert.expected_hash == mem.integrity_hash
175
+
176
+ def test_forget_audited(self, fortress):
177
+ mem = fortress.snapshot("Temp", "Will be deleted")
178
+ fortress.forget(mem.id)
179
+ trail = fortress.audit_trail(10)
180
+ ops = [r["op"] for r in trail]
181
+ assert "delete" in ops
182
+
183
+ def test_audit_trail_records_all_ops(self, fortress):
184
+ mem = fortress.snapshot("Op tracking", "content")
185
+ fortress.recall(mem.id)
186
+ fortress.forget(mem.id)
187
+
188
+ trail = fortress.audit_trail(10)
189
+ ops = [r["op"] for r in trail]
190
+ assert "store" in ops
191
+ assert "recall" in ops
192
+ assert "delete" in ops
193
+
194
+ def test_verify_all_clean_store(self, fortress):
195
+ for i in range(5):
196
+ fortress.snapshot(f"Memory {i}", f"Content {i}")
197
+ result = fortress.verify_all()
198
+ assert result["total"] == 5
199
+ assert result["passed"] == 5
200
+ assert result["tampered"] == []
201
+
202
+ def test_verify_all_finds_tampered(self, fortress):
203
+ alerts: list[TamperAlert] = []
204
+ fortress.register_alert_callback(alerts.append)
205
+
206
+ mem = fortress.snapshot("Good memory", "Original")
207
+
208
+ # Tamper via backend
209
+ raw = fortress.primary.load(mem.id)
210
+ raw.content = "CORRUPTED"
211
+ raw.integrity_hash = mem.integrity_hash
212
+ fortress.primary.save(raw)
213
+
214
+ result = fortress.verify_all()
215
+ assert mem.id in result["tampered"]
216
+ assert len(alerts) > 0
217
+
218
+ def test_verify_audit_chain(self, fortress):
219
+ fortress.snapshot("A", "a")
220
+ fortress.snapshot("B", "b")
221
+ ok, errors = fortress.verify_audit_chain()
222
+ assert ok, errors
223
+
224
+ def test_encryption_not_configured_raises(self, fortress):
225
+ with pytest.raises(RuntimeError, match="not configured"):
226
+ fortress.encrypt_payload('{"test": 1}')
227
+
228
+ def test_encryption_active_false_by_default(self, fortress):
229
+ assert fortress.encryption_active is False
230
+
231
+ def test_multiple_callbacks(self, fortress):
232
+ c1, c2 = [], []
233
+ fortress.register_alert_callback(c1.append)
234
+ fortress.register_alert_callback(c2.append)
235
+
236
+ mem = fortress.snapshot("Multi", "content")
237
+ raw = fortress.primary.load(mem.id)
238
+ raw.content = "bad"
239
+ raw.integrity_hash = mem.integrity_hash
240
+ fortress.primary.save(raw)
241
+
242
+ fortress.recall(mem.id)
243
+ assert len(c1) == 1
244
+ assert len(c2) == 1
245
+
246
+ def test_unsealed_memory_passes_silently(self, fortress):
247
+ """A memory with no integrity_hash is not flagged as tampered."""
248
+ mem = fortress.snapshot("Unsealed", "content")
249
+ raw = fortress.primary.load(mem.id)
250
+ raw.integrity_hash = "" # strip the seal
251
+ fortress.primary.save(raw)
252
+
253
+ recalled = fortress.recall(mem.id)
254
+ assert recalled is not None
255
+ # Should not have a warning — unsealed != tampered
256
+ assert "integrity_warning" not in recalled.metadata