Skip to content

Commit 4f97e61

Browse files
fix: VectorClock thread safety, integrity type hints, audit comment (#243)
- VectorClock: add threading.Lock to prevent race conditions in concurrent tick/merge/copy/eq operations (#160) - integrity.py: add Callable/ModuleType type hints to helper functions (#158) - audit.py: fix stale comment — hashes ARE computed by MerkleAuditChain.add_entry() (#176) Closes #160, closes #158, closes #176 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7a8a200 commit 4f97e61

File tree

3 files changed

+40
-19
lines changed

3 files changed

+40
-19
lines changed

packages/agent-compliance/src/agent_compliance/integrity.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
import json
2727
import logging
2828
import os
29+
from collections.abc import Callable
2930
from dataclasses import dataclass, field
3031
from datetime import datetime, timezone
31-
from typing import Optional
32+
from types import ModuleType
33+
from typing import Any, Optional
3234

3335
logger = logging.getLogger(__name__)
3436

@@ -178,7 +180,7 @@ def _hash_file(path: str) -> str:
178180
return h.hexdigest()
179181

180182

181-
def _hash_function_bytecode(func) -> str:
183+
def _hash_function_bytecode(func: Callable[..., Any]) -> str:
182184
"""SHA-256 hash of a function's compiled bytecode."""
183185
code = func.__code__
184186
h = hashlib.sha256()
@@ -187,10 +189,10 @@ def _hash_function_bytecode(func) -> str:
187189
return h.hexdigest()
188190

189191

190-
def _resolve_function(module, dotted_name: str):
192+
def _resolve_function(module: ModuleType, dotted_name: str) -> Any | None:
191193
"""Resolve 'ClassName.method' or 'function_name' from a module."""
192194
parts = dotted_name.split(".")
193-
obj = module
195+
obj: Any = module
194196
for part in parts:
195197
obj = getattr(obj, part, None)
196198
if obj is None:

packages/agent-hypervisor/src/hypervisor/session/vector_clock.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from __future__ import annotations
1212

13+
import threading
1314
from dataclasses import dataclass, field
1415

1516

@@ -19,23 +20,37 @@ class CausalViolationError(Exception):
1920

2021
@dataclass
2122
class VectorClock:
22-
"""A version counter (community edition: tracking only, no enforcement)."""
23+
"""A version counter (community edition: 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+
"""
2328

2429
clocks: dict[str, int] = field(default_factory=dict)
30+
_lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
2531

2632
def tick(self, agent_did: str) -> None:
2733
"""Increment the clock for an agent."""
28-
self.clocks[agent_did] = self.clocks.get(agent_did, 0) + 1
34+
with self._lock:
35+
self.clocks[agent_did] = self.clocks.get(agent_did, 0) + 1
2936

3037
def get(self, agent_did: str) -> int:
31-
return self.clocks.get(agent_did, 0)
38+
with self._lock:
39+
return self.clocks.get(agent_did, 0)
3240

3341
def merge(self, other: VectorClock) -> VectorClock:
34-
"""Merge two version counters (take component-wise max)."""
35-
merged = VectorClock(clocks=dict(self.clocks))
36-
for agent, clock in other.clocks.items():
37-
merged.clocks[agent] = max(merged.clocks.get(agent, 0), clock)
38-
return merged
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)
3954

4055
def happens_before(self, other: VectorClock) -> bool:
4156
return False
@@ -44,16 +59,20 @@ def is_concurrent(self, other: VectorClock) -> bool:
4459
return False
4560

4661
def copy(self) -> VectorClock:
47-
return VectorClock(clocks=dict(self.clocks))
62+
with self._lock:
63+
return VectorClock(clocks=dict(self.clocks))
4864

4965
def __eq__(self, other: object) -> bool:
5066
if not isinstance(other, VectorClock):
5167
return False
52-
all_agents = set(self.clocks.keys()) | set(other.clocks.keys())
53-
return all(
54-
self.clocks.get(a, 0) == other.clocks.get(a, 0)
55-
for a in all_agents
56-
)
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+
)
5776

5877

5978
class VectorClockManager:

packages/agent-mesh/src/agentmesh/governance/audit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class AuditEntry(BaseModel):
5151
policy_decision: Optional[str] = None
5252
matched_rule: Optional[str] = None
5353

54-
# Chaining — kept for API compatibility but not computed
54+
# Chaining — populated automatically by MerkleAuditChain.add_entry()
5555
previous_hash: str = Field(default="")
5656
entry_hash: str = Field(default="")
5757

0 commit comments

Comments
 (0)