superlocalmemory 3.8.13 → 4.0.0

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 (212) hide show
  1. package/ATTRIBUTION.md +4 -4
  2. package/CHANGELOG.md +113 -121
  3. package/README.md +65 -63
  4. package/docs/pi-dev-integration.md +1 -1
  5. package/package.json +6 -1
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/CLAUDE.md +3 -3
  8. package/plugin/agents/slm-governance-advisor.md +1 -1
  9. package/plugin/agents/slm-loop-runner.md +1 -1
  10. package/plugin/agents/slm-memory-advisor.md +1 -1
  11. package/plugin/agents/slm-optimize-advisor.md +1 -1
  12. package/plugin/requirements.txt +1 -1
  13. package/plugin/skills/slm-cache/SKILL.md +1 -1
  14. package/plugin/skills/slm-compress/SKILL.md +1 -1
  15. package/plugin/skills/slm-governance/SKILL.md +1 -1
  16. package/plugin/skills/slm-graph/SKILL.md +1 -1
  17. package/plugin/skills/slm-loop/SKILL.md +1 -1
  18. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  19. package/plugin/skills/slm-profile/SKILL.md +1 -1
  20. package/plugin/skills/slm-recall/SKILL.md +1 -1
  21. package/plugin/skills/slm-remember/SKILL.md +1 -1
  22. package/plugin/skills/slm-scope/SKILL.md +1 -1
  23. package/plugin/skills/slm-session/SKILL.md +1 -1
  24. package/plugin/skills/slm-status/SKILL.md +1 -1
  25. package/plugin-src/rules/AGENTS.md +1 -1
  26. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-governance/SKILL.md +248 -0
  29. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-loop/SKILL.md +99 -0
  31. package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
  32. package/plugin-src/skills/slm-profile/SKILL.md +148 -0
  33. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-scope/SKILL.md +176 -0
  36. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  37. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  38. package/pyproject.toml +11 -4
  39. package/src/superlocalmemory/__init__.py +1 -1
  40. package/src/superlocalmemory/cli/commands.py +125 -11
  41. package/src/superlocalmemory/cli/daemon.py +5 -1
  42. package/src/superlocalmemory/cli/main.py +35 -2
  43. package/src/superlocalmemory/cli/ops_cmd.py +281 -0
  44. package/src/superlocalmemory/cli/setup_wizard.py +1 -1
  45. package/src/superlocalmemory/compliance/audit.py +65 -0
  46. package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
  47. package/src/superlocalmemory/compliance/gdpr.py +416 -20
  48. package/src/superlocalmemory/compliance/retention.py +74 -22
  49. package/src/superlocalmemory/compliance/scheduler.py +78 -9
  50. package/src/superlocalmemory/core/actor_context.py +166 -0
  51. package/src/superlocalmemory/core/admission.py +549 -0
  52. package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
  53. package/src/superlocalmemory/core/config.py +202 -24
  54. package/src/superlocalmemory/core/consolidation_engine.py +13 -13
  55. package/src/superlocalmemory/core/context_cache.py +28 -0
  56. package/src/superlocalmemory/core/embeddings.py +64 -2
  57. package/src/superlocalmemory/core/engine.py +7 -2
  58. package/src/superlocalmemory/core/engine_ingestion.py +65 -3
  59. package/src/superlocalmemory/core/engine_wiring.py +36 -9
  60. package/src/superlocalmemory/core/ingest_policy.py +38 -0
  61. package/src/superlocalmemory/core/maintenance.py +255 -0
  62. package/src/superlocalmemory/core/modes.py +40 -13
  63. package/src/superlocalmemory/core/mutations.py +437 -44
  64. package/src/superlocalmemory/core/operation_policy.py +92 -0
  65. package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
  66. package/src/superlocalmemory/core/operation_request.py +127 -0
  67. package/src/superlocalmemory/core/ops_remediation.py +542 -0
  68. package/src/superlocalmemory/core/recall_pipeline.py +7 -0
  69. package/src/superlocalmemory/core/remember_runtime.py +202 -4
  70. package/src/superlocalmemory/core/remote_mode.py +20 -5
  71. package/src/superlocalmemory/core/store_pipeline.py +150 -0
  72. package/src/superlocalmemory/core/topic_signature.py +19 -4
  73. package/src/superlocalmemory/core/transactions/__init__.py +78 -0
  74. package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
  75. package/src/superlocalmemory/core/transactions/erasure.py +825 -0
  76. package/src/superlocalmemory/core/transactions/manifest.py +255 -0
  77. package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
  78. package/src/superlocalmemory/core/transactions/obligations.py +272 -0
  79. package/src/superlocalmemory/core/transactions/owners.py +114 -0
  80. package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
  81. package/src/superlocalmemory/core/transactions/service.py +330 -0
  82. package/src/superlocalmemory/core/worker_pool.py +33 -5
  83. package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
  84. package/src/superlocalmemory/encoding/emotional.py +75 -14
  85. package/src/superlocalmemory/encoding/scene_builder.py +115 -13
  86. package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
  87. package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
  88. package/src/superlocalmemory/evolution/evolution_store.py +244 -4
  89. package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
  90. package/src/superlocalmemory/evolution/model_selection.py +18 -3
  91. package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
  92. package/src/superlocalmemory/evolution/skill_activator.py +270 -0
  93. package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
  94. package/src/superlocalmemory/evolution/types.py +30 -8
  95. package/src/superlocalmemory/graph/cozo_backend.py +17 -9
  96. package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
  97. package/src/superlocalmemory/hooks/auto_recall.py +64 -30
  98. package/src/superlocalmemory/hooks/codex_assets.py +14 -1
  99. package/src/superlocalmemory/infra/backup.py +434 -7
  100. package/src/superlocalmemory/infra/process_reaper.py +18 -0
  101. package/src/superlocalmemory/infra/self_heal.py +401 -0
  102. package/src/superlocalmemory/learning/feedback.py +52 -9
  103. package/src/superlocalmemory/loops/engine.py +10 -0
  104. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  105. package/src/superlocalmemory/mcp/http_transport.py +30 -331
  106. package/src/superlocalmemory/mcp/profiles.py +5 -0
  107. package/src/superlocalmemory/mcp/resources.py +8 -0
  108. package/src/superlocalmemory/mcp/server.py +51 -4
  109. package/src/superlocalmemory/mcp/shared.py +19 -0
  110. package/src/superlocalmemory/mcp/tools_active.py +25 -4
  111. package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
  112. package/src/superlocalmemory/mcp/tools_context.py +50 -8
  113. package/src/superlocalmemory/mcp/tools_core.py +69 -21
  114. package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
  115. package/src/superlocalmemory/mcp/tools_learning.py +21 -10
  116. package/src/superlocalmemory/mcp/tools_loops.py +29 -18
  117. package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
  118. package/src/superlocalmemory/mcp/tools_ops.py +115 -0
  119. package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
  120. package/src/superlocalmemory/mcp/tools_v28.py +10 -3
  121. package/src/superlocalmemory/mcp/tools_v3.py +34 -14
  122. package/src/superlocalmemory/mcp/tools_v33.py +18 -33
  123. package/src/superlocalmemory/mesh/broker.py +124 -46
  124. package/src/superlocalmemory/mesh/broker_security.py +470 -0
  125. package/src/superlocalmemory/mesh/discovery.py +365 -0
  126. package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
  127. package/src/superlocalmemory/mesh/node_identity.py +97 -0
  128. package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
  129. package/src/superlocalmemory/mesh/remote_sync.py +511 -28
  130. package/src/superlocalmemory/mesh/state_sync.py +286 -0
  131. package/src/superlocalmemory/optimize/config/store.py +45 -0
  132. package/src/superlocalmemory/parameterization/cross_project.py +12 -0
  133. package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
  134. package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
  135. package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
  136. package/src/superlocalmemory/retrieval/ann_index.py +5 -0
  137. package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
  138. package/src/superlocalmemory/retrieval/engine.py +19 -4
  139. package/src/superlocalmemory/retrieval/fusion.py +4 -1
  140. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
  141. package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
  142. package/src/superlocalmemory/retrieval/reranker.py +32 -1
  143. package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
  144. package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
  145. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
  146. package/src/superlocalmemory/retrieval/vector_store.py +214 -8
  147. package/src/superlocalmemory/server/api.py +5 -5
  148. package/src/superlocalmemory/server/egress_policy.py +258 -0
  149. package/src/superlocalmemory/server/rbac_enforce.py +32 -0
  150. package/src/superlocalmemory/server/route_mutations.py +20 -0
  151. package/src/superlocalmemory/server/routes/compliance.py +153 -7
  152. package/src/superlocalmemory/server/routes/data_io.py +43 -2
  153. package/src/superlocalmemory/server/routes/events.py +15 -0
  154. package/src/superlocalmemory/server/routes/memories.py +56 -3
  155. package/src/superlocalmemory/server/routes/mesh.py +82 -1
  156. package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
  157. package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
  158. package/src/superlocalmemory/server/routes/v3_api.py +50 -27
  159. package/src/superlocalmemory/server/routes/ws.py +86 -0
  160. package/src/superlocalmemory/server/ui.py +6 -6
  161. package/src/superlocalmemory/server/unified_daemon.py +942 -119
  162. package/src/superlocalmemory/storage/_migration_internals.py +568 -0
  163. package/src/superlocalmemory/storage/_schema_version.py +110 -0
  164. package/src/superlocalmemory/storage/database.py +329 -24
  165. package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
  166. package/src/superlocalmemory/storage/erasure_fence.py +45 -0
  167. package/src/superlocalmemory/storage/generation_fence.py +63 -0
  168. package/src/superlocalmemory/storage/migration_runner.py +140 -417
  169. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
  170. package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
  171. package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
  172. package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
  173. package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
  174. package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
  175. package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
  176. package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
  177. package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
  178. package/src/superlocalmemory/storage/schema.py +67 -0
  179. package/src/superlocalmemory/storage/write_coordinator.py +125 -0
  180. package/src/superlocalmemory/trust/scorer.py +28 -4
  181. package/src/superlocalmemory/ui/index.html +14 -3
  182. package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
  183. package/src/superlocalmemory/ui/js/brain.js +6 -4
  184. package/src/superlocalmemory/ui/js/compliance.js +66 -12
  185. package/src/superlocalmemory/ui/js/dashboard.js +13 -3
  186. package/src/superlocalmemory/ui/js/feedback.js +8 -2
  187. package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
  188. package/src/superlocalmemory/ui/js/modal.js +272 -5
  189. package/src/superlocalmemory/ui/js/od-backup.js +9 -2
  190. package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
  191. package/src/superlocalmemory/ui/js/od-operations.js +154 -23
  192. package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
  193. package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
  194. package/src/superlocalmemory/ui/js/od-team.js +9 -2
  195. package/src/superlocalmemory/ui/js/optimize.js +13 -16
  196. package/src/superlocalmemory/ui/js/profiles.js +7 -3
  197. package/src/superlocalmemory/ui/js/settings.js +7 -1
  198. package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
  199. package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
  200. package/src/superlocalmemory/cli/post_install.py +0 -114
  201. package/src/superlocalmemory/core/clock_monitor.py +0 -45
  202. package/src/superlocalmemory/core/db_pool.py +0 -80
  203. package/src/superlocalmemory/core/error_catalog.py +0 -113
  204. package/src/superlocalmemory/core/loop_watchdog.py +0 -56
  205. package/src/superlocalmemory/core/priority_queue.py +0 -61
  206. package/src/superlocalmemory/core/pruning_engine.py +0 -216
  207. package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
  208. package/src/superlocalmemory/core/slmignore.py +0 -125
  209. package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
  210. package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
  211. package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
  212. package/src/superlocalmemory/storage/access_control.py +0 -182
@@ -0,0 +1,255 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import json
8
+ from collections.abc import Iterable
9
+ from dataclasses import dataclass
10
+ from enum import StrEnum
11
+
12
+ MANIFEST_V1: int = 1
13
+ MANIFEST_V2: int = 2
14
+
15
+ from superlocalmemory.core.transactions.obligations import Obligation
16
+ from superlocalmemory.core.transactions.owners import (
17
+ ObligationKind,
18
+ ObligationState,
19
+ is_terminal_success,
20
+ )
21
+
22
+
23
+ class ManifestState(StrEnum):
24
+ COMPLETE = "COMPLETE"
25
+ DEGRADED = "DEGRADED"
26
+ FAILED = "FAILED"
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class OwnerEvidence:
31
+ owner: str
32
+ kind: ObligationKind
33
+ state: ObligationState
34
+ revision: int
35
+ checksum: str
36
+
37
+ def as_dict(self) -> dict[str, object]:
38
+ return {
39
+ "owner": self.owner,
40
+ "kind": str(self.kind),
41
+ "state": str(self.state),
42
+ "revision": self.revision,
43
+ "checksum": self.checksum,
44
+ }
45
+
46
+
47
+ @dataclass(frozen=True, slots=True)
48
+ class CompletionManifest:
49
+ operation_id: str
50
+ profile_id: str
51
+ state: ManifestState
52
+ all_met: bool
53
+ obligation_count: int
54
+ owner_evidence: tuple[OwnerEvidence, ...]
55
+ manifest_hash: str
56
+ created_at: float
57
+ updated_at: float
58
+
59
+
60
+ def build_evidence(obligations: Iterable[Obligation]) -> tuple[OwnerEvidence, ...]:
61
+ evidence = [
62
+ OwnerEvidence(
63
+ owner=o.owner,
64
+ kind=o.kind,
65
+ state=o.state,
66
+ revision=o.revision,
67
+ checksum=o.checksum or "",
68
+ )
69
+ for o in obligations
70
+ ]
71
+ evidence.sort(key=lambda e: (e.owner, str(e.kind), e.revision))
72
+ return tuple(evidence)
73
+
74
+
75
+ def _sorted_evidence_dicts(evidence: Iterable[OwnerEvidence]) -> list[dict[str, object]]:
76
+ return sorted(
77
+ (e.as_dict() for e in evidence),
78
+ key=lambda d: (d["owner"], d["kind"], d["revision"]),
79
+ )
80
+
81
+
82
+ def evidence_json(evidence: Iterable[OwnerEvidence]) -> str:
83
+ return json.dumps(
84
+ _sorted_evidence_dicts(evidence),
85
+ sort_keys=True,
86
+ separators=(",", ":"),
87
+ ensure_ascii=False,
88
+ )
89
+
90
+
91
+ def compute_envelope_hash(
92
+ *,
93
+ operation_id: str,
94
+ profile_id: str,
95
+ state: ManifestState,
96
+ all_met: bool,
97
+ obligation_count: int,
98
+ evidence: Iterable[OwnerEvidence],
99
+ ) -> str:
100
+ envelope = {
101
+ "operation_id": operation_id,
102
+ "profile_id": profile_id,
103
+ "state": str(state),
104
+ "all_met": bool(all_met),
105
+ "obligation_count": obligation_count,
106
+ "evidence": _sorted_evidence_dicts(evidence),
107
+ }
108
+ canonical = json.dumps(
109
+ envelope, sort_keys=True, separators=(",", ":"), ensure_ascii=False
110
+ )
111
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
112
+
113
+
114
+ def hash_envelope_fields(
115
+ *,
116
+ operation_id: str,
117
+ profile_id: str,
118
+ state: str,
119
+ all_met: bool,
120
+ obligation_count: int,
121
+ evidence_dicts: list[dict[str, object]],
122
+ ) -> str:
123
+ envelope = {
124
+ "operation_id": operation_id,
125
+ "profile_id": profile_id,
126
+ "state": state,
127
+ "all_met": bool(all_met),
128
+ "obligation_count": obligation_count,
129
+ "evidence": evidence_dicts,
130
+ }
131
+ canonical = json.dumps(
132
+ envelope, sort_keys=True, separators=(",", ":"), ensure_ascii=False
133
+ )
134
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
135
+
136
+
137
+ def _canonical_envelope(
138
+ *,
139
+ operation_id: str,
140
+ profile_id: str,
141
+ state: str,
142
+ all_met: bool,
143
+ obligation_count: int,
144
+ evidence_dicts: list[dict[str, object]],
145
+ manifest_version: int | None = None,
146
+ ) -> bytes:
147
+ """Produce the deterministic byte representation of the manifest envelope.
148
+
149
+ When ``manifest_version`` is provided (v2+), it is bound into the canonical
150
+ bytes so a downgrade from v2 → v1 is detected by the MAC mismatch: the v1
151
+ SHA path does NOT include version, so any v2 HMAC covers a strictly different
152
+ byte string than any v1 SHA.
153
+ """
154
+ envelope: dict[str, object] = {
155
+ "operation_id": operation_id,
156
+ "profile_id": profile_id,
157
+ "state": state,
158
+ "all_met": bool(all_met),
159
+ "obligation_count": obligation_count,
160
+ "evidence": evidence_dicts,
161
+ }
162
+ if manifest_version is not None:
163
+ envelope["manifest_version"] = manifest_version
164
+ return json.dumps(
165
+ envelope, sort_keys=True, separators=(",", ":"), ensure_ascii=False,
166
+ ).encode("utf-8")
167
+
168
+
169
+ def compute_envelope_hmac(
170
+ *,
171
+ operation_id: str,
172
+ profile_id: str,
173
+ state: ManifestState,
174
+ all_met: bool,
175
+ obligation_count: int,
176
+ evidence: Iterable[OwnerEvidence],
177
+ key: bytes | None = None,
178
+ ) -> str:
179
+ """HMAC-SHA256 keyed manifest hash (v2). ``key`` is injected in tests;
180
+ production code passes ``key=None`` to auto-derive from the installation key."""
181
+ from superlocalmemory.core.transactions.manifest_key import (
182
+ compute_hmac,
183
+ derive_manifest_hmac_key,
184
+ )
185
+
186
+ actual_key = key if key is not None else derive_manifest_hmac_key()
187
+ payload = _canonical_envelope(
188
+ operation_id=operation_id,
189
+ profile_id=profile_id,
190
+ state=str(state),
191
+ all_met=all_met,
192
+ obligation_count=obligation_count,
193
+ evidence_dicts=_sorted_evidence_dicts(evidence),
194
+ manifest_version=MANIFEST_V2,
195
+ )
196
+ return compute_hmac(actual_key, payload)
197
+
198
+
199
+ def verify_envelope_hmac(
200
+ *,
201
+ operation_id: str,
202
+ profile_id: str,
203
+ state: str,
204
+ all_met: bool,
205
+ obligation_count: int,
206
+ evidence_dicts: list[dict[str, object]],
207
+ stored_mac: str,
208
+ key: bytes | None = None,
209
+ ) -> bool:
210
+ """Constant-time verify an HMAC-keyed manifest hash (v2)."""
211
+ from superlocalmemory.core.transactions.manifest_key import (
212
+ derive_manifest_hmac_key,
213
+ verify_hmac,
214
+ )
215
+
216
+ actual_key = key if key is not None else derive_manifest_hmac_key()
217
+ payload = _canonical_envelope(
218
+ operation_id=operation_id,
219
+ profile_id=profile_id,
220
+ state=state,
221
+ all_met=all_met,
222
+ obligation_count=obligation_count,
223
+ evidence_dicts=evidence_dicts,
224
+ manifest_version=MANIFEST_V2,
225
+ )
226
+ return verify_hmac(stored_mac, actual_key, payload)
227
+
228
+
229
+ def derive_state(
230
+ obligations: tuple[Obligation, ...], *, canonical_committed: bool,
231
+ ) -> tuple[ManifestState, bool]:
232
+ if not canonical_committed:
233
+ return ManifestState.FAILED, False
234
+ if not obligations:
235
+ return ManifestState.FAILED, False
236
+ all_met = all(is_terminal_success(o.state) for o in obligations)
237
+ if all_met:
238
+ return ManifestState.COMPLETE, True
239
+ return ManifestState.DEGRADED, False
240
+
241
+
242
+ __all__ = [
243
+ "MANIFEST_V1",
244
+ "MANIFEST_V2",
245
+ "CompletionManifest",
246
+ "ManifestState",
247
+ "OwnerEvidence",
248
+ "build_evidence",
249
+ "compute_envelope_hash",
250
+ "compute_envelope_hmac",
251
+ "derive_state",
252
+ "evidence_json",
253
+ "hash_envelope_fields",
254
+ "verify_envelope_hmac",
255
+ ]
@@ -0,0 +1,155 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """Installation-bound HMAC signing key for completion manifests and erasure receipts.
5
+
6
+ The signing key is a 32-byte secret persisted at
7
+ ``~/.superlocalmemory/.manifest_signing_key`` (mode 0600). It is generated
8
+ once on first use and read on every subsequent call. Purpose-specific
9
+ sub-keys are derived via HMAC so a key compromise in one context cannot be
10
+ used to forge a receipt in another.
11
+
12
+ The key is NEVER logged or included in any error message.
13
+
14
+ Key file location: resolved via ``state_path(".manifest_signing_key")``.
15
+ In tests, monkeypatch ``_signing_key_path`` and/or ``_ensure_signing_key``
16
+ to return a deterministic path or byte sequence.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import hashlib
22
+ import hmac as _hmac
23
+ import os
24
+ import secrets
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ _KEY_CONTEXT_MANIFEST: bytes = b"slm-manifest-v2"
29
+ _KEY_CONTEXT_RECEIPT: bytes = b"slm-receipt-v2"
30
+ _MANIFEST_VERSION: int = 2
31
+
32
+
33
+ def _signing_key_path() -> Path:
34
+ from superlocalmemory.infra.data_root import state_path
35
+
36
+ return state_path(".manifest_signing_key")
37
+
38
+
39
+ def _ensure_signing_key() -> bytes:
40
+ """Read the 32-byte hex signing key, creating it on first call.
41
+
42
+ Returns the raw bytes of the key (NOT the hex string).
43
+
44
+ Raises RuntimeError if the key cannot be obtained (corrupt, unreadable,
45
+ permission error). Ephemeral keys are NEVER returned — they would
46
+ permanently desync multi-process daemons and silently break all
47
+ verification of previously-signed manifests.
48
+ """
49
+ key_path = _signing_key_path()
50
+ try:
51
+ key_path.parent.mkdir(parents=True, exist_ok=True)
52
+ except OSError as exc:
53
+ raise RuntimeError(
54
+ "signing key: cannot create key directory (key never logged)"
55
+ ) from exc
56
+
57
+ try:
58
+ key_exists = key_path.exists()
59
+ except OSError as exc:
60
+ # stat() itself can fail (e.g. an unreadable parent dir). Fail closed
61
+ # with the same RuntimeError contract — never leak a raw OSError or
62
+ # fall through to an ephemeral key.
63
+ raise RuntimeError(
64
+ "signing key: cannot access key path (key never logged)"
65
+ ) from exc
66
+
67
+ if key_exists:
68
+ try:
69
+ raw = key_path.read_text(encoding="utf-8").strip()
70
+ except OSError as exc:
71
+ raise RuntimeError(
72
+ "signing key: cannot read existing key file (key never logged)"
73
+ ) from exc
74
+ if len(raw) != 64:
75
+ raise RuntimeError(
76
+ "signing key: file has unexpected length (corrupt or truncated) — "
77
+ "remove the file and restart to generate a fresh key"
78
+ )
79
+ return bytes.fromhex(raw)
80
+
81
+ # First-use: generate and persist atomically.
82
+ raw = secrets.token_hex(32)
83
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
84
+ if hasattr(os, "O_NOFOLLOW"):
85
+ flags |= os.O_NOFOLLOW
86
+ try:
87
+ fd = os.open(str(key_path), flags, 0o600)
88
+ try:
89
+ os.write(fd, raw.encode("utf-8"))
90
+ finally:
91
+ os.close(fd)
92
+ except FileExistsError:
93
+ # Another process won the race — read its key (never return our own).
94
+ try:
95
+ existing = key_path.read_text(encoding="utf-8").strip()
96
+ except OSError as exc:
97
+ raise RuntimeError(
98
+ "signing key: race winner's key unreadable (key never logged)"
99
+ ) from exc
100
+ if len(existing) != 64:
101
+ raise RuntimeError(
102
+ "signing key: race winner wrote a corrupt key (key never logged)"
103
+ )
104
+ return bytes.fromhex(existing)
105
+ except OSError as exc:
106
+ # Permission / symlink error — never return an ephemeral key.
107
+ raise RuntimeError(
108
+ "signing key: cannot create key file (key never logged)"
109
+ ) from exc
110
+
111
+ if sys.platform != "win32":
112
+ try:
113
+ os.chmod(str(key_path), 0o600)
114
+ except OSError:
115
+ pass
116
+
117
+ return bytes.fromhex(raw)
118
+
119
+
120
+ def derive_manifest_hmac_key() -> bytes:
121
+ """Return the HMAC sub-key for completion manifests."""
122
+ master = _ensure_signing_key()
123
+ return _hmac.new(master, _KEY_CONTEXT_MANIFEST, hashlib.sha256).digest()
124
+
125
+
126
+ def derive_receipt_hmac_key() -> bytes:
127
+ """Return the HMAC sub-key for erasure receipts."""
128
+ master = _ensure_signing_key()
129
+ return _hmac.new(master, _KEY_CONTEXT_RECEIPT, hashlib.sha256).digest()
130
+
131
+
132
+ def compute_hmac(key: bytes, payload: bytes) -> str:
133
+ """Return HMAC-SHA256 of ``payload`` under ``key`` as a hex string."""
134
+ return _hmac.new(key, payload, hashlib.sha256).hexdigest()
135
+
136
+
137
+ def verify_hmac(stored_mac: str, key: bytes, payload: bytes) -> bool:
138
+ """Constant-time verify that ``stored_mac`` matches HMAC of ``payload`` under ``key``."""
139
+ try:
140
+ expected = _hmac.new(key, payload, hashlib.sha256).digest()
141
+ stored_bytes = bytes.fromhex(stored_mac)
142
+ except (ValueError, TypeError):
143
+ return False
144
+ return _hmac.compare_digest(expected, stored_bytes)
145
+
146
+
147
+ __all__ = [
148
+ "MANIFEST_VERSION",
149
+ "compute_hmac",
150
+ "derive_manifest_hmac_key",
151
+ "derive_receipt_hmac_key",
152
+ "verify_hmac",
153
+ ]
154
+
155
+ MANIFEST_VERSION = _MANIFEST_VERSION
@@ -0,0 +1,272 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import json
8
+ import sqlite3
9
+ import time
10
+ from collections.abc import Iterable, Mapping
11
+ from dataclasses import dataclass
12
+ from typing import Any
13
+
14
+ from superlocalmemory.core.transactions.owners import (
15
+ ObligationKind,
16
+ ObligationState,
17
+ OperationContext,
18
+ )
19
+
20
+
21
+ class ObligationConflictError(ValueError):
22
+ pass
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class Obligation:
27
+ operation_id: str
28
+ profile_id: str
29
+ owner: str
30
+ kind: ObligationKind
31
+ subject_id: str
32
+ revision: int
33
+ state: ObligationState
34
+ checksum: str | None
35
+ detail: Mapping[str, Any] | None
36
+ attempts: int
37
+ context_digest: str | None
38
+ verify_attempts: int
39
+ last_verified_at: float | None
40
+ created_at: float
41
+ updated_at: float
42
+
43
+
44
+ _SELECT_COLUMNS = (
45
+ "operation_id, profile_id, owner, kind, subject_id, revision, state, "
46
+ "checksum, detail, attempts, context_digest, verify_attempts, "
47
+ "last_verified_at, created_at, updated_at"
48
+ )
49
+
50
+
51
+ def context_digest(
52
+ context: OperationContext, owner: str, kind: ObligationKind,
53
+ ) -> str:
54
+ payload = "\0".join([
55
+ context.operation_id,
56
+ context.profile_id,
57
+ owner,
58
+ str(kind),
59
+ ])
60
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
61
+
62
+
63
+ class ObligationLedger:
64
+ def record(
65
+ self,
66
+ conn: sqlite3.Connection,
67
+ context: OperationContext,
68
+ owner: str,
69
+ kind: ObligationKind,
70
+ ) -> None:
71
+ digest = context_digest(context, owner, kind)
72
+ existing = conn.execute(
73
+ "SELECT context_digest, profile_id FROM projection_obligations "
74
+ "WHERE operation_id = ? AND owner = ? AND kind = ?",
75
+ (context.operation_id, owner, str(kind)),
76
+ ).fetchone()
77
+ if existing is not None:
78
+ stored, stored_profile = existing[0], existing[1]
79
+ if stored_profile != context.profile_id:
80
+ raise ObligationConflictError(
81
+ f"obligation {context.operation_id}/{owner}/{kind} "
82
+ "already recorded for a different profile"
83
+ )
84
+ if stored is None:
85
+ conn.execute(
86
+ "UPDATE projection_obligations SET context_digest = ? "
87
+ "WHERE operation_id = ? AND owner = ? AND kind = ?",
88
+ (digest, context.operation_id, owner, str(kind)),
89
+ )
90
+ return
91
+ if stored != digest:
92
+ raise ObligationConflictError(
93
+ f"obligation {context.operation_id}/{owner}/{kind} "
94
+ "already recorded with a different context"
95
+ )
96
+ return
97
+ now = time.time()
98
+ conn.execute(
99
+ "INSERT INTO projection_obligations "
100
+ "(operation_id, profile_id, owner, kind, subject_id, revision, "
101
+ "state, checksum, detail, attempts, context_digest, "
102
+ "verify_attempts, last_verified_at, created_at, updated_at) "
103
+ "VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL, 0, ?, 0, NULL, ?, ?)",
104
+ (
105
+ context.operation_id,
106
+ context.profile_id,
107
+ owner,
108
+ str(kind),
109
+ context.subject_id,
110
+ context.revision,
111
+ str(ObligationState.PENDING),
112
+ digest,
113
+ now,
114
+ now,
115
+ ),
116
+ )
117
+
118
+ def record_many(
119
+ self,
120
+ conn: sqlite3.Connection,
121
+ context: OperationContext,
122
+ owners: Iterable[str],
123
+ kind: ObligationKind,
124
+ ) -> None:
125
+ for owner in owners:
126
+ self.record(conn, context, owner, kind)
127
+
128
+ def mark(
129
+ self,
130
+ conn: sqlite3.Connection,
131
+ operation_id: str,
132
+ owner: str,
133
+ kind: ObligationKind,
134
+ state: ObligationState,
135
+ *,
136
+ checksum: str | None = None,
137
+ detail: Mapping[str, Any] | None = None,
138
+ bump_attempts: bool = False,
139
+ bump_verify_attempts: bool = False,
140
+ set_verified_at: bool = False,
141
+ ) -> int:
142
+ detail_json = None if detail is None else json.dumps(
143
+ dict(detail), sort_keys=True, separators=(",", ":"), ensure_ascii=False
144
+ )
145
+ now = time.time()
146
+ cursor = conn.execute(
147
+ "UPDATE projection_obligations SET "
148
+ "state = ?, "
149
+ "checksum = COALESCE(?, checksum), "
150
+ "detail = ?, "
151
+ "attempts = attempts + ?, "
152
+ "verify_attempts = verify_attempts + ?, "
153
+ "last_verified_at = CASE WHEN ? THEN ? ELSE last_verified_at END, "
154
+ "updated_at = ? "
155
+ "WHERE operation_id = ? AND owner = ? AND kind = ?",
156
+ (
157
+ str(state),
158
+ checksum,
159
+ detail_json,
160
+ 1 if bump_attempts else 0,
161
+ 1 if bump_verify_attempts else 0,
162
+ 1 if set_verified_at else 0,
163
+ now,
164
+ now,
165
+ operation_id,
166
+ owner,
167
+ str(kind),
168
+ ),
169
+ )
170
+ return cursor.rowcount
171
+
172
+ def fetch(
173
+ self, conn: sqlite3.Connection, operation_id: str,
174
+ ) -> tuple[Obligation, ...]:
175
+ rows = conn.execute(
176
+ f"SELECT {_SELECT_COLUMNS} FROM projection_obligations "
177
+ "WHERE operation_id = ? ORDER BY owner, kind",
178
+ (operation_id,),
179
+ ).fetchall()
180
+ return tuple(_row_to_obligation(row) for row in rows)
181
+
182
+ def fetch_by_state(
183
+ self,
184
+ conn: sqlite3.Connection,
185
+ state: ObligationState,
186
+ *,
187
+ limit: int = 100,
188
+ ) -> tuple[Obligation, ...]:
189
+ rows = conn.execute(
190
+ f"SELECT {_SELECT_COLUMNS} FROM projection_obligations "
191
+ "WHERE state = ? ORDER BY updated_at LIMIT ?",
192
+ (str(state), limit),
193
+ ).fetchall()
194
+ return tuple(_row_to_obligation(row) for row in rows)
195
+
196
+ def pending_operation_ids(
197
+ self,
198
+ conn: sqlite3.Connection,
199
+ *,
200
+ profile_id: str | None = None,
201
+ limit: int = 100,
202
+ max_attempts: int = 10,
203
+ ) -> tuple[str, ...]:
204
+ where = "state NOT IN (?, ?) AND attempts < ?"
205
+ params: list[Any] = [
206
+ str(ObligationState.VERIFIED), str(ObligationState.ERASED), max_attempts,
207
+ ]
208
+ if profile_id is not None:
209
+ where += " AND profile_id = ?"
210
+ params.append(profile_id)
211
+ params.append(limit)
212
+ rows = conn.execute(
213
+ f"SELECT DISTINCT operation_id FROM projection_obligations "
214
+ f"WHERE {where} ORDER BY updated_at LIMIT ?",
215
+ tuple(params),
216
+ ).fetchall()
217
+ return tuple(row[0] for row in rows)
218
+
219
+ def operations_missing_manifest(
220
+ self,
221
+ conn: sqlite3.Connection,
222
+ *,
223
+ profile_id: str | None = None,
224
+ limit: int = 100,
225
+ max_attempts: int = 10,
226
+ ) -> tuple[str, ...]:
227
+ where = "m.operation_id IS NULL AND o.attempts < ?"
228
+ params: list[Any] = [max_attempts]
229
+ if profile_id is not None:
230
+ where += " AND o.profile_id = ?"
231
+ params.append(profile_id)
232
+ params.append(limit)
233
+ rows = conn.execute(
234
+ f"SELECT DISTINCT o.operation_id FROM projection_obligations o "
235
+ f"LEFT JOIN completion_manifests m ON m.operation_id = o.operation_id "
236
+ f"WHERE {where} ORDER BY o.operation_id LIMIT ?",
237
+ tuple(params),
238
+ ).fetchall()
239
+ return tuple(row[0] for row in rows)
240
+
241
+
242
+ def _row_to_obligation(row: Any) -> Obligation:
243
+ detail_raw = row[8]
244
+ detail: Mapping[str, Any] | None
245
+ if detail_raw is None:
246
+ detail = None
247
+ else:
248
+ try:
249
+ parsed = json.loads(detail_raw)
250
+ detail = parsed if isinstance(parsed, dict) else {"value": parsed}
251
+ except (TypeError, json.JSONDecodeError):
252
+ detail = None
253
+ return Obligation(
254
+ operation_id=row[0],
255
+ profile_id=row[1],
256
+ owner=row[2],
257
+ kind=ObligationKind(row[3]),
258
+ subject_id=row[4],
259
+ revision=int(row[5]),
260
+ state=ObligationState(row[6]),
261
+ checksum=row[7],
262
+ detail=detail,
263
+ attempts=int(row[9]),
264
+ context_digest=row[10],
265
+ verify_attempts=int(row[11]) if row[11] is not None else 0,
266
+ last_verified_at=float(row[12]) if row[12] is not None else None,
267
+ created_at=float(row[13]),
268
+ updated_at=float(row[14]),
269
+ )
270
+
271
+
272
+ __all__ = ["Obligation", "ObligationConflictError", "ObligationLedger", "context_digest"]