superlocalmemory 3.8.3 → 3.8.6
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/CHANGELOG.md +76 -0
- package/README.md +3 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +9 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/rbac.py +68 -76
- package/src/superlocalmemory/cli/commands.py +158 -404
- package/src/superlocalmemory/cli/ingest_cmd.py +11 -1
- package/src/superlocalmemory/cli/main.py +30 -0
- package/src/superlocalmemory/cli/pending_store.py +39 -14
- package/src/superlocalmemory/core/backend_orchestrator.py +93 -0
- package/src/superlocalmemory/core/component_registry.py +4 -2
- package/src/superlocalmemory/core/config.py +78 -0
- package/src/superlocalmemory/core/consolidation_engine.py +79 -73
- package/src/superlocalmemory/core/embeddings.py +33 -6
- package/src/superlocalmemory/core/engine.py +186 -60
- package/src/superlocalmemory/core/engine_ingestion.py +150 -63
- package/src/superlocalmemory/core/fact_consolidator.py +148 -30
- package/src/superlocalmemory/core/graph_pruner.py +436 -39
- package/src/superlocalmemory/core/ingestion_command.py +273 -32
- package/src/superlocalmemory/core/maintenance_scheduler.py +61 -1
- package/src/superlocalmemory/core/mutations.py +32 -10
- package/src/superlocalmemory/core/recall_pipeline.py +111 -74
- package/src/superlocalmemory/core/registry.py +5 -1
- package/src/superlocalmemory/core/remember_admission.py +152 -0
- package/src/superlocalmemory/core/remember_runtime.py +712 -0
- package/src/superlocalmemory/core/remote_mode.py +3 -1
- package/src/superlocalmemory/core/scale_engine.py +41 -18
- package/src/superlocalmemory/core/store_pipeline.py +18 -4
- package/src/superlocalmemory/encoding/entity_resolver.py +18 -11
- package/src/superlocalmemory/graph/cozo_backend.py +5 -5
- package/src/superlocalmemory/hooks/_outcome_common.py +9 -2
- package/src/superlocalmemory/hooks/adapter_base.py +58 -44
- package/src/superlocalmemory/hooks/ide_connector.py +26 -8
- package/src/superlocalmemory/hooks/portable_kit.py +105 -9
- package/src/superlocalmemory/hooks/prewarm_auth.py +21 -2
- package/src/superlocalmemory/infra/auth_middleware.py +3 -1
- package/src/superlocalmemory/infra/cloud_backup.py +26 -27
- package/src/superlocalmemory/infra/event_bus.py +250 -88
- package/src/superlocalmemory/learning/bandit.py +50 -1
- package/src/superlocalmemory/learning/consolidation_cycle.py +33 -16
- package/src/superlocalmemory/learning/entity_compiler.py +148 -132
- package/src/superlocalmemory/learning/memory_merge.py +97 -82
- package/src/superlocalmemory/learning/reward_archive.py +98 -90
- package/src/superlocalmemory/learning/reward_boost.py +40 -30
- package/src/superlocalmemory/learning/source_quality.py +38 -35
- package/src/superlocalmemory/mcp/_daemon_proxy.py +38 -15
- package/src/superlocalmemory/mcp/http_transport.py +335 -3
- package/src/superlocalmemory/mcp/tools_active.py +4 -41
- package/src/superlocalmemory/mcp/tools_core.py +26 -87
- package/src/superlocalmemory/mcp/tools_evolution.py +5 -10
- package/src/superlocalmemory/optimize/proxy/capture.py +196 -8
- package/src/superlocalmemory/retrieval/engine.py +15 -4
- package/src/superlocalmemory/retrieval/entity_channel.py +25 -1
- package/src/superlocalmemory/retrieval/reranker.py +130 -22
- package/src/superlocalmemory/retrieval/spreading_activation.py +20 -12
- package/src/superlocalmemory/retrieval/vector_store.py +84 -69
- package/src/superlocalmemory/server/loopback.py +85 -0
- package/src/superlocalmemory/server/origin.py +9 -4
- package/src/superlocalmemory/server/profile_runtime.py +14 -0
- package/src/superlocalmemory/server/routes/abstraction.py +2 -4
- package/src/superlocalmemory/server/routes/agents.py +3 -5
- package/src/superlocalmemory/server/routes/backup.py +6 -2
- package/src/superlocalmemory/server/routes/behavioral.py +11 -25
- package/src/superlocalmemory/server/routes/brain.py +6 -9
- package/src/superlocalmemory/server/routes/compliance.py +20 -23
- package/src/superlocalmemory/server/routes/config_api.py +83 -0
- package/src/superlocalmemory/server/routes/entity.py +3 -7
- package/src/superlocalmemory/server/routes/evolution.py +3 -5
- package/src/superlocalmemory/server/routes/helpers.py +57 -25
- package/src/superlocalmemory/server/routes/insights.py +2 -4
- package/src/superlocalmemory/server/routes/learning.py +2 -5
- package/src/superlocalmemory/server/routes/lifecycle.py +2 -4
- package/src/superlocalmemory/server/routes/memories.py +119 -98
- package/src/superlocalmemory/server/routes/mesh.py +7 -2
- package/src/superlocalmemory/server/routes/profiles.py +20 -21
- package/src/superlocalmemory/server/routes/rbac.py +0 -1
- package/src/superlocalmemory/server/routes/tiers.py +28 -35
- package/src/superlocalmemory/server/routes/timeline.py +2 -4
- package/src/superlocalmemory/server/routes/v3_api.py +85 -93
- package/src/superlocalmemory/server/unified_daemon.py +400 -140
- package/src/superlocalmemory/server/write_identity.py +22 -4
- package/src/superlocalmemory/storage/admission_codec.py +119 -0
- package/src/superlocalmemory/storage/admission_journal.py +728 -0
- package/src/superlocalmemory/storage/database.py +168 -19
- package/src/superlocalmemory/storage/deferred_writes.py +209 -0
- package/src/superlocalmemory/storage/embedding_migrator.py +19 -0
- package/src/superlocalmemory/storage/memory_write.py +115 -0
- package/src/superlocalmemory/storage/migration_runner.py +44 -0
- package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +113 -78
- package/src/superlocalmemory/storage/migrations/M031_dead_letter_operations.py +80 -0
- package/src/superlocalmemory/storage/migrations/M032_write_coordinator_admission.py +188 -0
- package/src/superlocalmemory/storage/read_connection.py +115 -0
- package/src/superlocalmemory/storage/write_coordinator.py +756 -0
- package/src/superlocalmemory/storage/write_lock.py +88 -0
- package/src/superlocalmemory/ui/index.html +1 -1
- package/src/superlocalmemory/ui/js/auto-settings.js +14 -1
- package/src/superlocalmemory/ui/js/od-settings.js +9 -3
|
@@ -18,6 +18,8 @@ from typing import Any
|
|
|
18
18
|
|
|
19
19
|
from fastapi import HTTPException, Request
|
|
20
20
|
|
|
21
|
+
from superlocalmemory.server.loopback import is_loopback as _is_loopback_host
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
# H-04 (3.7.9): the test-client auth bypass must be impossible in a real daemon
|
|
23
25
|
# even if SLM_TEST_ISOLATION=1 leaks into a production environment. Anchor it to
|
|
@@ -89,7 +91,7 @@ def require_write_actor(
|
|
|
89
91
|
# missing peer address (e.g. behind a proxy that strips it) must not be
|
|
90
92
|
# trusted just because an install token is presented. require_http_mutation_actor
|
|
91
93
|
# already excludes "" from its loopback set; keep the two paths consistent.
|
|
92
|
-
if is_test_client or client_host
|
|
94
|
+
if is_test_client or _is_loopback_host(client_host):
|
|
93
95
|
from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
|
|
94
96
|
|
|
95
97
|
return local_trusted_actor_id(actor_kind)
|
|
@@ -103,7 +105,15 @@ def require_write_actor(
|
|
|
103
105
|
).hexdigest()
|
|
104
106
|
return f"api-key:{actor_kind}:{fingerprint}"
|
|
105
107
|
|
|
106
|
-
raise HTTPException(
|
|
108
|
+
raise HTTPException(
|
|
109
|
+
403,
|
|
110
|
+
detail=(
|
|
111
|
+
"Write rejected: this origin requires an API key (X-SLM-API-Key). "
|
|
112
|
+
"The install token (X-Install-Token) is accepted only from loopback "
|
|
113
|
+
"(127.x.x.x / ::1 / localhost). If calling from a container or "
|
|
114
|
+
"remote host, configure SLM_API_KEY and present it as X-SLM-API-Key."
|
|
115
|
+
),
|
|
116
|
+
)
|
|
107
117
|
|
|
108
118
|
|
|
109
119
|
def require_http_mutation_actor(
|
|
@@ -134,7 +144,7 @@ def require_http_mutation_actor(
|
|
|
134
144
|
|
|
135
145
|
client_host = request.client.host if request.client else ""
|
|
136
146
|
is_test_client = client_host == "testclient" and _TEST_ISOLATION_ALLOWED
|
|
137
|
-
loopback = client_host
|
|
147
|
+
loopback = _is_loopback_host(client_host)
|
|
138
148
|
# H-01: the loopback trusted-actor bypass is suppressed when the operator
|
|
139
149
|
# opts into SLM_REQUIRE_CREDENTIALS; in-process tests keep the bypass.
|
|
140
150
|
if is_test_client or (loopback and not _REQUIRE_CREDENTIALS):
|
|
@@ -153,7 +163,15 @@ def require_http_mutation_actor(
|
|
|
153
163
|
).hexdigest()
|
|
154
164
|
return f"mesh-secret:{fingerprint}"
|
|
155
165
|
|
|
156
|
-
raise HTTPException(
|
|
166
|
+
raise HTTPException(
|
|
167
|
+
403,
|
|
168
|
+
detail=(
|
|
169
|
+
"Mutation rejected: present one of X-SLM-Daemon-Capability, "
|
|
170
|
+
"X-Install-Token (loopback only), or X-SLM-API-Key. "
|
|
171
|
+
"Uncredentialed writes are accepted only from loopback. "
|
|
172
|
+
"See SLM_API_KEY in the deployment guide for network access."
|
|
173
|
+
),
|
|
174
|
+
)
|
|
157
175
|
|
|
158
176
|
|
|
159
177
|
def authenticated_request_actor(
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
"""Production encryption codec for durable remember admission commands."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import stat
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
14
|
+
|
|
15
|
+
from superlocalmemory.infra.data_root import state_path
|
|
16
|
+
|
|
17
|
+
_KEY_BYTES = 32
|
|
18
|
+
_NONCE_BYTES = 12
|
|
19
|
+
_FORMAT_VERSION = b"\x01"
|
|
20
|
+
_ASSOCIATED_DATA = b"superlocalmemory:remember-admission:v1"
|
|
21
|
+
_KEY_READ_ATTEMPTS = 100
|
|
22
|
+
_KEY_READ_RETRY_SECONDS = 0.005
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AdmissionKeyError(RuntimeError):
|
|
26
|
+
"""The machine-local admission key cannot be created or loaded safely."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MachineKeyCommandCodec:
|
|
30
|
+
"""AES-256-GCM codec backed by a machine-local, permission-bound key.
|
|
31
|
+
|
|
32
|
+
The key is intentionally separate from cache encryption keys. Exclusive
|
|
33
|
+
creation makes concurrent daemon startup converge on one key, while the
|
|
34
|
+
format byte and AEAD associated data prevent cross-protocol ciphertext use.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, key_path: str | Path | None = None) -> None:
|
|
38
|
+
self.key_path = Path(key_path) if key_path is not None else state_path(
|
|
39
|
+
"admission-key.bin"
|
|
40
|
+
)
|
|
41
|
+
self._key = _load_or_create_key(self.key_path)
|
|
42
|
+
|
|
43
|
+
def encrypt(self, plaintext: bytes) -> bytes:
|
|
44
|
+
if not isinstance(plaintext, bytes):
|
|
45
|
+
raise TypeError("plaintext must be bytes")
|
|
46
|
+
nonce = os.urandom(_NONCE_BYTES)
|
|
47
|
+
ciphertext = AESGCM(self._key).encrypt(nonce, plaintext, _ASSOCIATED_DATA)
|
|
48
|
+
return _FORMAT_VERSION + nonce + ciphertext
|
|
49
|
+
|
|
50
|
+
def decrypt(self, ciphertext: bytes) -> bytes:
|
|
51
|
+
if not isinstance(ciphertext, bytes):
|
|
52
|
+
raise TypeError("ciphertext must be bytes")
|
|
53
|
+
minimum = len(_FORMAT_VERSION) + _NONCE_BYTES + 16
|
|
54
|
+
if len(ciphertext) < minimum or ciphertext[:1] != _FORMAT_VERSION:
|
|
55
|
+
raise ValueError("unsupported admission ciphertext format")
|
|
56
|
+
nonce = ciphertext[1 : 1 + _NONCE_BYTES]
|
|
57
|
+
payload = ciphertext[1 + _NONCE_BYTES :]
|
|
58
|
+
return AESGCM(self._key).decrypt(nonce, payload, _ASSOCIATED_DATA)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _load_or_create_key(path: Path) -> bytes:
|
|
62
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
if os.name != "nt":
|
|
64
|
+
os.chmod(path.parent, 0o700)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
fd = os.open(
|
|
68
|
+
path,
|
|
69
|
+
os.O_WRONLY
|
|
70
|
+
| os.O_CREAT
|
|
71
|
+
| os.O_EXCL
|
|
72
|
+
| getattr(os, "O_BINARY", 0),
|
|
73
|
+
0o600,
|
|
74
|
+
)
|
|
75
|
+
except FileExistsError:
|
|
76
|
+
fd = -1
|
|
77
|
+
else:
|
|
78
|
+
try:
|
|
79
|
+
key = os.urandom(_KEY_BYTES)
|
|
80
|
+
view = memoryview(key)
|
|
81
|
+
written = 0
|
|
82
|
+
while written < _KEY_BYTES:
|
|
83
|
+
count = os.write(fd, view[written:])
|
|
84
|
+
if count <= 0:
|
|
85
|
+
raise AdmissionKeyError("admission key write was incomplete")
|
|
86
|
+
written += count
|
|
87
|
+
os.fsync(fd)
|
|
88
|
+
except BaseException:
|
|
89
|
+
try:
|
|
90
|
+
path.unlink()
|
|
91
|
+
except OSError:
|
|
92
|
+
pass
|
|
93
|
+
raise
|
|
94
|
+
finally:
|
|
95
|
+
os.close(fd)
|
|
96
|
+
|
|
97
|
+
for attempt in range(_KEY_READ_ATTEMPTS):
|
|
98
|
+
try:
|
|
99
|
+
info = path.lstat()
|
|
100
|
+
except OSError as exc:
|
|
101
|
+
raise AdmissionKeyError("admission key is unavailable") from exc
|
|
102
|
+
if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode):
|
|
103
|
+
raise AdmissionKeyError("admission key path must be a regular file")
|
|
104
|
+
if os.name != "nt":
|
|
105
|
+
if info.st_uid != os.getuid():
|
|
106
|
+
raise AdmissionKeyError("admission key is not owned by the current user")
|
|
107
|
+
os.chmod(path, 0o600)
|
|
108
|
+
try:
|
|
109
|
+
key = path.read_bytes()
|
|
110
|
+
except OSError as exc:
|
|
111
|
+
raise AdmissionKeyError("admission key cannot be read") from exc
|
|
112
|
+
if len(key) == _KEY_BYTES:
|
|
113
|
+
return key
|
|
114
|
+
# A process that lost O_EXCL may observe the winning creator between
|
|
115
|
+
# file creation and its final fsync/close. Wait only for that bounded
|
|
116
|
+
# window; a persistently truncated or oversized key still fails closed.
|
|
117
|
+
if attempt + 1 < _KEY_READ_ATTEMPTS:
|
|
118
|
+
time.sleep(_KEY_READ_RETRY_SECONDS)
|
|
119
|
+
raise AdmissionKeyError("admission key has an invalid length")
|