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 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