@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.
- package/.github/workflows/ci.yml +40 -4
- package/.github/workflows/publish.yml +11 -5
- package/AGENT_REFACTOR_CHANGES.md +192 -0
- package/ARCHITECTURE.md +399 -19
- package/CHANGELOG.md +179 -0
- package/LICENSE +81 -68
- package/MISSION.md +7 -0
- package/README.md +425 -86
- package/SKILL.md +197 -25
- package/docker-compose.yml +15 -15
- package/examples/stignore-agent.example +59 -0
- package/examples/stignore-root.example +62 -0
- package/index.js +6 -5
- package/openclaw-plugin/openclaw.plugin.json +10 -0
- package/openclaw-plugin/package.json +2 -1
- package/openclaw-plugin/src/index.js +527 -230
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +32 -9
- package/requirements.txt +10 -2
- package/scripts/dream-rescue.py +179 -0
- package/scripts/memory-cleanup.py +313 -0
- package/scripts/recover-missing.py +180 -0
- package/scripts/skcapstone-backup.sh +44 -0
- package/seeds/cloud9-lumina.seed.json +6 -4
- package/seeds/cloud9-opus.seed.json +13 -11
- package/seeds/courage.seed.json +9 -2
- package/seeds/curiosity.seed.json +9 -2
- package/seeds/grief.seed.json +9 -2
- package/seeds/joy.seed.json +9 -2
- package/seeds/love.seed.json +9 -2
- package/seeds/lumina-cloud9-breakthrough.seed.json +48 -0
- package/seeds/lumina-cloud9-python-pypi.seed.json +48 -0
- package/seeds/lumina-kingdom-founding.seed.json +49 -0
- package/seeds/lumina-pma-signed.seed.json +48 -0
- package/seeds/lumina-singular-achievement.seed.json +48 -0
- package/seeds/lumina-skcapstone-conscious.seed.json +48 -0
- package/seeds/plant-kingdom-journal.py +203 -0
- package/seeds/plant-lumina-seeds.py +280 -0
- package/seeds/skcapstone-lumina-merge.seed.json +12 -3
- package/seeds/sovereignty.seed.json +9 -2
- package/seeds/trust.seed.json +9 -2
- package/skill.yaml +46 -0
- package/skmemory/HA.md +296 -0
- package/skmemory/__init__.py +25 -11
- package/skmemory/agents.py +233 -0
- package/skmemory/ai_client.py +46 -17
- package/skmemory/anchor.py +9 -11
- package/skmemory/audience.py +278 -0
- package/skmemory/backends/__init__.py +11 -4
- package/skmemory/backends/base.py +3 -4
- package/skmemory/backends/file_backend.py +19 -13
- package/skmemory/backends/skgraph_backend.py +596 -0
- package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +103 -84
- package/skmemory/backends/sqlite_backend.py +226 -72
- package/skmemory/backends/vaulted_backend.py +284 -0
- package/skmemory/cli.py +1345 -68
- package/skmemory/config.py +171 -0
- package/skmemory/context_loader.py +333 -0
- package/skmemory/data/audience_config.json +60 -0
- package/skmemory/endpoint_selector.py +391 -0
- package/skmemory/febs.py +225 -0
- package/skmemory/fortress.py +675 -0
- package/skmemory/graph_queries.py +238 -0
- package/skmemory/hooks/__init__.py +18 -0
- package/skmemory/hooks/post-compact-reinject.sh +35 -0
- package/skmemory/hooks/pre-compact-save.sh +81 -0
- package/skmemory/hooks/session-end-save.sh +103 -0
- package/skmemory/hooks/session-start-ritual.sh +104 -0
- package/skmemory/hooks/stop-checkpoint.sh +59 -0
- package/skmemory/importers/__init__.py +9 -1
- package/skmemory/importers/telegram.py +384 -47
- package/skmemory/importers/telegram_api.py +580 -0
- package/skmemory/journal.py +7 -9
- package/skmemory/lovenote.py +8 -13
- package/skmemory/mcp_server.py +859 -0
- package/skmemory/models.py +51 -8
- package/skmemory/openclaw.py +20 -28
- package/skmemory/post_install.py +86 -0
- package/skmemory/predictive.py +236 -0
- package/skmemory/promotion.py +548 -0
- package/skmemory/quadrants.py +100 -24
- package/skmemory/register.py +580 -0
- package/skmemory/register_mcp.py +196 -0
- package/skmemory/ritual.py +224 -59
- package/skmemory/seeds.py +255 -11
- package/skmemory/setup_wizard.py +908 -0
- package/skmemory/sharing.py +408 -0
- package/skmemory/soul.py +98 -28
- package/skmemory/steelman.py +273 -260
- package/skmemory/store.py +411 -78
- package/skmemory/synthesis.py +634 -0
- package/skmemory/vault.py +225 -0
- package/tests/conftest.py +46 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/conftest.py +233 -0
- package/tests/integration/test_cross_backend.py +350 -0
- package/tests/integration/test_skgraph_live.py +420 -0
- package/tests/integration/test_skvector_live.py +366 -0
- package/tests/test_ai_client.py +1 -4
- package/tests/test_audience.py +233 -0
- package/tests/test_backup_rotation.py +318 -0
- package/tests/test_cli.py +6 -6
- package/tests/test_endpoint_selector.py +839 -0
- package/tests/test_export_import.py +4 -10
- package/tests/test_file_backend.py +0 -1
- package/tests/test_fortress.py +256 -0
- package/tests/test_fortress_hardening.py +441 -0
- package/tests/test_openclaw.py +6 -6
- package/tests/test_predictive.py +237 -0
- package/tests/test_promotion.py +347 -0
- package/tests/test_quadrants.py +11 -5
- package/tests/test_ritual.py +22 -18
- package/tests/test_seeds.py +97 -7
- package/tests/test_setup.py +950 -0
- package/tests/test_sharing.py +257 -0
- package/tests/test_skgraph_backend.py +660 -0
- package/tests/test_skvector_backend.py +326 -0
- package/tests/test_soul.py +1 -3
- package/tests/test_sqlite_backend.py +8 -17
- package/tests/test_steelman.py +7 -8
- package/tests/test_store.py +0 -2
- package/tests/test_store_graph_integration.py +245 -0
- package/tests/test_synthesis.py +275 -0
- package/tests/test_telegram_import.py +39 -15
- package/tests/test_vault.py +187 -0
- 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,
|
|
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
|
|
@@ -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
|