@smilintux/skmemory 0.5.0 → 0.7.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 +39 -3
- package/.github/workflows/publish.yml +13 -6
- package/AGENT_REFACTOR_CHANGES.md +192 -0
- package/ARCHITECTURE.md +101 -19
- package/CHANGELOG.md +153 -0
- package/LICENSE +81 -68
- package/MISSION.md +7 -0
- package/README.md +419 -86
- package/SKILL.md +197 -25
- package/docker-compose.yml +15 -15
- package/index.js +6 -5
- package/openclaw-plugin/openclaw.plugin.json +10 -0
- package/openclaw-plugin/src/index.ts +255 -0
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +29 -9
- package/requirements.txt +10 -2
- package/seeds/cloud9-opus.seed.json +7 -7
- package/seeds/lumina-cloud9-breakthrough.seed.json +46 -0
- package/seeds/lumina-cloud9-python-pypi.seed.json +46 -0
- package/seeds/lumina-kingdom-founding.seed.json +47 -0
- package/seeds/lumina-pma-signed.seed.json +46 -0
- package/seeds/lumina-singular-achievement.seed.json +46 -0
- package/seeds/lumina-skcapstone-conscious.seed.json +46 -0
- package/seeds/plant-kingdom-journal.py +203 -0
- package/seeds/plant-lumina-seeds.py +280 -0
- package/skill.yaml +46 -0
- package/skmemory/HA.md +296 -0
- package/skmemory/__init__.py +12 -1
- package/skmemory/agents.py +233 -0
- package/skmemory/ai_client.py +40 -0
- package/skmemory/anchor.py +4 -2
- package/skmemory/backends/__init__.py +11 -4
- package/skmemory/backends/file_backend.py +2 -1
- package/skmemory/backends/skgraph_backend.py +608 -0
- package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +99 -69
- package/skmemory/backends/sqlite_backend.py +122 -51
- package/skmemory/backends/vaulted_backend.py +286 -0
- package/skmemory/cli.py +1238 -29
- package/skmemory/config.py +173 -0
- package/skmemory/context_loader.py +335 -0
- package/skmemory/endpoint_selector.py +386 -0
- package/skmemory/fortress.py +685 -0
- package/skmemory/graph_queries.py +238 -0
- package/skmemory/importers/__init__.py +9 -1
- package/skmemory/importers/telegram.py +351 -43
- package/skmemory/importers/telegram_api.py +488 -0
- package/skmemory/journal.py +4 -2
- package/skmemory/lovenote.py +4 -2
- package/skmemory/mcp_server.py +706 -0
- package/skmemory/models.py +41 -0
- package/skmemory/openclaw.py +8 -8
- package/skmemory/predictive.py +232 -0
- package/skmemory/promotion.py +524 -0
- package/skmemory/register.py +454 -0
- package/skmemory/register_mcp.py +197 -0
- package/skmemory/ritual.py +121 -47
- package/skmemory/seeds.py +257 -8
- package/skmemory/setup_wizard.py +920 -0
- package/skmemory/sharing.py +402 -0
- package/skmemory/soul.py +71 -20
- package/skmemory/steelman.py +250 -263
- package/skmemory/store.py +271 -60
- package/skmemory/vault.py +228 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/conftest.py +233 -0
- package/tests/integration/test_cross_backend.py +355 -0
- package/tests/integration/test_skgraph_live.py +424 -0
- package/tests/integration/test_skvector_live.py +369 -0
- package/tests/test_backup_rotation.py +327 -0
- package/tests/test_cli.py +6 -6
- package/tests/test_endpoint_selector.py +801 -0
- package/tests/test_fortress.py +255 -0
- package/tests/test_fortress_hardening.py +444 -0
- package/tests/test_openclaw.py +5 -2
- package/tests/test_predictive.py +237 -0
- package/tests/test_promotion.py +340 -0
- package/tests/test_ritual.py +4 -4
- package/tests/test_seeds.py +96 -0
- package/tests/test_setup.py +835 -0
- package/tests/test_sharing.py +250 -0
- package/tests/test_skgraph_backend.py +667 -0
- package/tests/test_skvector_backend.py +326 -0
- package/tests/test_steelman.py +5 -5
- package/tests/test_store_graph_integration.py +245 -0
- package/tests/test_vault.py +186 -0
- package/skmemory/backends/falkordb_backend.py +0 -310
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VaultedSQLiteBackend — transparent AES-256-GCM at-rest encryption.
|
|
3
|
+
|
|
4
|
+
Every memory JSON file is stored as encrypted bytes on disk.
|
|
5
|
+
The SQLite index stores metadata in plaintext for fast queries;
|
|
6
|
+
only the full JSON files are encrypted.
|
|
7
|
+
|
|
8
|
+
The ``SKMV1`` vault header lets the backend auto-detect encrypted vs.
|
|
9
|
+
plain files, so you can safely migrate an existing unencrypted store
|
|
10
|
+
by calling ``seal_all()``.
|
|
11
|
+
|
|
12
|
+
File format on disk (per memory file):
|
|
13
|
+
SKMV1 || salt(16) || nonce(12) || AES-GCM(json_bytes) || tag(16)
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
backend = VaultedSQLiteBackend(passphrase="sovereign-key")
|
|
17
|
+
store = MemoryStore(primary=backend, use_sqlite=False)
|
|
18
|
+
mem = store.snapshot("title", "content")
|
|
19
|
+
recalled = store.recall(mem.id) # transparent decrypt
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import sqlite3
|
|
26
|
+
from datetime import date, datetime, timezone
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Optional
|
|
29
|
+
|
|
30
|
+
from ..models import Memory, MemoryLayer
|
|
31
|
+
from ..vault import VAULT_HEADER, MemoryVault
|
|
32
|
+
from .sqlite_backend import SQLiteBackend
|
|
33
|
+
from .sqlite_backend import DEFAULT_BASE_PATH
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class VaultedSQLiteBackend(SQLiteBackend):
|
|
37
|
+
"""SQLiteBackend with transparent AES-256-GCM at-rest encryption.
|
|
38
|
+
|
|
39
|
+
Subclasses :class:`SQLiteBackend` and overrides all file I/O to
|
|
40
|
+
transparently encrypt on write and decrypt on read. The SQLite
|
|
41
|
+
index is unencrypted so queries remain fast.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
passphrase: Secret used to derive the AES-256 key via PBKDF2-SHA256
|
|
45
|
+
with 600 000 iterations. Use a strong, unique passphrase.
|
|
46
|
+
base_path: Root directory for memory files and index.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ImportError: If the ``cryptography`` package is not installed.
|
|
50
|
+
|
|
51
|
+
Example::
|
|
52
|
+
|
|
53
|
+
backend = VaultedSQLiteBackend(passphrase="my-secret")
|
|
54
|
+
store = MemoryStore(primary=backend, use_sqlite=False)
|
|
55
|
+
m = store.snapshot("Private thought", "End-to-end encrypted on disk")
|
|
56
|
+
r = store.recall(m.id) # decrypted on the fly
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, passphrase: str, base_path: str = DEFAULT_BASE_PATH) -> None:
|
|
60
|
+
self._vault = MemoryVault(passphrase)
|
|
61
|
+
super().__init__(base_path=base_path)
|
|
62
|
+
|
|
63
|
+
# ------------------------------------------------------------------
|
|
64
|
+
# Core I/O overrides
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
def save(self, memory: Memory) -> str:
|
|
68
|
+
"""Encrypt JSON bytes before writing to disk.
|
|
69
|
+
|
|
70
|
+
The SQLite index is updated with plaintext metadata so that
|
|
71
|
+
queries still work without decryption.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
memory: Memory to persist.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
str: The memory ID.
|
|
78
|
+
"""
|
|
79
|
+
path = self._file_path(memory)
|
|
80
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
json_bytes = json.dumps(memory.model_dump(), indent=2, default=str).encode("utf-8")
|
|
82
|
+
path.write_bytes(self._vault.encrypt(json_bytes))
|
|
83
|
+
self._index_memory(memory, path)
|
|
84
|
+
return memory.id
|
|
85
|
+
|
|
86
|
+
def load(self, memory_id: str) -> Optional[Memory]:
|
|
87
|
+
"""Decrypt and parse a memory file.
|
|
88
|
+
|
|
89
|
+
Handles both encrypted (``SKMV1`` header) and plaintext files
|
|
90
|
+
so a partially-migrated store keeps working.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
memory_id: The memory identifier.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Optional[Memory]: The memory if found, None otherwise.
|
|
97
|
+
"""
|
|
98
|
+
path = self._find_file(memory_id)
|
|
99
|
+
if path is None:
|
|
100
|
+
return None
|
|
101
|
+
return self._read_memory_file(path)
|
|
102
|
+
|
|
103
|
+
def _row_to_memory(self, row: sqlite3.Row) -> Optional[Memory]:
|
|
104
|
+
"""Load a full Memory object, decrypting if needed.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
row: SQLite row with ``file_path``.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Optional[Memory]: Full memory or None if file missing / unreadable.
|
|
111
|
+
"""
|
|
112
|
+
path = Path(row["file_path"])
|
|
113
|
+
if not path.exists():
|
|
114
|
+
return None
|
|
115
|
+
return self._read_memory_file(path)
|
|
116
|
+
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
# Bulk operations
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def reindex(self) -> int:
|
|
122
|
+
"""Rebuild the SQLite index by scanning and decrypting all JSON files.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
int: Number of memories re-indexed.
|
|
126
|
+
"""
|
|
127
|
+
conn = self._get_conn()
|
|
128
|
+
conn.execute("DELETE FROM memories")
|
|
129
|
+
conn.commit()
|
|
130
|
+
|
|
131
|
+
count = 0
|
|
132
|
+
for layer in MemoryLayer:
|
|
133
|
+
layer_dir = self.base_path / layer.value
|
|
134
|
+
if not layer_dir.exists():
|
|
135
|
+
continue
|
|
136
|
+
for json_file in layer_dir.glob("*.json"):
|
|
137
|
+
try:
|
|
138
|
+
memory = self._read_memory_file(json_file)
|
|
139
|
+
if memory is not None:
|
|
140
|
+
self._index_memory(memory, json_file)
|
|
141
|
+
count += 1
|
|
142
|
+
except Exception:
|
|
143
|
+
continue
|
|
144
|
+
return count
|
|
145
|
+
|
|
146
|
+
def export_all(self, output_path: Optional[str] = None) -> str:
|
|
147
|
+
"""Export all memories (decrypted) to a JSON backup file.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
output_path: Destination path. Defaults to
|
|
151
|
+
``~/.skcapstone/backups/skmemory-backup-YYYY-MM-DD.json``.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
str: Path to the written backup file.
|
|
155
|
+
"""
|
|
156
|
+
from .. import __version__
|
|
157
|
+
|
|
158
|
+
if output_path is None:
|
|
159
|
+
backup_dir = self.base_path.parent / "backups"
|
|
160
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
output_path = str(backup_dir / f"skmemory-backup-{date.today().isoformat()}.json")
|
|
162
|
+
|
|
163
|
+
memories: list[dict] = []
|
|
164
|
+
for layer in MemoryLayer:
|
|
165
|
+
layer_dir = self.base_path / layer.value
|
|
166
|
+
if not layer_dir.exists():
|
|
167
|
+
continue
|
|
168
|
+
for json_file in sorted(layer_dir.glob("*.json")):
|
|
169
|
+
try:
|
|
170
|
+
memory = self._read_memory_file(json_file)
|
|
171
|
+
if memory is not None:
|
|
172
|
+
memories.append(memory.model_dump())
|
|
173
|
+
except Exception:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
payload = {
|
|
177
|
+
"skmemory_version": __version__,
|
|
178
|
+
"exported_at": datetime.now(timezone.utc).isoformat(),
|
|
179
|
+
"memory_count": len(memories),
|
|
180
|
+
"base_path": str(self.base_path),
|
|
181
|
+
"memories": memories,
|
|
182
|
+
}
|
|
183
|
+
Path(output_path).write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")
|
|
184
|
+
return output_path
|
|
185
|
+
|
|
186
|
+
# ------------------------------------------------------------------
|
|
187
|
+
# Vault management
|
|
188
|
+
# ------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
def seal_all(self) -> int:
|
|
191
|
+
"""Encrypt all plaintext JSON files in the store.
|
|
192
|
+
|
|
193
|
+
Safe to call multiple times — already-encrypted files are skipped.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
int: Number of files newly encrypted.
|
|
197
|
+
"""
|
|
198
|
+
count = 0
|
|
199
|
+
for layer in MemoryLayer:
|
|
200
|
+
layer_dir = self.base_path / layer.value
|
|
201
|
+
if not layer_dir.exists():
|
|
202
|
+
continue
|
|
203
|
+
for json_file in layer_dir.glob("*.json"):
|
|
204
|
+
try:
|
|
205
|
+
raw = json_file.read_bytes()
|
|
206
|
+
if raw[: len(VAULT_HEADER)] == VAULT_HEADER:
|
|
207
|
+
continue # already encrypted
|
|
208
|
+
json_file.write_bytes(self._vault.encrypt(raw))
|
|
209
|
+
count += 1
|
|
210
|
+
except Exception:
|
|
211
|
+
continue
|
|
212
|
+
return count
|
|
213
|
+
|
|
214
|
+
def unseal_all(self) -> int:
|
|
215
|
+
"""Decrypt all vault-encrypted JSON files in the store.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
int: Number of files decrypted.
|
|
219
|
+
"""
|
|
220
|
+
count = 0
|
|
221
|
+
for layer in MemoryLayer:
|
|
222
|
+
layer_dir = self.base_path / layer.value
|
|
223
|
+
if not layer_dir.exists():
|
|
224
|
+
continue
|
|
225
|
+
for json_file in layer_dir.glob("*.json"):
|
|
226
|
+
try:
|
|
227
|
+
raw = json_file.read_bytes()
|
|
228
|
+
if raw[: len(VAULT_HEADER)] != VAULT_HEADER:
|
|
229
|
+
continue # not encrypted
|
|
230
|
+
json_file.write_bytes(self._vault.decrypt(raw))
|
|
231
|
+
count += 1
|
|
232
|
+
except Exception:
|
|
233
|
+
continue
|
|
234
|
+
return count
|
|
235
|
+
|
|
236
|
+
def vault_status(self) -> dict:
|
|
237
|
+
"""Scan memory files and report encryption coverage.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
dict: ``{total, encrypted, plaintext, coverage_pct}``.
|
|
241
|
+
"""
|
|
242
|
+
total = encrypted = 0
|
|
243
|
+
header_len = len(VAULT_HEADER)
|
|
244
|
+
for layer in MemoryLayer:
|
|
245
|
+
layer_dir = self.base_path / layer.value
|
|
246
|
+
if not layer_dir.exists():
|
|
247
|
+
continue
|
|
248
|
+
for json_file in layer_dir.glob("*.json"):
|
|
249
|
+
total += 1
|
|
250
|
+
try:
|
|
251
|
+
with json_file.open("rb") as fh:
|
|
252
|
+
header = fh.read(header_len)
|
|
253
|
+
if header == VAULT_HEADER:
|
|
254
|
+
encrypted += 1
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
plaintext = total - encrypted
|
|
258
|
+
pct = (encrypted / total * 100) if total else 100.0
|
|
259
|
+
return {
|
|
260
|
+
"total": total,
|
|
261
|
+
"encrypted": encrypted,
|
|
262
|
+
"plaintext": plaintext,
|
|
263
|
+
"coverage_pct": round(pct, 1),
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# ------------------------------------------------------------------
|
|
267
|
+
# Internal
|
|
268
|
+
# ------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
def _read_memory_file(self, path: Path) -> Optional[Memory]:
|
|
271
|
+
"""Read a file and parse to Memory, decrypting if needed.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
path: Path to the JSON file (may be encrypted).
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Optional[Memory]: Parsed memory or None on failure.
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
raw = path.read_bytes()
|
|
281
|
+
if raw[: len(VAULT_HEADER)] == VAULT_HEADER:
|
|
282
|
+
raw = self._vault.decrypt(raw)
|
|
283
|
+
data = json.loads(raw.decode("utf-8"))
|
|
284
|
+
return Memory(**data)
|
|
285
|
+
except Exception:
|
|
286
|
+
return None
|