superlocalmemory 4.0.4 → 4.0.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 +90 -0
- package/README.md +23 -14
- package/ide/configs/codex-mcp.toml +2 -2
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -0
- 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 +3 -2
- 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 +2 -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 +6 -5
- package/plugin-src/skills/slm-graph/SKILL.md +2 -1
- package/plugin-src/skills/slm-profile/SKILL.md +1 -0
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/rbac.py +106 -0
- package/src/superlocalmemory/brain/__init__.py +5 -0
- package/src/superlocalmemory/brain/truth.py +418 -0
- package/src/superlocalmemory/cli/__main__.py +17 -0
- package/src/superlocalmemory/cli/commands.py +96 -28
- package/src/superlocalmemory/cli/gdpr_cmd.py +779 -0
- package/src/superlocalmemory/cli/gdpr_io.py +109 -0
- package/src/superlocalmemory/cli/main.py +88 -0
- package/src/superlocalmemory/code_graph/extractors/__init__.py +17 -0
- package/src/superlocalmemory/code_graph/graph_store.py +180 -3
- package/src/superlocalmemory/code_graph/parser.py +280 -100
- package/src/superlocalmemory/compliance/gdpr.py +358 -0
- package/src/superlocalmemory/core/config.py +44 -1
- package/src/superlocalmemory/core/context_cache.py +58 -1
- package/src/superlocalmemory/core/engine_wiring.py +5 -1
- package/src/superlocalmemory/core/maintenance.py +43 -1
- package/src/superlocalmemory/core/mutations.py +155 -25
- package/src/superlocalmemory/core/recall_pipeline.py +6 -10
- package/src/superlocalmemory/core/recall_worker.py +33 -12
- package/src/superlocalmemory/core/remember_runtime.py +271 -2
- package/src/superlocalmemory/core/store_pipeline.py +100 -38
- package/src/superlocalmemory/encoding/consolidator.py +17 -47
- package/src/superlocalmemory/encoding/temporal_validator.py +14 -18
- package/src/superlocalmemory/hooks/user_prompt_hook.py +1 -1
- package/src/superlocalmemory/infra/backup.py +138 -0
- package/src/superlocalmemory/infra/backup_obligations.py +423 -0
- package/src/superlocalmemory/integrations/bounded_loops_mcp.py +4 -3
- package/src/superlocalmemory/learning/engagement.py +165 -0
- package/src/superlocalmemory/mcp/profiles.py +19 -7
- package/src/superlocalmemory/mcp/server.py +4 -2
- package/src/superlocalmemory/mcp/tools_brain.py +54 -10
- package/src/superlocalmemory/mcp/tools_code_graph.py +31 -4
- package/src/superlocalmemory/mcp/tools_core.py +88 -3
- package/src/superlocalmemory/mcp/tools_v3.py +20 -6
- package/src/superlocalmemory/retrieval/engine.py +28 -10
- package/src/superlocalmemory/retrieval/remote_reranker.py +108 -11
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +119 -19
- package/src/superlocalmemory/server/routes/brain.py +297 -14
- package/src/superlocalmemory/server/routes/learning.py +13 -25
- package/src/superlocalmemory/server/routes/memories.py +129 -3
- package/src/superlocalmemory/server/routes/v3_api.py +171 -60
- package/src/superlocalmemory/storage/_migration_internals.py +4 -0
- package/src/superlocalmemory/storage/_schema_version.py +2 -2
- package/src/superlocalmemory/storage/correction_cases.py +670 -0
- package/src/superlocalmemory/storage/database.py +230 -24
- package/src/superlocalmemory/storage/migration_runner.py +7 -0
- package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +245 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +2 -0
- package/src/superlocalmemory/storage/models.py +12 -4
- package/src/superlocalmemory/storage/write_coordinator.py +4 -0
- package/src/superlocalmemory/summaries/__init__.py +37 -0
- package/src/superlocalmemory/summaries/base.py +108 -0
- package/src/superlocalmemory/summaries/daily_reflection.py +293 -0
- package/src/superlocalmemory/summaries/project_work_log.py +424 -0
- package/src/superlocalmemory/summaries/session_summary.py +307 -0
- package/src/superlocalmemory/ui/css/design-system.css +76 -1
- package/src/superlocalmemory/ui/index.html +28 -11
- package/src/superlocalmemory/ui/js/brain.js +43 -7
- package/src/superlocalmemory/ui/js/od-agents.js +49 -5
- package/src/superlocalmemory/ui/js/od-brain.js +280 -84
- package/src/superlocalmemory/ui/js/od-graph.js +147 -6
|
@@ -260,6 +260,131 @@ async def set_mode(request: Request):
|
|
|
260
260
|
return _internal_error()
|
|
261
261
|
|
|
262
262
|
|
|
263
|
+
def apply_settings_update(config: "SLMConfig", payload: dict) -> "SLMConfig":
|
|
264
|
+
"""Credential-safe LLM config update for the settings dashboard (fixes #119).
|
|
265
|
+
|
|
266
|
+
This is the SINGLE authoritative place where incoming dashboard save payloads
|
|
267
|
+
are merged onto the stored config. The POST /api/v3/mode/set handler calls
|
|
268
|
+
this function after its HTTP-layer concerns (SSRF guard, auth) are settled,
|
|
269
|
+
so the acceptance gate (test_wave2_acceptance.py::P3) tests real product
|
|
270
|
+
behaviour — not a reimplementation.
|
|
271
|
+
|
|
272
|
+
SEC-L-01 PRESERVED:
|
|
273
|
+
The GET /mode route returns only ``urlparse(api_base).netloc`` (the host),
|
|
274
|
+
never the full URL. When the UI echoes that value back on save,
|
|
275
|
+
apply_settings_update detects the scheme-less netloc and silently restores
|
|
276
|
+
the stored full URL (scheme + path). This means the server-side logic
|
|
277
|
+
internally reads the stored URL but NEVER returns it to callers — viewer
|
|
278
|
+
users remain unable to discover the full endpoint topology.
|
|
279
|
+
|
|
280
|
+
Credential semantics:
|
|
281
|
+
api_key blank/omitted → UNCHANGED (browsers never repopulate pw fields)
|
|
282
|
+
api_key non-blank → applied
|
|
283
|
+
clear_api_key = True → explicit "" (deliberate wipe — only way to clear)
|
|
284
|
+
|
|
285
|
+
endpoint absent/empty → UNCHANGED
|
|
286
|
+
endpoint == stored netloc (no scheme "://") → UNCHANGED (redacted echo)
|
|
287
|
+
endpoint with "://" → applied
|
|
288
|
+
clear_base_url = True → explicit "" (deliberate wipe)
|
|
289
|
+
|
|
290
|
+
Security rule: if the destination provider or endpoint genuinely changes AND
|
|
291
|
+
no new key was supplied, the stored key is cleared to prevent accidental
|
|
292
|
+
credential reuse across providers. An explicit clear_api_key=True is
|
|
293
|
+
always honoured regardless.
|
|
294
|
+
|
|
295
|
+
NEVER logs api_key.
|
|
296
|
+
"""
|
|
297
|
+
import dataclasses as _dc
|
|
298
|
+
from urllib.parse import urlparse, urlsplit, urlunsplit
|
|
299
|
+
|
|
300
|
+
stored = config.llm
|
|
301
|
+
stored_key: str = stored.api_key
|
|
302
|
+
stored_base: str = stored.api_base or ""
|
|
303
|
+
|
|
304
|
+
# ── endpoint ─────────────────────────────────────────────────────────────
|
|
305
|
+
raw_endpoint: str = (payload.get("endpoint") or payload.get("base_url") or "").strip()
|
|
306
|
+
clear_endpoint: bool = payload.get("clear_base_url") is True
|
|
307
|
+
|
|
308
|
+
if clear_endpoint:
|
|
309
|
+
new_base = ""
|
|
310
|
+
endpoint_changed = bool(stored_base)
|
|
311
|
+
elif not raw_endpoint:
|
|
312
|
+
# Nothing supplied — keep stored URL intact.
|
|
313
|
+
new_base = stored_base
|
|
314
|
+
endpoint_changed = False
|
|
315
|
+
else:
|
|
316
|
+
# Detect the SEC-L-01 redacted echo: GET /mode returns
|
|
317
|
+
# urlparse(api_base).netloc (host only, no scheme, no path). If the
|
|
318
|
+
# client sent that exact string back, it did NOT change the endpoint —
|
|
319
|
+
# restore the full stored URL. We compare the raw string directly to
|
|
320
|
+
# stored_netloc so that hosts with ports ("api.host.com:443") also match.
|
|
321
|
+
stored_netloc: str = urlparse(stored_base).netloc if stored_base else ""
|
|
322
|
+
is_redacted_echo: bool = bool(stored_netloc) and (raw_endpoint == stored_netloc)
|
|
323
|
+
|
|
324
|
+
if is_redacted_echo:
|
|
325
|
+
new_base = stored_base
|
|
326
|
+
endpoint_changed = False
|
|
327
|
+
else:
|
|
328
|
+
new_base = raw_endpoint
|
|
329
|
+
# Canonical comparison (strip trailing slash, normalise scheme/host
|
|
330
|
+
# case) so a harmless trailing-slash difference does not clear the key.
|
|
331
|
+
def _canonical(u: str) -> str:
|
|
332
|
+
p = urlsplit(u)
|
|
333
|
+
return urlunsplit((
|
|
334
|
+
p.scheme.lower(), p.netloc.lower(),
|
|
335
|
+
p.path.rstrip("/"), p.query, "",
|
|
336
|
+
))
|
|
337
|
+
endpoint_changed = _canonical(new_base) != _canonical(stored_base)
|
|
338
|
+
|
|
339
|
+
# ── provider ─────────────────────────────────────────────────────────────
|
|
340
|
+
raw_provider: str = (payload.get("provider") or "").strip()
|
|
341
|
+
if raw_provider == "none":
|
|
342
|
+
new_provider = "" # "none" sentinel means "clear provider"
|
|
343
|
+
elif raw_provider:
|
|
344
|
+
new_provider = raw_provider
|
|
345
|
+
else:
|
|
346
|
+
new_provider = stored.provider or ""
|
|
347
|
+
|
|
348
|
+
provider_changed: bool = new_provider != (stored.provider or "")
|
|
349
|
+
|
|
350
|
+
# ── api_key ───────────────────────────────────────────────────────────────
|
|
351
|
+
# Evaluated AFTER endpoint/provider so we know whether the destination changed.
|
|
352
|
+
raw_key: str = (payload.get("api_key") or "").strip()
|
|
353
|
+
clear_key: bool = payload.get("clear_api_key") is True
|
|
354
|
+
destination_changed: bool = provider_changed or endpoint_changed
|
|
355
|
+
|
|
356
|
+
if clear_key:
|
|
357
|
+
new_key = "" # explicit user wipe
|
|
358
|
+
elif raw_key:
|
|
359
|
+
new_key = raw_key # user supplied a replacement key
|
|
360
|
+
elif destination_changed:
|
|
361
|
+
# Security: destination changed but no new key → clear to prevent
|
|
362
|
+
# the stored credential from being silently redirected to a new
|
|
363
|
+
# provider or endpoint the user may not own.
|
|
364
|
+
new_key = ""
|
|
365
|
+
else:
|
|
366
|
+
# Blank key + same destination = browser did not repopulate the
|
|
367
|
+
# password field. Preserve the stored key verbatim.
|
|
368
|
+
new_key = stored_key
|
|
369
|
+
|
|
370
|
+
# ── model ────────────────────────────────────────────────────────────────
|
|
371
|
+
raw_model: str = (payload.get("model") or "").strip()
|
|
372
|
+
new_model: str = raw_model if raw_model else (stored.model or "")
|
|
373
|
+
|
|
374
|
+
# ── apply ─────────────────────────────────────────────────────────────────
|
|
375
|
+
# dataclasses.replace preserves temperature / max_tokens / timeout_seconds.
|
|
376
|
+
# Assigning to config.llm works because SLMConfig is not frozen.
|
|
377
|
+
# NEVER include new_key in any log call.
|
|
378
|
+
config.llm = _dc.replace(
|
|
379
|
+
stored,
|
|
380
|
+
provider=new_provider,
|
|
381
|
+
model=new_model,
|
|
382
|
+
api_key=new_key,
|
|
383
|
+
api_base=new_base,
|
|
384
|
+
)
|
|
385
|
+
return config
|
|
386
|
+
|
|
387
|
+
|
|
263
388
|
@router.post("/mode/set")
|
|
264
389
|
async def set_full_config(request: Request):
|
|
265
390
|
"""Save mode + provider + model + API key together.
|
|
@@ -273,7 +398,7 @@ async def set_full_config(request: Request):
|
|
|
273
398
|
body = await request.json()
|
|
274
399
|
if not isinstance(body, dict):
|
|
275
400
|
return JSONResponse({"error": "Request body must be a JSON object"}, status_code=400)
|
|
276
|
-
from superlocalmemory.core.config import SLMConfig, EmbeddingConfig
|
|
401
|
+
from superlocalmemory.core.config import SLMConfig, EmbeddingConfig
|
|
277
402
|
from superlocalmemory.storage.models import Mode
|
|
278
403
|
from superlocalmemory.server.routes.helpers import log_mode_change
|
|
279
404
|
|
|
@@ -316,68 +441,54 @@ async def set_full_config(request: Request):
|
|
|
316
441
|
if new_mode not in ("a", "b", "c"):
|
|
317
442
|
return JSONResponse({"error": "Invalid mode"}, status_code=400)
|
|
318
443
|
|
|
319
|
-
#
|
|
320
|
-
#
|
|
321
|
-
#
|
|
322
|
-
#
|
|
323
|
-
#
|
|
324
|
-
|
|
325
|
-
model = model_input or config.llm.model
|
|
326
|
-
_endpoint = "" if clear_base_url else (base_url_input or endpoint_input or "")
|
|
444
|
+
# Resolve the effective endpoint value before SSRF validation.
|
|
445
|
+
# Only the Ollama default injection happens here; the fallback to the
|
|
446
|
+
# stored URL (and redacted-echo detection) live inside
|
|
447
|
+
# apply_settings_update so that the P3 acceptance gate exercises
|
|
448
|
+
# the same code path as the HTTP handler — not a reimplementation.
|
|
449
|
+
_raw_ep: str = "" if clear_base_url else (base_url_input or endpoint_input or "")
|
|
327
450
|
if (
|
|
328
|
-
not
|
|
451
|
+
not _raw_ep
|
|
452
|
+
and provider_input == "ollama"
|
|
329
453
|
and config.llm.provider != "ollama"
|
|
330
454
|
and not clear_base_url
|
|
331
455
|
):
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
#
|
|
337
|
-
#
|
|
338
|
-
if base_url_input is not None or endpoint_input is not None:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
api_key = "" if (clear_api_key or (destination_changed and not api_key_input)) else (
|
|
368
|
-
api_key_input or config.llm.api_key
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
# Mutate only the fields the dashboard sent — all other config blocks
|
|
372
|
-
# (forgetting, injection, retrieval, math, consolidation, scope, …) are
|
|
373
|
-
# preserved because we loaded the full existing config above.
|
|
456
|
+
_raw_ep = "http://localhost:11434"
|
|
457
|
+
|
|
458
|
+
# SSRF guard — fires only for genuinely new egress destinations.
|
|
459
|
+
# Redacted echoes (the netloc-only string GET /mode returns per
|
|
460
|
+
# SEC-L-01) are NOT outbound targets; they will be silently replaced
|
|
461
|
+
# with the stored full URL by apply_settings_update.
|
|
462
|
+
if _raw_ep and (base_url_input is not None or endpoint_input is not None):
|
|
463
|
+
from urllib.parse import urlparse as _up
|
|
464
|
+
_stored_nl: str = _up(config.llm.api_base or "").netloc
|
|
465
|
+
_is_redacted_echo: bool = bool(_stored_nl) and (_raw_ep == _stored_nl)
|
|
466
|
+
if not _is_redacted_echo:
|
|
467
|
+
client = getattr(request, "client", None)
|
|
468
|
+
endpoint_error = _validate_provider_url(
|
|
469
|
+
_raw_ep, getattr(client, "host", "") if client else ""
|
|
470
|
+
)
|
|
471
|
+
if endpoint_error:
|
|
472
|
+
return JSONResponse({"error": endpoint_error}, status_code=400)
|
|
473
|
+
|
|
474
|
+
# Build the normalised body for apply_settings_update. Inject the
|
|
475
|
+
# resolved endpoint (Ollama default when applicable) so that the
|
|
476
|
+
# credential-safe logic sees the approved value. Consolidate
|
|
477
|
+
# base_url → endpoint to keep apply_settings_update's lookup simple.
|
|
478
|
+
_apply_body: dict = dict(body)
|
|
479
|
+
if _raw_ep or clear_base_url:
|
|
480
|
+
_apply_body["endpoint"] = _raw_ep
|
|
481
|
+
_apply_body.pop("base_url", None)
|
|
482
|
+
|
|
483
|
+
# Credential-safe LLM update. All field presence, preservation, and
|
|
484
|
+
# security-clearing rules live here. Other config blocks (forgetting,
|
|
485
|
+
# injection, retrieval, math, consolidation, scope, …) are preserved
|
|
486
|
+
# because we loaded the full existing config at the top of this handler.
|
|
487
|
+
config = apply_settings_update(config, _apply_body)
|
|
488
|
+
|
|
489
|
+
# Mode switch — happens after LLM update so the correct provider/key
|
|
490
|
+
# is already in place when the runtime engine is reconfigured.
|
|
374
491
|
config.mode = Mode(new_mode)
|
|
375
|
-
config.llm = LLMConfig(
|
|
376
|
-
provider=provider if provider != "none" else "",
|
|
377
|
-
model=model,
|
|
378
|
-
api_key=api_key,
|
|
379
|
-
api_base=_endpoint,
|
|
380
|
-
)
|
|
381
492
|
|
|
382
493
|
# Update embedding only when the dashboard explicitly sent those fields;
|
|
383
494
|
# absence means "leave it alone" (AIDEV-86 / broader fix).
|
|
@@ -423,8 +534,8 @@ async def set_full_config(request: Request):
|
|
|
423
534
|
return {
|
|
424
535
|
"success": True,
|
|
425
536
|
"mode": new_mode,
|
|
426
|
-
"provider": provider,
|
|
427
|
-
"model": model,
|
|
537
|
+
"provider": config.llm.provider or "none",
|
|
538
|
+
"model": config.llm.model,
|
|
428
539
|
"embedding_provider": config.embedding.provider,
|
|
429
540
|
"embedding_model": config.embedding.model_name,
|
|
430
541
|
"embedding_dimension": config.embedding.dimension,
|
|
@@ -150,6 +150,9 @@ from superlocalmemory.storage.migrations import (
|
|
|
150
150
|
from superlocalmemory.storage.migrations import (
|
|
151
151
|
M041_external_evidence_receipts as _M041,
|
|
152
152
|
)
|
|
153
|
+
from superlocalmemory.storage.migrations import (
|
|
154
|
+
M042_correction_case_ledger as _M042,
|
|
155
|
+
)
|
|
153
156
|
|
|
154
157
|
# Emit under the runner's logger name so operational log filters that key on
|
|
155
158
|
# "superlocalmemory.storage.migration_runner" keep matching after this split.
|
|
@@ -199,6 +202,7 @@ _MODULES = {
|
|
|
199
202
|
_M039.NAME: _M039,
|
|
200
203
|
_M040.NAME: _M040,
|
|
201
204
|
_M041.NAME: _M041,
|
|
205
|
+
_M042.NAME: _M042,
|
|
202
206
|
}
|
|
203
207
|
|
|
204
208
|
# Exact historical DDL fingerprints whose resulting schema is intentionally
|
|
@@ -21,9 +21,9 @@ import sqlite3
|
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
23
|
#: Highest schema_version this runner can write. Matches the trailing serial
|
|
24
|
-
#: of the latest migration (
|
|
24
|
+
#: of the latest migration (M042). Increment when adding new migrations or
|
|
25
25
|
#: table-level breaking changes.
|
|
26
|
-
SUPPORTED_SCHEMA_VERSION: int =
|
|
26
|
+
SUPPORTED_SCHEMA_VERSION: int = 42
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class SchemaVersionError(RuntimeError):
|