dualtrust 0.3.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.
- dualtrust/__init__.py +119 -0
- dualtrust/a2a.py +455 -0
- dualtrust/adapters/__init__.py +13 -0
- dualtrust/adapters/crewai.py +76 -0
- dualtrust/adapters/langchain.py +118 -0
- dualtrust/adapters/mcp.py +419 -0
- dualtrust/adapters/openai.py +132 -0
- dualtrust/crypto_ledger.py +264 -0
- dualtrust/engine.py +1711 -0
- dualtrust/exceptions.py +92 -0
- dualtrust/math_engine.py +198 -0
- dualtrust/models.py +378 -0
- dualtrust/storage.py +825 -0
- dualtrust/watchdog.py +234 -0
- dualtrust-0.3.0.dist-info/METADATA +486 -0
- dualtrust-0.3.0.dist-info/RECORD +18 -0
- dualtrust-0.3.0.dist-info/WHEEL +4 -0
- dualtrust-0.3.0.dist-info/licenses/LICENSE +21 -0
dualtrust/__init__.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""DualTrust: Human Social Credit and AI Agent Reputation Engine with Rogue Quarantine.
|
|
2
|
+
|
|
3
|
+
Empowering multi-agent systems and ambient computing with dual-trust authorization,
|
|
4
|
+
asymmetric exponential slashing, and rogue agent containment.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dualtrust import adapters
|
|
8
|
+
from dualtrust.a2a import MandateManager
|
|
9
|
+
from dualtrust.adapters.mcp import DualTrustMCPGateway
|
|
10
|
+
from dualtrust.crypto_ledger import AuditBlock, CryptographicLedger, MerkleTree
|
|
11
|
+
from dualtrust.engine import DualTrust
|
|
12
|
+
from dualtrust.exceptions import (
|
|
13
|
+
AgentQuarantinedError,
|
|
14
|
+
AnomalyDetectedError,
|
|
15
|
+
AuthorizationDeniedError,
|
|
16
|
+
ChallengeVerificationError,
|
|
17
|
+
DelegationError,
|
|
18
|
+
DualTrustError,
|
|
19
|
+
EntityNotFoundError,
|
|
20
|
+
HandshakeFailedError,
|
|
21
|
+
HumanApprovalRequiredError,
|
|
22
|
+
InsufficientAgentScoreError,
|
|
23
|
+
InsufficientCreditsError,
|
|
24
|
+
InsufficientHumanScoreError,
|
|
25
|
+
MandateBudgetExceededError,
|
|
26
|
+
MandateDepthExceededError,
|
|
27
|
+
MandateExpiredError,
|
|
28
|
+
MandateRevokedError,
|
|
29
|
+
MandateScopeViolationError,
|
|
30
|
+
ProtocolTrespassError,
|
|
31
|
+
VelocityLimitExceededError,
|
|
32
|
+
)
|
|
33
|
+
from dualtrust.math_engine import ReputationMath
|
|
34
|
+
from dualtrust.models import (
|
|
35
|
+
AgentProfile,
|
|
36
|
+
AgentStanding,
|
|
37
|
+
AgentStatus,
|
|
38
|
+
AgentTelemetry,
|
|
39
|
+
AnomalyType,
|
|
40
|
+
ChallengeStatus,
|
|
41
|
+
DelegationMandate,
|
|
42
|
+
EpisodicReflection,
|
|
43
|
+
HandshakeResult,
|
|
44
|
+
HumanApprovalChallenge,
|
|
45
|
+
HumanProfile,
|
|
46
|
+
HumanTier,
|
|
47
|
+
MandateStatus,
|
|
48
|
+
PreflightVerdict,
|
|
49
|
+
RiskLevel,
|
|
50
|
+
|
|
51
|
+
TrustVerdict,
|
|
52
|
+
ViolationEvent,
|
|
53
|
+
ViolationType,
|
|
54
|
+
WatchdogVerdict,
|
|
55
|
+
)
|
|
56
|
+
from dualtrust.storage import MemoryStorage, SQLiteStorage, StorageBackend
|
|
57
|
+
from dualtrust.watchdog import AgentWatchdog
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
__version__ = "0.3.0"
|
|
61
|
+
__all__ = [
|
|
62
|
+
# Core Engine & Cryptographic Ledger
|
|
63
|
+
"DualTrust",
|
|
64
|
+
"DualTrustMCPGateway",
|
|
65
|
+
"MandateManager",
|
|
66
|
+
"AgentWatchdog",
|
|
67
|
+
"CryptographicLedger",
|
|
68
|
+
"AuditBlock",
|
|
69
|
+
"MerkleTree",
|
|
70
|
+
"adapters",
|
|
71
|
+
# Math Engine
|
|
72
|
+
"ReputationMath",
|
|
73
|
+
# Models & Enums
|
|
74
|
+
"AgentProfile",
|
|
75
|
+
"AgentStanding",
|
|
76
|
+
"AgentStatus",
|
|
77
|
+
"AgentTelemetry",
|
|
78
|
+
"AnomalyType",
|
|
79
|
+
"ChallengeStatus",
|
|
80
|
+
"DelegationMandate",
|
|
81
|
+
"EpisodicReflection",
|
|
82
|
+
"HandshakeResult",
|
|
83
|
+
"HumanApprovalChallenge",
|
|
84
|
+
|
|
85
|
+
"HumanProfile",
|
|
86
|
+
"HumanTier",
|
|
87
|
+
"MandateStatus",
|
|
88
|
+
"PreflightVerdict",
|
|
89
|
+
"RiskLevel",
|
|
90
|
+
"TrustVerdict",
|
|
91
|
+
"ViolationEvent",
|
|
92
|
+
"ViolationType",
|
|
93
|
+
"WatchdogVerdict",
|
|
94
|
+
# Storage
|
|
95
|
+
"StorageBackend",
|
|
96
|
+
"MemoryStorage",
|
|
97
|
+
"SQLiteStorage",
|
|
98
|
+
# Exceptions
|
|
99
|
+
"DualTrustError",
|
|
100
|
+
"EntityNotFoundError",
|
|
101
|
+
"AuthorizationDeniedError",
|
|
102
|
+
"AgentQuarantinedError",
|
|
103
|
+
"HumanApprovalRequiredError",
|
|
104
|
+
"ChallengeVerificationError",
|
|
105
|
+
"InsufficientHumanScoreError",
|
|
106
|
+
"InsufficientAgentScoreError",
|
|
107
|
+
"InsufficientCreditsError",
|
|
108
|
+
"ProtocolTrespassError",
|
|
109
|
+
"VelocityLimitExceededError",
|
|
110
|
+
"AnomalyDetectedError",
|
|
111
|
+
"DelegationError",
|
|
112
|
+
"MandateExpiredError",
|
|
113
|
+
"MandateScopeViolationError",
|
|
114
|
+
"MandateBudgetExceededError",
|
|
115
|
+
"MandateDepthExceededError",
|
|
116
|
+
"MandateRevokedError",
|
|
117
|
+
"HandshakeFailedError",
|
|
118
|
+
]
|
|
119
|
+
|
dualtrust/a2a.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""Agent-to-Agent (A2A) Governance, Delegation Mandates, and Attestation Engine.
|
|
2
|
+
|
|
3
|
+
Implements zero-dependency cryptographic delegation mandates for multi-agent swarms,
|
|
4
|
+
conforming to the Principle of Least Privilege (PoLP) and Macaroons capability-based
|
|
5
|
+
access control models with strict hierarchical budget reservation and cascading revocation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import replace
|
|
9
|
+
import hashlib
|
|
10
|
+
import hmac
|
|
11
|
+
import secrets
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
15
|
+
|
|
16
|
+
from .exceptions import (
|
|
17
|
+
DelegationError,
|
|
18
|
+
MandateBudgetExceededError,
|
|
19
|
+
MandateDepthExceededError,
|
|
20
|
+
MandateExpiredError,
|
|
21
|
+
MandateRevokedError,
|
|
22
|
+
MandateScopeViolationError,
|
|
23
|
+
)
|
|
24
|
+
from .models import DelegationMandate, MandateStatus
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MandateManager:
|
|
28
|
+
"""Manages creation, cryptographic signing, tamper verification, and lifecycle of A2A delegation mandates."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, secret_key: Optional[str] = None, storage: Optional[Any] = None) -> None:
|
|
31
|
+
"""Initialize the MandateManager with an HMAC signing secret and optional persistent storage."""
|
|
32
|
+
self._secret_key: bytes = (secret_key or secrets.token_hex(32)).encode("utf-8")
|
|
33
|
+
self._mandates: Dict[str, DelegationMandate] = {}
|
|
34
|
+
self._storage = storage
|
|
35
|
+
self._lock = threading.RLock()
|
|
36
|
+
if self._storage is not None and hasattr(self._storage, "get_all_mandates"):
|
|
37
|
+
for m in self._storage.get_all_mandates():
|
|
38
|
+
self._mandates[m.mandate_id] = m
|
|
39
|
+
|
|
40
|
+
def _record_mandate(self, mandate: DelegationMandate) -> None:
|
|
41
|
+
"""Atomically update memory registry and persistent storage if configured."""
|
|
42
|
+
self._mandates[mandate.mandate_id] = mandate
|
|
43
|
+
if self._storage is not None and hasattr(self._storage, "save_mandate"):
|
|
44
|
+
self._storage.save_mandate(mandate)
|
|
45
|
+
|
|
46
|
+
def _canonical_representation(
|
|
47
|
+
self,
|
|
48
|
+
mandate_id: str,
|
|
49
|
+
parent_mandate_id: Optional[str],
|
|
50
|
+
parent_agent_id: str,
|
|
51
|
+
sub_agent_id: str,
|
|
52
|
+
user_id: str,
|
|
53
|
+
allowed_actions: List[str],
|
|
54
|
+
max_credits: int,
|
|
55
|
+
max_depth: int,
|
|
56
|
+
current_depth: int,
|
|
57
|
+
created_at: float,
|
|
58
|
+
expires_at: float,
|
|
59
|
+
) -> str:
|
|
60
|
+
"""Construct the deterministic canonical string for cryptographic attestation."""
|
|
61
|
+
sorted_actions = ",".join(sorted(allowed_actions))
|
|
62
|
+
parent_ref = parent_mandate_id or "ROOT"
|
|
63
|
+
return (
|
|
64
|
+
f"{mandate_id}:{parent_ref}:{parent_agent_id}:{sub_agent_id}:{user_id}:"
|
|
65
|
+
f"{sorted_actions}:{max_credits}:{max_depth}:{current_depth}:"
|
|
66
|
+
f"{created_at:.4f}:{expires_at:.4f}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _sign_payload(self, canonical_str: str) -> str:
|
|
70
|
+
"""Generate a deterministic HMAC-SHA256 signature for the canonical representation."""
|
|
71
|
+
return hmac.new(self._secret_key, canonical_str.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
72
|
+
|
|
73
|
+
def issue_mandate(
|
|
74
|
+
self,
|
|
75
|
+
parent_agent_id: str,
|
|
76
|
+
sub_agent_id: str,
|
|
77
|
+
user_id: str,
|
|
78
|
+
allowed_actions: List[str],
|
|
79
|
+
max_credits: int,
|
|
80
|
+
ttl_seconds: float = 300.0,
|
|
81
|
+
max_depth: int = 1,
|
|
82
|
+
current_depth: int = 1,
|
|
83
|
+
parent_mandate_id: Optional[str] = None,
|
|
84
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
85
|
+
) -> DelegationMandate:
|
|
86
|
+
"""Issue a cryptographically sealed delegation mandate authorizing a sub-agent to act on behalf of a parent."""
|
|
87
|
+
if not parent_agent_id or not sub_agent_id:
|
|
88
|
+
raise DelegationError("Both parent_agent_id and sub_agent_id must be non-empty strings.")
|
|
89
|
+
if parent_agent_id == sub_agent_id:
|
|
90
|
+
raise DelegationError("An agent cannot delegate an A2A mandate to itself.")
|
|
91
|
+
if not user_id:
|
|
92
|
+
raise DelegationError("Mandate user_id must be non-empty.")
|
|
93
|
+
if max_credits <= 0:
|
|
94
|
+
raise DelegationError(f"max_credits must be strictly positive, got {max_credits}.")
|
|
95
|
+
if ttl_seconds <= 0:
|
|
96
|
+
raise DelegationError(f"ttl_seconds must be strictly positive, got {ttl_seconds}.")
|
|
97
|
+
if max_depth < 1:
|
|
98
|
+
raise DelegationError(f"max_depth must be at least 1, got {max_depth}.")
|
|
99
|
+
if current_depth > max_depth:
|
|
100
|
+
raise MandateDepthExceededError(
|
|
101
|
+
f"Cannot issue mandate: current_depth ({current_depth}) exceeds max_depth ({max_depth})."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Enforce explicit scope: Empty list or None is strictly rejected (prevent silent wildcard escalation)
|
|
105
|
+
if not allowed_actions:
|
|
106
|
+
raise DelegationError(
|
|
107
|
+
"allowed_actions cannot be empty or None; specify explicit actions or ['*'] for full scope."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
cleaned_actions = sorted(list(set(allowed_actions)))
|
|
111
|
+
|
|
112
|
+
now = time.time()
|
|
113
|
+
expires_at = now + ttl_seconds
|
|
114
|
+
mandate_id = secrets.token_hex(16)
|
|
115
|
+
|
|
116
|
+
canonical = self._canonical_representation(
|
|
117
|
+
mandate_id=mandate_id,
|
|
118
|
+
parent_mandate_id=parent_mandate_id,
|
|
119
|
+
parent_agent_id=parent_agent_id,
|
|
120
|
+
sub_agent_id=sub_agent_id,
|
|
121
|
+
user_id=user_id,
|
|
122
|
+
allowed_actions=cleaned_actions,
|
|
123
|
+
max_credits=max_credits,
|
|
124
|
+
max_depth=max_depth,
|
|
125
|
+
current_depth=current_depth,
|
|
126
|
+
created_at=now,
|
|
127
|
+
expires_at=expires_at,
|
|
128
|
+
)
|
|
129
|
+
signature = self._sign_payload(canonical)
|
|
130
|
+
|
|
131
|
+
mandate = DelegationMandate(
|
|
132
|
+
mandate_id=mandate_id,
|
|
133
|
+
parent_agent_id=parent_agent_id,
|
|
134
|
+
sub_agent_id=sub_agent_id,
|
|
135
|
+
user_id=user_id,
|
|
136
|
+
allowed_actions=cleaned_actions,
|
|
137
|
+
max_credits=max_credits,
|
|
138
|
+
credits_used=0,
|
|
139
|
+
reserved_child_credits=0,
|
|
140
|
+
parent_mandate_id=parent_mandate_id,
|
|
141
|
+
max_depth=max_depth,
|
|
142
|
+
current_depth=current_depth,
|
|
143
|
+
created_at=now,
|
|
144
|
+
expires_at=expires_at,
|
|
145
|
+
status=MandateStatus.ACTIVE,
|
|
146
|
+
metadata=dict(metadata or {}),
|
|
147
|
+
signature=signature,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
with self._lock:
|
|
151
|
+
self._record_mandate(mandate)
|
|
152
|
+
|
|
153
|
+
return replace(mandate)
|
|
154
|
+
|
|
155
|
+
def verify_mandate(
|
|
156
|
+
self,
|
|
157
|
+
mandate: DelegationMandate,
|
|
158
|
+
action: Optional[str] = None,
|
|
159
|
+
cost_credits: int = 0,
|
|
160
|
+
current_time: Optional[float] = None,
|
|
161
|
+
) -> Tuple[bool, str]:
|
|
162
|
+
"""Perform comprehensive cryptographic and policy verification on a delegation mandate."""
|
|
163
|
+
now = current_time if current_time is not None else time.time()
|
|
164
|
+
|
|
165
|
+
# 1. Cryptographic HMAC tamper check
|
|
166
|
+
canonical = self._canonical_representation(
|
|
167
|
+
mandate_id=mandate.mandate_id,
|
|
168
|
+
parent_mandate_id=mandate.parent_mandate_id,
|
|
169
|
+
parent_agent_id=mandate.parent_agent_id,
|
|
170
|
+
sub_agent_id=mandate.sub_agent_id,
|
|
171
|
+
user_id=mandate.user_id,
|
|
172
|
+
allowed_actions=mandate.allowed_actions,
|
|
173
|
+
max_credits=mandate.max_credits,
|
|
174
|
+
max_depth=mandate.max_depth,
|
|
175
|
+
current_depth=mandate.current_depth,
|
|
176
|
+
created_at=mandate.created_at,
|
|
177
|
+
expires_at=mandate.expires_at,
|
|
178
|
+
)
|
|
179
|
+
expected_sig = self._sign_payload(canonical)
|
|
180
|
+
if not hmac.compare_digest(mandate.signature, expected_sig):
|
|
181
|
+
return False, "Cryptographic signature mismatch: mandate has been forged or altered."
|
|
182
|
+
|
|
183
|
+
with self._lock:
|
|
184
|
+
stored_mandate = self._mandates.get(mandate.mandate_id)
|
|
185
|
+
if not stored_mandate:
|
|
186
|
+
return False, f"Mandate '{mandate.mandate_id}' not found in authoritative registry."
|
|
187
|
+
current_status = stored_mandate.status
|
|
188
|
+
current_used = stored_mandate.credits_used
|
|
189
|
+
|
|
190
|
+
# 2. Check Ancestor Chain (Cascading Revocation & Hierarchical Validity)
|
|
191
|
+
curr_parent_id = stored_mandate.parent_mandate_id
|
|
192
|
+
while curr_parent_id:
|
|
193
|
+
ancestor = self._mandates.get(curr_parent_id)
|
|
194
|
+
if not ancestor:
|
|
195
|
+
return False, f"Ancestor mandate '{curr_parent_id}' not found in registry."
|
|
196
|
+
if ancestor.status == MandateStatus.REVOKED:
|
|
197
|
+
return False, f"Parent mandate '{ancestor.mandate_id}' in delegation chain has been revoked."
|
|
198
|
+
if ancestor.is_expired(now):
|
|
199
|
+
return False, f"Parent mandate '{ancestor.mandate_id}' in delegation chain has expired."
|
|
200
|
+
if cost_credits > 0 and (ancestor.credits_used + cost_credits) > ancestor.max_credits:
|
|
201
|
+
return False, f"Ancestor mandate '{ancestor.mandate_id}' credit budget exhausted."
|
|
202
|
+
curr_parent_id = ancestor.parent_mandate_id
|
|
203
|
+
|
|
204
|
+
# 3. Status verification
|
|
205
|
+
if current_status == MandateStatus.REVOKED:
|
|
206
|
+
return False, "Mandate has been revoked by the parent agent or human."
|
|
207
|
+
if current_status == MandateStatus.EXHAUSTED:
|
|
208
|
+
return False, "Mandate credit budget is exhausted."
|
|
209
|
+
|
|
210
|
+
# 4. Expiration check
|
|
211
|
+
if mandate.is_expired(now):
|
|
212
|
+
return False, f"Mandate token expired at {mandate.expires_at:.2f} (current: {now:.2f})."
|
|
213
|
+
|
|
214
|
+
# 5. Delegation depth check
|
|
215
|
+
if mandate.current_depth > mandate.max_depth:
|
|
216
|
+
return False, f"Mandate delegation depth ({mandate.current_depth}) exceeds maximum ({mandate.max_depth})."
|
|
217
|
+
|
|
218
|
+
# 6. Action scope check
|
|
219
|
+
if action is not None and not mandate.can_perform(action):
|
|
220
|
+
allowed_str = ", ".join(mandate.allowed_actions)
|
|
221
|
+
return False, f"Action '{action}' is not permitted by mandate scope [{allowed_str}]."
|
|
222
|
+
|
|
223
|
+
# 7. Credit budget check
|
|
224
|
+
if cost_credits > 0:
|
|
225
|
+
effective_reserved = stored_mandate.reserved_child_credits
|
|
226
|
+
if (current_used + cost_credits + effective_reserved) > mandate.max_credits:
|
|
227
|
+
available = max(0, mandate.max_credits - current_used - effective_reserved)
|
|
228
|
+
return False, (
|
|
229
|
+
f"Insufficient mandate credit budget: requested {cost_credits}, "
|
|
230
|
+
f"available {available} ({effective_reserved} reserved for sub-mandates)."
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return True, "Mandate verified and authorized."
|
|
234
|
+
|
|
235
|
+
def validate_and_consume(
|
|
236
|
+
self,
|
|
237
|
+
mandate_id: str,
|
|
238
|
+
action: str,
|
|
239
|
+
cost_credits: int = 0,
|
|
240
|
+
current_time: Optional[float] = None,
|
|
241
|
+
) -> DelegationMandate:
|
|
242
|
+
"""Validate mandate policies and atomically deduct the action cost from child and all ancestor budgets."""
|
|
243
|
+
with self._lock:
|
|
244
|
+
mandate = self._mandates.get(mandate_id)
|
|
245
|
+
if not mandate:
|
|
246
|
+
raise DelegationError(f"Mandate '{mandate_id}' not found in registry.")
|
|
247
|
+
|
|
248
|
+
now = current_time if current_time is not None else time.time()
|
|
249
|
+
|
|
250
|
+
# Check validity
|
|
251
|
+
is_valid, reason = self.verify_mandate(
|
|
252
|
+
mandate, action=action, cost_credits=cost_credits, current_time=now
|
|
253
|
+
)
|
|
254
|
+
if not is_valid:
|
|
255
|
+
if "signature mismatch" in reason:
|
|
256
|
+
raise DelegationError(reason)
|
|
257
|
+
if "revoked" in reason:
|
|
258
|
+
raise MandateRevokedError(reason)
|
|
259
|
+
if "expired" in reason:
|
|
260
|
+
raise MandateExpiredError(reason)
|
|
261
|
+
if "depth" in reason:
|
|
262
|
+
raise MandateDepthExceededError(reason)
|
|
263
|
+
if "not permitted by mandate scope" in reason:
|
|
264
|
+
raise MandateScopeViolationError(reason)
|
|
265
|
+
if "Insufficient mandate credit budget" in reason or "exhausted" in reason:
|
|
266
|
+
raise MandateBudgetExceededError(reason)
|
|
267
|
+
raise DelegationError(reason)
|
|
268
|
+
|
|
269
|
+
# Consume budget in child mandate
|
|
270
|
+
new_used = mandate.credits_used + cost_credits
|
|
271
|
+
new_status = MandateStatus.EXHAUSTED if new_used >= mandate.max_credits else MandateStatus.ACTIVE
|
|
272
|
+
updated_mandate = replace(mandate, credits_used=new_used, status=new_status)
|
|
273
|
+
self._record_mandate(updated_mandate)
|
|
274
|
+
|
|
275
|
+
# Hierarchical bubble-up: Deduct from all ancestors to prevent budget oversubscription
|
|
276
|
+
curr_parent_id = mandate.parent_mandate_id
|
|
277
|
+
while curr_parent_id:
|
|
278
|
+
ancestor = self._mandates.get(curr_parent_id)
|
|
279
|
+
if ancestor:
|
|
280
|
+
anc_used = ancestor.credits_used + cost_credits
|
|
281
|
+
anc_reserved = max(0, ancestor.reserved_child_credits - cost_credits)
|
|
282
|
+
anc_status = MandateStatus.EXHAUSTED if anc_used >= ancestor.max_credits else MandateStatus.ACTIVE
|
|
283
|
+
self._record_mandate(
|
|
284
|
+
replace(
|
|
285
|
+
ancestor,
|
|
286
|
+
credits_used=anc_used,
|
|
287
|
+
reserved_child_credits=anc_reserved,
|
|
288
|
+
status=anc_status,
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
curr_parent_id = ancestor.parent_mandate_id
|
|
292
|
+
else:
|
|
293
|
+
break
|
|
294
|
+
|
|
295
|
+
return replace(updated_mandate)
|
|
296
|
+
|
|
297
|
+
def rollback_consumption(self, mandate_id: str, cost_credits: int) -> DelegationMandate:
|
|
298
|
+
"""Rollback credit consumption on child and all ancestors (compensating transaction)."""
|
|
299
|
+
with self._lock:
|
|
300
|
+
mandate = self._mandates.get(mandate_id)
|
|
301
|
+
if not mandate:
|
|
302
|
+
raise DelegationError(f"Mandate '{mandate_id}' not found in registry.")
|
|
303
|
+
|
|
304
|
+
new_used = max(0, mandate.credits_used - cost_credits)
|
|
305
|
+
new_status = MandateStatus.ACTIVE if mandate.status == MandateStatus.EXHAUSTED else mandate.status
|
|
306
|
+
updated_mandate = replace(mandate, credits_used=new_used, status=new_status)
|
|
307
|
+
self._record_mandate(updated_mandate)
|
|
308
|
+
|
|
309
|
+
# Hierarchical rollback to all ancestors
|
|
310
|
+
curr_parent_id = mandate.parent_mandate_id
|
|
311
|
+
while curr_parent_id:
|
|
312
|
+
ancestor = self._mandates.get(curr_parent_id)
|
|
313
|
+
if ancestor:
|
|
314
|
+
anc_used = max(0, ancestor.credits_used - cost_credits)
|
|
315
|
+
anc_reserved = ancestor.reserved_child_credits + cost_credits
|
|
316
|
+
anc_status = MandateStatus.ACTIVE if ancestor.status == MandateStatus.EXHAUSTED else ancestor.status
|
|
317
|
+
self._record_mandate(
|
|
318
|
+
replace(
|
|
319
|
+
ancestor,
|
|
320
|
+
credits_used=anc_used,
|
|
321
|
+
reserved_child_credits=anc_reserved,
|
|
322
|
+
status=anc_status,
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
curr_parent_id = ancestor.parent_mandate_id
|
|
326
|
+
else:
|
|
327
|
+
break
|
|
328
|
+
|
|
329
|
+
return replace(updated_mandate)
|
|
330
|
+
|
|
331
|
+
def derive_sub_mandate(
|
|
332
|
+
self,
|
|
333
|
+
parent_mandate_id: str,
|
|
334
|
+
new_sub_agent_id: str,
|
|
335
|
+
allowed_actions: Optional[List[str]] = None,
|
|
336
|
+
max_credits: Optional[int] = None,
|
|
337
|
+
ttl_seconds: Optional[float] = None,
|
|
338
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
339
|
+
) -> DelegationMandate:
|
|
340
|
+
"""Derive an attenuated sub-mandate enforcing hierarchical budget reservation and sibling anti-amplification."""
|
|
341
|
+
with self._lock:
|
|
342
|
+
parent = self._mandates.get(parent_mandate_id)
|
|
343
|
+
if not parent:
|
|
344
|
+
raise DelegationError(f"Parent mandate '{parent_mandate_id}' not found.")
|
|
345
|
+
|
|
346
|
+
# Check status and expiration of parent
|
|
347
|
+
now = time.time()
|
|
348
|
+
if parent.is_expired(now):
|
|
349
|
+
raise MandateExpiredError("Cannot derive sub-mandate: parent mandate is expired.")
|
|
350
|
+
if parent.status != MandateStatus.ACTIVE:
|
|
351
|
+
raise DelegationError(f"Cannot derive sub-mandate: parent mandate status is {parent.status.value}.")
|
|
352
|
+
|
|
353
|
+
# Depth recursion check
|
|
354
|
+
new_depth = parent.current_depth + 1
|
|
355
|
+
if new_depth > parent.max_depth:
|
|
356
|
+
raise MandateDepthExceededError(
|
|
357
|
+
f"Sub-delegation denied: delegation depth {new_depth} exceeds max allowed depth ({parent.max_depth})."
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Scope attenuation (Principle of Least Privilege)
|
|
361
|
+
if allowed_actions is None:
|
|
362
|
+
derived_actions = list(parent.allowed_actions)
|
|
363
|
+
else:
|
|
364
|
+
if len(allowed_actions) == 0:
|
|
365
|
+
raise DelegationError("allowed_actions cannot be empty list in sub-mandate.")
|
|
366
|
+
if "*" not in parent.allowed_actions:
|
|
367
|
+
if "*" in allowed_actions:
|
|
368
|
+
raise MandateScopeViolationError(
|
|
369
|
+
"Cannot escalate scope: sub-mandate cannot request wildcard '*' when parent mandate has restricted scope."
|
|
370
|
+
)
|
|
371
|
+
for act in allowed_actions:
|
|
372
|
+
if act not in parent.allowed_actions:
|
|
373
|
+
raise MandateScopeViolationError(
|
|
374
|
+
f"Cannot escalate scope: action '{act}' is not in parent mandate scope."
|
|
375
|
+
)
|
|
376
|
+
derived_actions = allowed_actions
|
|
377
|
+
|
|
378
|
+
# Hierarchical Budget Reservation (Anti-Fan-Out Sibling Amplification)
|
|
379
|
+
available_delegation = parent.available_for_delegation
|
|
380
|
+
if max_credits is None:
|
|
381
|
+
derived_credits = available_delegation
|
|
382
|
+
else:
|
|
383
|
+
if max_credits <= 0:
|
|
384
|
+
raise DelegationError(f"Sub-mandate max_credits must be strictly positive, got {max_credits}.")
|
|
385
|
+
if max_credits > available_delegation:
|
|
386
|
+
raise MandateBudgetExceededError(
|
|
387
|
+
f"Cannot allocate {max_credits} credits: parent mandate only has {available_delegation} "
|
|
388
|
+
f"credits available for delegation ({parent.reserved_child_credits} already reserved by sibling sub-mandates)."
|
|
389
|
+
)
|
|
390
|
+
derived_credits = max_credits
|
|
391
|
+
|
|
392
|
+
# TTL attenuation
|
|
393
|
+
max_ttl = max(1.0, parent.expires_at - now)
|
|
394
|
+
derived_ttl = min(ttl_seconds, max_ttl) if ttl_seconds is not None else max_ttl
|
|
395
|
+
|
|
396
|
+
# Reserve child budget in parent record
|
|
397
|
+
updated_parent = replace(parent, reserved_child_credits=parent.reserved_child_credits + derived_credits)
|
|
398
|
+
self._record_mandate(updated_parent)
|
|
399
|
+
|
|
400
|
+
# Issue the derived sub-mandate cryptographically bound to parent_mandate_id
|
|
401
|
+
return self.issue_mandate(
|
|
402
|
+
parent_agent_id=parent.sub_agent_id,
|
|
403
|
+
sub_agent_id=new_sub_agent_id,
|
|
404
|
+
user_id=parent.user_id,
|
|
405
|
+
allowed_actions=derived_actions,
|
|
406
|
+
max_credits=derived_credits,
|
|
407
|
+
ttl_seconds=derived_ttl,
|
|
408
|
+
max_depth=parent.max_depth,
|
|
409
|
+
current_depth=new_depth,
|
|
410
|
+
parent_mandate_id=parent.mandate_id,
|
|
411
|
+
metadata=metadata,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
def revoke_mandate(self, mandate_id: str, reason: str = "") -> bool:
|
|
415
|
+
"""Revoke an active delegation mandate, release unspent reserved quota to parent, and cascade to descendants."""
|
|
416
|
+
with self._lock:
|
|
417
|
+
mandate = self._mandates.get(mandate_id)
|
|
418
|
+
if not mandate:
|
|
419
|
+
return False
|
|
420
|
+
|
|
421
|
+
if mandate.status == MandateStatus.REVOKED:
|
|
422
|
+
return True
|
|
423
|
+
|
|
424
|
+
revoked_meta = dict(mandate.metadata)
|
|
425
|
+
if reason:
|
|
426
|
+
revoked_meta["revocation_reason"] = reason
|
|
427
|
+
|
|
428
|
+
# Cascading revocation to all child descendants first
|
|
429
|
+
for mid, m in list(self._mandates.items()):
|
|
430
|
+
if m.parent_mandate_id == mandate_id and m.status != MandateStatus.REVOKED:
|
|
431
|
+
self.revoke_mandate(mid, reason=f"Cascading revocation from parent '{mandate_id}'")
|
|
432
|
+
|
|
433
|
+
# Release unspent reserved budget back to parent
|
|
434
|
+
unspent = max(0, mandate.max_credits - mandate.credits_used)
|
|
435
|
+
if mandate.parent_mandate_id and unspent > 0:
|
|
436
|
+
parent = self._mandates.get(mandate.parent_mandate_id)
|
|
437
|
+
if parent:
|
|
438
|
+
new_reserved = max(0, parent.reserved_child_credits - unspent)
|
|
439
|
+
self._record_mandate(replace(parent, reserved_child_credits=new_reserved))
|
|
440
|
+
|
|
441
|
+
self._record_mandate(
|
|
442
|
+
replace(
|
|
443
|
+
mandate,
|
|
444
|
+
status=MandateStatus.REVOKED,
|
|
445
|
+
metadata=revoked_meta,
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
return True
|
|
450
|
+
|
|
451
|
+
def get_mandate(self, mandate_id: str) -> Optional[DelegationMandate]:
|
|
452
|
+
"""Retrieve an immutable mandate snapshot by ID."""
|
|
453
|
+
with self._lock:
|
|
454
|
+
m = self._mandates.get(mandate_id)
|
|
455
|
+
return replace(m) if m is not None else None
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""DualTrust Multi-Agent Framework Adapters (LangChain, LangGraph, CrewAI, AutoGen, OpenAI)."""
|
|
2
|
+
|
|
3
|
+
from dualtrust.adapters.crewai import guard_crewai_tool
|
|
4
|
+
from dualtrust.adapters.langchain import DualTrustCallbackHandler
|
|
5
|
+
from dualtrust.adapters.mcp import DualTrustMCPGateway
|
|
6
|
+
from dualtrust.adapters.openai import OpenAIToolDispatcher
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"DualTrustCallbackHandler",
|
|
10
|
+
"OpenAIToolDispatcher",
|
|
11
|
+
"DualTrustMCPGateway",
|
|
12
|
+
"guard_crewai_tool",
|
|
13
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""CrewAI and AutoGen tool wrapper for DualTrust policy enforcement."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from typing import Any, Callable, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from dualtrust.engine import DualTrust
|
|
7
|
+
from dualtrust.models import HumanTier
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def guard_crewai_tool(
|
|
11
|
+
tool: Any,
|
|
12
|
+
dualtrust: DualTrust,
|
|
13
|
+
user_id: str,
|
|
14
|
+
agent_id: str,
|
|
15
|
+
min_human_score: int = 600,
|
|
16
|
+
min_agent_score: float = 0.50,
|
|
17
|
+
required_tier: Optional[HumanTier] = None,
|
|
18
|
+
cost_credits: int = 0,
|
|
19
|
+
) -> Any:
|
|
20
|
+
"""Wrap a CrewAI BaseTool instance or callable with DualTrust pre-execution gating.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
tool: A CrewAI BaseTool instance, an object with a `_run` method, or a plain callable.
|
|
24
|
+
dualtrust: The DualTrust engine instance.
|
|
25
|
+
user_id: Human principal requesting the action.
|
|
26
|
+
agent_id: CrewAI agent executing the tool.
|
|
27
|
+
min_human_score: Required social credit threshold.
|
|
28
|
+
min_agent_score: Required agent trust score.
|
|
29
|
+
required_tier: Optional minimum clearance tier.
|
|
30
|
+
cost_credits: Credits consumed on execution.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The tool with its execution method securely intercepted.
|
|
34
|
+
"""
|
|
35
|
+
def _evaluate_and_run(original_method: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
|
|
36
|
+
verdict = dualtrust.evaluate(
|
|
37
|
+
user_id=user_id,
|
|
38
|
+
agent_id=agent_id,
|
|
39
|
+
min_human_score=min_human_score,
|
|
40
|
+
min_agent_score=min_agent_score,
|
|
41
|
+
required_tier=required_tier,
|
|
42
|
+
cost_credits=cost_credits,
|
|
43
|
+
deduct_cost=True,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if not verdict.authorized:
|
|
47
|
+
reasons = "; ".join(verdict.reasons)
|
|
48
|
+
dualtrust.ledger.append(
|
|
49
|
+
event_type="CREWAI_TOOL_BLOCKED",
|
|
50
|
+
entity_id=agent_id,
|
|
51
|
+
payload={"tool_class": tool.__class__.__name__, "reasons": verdict.reasons},
|
|
52
|
+
)
|
|
53
|
+
raise PermissionError(f"[DualTrust Security Block] CrewAI tool execution denied: {reasons}")
|
|
54
|
+
|
|
55
|
+
return original_method(*args, **kwargs)
|
|
56
|
+
|
|
57
|
+
# If it is a CrewAI BaseTool object with _run method
|
|
58
|
+
if hasattr(tool, "_run") and callable(getattr(tool, "_run")):
|
|
59
|
+
orig_run = tool._run
|
|
60
|
+
|
|
61
|
+
@functools.wraps(orig_run)
|
|
62
|
+
def wrapped_run(*args: Any, **kwargs: Any) -> Any:
|
|
63
|
+
return _evaluate_and_run(orig_run, *args, **kwargs)
|
|
64
|
+
|
|
65
|
+
tool._run = wrapped_run
|
|
66
|
+
return tool
|
|
67
|
+
|
|
68
|
+
# Plain callable
|
|
69
|
+
if callable(tool):
|
|
70
|
+
@functools.wraps(tool)
|
|
71
|
+
def wrapped_callable(*args: Any, **kwargs: Any) -> Any:
|
|
72
|
+
return _evaluate_and_run(tool, *args, **kwargs)
|
|
73
|
+
|
|
74
|
+
return wrapped_callable
|
|
75
|
+
|
|
76
|
+
return tool
|