agmem 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/METADATA +138 -14
  2. {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/RECORD +45 -26
  3. memvcs/cli.py +10 -0
  4. memvcs/commands/add.py +6 -0
  5. memvcs/commands/audit.py +59 -0
  6. memvcs/commands/clone.py +7 -0
  7. memvcs/commands/daemon.py +28 -0
  8. memvcs/commands/distill.py +16 -0
  9. memvcs/commands/federated.py +53 -0
  10. memvcs/commands/fsck.py +31 -0
  11. memvcs/commands/garden.py +14 -0
  12. memvcs/commands/gc.py +51 -0
  13. memvcs/commands/merge.py +55 -1
  14. memvcs/commands/prove.py +66 -0
  15. memvcs/commands/pull.py +27 -0
  16. memvcs/commands/resolve.py +130 -0
  17. memvcs/commands/verify.py +74 -23
  18. memvcs/core/audit.py +124 -0
  19. memvcs/core/consistency.py +9 -9
  20. memvcs/core/crypto_verify.py +280 -0
  21. memvcs/core/distiller.py +25 -25
  22. memvcs/core/encryption.py +169 -0
  23. memvcs/core/federated.py +86 -0
  24. memvcs/core/gardener.py +23 -24
  25. memvcs/core/ipfs_remote.py +39 -0
  26. memvcs/core/knowledge_graph.py +1 -0
  27. memvcs/core/llm/__init__.py +10 -0
  28. memvcs/core/llm/anthropic_provider.py +50 -0
  29. memvcs/core/llm/base.py +27 -0
  30. memvcs/core/llm/factory.py +30 -0
  31. memvcs/core/llm/openai_provider.py +36 -0
  32. memvcs/core/merge.py +36 -23
  33. memvcs/core/objects.py +16 -6
  34. memvcs/core/pack.py +92 -0
  35. memvcs/core/privacy_budget.py +63 -0
  36. memvcs/core/remote.py +38 -0
  37. memvcs/core/repository.py +82 -2
  38. memvcs/core/temporal_index.py +9 -0
  39. memvcs/core/trust.py +103 -0
  40. memvcs/core/vector_store.py +15 -1
  41. memvcs/core/zk_proofs.py +26 -0
  42. {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/WHEEL +0 -0
  43. {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/entry_points.txt +0 -0
  44. {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/licenses/LICENSE +0 -0
  45. {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/top_level.txt +0 -0
memvcs/core/remote.py CHANGED
@@ -164,6 +164,28 @@ class Remote:
164
164
  refs = RefsManager(self.mem_dir)
165
165
  store = ObjectStore(self.objects_dir)
166
166
 
167
+ # Push conflict detection: remote tip must be ancestor of local tip (non-fast-forward reject)
168
+ remote_heads = remote_refs / "heads"
169
+ for b in refs.list_branches():
170
+ if branch and b != branch:
171
+ continue
172
+ local_ch = refs.get_branch_commit(b)
173
+ if not local_ch:
174
+ continue
175
+ remote_branch_file = remote_heads / b
176
+ if remote_branch_file.exists():
177
+ remote_ch = remote_branch_file.read_text().strip()
178
+ if remote_ch and _valid_object_hash(remote_ch):
179
+ from .merge import MergeEngine
180
+ from .repository import Repository
181
+
182
+ repo = Repository(self.repo_path)
183
+ engine = MergeEngine(repo)
184
+ if not engine.find_common_ancestor(remote_ch, local_ch) == remote_ch:
185
+ raise ValueError(
186
+ "Push rejected: remote has diverged. Pull and merge first."
187
+ )
188
+
167
189
  # Collect objects to push
168
190
  to_push = set()
169
191
  for b in refs.list_branches():
@@ -206,6 +228,14 @@ class Remote:
206
228
  (remote_tags_dir / t).parent.mkdir(parents=True, exist_ok=True)
207
229
  (remote_tags_dir / t).write_text(ch + "\n")
208
230
 
231
+ try:
232
+ from .audit import append_audit
233
+
234
+ append_audit(
235
+ self.mem_dir, "push", {"remote": self.name, "branch": branch, "copied": copied}
236
+ )
237
+ except Exception:
238
+ pass
209
239
  return f"Pushed {copied} object(s) to {self.name}"
210
240
 
211
241
  def fetch(self, branch: Optional[str] = None) -> str:
@@ -275,4 +305,12 @@ class Remote:
275
305
  if ch:
276
306
  refs.create_tag(tag_name, ch)
277
307
 
308
+ try:
309
+ from .audit import append_audit
310
+
311
+ append_audit(
312
+ self.mem_dir, "fetch", {"remote": self.name, "branch": branch, "copied": copied}
313
+ )
314
+ except Exception:
315
+ pass
278
316
  return f"Fetched {copied} object(s) from {self.name}"
memvcs/core/repository.py CHANGED
@@ -36,7 +36,23 @@ class Repository:
36
36
 
37
37
  def _init_components(self):
38
38
  """Initialize repository components."""
39
- self.object_store = ObjectStore(self.mem_dir / "objects")
39
+ encryptor = None
40
+ try:
41
+ config = self.get_config()
42
+ if config.get("encryption", {}).get("enabled"):
43
+ from .encryption import (
44
+ load_encryption_config,
45
+ ObjectStoreEncryptor,
46
+ get_key_from_env_or_cache,
47
+ )
48
+
49
+ if load_encryption_config(self.mem_dir):
50
+ encryptor = ObjectStoreEncryptor(
51
+ lambda: get_key_from_env_or_cache(self.mem_dir)
52
+ )
53
+ except Exception:
54
+ pass
55
+ self.object_store = ObjectStore(self.mem_dir / "objects", encryptor=encryptor)
40
56
  self.staging = StagingArea(self.mem_dir)
41
57
  self.refs = RefsManager(self.mem_dir)
42
58
 
@@ -96,6 +112,14 @@ class Repository:
96
112
  # Initialize HEAD
97
113
  repo.refs.init_head("main")
98
114
 
115
+ # Tamper-evident audit
116
+ try:
117
+ from .audit import append_audit
118
+
119
+ append_audit(repo.mem_dir, "init", {"author": author_name, "branch": "main"})
120
+ except Exception:
121
+ pass
122
+
99
123
  return repo
100
124
 
101
125
  def is_valid_repo(self) -> bool:
@@ -115,6 +139,12 @@ class Repository:
115
139
  def set_config(self, config: Dict[str, Any]):
116
140
  """Set repository configuration."""
117
141
  self.config_file.write_text(json.dumps(config, indent=2))
142
+ try:
143
+ from .audit import append_audit
144
+
145
+ append_audit(self.mem_dir, "config_change", {})
146
+ except Exception:
147
+ pass
118
148
 
119
149
  def get_author(self) -> str:
120
150
  """Get the configured author string."""
@@ -298,6 +328,30 @@ class Repository:
298
328
  head_commit = self.get_head_commit()
299
329
  parents = [head_commit.store(self.object_store)] if head_commit else []
300
330
 
331
+ # Cryptographic verification: Merkle root + optional signing (private key from env)
332
+ meta = dict(metadata or {})
333
+ try:
334
+ from .crypto_verify import (
335
+ _collect_blob_hashes_from_tree,
336
+ build_merkle_tree,
337
+ load_private_key_from_env,
338
+ sign_merkle_root,
339
+ ED25519_AVAILABLE,
340
+ )
341
+ from .objects import Tree
342
+
343
+ tree = Tree.load(self.object_store, tree_hash)
344
+ if tree:
345
+ blobs = _collect_blob_hashes_from_tree(self.object_store, tree_hash)
346
+ merkle_root = build_merkle_tree(blobs)
347
+ meta["merkle_root"] = merkle_root
348
+ if ED25519_AVAILABLE:
349
+ private_pem = load_private_key_from_env()
350
+ if private_pem:
351
+ meta["signature"] = sign_merkle_root(merkle_root, private_pem)
352
+ except Exception:
353
+ pass
354
+
301
355
  # Create commit
302
356
  commit = Commit(
303
357
  tree=tree_hash,
@@ -305,7 +359,7 @@ class Repository:
305
359
  author=self.get_author(),
306
360
  timestamp=datetime.utcnow().isoformat() + "Z",
307
361
  message=message,
308
- metadata=metadata or {},
362
+ metadata=meta,
309
363
  )
310
364
  commit_hash = commit.store(self.object_store)
311
365
 
@@ -313,6 +367,14 @@ class Repository:
313
367
  old_hash = parents[0] if parents else "0" * 64
314
368
  self.refs.append_reflog("HEAD", old_hash, commit_hash, f"commit: {message}")
315
369
 
370
+ # Audit
371
+ try:
372
+ from .audit import append_audit
373
+
374
+ append_audit(self.mem_dir, "commit", {"commit": commit_hash, "message": message})
375
+ except Exception:
376
+ pass
377
+
316
378
  # Update HEAD
317
379
  head = self.refs.get_head()
318
380
  if head["type"] == "branch":
@@ -354,6 +416,16 @@ class Repository:
354
416
  if not tree:
355
417
  raise ValueError(f"Reference not found: {ref}")
356
418
 
419
+ # Cryptographic verification: reject if Merkle/signature invalid
420
+ try:
421
+ from .crypto_verify import verify_commit_optional
422
+
423
+ verify_commit_optional(
424
+ self.object_store, commit_hash, mem_dir=self.mem_dir, strict=False
425
+ )
426
+ except ValueError as e:
427
+ raise ValueError(str(e))
428
+
357
429
  # Check for uncommitted changes
358
430
  if not force:
359
431
  staged = self.staging.get_staged_files()
@@ -377,6 +449,14 @@ class Repository:
377
449
  # Clear staging
378
450
  self.staging.clear()
379
451
 
452
+ # Audit
453
+ try:
454
+ from .audit import append_audit
455
+
456
+ append_audit(self.mem_dir, "checkout", {"ref": ref, "commit": commit_hash})
457
+ except Exception:
458
+ pass
459
+
380
460
  return commit_hash
381
461
 
382
462
  def get_status(self) -> Dict[str, Any]:
@@ -110,3 +110,12 @@ class TemporalIndex:
110
110
  if idx == 0:
111
111
  return None # All commits are after the requested time
112
112
  return timeline[idx - 1][1]
113
+
114
+ def range_query(self, start_str: str, end_str: str) -> List[Tuple[datetime, str]]:
115
+ """Return (timestamp, commit_hash) entries in [start, end]. For range queries."""
116
+ start_dt = _parse_iso_timestamp(start_str)
117
+ end_dt = _parse_iso_timestamp(end_str)
118
+ if not start_dt or not end_dt:
119
+ return []
120
+ timeline = self._build_commit_timeline()
121
+ return [(t, h) for t, h in timeline if start_dt <= t <= end_dt]
memvcs/core/trust.py ADDED
@@ -0,0 +1,103 @@
1
+ """
2
+ Multi-agent trust and identity model for agmem.
3
+
4
+ Trust store: map public keys to levels (full | conditional | untrusted).
5
+ Used on pull/merge to decide auto-merge, prompt, or block.
6
+ """
7
+
8
+ import hashlib
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Optional, Dict, List, Any, Union
12
+
13
+ TRUST_LEVELS = ("full", "conditional", "untrusted")
14
+
15
+
16
+ def _trust_dir(mem_dir: Path) -> Path:
17
+ return mem_dir / "trust"
18
+
19
+
20
+ def _trust_file(mem_dir: Path) -> Path:
21
+ return _trust_dir(mem_dir) / "trust.json"
22
+
23
+
24
+ def _key_id(public_key_pem: bytes) -> str:
25
+ """Stable id for a public key (hash of PEM)."""
26
+ return hashlib.sha256(public_key_pem).hexdigest()[:16]
27
+
28
+
29
+ def _ensure_bytes(pem: Union[bytes, str]) -> bytes:
30
+ """Normalize PEM to bytes for hashing/serialization."""
31
+ return pem.encode("utf-8") if isinstance(pem, str) else pem
32
+
33
+
34
+ def load_trust_store(mem_dir: Path) -> List[Dict[str, Any]]:
35
+ """Load trust store: list of { key_id, public_key_pem, level }."""
36
+ path = _trust_file(mem_dir)
37
+ if not path.exists():
38
+ return []
39
+ try:
40
+ data = json.loads(path.read_text())
41
+ return data.get("entries", [])
42
+ except Exception:
43
+ return []
44
+
45
+
46
+ def get_trust_level(mem_dir: Path, public_key_pem: Union[bytes, str]) -> Optional[str]:
47
+ """Get trust level for a public key. Returns 'full'|'conditional'|'untrusted' or None if unknown."""
48
+ pem_b = _ensure_bytes(public_key_pem)
49
+ kid = _key_id(pem_b)
50
+ key_pem_str = pem_b.decode("utf-8")
51
+ for e in load_trust_store(mem_dir):
52
+ entry_id = e.get("key_id") or _key_id((e.get("public_key_pem") or "").encode())
53
+ if entry_id == kid:
54
+ return e.get("level")
55
+ if e.get("public_key_pem") == key_pem_str:
56
+ return e.get("level")
57
+ return None
58
+
59
+
60
+ # Reasonable upper bound for PEM to avoid DoS (typical Ed25519 public PEM ~120 bytes)
61
+ _MAX_PEM_BYTES = 8192
62
+
63
+
64
+ def set_trust(mem_dir: Path, public_key_pem: Union[bytes, str], level: str) -> None:
65
+ """Set trust level for a public key. level: full | conditional | untrusted."""
66
+ if level not in TRUST_LEVELS:
67
+ raise ValueError(f"level must be one of {TRUST_LEVELS}")
68
+ pem_b = _ensure_bytes(public_key_pem)
69
+ if len(pem_b) > _MAX_PEM_BYTES:
70
+ raise ValueError("Public key PEM exceeds maximum size")
71
+ kid = _key_id(pem_b)
72
+ key_pem_str = pem_b.decode("utf-8")
73
+ _trust_dir(mem_dir).mkdir(parents=True, exist_ok=True)
74
+ entries = load_trust_store(mem_dir)
75
+ entries = [
76
+ e
77
+ for e in entries
78
+ if (e.get("key_id") or _key_id((e.get("public_key_pem") or "").encode())) != kid
79
+ ]
80
+ entries.append({"key_id": kid, "public_key_pem": key_pem_str, "level": level})
81
+ _trust_file(mem_dir).write_text(json.dumps({"entries": entries}, indent=2))
82
+
83
+
84
+ def find_verifying_key(mem_dir: Path, commit_metadata: Dict[str, Any]) -> Optional[bytes]:
85
+ """
86
+ Try each key in trust store to verify the commit's signature.
87
+ commit_metadata should have merkle_root and signature.
88
+ Returns public_key_pem of first key that verifies, or None.
89
+ """
90
+ from .crypto_verify import verify_signature
91
+
92
+ root = commit_metadata.get("merkle_root")
93
+ sig = commit_metadata.get("signature")
94
+ if not root or not sig:
95
+ return None
96
+ for e in load_trust_store(mem_dir):
97
+ pem = e.get("public_key_pem")
98
+ if not pem:
99
+ continue
100
+ pem_b = _ensure_bytes(pem)
101
+ if verify_signature(root, sig, pem_b):
102
+ return pem_b
103
+ return None
@@ -56,6 +56,19 @@ class VectorStore:
56
56
  "On macOS, try: brew install python (for Homebrew SQLite)"
57
57
  ) from e
58
58
 
59
+ def _device(self) -> str:
60
+ """Return device for embeddings: cuda/mps/cpu. GPU acceleration when available."""
61
+ try:
62
+ import torch
63
+
64
+ if torch.cuda.is_available():
65
+ return "cuda"
66
+ if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
67
+ return "mps"
68
+ except ImportError:
69
+ pass
70
+ return "cpu"
71
+
59
72
  def _get_model(self):
60
73
  """Lazy-load the sentence-transformers model."""
61
74
  if self._model is not None:
@@ -64,7 +77,8 @@ class VectorStore:
64
77
  try:
65
78
  from sentence_transformers import SentenceTransformer
66
79
 
67
- self._model = SentenceTransformer("all-MiniLM-L6-v2")
80
+ device = self._device()
81
+ self._model = SentenceTransformer("all-MiniLM-L6-v2", device=device)
68
82
  return self._model
69
83
  except ImportError as e:
70
84
  raise ImportError(
@@ -0,0 +1,26 @@
1
+ """
2
+ Zero-knowledge proof system for agmem (stub).
3
+
4
+ Planned: zk-SNARKs (Groth16) for keyword containment, memory freshness, competence verification.
5
+ Requires optional zk extra (circuit lib, proving system). Trusted setup: public ceremony or small multi-party.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import Optional, Tuple
10
+
11
+
12
+ def prove_keyword_containment(memory_path: Path, keyword: str, output_proof_path: Path) -> bool:
13
+ """Prove memory file contains keyword without revealing content. Stub: returns False until zk backend added."""
14
+ return False
15
+
16
+
17
+ def prove_memory_freshness(
18
+ memory_path: Path, after_timestamp: str, output_proof_path: Path
19
+ ) -> bool:
20
+ """Prove memory was updated after date without revealing content. Stub: returns False until zk backend added."""
21
+ return False
22
+
23
+
24
+ def verify_proof(proof_path: Path, statement_type: str, **kwargs) -> bool:
25
+ """Verify a zk proof. Stub: returns False until zk backend added."""
26
+ return False
File without changes