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.
- {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/METADATA +138 -14
- {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/RECORD +45 -26
- memvcs/cli.py +10 -0
- memvcs/commands/add.py +6 -0
- memvcs/commands/audit.py +59 -0
- memvcs/commands/clone.py +7 -0
- memvcs/commands/daemon.py +28 -0
- memvcs/commands/distill.py +16 -0
- memvcs/commands/federated.py +53 -0
- memvcs/commands/fsck.py +31 -0
- memvcs/commands/garden.py +14 -0
- memvcs/commands/gc.py +51 -0
- memvcs/commands/merge.py +55 -1
- memvcs/commands/prove.py +66 -0
- memvcs/commands/pull.py +27 -0
- memvcs/commands/resolve.py +130 -0
- memvcs/commands/verify.py +74 -23
- memvcs/core/audit.py +124 -0
- memvcs/core/consistency.py +9 -9
- memvcs/core/crypto_verify.py +280 -0
- memvcs/core/distiller.py +25 -25
- memvcs/core/encryption.py +169 -0
- memvcs/core/federated.py +86 -0
- memvcs/core/gardener.py +23 -24
- memvcs/core/ipfs_remote.py +39 -0
- memvcs/core/knowledge_graph.py +1 -0
- memvcs/core/llm/__init__.py +10 -0
- memvcs/core/llm/anthropic_provider.py +50 -0
- memvcs/core/llm/base.py +27 -0
- memvcs/core/llm/factory.py +30 -0
- memvcs/core/llm/openai_provider.py +36 -0
- memvcs/core/merge.py +36 -23
- memvcs/core/objects.py +16 -6
- memvcs/core/pack.py +92 -0
- memvcs/core/privacy_budget.py +63 -0
- memvcs/core/remote.py +38 -0
- memvcs/core/repository.py +82 -2
- memvcs/core/temporal_index.py +9 -0
- memvcs/core/trust.py +103 -0
- memvcs/core/vector_store.py +15 -1
- memvcs/core/zk_proofs.py +26 -0
- {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/WHEEL +0 -0
- {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/entry_points.txt +0 -0
- {agmem-0.1.2.dist-info → agmem-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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=
|
|
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]:
|
memvcs/core/temporal_index.py
CHANGED
|
@@ -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
|
memvcs/core/vector_store.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
memvcs/core/zk_proofs.py
ADDED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|