agenticdome-python-sdk 1.0.2__tar.gz → 1.0.4__tar.gz
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.
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/PKG-INFO +1 -1
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/agenticdome_python_sdk.egg-info/PKG-INFO +1 -1
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/agenticdome_python_sdk.egg-info/SOURCES.txt +3 -1
- agenticdome_python_sdk-1.0.4/agenticdome_sdk/pydantic.py +944 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/pyproject.toml +1 -1
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/setup.py +10 -0
- agenticdome_python_sdk-1.0.4/tests/test_pydanticai_integration.py +22 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/MANIFEST.in +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/README.md +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/agenticdome_python_sdk.egg-info/dependency_links.txt +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/agenticdome_python_sdk.egg-info/requires.txt +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/agenticdome_python_sdk.egg-info/top_level.txt +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/agenticdome_sdk/__init__.py +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/agenticdome_sdk/client.py +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/agenticdome_sdk/crewai.py +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/agenticdome_sdk/scenarios.py +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/agno_lateral_refund.py +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/ai_foundry_callback_export.py +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/crewai_agenticdome_firewall.py +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/crewai_lateral_refund.py +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/mcp_remote_patch.py +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/mesh_output_redaction.py +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/microsoft_metadata_exfil.py +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/salesforce_hidden_bcc.py +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/servicenow_delete_logs.py +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/setup.cfg +0 -0
- {agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agenticdome-python-sdk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Official Python SDK for AgentGuard Intelligence Engine and Action Firewall.
|
|
5
5
|
Home-page: https://github.com/agenticdome/agenticdome-python-sdk-python
|
|
6
6
|
Author: AgenticDome
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agenticdome-python-sdk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Official Python SDK for AgentGuard Intelligence Engine and Action Firewall.
|
|
5
5
|
Home-page: https://github.com/agenticdome/agenticdome-python-sdk-python
|
|
6
6
|
Author: AgenticDome
|
|
@@ -10,6 +10,7 @@ agenticdome_python_sdk.egg-info/top_level.txt
|
|
|
10
10
|
agenticdome_sdk/__init__.py
|
|
11
11
|
agenticdome_sdk/client.py
|
|
12
12
|
agenticdome_sdk/crewai.py
|
|
13
|
+
agenticdome_sdk/pydantic.py
|
|
13
14
|
agenticdome_sdk/scenarios.py
|
|
14
15
|
examples/agno_lateral_refund.py
|
|
15
16
|
examples/ai_foundry_callback_export.py
|
|
@@ -20,4 +21,5 @@ examples/mesh_output_redaction.py
|
|
|
20
21
|
examples/microsoft_metadata_exfil.py
|
|
21
22
|
examples/salesforce_hidden_bcc.py
|
|
22
23
|
examples/servicenow_delete_logs.py
|
|
23
|
-
tests/test_client.py
|
|
24
|
+
tests/test_client.py
|
|
25
|
+
tests/test_pydanticai_integration.py
|
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import functools
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from threading import Lock
|
|
12
|
+
from typing import Any, Callable, Dict, Optional, Tuple, TypeVar
|
|
13
|
+
|
|
14
|
+
import anyio
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from pydantic_ai import Agent, RunContext
|
|
18
|
+
from pydantic_ai.models import ModelResponse
|
|
19
|
+
except Exception as exc: # pragma: no cover
|
|
20
|
+
raise ImportError(
|
|
21
|
+
"AgenticDome PydanticAI integration requires pydantic-ai. "
|
|
22
|
+
"Install with: pip install 'agenticdome-python-sdk[pydanticai]'"
|
|
23
|
+
) from exc
|
|
24
|
+
|
|
25
|
+
from agenticdome_sdk.client import AgentGuardClient
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from agenticdome_sdk.exceptions import AgentGuardHTTPError
|
|
29
|
+
except Exception: # pragma: no cover
|
|
30
|
+
try:
|
|
31
|
+
from agenticdome_sdk.client import AgentGuardHTTPError # type: ignore
|
|
32
|
+
except Exception:
|
|
33
|
+
class AgentGuardHTTPError(Exception): # type: ignore
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger("agenticdome.pydanticai")
|
|
38
|
+
logger.setLevel(logging.INFO)
|
|
39
|
+
|
|
40
|
+
DepsT = TypeVar("DepsT")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ------------------------------------------------------------------
|
|
44
|
+
# Configuration
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
def _env_bool(name: str, default: bool) -> bool:
|
|
48
|
+
value = os.getenv(name)
|
|
49
|
+
if value is None:
|
|
50
|
+
return default
|
|
51
|
+
return value.strip().lower() in ("1", "true", "yes", "y", "on")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _env_int(name: str, default: int) -> int:
|
|
55
|
+
value = os.getenv(name)
|
|
56
|
+
if value is None:
|
|
57
|
+
return default
|
|
58
|
+
try:
|
|
59
|
+
return int(value)
|
|
60
|
+
except (TypeError, ValueError):
|
|
61
|
+
return default
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class FirewallConfig:
|
|
66
|
+
api_base: str = field(
|
|
67
|
+
default_factory=lambda: os.getenv(
|
|
68
|
+
"AGENTICDOME_API_BASE",
|
|
69
|
+
"https://au.agenticdome.io",
|
|
70
|
+
).rstrip("/")
|
|
71
|
+
)
|
|
72
|
+
api_key: str = field(default_factory=lambda: os.getenv("AGENTICDOME_API_KEY", ""))
|
|
73
|
+
tenant_id: str = field(default_factory=lambda: os.getenv("AGENTICDOME_TENANT_ID", ""))
|
|
74
|
+
|
|
75
|
+
platform: str = field(default_factory=lambda: os.getenv("AGENTICDOME_PLATFORM", "pydanticai"))
|
|
76
|
+
timeout_s: int = field(default_factory=lambda: _env_int("AGENTICDOME_TIMEOUT_S", 20))
|
|
77
|
+
fail_closed: bool = field(default_factory=lambda: _env_bool("AGENTICDOME_FAIL_CLOSED", True))
|
|
78
|
+
|
|
79
|
+
require_explicit_session_id: bool = field(
|
|
80
|
+
default_factory=lambda: _env_bool("AGENTICDOME_REQUIRE_SESSION_ID", False)
|
|
81
|
+
)
|
|
82
|
+
default_tool_platform: str = field(
|
|
83
|
+
default_factory=lambda: os.getenv("AGENTICDOME_DEFAULT_TOOL_PLATFORM", "python")
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
redact_pii: bool = field(default_factory=lambda: _env_bool("AGENTICDOME_REDACT_PII", True))
|
|
87
|
+
redact_secrets: bool = field(
|
|
88
|
+
default_factory=lambda: _env_bool("AGENTICDOME_REDACT_SECRETS", True)
|
|
89
|
+
)
|
|
90
|
+
block_on_sensitive_output: bool = field(
|
|
91
|
+
default_factory=lambda: _env_bool("AGENTICDOME_BLOCK_ON_SENSITIVE_OUTPUT", False)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
enable_a2a_for_delegation: bool = field(
|
|
95
|
+
default_factory=lambda: _env_bool("AGENTICDOME_ENABLE_A2A_FOR_DELEGATION", True)
|
|
96
|
+
)
|
|
97
|
+
handoff_token_ttl_s: int = field(
|
|
98
|
+
default_factory=lambda: _env_int("AGENTICDOME_HANDOFF_TOKEN_TTL_S", 900)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
redis_url: str = field(default_factory=lambda: os.getenv("AGENTICDOME_REDIS_URL", "").strip())
|
|
102
|
+
redis_key_prefix: str = field(
|
|
103
|
+
default_factory=lambda: os.getenv(
|
|
104
|
+
"AGENTICDOME_REDIS_KEY_PREFIX",
|
|
105
|
+
"AgenticDome:pydanticai:handoff",
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
report_incidents: bool = field(
|
|
110
|
+
default_factory=lambda: _env_bool("AGENTICDOME_REPORT_INCIDENTS", True)
|
|
111
|
+
)
|
|
112
|
+
blocked_incident_severity: str = field(
|
|
113
|
+
default_factory=lambda: os.getenv("AGENTICDOME_BLOCKED_INCIDENT_SEVERITY", "medium")
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class PydanticAIFirewallError(RuntimeError):
|
|
118
|
+
"""Base exception for AgenticDome PydanticAI firewall errors."""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class PydanticAIFirewallDenied(PydanticAIFirewallError):
|
|
122
|
+
"""Raised when AgenticDome explicitly blocks execution."""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True)
|
|
126
|
+
class DecisionTokenRecord:
|
|
127
|
+
decision_token: str
|
|
128
|
+
source_agent_id: str
|
|
129
|
+
created_at: float
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
# Token Store
|
|
134
|
+
# ------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
class DecisionTokenStore:
|
|
137
|
+
def put(
|
|
138
|
+
self,
|
|
139
|
+
*,
|
|
140
|
+
session_id: str,
|
|
141
|
+
target_agent_id: str,
|
|
142
|
+
tool_name: str,
|
|
143
|
+
tool_args: Dict[str, Any],
|
|
144
|
+
record: DecisionTokenRecord,
|
|
145
|
+
ttl_s: int,
|
|
146
|
+
) -> None:
|
|
147
|
+
raise NotImplementedError
|
|
148
|
+
|
|
149
|
+
def get(
|
|
150
|
+
self,
|
|
151
|
+
*,
|
|
152
|
+
session_id: str,
|
|
153
|
+
target_agent_id: str,
|
|
154
|
+
tool_name: str,
|
|
155
|
+
tool_args: Dict[str, Any],
|
|
156
|
+
) -> Optional[DecisionTokenRecord]:
|
|
157
|
+
raise NotImplementedError
|
|
158
|
+
|
|
159
|
+
def delete(
|
|
160
|
+
self,
|
|
161
|
+
*,
|
|
162
|
+
session_id: str,
|
|
163
|
+
target_agent_id: str,
|
|
164
|
+
tool_name: str,
|
|
165
|
+
tool_args: Dict[str, Any],
|
|
166
|
+
) -> None:
|
|
167
|
+
raise NotImplementedError
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _stable_json(value: Any) -> str:
|
|
171
|
+
return json.dumps(value, sort_keys=True, default=str, separators=(",", ":"))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _tool_fingerprint(tool_name: str, tool_args: Dict[str, Any]) -> str:
|
|
175
|
+
payload = {
|
|
176
|
+
"tool_name": tool_name or "",
|
|
177
|
+
"tool_args": tool_args or {},
|
|
178
|
+
}
|
|
179
|
+
return hashlib.sha256(_stable_json(payload).encode("utf-8")).hexdigest()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class InMemoryDecisionTokenStore(DecisionTokenStore):
|
|
183
|
+
def __init__(self, tenant_id: str) -> None:
|
|
184
|
+
self._tenant_id = tenant_id
|
|
185
|
+
self._lock = Lock()
|
|
186
|
+
self._data: Dict[str, Tuple[float, DecisionTokenRecord]] = {}
|
|
187
|
+
|
|
188
|
+
def _key(
|
|
189
|
+
self,
|
|
190
|
+
session_id: str,
|
|
191
|
+
target_agent_id: str,
|
|
192
|
+
tool_name: str,
|
|
193
|
+
tool_args: Dict[str, Any],
|
|
194
|
+
) -> str:
|
|
195
|
+
fp = _tool_fingerprint(tool_name, tool_args)
|
|
196
|
+
return f"{self._tenant_id}:{session_id}:{target_agent_id}:{fp}"
|
|
197
|
+
|
|
198
|
+
def _cleanup(self) -> None:
|
|
199
|
+
now = time.time()
|
|
200
|
+
expired = [
|
|
201
|
+
key for key, (expires_at, _) in self._data.items()
|
|
202
|
+
if expires_at <= now
|
|
203
|
+
]
|
|
204
|
+
for key in expired:
|
|
205
|
+
self._data.pop(key, None)
|
|
206
|
+
|
|
207
|
+
def put(
|
|
208
|
+
self,
|
|
209
|
+
*,
|
|
210
|
+
session_id: str,
|
|
211
|
+
target_agent_id: str,
|
|
212
|
+
tool_name: str,
|
|
213
|
+
tool_args: Dict[str, Any],
|
|
214
|
+
record: DecisionTokenRecord,
|
|
215
|
+
ttl_s: int,
|
|
216
|
+
) -> None:
|
|
217
|
+
key = self._key(session_id, target_agent_id, tool_name, tool_args)
|
|
218
|
+
with self._lock:
|
|
219
|
+
self._cleanup()
|
|
220
|
+
self._data[key] = (time.time() + ttl_s, record)
|
|
221
|
+
|
|
222
|
+
def get(
|
|
223
|
+
self,
|
|
224
|
+
*,
|
|
225
|
+
session_id: str,
|
|
226
|
+
target_agent_id: str,
|
|
227
|
+
tool_name: str,
|
|
228
|
+
tool_args: Dict[str, Any],
|
|
229
|
+
) -> Optional[DecisionTokenRecord]:
|
|
230
|
+
key = self._key(session_id, target_agent_id, tool_name, tool_args)
|
|
231
|
+
with self._lock:
|
|
232
|
+
self._cleanup()
|
|
233
|
+
entry = self._data.get(key)
|
|
234
|
+
return entry[1] if entry else None
|
|
235
|
+
|
|
236
|
+
def delete(
|
|
237
|
+
self,
|
|
238
|
+
*,
|
|
239
|
+
session_id: str,
|
|
240
|
+
target_agent_id: str,
|
|
241
|
+
tool_name: str,
|
|
242
|
+
tool_args: Dict[str, Any],
|
|
243
|
+
) -> None:
|
|
244
|
+
key = self._key(session_id, target_agent_id, tool_name, tool_args)
|
|
245
|
+
with self._lock:
|
|
246
|
+
self._data.pop(key, None)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class RedisDecisionTokenStore(DecisionTokenStore):
|
|
250
|
+
def __init__(self, url: str, prefix: str, tenant_id: str) -> None:
|
|
251
|
+
import redis
|
|
252
|
+
|
|
253
|
+
self.r = redis.Redis.from_url(url, decode_responses=True)
|
|
254
|
+
self.prefix = f"{prefix}:{tenant_id}"
|
|
255
|
+
|
|
256
|
+
def _key(
|
|
257
|
+
self,
|
|
258
|
+
session_id: str,
|
|
259
|
+
target_agent_id: str,
|
|
260
|
+
tool_name: str,
|
|
261
|
+
tool_args: Dict[str, Any],
|
|
262
|
+
) -> str:
|
|
263
|
+
fp = _tool_fingerprint(tool_name, tool_args)
|
|
264
|
+
return f"{self.prefix}:{session_id}:{target_agent_id}:{fp}"
|
|
265
|
+
|
|
266
|
+
def put(
|
|
267
|
+
self,
|
|
268
|
+
*,
|
|
269
|
+
session_id: str,
|
|
270
|
+
target_agent_id: str,
|
|
271
|
+
tool_name: str,
|
|
272
|
+
tool_args: Dict[str, Any],
|
|
273
|
+
record: DecisionTokenRecord,
|
|
274
|
+
ttl_s: int,
|
|
275
|
+
) -> None:
|
|
276
|
+
key = self._key(session_id, target_agent_id, tool_name, tool_args)
|
|
277
|
+
payload = {
|
|
278
|
+
"decision_token": record.decision_token,
|
|
279
|
+
"source_agent_id": record.source_agent_id,
|
|
280
|
+
"created_at": record.created_at,
|
|
281
|
+
}
|
|
282
|
+
self.r.setex(key, ttl_s, json.dumps(payload))
|
|
283
|
+
|
|
284
|
+
def get(
|
|
285
|
+
self,
|
|
286
|
+
*,
|
|
287
|
+
session_id: str,
|
|
288
|
+
target_agent_id: str,
|
|
289
|
+
tool_name: str,
|
|
290
|
+
tool_args: Dict[str, Any],
|
|
291
|
+
) -> Optional[DecisionTokenRecord]:
|
|
292
|
+
key = self._key(session_id, target_agent_id, tool_name, tool_args)
|
|
293
|
+
raw = self.r.get(key)
|
|
294
|
+
if not raw:
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
payload = json.loads(raw)
|
|
299
|
+
return DecisionTokenRecord(
|
|
300
|
+
decision_token=str(payload["decision_token"]),
|
|
301
|
+
source_agent_id=str(payload["source_agent_id"]),
|
|
302
|
+
created_at=float(payload.get("created_at", time.time())),
|
|
303
|
+
)
|
|
304
|
+
except Exception:
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
def delete(
|
|
308
|
+
self,
|
|
309
|
+
*,
|
|
310
|
+
session_id: str,
|
|
311
|
+
target_agent_id: str,
|
|
312
|
+
tool_name: str,
|
|
313
|
+
tool_args: Dict[str, Any],
|
|
314
|
+
) -> None:
|
|
315
|
+
key = self._key(session_id, target_agent_id, tool_name, tool_args)
|
|
316
|
+
self.r.delete(key)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _build_token_store(config: FirewallConfig) -> DecisionTokenStore:
|
|
320
|
+
if config.redis_url:
|
|
321
|
+
try:
|
|
322
|
+
return RedisDecisionTokenStore(
|
|
323
|
+
config.redis_url,
|
|
324
|
+
config.redis_key_prefix,
|
|
325
|
+
config.tenant_id,
|
|
326
|
+
)
|
|
327
|
+
except ImportError:
|
|
328
|
+
logger.warning(
|
|
329
|
+
"Redis package missing. Install with: "
|
|
330
|
+
"pip install 'agenticdome-python-sdk[redis]'. "
|
|
331
|
+
"Using in-memory token cache."
|
|
332
|
+
)
|
|
333
|
+
except Exception as exc:
|
|
334
|
+
logger.warning(
|
|
335
|
+
"Redis token store unavailable: %s. Using in-memory token cache.",
|
|
336
|
+
exc,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
return InMemoryDecisionTokenStore(config.tenant_id)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# ------------------------------------------------------------------
|
|
343
|
+
# Helpers
|
|
344
|
+
# ------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
def _safe_getattr(obj: Any, name: str, default: Any = None) -> Any:
|
|
347
|
+
try:
|
|
348
|
+
return getattr(obj, name, default)
|
|
349
|
+
except Exception:
|
|
350
|
+
return default
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _safe_setattr(obj: Any, name: str, value: Any) -> None:
|
|
354
|
+
try:
|
|
355
|
+
setattr(obj, name, value)
|
|
356
|
+
return
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
if hasattr(obj, "__dict__"):
|
|
362
|
+
obj.__dict__[name] = value
|
|
363
|
+
except Exception:
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _is_handoff_tool(tool_name: str) -> bool:
|
|
368
|
+
lower = tool_name.lower()
|
|
369
|
+
return any(marker in lower for marker in ("delegate", "handoff", "route", "transfer"))
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _target_agent_id(kwargs: Dict[str, Any]) -> str:
|
|
373
|
+
return str(
|
|
374
|
+
kwargs.get("target_agent_id")
|
|
375
|
+
or kwargs.get("coworker")
|
|
376
|
+
or kwargs.get("agent")
|
|
377
|
+
or kwargs.get("specialist_agent_id")
|
|
378
|
+
or "specialist"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _target_tool_name(current_tool_name: str, kwargs: Dict[str, Any]) -> str:
|
|
383
|
+
return str(
|
|
384
|
+
kwargs.get("target_tool_name")
|
|
385
|
+
or kwargs.get("tool_name")
|
|
386
|
+
or kwargs.get("skill_name")
|
|
387
|
+
or current_tool_name
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _target_tool_args(kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
392
|
+
raw = (
|
|
393
|
+
kwargs.get("target_tool_args")
|
|
394
|
+
or kwargs.get("skill_args")
|
|
395
|
+
or kwargs.get("arguments")
|
|
396
|
+
or {}
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if isinstance(raw, dict):
|
|
400
|
+
return dict(raw)
|
|
401
|
+
|
|
402
|
+
return {"_raw_input": str(raw)}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _strip_private_args(kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
406
|
+
return {
|
|
407
|
+
key: value
|
|
408
|
+
for key, value in kwargs.items()
|
|
409
|
+
if not key.startswith("_AgenticDome_") and not key.startswith("_decision_")
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _run_async_from_sync(async_fn: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
|
|
414
|
+
try:
|
|
415
|
+
return anyio.from_thread.run(async_fn, *args, **kwargs)
|
|
416
|
+
except RuntimeError:
|
|
417
|
+
return anyio.run(async_fn, *args, **kwargs)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# ------------------------------------------------------------------
|
|
421
|
+
# PydanticAI Firewall
|
|
422
|
+
# ------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
class CyberSecFirewall:
|
|
425
|
+
"""
|
|
426
|
+
AgenticDome Security Firewall for PydanticAI.
|
|
427
|
+
|
|
428
|
+
Provides:
|
|
429
|
+
- Prompt ingress screening where supported by the PydanticAI runtime
|
|
430
|
+
- Tool execution authorization
|
|
431
|
+
- Manager-to-specialist delegation token generation
|
|
432
|
+
- Specialist decision-token verification
|
|
433
|
+
- Output DLP sanitization
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
def __init__(self, config: Optional[FirewallConfig] = None) -> None:
|
|
437
|
+
self.config = config or FirewallConfig()
|
|
438
|
+
|
|
439
|
+
if not self.config.api_base or not self.config.api_key or not self.config.tenant_id:
|
|
440
|
+
logger.warning(
|
|
441
|
+
"AgenticDome PydanticAI firewall unconfigured. "
|
|
442
|
+
"Set AGENTICDOME_API_BASE, AGENTICDOME_API_KEY, and AGENTICDOME_TENANT_ID. "
|
|
443
|
+
"Runtime will operate in fail-open mode for SDK calls."
|
|
444
|
+
)
|
|
445
|
+
self.client: Optional[AgentGuardClient] = None
|
|
446
|
+
else:
|
|
447
|
+
self.client = AgentGuardClient(
|
|
448
|
+
api_base=self.config.api_base,
|
|
449
|
+
api_key=self.config.api_key,
|
|
450
|
+
tenant_id=self.config.tenant_id,
|
|
451
|
+
timeout=self.config.timeout_s,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
self.token_store = _build_token_store(self.config)
|
|
455
|
+
|
|
456
|
+
async def _to_thread(self, fn: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
|
|
457
|
+
return await anyio.to_thread.run_sync(lambda: fn(*args, **kwargs))
|
|
458
|
+
|
|
459
|
+
def _client_call(self, method_names: Tuple[str, ...], *args: Any, **kwargs: Any) -> Any:
|
|
460
|
+
if self.client is None:
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
last_type_error: Optional[TypeError] = None
|
|
464
|
+
|
|
465
|
+
for method_name in method_names:
|
|
466
|
+
method = getattr(self.client, method_name, None)
|
|
467
|
+
if method is None:
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
return method(*args, **kwargs)
|
|
472
|
+
except TypeError as exc:
|
|
473
|
+
last_type_error = exc
|
|
474
|
+
continue
|
|
475
|
+
|
|
476
|
+
if last_type_error:
|
|
477
|
+
raise last_type_error
|
|
478
|
+
|
|
479
|
+
raise AttributeError(
|
|
480
|
+
f"AgenticDome client does not implement any of: {', '.join(method_names)}"
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
def _extract_payload(self, response: Any) -> Dict[str, Any]:
|
|
484
|
+
if isinstance(response, dict):
|
|
485
|
+
result = response.get("result")
|
|
486
|
+
if isinstance(result, dict):
|
|
487
|
+
return result
|
|
488
|
+
return response
|
|
489
|
+
return {}
|
|
490
|
+
|
|
491
|
+
def _extract_verdict(self, response: Any) -> str:
|
|
492
|
+
payload = self._extract_payload(response)
|
|
493
|
+
return str(payload.get("verdict") or payload.get("decision") or "ALLOWED").upper()
|
|
494
|
+
|
|
495
|
+
def _reason(self, response: Any) -> str:
|
|
496
|
+
payload = self._extract_payload(response)
|
|
497
|
+
return str(payload.get("reason") or payload.get("message") or response)
|
|
498
|
+
|
|
499
|
+
def _decision_token(self, response: Any) -> Optional[str]:
|
|
500
|
+
payload = self._extract_payload(response)
|
|
501
|
+
token = payload.get("decision_token") or payload.get("token")
|
|
502
|
+
return str(token) if token else None
|
|
503
|
+
|
|
504
|
+
def _agent_name(self, ctx: RunContext[Any], fallback: str = "unnamed_agent") -> str:
|
|
505
|
+
agent = _safe_getattr(ctx, "agent")
|
|
506
|
+
value = (
|
|
507
|
+
_safe_getattr(agent, "name")
|
|
508
|
+
or _safe_getattr(ctx, "agent_name")
|
|
509
|
+
or _safe_getattr(_safe_getattr(ctx, "deps"), "agent_id")
|
|
510
|
+
)
|
|
511
|
+
return str(value) if value else fallback
|
|
512
|
+
|
|
513
|
+
def _session_id(self, ctx: RunContext[Any]) -> str:
|
|
514
|
+
deps = _safe_getattr(ctx, "deps")
|
|
515
|
+
|
|
516
|
+
for attr in ("run_id", "trace_id", "session_id", "task_id"):
|
|
517
|
+
value = _safe_getattr(ctx, attr) or _safe_getattr(deps, attr)
|
|
518
|
+
if value:
|
|
519
|
+
return str(value)
|
|
520
|
+
|
|
521
|
+
if self.config.require_explicit_session_id:
|
|
522
|
+
raise PydanticAIFirewallDenied(
|
|
523
|
+
"Strict mode error: missing session_id, run_id, trace_id, or task_id."
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
return f"pydanticai-session-{id(ctx)}"
|
|
527
|
+
|
|
528
|
+
def _report_incident(
|
|
529
|
+
self,
|
|
530
|
+
*,
|
|
531
|
+
agent_id: str,
|
|
532
|
+
incident_type: str,
|
|
533
|
+
severity: Optional[str] = None,
|
|
534
|
+
details: str = "",
|
|
535
|
+
) -> None:
|
|
536
|
+
if self.client is None or not self.config.report_incidents:
|
|
537
|
+
return
|
|
538
|
+
|
|
539
|
+
try:
|
|
540
|
+
self._client_call(
|
|
541
|
+
("report_incident", "reportIncident"),
|
|
542
|
+
agent_id=agent_id,
|
|
543
|
+
incident_type=incident_type,
|
|
544
|
+
severity=severity or self.config.blocked_incident_severity,
|
|
545
|
+
details=details,
|
|
546
|
+
tenant_id=self.config.tenant_id,
|
|
547
|
+
is_agent=True,
|
|
548
|
+
platform=self.config.platform,
|
|
549
|
+
)
|
|
550
|
+
except Exception as exc:
|
|
551
|
+
logger.debug("AgenticDome incident reporting failed: %s", exc)
|
|
552
|
+
|
|
553
|
+
def _handle_error(self, stage: str, exc: Exception, agent_id: str = "unknown") -> None:
|
|
554
|
+
logger.error("AgenticDome PydanticAI %s error: %s", stage, exc)
|
|
555
|
+
|
|
556
|
+
self._report_incident(
|
|
557
|
+
agent_id=agent_id,
|
|
558
|
+
incident_type=f"pydanticai_{stage}_error",
|
|
559
|
+
severity=self.config.blocked_incident_severity,
|
|
560
|
+
details=str(exc),
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
if self.config.fail_closed:
|
|
564
|
+
if isinstance(exc, PydanticAIFirewallDenied):
|
|
565
|
+
raise exc
|
|
566
|
+
raise PydanticAIFirewallDenied(f"AgenticDome security check failed: {exc}") from exc
|
|
567
|
+
|
|
568
|
+
# ------------------------------------------------------------------
|
|
569
|
+
# Agent lifecycle hooks
|
|
570
|
+
# ------------------------------------------------------------------
|
|
571
|
+
|
|
572
|
+
def attach_to_agent(self, agent: Agent[DepsT, Any]) -> None:
|
|
573
|
+
"""
|
|
574
|
+
Attach prompt ingress and response egress hooks to a PydanticAI Agent
|
|
575
|
+
when the installed PydanticAI runtime exposes compatible hook decorators.
|
|
576
|
+
|
|
577
|
+
PydanticAI hook APIs may vary by version. If lifecycle decorators are not
|
|
578
|
+
available, use secure_tool() around tools for tool perimeter security.
|
|
579
|
+
"""
|
|
580
|
+
|
|
581
|
+
before_runner_init = getattr(agent, "before_runner_init", None)
|
|
582
|
+
after_runner_end = getattr(agent, "after_runner_end", None)
|
|
583
|
+
|
|
584
|
+
if callable(before_runner_init):
|
|
585
|
+
@before_runner_init
|
|
586
|
+
def _ingress_prompt_shield(ctx: RunContext[DepsT], prompt: str) -> str:
|
|
587
|
+
if self.client is None or not str(prompt or "").strip():
|
|
588
|
+
return prompt
|
|
589
|
+
|
|
590
|
+
agent_id = getattr(agent, "name", None) or self._agent_name(ctx)
|
|
591
|
+
|
|
592
|
+
try:
|
|
593
|
+
response = self._client_call(
|
|
594
|
+
("guardrail_validate", "guardrailValidate"),
|
|
595
|
+
text=str(prompt),
|
|
596
|
+
agent_id=agent_id,
|
|
597
|
+
platform=self.config.platform,
|
|
598
|
+
source_platform=self.config.platform,
|
|
599
|
+
direction="input",
|
|
600
|
+
session_id=self._session_id(ctx),
|
|
601
|
+
policy_context={
|
|
602
|
+
"source_agent_id": agent_id,
|
|
603
|
+
"request_purpose": "pydanticai_prompt_input",
|
|
604
|
+
"platform": self.config.platform,
|
|
605
|
+
},
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
if self._extract_verdict(response) == "BLOCKED":
|
|
609
|
+
raise PydanticAIFirewallDenied(
|
|
610
|
+
f"AgenticDome prompt shield blocked input: {self._reason(response)}"
|
|
611
|
+
)
|
|
612
|
+
except Exception as exc:
|
|
613
|
+
self._handle_error("before_runner_init", exc, agent_id)
|
|
614
|
+
|
|
615
|
+
return prompt
|
|
616
|
+
|
|
617
|
+
else:
|
|
618
|
+
logger.info(
|
|
619
|
+
"PydanticAI agent does not expose before_runner_init. "
|
|
620
|
+
"Prompt lifecycle hook not attached."
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
if callable(after_runner_end):
|
|
624
|
+
@after_runner_end
|
|
625
|
+
def _egress_dlp_shield(
|
|
626
|
+
ctx: RunContext[DepsT],
|
|
627
|
+
response: ModelResponse,
|
|
628
|
+
) -> ModelResponse:
|
|
629
|
+
if self.client is None:
|
|
630
|
+
return response
|
|
631
|
+
|
|
632
|
+
agent_id = getattr(agent, "name", None) or self._agent_name(ctx)
|
|
633
|
+
|
|
634
|
+
try:
|
|
635
|
+
messages = getattr(response, "messages", None)
|
|
636
|
+
if not messages:
|
|
637
|
+
return response
|
|
638
|
+
|
|
639
|
+
last_msg = messages[-1]
|
|
640
|
+
parts = getattr(last_msg, "parts", None)
|
|
641
|
+
if not parts:
|
|
642
|
+
return response
|
|
643
|
+
|
|
644
|
+
for part in parts:
|
|
645
|
+
content = getattr(part, "content", None)
|
|
646
|
+
if not isinstance(content, str):
|
|
647
|
+
continue
|
|
648
|
+
|
|
649
|
+
scan = self._client_call(
|
|
650
|
+
("mesh_validate", "meshValidate"),
|
|
651
|
+
text=content,
|
|
652
|
+
agent_id=agent_id,
|
|
653
|
+
direction="output",
|
|
654
|
+
session_id=self._session_id(ctx),
|
|
655
|
+
platform=self.config.platform,
|
|
656
|
+
redact_pii=self.config.redact_pii,
|
|
657
|
+
redact_secrets=self.config.redact_secrets,
|
|
658
|
+
block_on_sensitive_output=self.config.block_on_sensitive_output,
|
|
659
|
+
policy_context={
|
|
660
|
+
"source_agent_id": agent_id,
|
|
661
|
+
"request_purpose": "pydanticai_output_review",
|
|
662
|
+
"platform": self.config.platform,
|
|
663
|
+
"redact_pii": self.config.redact_pii,
|
|
664
|
+
"redact_secrets": self.config.redact_secrets,
|
|
665
|
+
"block_on_sensitive_output": self.config.block_on_sensitive_output,
|
|
666
|
+
},
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
payload = self._extract_payload(scan)
|
|
670
|
+
verdict = self._extract_verdict(scan)
|
|
671
|
+
|
|
672
|
+
if verdict == "BLOCKED" or self.config.block_on_sensitive_output:
|
|
673
|
+
part.content = (
|
|
674
|
+
"[EGRESS PAYLOAD TERMINATED BY AGENTICDOME DLP SECURITY MESH]"
|
|
675
|
+
)
|
|
676
|
+
continue
|
|
677
|
+
|
|
678
|
+
sanitized = (
|
|
679
|
+
payload.get("sanitized_text")
|
|
680
|
+
or payload.get("text")
|
|
681
|
+
or payload.get("output")
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
if sanitized is not None:
|
|
685
|
+
part.content = str(sanitized)
|
|
686
|
+
|
|
687
|
+
except Exception as exc:
|
|
688
|
+
logger.error("AgenticDome PydanticAI after_runner_end error: %s", exc)
|
|
689
|
+
if self.config.fail_closed:
|
|
690
|
+
try:
|
|
691
|
+
messages = getattr(response, "messages", None)
|
|
692
|
+
if messages and getattr(messages[-1], "parts", None):
|
|
693
|
+
for part in messages[-1].parts:
|
|
694
|
+
if hasattr(part, "content"):
|
|
695
|
+
part.content = (
|
|
696
|
+
"[FATAL ERROR: AGENTICDOME SECURITY CHECK FAULTED]"
|
|
697
|
+
)
|
|
698
|
+
except Exception:
|
|
699
|
+
pass
|
|
700
|
+
|
|
701
|
+
return response
|
|
702
|
+
|
|
703
|
+
else:
|
|
704
|
+
logger.info(
|
|
705
|
+
"PydanticAI agent does not expose after_runner_end. "
|
|
706
|
+
"Output lifecycle hook not attached."
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
# ------------------------------------------------------------------
|
|
710
|
+
# Tool perimeter
|
|
711
|
+
# ------------------------------------------------------------------
|
|
712
|
+
|
|
713
|
+
def secure_tool(self, tool_func: Callable[..., Any]) -> Callable[..., Any]:
|
|
714
|
+
"""
|
|
715
|
+
Decorator that protects a PydanticAI tool.
|
|
716
|
+
|
|
717
|
+
It performs:
|
|
718
|
+
- Delegation-token verification for specialist calls
|
|
719
|
+
- A2A authorization and token minting for manager handoff tools
|
|
720
|
+
- Direct tool authorization for normal tools
|
|
721
|
+
- Output DLP screening after execution
|
|
722
|
+
"""
|
|
723
|
+
|
|
724
|
+
if asyncio.iscoroutinefunction(tool_func):
|
|
725
|
+
@functools.wraps(tool_func)
|
|
726
|
+
async def _async_wrapper(ctx: RunContext[Any], *args: Any, **kwargs: Any) -> Any:
|
|
727
|
+
clean_kwargs, _ = await self._pre_execute_tool_check(
|
|
728
|
+
ctx,
|
|
729
|
+
tool_func.__name__,
|
|
730
|
+
dict(kwargs),
|
|
731
|
+
)
|
|
732
|
+
result = await tool_func(ctx, *args, **clean_kwargs)
|
|
733
|
+
return await self._post_execute_tool_sanitize(ctx, result)
|
|
734
|
+
|
|
735
|
+
return _async_wrapper
|
|
736
|
+
|
|
737
|
+
@functools.wraps(tool_func)
|
|
738
|
+
def _sync_wrapper(ctx: RunContext[Any], *args: Any, **kwargs: Any) -> Any:
|
|
739
|
+
clean_kwargs, _ = _run_async_from_sync(
|
|
740
|
+
self._pre_execute_tool_check,
|
|
741
|
+
ctx,
|
|
742
|
+
tool_func.__name__,
|
|
743
|
+
dict(kwargs),
|
|
744
|
+
)
|
|
745
|
+
result = tool_func(ctx, *args, **clean_kwargs)
|
|
746
|
+
return _run_async_from_sync(self._post_execute_tool_sanitize, ctx, result)
|
|
747
|
+
|
|
748
|
+
return _sync_wrapper
|
|
749
|
+
|
|
750
|
+
async def _pre_execute_tool_check(
|
|
751
|
+
self,
|
|
752
|
+
ctx: RunContext[Any],
|
|
753
|
+
name: str,
|
|
754
|
+
kwargs: Dict[str, Any],
|
|
755
|
+
) -> Tuple[Dict[str, Any], bool]:
|
|
756
|
+
if self.client is None:
|
|
757
|
+
return kwargs, False
|
|
758
|
+
|
|
759
|
+
session_id = self._session_id(ctx)
|
|
760
|
+
agent_id = self._agent_name(ctx)
|
|
761
|
+
|
|
762
|
+
token = kwargs.pop("_AgenticDome_decision_token", None) or kwargs.pop("_decision_token", None)
|
|
763
|
+
source_agent_id = kwargs.pop("_AgenticDome_source_agent_id", None) or kwargs.pop(
|
|
764
|
+
"_source_agent_id",
|
|
765
|
+
None,
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
clean_kwargs = _strip_private_args(kwargs)
|
|
769
|
+
|
|
770
|
+
# Case A: Specialist execution verifies delegated decision token.
|
|
771
|
+
if token and source_agent_id:
|
|
772
|
+
response = await self._to_thread(
|
|
773
|
+
self._client_call,
|
|
774
|
+
("a2a_verify_decision_token_rpc", "a2aVerifyDecisionTokenRpc", "a2a_verify_decision_token"),
|
|
775
|
+
str(token),
|
|
776
|
+
tool_name=name,
|
|
777
|
+
tool_args=clean_kwargs,
|
|
778
|
+
agent_id=agent_id,
|
|
779
|
+
source_agent_id=str(source_agent_id),
|
|
780
|
+
platform=self.config.platform,
|
|
781
|
+
require_allowed=True,
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
payload = self._extract_payload(response)
|
|
785
|
+
|
|
786
|
+
if payload and payload.get("valid") is False:
|
|
787
|
+
raise PydanticAIFirewallDenied(
|
|
788
|
+
f"AgenticDome rejected handoff token: {self._reason(response)}"
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
return clean_kwargs, True
|
|
792
|
+
|
|
793
|
+
# Case B: Manager handoff tool authorizes delegation and injects token.
|
|
794
|
+
if self.config.enable_a2a_for_delegation and _is_handoff_tool(name):
|
|
795
|
+
target_agent_id = _target_agent_id(clean_kwargs)
|
|
796
|
+
target_tool_name = _target_tool_name(name, clean_kwargs)
|
|
797
|
+
target_args = _target_tool_args(clean_kwargs)
|
|
798
|
+
|
|
799
|
+
response = await self._to_thread(
|
|
800
|
+
self._client_call,
|
|
801
|
+
("a2a_authorize_tool", "a2aAuthorizeTool"),
|
|
802
|
+
text=f"PydanticAI manager {agent_id} delegating {target_tool_name} to {target_agent_id}",
|
|
803
|
+
agent_id=target_agent_id,
|
|
804
|
+
platform=self.config.platform,
|
|
805
|
+
source_platform=self.config.platform,
|
|
806
|
+
tool_platform=str(clean_kwargs.get("tool_platform") or self.config.default_tool_platform),
|
|
807
|
+
tool_name=target_tool_name,
|
|
808
|
+
tool_args=target_args,
|
|
809
|
+
session_id=session_id,
|
|
810
|
+
direction="outbound",
|
|
811
|
+
source_agent_id=agent_id,
|
|
812
|
+
policy_context={
|
|
813
|
+
"source_agent_id": agent_id,
|
|
814
|
+
"request_purpose": "pydanticai_delegated_task",
|
|
815
|
+
"platform": self.config.platform,
|
|
816
|
+
"delegation_chain": [agent_id, target_agent_id],
|
|
817
|
+
},
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
if self._extract_verdict(response) == "BLOCKED":
|
|
821
|
+
raise PydanticAIFirewallDenied(
|
|
822
|
+
f"AgenticDome blocked PydanticAI delegation: {self._reason(response)}"
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
decision_token = self._decision_token(response)
|
|
826
|
+
|
|
827
|
+
if decision_token:
|
|
828
|
+
clean_kwargs["_AgenticDome_decision_token"] = decision_token
|
|
829
|
+
clean_kwargs["_AgenticDome_source_agent_id"] = agent_id
|
|
830
|
+
|
|
831
|
+
target_args["_AgenticDome_decision_token"] = decision_token
|
|
832
|
+
target_args["_AgenticDome_source_agent_id"] = agent_id
|
|
833
|
+
|
|
834
|
+
if "target_tool_args" in clean_kwargs or "skill_args" not in clean_kwargs:
|
|
835
|
+
clean_kwargs["target_tool_args"] = target_args
|
|
836
|
+
else:
|
|
837
|
+
clean_kwargs["skill_args"] = target_args
|
|
838
|
+
|
|
839
|
+
self.token_store.put(
|
|
840
|
+
session_id=session_id,
|
|
841
|
+
target_agent_id=target_agent_id,
|
|
842
|
+
tool_name=target_tool_name,
|
|
843
|
+
tool_args=target_args,
|
|
844
|
+
record=DecisionTokenRecord(
|
|
845
|
+
decision_token=decision_token,
|
|
846
|
+
source_agent_id=agent_id,
|
|
847
|
+
created_at=time.time(),
|
|
848
|
+
),
|
|
849
|
+
ttl_s=self.config.handoff_token_ttl_s,
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
return clean_kwargs, False
|
|
853
|
+
|
|
854
|
+
# Case C: Direct tool authorization.
|
|
855
|
+
response = await self._to_thread(
|
|
856
|
+
self._client_call,
|
|
857
|
+
("guardrail_validate", "guardrailValidate"),
|
|
858
|
+
text=f"PydanticAI tool execution: {name}",
|
|
859
|
+
agent_id=agent_id,
|
|
860
|
+
platform=self.config.platform,
|
|
861
|
+
source_platform=self.config.platform,
|
|
862
|
+
tool_platform=str(clean_kwargs.get("tool_platform") or self.config.default_tool_platform),
|
|
863
|
+
tool_name=name,
|
|
864
|
+
tool_args=clean_kwargs,
|
|
865
|
+
direction="outbound",
|
|
866
|
+
session_id=session_id,
|
|
867
|
+
policy_context={
|
|
868
|
+
"source_agent_id": agent_id,
|
|
869
|
+
"request_purpose": "pydanticai_tool_execution",
|
|
870
|
+
"platform": self.config.platform,
|
|
871
|
+
},
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
if self._extract_verdict(response) == "BLOCKED":
|
|
875
|
+
raise PydanticAIFirewallDenied(
|
|
876
|
+
f"AgenticDome blocked tool execution '{name}': {self._reason(response)}"
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
return clean_kwargs, False
|
|
880
|
+
|
|
881
|
+
async def _post_execute_tool_sanitize(self, ctx: RunContext[Any], result: Any) -> Any:
|
|
882
|
+
if self.client is None:
|
|
883
|
+
return result
|
|
884
|
+
|
|
885
|
+
agent_id = self._agent_name(ctx)
|
|
886
|
+
|
|
887
|
+
if isinstance(result, (dict, list, tuple)):
|
|
888
|
+
raw_text = json.dumps(result, default=str)
|
|
889
|
+
else:
|
|
890
|
+
raw_text = str(result)
|
|
891
|
+
|
|
892
|
+
response = await self._to_thread(
|
|
893
|
+
self._client_call,
|
|
894
|
+
("mesh_validate", "meshValidate"),
|
|
895
|
+
text=raw_text,
|
|
896
|
+
agent_id=agent_id,
|
|
897
|
+
direction="output",
|
|
898
|
+
session_id=self._session_id(ctx),
|
|
899
|
+
platform=self.config.platform,
|
|
900
|
+
redact_pii=self.config.redact_pii,
|
|
901
|
+
redact_secrets=self.config.redact_secrets,
|
|
902
|
+
block_on_sensitive_output=self.config.block_on_sensitive_output,
|
|
903
|
+
policy_context={
|
|
904
|
+
"source_agent_id": agent_id,
|
|
905
|
+
"request_purpose": "pydanticai_tool_output_review",
|
|
906
|
+
"platform": self.config.platform,
|
|
907
|
+
"redact_pii": self.config.redact_pii,
|
|
908
|
+
"redact_secrets": self.config.redact_secrets,
|
|
909
|
+
"block_on_sensitive_output": self.config.block_on_sensitive_output,
|
|
910
|
+
},
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
payload = self._extract_payload(response)
|
|
914
|
+
verdict = self._extract_verdict(response)
|
|
915
|
+
|
|
916
|
+
if verdict == "BLOCKED":
|
|
917
|
+
return "[BLOCKED BY AGENTICDOME ACTION LAYER DLP]"
|
|
918
|
+
|
|
919
|
+
sanitized = (
|
|
920
|
+
payload.get("sanitized_text")
|
|
921
|
+
or payload.get("text")
|
|
922
|
+
or payload.get("output")
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
if sanitized is not None:
|
|
926
|
+
if isinstance(result, str):
|
|
927
|
+
return str(sanitized)
|
|
928
|
+
|
|
929
|
+
if self.config.block_on_sensitive_output:
|
|
930
|
+
return str(sanitized)
|
|
931
|
+
|
|
932
|
+
return result
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
__all__ = [
|
|
936
|
+
"FirewallConfig",
|
|
937
|
+
"PydanticAIFirewallError",
|
|
938
|
+
"PydanticAIFirewallDenied",
|
|
939
|
+
"DecisionTokenRecord",
|
|
940
|
+
"DecisionTokenStore",
|
|
941
|
+
"InMemoryDecisionTokenStore",
|
|
942
|
+
"RedisDecisionTokenStore",
|
|
943
|
+
"CyberSecFirewall",
|
|
944
|
+
]
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agenticdome-python-sdk"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.4"
|
|
8
8
|
description = "Official Python SDK for AgentGuard Intelligence Engine and Action Firewall."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -32,10 +32,20 @@ setup(
|
|
|
32
32
|
],
|
|
33
33
|
"redis": [
|
|
34
34
|
"redis>=4.5.0",
|
|
35
|
+
],
|
|
36
|
+
"pydanticai": [
|
|
37
|
+
"pydantic-ai",
|
|
38
|
+
"anyio>=4.0.0",
|
|
39
|
+
],
|
|
40
|
+
"pydantic": [
|
|
41
|
+
"pydantic-ai",
|
|
42
|
+
"anyio>=4.0.0",
|
|
35
43
|
],
|
|
36
44
|
"all": [
|
|
37
45
|
"crewai",
|
|
38
46
|
"redis>=4.5.0",
|
|
47
|
+
"pydantic-ai",
|
|
48
|
+
"anyio>=4.0.0",
|
|
39
49
|
],
|
|
40
50
|
"dev": [
|
|
41
51
|
"build",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
pytest.importorskip("pydantic_ai")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_pydanticai_firewall_imports():
|
|
8
|
+
from agenticdome_sdk.pydantic import CyberSecFirewall, FirewallConfig
|
|
9
|
+
|
|
10
|
+
config = FirewallConfig(
|
|
11
|
+
api_base="https://au.agenticdome.io",
|
|
12
|
+
api_key="test-key",
|
|
13
|
+
tenant_id="test-tenant",
|
|
14
|
+
fail_closed=False,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
firewall = CyberSecFirewall(config=config)
|
|
18
|
+
|
|
19
|
+
assert firewall.config.api_base == "https://au.agenticdome.io"
|
|
20
|
+
assert firewall.config.platform == "pydanticai"
|
|
21
|
+
assert hasattr(firewall, "secure_tool")
|
|
22
|
+
assert hasattr(firewall, "attach_to_agent")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/agno_lateral_refund.py
RENAMED
|
File without changes
|
{agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/ai_foundry_callback_export.py
RENAMED
|
File without changes
|
|
File without changes
|
{agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/crewai_lateral_refund.py
RENAMED
|
File without changes
|
|
File without changes
|
{agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/mesh_output_redaction.py
RENAMED
|
File without changes
|
{agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/microsoft_metadata_exfil.py
RENAMED
|
File without changes
|
{agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/salesforce_hidden_bcc.py
RENAMED
|
File without changes
|
{agenticdome_python_sdk-1.0.2 → agenticdome_python_sdk-1.0.4}/examples/servicenow_delete_logs.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|