superlocalmemory 3.7.7 → 3.8.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +85 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -2
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +96 -12
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +2 -0
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +35 -11
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/auth_middleware.py +28 -0
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +243 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +21 -49
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +50 -5
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +21 -4
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +72 -16
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +221 -49
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +194 -81
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +30 -5
- package/src/superlocalmemory/server/unified_daemon.py +431 -75
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v32.py +0 -9
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +2 -2
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -25,6 +25,7 @@ import json
|
|
|
25
25
|
import logging
|
|
26
26
|
import os
|
|
27
27
|
import socket
|
|
28
|
+
import sys
|
|
28
29
|
import threading
|
|
29
30
|
import time
|
|
30
31
|
from pathlib import Path
|
|
@@ -35,6 +36,12 @@ logger = logging.getLogger(__name__)
|
|
|
35
36
|
|
|
36
37
|
_DEFAULT_SOCK_NAME = "hook_daemon.sock"
|
|
37
38
|
|
|
39
|
+
# AF_UNIX is absent on Windows builds < 10.0.17063 and on Python < 3.9. When it
|
|
40
|
+
# is unavailable the hook daemon does not start and callers fall back to the
|
|
41
|
+
# subprocess recall path. Detect it explicitly (once, with a log) instead of
|
|
42
|
+
# relying on an AttributeError being swallowed by a broad except.
|
|
43
|
+
_AF_UNIX = getattr(socket, "AF_UNIX", None)
|
|
44
|
+
|
|
38
45
|
|
|
39
46
|
def _default_sock_path() -> Path:
|
|
40
47
|
return state_path(_DEFAULT_SOCK_NAME)
|
|
@@ -77,7 +84,13 @@ class HookDaemon:
|
|
|
77
84
|
from superlocalmemory.core.recall_queue import RecallQueue
|
|
78
85
|
self._queue = RecallQueue(self._queue_db_path)
|
|
79
86
|
|
|
80
|
-
|
|
87
|
+
if _AF_UNIX is None:
|
|
88
|
+
logger.info(
|
|
89
|
+
"HookDaemon: AF_UNIX unavailable on %s; hook recall uses the "
|
|
90
|
+
"subprocess fallback", sys.platform,
|
|
91
|
+
)
|
|
92
|
+
raise RuntimeError("AF_UNIX unavailable on this platform")
|
|
93
|
+
self._server_sock = socket.socket(_AF_UNIX, socket.SOCK_STREAM)
|
|
81
94
|
self._server_sock.bind(str(self._sock_path))
|
|
82
95
|
self._server_sock.listen(8)
|
|
83
96
|
self._server_sock.settimeout(1.0)
|
|
@@ -226,9 +239,11 @@ def try_socket_recall(
|
|
|
226
239
|
path = sock_path or _default_sock_path()
|
|
227
240
|
if not path.exists():
|
|
228
241
|
return None
|
|
242
|
+
if _AF_UNIX is None:
|
|
243
|
+
return None
|
|
229
244
|
|
|
230
245
|
try:
|
|
231
|
-
client = socket.socket(
|
|
246
|
+
client = socket.socket(_AF_UNIX, socket.SOCK_STREAM)
|
|
232
247
|
client.settimeout(timeout)
|
|
233
248
|
client.connect(str(path))
|
|
234
249
|
|
|
@@ -259,10 +274,12 @@ def ensure_hook_daemon(
|
|
|
259
274
|
) -> HookDaemon | None:
|
|
260
275
|
"""Start hook daemon if not already running. Returns daemon or None."""
|
|
261
276
|
path = sock_path or _default_sock_path()
|
|
277
|
+
if _AF_UNIX is None:
|
|
278
|
+
return None
|
|
262
279
|
|
|
263
280
|
if path.exists():
|
|
264
281
|
try:
|
|
265
|
-
test = socket.socket(
|
|
282
|
+
test = socket.socket(_AF_UNIX, socket.SOCK_STREAM)
|
|
266
283
|
test.settimeout(1.0)
|
|
267
284
|
test.connect(str(path))
|
|
268
285
|
test.close()
|
|
@@ -93,6 +93,58 @@ def memory_protocol_markdown() -> str:
|
|
|
93
93
|
"learned>\", \"tags\": \"<comma-separated kebab-case keywords>\"}`.\n"
|
|
94
94
|
"- A \"substantial task\" is anything you would write a commit "
|
|
95
95
|
"message or handoff note about — not every tool call.\n"
|
|
96
|
+
"\n"
|
|
97
|
+
+ optimize_protocol_markdown()
|
|
98
|
+
+ "\n"
|
|
99
|
+
+ loop_protocol_markdown()
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def optimize_protocol_markdown() -> str:
|
|
104
|
+
"""Return the agent-facing token-optimization protocol block (cache/compress).
|
|
105
|
+
|
|
106
|
+
Proxy-free: these MCP tools let the agent losslessly shrink large tool output
|
|
107
|
+
and reuse repeated reads through the same surface, so an agent on any
|
|
108
|
+
front-end (not just Claude Code) can save tokens without a proxy. Fail-open —
|
|
109
|
+
a non-ok result means "use the original and continue".
|
|
110
|
+
"""
|
|
111
|
+
return (
|
|
112
|
+
"## Runtime token-optimization protocol (fail-open)\n"
|
|
113
|
+
"SLM can losslessly compress large tool output and cache repeated reads "
|
|
114
|
+
"through the same MCP surface — no proxy required. These calls only save "
|
|
115
|
+
"tokens; if one returns `ok: false`, use the original and continue.\n\n"
|
|
116
|
+
"- **Large tool output (>2000 chars)** → `hub__call_tool` with "
|
|
117
|
+
"`tool=\"slm__slm_compress\"` and `arguments={\"content\": \"<text>\", "
|
|
118
|
+
"\"mode\": \"auto\", \"reversible\": true}`; keep the returned `ccr_id` "
|
|
119
|
+
"and call `tool=\"slm__slm_retrieve\"` if you later need the full "
|
|
120
|
+
"original.\n"
|
|
121
|
+
"- **Repeated reads/searches** → `hub__call_tool` with "
|
|
122
|
+
"`tool=\"slm__slm_cache_get\"` and `arguments={\"key\": \"file:<path>\"}` "
|
|
123
|
+
"first; on a miss, store the result with `tool=\"slm__slm_cache_set\"` "
|
|
124
|
+
"(ttl ~1800).\n"
|
|
125
|
+
"- **Never compress or cache**: code you will edit, JSON you will parse, "
|
|
126
|
+
"secrets, ccr_ids, or anything under ~500 chars.\n"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def loop_protocol_markdown() -> str:
|
|
131
|
+
"""Return the agent-facing bounded-loop protocol block.
|
|
132
|
+
|
|
133
|
+
Bounded loops make an agent stop when an INDEPENDENT gate passes — not when
|
|
134
|
+
the agent claims it is done. Persisting each lap to SLM memory makes a run
|
|
135
|
+
auditable and resumable. This block is appended to the shared memory
|
|
136
|
+
protocol so any connected front-end (Claude Code, Codex, Antigravity,
|
|
137
|
+
Cursor, and other IDEs) learns the feature exists and how to reach it.
|
|
138
|
+
"""
|
|
139
|
+
return (
|
|
140
|
+
"## Runtime bounded-loop protocol\n"
|
|
141
|
+
"For a task with a checkable gate (tests, schema, lint, reconciliation), "
|
|
142
|
+
"run a *bounded loop*: iterate until an INDEPENDENT gate passes — never "
|
|
143
|
+
"on the agent's own claim, which is advisory only. Try `slm loop demo`; "
|
|
144
|
+
"inspect with `slm loop history` / `slm loop show <run_id>` (each lap "
|
|
145
|
+
"persists as SLM memory, tag `loop:<name>`). Statuses: DONE / HALT / "
|
|
146
|
+
"PAUSE / KILLED / ERROR — report exactly, never as success unless DONE. "
|
|
147
|
+
"Full guide: the slm-loop skill.\n"
|
|
96
148
|
)
|
|
97
149
|
|
|
98
150
|
|
|
@@ -101,4 +153,6 @@ __all__ = (
|
|
|
101
153
|
"SLM_MARKER_END",
|
|
102
154
|
"strip_slm_block",
|
|
103
155
|
"memory_protocol_markdown",
|
|
156
|
+
"optimize_protocol_markdown",
|
|
157
|
+
"loop_protocol_markdown",
|
|
104
158
|
)
|
|
@@ -139,7 +139,7 @@ IDE_MATRIX: dict[str, IDEDescriptor] = {
|
|
|
139
139
|
mcp_path_project=".mcp.json",
|
|
140
140
|
server_key="mcpServers",
|
|
141
141
|
fmt="json",
|
|
142
|
-
agents_md_path=
|
|
142
|
+
agents_md_path=".junie/AGENTS.md", # Junie guidelines file (GA) — carries memory + optimize protocol
|
|
143
143
|
server_block={"command": "slm", "args": ["mcp"], "type": "stdio"},
|
|
144
144
|
caveats="path per product [CN-ONLINE]",
|
|
145
145
|
),
|
|
@@ -347,6 +347,54 @@ def connect_ide(
|
|
|
347
347
|
return result
|
|
348
348
|
|
|
349
349
|
|
|
350
|
+
def connect_many(
|
|
351
|
+
ide_ids: list[str],
|
|
352
|
+
*,
|
|
353
|
+
home: Path | None = None,
|
|
354
|
+
project: Path | None = None,
|
|
355
|
+
here: bool = False,
|
|
356
|
+
profile: str | None = None,
|
|
357
|
+
agents_md_source: Callable[[], str] | None = None,
|
|
358
|
+
) -> list[dict[str, Any]]:
|
|
359
|
+
"""Wire SLM into multiple IDE configs via non-destructive merge.
|
|
360
|
+
|
|
361
|
+
Iterates over ``ide_ids`` and calls :func:`connect_ide` for each entry.
|
|
362
|
+
Each IDE is processed independently — a failure on one IDE does NOT abort
|
|
363
|
+
the remaining targets.
|
|
364
|
+
|
|
365
|
+
The underlying :func:`connect_ide` is MERGE-NOT-CLOBBER:
|
|
366
|
+
- Only the ``superlocalmemory`` server key is touched.
|
|
367
|
+
- All other MCP servers + top-level keys are preserved byte-for-byte.
|
|
368
|
+
- Writes are atomic (.tmp + os.replace).
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
ide_ids: IDE ids to wire (from :data:`IDE_MATRIX`). Pass an empty
|
|
372
|
+
list to no-op. Unknown ids produce error entries in the output.
|
|
373
|
+
home: Override ``$HOME`` (test hook).
|
|
374
|
+
project: Project root for ``here=True`` installs.
|
|
375
|
+
here: When True, write to project-relative path instead of global.
|
|
376
|
+
profile: Inject ``SLM_MCP_PROFILE`` env-var into every server block.
|
|
377
|
+
agents_md_source: Callable returning AGENTS.md content to append.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
List of per-IDE result dicts, one per input id. Each dict has the
|
|
381
|
+
same shape as :func:`connect_ide`'s return value::
|
|
382
|
+
|
|
383
|
+
{ide, mcp_config, mcp_path, agents_md, servers_preserved, error}
|
|
384
|
+
"""
|
|
385
|
+
return [
|
|
386
|
+
connect_ide(
|
|
387
|
+
ide_id,
|
|
388
|
+
home=home,
|
|
389
|
+
project=project,
|
|
390
|
+
here=here,
|
|
391
|
+
profile=profile,
|
|
392
|
+
agents_md_source=agents_md_source,
|
|
393
|
+
)
|
|
394
|
+
for ide_id in ide_ids
|
|
395
|
+
]
|
|
396
|
+
|
|
397
|
+
|
|
350
398
|
# ---------------------------------------------------------------------------
|
|
351
399
|
# Internal helpers
|
|
352
400
|
# ---------------------------------------------------------------------------
|
|
@@ -504,3 +552,68 @@ def _handle_agents_md(
|
|
|
504
552
|
_tmp.write_text(existing + section, encoding="utf-8")
|
|
505
553
|
os.replace(_tmp, agents_path)
|
|
506
554
|
return "wrote"
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
# ---------------------------------------------------------------------------
|
|
558
|
+
# __main__ — `python -m superlocalmemory.hooks.portable_kit [ids...]`
|
|
559
|
+
#
|
|
560
|
+
# Used by the npm installer to execute non-destructive IDE connects when
|
|
561
|
+
# Python is available at postinstall time. If Python / the package is not
|
|
562
|
+
# importable the installer records a pending_ide_connections.json instead.
|
|
563
|
+
# ---------------------------------------------------------------------------
|
|
564
|
+
|
|
565
|
+
if __name__ == "__main__": # pragma: no cover
|
|
566
|
+
import argparse as _argparse
|
|
567
|
+
|
|
568
|
+
_parser = _argparse.ArgumentParser(
|
|
569
|
+
description="SLM IDE connector — non-destructive merge-not-clobber."
|
|
570
|
+
)
|
|
571
|
+
_parser.add_argument(
|
|
572
|
+
"ide_ids",
|
|
573
|
+
nargs="*",
|
|
574
|
+
metavar="IDE",
|
|
575
|
+
help=(
|
|
576
|
+
"IDE ids to connect. Pass 'all' to connect every supported IDE "
|
|
577
|
+
"(excluding claude-code which uses the WP-06 plugin)."
|
|
578
|
+
),
|
|
579
|
+
)
|
|
580
|
+
_parser.add_argument("--home", help="Override home directory (test hook).")
|
|
581
|
+
_parser.add_argument("--profile", help="SLM_MCP_PROFILE to inject.")
|
|
582
|
+
_parser.add_argument(
|
|
583
|
+
"--list", action="store_true",
|
|
584
|
+
help="Print supported IDE ids with display names and exit.",
|
|
585
|
+
)
|
|
586
|
+
_args = _parser.parse_args()
|
|
587
|
+
|
|
588
|
+
if _args.list:
|
|
589
|
+
for _id, _desc in IDE_MATRIX.items():
|
|
590
|
+
_flag = "[OUT]" if not _desc.fmt else ""
|
|
591
|
+
print(f" {_id:22s} {_desc.display} {_flag}".rstrip())
|
|
592
|
+
sys.exit(0)
|
|
593
|
+
|
|
594
|
+
_home = Path(_args.home) if _args.home else None
|
|
595
|
+
|
|
596
|
+
# Resolve targets: "all" → every MCP-capable IDE (fmt != ""), else explicit list.
|
|
597
|
+
if "all" in _args.ide_ids:
|
|
598
|
+
_targets = [k for k, d in IDE_MATRIX.items() if d.fmt]
|
|
599
|
+
else:
|
|
600
|
+
_targets = [t for t in _args.ide_ids if t]
|
|
601
|
+
|
|
602
|
+
if not _targets:
|
|
603
|
+
print("No IDE ids given. Pass ide ids or 'all'. Use --list to see options.")
|
|
604
|
+
sys.exit(0)
|
|
605
|
+
|
|
606
|
+
_results = connect_many(_targets, home=_home, profile=_args.profile)
|
|
607
|
+
_ok = 0
|
|
608
|
+
_fail = 0
|
|
609
|
+
for _r in _results:
|
|
610
|
+
_err = _r.get("error")
|
|
611
|
+
_status = _r.get("mcp_config", "error")
|
|
612
|
+
if _err:
|
|
613
|
+
print(f" ERROR {_r['ide']}: {_err}", file=sys.stderr)
|
|
614
|
+
_fail += 1
|
|
615
|
+
else:
|
|
616
|
+
print(f" {_status.upper():9s} {_r['ide']} → {_r.get('mcp_path', '')}")
|
|
617
|
+
_ok += 1
|
|
618
|
+
print(f"\n{_ok} connected, {_fail} errors.")
|
|
619
|
+
sys.exit(0 if _fail == 0 else 1)
|
|
@@ -15,6 +15,7 @@ V3 change: base directory moved from ``~/.claude-memory/`` to
|
|
|
15
15
|
import hashlib
|
|
16
16
|
import hmac
|
|
17
17
|
import logging
|
|
18
|
+
import os
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
from typing import Optional
|
|
20
21
|
|
|
@@ -26,6 +27,13 @@ logger = logging.getLogger("superlocalmemory.auth")
|
|
|
26
27
|
MEMORY_DIR = DynamicStatePath()
|
|
27
28
|
API_KEY_FILE = DynamicStatePath("api_key")
|
|
28
29
|
|
|
30
|
+
# v3.7.8 (F1): opt-in env flag that restores the pre-v3.7.6 shared-host
|
|
31
|
+
# posture -- when set AND an api_key file is configured, uncredentialed
|
|
32
|
+
# loopback writes must also present a matching X-SLM-API-Key. Default OFF
|
|
33
|
+
# preserves the v3.7.6 local-first fix (#71/#73/#74): loopback callers are
|
|
34
|
+
# trusted as the local OS-user boundary without needing any credential.
|
|
35
|
+
SLM_REQUIRE_API_KEY_LOOPBACK_ENV = "SLM_REQUIRE_API_KEY_LOOPBACK"
|
|
36
|
+
|
|
29
37
|
|
|
30
38
|
def _load_api_key_hash(key_file: Optional[Path] = None) -> Optional[str]:
|
|
31
39
|
"""Load and hash the API key from disk.
|
|
@@ -96,6 +104,26 @@ def verify_api_key(
|
|
|
96
104
|
return hmac.compare_digest(actual, expected)
|
|
97
105
|
|
|
98
106
|
|
|
107
|
+
def loopback_strict_mode_enabled(key_file: Optional[Path] = None) -> bool:
|
|
108
|
+
"""Whether uncredentialed loopback writes must present the API key.
|
|
109
|
+
|
|
110
|
+
v3.7.8 (F1/F2): opt-in via ``SLM_REQUIRE_API_KEY_LOOPBACK`` (any of
|
|
111
|
+
"1"/"true"/"yes"/"on", case-insensitive). This is the sole enforcement
|
|
112
|
+
point for the strict shared-host posture -- it is checked ONLY for the
|
|
113
|
+
uncredentialed-loopback case (a caller presenting none of
|
|
114
|
+
X-SLM-Daemon-Capability / X-Install-Token / X-SLM-API-Key). Callers who
|
|
115
|
+
already present a valid capability or install token are unaffected: those
|
|
116
|
+
are stronger, explicit credentials and this flag never re-litigates them.
|
|
117
|
+
|
|
118
|
+
Returns ``False`` (no-op) when the flag is unset/false, OR when no
|
|
119
|
+
api_key file is configured -- there is nothing to require in that case.
|
|
120
|
+
"""
|
|
121
|
+
raw = os.environ.get(SLM_REQUIRE_API_KEY_LOOPBACK_ENV, "")
|
|
122
|
+
if raw.strip().lower() not in ("1", "true", "yes", "on"):
|
|
123
|
+
return False
|
|
124
|
+
return _load_api_key_hash(key_file) is not None
|
|
125
|
+
|
|
126
|
+
|
|
99
127
|
def authorize_http_mcp_request(
|
|
100
128
|
request_headers: dict,
|
|
101
129
|
*,
|
|
@@ -266,7 +266,18 @@ class BackupManager:
|
|
|
266
266
|
|
|
267
267
|
A safety snapshot of the current state is taken first.
|
|
268
268
|
"""
|
|
269
|
-
|
|
269
|
+
# Containment: filename must be a bare .db name inside backup_dir — no
|
|
270
|
+
# path separators or traversal. Prevents restoring (and thus copying
|
|
271
|
+
# over memory.db) an arbitrary file the daemon user can read.
|
|
272
|
+
if (not filename or "/" in filename or "\\" in filename
|
|
273
|
+
or ".." in filename or not filename.endswith(".db")):
|
|
274
|
+
logger.error("Restore rejected: invalid backup filename: %r", filename)
|
|
275
|
+
return False
|
|
276
|
+
backup_dir = self.backup_dir.resolve()
|
|
277
|
+
backup_path = (self.backup_dir / filename).resolve()
|
|
278
|
+
if backup_path.parent != backup_dir:
|
|
279
|
+
logger.error("Restore rejected: path escapes backup dir: %r", filename)
|
|
280
|
+
return False
|
|
270
281
|
if not backup_path.exists():
|
|
271
282
|
logger.error("Backup not found: %s", filename)
|
|
272
283
|
return False
|
|
@@ -14,8 +14,11 @@ import getpass
|
|
|
14
14
|
import hashlib
|
|
15
15
|
import hmac
|
|
16
16
|
import json
|
|
17
|
+
import logging
|
|
17
18
|
import os
|
|
18
19
|
import secrets
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
19
22
|
import time
|
|
20
23
|
import uuid
|
|
21
24
|
from dataclasses import asdict, dataclass
|
|
@@ -30,6 +33,8 @@ DAEMON_SERVICE = "superlocalmemory-daemon"
|
|
|
30
33
|
_NAMESPACE_DOMAIN = b"superlocalmemory-daemon-namespace-v1\0"
|
|
31
34
|
_CAPABILITY_DOMAIN = b"superlocalmemory-daemon-capability-v1\0"
|
|
32
35
|
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
33
38
|
|
|
34
39
|
def _canonical_path(value: str | Path) -> Path:
|
|
35
40
|
expanded = Path(value).expanduser().resolve(strict=False)
|
|
@@ -144,6 +149,40 @@ def descriptor_path(data_root: str | Path | None = None) -> Path:
|
|
|
144
149
|
return root / "daemon.json"
|
|
145
150
|
|
|
146
151
|
|
|
152
|
+
def _restrict_to_owner(path: Path) -> None:
|
|
153
|
+
"""Restrict a sensitive file to the current user on every platform.
|
|
154
|
+
|
|
155
|
+
``daemon.json`` holds the capability token that authorizes write requests.
|
|
156
|
+
POSIX gets a 0600 chmod. On Windows chmod is a no-op, so use icacls to strip
|
|
157
|
+
inherited ACEs and grant the current user only — otherwise the token can be
|
|
158
|
+
read by other local accounts (privilege escalation on shared machines).
|
|
159
|
+
Files under %USERPROFILE% usually inherit user-only ACLs already; this is
|
|
160
|
+
defense-in-depth. Fail-soft — a hardening failure warns, never crashes.
|
|
161
|
+
"""
|
|
162
|
+
if sys.platform == "win32":
|
|
163
|
+
try:
|
|
164
|
+
user = getpass.getuser()
|
|
165
|
+
subprocess.run(
|
|
166
|
+
["icacls", str(path), "/inheritance:r"],
|
|
167
|
+
check=False, capture_output=True,
|
|
168
|
+
)
|
|
169
|
+
if user:
|
|
170
|
+
subprocess.run(
|
|
171
|
+
["icacls", str(path), "/grant:r", f"{user}:F"],
|
|
172
|
+
check=False, capture_output=True,
|
|
173
|
+
)
|
|
174
|
+
except Exception as exc: # noqa: BLE001
|
|
175
|
+
logger.warning(
|
|
176
|
+
"could not restrict ACL on %s (%s); the capability token may be "
|
|
177
|
+
"readable by other local users", path, exc,
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
try:
|
|
181
|
+
os.chmod(path, 0o600)
|
|
182
|
+
except OSError as exc:
|
|
183
|
+
logger.warning("could not chmod %s to 0600: %s", path, exc)
|
|
184
|
+
|
|
185
|
+
|
|
147
186
|
def write_descriptor(
|
|
148
187
|
descriptor: DaemonDescriptor,
|
|
149
188
|
*,
|
|
@@ -162,10 +201,7 @@ def write_descriptor(
|
|
|
162
201
|
stream.flush()
|
|
163
202
|
os.fsync(stream.fileno())
|
|
164
203
|
os.replace(temporary, destination)
|
|
165
|
-
|
|
166
|
-
os.chmod(destination, 0o600)
|
|
167
|
-
except OSError:
|
|
168
|
-
pass
|
|
204
|
+
_restrict_to_owner(destination)
|
|
169
205
|
finally:
|
|
170
206
|
temporary.unlink(missing_ok=True)
|
|
171
207
|
return destination
|
|
@@ -11,6 +11,7 @@ explicit process contract and always wins over persisted configuration.
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import json
|
|
14
|
+
import logging
|
|
14
15
|
import os
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
@@ -24,6 +25,8 @@ _DURABLE_IDENTITY_NAMES = frozenset(
|
|
|
24
25
|
},
|
|
25
26
|
)
|
|
26
27
|
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
27
30
|
|
|
28
31
|
class DataRootConflictError(RuntimeError):
|
|
29
32
|
"""Raised when two state-bearing roots make startup ambiguous."""
|
|
@@ -122,17 +125,53 @@ def assert_no_durable_root_conflict(
|
|
|
122
125
|
*,
|
|
123
126
|
home: str | Path | None = None,
|
|
124
127
|
) -> None:
|
|
125
|
-
"""Refuse ambiguous startup when
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
"""Refuse *ambiguous* startup when two state roots make the live namespace unclear.
|
|
129
|
+
|
|
130
|
+
The root actually selected for this process is always inspected: an
|
|
131
|
+
unreadable selected root fails closed. Beyond that, a conflict is only raised
|
|
132
|
+
when the selection was *implicit* — resolved from the legacy
|
|
133
|
+
``config.json:base_dir`` relocation hint — and a separately-populated default
|
|
134
|
+
root leaves it genuinely ambiguous which namespace is live.
|
|
135
|
+
|
|
136
|
+
An explicit environment selection (``SLM_DATA_DIR`` / ``SL_MEMORY_PATH`` /
|
|
137
|
+
``SLM_HOME``) is an unambiguous operator contract: a separately-populated
|
|
138
|
+
default root is then a deliberate multi-root / per-team / second-instance
|
|
139
|
+
layout, not an ambiguity, so startup proceeds. If the explicitly chosen root
|
|
140
|
+
is empty while the old default still holds data, that likely-mistyped path is
|
|
141
|
+
surfaced as a warning rather than a hard block.
|
|
142
|
+
|
|
143
|
+
This check never writes, copies, or deletes data.
|
|
129
144
|
"""
|
|
130
145
|
home_path = _canonical_path(home if home is not None else Path.home())
|
|
131
146
|
default_root = _canonical_path(home_path / ".superlocalmemory")
|
|
132
147
|
selected_root = canonical_data_root(home=home_path)
|
|
133
148
|
if selected_root == default_root:
|
|
134
149
|
return
|
|
150
|
+
|
|
151
|
+
# The root about to be used must be inspectable regardless of how it was
|
|
152
|
+
# chosen; an unreadable selected root fails closed inside _durable_markers.
|
|
135
153
|
selected_markers = _durable_markers(selected_root)
|
|
154
|
+
|
|
155
|
+
if environment_data_root() is not None:
|
|
156
|
+
# Explicit selection wins; a populated default root is a deliberate
|
|
157
|
+
# multi-root layout, not an ambiguity. Only warn on the "empty new root
|
|
158
|
+
# while the old default still holds data" case so a wrong SLM_DATA_DIR
|
|
159
|
+
# stays visible. Inspection of the unused default never blocks startup.
|
|
160
|
+
if not selected_markers:
|
|
161
|
+
try:
|
|
162
|
+
default_has_data = bool(_durable_markers(default_root))
|
|
163
|
+
except DataRootConflictError:
|
|
164
|
+
default_has_data = False
|
|
165
|
+
if default_has_data:
|
|
166
|
+
logger.warning(
|
|
167
|
+
"SLM_DATA_DIR selects an empty state root (%s) while the "
|
|
168
|
+
"default root (%s) still holds data; starting with the empty "
|
|
169
|
+
"root as explicitly requested.",
|
|
170
|
+
selected_root,
|
|
171
|
+
default_root,
|
|
172
|
+
)
|
|
173
|
+
return
|
|
174
|
+
|
|
136
175
|
default_markers = _durable_markers(default_root)
|
|
137
176
|
if not selected_markers or not default_markers:
|
|
138
177
|
return
|