agent_hypervisor 3.1.0__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.
- agent_hypervisor-3.1.0.dist-info/METADATA +824 -0
- agent_hypervisor-3.1.0.dist-info/RECORD +60 -0
- agent_hypervisor-3.1.0.dist-info/WHEEL +4 -0
- agent_hypervisor-3.1.0.dist-info/entry_points.txt +2 -0
- agent_hypervisor-3.1.0.dist-info/licenses/LICENSE +21 -0
- hypervisor/__init__.py +160 -0
- hypervisor/api/__init__.py +7 -0
- hypervisor/api/models.py +285 -0
- hypervisor/api/server.py +742 -0
- hypervisor/audit/__init__.py +4 -0
- hypervisor/audit/commitment.py +76 -0
- hypervisor/audit/delta.py +135 -0
- hypervisor/audit/gc.py +99 -0
- hypervisor/cli/__init__.py +3 -0
- hypervisor/cli/formatters.py +99 -0
- hypervisor/cli/session_commands.py +200 -0
- hypervisor/constants.py +106 -0
- hypervisor/core.py +352 -0
- hypervisor/integrations/__init__.py +10 -0
- hypervisor/integrations/iatp_adapter.py +142 -0
- hypervisor/integrations/nexus_adapter.py +108 -0
- hypervisor/integrations/verification_adapter.py +122 -0
- hypervisor/liability/__init__.py +142 -0
- hypervisor/liability/attribution.py +86 -0
- hypervisor/liability/ledger.py +121 -0
- hypervisor/liability/quarantine.py +119 -0
- hypervisor/liability/slashing.py +80 -0
- hypervisor/liability/vouching.py +134 -0
- hypervisor/models.py +277 -0
- hypervisor/observability/__init__.py +27 -0
- hypervisor/observability/causal_trace.py +70 -0
- hypervisor/observability/event_bus.py +222 -0
- hypervisor/observability/prometheus_collector.py +248 -0
- hypervisor/observability/saga_span_exporter.py +341 -0
- hypervisor/providers.py +121 -0
- hypervisor/py.typed +0 -0
- hypervisor/reversibility/__init__.py +3 -0
- hypervisor/reversibility/registry.py +108 -0
- hypervisor/rings/__init__.py +21 -0
- hypervisor/rings/breach_detector.py +200 -0
- hypervisor/rings/classifier.py +78 -0
- hypervisor/rings/elevation.py +219 -0
- hypervisor/rings/enforcer.py +97 -0
- hypervisor/saga/__init__.py +22 -0
- hypervisor/saga/checkpoint.py +110 -0
- hypervisor/saga/dsl.py +190 -0
- hypervisor/saga/fan_out.py +126 -0
- hypervisor/saga/orchestrator.py +229 -0
- hypervisor/saga/schema.py +244 -0
- hypervisor/saga/state_machine.py +157 -0
- hypervisor/security/__init__.py +13 -0
- hypervisor/security/kill_switch.py +200 -0
- hypervisor/security/rate_limiter.py +190 -0
- hypervisor/session/__init__.py +194 -0
- hypervisor/session/intent_locks.py +118 -0
- hypervisor/session/isolation.py +37 -0
- hypervisor/session/sso.py +169 -0
- hypervisor/session/vector_clock.py +118 -0
- hypervisor/verification/__init__.py +3 -0
- hypervisor/verification/history.py +173 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
# Public Preview — basic implementation
|
|
4
|
+
"""Session-scoped VFS — simple dict-based storage."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import collections
|
|
9
|
+
import hashlib
|
|
10
|
+
import uuid
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class VFSEdit:
|
|
18
|
+
"""A tracked edit to the session VFS."""
|
|
19
|
+
|
|
20
|
+
path: str
|
|
21
|
+
operation: str # "create", "update", "delete", "permission", "restore"
|
|
22
|
+
agent_did: str
|
|
23
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
24
|
+
content_hash: str | None = None
|
|
25
|
+
previous_hash: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class VFSPermissionError(Exception):
|
|
29
|
+
"""Raised when an agent lacks permission to access a VFS path."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SessionVFS:
|
|
33
|
+
"""
|
|
34
|
+
Simple dict-based session storage.
|
|
35
|
+
|
|
36
|
+
Public Preview: basic get/set/delete with a single global lock
|
|
37
|
+
(threading.Lock omitted since Python GIL provides serialization).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, session_id: str, namespace: str | None = None):
|
|
41
|
+
self.session_id = session_id
|
|
42
|
+
self.namespace = namespace or f"/sessions/{session_id}"
|
|
43
|
+
self._files: dict[str, str] = {}
|
|
44
|
+
self._permissions: dict[str, set[str]] = {}
|
|
45
|
+
self._edit_log: collections.deque[VFSEdit] = collections.deque(maxlen=10_000)
|
|
46
|
+
self._snapshots: dict[str, dict[str, Any]] = {}
|
|
47
|
+
|
|
48
|
+
def write(self, path: str, content: str, agent_did: str) -> VFSEdit:
|
|
49
|
+
"""Write a file."""
|
|
50
|
+
full_path = self._resolve(path)
|
|
51
|
+
self._check_permission(full_path, agent_did)
|
|
52
|
+
operation = "update" if full_path in self._files else "create"
|
|
53
|
+
previous_hash = _hash(self._files.get(full_path, "")) if operation == "update" else None
|
|
54
|
+
self._files[full_path] = content
|
|
55
|
+
edit = VFSEdit(
|
|
56
|
+
path=full_path,
|
|
57
|
+
operation=operation,
|
|
58
|
+
agent_did=agent_did,
|
|
59
|
+
content_hash=_hash(content),
|
|
60
|
+
previous_hash=previous_hash,
|
|
61
|
+
)
|
|
62
|
+
self._edit_log.append(edit)
|
|
63
|
+
return edit
|
|
64
|
+
|
|
65
|
+
def read(self, path: str, agent_did: str | None = None) -> str | None:
|
|
66
|
+
"""Read a file."""
|
|
67
|
+
full_path = self._resolve(path)
|
|
68
|
+
if agent_did is not None:
|
|
69
|
+
self._check_permission(full_path, agent_did)
|
|
70
|
+
return self._files.get(full_path)
|
|
71
|
+
|
|
72
|
+
def delete(self, path: str, agent_did: str) -> VFSEdit:
|
|
73
|
+
"""Delete a file."""
|
|
74
|
+
full_path = self._resolve(path)
|
|
75
|
+
if full_path not in self._files:
|
|
76
|
+
raise FileNotFoundError(f"{full_path} not found in session VFS")
|
|
77
|
+
self._check_permission(full_path, agent_did)
|
|
78
|
+
previous_hash = _hash(self._files.pop(full_path))
|
|
79
|
+
self._permissions.pop(full_path, None)
|
|
80
|
+
edit = VFSEdit(
|
|
81
|
+
path=full_path,
|
|
82
|
+
operation="delete",
|
|
83
|
+
agent_did=agent_did,
|
|
84
|
+
previous_hash=previous_hash,
|
|
85
|
+
)
|
|
86
|
+
self._edit_log.append(edit)
|
|
87
|
+
return edit
|
|
88
|
+
|
|
89
|
+
def list_files(self) -> list[str]:
|
|
90
|
+
"""List all files in the session VFS."""
|
|
91
|
+
prefix = self.namespace
|
|
92
|
+
return [p.removeprefix(prefix) for p in self._files if p.startswith(prefix)]
|
|
93
|
+
|
|
94
|
+
def set_permissions(
|
|
95
|
+
self, path: str, allowed_agents: set[str], agent_did: str
|
|
96
|
+
) -> VFSEdit:
|
|
97
|
+
"""Set path-level permissions."""
|
|
98
|
+
full_path = self._resolve(path)
|
|
99
|
+
self._permissions[full_path] = set(allowed_agents)
|
|
100
|
+
edit = VFSEdit(path=full_path, operation="permission", agent_did=agent_did)
|
|
101
|
+
self._edit_log.append(edit)
|
|
102
|
+
return edit
|
|
103
|
+
|
|
104
|
+
def clear_permissions(self, path: str) -> None:
|
|
105
|
+
full_path = self._resolve(path)
|
|
106
|
+
self._permissions.pop(full_path, None)
|
|
107
|
+
|
|
108
|
+
def get_permissions(self, path: str) -> set[str] | None:
|
|
109
|
+
return self._permissions.get(self._resolve(path))
|
|
110
|
+
|
|
111
|
+
def create_snapshot(self, snapshot_id: str | None = None) -> str:
|
|
112
|
+
"""Snapshot current state (simple deep copy)."""
|
|
113
|
+
sid = snapshot_id or f"snap:{uuid.uuid4()}"
|
|
114
|
+
self._snapshots[sid] = {
|
|
115
|
+
"files": dict(self._files),
|
|
116
|
+
"permissions": {k: set(v) for k, v in self._permissions.items()},
|
|
117
|
+
}
|
|
118
|
+
return sid
|
|
119
|
+
|
|
120
|
+
def restore_snapshot(self, snapshot_id: str, agent_did: str) -> None:
|
|
121
|
+
"""Restore VFS to a previous snapshot."""
|
|
122
|
+
if snapshot_id not in self._snapshots:
|
|
123
|
+
raise KeyError(f"Snapshot {snapshot_id} not found")
|
|
124
|
+
snapshot = self._snapshots[snapshot_id]
|
|
125
|
+
self._files = dict(snapshot["files"])
|
|
126
|
+
self._permissions = {k: set(v) for k, v in snapshot["permissions"].items()}
|
|
127
|
+
self._edit_log.append(VFSEdit(
|
|
128
|
+
path=self.namespace, operation="restore", agent_did=agent_did,
|
|
129
|
+
))
|
|
130
|
+
|
|
131
|
+
def list_snapshots(self) -> list[str]:
|
|
132
|
+
return list(self._snapshots.keys())
|
|
133
|
+
|
|
134
|
+
def delete_snapshot(self, snapshot_id: str) -> None:
|
|
135
|
+
if snapshot_id not in self._snapshots:
|
|
136
|
+
raise KeyError(f"Snapshot {snapshot_id} not found")
|
|
137
|
+
del self._snapshots[snapshot_id]
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def edit_log(self) -> list[VFSEdit]:
|
|
141
|
+
return list(self._edit_log)
|
|
142
|
+
|
|
143
|
+
def edits_by_agent(self, agent_did: str) -> list[VFSEdit]:
|
|
144
|
+
return [e for e in self._edit_log if e.agent_did == agent_did]
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def file_count(self) -> int:
|
|
148
|
+
return len(self._files)
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def snapshot_count(self) -> int:
|
|
152
|
+
return len(self._snapshots)
|
|
153
|
+
|
|
154
|
+
def _resolve(self, path: str) -> str:
|
|
155
|
+
if path.startswith(self.namespace):
|
|
156
|
+
return path
|
|
157
|
+
clean = path.lstrip("/")
|
|
158
|
+
return f"{self.namespace}/{clean}"
|
|
159
|
+
|
|
160
|
+
def _check_permission(self, full_path: str, agent_did: str) -> None:
|
|
161
|
+
allowed = self._permissions.get(full_path)
|
|
162
|
+
if allowed is not None and agent_did not in allowed:
|
|
163
|
+
raise VFSPermissionError(
|
|
164
|
+
f"Agent {agent_did} not permitted to access {full_path}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _hash(content: str) -> str:
|
|
169
|
+
return hashlib.sha256(content.encode()).hexdigest()
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
# Public Preview — basic implementation
|
|
4
|
+
"""
|
|
5
|
+
Version Counters — stub implementation.
|
|
6
|
+
|
|
7
|
+
Public Preview: no causal consistency enforcement.
|
|
8
|
+
VectorClock and VectorClockManager are retained for API compatibility.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import threading
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CausalViolationError(Exception):
|
|
18
|
+
"""Raised when a write would violate causal ordering."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class VectorClock:
|
|
23
|
+
"""A version counter (Public Preview: tracking only, no enforcement).
|
|
24
|
+
|
|
25
|
+
Thread-safe: all reads and mutations are guarded by an internal lock
|
|
26
|
+
to prevent data races when multiple agents tick/merge concurrently.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
clocks: dict[str, int] = field(default_factory=dict)
|
|
30
|
+
_lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
|
31
|
+
|
|
32
|
+
def tick(self, agent_did: str) -> None:
|
|
33
|
+
"""Increment the clock for an agent."""
|
|
34
|
+
with self._lock:
|
|
35
|
+
self.clocks[agent_did] = self.clocks.get(agent_did, 0) + 1
|
|
36
|
+
|
|
37
|
+
def get(self, agent_did: str) -> int:
|
|
38
|
+
with self._lock:
|
|
39
|
+
return self.clocks.get(agent_did, 0)
|
|
40
|
+
|
|
41
|
+
def merge(self, other: VectorClock) -> VectorClock:
|
|
42
|
+
"""Merge two version counters (take component-wise max).
|
|
43
|
+
|
|
44
|
+
Acquires locks on both clocks to get consistent snapshots.
|
|
45
|
+
"""
|
|
46
|
+
# Deterministic lock ordering by id() to prevent deadlocks
|
|
47
|
+
first, second = sorted([self, other], key=id)
|
|
48
|
+
with first._lock:
|
|
49
|
+
with second._lock:
|
|
50
|
+
merged_clocks = dict(self.clocks)
|
|
51
|
+
for agent, clock in other.clocks.items():
|
|
52
|
+
merged_clocks[agent] = max(merged_clocks.get(agent, 0), clock)
|
|
53
|
+
return VectorClock(clocks=merged_clocks)
|
|
54
|
+
|
|
55
|
+
def happens_before(self, other: VectorClock) -> bool:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def is_concurrent(self, other: VectorClock) -> bool:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
def copy(self) -> VectorClock:
|
|
62
|
+
with self._lock:
|
|
63
|
+
return VectorClock(clocks=dict(self.clocks))
|
|
64
|
+
|
|
65
|
+
def __eq__(self, other: object) -> bool:
|
|
66
|
+
if not isinstance(other, VectorClock):
|
|
67
|
+
return False
|
|
68
|
+
first, second = sorted([self, other], key=id)
|
|
69
|
+
with first._lock:
|
|
70
|
+
with second._lock:
|
|
71
|
+
all_agents = set(self.clocks.keys()) | set(other.clocks.keys())
|
|
72
|
+
return all(
|
|
73
|
+
self.clocks.get(a, 0) == other.clocks.get(a, 0)
|
|
74
|
+
for a in all_agents
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class VectorClockManager:
|
|
79
|
+
"""
|
|
80
|
+
Version counter stub (Public Preview: no causal enforcement).
|
|
81
|
+
Reads and writes always succeed.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self) -> None:
|
|
85
|
+
self._path_clocks: dict[str, VectorClock] = {}
|
|
86
|
+
self._agent_clocks: dict[str, VectorClock] = {}
|
|
87
|
+
self._conflict_count: int = 0
|
|
88
|
+
|
|
89
|
+
def read(self, path: str, agent_did: str) -> VectorClock:
|
|
90
|
+
"""Record a read (no enforcement)."""
|
|
91
|
+
return self._path_clocks.get(path, VectorClock()).copy()
|
|
92
|
+
|
|
93
|
+
def write(
|
|
94
|
+
self,
|
|
95
|
+
path: str,
|
|
96
|
+
agent_did: str,
|
|
97
|
+
strict: bool = True,
|
|
98
|
+
) -> VectorClock:
|
|
99
|
+
"""Record a write (Public Preview: never rejects)."""
|
|
100
|
+
agent_clock = self._agent_clocks.get(agent_did, VectorClock())
|
|
101
|
+
agent_clock.tick(agent_did)
|
|
102
|
+
self._path_clocks[path] = agent_clock.copy()
|
|
103
|
+
self._agent_clocks[agent_did] = agent_clock
|
|
104
|
+
return self._path_clocks[path]
|
|
105
|
+
|
|
106
|
+
def get_path_clock(self, path: str) -> VectorClock:
|
|
107
|
+
return self._path_clocks.get(path, VectorClock()).copy()
|
|
108
|
+
|
|
109
|
+
def get_agent_clock(self, agent_did: str) -> VectorClock:
|
|
110
|
+
return self._agent_clocks.get(agent_did, VectorClock()).copy()
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def conflict_count(self) -> int:
|
|
114
|
+
return self._conflict_count
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def tracked_paths(self) -> int:
|
|
118
|
+
return len(self._path_clocks)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
DID Transaction History Verification
|
|
5
|
+
|
|
6
|
+
Verifies an agent's declared behavioral history by checking Summary Hash
|
|
7
|
+
consistency (duplicate hashes, temporal ordering, hash validity).
|
|
8
|
+
|
|
9
|
+
NOTE: This verifier checks the *integrity* of history declared by the
|
|
10
|
+
agent during the IATP handshake. It does NOT resolve DIDs from an
|
|
11
|
+
external DID registry or blockchain. A malicious agent that fabricates
|
|
12
|
+
a self-consistent history will pass verification. External DID
|
|
13
|
+
resolution is planned for a future release.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from datetime import UTC, datetime
|
|
20
|
+
from enum import Enum
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class VerificationStatus(str, Enum):
|
|
24
|
+
"""Result of transaction history verification."""
|
|
25
|
+
|
|
26
|
+
VERIFIED = "verified"
|
|
27
|
+
PROBATIONARY = "probationary" # new DID, limited history
|
|
28
|
+
SUSPICIOUS = "suspicious" # inconsistent hashes
|
|
29
|
+
UNREACHABLE = "unreachable" # couldn't fetch history
|
|
30
|
+
UNKNOWN = "unknown"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class TransactionRecord:
|
|
35
|
+
"""A historical transaction record from a DID."""
|
|
36
|
+
|
|
37
|
+
session_id: str
|
|
38
|
+
summary_hash: str
|
|
39
|
+
timestamp: datetime
|
|
40
|
+
participant_count: int = 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class VerificationResult:
|
|
45
|
+
"""Result of verifying an agent's transaction history."""
|
|
46
|
+
|
|
47
|
+
agent_did: str
|
|
48
|
+
status: VerificationStatus
|
|
49
|
+
transactions_checked: int
|
|
50
|
+
transactions_found: int
|
|
51
|
+
inconsistencies: list[str] = field(default_factory=list)
|
|
52
|
+
verified_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
53
|
+
cached: bool = False
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_trustworthy(self) -> bool:
|
|
57
|
+
return self.status in (VerificationStatus.VERIFIED, VerificationStatus.PROBATIONARY)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TransactionHistoryVerifier:
|
|
61
|
+
"""
|
|
62
|
+
Verifies agent transaction history integrity.
|
|
63
|
+
|
|
64
|
+
During handshake, checks the last N declared transaction Summary Hashes
|
|
65
|
+
for internal consistency (duplicates, ordering, validity).
|
|
66
|
+
|
|
67
|
+
**Limitations:**
|
|
68
|
+
- Only validates *declared* history — does not fetch from DID registries
|
|
69
|
+
- A fabricated but self-consistent history will pass
|
|
70
|
+
- Results are cached in-memory (no persistent cache)
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
REQUIRED_HISTORY_DEPTH = 5
|
|
74
|
+
|
|
75
|
+
def __init__(self) -> None:
|
|
76
|
+
self._cache: dict[str, VerificationResult] = {}
|
|
77
|
+
|
|
78
|
+
def verify(
|
|
79
|
+
self,
|
|
80
|
+
agent_did: str,
|
|
81
|
+
declared_history: list[TransactionRecord] | None = None,
|
|
82
|
+
) -> VerificationResult:
|
|
83
|
+
"""
|
|
84
|
+
Verify an agent's transaction history.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
agent_did: The agent's DID to verify
|
|
88
|
+
declared_history: History declared by the agent (to cross-check)
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
VerificationResult with status and details
|
|
92
|
+
"""
|
|
93
|
+
# Check cache first
|
|
94
|
+
if agent_did in self._cache:
|
|
95
|
+
cached = self._cache[agent_did]
|
|
96
|
+
cached.cached = True
|
|
97
|
+
return cached
|
|
98
|
+
|
|
99
|
+
if declared_history is None or len(declared_history) == 0:
|
|
100
|
+
# No history — treat as new/probationary
|
|
101
|
+
result = VerificationResult(
|
|
102
|
+
agent_did=agent_did,
|
|
103
|
+
status=VerificationStatus.PROBATIONARY,
|
|
104
|
+
transactions_checked=0,
|
|
105
|
+
transactions_found=0,
|
|
106
|
+
inconsistencies=["No transaction history available"],
|
|
107
|
+
)
|
|
108
|
+
elif len(declared_history) < self.REQUIRED_HISTORY_DEPTH:
|
|
109
|
+
# Insufficient history
|
|
110
|
+
result = VerificationResult(
|
|
111
|
+
agent_did=agent_did,
|
|
112
|
+
status=VerificationStatus.PROBATIONARY,
|
|
113
|
+
transactions_checked=len(declared_history),
|
|
114
|
+
transactions_found=len(declared_history),
|
|
115
|
+
inconsistencies=[
|
|
116
|
+
f"Only {len(declared_history)} transactions "
|
|
117
|
+
f"(need {self.REQUIRED_HISTORY_DEPTH})"
|
|
118
|
+
],
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
# Validate hash consistency
|
|
122
|
+
inconsistencies = self._check_consistency(declared_history)
|
|
123
|
+
status = (
|
|
124
|
+
VerificationStatus.SUSPICIOUS
|
|
125
|
+
if inconsistencies
|
|
126
|
+
else VerificationStatus.VERIFIED
|
|
127
|
+
)
|
|
128
|
+
result = VerificationResult(
|
|
129
|
+
agent_did=agent_did,
|
|
130
|
+
status=status,
|
|
131
|
+
transactions_checked=len(declared_history),
|
|
132
|
+
transactions_found=len(declared_history),
|
|
133
|
+
inconsistencies=inconsistencies,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
self._cache[agent_did] = result
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
def clear_cache(self, agent_did: str | None = None) -> None:
|
|
140
|
+
"""Clear verification cache."""
|
|
141
|
+
if agent_did:
|
|
142
|
+
self._cache.pop(agent_did, None)
|
|
143
|
+
else:
|
|
144
|
+
self._cache.clear()
|
|
145
|
+
|
|
146
|
+
def _check_consistency(self, history: list[TransactionRecord]) -> list[str]:
|
|
147
|
+
"""Check transaction history for inconsistencies."""
|
|
148
|
+
issues: list[str] = []
|
|
149
|
+
|
|
150
|
+
# Check for duplicate hashes (different sessions, same hash = suspicious)
|
|
151
|
+
seen_hashes: dict[str, str] = {}
|
|
152
|
+
for tx in history:
|
|
153
|
+
if tx.summary_hash in seen_hashes:
|
|
154
|
+
issues.append(
|
|
155
|
+
f"Duplicate hash in sessions {seen_hashes[tx.summary_hash]} "
|
|
156
|
+
f"and {tx.session_id}"
|
|
157
|
+
)
|
|
158
|
+
seen_hashes[tx.summary_hash] = tx.session_id
|
|
159
|
+
|
|
160
|
+
# Check for temporal ordering
|
|
161
|
+
for i in range(1, len(history)):
|
|
162
|
+
if history[i].timestamp < history[i - 1].timestamp:
|
|
163
|
+
issues.append(
|
|
164
|
+
f"Non-monotonic timestamps: {history[i].session_id} "
|
|
165
|
+
f"predates {history[i-1].session_id}"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Check for empty hashes
|
|
169
|
+
for tx in history:
|
|
170
|
+
if not tx.summary_hash or len(tx.summary_hash) < 16:
|
|
171
|
+
issues.append(f"Invalid hash in session {tx.session_id}")
|
|
172
|
+
|
|
173
|
+
return issues
|