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/commands/fsck.py
CHANGED
|
@@ -66,6 +66,10 @@ class FsckCommand:
|
|
|
66
66
|
issues_found += ref_issues
|
|
67
67
|
issues_fixed += ref_fixed
|
|
68
68
|
|
|
69
|
+
# Cryptographic verification (Merkle + signature)
|
|
70
|
+
crypto_issues = FsckCommand._check_crypto(repo, args.verbose)
|
|
71
|
+
issues_found += crypto_issues
|
|
72
|
+
|
|
69
73
|
# Print summary
|
|
70
74
|
print()
|
|
71
75
|
print("=" * 40)
|
|
@@ -195,3 +199,30 @@ class FsckCommand:
|
|
|
195
199
|
print(f" Found {issues} ref issues")
|
|
196
200
|
|
|
197
201
|
return issues, 0
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _check_crypto(repo, verbose: bool) -> int:
|
|
205
|
+
"""Verify Merkle/signature on branch tips. Returns number of issues."""
|
|
206
|
+
print("\nChecking commit signatures...")
|
|
207
|
+
try:
|
|
208
|
+
from ..core.crypto_verify import verify_commit, load_public_key
|
|
209
|
+
except ImportError:
|
|
210
|
+
if verbose:
|
|
211
|
+
print(" Crypto verification not available")
|
|
212
|
+
return 0
|
|
213
|
+
issues = 0
|
|
214
|
+
pub = load_public_key(repo.mem_dir)
|
|
215
|
+
for branch in repo.refs.list_branches():
|
|
216
|
+
ch = repo.refs.get_branch_commit(branch)
|
|
217
|
+
if not ch:
|
|
218
|
+
continue
|
|
219
|
+
ok, err = verify_commit(repo.object_store, ch, public_key_pem=pub, mem_dir=repo.mem_dir)
|
|
220
|
+
if not ok:
|
|
221
|
+
issues += 1
|
|
222
|
+
if verbose:
|
|
223
|
+
print(f" {branch} ({ch[:8]}): {err}")
|
|
224
|
+
if issues == 0:
|
|
225
|
+
print(" Commit signatures consistent")
|
|
226
|
+
else:
|
|
227
|
+
print(f" Found {issues} commit(s) with verification issues")
|
|
228
|
+
return issues
|
memvcs/commands/garden.py
CHANGED
|
@@ -38,6 +38,11 @@ class GardenCommand:
|
|
|
38
38
|
help="LLM provider for summarization (default: none)",
|
|
39
39
|
)
|
|
40
40
|
parser.add_argument("--model", help="LLM model to use (e.g., gpt-3.5-turbo)")
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--private",
|
|
43
|
+
action="store_true",
|
|
44
|
+
help="Use differential privacy (spend epsilon from budget)",
|
|
45
|
+
)
|
|
41
46
|
|
|
42
47
|
@staticmethod
|
|
43
48
|
def execute(args) -> int:
|
|
@@ -45,6 +50,15 @@ class GardenCommand:
|
|
|
45
50
|
if code != 0:
|
|
46
51
|
return code
|
|
47
52
|
|
|
53
|
+
if getattr(args, "private", False):
|
|
54
|
+
from ..core.privacy_budget import load_budget, spend_epsilon
|
|
55
|
+
|
|
56
|
+
spent, max_eps, _ = load_budget(repo.mem_dir)
|
|
57
|
+
epsilon_cost = 0.1
|
|
58
|
+
if not spend_epsilon(repo.mem_dir, epsilon_cost):
|
|
59
|
+
print(f"Privacy budget exceeded (spent {spent:.2f}, max {max_eps}).")
|
|
60
|
+
return 1
|
|
61
|
+
|
|
48
62
|
# Build config
|
|
49
63
|
config = GardenerConfig(
|
|
50
64
|
threshold=args.threshold,
|
memvcs/commands/gc.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem gc - Garbage collection.
|
|
3
|
+
|
|
4
|
+
Remove unreachable objects; optionally repack.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
|
|
9
|
+
from ..commands.base import require_repo
|
|
10
|
+
from ..core.pack import run_gc
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GcCommand:
|
|
14
|
+
"""Garbage collect unreachable objects."""
|
|
15
|
+
|
|
16
|
+
name = "gc"
|
|
17
|
+
help = "Remove unreachable objects (garbage collection)"
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--dry-run",
|
|
23
|
+
action="store_true",
|
|
24
|
+
help="Report what would be removed without deleting",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--prune-days",
|
|
28
|
+
type=int,
|
|
29
|
+
default=90,
|
|
30
|
+
metavar="N",
|
|
31
|
+
help="Consider reflog entries within N days (default 90)",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def execute(args) -> int:
|
|
36
|
+
repo, code = require_repo()
|
|
37
|
+
if code != 0:
|
|
38
|
+
return code
|
|
39
|
+
|
|
40
|
+
gc_prune_days = getattr(args, "prune_days", 90)
|
|
41
|
+
deleted, freed = run_gc(
|
|
42
|
+
repo.mem_dir,
|
|
43
|
+
repo.object_store,
|
|
44
|
+
gc_prune_days=gc_prune_days,
|
|
45
|
+
dry_run=args.dry_run,
|
|
46
|
+
)
|
|
47
|
+
if args.dry_run:
|
|
48
|
+
print(f"Would remove {deleted} unreachable object(s) ({freed} bytes).")
|
|
49
|
+
else:
|
|
50
|
+
print(f"Removed {deleted} unreachable object(s) ({freed} bytes reclaimed).")
|
|
51
|
+
return 0
|
memvcs/commands/merge.py
CHANGED
|
@@ -23,6 +23,11 @@ class MergeCommand:
|
|
|
23
23
|
"--no-commit", action="store_true", help="Perform merge but do not commit"
|
|
24
24
|
)
|
|
25
25
|
parser.add_argument("--abort", action="store_true", help="Abort the current merge")
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--yes",
|
|
28
|
+
action="store_true",
|
|
29
|
+
help="Accept conditionally trusted branch commits without prompting",
|
|
30
|
+
)
|
|
26
31
|
|
|
27
32
|
@staticmethod
|
|
28
33
|
def execute(args) -> int:
|
|
@@ -53,6 +58,24 @@ class MergeCommand:
|
|
|
53
58
|
print(f"Error: Branch '{args.branch}' not found.")
|
|
54
59
|
return 1
|
|
55
60
|
|
|
61
|
+
# Trust check for branch tip (may be from another agent)
|
|
62
|
+
other_commit_hash = repo.refs.get_branch_commit(args.branch)
|
|
63
|
+
if other_commit_hash:
|
|
64
|
+
from ..core.objects import Commit
|
|
65
|
+
from ..core.trust import find_verifying_key, get_trust_level
|
|
66
|
+
|
|
67
|
+
other_commit = Commit.load(repo.object_store, other_commit_hash)
|
|
68
|
+
if other_commit and other_commit.metadata:
|
|
69
|
+
key_pem = find_verifying_key(repo.mem_dir, other_commit.metadata)
|
|
70
|
+
if key_pem is not None:
|
|
71
|
+
level = get_trust_level(repo.mem_dir, key_pem)
|
|
72
|
+
if level == "untrusted":
|
|
73
|
+
print(f"Merge blocked: branch '{args.branch}' signed by untrusted key.")
|
|
74
|
+
return 1
|
|
75
|
+
if level == "conditional" and not getattr(args, "yes", False):
|
|
76
|
+
print("Branch signed by conditionally trusted key. Use --yes to merge.")
|
|
77
|
+
return 1
|
|
78
|
+
|
|
56
79
|
# Perform merge
|
|
57
80
|
engine = MergeEngine(repo)
|
|
58
81
|
result = engine.merge(args.branch, message=args.message)
|
|
@@ -61,16 +84,47 @@ class MergeCommand:
|
|
|
61
84
|
print(f"Merge successful: {result.message}")
|
|
62
85
|
if result.commit_hash:
|
|
63
86
|
print(f" Commit: {result.commit_hash[:8]}")
|
|
87
|
+
try:
|
|
88
|
+
from ..core.audit import append_audit
|
|
89
|
+
|
|
90
|
+
append_audit(
|
|
91
|
+
repo.mem_dir, "merge", {"branch": args.branch, "commit": result.commit_hash}
|
|
92
|
+
)
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
64
95
|
return 0
|
|
65
96
|
else:
|
|
66
97
|
print(f"Merge failed: {result.message}")
|
|
67
98
|
|
|
68
99
|
if result.conflicts:
|
|
100
|
+
# Persist conflicts for agmem resolve
|
|
101
|
+
try:
|
|
102
|
+
import json
|
|
103
|
+
|
|
104
|
+
merge_dir = repo.mem_dir / "merge"
|
|
105
|
+
merge_dir.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
conflicts_data = [
|
|
107
|
+
{
|
|
108
|
+
"path": c.path,
|
|
109
|
+
"message": c.message,
|
|
110
|
+
"memory_type": getattr(c, "memory_type", None),
|
|
111
|
+
"payload": getattr(c, "payload", None),
|
|
112
|
+
"ours_content": c.ours_content,
|
|
113
|
+
"theirs_content": c.theirs_content,
|
|
114
|
+
"base_content": c.base_content,
|
|
115
|
+
}
|
|
116
|
+
for c in result.conflicts
|
|
117
|
+
]
|
|
118
|
+
(merge_dir / "conflicts.json").write_text(json.dumps(conflicts_data, indent=2))
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
69
121
|
print()
|
|
70
122
|
print("Conflicts:")
|
|
71
123
|
for conflict in result.conflicts:
|
|
72
124
|
print(f" {conflict.path}")
|
|
73
125
|
print()
|
|
74
|
-
print(
|
|
126
|
+
print(
|
|
127
|
+
"Resolve conflicts with 'agmem resolve' or edit files and run 'agmem commit'."
|
|
128
|
+
)
|
|
75
129
|
|
|
76
130
|
return 1
|
memvcs/commands/prove.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem prove - Generate zero-knowledge proofs (stub).
|
|
3
|
+
|
|
4
|
+
Prove properties of memory (keyword, freshness) without revealing content.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ..commands.base import require_repo
|
|
11
|
+
from ..core.zk_proofs import prove_keyword_containment, prove_memory_freshness
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProveCommand:
|
|
15
|
+
"""Generate zk proofs for memory properties."""
|
|
16
|
+
|
|
17
|
+
name = "prove"
|
|
18
|
+
help = "Prove a property of memory without revealing content (zk stub)"
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--memory", "-m", required=True, help="Memory file path (under current/)"
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--property",
|
|
27
|
+
"-p",
|
|
28
|
+
choices=["keyword", "freshness"],
|
|
29
|
+
required=True,
|
|
30
|
+
help="Property to prove",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument("--value", "-v", help="Value (e.g. keyword or ISO date)")
|
|
33
|
+
parser.add_argument("--output", "-o", help="Output proof file path")
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def execute(args) -> int:
|
|
37
|
+
repo, code = require_repo()
|
|
38
|
+
if code != 0:
|
|
39
|
+
return code
|
|
40
|
+
|
|
41
|
+
path = repo.current_dir / args.memory
|
|
42
|
+
if not path.exists():
|
|
43
|
+
print(f"Memory file not found: {args.memory}")
|
|
44
|
+
return 1
|
|
45
|
+
|
|
46
|
+
out = args.output or "proof.bin"
|
|
47
|
+
out_path = Path(out)
|
|
48
|
+
if not out_path.is_absolute():
|
|
49
|
+
out_path = repo.root / out_path
|
|
50
|
+
|
|
51
|
+
if args.property == "keyword":
|
|
52
|
+
if not args.value:
|
|
53
|
+
print("--value required for keyword (the keyword)")
|
|
54
|
+
return 1
|
|
55
|
+
ok = prove_keyword_containment(path, args.value, out_path)
|
|
56
|
+
else:
|
|
57
|
+
if not args.value:
|
|
58
|
+
print("--value required for freshness (ISO date)")
|
|
59
|
+
return 1
|
|
60
|
+
ok = prove_memory_freshness(path, args.value, out_path)
|
|
61
|
+
|
|
62
|
+
if not ok:
|
|
63
|
+
print("Proof generation not yet implemented (zk backend required).")
|
|
64
|
+
return 1
|
|
65
|
+
print(f"Proof written to {out_path}")
|
|
66
|
+
return 0
|
memvcs/commands/pull.py
CHANGED
|
@@ -25,6 +25,11 @@ class PullCommand:
|
|
|
25
25
|
nargs="?",
|
|
26
26
|
help="Branch to pull (default: all)",
|
|
27
27
|
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--yes",
|
|
30
|
+
action="store_true",
|
|
31
|
+
help="Accept conditionally trusted remote commits without prompting",
|
|
32
|
+
)
|
|
28
33
|
|
|
29
34
|
@staticmethod
|
|
30
35
|
def execute(args) -> int:
|
|
@@ -51,6 +56,28 @@ class PullCommand:
|
|
|
51
56
|
remote_ref = f"{args.remote}/{current_branch}"
|
|
52
57
|
remote_hash = repo.resolve_ref(remote_ref)
|
|
53
58
|
if remote_hash:
|
|
59
|
+
from memvcs.core.crypto_verify import verify_commit_optional
|
|
60
|
+
|
|
61
|
+
verify_commit_optional(
|
|
62
|
+
repo.object_store, remote_hash, mem_dir=repo.mem_dir, strict=False
|
|
63
|
+
)
|
|
64
|
+
# Trust check: block or require confirmation for untrusted/conditional
|
|
65
|
+
from memvcs.core.objects import Commit
|
|
66
|
+
from memvcs.core.trust import find_verifying_key, get_trust_level
|
|
67
|
+
|
|
68
|
+
remote_commit = Commit.load(repo.object_store, remote_hash)
|
|
69
|
+
if remote_commit and remote_commit.metadata:
|
|
70
|
+
key_pem = find_verifying_key(repo.mem_dir, remote_commit.metadata)
|
|
71
|
+
if key_pem is not None:
|
|
72
|
+
level = get_trust_level(repo.mem_dir, key_pem)
|
|
73
|
+
if level == "untrusted":
|
|
74
|
+
print(f"Pull blocked: remote commit signed by untrusted key.")
|
|
75
|
+
return 1
|
|
76
|
+
if level == "conditional" and not getattr(args, "yes", False):
|
|
77
|
+
print(
|
|
78
|
+
"Remote commit from conditionally trusted key. Use --yes to merge."
|
|
79
|
+
)
|
|
80
|
+
return 1
|
|
54
81
|
from memvcs.core.merge import MergeEngine
|
|
55
82
|
|
|
56
83
|
merge_engine = MergeEngine(repo)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem resolve - Structured conflict resolution.
|
|
3
|
+
|
|
4
|
+
Resolve merge conflicts with ours/theirs/both choices; record in audit.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from ..commands.base import require_repo
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _path_under_current(path_str: str, current_dir: Path) -> Optional[Path]:
|
|
16
|
+
"""Resolve path under current_dir; return None if it escapes (path traversal)."""
|
|
17
|
+
try:
|
|
18
|
+
full = (current_dir / path_str).resolve()
|
|
19
|
+
full.relative_to(current_dir.resolve())
|
|
20
|
+
return full
|
|
21
|
+
except (ValueError, RuntimeError):
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _resolved_content(choice: str, ours: str, theirs: str) -> str:
|
|
26
|
+
"""Return content for choice: ours, theirs, or both (merged)."""
|
|
27
|
+
if choice == "ours":
|
|
28
|
+
return ours
|
|
29
|
+
if choice == "theirs":
|
|
30
|
+
return theirs
|
|
31
|
+
return ours.rstrip() + "\n\n--- merged ---\n\n" + theirs
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ResolveCommand:
|
|
35
|
+
"""Resolve merge conflicts interactively (ours/theirs/both)."""
|
|
36
|
+
|
|
37
|
+
name = "resolve"
|
|
38
|
+
help = "Resolve merge conflicts (ours / theirs / both)"
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"path",
|
|
44
|
+
nargs="?",
|
|
45
|
+
help="Conflict path to resolve (or resolve all if omitted)",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--ours",
|
|
49
|
+
action="store_true",
|
|
50
|
+
help="Resolve with our version",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--theirs",
|
|
54
|
+
action="store_true",
|
|
55
|
+
help="Resolve with their version",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--both",
|
|
59
|
+
action="store_true",
|
|
60
|
+
help="Keep both (append theirs after ours with separator)",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def execute(args) -> int:
|
|
65
|
+
repo, code = require_repo()
|
|
66
|
+
if code != 0:
|
|
67
|
+
return code
|
|
68
|
+
|
|
69
|
+
merge_dir = repo.mem_dir / "merge"
|
|
70
|
+
conflicts_file = merge_dir / "conflicts.json"
|
|
71
|
+
if not conflicts_file.exists():
|
|
72
|
+
print("No unresolved conflicts.")
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
conflicts = json.loads(conflicts_file.read_text())
|
|
77
|
+
except Exception:
|
|
78
|
+
print("Could not read conflicts file.")
|
|
79
|
+
return 1
|
|
80
|
+
|
|
81
|
+
if not conflicts:
|
|
82
|
+
print("No unresolved conflicts.")
|
|
83
|
+
conflicts_file.unlink(missing_ok=True)
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
choice = None
|
|
87
|
+
if args.ours:
|
|
88
|
+
choice = "ours"
|
|
89
|
+
elif args.theirs:
|
|
90
|
+
choice = "theirs"
|
|
91
|
+
elif args.both:
|
|
92
|
+
choice = "both"
|
|
93
|
+
|
|
94
|
+
resolved = 0
|
|
95
|
+
remaining = []
|
|
96
|
+
for c in conflicts:
|
|
97
|
+
path = c.get("path", "")
|
|
98
|
+
if args.path and path != args.path:
|
|
99
|
+
remaining.append(c)
|
|
100
|
+
continue
|
|
101
|
+
if choice is None:
|
|
102
|
+
print(f"Conflict: {path}")
|
|
103
|
+
print(" Use: agmem resolve <path> --ours | --theirs | --both")
|
|
104
|
+
remaining.append(c)
|
|
105
|
+
continue
|
|
106
|
+
ours_content = c.get("ours_content") or ""
|
|
107
|
+
theirs_content = c.get("theirs_content") or ""
|
|
108
|
+
full_path = _path_under_current(path, repo.current_dir)
|
|
109
|
+
if full_path is None:
|
|
110
|
+
print(f"Error: Conflict path escapes repository: {path}")
|
|
111
|
+
remaining.append(c)
|
|
112
|
+
continue
|
|
113
|
+
content = _resolved_content(choice, ours_content, theirs_content)
|
|
114
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
full_path.write_text(content, encoding="utf-8")
|
|
116
|
+
resolved += 1
|
|
117
|
+
try:
|
|
118
|
+
from ..core.audit import append_audit
|
|
119
|
+
|
|
120
|
+
append_audit(repo.mem_dir, "resolve", {"path": path, "choice": choice})
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
if remaining:
|
|
125
|
+
conflicts_file.write_text(json.dumps(remaining, indent=2))
|
|
126
|
+
else:
|
|
127
|
+
conflicts_file.unlink(missing_ok=True)
|
|
128
|
+
if resolved:
|
|
129
|
+
print(f"Resolved {resolved} conflict(s). Stage and commit to complete.")
|
|
130
|
+
return 0
|
memvcs/commands/verify.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
agmem verify - Belief consistency
|
|
2
|
+
agmem verify - Belief consistency and cryptographic commit verification.
|
|
3
3
|
|
|
4
|
-
Scans semantic memories for logical contradictions.
|
|
4
|
+
Scans semantic memories for logical contradictions; optionally verifies commit Merkle/signatures.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import argparse
|
|
@@ -12,10 +12,10 @@ from ..core.consistency import ConsistencyChecker, ConsistencyResult
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class VerifyCommand:
|
|
15
|
-
"""Verify belief consistency of
|
|
15
|
+
"""Verify belief consistency and/or cryptographic integrity of commits."""
|
|
16
16
|
|
|
17
17
|
name = "verify"
|
|
18
|
-
help = "Scan semantic memories for
|
|
18
|
+
help = "Scan semantic memories for contradictions; optionally verify commit signatures"
|
|
19
19
|
|
|
20
20
|
@staticmethod
|
|
21
21
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
@@ -23,8 +23,17 @@ class VerifyCommand:
|
|
|
23
23
|
"--consistency",
|
|
24
24
|
"-c",
|
|
25
25
|
action="store_true",
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
help="Check semantic memories for contradictions",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--crypto",
|
|
30
|
+
action="store_true",
|
|
31
|
+
help="Verify Merkle tree and signatures for commits",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--ref",
|
|
35
|
+
metavar="REF",
|
|
36
|
+
help="Commit or ref to verify (with --crypto); default HEAD",
|
|
28
37
|
)
|
|
29
38
|
parser.add_argument(
|
|
30
39
|
"--llm",
|
|
@@ -32,28 +41,70 @@ class VerifyCommand:
|
|
|
32
41
|
help="Use LLM for triple extraction (requires OpenAI)",
|
|
33
42
|
)
|
|
34
43
|
|
|
44
|
+
@staticmethod
|
|
45
|
+
def _run_crypto_verify(repo, ref: str = None) -> int:
|
|
46
|
+
"""Run cryptographic verification. Returns 0 if all OK, 1 on failure."""
|
|
47
|
+
from ..core.crypto_verify import verify_commit, load_public_key
|
|
48
|
+
|
|
49
|
+
if ref:
|
|
50
|
+
commit_hash = repo.resolve_ref(ref)
|
|
51
|
+
if not commit_hash:
|
|
52
|
+
print(f"Ref not found: {ref}")
|
|
53
|
+
return 1
|
|
54
|
+
else:
|
|
55
|
+
head = repo.refs.get_head()
|
|
56
|
+
if head["type"] == "branch":
|
|
57
|
+
commit_hash = repo.refs.get_branch_commit(head["value"])
|
|
58
|
+
else:
|
|
59
|
+
commit_hash = head.get("value")
|
|
60
|
+
if not commit_hash:
|
|
61
|
+
print("No commit to verify (empty repo).")
|
|
62
|
+
return 0
|
|
63
|
+
pub = load_public_key(repo.mem_dir)
|
|
64
|
+
ok, err = verify_commit(
|
|
65
|
+
repo.object_store, commit_hash, public_key_pem=pub, mem_dir=repo.mem_dir
|
|
66
|
+
)
|
|
67
|
+
if ok:
|
|
68
|
+
print(f"Commit {commit_hash[:8]} verified (Merkle + signature OK).")
|
|
69
|
+
return 0
|
|
70
|
+
print(f"Commit {commit_hash[:8]} verification failed: {err}")
|
|
71
|
+
return 1
|
|
72
|
+
|
|
35
73
|
@staticmethod
|
|
36
74
|
def execute(args) -> int:
|
|
37
75
|
repo, code = require_repo()
|
|
38
76
|
if code != 0:
|
|
39
77
|
return code
|
|
40
78
|
|
|
41
|
-
|
|
42
|
-
|
|
79
|
+
run_consistency = args.consistency
|
|
80
|
+
run_crypto = args.crypto
|
|
81
|
+
if not run_consistency and not run_crypto:
|
|
82
|
+
run_consistency = True
|
|
43
83
|
|
|
44
|
-
|
|
45
|
-
if result.valid:
|
|
46
|
-
print("No contradictions found.")
|
|
47
|
-
return 0
|
|
84
|
+
exit_code = 0
|
|
48
85
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
86
|
+
if run_crypto:
|
|
87
|
+
if VerifyCommand._run_crypto_verify(repo, args.ref) != 0:
|
|
88
|
+
exit_code = 1
|
|
89
|
+
|
|
90
|
+
if run_consistency:
|
|
91
|
+
checker = ConsistencyChecker(repo, llm_provider="openai" if args.llm else None)
|
|
92
|
+
result = checker.check(use_llm=args.llm)
|
|
93
|
+
|
|
94
|
+
print(f"Checked {result.files_checked} semantic file(s)")
|
|
95
|
+
if result.valid:
|
|
96
|
+
print("No contradictions found.")
|
|
97
|
+
else:
|
|
98
|
+
exit_code = 1
|
|
99
|
+
print(f"\nFound {len(result.contradictions)} contradiction(s):")
|
|
100
|
+
for i, c in enumerate(result.contradictions, 1):
|
|
101
|
+
print(f"\n[{i}] {c.reason}")
|
|
102
|
+
print(
|
|
103
|
+
f" {c.triple1.source}:{c.triple1.line}: {c.triple1.subject} {c.triple1.predicate} {c.triple1.obj}"
|
|
104
|
+
)
|
|
105
|
+
print(
|
|
106
|
+
f" {c.triple2.source}:{c.triple2.line}: {c.triple2.subject} {c.triple2.predicate} {c.triple2.obj}"
|
|
107
|
+
)
|
|
108
|
+
print("\nUse 'agmem repair --strategy confidence' to attempt auto-fix.")
|
|
109
|
+
|
|
110
|
+
return exit_code
|