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,542 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """OperationPolicyRegistry — declarative admission / policy layer (V4 Phase 4).
5
+
6
+ Design contract (NON-NEGOTIABLE):
7
+ 1. evaluate() is PURE and CPU-ONLY. No file, network, or DB access. Microseconds.
8
+ 2. Default policy is ADDITIVE: every operation kind that succeeds today continues
9
+ to succeed after this module is imported. The registry adds checks on top of
10
+ the existing trust hook + RBAC + ingest gate — it does NOT replace them.
11
+ 3. Unknown kind: fail-OPEN in local/single-user mode (annotate with audit=True).
12
+ fail-CLOSED in company/remote mode (deny with reason string).
13
+ 4. Payload > max_payload_bytes: ANNOTATED only — never rejected here. The ingest
14
+ gate already enforces the 1 MiB hard cap; double-rejection is forbidden.
15
+ 5. The module-level _DEFAULT_REGISTRY singleton is the safe default for all
16
+ call sites. It never rejects any REMEMBER that the existing stack accepts.
17
+
18
+ Part of SuperLocalMemory V4 | Phase 4: Admission/Policy Layer
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import types
24
+ from dataclasses import dataclass, field
25
+
26
+ from superlocalmemory.core.actor_context import ActorContext, ActorRole, Transport
27
+ from superlocalmemory.core.operation_policy import (
28
+ _ADMIN_TRANSPORTS,
29
+ _ALL_TRANSPORTS,
30
+ _MESH_TRANSPORTS,
31
+ OperationPolicy,
32
+ )
33
+ from superlocalmemory.core.operation_request import OperationKind
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Mode sentinel sets — used by evaluate() to decide unknown-kind behaviour.
37
+ # ---------------------------------------------------------------------------
38
+
39
+ _LOCAL_MODES: frozenset[str] = frozenset({
40
+ "local", "single-user", "single_user", "personal",
41
+ })
42
+ _COMPANY_MODES: frozenset[str] = frozenset({
43
+ "company", "remote", "multi-user", "multi_user", "enterprise",
44
+ })
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # PolicyDecision — the return value of evaluate()
48
+ # ---------------------------------------------------------------------------
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class PolicyDecision:
53
+ """Result of one OperationPolicyRegistry.evaluate() call.
54
+
55
+ ``allowed`` Whether the operation is admitted by policy.
56
+ ``reason`` Machine-readable reason code (never a user-visible message).
57
+ ``annotations`` Supplementary key/value pairs for audit, telemetry, and
58
+ downstream enrichment. Exposed as a read-only mapping so a
59
+ frozen decision cannot be mutated after construction.
60
+ """
61
+
62
+ allowed: bool
63
+ reason: str
64
+ annotations: dict = field(default_factory=dict)
65
+
66
+ def __post_init__(self) -> None:
67
+ # Freeze the annotations mapping so the whole decision is immutable
68
+ # (the outer dataclass is frozen; this closes the nested-dict hole).
69
+ from types import MappingProxyType
70
+
71
+ if not isinstance(self.annotations, MappingProxyType):
72
+ object.__setattr__(
73
+ self, "annotations", MappingProxyType(dict(self.annotations))
74
+ )
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # OperationPolicyRegistry
79
+ # ---------------------------------------------------------------------------
80
+
81
+
82
+ class OperationPolicyRegistry:
83
+ """Declarative, immutable registry mapping OperationKind → OperationPolicy.
84
+
85
+ Construction
86
+ ------------
87
+ Use ``OperationPolicyRegistry.default()`` to obtain the safe production
88
+ default. All known OperationKind values are pre-populated with policies
89
+ that preserve the current single-user allow behaviour.
90
+
91
+ Immutability
92
+ ------------
93
+ The internal policy table is wrapped in ``types.MappingProxyType`` to
94
+ prevent external mutation. ``register_deny`` and ``with_policy`` return
95
+ NEW registry instances (immutable-update pattern).
96
+
97
+ evaluate() contract
98
+ -------------------
99
+ - Pure: no I/O, no global mutation, no side effects.
100
+ - Fast: dict lookup + frozenset.isdisjoint() + field comparisons.
101
+ - Thread-safe: all state is read-only after construction.
102
+ """
103
+
104
+ def __init__(
105
+ self,
106
+ policies: dict[OperationKind, OperationPolicy],
107
+ explicit_denies: frozenset[OperationKind] = frozenset(),
108
+ ) -> None:
109
+ # MappingProxyType enforces read-only access after construction.
110
+ self._policies: types.MappingProxyType[
111
+ OperationKind, OperationPolicy
112
+ ] = types.MappingProxyType(dict(policies))
113
+ self._explicit_denies: frozenset[OperationKind] = frozenset(explicit_denies)
114
+
115
+ # ------------------------------------------------------------------
116
+ # Constructors
117
+ # ------------------------------------------------------------------
118
+
119
+ @classmethod
120
+ def default(cls) -> OperationPolicyRegistry:
121
+ """Build the production-default registry.
122
+
123
+ Every known OperationKind is assigned a policy whose required_roles
124
+ includes ActorRole.OWNER and allowed_transports includes
125
+ Transport.INTERNAL — so the existing in-process Python API path and the
126
+ single-user HTTP path continue to be admitted without any new rejection.
127
+
128
+ Policy table rationale
129
+ ----------------------
130
+ REMEMBER / RECALL Core R/W — open to OWNER, ADMIN, MEMBER.
131
+ FORGET / CORRECT Reversible mutations — OWNER and ADMIN only.
132
+ ERASE Irreversible — OWNER only.
133
+ CONSOLIDATE / ARCHIVE Maintenance — OWNER and ADMIN.
134
+ BACKUP / RESTORE_BACKUP Administrative — OWNER only, any transport.
135
+ MESH_* Collaboration — OWNER, ADMIN, MEMBER; mesh transports.
136
+ PROVIDER_TEST Internal probing — OWNER and ADMIN; CLI/INTERNAL only.
137
+ MODE_CHANGE System config — OWNER only; admin transports only.
138
+ PROFILE_SWITCH Profile ops — OWNER and ADMIN; all transports.
139
+ SCHEMA_MIGRATE Dangerous DDL — OWNER only; CLI/INTERNAL only.
140
+ VECTOR_MIGRATE Index DDL — OWNER and ADMIN; CLI/INTERNAL only.
141
+ """
142
+ _owner_only = frozenset({ActorRole.OWNER})
143
+ _owner_admin = frozenset({ActorRole.OWNER, ActorRole.ADMIN})
144
+ _owner_admin_member = frozenset({
145
+ ActorRole.OWNER, ActorRole.ADMIN, ActorRole.MEMBER,
146
+ })
147
+ _all_reads = frozenset({
148
+ ActorRole.OWNER, ActorRole.ADMIN, ActorRole.MEMBER, ActorRole.VIEWER,
149
+ })
150
+
151
+ policies: dict[OperationKind, OperationPolicy] = {
152
+ OperationKind.REMEMBER: OperationPolicy(
153
+ kind=OperationKind.REMEMBER,
154
+ required_roles=_owner_admin_member,
155
+ required_authentication=True,
156
+ allowed_transports=_ALL_TRANSPORTS,
157
+ audit_level="standard",
158
+ ),
159
+ OperationKind.RECALL: OperationPolicy(
160
+ kind=OperationKind.RECALL,
161
+ required_roles=_all_reads,
162
+ required_authentication=True,
163
+ allowed_transports=_ALL_TRANSPORTS,
164
+ audit_level="none",
165
+ ),
166
+ OperationKind.FORGET: OperationPolicy(
167
+ kind=OperationKind.FORGET,
168
+ required_roles=_owner_admin,
169
+ required_authentication=True,
170
+ allowed_transports=_ALL_TRANSPORTS,
171
+ audit_level="full",
172
+ ),
173
+ OperationKind.ARCHIVE: OperationPolicy(
174
+ kind=OperationKind.ARCHIVE,
175
+ required_roles=_owner_admin,
176
+ required_authentication=True,
177
+ allowed_transports=_ALL_TRANSPORTS,
178
+ audit_level="standard",
179
+ ),
180
+ OperationKind.RESTORE: OperationPolicy(
181
+ kind=OperationKind.RESTORE,
182
+ required_roles=_owner_admin,
183
+ required_authentication=True,
184
+ allowed_transports=_ALL_TRANSPORTS,
185
+ audit_level="standard",
186
+ ),
187
+ OperationKind.CORRECT: OperationPolicy(
188
+ kind=OperationKind.CORRECT,
189
+ required_roles=_owner_admin,
190
+ required_authentication=True,
191
+ allowed_transports=_ALL_TRANSPORTS,
192
+ audit_level="full",
193
+ ),
194
+ OperationKind.ERASE: OperationPolicy(
195
+ kind=OperationKind.ERASE,
196
+ required_roles=_owner_only,
197
+ required_authentication=True,
198
+ allowed_transports=_ALL_TRANSPORTS,
199
+ audit_level="full",
200
+ redaction_policy="full",
201
+ resource_ownership_check=True,
202
+ ),
203
+ OperationKind.CONSOLIDATE: OperationPolicy(
204
+ kind=OperationKind.CONSOLIDATE,
205
+ required_roles=_owner_admin,
206
+ required_authentication=True,
207
+ allowed_transports=_ALL_TRANSPORTS,
208
+ audit_level="standard",
209
+ ),
210
+ OperationKind.BACKUP: OperationPolicy(
211
+ kind=OperationKind.BACKUP,
212
+ required_roles=_owner_only,
213
+ required_authentication=True,
214
+ allowed_transports=_ALL_TRANSPORTS,
215
+ audit_level="standard",
216
+ ),
217
+ OperationKind.RESTORE_BACKUP: OperationPolicy(
218
+ kind=OperationKind.RESTORE_BACKUP,
219
+ required_roles=_owner_only,
220
+ required_authentication=True,
221
+ allowed_transports=_ALL_TRANSPORTS,
222
+ audit_level="full",
223
+ ),
224
+ OperationKind.MESH_SEND: OperationPolicy(
225
+ kind=OperationKind.MESH_SEND,
226
+ required_roles=_owner_admin_member,
227
+ required_authentication=True,
228
+ allowed_transports=_MESH_TRANSPORTS,
229
+ audit_level="standard",
230
+ ),
231
+ OperationKind.MESH_LOCK: OperationPolicy(
232
+ kind=OperationKind.MESH_LOCK,
233
+ required_roles=_owner_admin,
234
+ required_authentication=True,
235
+ allowed_transports=frozenset({
236
+ Transport.MESH, Transport.INTERNAL, Transport.MCP,
237
+ }),
238
+ audit_level="full",
239
+ ),
240
+ OperationKind.PROVIDER_TEST: OperationPolicy(
241
+ kind=OperationKind.PROVIDER_TEST,
242
+ required_roles=_owner_admin,
243
+ required_authentication=True,
244
+ allowed_transports=frozenset({
245
+ Transport.CLI, Transport.INTERNAL, Transport.DASHBOARD,
246
+ }),
247
+ audit_level="standard",
248
+ ),
249
+ OperationKind.MODE_CHANGE: OperationPolicy(
250
+ kind=OperationKind.MODE_CHANGE,
251
+ required_roles=_owner_only,
252
+ required_authentication=True,
253
+ allowed_transports=_ADMIN_TRANSPORTS | frozenset({Transport.MCP}),
254
+ audit_level="full",
255
+ ),
256
+ OperationKind.PROFILE_SWITCH: OperationPolicy(
257
+ kind=OperationKind.PROFILE_SWITCH,
258
+ required_roles=_owner_admin,
259
+ required_authentication=True,
260
+ allowed_transports=_ALL_TRANSPORTS,
261
+ audit_level="standard",
262
+ ),
263
+ OperationKind.SCHEMA_MIGRATE: OperationPolicy(
264
+ kind=OperationKind.SCHEMA_MIGRATE,
265
+ required_roles=_owner_only,
266
+ required_authentication=True,
267
+ allowed_transports=frozenset({Transport.CLI, Transport.INTERNAL}),
268
+ audit_level="full",
269
+ ),
270
+ OperationKind.VECTOR_MIGRATE: OperationPolicy(
271
+ kind=OperationKind.VECTOR_MIGRATE,
272
+ required_roles=_owner_admin,
273
+ required_authentication=True,
274
+ allowed_transports=frozenset({Transport.CLI, Transport.INTERNAL}),
275
+ audit_level="full",
276
+ ),
277
+ OperationKind.EVOLVE_SKILL: OperationPolicy(
278
+ kind=OperationKind.EVOLVE_SKILL,
279
+ required_roles=_owner_admin,
280
+ required_authentication=True,
281
+ allowed_transports=_ALL_TRANSPORTS,
282
+ audit_level="full",
283
+ ),
284
+ # Operational recovery & admin remediation (Wave-3 resilience slice)
285
+ # OPS_INSPECT: read-only listing of failed/stuck/degraded ops.
286
+ # Allowed over all transports so dashboard, MCP, and CLI all work.
287
+ OperationKind.OPS_INSPECT: OperationPolicy(
288
+ kind=OperationKind.OPS_INSPECT,
289
+ required_roles=_owner_admin,
290
+ required_authentication=True,
291
+ allowed_transports=_ALL_TRANSPORTS,
292
+ audit_level="standard",
293
+ ),
294
+ # OPS_RESOLVE: retry/force-reconcile/cancel a failed operation.
295
+ # Write permission — OWNER/ADMIN only, full audit trail.
296
+ OperationKind.OPS_RESOLVE: OperationPolicy(
297
+ kind=OperationKind.OPS_RESOLVE,
298
+ required_roles=_owner_admin,
299
+ required_authentication=True,
300
+ allowed_transports=_ALL_TRANSPORTS,
301
+ audit_level="full",
302
+ ),
303
+ }
304
+ return cls(policies)
305
+
306
+ # ------------------------------------------------------------------
307
+ # Immutable-update helpers
308
+ # ------------------------------------------------------------------
309
+
310
+ def with_policy(self, policy: OperationPolicy) -> OperationPolicyRegistry:
311
+ """Return a new registry with one policy replaced or added."""
312
+ updated = dict(self._policies)
313
+ updated[policy.kind] = policy
314
+ return OperationPolicyRegistry(updated, self._explicit_denies)
315
+
316
+ def register_deny(self, kind: OperationKind) -> OperationPolicyRegistry:
317
+ """Return a new registry that explicitly denies ``kind`` regardless of actor."""
318
+ return OperationPolicyRegistry(
319
+ dict(self._policies),
320
+ self._explicit_denies | {kind},
321
+ )
322
+
323
+ # ------------------------------------------------------------------
324
+ # Queries
325
+ # ------------------------------------------------------------------
326
+
327
+ def coverage(self) -> dict[str, dict]:
328
+ """Return a coverage summary for every known OperationKind.
329
+
330
+ Returns a mapping of {kind_value: {"has_policy": bool}} for every
331
+ value in OperationKind. Used by the startup self-check and unit tests.
332
+
333
+ Example::
334
+
335
+ cov = _DEFAULT_REGISTRY.coverage()
336
+ assert cov["remember"]["has_policy"] is True
337
+ """
338
+ result: dict[str, dict] = {}
339
+ for kind in OperationKind:
340
+ has_policy = kind in self._policies
341
+ entry: dict = {"has_policy": has_policy, "kind": kind.value}
342
+ if has_policy:
343
+ policy = self._policies[kind]
344
+ entry["has_transports"] = len(policy.allowed_transports) > 0
345
+ else:
346
+ entry["has_transports"] = False
347
+ result[kind.value] = entry
348
+ return result
349
+
350
+ def get_policy(
351
+ self,
352
+ kind: OperationKind,
353
+ transport: Transport | None = None, # reserved for future per-transport overrides
354
+ ) -> OperationPolicy | None:
355
+ """Return the policy for ``kind``, or None if no policy is registered.
356
+
357
+ The ``transport`` argument is reserved for future per-transport policy
358
+ overrides. In V4 the table is keyed only by kind; the argument is
359
+ accepted but not used for lookup.
360
+ """
361
+ return self._policies.get(kind)
362
+
363
+ # ------------------------------------------------------------------
364
+ # Core: pure, CPU-only evaluation
365
+ # ------------------------------------------------------------------
366
+
367
+ def evaluate(
368
+ self,
369
+ kind: OperationKind | str,
370
+ actor: ActorContext,
371
+ mode: str = "local",
372
+ *,
373
+ payload_bytes: int = 0,
374
+ ) -> PolicyDecision:
375
+ """Evaluate whether ``actor`` may perform ``kind`` under ``mode``.
376
+
377
+ This method is PURE. No I/O of any kind. Microseconds.
378
+
379
+ Parameters
380
+ ----------
381
+ kind The operation being requested. May be an OperationKind
382
+ enum value or a raw string (unknown strings are treated
383
+ as unknown kinds).
384
+ actor Server-derived actor identity. NEVER from request body.
385
+ mode Deployment mode: "local" / "single-user" → fail-open for
386
+ unknown kinds. "company" / "remote" → fail-closed.
387
+ Defaults to "local" (safe for internal path).
388
+ payload_bytes Optional advisory payload size. If > policy.max_payload_bytes,
389
+ the decision is annotated but NOT denied (double-rejection
390
+ is forbidden — the ingest gate already enforces the cap).
391
+
392
+ Returns
393
+ -------
394
+ PolicyDecision ``allowed=True`` → proceed; ``allowed=False`` → deny.
395
+ Never raises; all errors produce ``allowed=False``.
396
+ """
397
+ annotations: dict = {}
398
+
399
+ # ----------------------------------------------------------------
400
+ # Step 1: Resolve kind string → OperationKind enum.
401
+ # ----------------------------------------------------------------
402
+ if not isinstance(kind, OperationKind):
403
+ try:
404
+ kind = OperationKind(str(kind))
405
+ except ValueError:
406
+ return self._unknown_kind_decision(str(kind), mode)
407
+
408
+ # ----------------------------------------------------------------
409
+ # Step 2: Check explicit deny list (highest priority).
410
+ # ----------------------------------------------------------------
411
+ if kind in self._explicit_denies:
412
+ return PolicyDecision(
413
+ allowed=False,
414
+ reason="explicit_deny",
415
+ annotations={"kind": kind.value},
416
+ )
417
+
418
+ # ----------------------------------------------------------------
419
+ # Step 3: Look up policy for this kind.
420
+ # ----------------------------------------------------------------
421
+ policy = self._policies.get(kind)
422
+ if policy is None:
423
+ # Known enum value but no policy registered — treat as unknown.
424
+ return self._unknown_kind_decision(kind.value, mode)
425
+
426
+ # ----------------------------------------------------------------
427
+ # Step 4: Role check — actor must hold at least one required role.
428
+ # An empty required_roles set means "deny all" (explicit deny policy).
429
+ # ----------------------------------------------------------------
430
+ if not policy.required_roles:
431
+ return PolicyDecision(
432
+ allowed=False,
433
+ reason="policy_denies_all_roles",
434
+ annotations={"kind": kind.value},
435
+ )
436
+
437
+ # ----------------------------------------------------------------
438
+ # Step 5: Authentication before authorization — an unauthenticated
439
+ # actor is rejected for authentication, not role, so the reason is
440
+ # actionable.
441
+ # ----------------------------------------------------------------
442
+ if policy.required_authentication and not actor.is_authenticated:
443
+ return PolicyDecision(
444
+ allowed=False,
445
+ reason="authentication_required",
446
+ annotations={"kind": kind.value, "principal_id": actor.principal_id},
447
+ )
448
+
449
+ # ----------------------------------------------------------------
450
+ # Step 6: Role check — actor must hold at least one required role.
451
+ # ----------------------------------------------------------------
452
+ if actor.roles.isdisjoint(policy.required_roles):
453
+ return PolicyDecision(
454
+ allowed=False,
455
+ reason="insufficient_roles",
456
+ annotations={
457
+ "kind": kind.value,
458
+ "actor_roles": sorted(r.value for r in actor.roles),
459
+ "required_roles": sorted(r.value for r in policy.required_roles),
460
+ },
461
+ )
462
+
463
+ # ----------------------------------------------------------------
464
+ # Step 7: Transport check.
465
+ # ----------------------------------------------------------------
466
+ if actor.transport not in policy.allowed_transports:
467
+ return PolicyDecision(
468
+ allowed=False,
469
+ reason="transport_not_allowed",
470
+ annotations={
471
+ "kind": kind.value,
472
+ "actor_transport": actor.transport.value,
473
+ "allowed_transports": sorted(
474
+ t.value for t in policy.allowed_transports
475
+ ),
476
+ },
477
+ )
478
+
479
+ # ----------------------------------------------------------------
480
+ # Step 7: Payload size — annotate only, never reject.
481
+ # The ingest gate already enforces the 1 MiB hard cap. Annotating
482
+ # here gives the audit log a signal without double-rejecting.
483
+ # ----------------------------------------------------------------
484
+ if payload_bytes > 0 and payload_bytes > policy.max_payload_bytes:
485
+ annotations["payload_oversized"] = True
486
+ annotations["payload_bytes"] = payload_bytes
487
+ annotations["max_payload_bytes"] = policy.max_payload_bytes
488
+
489
+ # ----------------------------------------------------------------
490
+ # Step 8: Admit.
491
+ # ----------------------------------------------------------------
492
+ return PolicyDecision(
493
+ allowed=True,
494
+ reason="allow",
495
+ annotations=annotations,
496
+ )
497
+
498
+ # ------------------------------------------------------------------
499
+ # Private helpers
500
+ # ------------------------------------------------------------------
501
+
502
+ def _unknown_kind_decision(self, kind_raw: str, mode: str) -> PolicyDecision:
503
+ """Fail-open in local mode, fail-closed in company/remote mode."""
504
+ if mode in _LOCAL_MODES:
505
+ return PolicyDecision(
506
+ allowed=True,
507
+ reason="unknown_kind_allow_local",
508
+ annotations={"audit": True, "kind_raw": kind_raw},
509
+ )
510
+ # Company / remote mode: fail closed. An unknown kind may be a
511
+ # future privileged operation or a typo from an untrusted caller.
512
+ return PolicyDecision(
513
+ allowed=False,
514
+ reason="unknown_kind_deny_company",
515
+ annotations={"kind_raw": kind_raw},
516
+ )
517
+
518
+ def __repr__(self) -> str:
519
+ return (
520
+ f"OperationPolicyRegistry("
521
+ f"kinds={sorted(k.value for k in self._policies)}, "
522
+ f"explicit_denies={sorted(k.value for k in self._explicit_denies)})"
523
+ )
524
+
525
+
526
+ # ---------------------------------------------------------------------------
527
+ # Module-level singleton — safe default for all wire-up call sites.
528
+ #
529
+ # CRIT: This singleton is constructed ONCE at import time. It is read-only
530
+ # (MappingProxyType internal storage). Thread-safe by construction.
531
+ # The default evaluate() for REMEMBER + OWNER + local ALWAYS returns
532
+ # allowed=True — zero new rejections on the existing happy path.
533
+ # ---------------------------------------------------------------------------
534
+
535
+ _DEFAULT_REGISTRY: OperationPolicyRegistry = OperationPolicyRegistry.default()
536
+
537
+
538
+ __all__ = [
539
+ "OperationPolicyRegistry",
540
+ "PolicyDecision",
541
+ "_DEFAULT_REGISTRY",
542
+ ]
@@ -0,0 +1,127 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """Admission envelope (OperationRequest) and status types for V4.
5
+
6
+ OperationRequest is the typed, immutable boundary between every caller and the
7
+ policy/ingestion layer. The registry uses only the fields it can evaluate
8
+ purely in memory (kind, actor, scope, payload_hash) — no I/O.
9
+
10
+ Part of SuperLocalMemory V4 | Phase 4: Admission/Policy Layer
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import uuid
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime, timezone
18
+ from enum import Enum
19
+ from typing import TYPE_CHECKING
20
+
21
+ from superlocalmemory.core.actor_context import ActorContext, ActorRole, Transport
22
+
23
+ if TYPE_CHECKING:
24
+ pass # reserved for future typing imports
25
+
26
+
27
+ class OperationKind(str, Enum):
28
+ """Canonical names for every mutation kind the system accepts.
29
+
30
+ The registry stores one OperationPolicy per OperationKind. An operation
31
+ whose kind is absent from the registry is treated as "unknown" and
32
+ evaluated by the mode-specific fallback rule (fail-open local /
33
+ fail-closed company).
34
+ """
35
+
36
+ REMEMBER = "remember"
37
+ RECALL = "recall"
38
+ FORGET = "forget"
39
+ ARCHIVE = "archive"
40
+ RESTORE = "restore"
41
+ CORRECT = "correct"
42
+ ERASE = "erase"
43
+ CONSOLIDATE = "consolidate"
44
+ BACKUP = "backup"
45
+ RESTORE_BACKUP = "restore_backup"
46
+ MESH_SEND = "mesh_send"
47
+ MESH_LOCK = "mesh_lock"
48
+ PROVIDER_TEST = "provider_test"
49
+ MODE_CHANGE = "mode_change"
50
+ PROFILE_SWITCH = "profile_switch"
51
+ SCHEMA_MIGRATE = "schema_migrate"
52
+ VECTOR_MIGRATE = "vector_migrate"
53
+ EVOLVE_SKILL = "evolve_skill"
54
+ # Operational recovery & admin remediation (V4 Wave-3 resilience slice)
55
+ OPS_INSPECT = "ops_inspect" # List failed/stuck/degraded operations (OWNER/ADMIN)
56
+ OPS_RESOLVE = "ops_resolve" # Retry/force-reconcile/cancel an operation (OWNER/ADMIN)
57
+
58
+
59
+ class OperationStatus(str, Enum):
60
+ """Lifecycle states for a durable operation."""
61
+
62
+ ACCEPTED = "accepted" # Journaled, not yet committed
63
+ COMMITTED = "committed" # Written to canonical store
64
+ PROJECTING = "projecting" # Running external projections
65
+ COMPLETE = "complete" # All obligations met
66
+ DEGRADED = "degraded" # Some projections failed, retryable
67
+ FAILED = "failed" # Non-retryable failure
68
+ ROLLED_BACK = "rolled_back" # Compensation complete
69
+
70
+
71
+ def _default_actor() -> ActorContext:
72
+ """Zero-value ActorContext for the internal Python-API path.
73
+
74
+ Used as the default in OperationRequest when a caller does not supply an
75
+ explicit actor. The principal_id "local-operator" signals an in-process
76
+ call but is NOT authenticated (no session token). Callers that need a
77
+ fully-authenticated context must supply one explicitly.
78
+ """
79
+ return ActorContext(
80
+ principal_id="local-operator",
81
+ roles=frozenset({ActorRole.OWNER}),
82
+ transport=Transport.INTERNAL,
83
+ client_host="",
84
+ )
85
+
86
+
87
+ @dataclass(frozen=True, slots=True)
88
+ class OperationRequest:
89
+ """Immutable admission envelope for every mutation entering the system.
90
+
91
+ Construction rules (NON-NEGOTIABLE)
92
+ ------------------------------------
93
+ 1. ``actor`` is ALWAYS server-derived — session store, RBAC, daemon
94
+ descriptor. NEVER populated from the HTTP/MCP request body.
95
+ 2. ``operation_id`` is auto-generated when omitted (UUID hex).
96
+ 3. ``idempotency_key`` falls back to ``operation_id`` in __post_init__.
97
+ 4. ``payload_hash`` is the SHA-256 of the raw content before any scrubbing;
98
+ callers may leave it empty for lightweight envelopes.
99
+ """
100
+
101
+ operation_id: str = field(default_factory=lambda: uuid.uuid4().hex)
102
+ idempotency_key: str = ""
103
+ kind: OperationKind = OperationKind.REMEMBER
104
+ actor: ActorContext = field(default_factory=_default_actor)
105
+ profile_id: str = ""
106
+ profile_generation: int = 0
107
+ resource_ids: tuple[str, ...] = ()
108
+ scope: str = "personal"
109
+ deadline_ms: int = 30_000
110
+ schema_capability: str = ""
111
+ payload_hash: str = ""
112
+ trace_id: str = field(default_factory=lambda: uuid.uuid4().hex[:16])
113
+ created_at: str = field(
114
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
115
+ )
116
+
117
+ def __post_init__(self) -> None:
118
+ # Guarantee: idempotency_key is never empty after construction.
119
+ if not self.idempotency_key:
120
+ object.__setattr__(self, "idempotency_key", self.operation_id)
121
+
122
+
123
+ __all__ = [
124
+ "OperationKind",
125
+ "OperationRequest",
126
+ "OperationStatus",
127
+ ]