superlocalmemory 4.0.1 → 4.0.2
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 +24 -0
- package/README.md +10 -11
- 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-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +1 -1
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/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-scope/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 +3 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +60 -1
- package/src/superlocalmemory/cli/main.py +24 -1
- package/src/superlocalmemory/compliance/gdpr.py +104 -73
- package/src/superlocalmemory/contracts/__init__.py +1 -0
- package/src/superlocalmemory/contracts/schemas/agent-experience-v1.schema.json +92 -0
- package/src/superlocalmemory/contracts/schemas/agent-integration-contract-v2.schema.json +46 -0
- package/src/superlocalmemory/contracts/schemas/cognitive-turn-receipt-v1.schema.json +59 -0
- package/src/superlocalmemory/contracts/v402.py +62 -0
- package/src/superlocalmemory/core/engine.py +10 -0
- package/src/superlocalmemory/core/recall_pipeline.py +6 -0
- package/src/superlocalmemory/core/recall_worker.py +12 -0
- package/src/superlocalmemory/core/worker_pool.py +12 -0
- package/src/superlocalmemory/hooks/hook_handlers.py +16 -0
- package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +12 -6
- package/src/superlocalmemory/hooks/session_registry.py +136 -3
- package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -2
- package/src/superlocalmemory/integrations/__init__.py +1 -0
- package/src/superlocalmemory/integrations/bounded_loops_v051.py +236 -0
- package/src/superlocalmemory/learning/database.py +21 -14
- package/src/superlocalmemory/mcp/_daemon_proxy.py +9 -0
- package/src/superlocalmemory/mcp/server.py +5 -0
- package/src/superlocalmemory/mcp/tools_brain.py +132 -0
- package/src/superlocalmemory/mcp/tools_core.py +25 -6
- package/src/superlocalmemory/mcp/tools_v3.py +16 -2
- package/src/superlocalmemory/retrieval/engine.py +43 -1
- package/src/superlocalmemory/retrieval/temporal_utils.py +16 -1
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +151 -0
- package/src/superlocalmemory/server/routes/brain.py +206 -1
- package/src/superlocalmemory/server/routes/helpers.py +53 -35
- package/src/superlocalmemory/server/routes/v3_api.py +118 -11
- package/src/superlocalmemory/server/unified_daemon.py +25 -0
- package/src/superlocalmemory/storage/_migration_internals.py +4 -0
- package/src/superlocalmemory/storage/_schema_version.py +2 -2
- package/src/superlocalmemory/storage/agent_experience.py +490 -0
- package/src/superlocalmemory/storage/database.py +189 -34
- package/src/superlocalmemory/storage/migration_runner.py +8 -0
- package/src/superlocalmemory/storage/migrations/M015_add_pinned_column.py +18 -0
- package/src/superlocalmemory/storage/migrations/M040_agent_experience_receipts.py +254 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +2 -0
- package/src/superlocalmemory/storage/schema.py +4 -0
- package/src/superlocalmemory/ui/js/auto-settings.js +18 -14
- package/src/superlocalmemory/ui/js/brain.js +57 -1
- package/src/superlocalmemory/ui/js/od-brain.js +114 -40
- package/src/superlocalmemory/ui/js/od-settings.js +8 -1
|
@@ -53,11 +53,12 @@ class GDPRCompliance:
|
|
|
53
53
|
|
|
54
54
|
def _memory_has_siblings(self, memory_id: str, profile_id: str) -> bool:
|
|
55
55
|
try:
|
|
56
|
-
return bool(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
return bool(
|
|
57
|
+
self._db.execute(
|
|
58
|
+
"SELECT 1 FROM atomic_facts WHERE memory_id = ? AND profile_id = ? LIMIT 1",
|
|
59
|
+
(memory_id, profile_id),
|
|
60
|
+
)
|
|
61
|
+
)
|
|
61
62
|
except Exception:
|
|
62
63
|
return True
|
|
63
64
|
|
|
@@ -69,8 +70,12 @@ class GDPRCompliance:
|
|
|
69
70
|
from superlocalmemory.core.transactions.erasure import write_tombstones
|
|
70
71
|
|
|
71
72
|
write_tombstones(
|
|
72
|
-
self._db,
|
|
73
|
-
|
|
73
|
+
self._db,
|
|
74
|
+
profile_id,
|
|
75
|
+
(fact_id,),
|
|
76
|
+
uuid.uuid4().hex,
|
|
77
|
+
time.time(),
|
|
78
|
+
memory_id,
|
|
74
79
|
)
|
|
75
80
|
except Exception:
|
|
76
81
|
pass
|
|
@@ -192,9 +197,7 @@ class GDPRCompliance:
|
|
|
192
197
|
try:
|
|
193
198
|
names = [
|
|
194
199
|
dict(r)["name"]
|
|
195
|
-
for r in self._db.execute(
|
|
196
|
-
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
197
|
-
)
|
|
200
|
+
for r in self._db.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
198
201
|
]
|
|
199
202
|
except Exception:
|
|
200
203
|
return []
|
|
@@ -230,18 +233,14 @@ class GDPRCompliance:
|
|
|
230
233
|
|
|
231
234
|
# Profile record itself (the tenant metadata).
|
|
232
235
|
try:
|
|
233
|
-
rows = self._db.execute(
|
|
234
|
-
"SELECT * FROM profiles WHERE profile_id = ?", (profile_id,)
|
|
235
|
-
)
|
|
236
|
+
rows = self._db.execute("SELECT * FROM profiles WHERE profile_id = ?", (profile_id,))
|
|
236
237
|
data["profile_record"] = [dict(r) for r in rows]
|
|
237
238
|
except Exception:
|
|
238
239
|
data["profile_record"] = []
|
|
239
240
|
|
|
240
241
|
# total_items counts the canonical (table-name) keys only, before
|
|
241
242
|
# friendly aliases are added, so it is not double-counted.
|
|
242
|
-
data["total_items"] = sum(
|
|
243
|
-
len(v) for v in data.values() if isinstance(v, list)
|
|
244
|
-
)
|
|
243
|
+
data["total_items"] = sum(len(v) for v in data.values() if isinstance(v, list))
|
|
245
244
|
|
|
246
245
|
# Backward-compatible friendly aliases for the well-known keys (stable
|
|
247
246
|
# export contract) — they reference the same lists, not copies.
|
|
@@ -264,8 +263,9 @@ class GDPRCompliance:
|
|
|
264
263
|
the chain in a separate DB is the durable evidence.
|
|
265
264
|
"""
|
|
266
265
|
if profile_id == "default":
|
|
267
|
-
raise ValueError(
|
|
268
|
-
|
|
266
|
+
raise ValueError(
|
|
267
|
+
"Cannot delete the default profile via GDPR erasure. Use profile deletion instead."
|
|
268
|
+
)
|
|
269
269
|
|
|
270
270
|
counts: dict[str, int] = {}
|
|
271
271
|
|
|
@@ -276,14 +276,18 @@ class GDPRCompliance:
|
|
|
276
276
|
try:
|
|
277
277
|
from superlocalmemory.compliance.audit import AuditChain
|
|
278
278
|
from superlocalmemory.infra.data_root import state_path
|
|
279
|
+
|
|
279
280
|
AuditChain(str(state_path("audit_chain.db"))).log(
|
|
280
|
-
"gdpr_erase",
|
|
281
|
+
"gdpr_erase",
|
|
282
|
+
agent_id="gdpr",
|
|
283
|
+
profile_id=profile_id,
|
|
281
284
|
metadata={"basis": "GDPR Art.17 right-to-erasure"},
|
|
282
285
|
)
|
|
283
286
|
except Exception as exc:
|
|
284
287
|
logger.error(
|
|
285
288
|
"GDPR erase ABORTED for %r: pre-deletion audit-chain log failed: %s",
|
|
286
|
-
profile_id,
|
|
289
|
+
profile_id,
|
|
290
|
+
exc,
|
|
287
291
|
)
|
|
288
292
|
counts["audit_request_failed"] = 1
|
|
289
293
|
counts["erasure_aborted"] = 1
|
|
@@ -324,6 +328,7 @@ class GDPRCompliance:
|
|
|
324
328
|
data_root = self._data_root
|
|
325
329
|
try:
|
|
326
330
|
from superlocalmemory.core.context_cache import purge_profile_from_cache_db
|
|
331
|
+
|
|
327
332
|
if data_root is None:
|
|
328
333
|
db_path = getattr(self._db, "db_path", None)
|
|
329
334
|
if db_path is not None:
|
|
@@ -389,10 +394,9 @@ class GDPRCompliance:
|
|
|
389
394
|
"SELECT fact_id FROM atomic_facts WHERE profile_id = ?",
|
|
390
395
|
(profile_id,),
|
|
391
396
|
)
|
|
392
|
-
_profile_fact_ids = tuple(
|
|
393
|
-
dict(r)["fact_id"] for r in _fact_rows
|
|
394
|
-
|
|
395
|
-
))
|
|
397
|
+
_profile_fact_ids = tuple(
|
|
398
|
+
sorted(dict(r)["fact_id"] for r in _fact_rows if dict(r).get("fact_id") is not None)
|
|
399
|
+
)
|
|
396
400
|
except Exception as exc:
|
|
397
401
|
logger.warning("GDPR profile erase: fact_id scan failed: %s", exc)
|
|
398
402
|
|
|
@@ -417,7 +421,8 @@ class GDPRCompliance:
|
|
|
417
421
|
)
|
|
418
422
|
_remove_result = _erasure_svc.remove(self._db, _ctx)
|
|
419
423
|
_receipt = _erasure_svc.finalize(
|
|
420
|
-
self._db,
|
|
424
|
+
self._db,
|
|
425
|
+
_ctx,
|
|
421
426
|
subject_type="profile",
|
|
422
427
|
subject_id=profile_id,
|
|
423
428
|
requested_by="gdpr",
|
|
@@ -432,6 +437,33 @@ class GDPRCompliance:
|
|
|
432
437
|
counts["receipt_error"] = str(exc)
|
|
433
438
|
raise
|
|
434
439
|
|
|
440
|
+
# Purge the learning sidecar *before* removing memory/profile rows. A
|
|
441
|
+
# learning failure is retryable and must leave the profile intact; the
|
|
442
|
+
# former best-effort-after-delete ordering could orphan receipts.
|
|
443
|
+
if data_root is None:
|
|
444
|
+
# Compatibility for third-party legacy wrappers that expose no
|
|
445
|
+
# durable path. We cannot safely guess another installation's
|
|
446
|
+
# sidecar. Native v4.0.2 runtime objects always provide the root.
|
|
447
|
+
logger.warning(
|
|
448
|
+
"GDPR erase: learning receipt purge skipped for profile %r — "
|
|
449
|
+
"data root could not be resolved",
|
|
450
|
+
profile_id,
|
|
451
|
+
)
|
|
452
|
+
counts["learning_db_skipped"] = 1
|
|
453
|
+
else:
|
|
454
|
+
try:
|
|
455
|
+
from superlocalmemory.learning.database import LearningDatabase
|
|
456
|
+
|
|
457
|
+
learning_db = LearningDatabase(data_root / "learning.db")
|
|
458
|
+
learning_db.reset(profile_id)
|
|
459
|
+
counts["learning_db"] = 1
|
|
460
|
+
except Exception as exc:
|
|
461
|
+
logger.warning("GDPR erase: learning-db reset failed: %s", exc)
|
|
462
|
+
counts["learning_db_failed"] = 1
|
|
463
|
+
raise RuntimeError(
|
|
464
|
+
"learning receipt purge failed; profile deletion was not started"
|
|
465
|
+
) from exc
|
|
466
|
+
|
|
435
467
|
# Pass 2 — full-tenant wipe with FK enforcement OFF so table order is
|
|
436
468
|
# irrelevant (every profile row in every table goes). FTS shadow rows
|
|
437
469
|
# are still removed by the base-table delete triggers.
|
|
@@ -443,9 +475,7 @@ class GDPRCompliance:
|
|
|
443
475
|
try:
|
|
444
476
|
for table in tables:
|
|
445
477
|
try:
|
|
446
|
-
self._db.execute(
|
|
447
|
-
f"DELETE FROM {table} WHERE profile_id = ?", (profile_id,)
|
|
448
|
-
)
|
|
478
|
+
self._db.execute(f"DELETE FROM {table} WHERE profile_id = ?", (profile_id,))
|
|
449
479
|
except Exception as exc: # pragma: no cover — defensive per-table
|
|
450
480
|
logger.warning("GDPR erase: delete %s failed: %s", table, exc)
|
|
451
481
|
table_delete_failures.append(table)
|
|
@@ -460,21 +490,6 @@ class GDPRCompliance:
|
|
|
460
490
|
if table_delete_failures:
|
|
461
491
|
counts["table_delete_failures"] = len(table_delete_failures)
|
|
462
492
|
|
|
463
|
-
# Erase the learning sidecar next to the active memory database. A
|
|
464
|
-
# custom SLM data root must never fall back to another installation's
|
|
465
|
-
# DEFAULT_BASE_DIR: doing so can both miss the subject data and erase
|
|
466
|
-
# unrelated learning state.
|
|
467
|
-
try:
|
|
468
|
-
from superlocalmemory.learning.database import LearningDatabase
|
|
469
|
-
if data_root is None:
|
|
470
|
-
raise RuntimeError("active data root could not be resolved")
|
|
471
|
-
learning_db = LearningDatabase(data_root / "learning.db")
|
|
472
|
-
learning_db.reset(profile_id)
|
|
473
|
-
counts["learning_db"] = 1
|
|
474
|
-
except Exception as exc:
|
|
475
|
-
logger.warning("GDPR erase: learning-db reset failed: %s", exc)
|
|
476
|
-
counts["learning_db_failed"] = 1
|
|
477
|
-
|
|
478
493
|
# VACUUM to remove deleted data from physical file
|
|
479
494
|
try:
|
|
480
495
|
self._db.execute("VACUUM")
|
|
@@ -497,28 +512,34 @@ class GDPRCompliance:
|
|
|
497
512
|
# Fail-closed: a residue re-count that cannot be performed is a
|
|
498
513
|
# verification failure, not zero residue. We cannot certify the
|
|
499
514
|
# table is clean, so erasure must not report complete.
|
|
500
|
-
logger.warning(
|
|
501
|
-
"GDPR erase: residue re-count for %s failed: %s", table, exc
|
|
502
|
-
)
|
|
515
|
+
logger.warning("GDPR erase: residue re-count for %s failed: %s", table, exc)
|
|
503
516
|
residue_recount_failed = True
|
|
504
517
|
counts["residue_rows"] = residue_rows
|
|
505
518
|
if residue_recount_failed:
|
|
506
519
|
counts["residue_recount_failed"] = 1
|
|
507
|
-
counts["erasure_complete"] =
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
520
|
+
counts["erasure_complete"] = (
|
|
521
|
+
1
|
|
522
|
+
if (
|
|
523
|
+
residue_rows == 0
|
|
524
|
+
and not residue_recount_failed
|
|
525
|
+
and not table_delete_failures
|
|
526
|
+
and not counts.get("learning_db_failed")
|
|
527
|
+
and not counts.get("learning_db_skipped")
|
|
528
|
+
and not counts.get("vector_store_failures")
|
|
529
|
+
and not counts.get("context_cache_failed")
|
|
530
|
+
and not counts.get("owner_erasure_incomplete")
|
|
531
|
+
)
|
|
532
|
+
else 0
|
|
533
|
+
)
|
|
516
534
|
|
|
517
535
|
try:
|
|
518
536
|
from superlocalmemory.compliance.audit import AuditChain
|
|
519
537
|
from superlocalmemory.infra.data_root import state_path
|
|
538
|
+
|
|
520
539
|
AuditChain(str(state_path("audit_chain.db"))).log(
|
|
521
|
-
"gdpr_erase_complete",
|
|
540
|
+
"gdpr_erase_complete",
|
|
541
|
+
agent_id="gdpr",
|
|
542
|
+
profile_id=profile_id,
|
|
522
543
|
metadata={
|
|
523
544
|
"basis": "GDPR Art.17 right-to-erasure",
|
|
524
545
|
"tables_erased": len(tables),
|
|
@@ -539,13 +560,17 @@ class GDPRCompliance:
|
|
|
539
560
|
and the entity itself. For targeted erasure requests.
|
|
540
561
|
"""
|
|
541
562
|
import time
|
|
563
|
+
|
|
542
564
|
requested_at = time.time()
|
|
543
565
|
audit_request_ok = True
|
|
544
566
|
try:
|
|
545
567
|
from superlocalmemory.compliance.audit import AuditChain
|
|
546
568
|
from superlocalmemory.infra.data_root import state_path
|
|
569
|
+
|
|
547
570
|
AuditChain(str(state_path("audit_chain.db"))).log(
|
|
548
|
-
"gdpr_erase_entity",
|
|
571
|
+
"gdpr_erase_entity",
|
|
572
|
+
agent_id="gdpr",
|
|
573
|
+
profile_id=profile_id,
|
|
549
574
|
metadata={
|
|
550
575
|
"basis": "GDPR Art.17 right-to-erasure",
|
|
551
576
|
"entity": entity_name,
|
|
@@ -554,9 +579,13 @@ class GDPRCompliance:
|
|
|
554
579
|
except Exception as exc:
|
|
555
580
|
logger.warning("GDPR entity erase: audit-chain log failed: %s", exc)
|
|
556
581
|
audit_request_ok = False
|
|
557
|
-
self._audit(
|
|
558
|
-
|
|
559
|
-
|
|
582
|
+
self._audit(
|
|
583
|
+
"delete",
|
|
584
|
+
"entity",
|
|
585
|
+
entity_name,
|
|
586
|
+
f"GDPR entity erasure in profile {profile_id}",
|
|
587
|
+
profile_id=profile_id,
|
|
588
|
+
)
|
|
560
589
|
|
|
561
590
|
entity = self._db.get_entity_by_name(entity_name, profile_id)
|
|
562
591
|
if entity is None:
|
|
@@ -597,7 +626,8 @@ class GDPRCompliance:
|
|
|
597
626
|
)
|
|
598
627
|
erasure_svc.remove(self._db, ctx)
|
|
599
628
|
receipt = erasure_svc.finalize(
|
|
600
|
-
self._db,
|
|
629
|
+
self._db,
|
|
630
|
+
ctx,
|
|
601
631
|
subject_type="entity",
|
|
602
632
|
subject_id=entity_name,
|
|
603
633
|
requested_by="gdpr",
|
|
@@ -606,9 +636,7 @@ class GDPRCompliance:
|
|
|
606
636
|
if not receipt.persisted:
|
|
607
637
|
counts["receipt_persist_failed"] = 1
|
|
608
638
|
if not receipt.all_erased:
|
|
609
|
-
counts["vector_store_failures"] = sum(
|
|
610
|
-
1 for p in receipt.proofs if not p.erased
|
|
611
|
-
)
|
|
639
|
+
counts["vector_store_failures"] = sum(1 for p in receipt.proofs if not p.erased)
|
|
612
640
|
|
|
613
641
|
for fid, mid in targets:
|
|
614
642
|
self._db.delete_fact(fid)
|
|
@@ -636,11 +664,12 @@ class GDPRCompliance:
|
|
|
636
664
|
# Delete aliases + entity (profile-scoped — entity_id is UUID-global but
|
|
637
665
|
# keep the tenant predicate for consistent Art.17 isolation).
|
|
638
666
|
self._db.execute(
|
|
639
|
-
"DELETE FROM entity_aliases WHERE entity_id = ? AND profile_id = ?",
|
|
640
|
-
|
|
667
|
+
"DELETE FROM entity_aliases WHERE entity_id = ? AND profile_id = ?", (eid, profile_id)
|
|
668
|
+
)
|
|
641
669
|
self._db.execute(
|
|
642
670
|
"DELETE FROM canonical_entities WHERE entity_id = ? AND profile_id = ?",
|
|
643
|
-
(eid, profile_id)
|
|
671
|
+
(eid, profile_id),
|
|
672
|
+
)
|
|
644
673
|
counts["entity"] = 1
|
|
645
674
|
if not audit_request_ok:
|
|
646
675
|
counts["audit_request_failed"] = 1
|
|
@@ -650,23 +679,25 @@ class GDPRCompliance:
|
|
|
650
679
|
|
|
651
680
|
# -- Audit Trail -------------------------------------------------------
|
|
652
681
|
|
|
653
|
-
def get_audit_trail(
|
|
654
|
-
self, profile_id: str, limit: int = 100
|
|
655
|
-
) -> list[dict]:
|
|
682
|
+
def get_audit_trail(self, profile_id: str, limit: int = 100) -> list[dict]:
|
|
656
683
|
"""Get compliance audit trail for a profile."""
|
|
657
684
|
rows = self._db.execute(
|
|
658
|
-
"SELECT * FROM compliance_audit WHERE profile_id = ? "
|
|
659
|
-
"ORDER BY timestamp DESC LIMIT ?",
|
|
685
|
+
"SELECT * FROM compliance_audit WHERE profile_id = ? ORDER BY timestamp DESC LIMIT ?",
|
|
660
686
|
(profile_id, limit),
|
|
661
687
|
)
|
|
662
688
|
return [dict(r) for r in rows]
|
|
663
689
|
|
|
664
690
|
def _audit(
|
|
665
|
-
self,
|
|
691
|
+
self,
|
|
692
|
+
action: str,
|
|
693
|
+
target_type: str,
|
|
694
|
+
target_id: str,
|
|
695
|
+
details: str,
|
|
666
696
|
profile_id: str | None = None,
|
|
667
697
|
) -> None:
|
|
668
698
|
"""Log a compliance action."""
|
|
669
699
|
from superlocalmemory.storage.models import _new_id
|
|
700
|
+
|
|
670
701
|
pid = profile_id if profile_id is not None else target_id
|
|
671
702
|
self._db.execute(
|
|
672
703
|
"INSERT INTO compliance_audit "
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Versioned public contracts for cross-SLM integration surfaces."""
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://superlocalmemory.com/schemas/agent-experience-v1.schema.json",
|
|
4
|
+
"title": "SLM Agent Experience v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"experience_id",
|
|
9
|
+
"profile_id",
|
|
10
|
+
"occurred_at",
|
|
11
|
+
"task_class",
|
|
12
|
+
"project_scope",
|
|
13
|
+
"route",
|
|
14
|
+
"verification",
|
|
15
|
+
"producer_claim",
|
|
16
|
+
"terminal_status"
|
|
17
|
+
],
|
|
18
|
+
"properties": {
|
|
19
|
+
"experience_id": {"type": "string", "minLength": 1},
|
|
20
|
+
"profile_id": {"type": "string", "minLength": 1},
|
|
21
|
+
"occurred_at": {"type": "string", "format": "date-time"},
|
|
22
|
+
"task_class": {"type": "string", "minLength": 1},
|
|
23
|
+
"project_scope": {"type": "string", "minLength": 1},
|
|
24
|
+
"complexity_band": {"type": "string"},
|
|
25
|
+
"risk": {"type": "string"},
|
|
26
|
+
"route": {"$ref": "#/$defs/route_identity"},
|
|
27
|
+
"bounds": {"$ref": "#/$defs/bounds"},
|
|
28
|
+
"usage": {"$ref": "#/$defs/usage"},
|
|
29
|
+
"verification": {"$ref": "#/$defs/verification"},
|
|
30
|
+
"producer_claim": {"enum": ["success", "failure", "partial", "unknown"]},
|
|
31
|
+
"terminal_status": {"enum": ["succeeded", "failed", "cancelled", "timed_out"]},
|
|
32
|
+
"failure_class": {"type": "string"},
|
|
33
|
+
"human_intervention": {"type": "boolean"},
|
|
34
|
+
"lessons": {"type": "string", "maxLength": 2000},
|
|
35
|
+
"receipt_digest": {"$ref": "#/$defs/sha256"},
|
|
36
|
+
"artifact_digests": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": {"$ref": "#/$defs/sha256"},
|
|
39
|
+
"uniqueItems": true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"$defs": {
|
|
43
|
+
"sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
44
|
+
"route_identity": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"additionalProperties": false,
|
|
47
|
+
"required": ["harness", "provider", "model", "effort", "machine"],
|
|
48
|
+
"properties": {
|
|
49
|
+
"harness": {"type": "string", "minLength": 1},
|
|
50
|
+
"provider": {"type": "string", "minLength": 1},
|
|
51
|
+
"model": {"type": "string", "minLength": 1},
|
|
52
|
+
"effort": {"type": "string", "minLength": 1},
|
|
53
|
+
"machine": {"type": "string", "minLength": 1}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"bounds": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": false,
|
|
59
|
+
"properties": {
|
|
60
|
+
"max_steps": {"type": "integer", "minimum": 1},
|
|
61
|
+
"max_wallclock_ms": {"type": "integer", "minimum": 1},
|
|
62
|
+
"max_cost_usd_micros": {"type": "integer", "minimum": 0}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"usage": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"additionalProperties": false,
|
|
68
|
+
"properties": {
|
|
69
|
+
"input_tokens": {"type": "integer", "minimum": 0},
|
|
70
|
+
"output_tokens": {"type": "integer", "minimum": 0},
|
|
71
|
+
"wallclock_ms": {"type": "integer", "minimum": 0}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"verification": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"additionalProperties": false,
|
|
77
|
+
"required": ["authority", "evidence_digest"],
|
|
78
|
+
"properties": {
|
|
79
|
+
"authority": {
|
|
80
|
+
"enum": [
|
|
81
|
+
"deterministic_gate",
|
|
82
|
+
"bounded_loop_receipt",
|
|
83
|
+
"human_approval",
|
|
84
|
+
"independent_model_audit"
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
"evidence_digest": {"$ref": "#/$defs/sha256"},
|
|
88
|
+
"reference": {"type": "string", "maxLength": 1024}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://superlocalmemory.com/schemas/agent-integration-contract-v2.schema.json",
|
|
4
|
+
"title": "SLM Agent Integration Contract v2",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schema_version", "host", "stage", "lifecycle", "artifact_digest", "evidence"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_version": {"const": "v2"},
|
|
10
|
+
"host": {"type": "string", "minLength": 1},
|
|
11
|
+
"stage": {"$ref": "#/$defs/certification_stage"},
|
|
12
|
+
"lifecycle": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"items": {
|
|
15
|
+
"enum": [
|
|
16
|
+
"INSTALL", "CONFIG_PARSE", "PROCESS_START", "MCP_DISCOVERY", "SESSION_OPEN",
|
|
17
|
+
"TARGETED_RECALL", "CONTEXT_INJECTION", "MEMORY_WRITE", "OUTCOME_CAPTURE",
|
|
18
|
+
"RECONNECT", "SESSION_CLOSE", "UNINSTALL", "CONFIG_PRESERVATION", "SECRET_BOUNDARY"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"uniqueItems": true,
|
|
22
|
+
"minItems": 14,
|
|
23
|
+
"maxItems": 14
|
|
24
|
+
},
|
|
25
|
+
"artifact_digest": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
26
|
+
"evidence": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"minItems": 1,
|
|
29
|
+
"items": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"additionalProperties": false,
|
|
32
|
+
"required": ["kind", "digest", "reference"],
|
|
33
|
+
"properties": {
|
|
34
|
+
"kind": {"enum": ["test_run", "artifact", "receipt", "manual_check"]},
|
|
35
|
+
"digest": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
36
|
+
"reference": {"type": "string", "minLength": 1, "maxLength": 1024}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"$defs": {
|
|
42
|
+
"certification_stage": {
|
|
43
|
+
"enum": ["STATIC", "CONFIG_PROVEN", "LOCAL_RUNTIME", "END_TO_END", "RELEASE_GATED"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://superlocalmemory.com/schemas/cognitive-turn-receipt-v1.schema.json",
|
|
4
|
+
"title": "SLM Cognitive Turn Receipt v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"receipt_id",
|
|
9
|
+
"task_id",
|
|
10
|
+
"profile_id",
|
|
11
|
+
"project_scope",
|
|
12
|
+
"query_digest",
|
|
13
|
+
"fact_decisions",
|
|
14
|
+
"state"
|
|
15
|
+
],
|
|
16
|
+
"properties": {
|
|
17
|
+
"receipt_id": {"type": "string", "minLength": 1},
|
|
18
|
+
"task_id": {"type": "string", "minLength": 1},
|
|
19
|
+
"profile_id": {"type": "string", "minLength": 1},
|
|
20
|
+
"project_scope": {"type": "string", "minLength": 1},
|
|
21
|
+
"query_digest": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
22
|
+
"fact_decisions": {
|
|
23
|
+
"description": "One decision per opaque fact identifier. An object, rather than an array, makes conflicting duplicate decisions structurally impossible for every schema consumer.",
|
|
24
|
+
"type": "object",
|
|
25
|
+
"minProperties": 1,
|
|
26
|
+
"additionalProperties": false,
|
|
27
|
+
"patternProperties": {
|
|
28
|
+
".+": {"enum": ["considered", "used", "rejected", "corrected"]}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"outcome": {"$ref": "#/$defs/outcome"},
|
|
32
|
+
"state": {"enum": ["open", "finalized", "abandoned", "reconciled"]}
|
|
33
|
+
},
|
|
34
|
+
"allOf": [
|
|
35
|
+
{
|
|
36
|
+
"if": {"properties": {"state": {"const": "finalized"}}, "required": ["state"]},
|
|
37
|
+
"then": {"required": ["outcome"]}
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"$defs": {
|
|
41
|
+
"outcome": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"required": ["authority", "receipt_digest", "reference"],
|
|
45
|
+
"properties": {
|
|
46
|
+
"authority": {
|
|
47
|
+
"enum": [
|
|
48
|
+
"deterministic_gate",
|
|
49
|
+
"bounded_loop_receipt",
|
|
50
|
+
"human_approval",
|
|
51
|
+
"independent_model_audit"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"receipt_digest": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
55
|
+
"reference": {"type": "string", "minLength": 1, "maxLength": 1024}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Validated SLM 4.0.2 contract boundaries.
|
|
2
|
+
|
|
3
|
+
These validators are the required pre-persistence boundary for forthcoming
|
|
4
|
+
Agent Experience and host-certificate writers. They prevent host/self-reported
|
|
5
|
+
telemetry from masquerading as independently verified learning evidence.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from importlib import resources
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from jsonschema import Draft202012Validator, FormatChecker # type: ignore[import-untyped]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ContractValidationError(ValueError):
|
|
19
|
+
"""Raised when a cross-lane public contract is invalid."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_FORMAT_CHECKER = FormatChecker()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@_FORMAT_CHECKER.checks("date-time")
|
|
26
|
+
def _is_rfc3339_datetime(value: object) -> bool:
|
|
27
|
+
"""Accept only timezone-aware ISO/RFC3339 timestamps."""
|
|
28
|
+
if not isinstance(value, str):
|
|
29
|
+
return False
|
|
30
|
+
try:
|
|
31
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
32
|
+
except ValueError:
|
|
33
|
+
return False
|
|
34
|
+
return parsed.tzinfo is not None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _validate(schema_name: str, payload: dict[str, Any]) -> None:
|
|
38
|
+
schema_path = resources.files(__package__).joinpath("schemas", schema_name)
|
|
39
|
+
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
40
|
+
errors = sorted(
|
|
41
|
+
Draft202012Validator(schema, format_checker=_FORMAT_CHECKER).iter_errors(payload),
|
|
42
|
+
key=lambda error: list(error.absolute_path),
|
|
43
|
+
)
|
|
44
|
+
if errors:
|
|
45
|
+
error = errors[0]
|
|
46
|
+
path = ".".join(str(part) for part in error.absolute_path) or "payload"
|
|
47
|
+
raise ContractValidationError(f"{path}: {error.message}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def validate_agent_experience(payload: dict[str, Any]) -> None:
|
|
51
|
+
"""Validate independent outcome evidence before an experience is stored."""
|
|
52
|
+
_validate("agent-experience-v1.schema.json", payload)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def validate_cognitive_turn(payload: dict[str, Any]) -> None:
|
|
56
|
+
"""Validate the language-neutral fact-keyed receipt structure."""
|
|
57
|
+
_validate("cognitive-turn-receipt-v1.schema.json", payload)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def validate_integration_certificate(payload: dict[str, Any]) -> None:
|
|
61
|
+
"""Validate a hash-bound host lifecycle certificate."""
|
|
62
|
+
_validate("agent-integration-contract-v2.schema.json", payload)
|
|
@@ -707,6 +707,9 @@ class MemoryEngine:
|
|
|
707
707
|
include_shared: bool | None = None,
|
|
708
708
|
window: str | tuple[str, str] | None = None,
|
|
709
709
|
as_of: str | None = None,
|
|
710
|
+
known_as_of: str | None = None,
|
|
711
|
+
valid_at: str | None = None,
|
|
712
|
+
include_unknown: bool = False,
|
|
710
713
|
) -> RecallResponse:
|
|
711
714
|
"""Recall relevant facts for a query.
|
|
712
715
|
|
|
@@ -744,6 +747,10 @@ class MemoryEngine:
|
|
|
744
747
|
if include_shared is None:
|
|
745
748
|
include_shared = bool(getattr(_scope_cfg, "recall_include_shared", False))
|
|
746
749
|
|
|
750
|
+
from superlocalmemory.retrieval.temporal_utils import normalize_strict_boundary
|
|
751
|
+
known_as_of = normalize_strict_boundary(known_as_of, "known_as_of")
|
|
752
|
+
valid_at = normalize_strict_boundary(valid_at, "valid_at")
|
|
753
|
+
|
|
747
754
|
pid = profile_id or self._profile_id
|
|
748
755
|
|
|
749
756
|
from superlocalmemory.core.recall_pipeline import run_recall
|
|
@@ -763,6 +770,9 @@ class MemoryEngine:
|
|
|
763
770
|
include_shared=include_shared,
|
|
764
771
|
window=window,
|
|
765
772
|
as_of=as_of,
|
|
773
|
+
known_as_of=known_as_of,
|
|
774
|
+
valid_at=valid_at,
|
|
775
|
+
include_unknown=include_unknown,
|
|
766
776
|
)
|
|
767
777
|
except Exception:
|
|
768
778
|
# Diagnostics are intentionally not recorded here. A recall is a
|
|
@@ -787,6 +787,9 @@ def run_recall(
|
|
|
787
787
|
include_shared: bool = False,
|
|
788
788
|
window: str | tuple[str, str] | None = None,
|
|
789
789
|
as_of: str | None = None,
|
|
790
|
+
known_as_of: str | None = None,
|
|
791
|
+
valid_at: str | None = None,
|
|
792
|
+
include_unknown: bool = False,
|
|
790
793
|
) -> RecallResponse:
|
|
791
794
|
"""Recall relevant facts for a query.
|
|
792
795
|
|
|
@@ -836,6 +839,9 @@ def run_recall(
|
|
|
836
839
|
include_shared=include_shared,
|
|
837
840
|
window=window,
|
|
838
841
|
as_of=as_of,
|
|
842
|
+
known_as_of=known_as_of,
|
|
843
|
+
valid_at=valid_at,
|
|
844
|
+
include_unknown=include_unknown,
|
|
839
845
|
)
|
|
840
846
|
_mark("retrieval(chan+rerank)")
|
|
841
847
|
|