codesec-agentguard 1.0.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.
- agentguard/__init__.py +31 -0
- agentguard/client.py +278 -0
- agentguard/exceptions.py +120 -0
- agentguard/models.py +122 -0
- agentguard/py.typed +1 -0
- codesec_agentguard-1.0.0.dist-info/LICENSE +21 -0
- codesec_agentguard-1.0.0.dist-info/METADATA +96 -0
- codesec_agentguard-1.0.0.dist-info/RECORD +10 -0
- codesec_agentguard-1.0.0.dist-info/WHEEL +5 -0
- codesec_agentguard-1.0.0.dist-info/top_level.txt +1 -0
agentguard/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""AgentGuard Python SDK."""
|
|
2
|
+
|
|
3
|
+
from .client import AgentGuard
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
AgentGuardAuthenticationError,
|
|
6
|
+
AgentGuardError,
|
|
7
|
+
AgentGuardForbiddenError,
|
|
8
|
+
AgentGuardGatewayError,
|
|
9
|
+
AgentGuardNotFoundError,
|
|
10
|
+
AgentGuardRateLimitError,
|
|
11
|
+
AgentGuardTimeoutError,
|
|
12
|
+
AgentGuardValidationError,
|
|
13
|
+
)
|
|
14
|
+
from .models import ApprovalRecord, EvaluationResult, PromptScanResult
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AgentGuard",
|
|
18
|
+
"AgentGuardError",
|
|
19
|
+
"AgentGuardAuthenticationError",
|
|
20
|
+
"AgentGuardForbiddenError",
|
|
21
|
+
"AgentGuardValidationError",
|
|
22
|
+
"AgentGuardNotFoundError",
|
|
23
|
+
"AgentGuardRateLimitError",
|
|
24
|
+
"AgentGuardTimeoutError",
|
|
25
|
+
"AgentGuardGatewayError",
|
|
26
|
+
"EvaluationResult",
|
|
27
|
+
"ApprovalRecord",
|
|
28
|
+
"PromptScanResult",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
__version__ = "1.0.0"
|
agentguard/client.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""AgentGuard Official Python Client."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import socket
|
|
5
|
+
import time
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.parse
|
|
8
|
+
import urllib.request
|
|
9
|
+
from typing import Any, Dict, Optional, Union
|
|
10
|
+
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
AgentGuardAuthenticationError,
|
|
13
|
+
AgentGuardError,
|
|
14
|
+
AgentGuardForbiddenError,
|
|
15
|
+
AgentGuardGatewayError,
|
|
16
|
+
AgentGuardNotFoundError,
|
|
17
|
+
AgentGuardRateLimitError,
|
|
18
|
+
AgentGuardTimeoutError,
|
|
19
|
+
AgentGuardValidationError,
|
|
20
|
+
)
|
|
21
|
+
from .models import ApprovalRecord, EvaluationResult, PromptScanResult
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AgentGuard:
|
|
25
|
+
"""Official Python Client for the AgentGuard AI Agent Security Gateway.
|
|
26
|
+
|
|
27
|
+
Intercepts and authorizes agent tool calls, financial actions, and prompt inputs
|
|
28
|
+
using deterministic policy enforcement and adversarial heuristic scanning.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
base_url: str,
|
|
34
|
+
api_key: str,
|
|
35
|
+
timeout: float = 5.0,
|
|
36
|
+
max_retries: int = 2,
|
|
37
|
+
headers: Optional[Dict[str, str]] = None,
|
|
38
|
+
workspace_id: Optional[str] = None,
|
|
39
|
+
):
|
|
40
|
+
if not base_url or not isinstance(base_url, str):
|
|
41
|
+
raise AgentGuardError("AgentGuard base_url is required and must be a non-empty string.")
|
|
42
|
+
if not api_key or not isinstance(api_key, str):
|
|
43
|
+
raise AgentGuardError("AgentGuard api_key is required and must be a non-empty string.")
|
|
44
|
+
|
|
45
|
+
self.base_url = base_url.rstrip("/")
|
|
46
|
+
self.api_key = api_key.strip()
|
|
47
|
+
self.timeout = float(timeout)
|
|
48
|
+
self.max_retries = int(max_retries)
|
|
49
|
+
self.workspace_id = workspace_id
|
|
50
|
+
|
|
51
|
+
self.default_headers = {
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
"Accept": "application/json",
|
|
54
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
55
|
+
"X-API-Key": self.api_key,
|
|
56
|
+
"User-Agent": "agentguard-sdk-python/1.0.0",
|
|
57
|
+
}
|
|
58
|
+
if headers:
|
|
59
|
+
self.default_headers.update(headers)
|
|
60
|
+
if self.workspace_id:
|
|
61
|
+
self.default_headers["X-Workspace-Id"] = self.workspace_id
|
|
62
|
+
|
|
63
|
+
def __enter__(self) -> "AgentGuard":
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
67
|
+
self.close()
|
|
68
|
+
|
|
69
|
+
def close(self) -> None:
|
|
70
|
+
"""Explicit resource cleanup if required."""
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
def evaluate(
|
|
74
|
+
self,
|
|
75
|
+
action: Optional[Union[str, Dict[str, Any]]] = None,
|
|
76
|
+
amount: Optional[Union[float, int, str]] = None,
|
|
77
|
+
currency: str = "USD",
|
|
78
|
+
recipient: Optional[Union[str, Dict[str, Any]]] = None,
|
|
79
|
+
description: Optional[str] = None,
|
|
80
|
+
agent_id: Optional[str] = None,
|
|
81
|
+
tool: Optional[str] = None,
|
|
82
|
+
operation: Optional[str] = None,
|
|
83
|
+
prompt_context: Optional[str] = None,
|
|
84
|
+
human_approval_token: Optional[str] = None,
|
|
85
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
86
|
+
idempotency_key: Optional[str] = None,
|
|
87
|
+
**extra_kwargs: Any,
|
|
88
|
+
) -> EvaluationResult:
|
|
89
|
+
"""Pre-flight policy evaluation for autonomous AI agents and backend actions.
|
|
90
|
+
|
|
91
|
+
Supports both dictionary style (evaluate({"action": "payment.create", ...}))
|
|
92
|
+
and keyword-argument style (evaluate(action="payment.create", amount=1500)).
|
|
93
|
+
"""
|
|
94
|
+
payload: Dict[str, Any] = {}
|
|
95
|
+
|
|
96
|
+
if isinstance(action, dict):
|
|
97
|
+
payload = dict(action)
|
|
98
|
+
elif isinstance(action, str):
|
|
99
|
+
payload["action"] = action
|
|
100
|
+
if amount is not None:
|
|
101
|
+
payload["amount"] = amount
|
|
102
|
+
if currency:
|
|
103
|
+
payload["currency"] = currency
|
|
104
|
+
if recipient is not None:
|
|
105
|
+
payload["recipient"] = recipient
|
|
106
|
+
if description is not None:
|
|
107
|
+
payload["description"] = description
|
|
108
|
+
if agent_id is not None:
|
|
109
|
+
payload["agentId"] = agent_id
|
|
110
|
+
if tool is not None:
|
|
111
|
+
payload["tool"] = tool
|
|
112
|
+
if operation is not None:
|
|
113
|
+
payload["operation"] = operation
|
|
114
|
+
if prompt_context is not None:
|
|
115
|
+
payload["promptContext"] = prompt_context
|
|
116
|
+
if human_approval_token is not None:
|
|
117
|
+
payload["humanApprovalToken"] = human_approval_token
|
|
118
|
+
if metadata is not None:
|
|
119
|
+
payload["metadata"] = metadata
|
|
120
|
+
if idempotency_key is not None:
|
|
121
|
+
payload["idempotencyKey"] = idempotency_key
|
|
122
|
+
if extra_kwargs:
|
|
123
|
+
payload.update(extra_kwargs)
|
|
124
|
+
else:
|
|
125
|
+
raise AgentGuardValidationError("First argument must be either an action string or a payload dictionary.")
|
|
126
|
+
|
|
127
|
+
if not payload.get("action"):
|
|
128
|
+
raise AgentGuardValidationError("Evaluation request must specify an 'action'.")
|
|
129
|
+
|
|
130
|
+
request_id = payload.get("idempotencyKey") or f"py_sdk_{int(time.time() * 1000)}"
|
|
131
|
+
headers = {"X-Request-Id": request_id}
|
|
132
|
+
|
|
133
|
+
status_code, body = self._request("POST", "/api/v1/evaluate", payload=payload, extra_headers=headers)
|
|
134
|
+
|
|
135
|
+
# Policy decision returns structured evaluation even on HTTP 202 (HOLD) or 403 (BLOCK)
|
|
136
|
+
if isinstance(body, dict) and body.get("status") in ("ALLOW", "HOLD", "BLOCK"):
|
|
137
|
+
return EvaluationResult.from_dict(body)
|
|
138
|
+
|
|
139
|
+
if status_code in (200, 201, 202):
|
|
140
|
+
return EvaluationResult.from_dict(body)
|
|
141
|
+
|
|
142
|
+
raise self._parse_error(status_code, body, request_id)
|
|
143
|
+
|
|
144
|
+
def evaluate_safely(
|
|
145
|
+
self,
|
|
146
|
+
action: Optional[Union[str, Dict[str, Any]]] = None,
|
|
147
|
+
**kwargs: Any,
|
|
148
|
+
) -> EvaluationResult:
|
|
149
|
+
"""Safe evaluation wrapper with strict fail-closed guarantee.
|
|
150
|
+
|
|
151
|
+
Never raises exceptions for network failures or gateway outages;
|
|
152
|
+
strictly returns an authoritative BLOCK decision.
|
|
153
|
+
"""
|
|
154
|
+
try:
|
|
155
|
+
return self.evaluate(action, **kwargs)
|
|
156
|
+
except Exception as exc: # pylint: disable=broad-except
|
|
157
|
+
reason = str(exc)
|
|
158
|
+
return EvaluationResult(
|
|
159
|
+
status="BLOCK",
|
|
160
|
+
decision="BLOCK",
|
|
161
|
+
is_allowed=False,
|
|
162
|
+
is_held=False,
|
|
163
|
+
is_blocked=True,
|
|
164
|
+
reason=f"FAIL_CLOSED: {reason}",
|
|
165
|
+
violations=[f"Gateway communication error: {reason}"],
|
|
166
|
+
request_id=f"fail_closed_{int(time.time() * 1000)}",
|
|
167
|
+
timestamp=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def get_approval(self, approval_id: str) -> ApprovalRecord:
|
|
171
|
+
"""Fetch human review approval record by ID."""
|
|
172
|
+
if not approval_id or not isinstance(approval_id, str):
|
|
173
|
+
raise AgentGuardError("approval_id is required.")
|
|
174
|
+
|
|
175
|
+
encoded_id = urllib.parse.quote(approval_id)
|
|
176
|
+
status_code, body = self._request("GET", f"/api/v1/approvals/{encoded_id}")
|
|
177
|
+
|
|
178
|
+
if status_code == 200 and isinstance(body, dict):
|
|
179
|
+
return ApprovalRecord.from_dict(body)
|
|
180
|
+
|
|
181
|
+
raise self._parse_error(status_code, body)
|
|
182
|
+
|
|
183
|
+
def wait_for_approval(
|
|
184
|
+
self,
|
|
185
|
+
approval_id: str,
|
|
186
|
+
poll_interval: float = 1.0,
|
|
187
|
+
timeout: float = 30.0,
|
|
188
|
+
) -> ApprovalRecord:
|
|
189
|
+
"""Poll a held transaction until approved, rejected, or timed out."""
|
|
190
|
+
start_time = time.time()
|
|
191
|
+
while time.time() - start_time < timeout:
|
|
192
|
+
record = self.get_approval(approval_id)
|
|
193
|
+
if record.status != "PENDING":
|
|
194
|
+
return record
|
|
195
|
+
time.sleep(poll_interval)
|
|
196
|
+
|
|
197
|
+
raise AgentGuardTimeoutError(
|
|
198
|
+
f"Timed out waiting for approval on '{approval_id}' after {timeout} seconds."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def scan_prompt(self, input_text: str) -> PromptScanResult:
|
|
202
|
+
"""Scan prompt context for adversarial prompt injection heuristics."""
|
|
203
|
+
status_code, body = self._request("POST", "/api/v1/scan", payload={"input": input_text})
|
|
204
|
+
if status_code == 200 and isinstance(body, dict):
|
|
205
|
+
return PromptScanResult.from_dict(body)
|
|
206
|
+
raise self._parse_error(status_code, body)
|
|
207
|
+
|
|
208
|
+
def _request(
|
|
209
|
+
self,
|
|
210
|
+
method: str,
|
|
211
|
+
path: str,
|
|
212
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
213
|
+
extra_headers: Optional[Dict[str, str]] = None,
|
|
214
|
+
) -> tuple[int, Any]:
|
|
215
|
+
url = f"{self.base_url}{path}"
|
|
216
|
+
headers = dict(self.default_headers)
|
|
217
|
+
if extra_headers:
|
|
218
|
+
headers.update(extra_headers)
|
|
219
|
+
|
|
220
|
+
data = json.dumps(payload).encode("utf-8") if payload is not None else None
|
|
221
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as response:
|
|
225
|
+
status_code = response.status
|
|
226
|
+
response_text = response.read().decode("utf-8")
|
|
227
|
+
try:
|
|
228
|
+
return status_code, json.loads(response_text)
|
|
229
|
+
except Exception:
|
|
230
|
+
return status_code, {"raw": response_text}
|
|
231
|
+
except urllib.error.HTTPError as exc:
|
|
232
|
+
status_code = exc.code
|
|
233
|
+
try:
|
|
234
|
+
error_body = json.loads(exc.read().decode("utf-8"))
|
|
235
|
+
except Exception:
|
|
236
|
+
error_body = {"error": exc.reason}
|
|
237
|
+
return status_code, error_body
|
|
238
|
+
except urllib.error.URLError as exc:
|
|
239
|
+
reason = str(exc.reason)
|
|
240
|
+
if "timed out" in reason.lower() or isinstance(getattr(exc, "reason", None), (socket.timeout, TimeoutError)):
|
|
241
|
+
raise AgentGuardTimeoutError(
|
|
242
|
+
f"AgentGuard request timed out after {self.timeout}s: {reason}"
|
|
243
|
+
) from exc
|
|
244
|
+
raise AgentGuardGatewayError(
|
|
245
|
+
f"Failed to connect to AgentGuard gateway at {self.base_url}: {reason}",
|
|
246
|
+
status_code=503,
|
|
247
|
+
) from exc
|
|
248
|
+
except (socket.timeout, TimeoutError) as exc:
|
|
249
|
+
raise AgentGuardTimeoutError(
|
|
250
|
+
f"AgentGuard request timed out after {self.timeout}s: {exc}"
|
|
251
|
+
) from exc
|
|
252
|
+
except Exception as exc:
|
|
253
|
+
raise AgentGuardGatewayError(f"Unexpected error communicating with AgentGuard: {exc}") from exc
|
|
254
|
+
|
|
255
|
+
def _parse_error(self, status_code: int, body: Any, request_id: Optional[str] = None) -> AgentGuardError:
|
|
256
|
+
message = (
|
|
257
|
+
body.get("message")
|
|
258
|
+
or body.get("error")
|
|
259
|
+
or f"AgentGuard request failed with HTTP {status_code}"
|
|
260
|
+
if isinstance(body, dict)
|
|
261
|
+
else f"AgentGuard request failed with HTTP {status_code}"
|
|
262
|
+
)
|
|
263
|
+
details = body.get("details") if isinstance(body, dict) else None
|
|
264
|
+
|
|
265
|
+
if status_code == 401:
|
|
266
|
+
return AgentGuardAuthenticationError(message, request_id=request_id, details=details)
|
|
267
|
+
if status_code == 403:
|
|
268
|
+
return AgentGuardForbiddenError(message, request_id=request_id, details=details)
|
|
269
|
+
if status_code == 400:
|
|
270
|
+
return AgentGuardValidationError(message, request_id=request_id, details=details)
|
|
271
|
+
if status_code == 404:
|
|
272
|
+
return AgentGuardNotFoundError(message, request_id=request_id, details=details)
|
|
273
|
+
if status_code == 429:
|
|
274
|
+
return AgentGuardRateLimitError(message, request_id=request_id, details=details)
|
|
275
|
+
if status_code >= 500:
|
|
276
|
+
return AgentGuardGatewayError(message, status_code=status_code, request_id=request_id, details=details)
|
|
277
|
+
|
|
278
|
+
return AgentGuardError(message, status_code=status_code, request_id=request_id, details=details)
|
agentguard/exceptions.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""AgentGuard Python SDK Exception Hierarchy."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AgentGuardError(Exception):
|
|
7
|
+
"""Base exception for all AgentGuard errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
status_code: Optional[int] = None,
|
|
13
|
+
error_code: Optional[str] = None,
|
|
14
|
+
request_id: Optional[str] = None,
|
|
15
|
+
details: Optional[Any] = None,
|
|
16
|
+
):
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.message = message
|
|
19
|
+
self.status_code = status_code
|
|
20
|
+
self.error_code = error_code
|
|
21
|
+
self.request_id = request_id
|
|
22
|
+
self.details = details
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def status(self) -> Optional[int]:
|
|
26
|
+
"""HTTP status code alias."""
|
|
27
|
+
return self.status_code
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def code(self) -> Optional[str]:
|
|
31
|
+
"""Error code alias."""
|
|
32
|
+
return self.error_code
|
|
33
|
+
|
|
34
|
+
def __str__(self) -> str:
|
|
35
|
+
req_suffix = f" [Request ID: {self.request_id}]" if self.request_id else ""
|
|
36
|
+
return f"{self.message}{req_suffix}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AgentGuardAuthenticationError(AgentGuardError):
|
|
40
|
+
"""Raised when authentication fails (HTTP 401: missing, expired, or revoked API key)."""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
message: str = "Authentication failed. Please verify your AgentGuard API key.",
|
|
45
|
+
request_id: Optional[str] = None,
|
|
46
|
+
details: Optional[Any] = None,
|
|
47
|
+
):
|
|
48
|
+
super().__init__(message, status_code=401, error_code="AUTHENTICATION_FAILED", request_id=request_id, details=details)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AgentGuardForbiddenError(AgentGuardError):
|
|
52
|
+
"""Raised when permissions are insufficient (HTTP 403: API key lacks required scope)."""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
message: str = "Permission denied. The API key lacks the necessary permissions.",
|
|
57
|
+
request_id: Optional[str] = None,
|
|
58
|
+
details: Optional[Any] = None,
|
|
59
|
+
):
|
|
60
|
+
super().__init__(message, status_code=403, error_code="FORBIDDEN", request_id=request_id, details=details)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AgentGuardValidationError(AgentGuardError):
|
|
64
|
+
"""Raised when request parameters fail schema validation (HTTP 400)."""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
message: str = "Invalid request parameters.",
|
|
69
|
+
request_id: Optional[str] = None,
|
|
70
|
+
details: Optional[Any] = None,
|
|
71
|
+
):
|
|
72
|
+
super().__init__(message, status_code=400, error_code="VALIDATION_ERROR", request_id=request_id, details=details)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class AgentGuardNotFoundError(AgentGuardError):
|
|
76
|
+
"""Raised when a requested resource (e.g., approval ID) is not found (HTTP 404)."""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
message: str = "Requested resource not found.",
|
|
81
|
+
request_id: Optional[str] = None,
|
|
82
|
+
details: Optional[Any] = None,
|
|
83
|
+
):
|
|
84
|
+
super().__init__(message, status_code=404, error_code="NOT_FOUND", request_id=request_id, details=details)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AgentGuardRateLimitError(AgentGuardError):
|
|
88
|
+
"""Raised when the gateway rate limit is exceeded (HTTP 429)."""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
message: str = "Rate limit exceeded. Please back off before retrying.",
|
|
93
|
+
request_id: Optional[str] = None,
|
|
94
|
+
details: Optional[Any] = None,
|
|
95
|
+
):
|
|
96
|
+
super().__init__(message, status_code=429, error_code="RATE_LIMIT_EXCEEDED", request_id=request_id, details=details)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class AgentGuardTimeoutError(AgentGuardError):
|
|
100
|
+
"""Raised when an HTTP request or approval wait times out."""
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
message: str = "AgentGuard evaluation request timed out.",
|
|
105
|
+
request_id: Optional[str] = None,
|
|
106
|
+
):
|
|
107
|
+
super().__init__(message, status_code=408, error_code="TIMEOUT", request_id=request_id)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class AgentGuardGatewayError(AgentGuardError):
|
|
111
|
+
"""Raised when the AgentGuard gateway is unreachable or returns HTTP 5xx."""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
message: str = "AgentGuard gateway error or service unavailable.",
|
|
116
|
+
status_code: int = 500,
|
|
117
|
+
request_id: Optional[str] = None,
|
|
118
|
+
details: Optional[Any] = None,
|
|
119
|
+
):
|
|
120
|
+
super().__init__(message, status_code=status_code, error_code="GATEWAY_ERROR", request_id=request_id, details=details)
|
agentguard/models.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""AgentGuard Python SDK Data Models."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class EvaluationResult:
|
|
9
|
+
"""Authoritative decision returned by the AgentGuard security gateway."""
|
|
10
|
+
|
|
11
|
+
status: str
|
|
12
|
+
decision: str
|
|
13
|
+
is_allowed: bool
|
|
14
|
+
is_held: bool
|
|
15
|
+
is_blocked: bool
|
|
16
|
+
reason: str
|
|
17
|
+
violations: List[str] = field(default_factory=list)
|
|
18
|
+
request_id: str = ""
|
|
19
|
+
policy_id: Optional[str] = None
|
|
20
|
+
policy_version: Optional[int] = None
|
|
21
|
+
pending_approval_id: Optional[str] = None
|
|
22
|
+
authorization_token: Optional[str] = None
|
|
23
|
+
amount_formatted: Optional[str] = None
|
|
24
|
+
amount_cents: Optional[str] = None
|
|
25
|
+
currency: Optional[str] = None
|
|
26
|
+
timestamp: str = ""
|
|
27
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_dict(cls, data: Dict[str, Any]) -> "EvaluationResult":
|
|
31
|
+
status = data.get("status") or data.get("decision", "BLOCK")
|
|
32
|
+
return cls(
|
|
33
|
+
status=status,
|
|
34
|
+
decision=status,
|
|
35
|
+
is_allowed=(status == "ALLOW"),
|
|
36
|
+
is_held=(status == "HOLD"),
|
|
37
|
+
is_blocked=(status == "BLOCK"),
|
|
38
|
+
reason=data.get("reason", "No reason provided by gateway"),
|
|
39
|
+
violations=data.get("violations", []),
|
|
40
|
+
request_id=data.get("requestId", ""),
|
|
41
|
+
policy_id=data.get("policyId"),
|
|
42
|
+
policy_version=data.get("policyVersion"),
|
|
43
|
+
pending_approval_id=data.get("pendingApprovalId"),
|
|
44
|
+
authorization_token=data.get("authorizationToken"),
|
|
45
|
+
amount_formatted=data.get("amountFormatted"),
|
|
46
|
+
amount_cents=data.get("amountCents"),
|
|
47
|
+
currency=data.get("currency"),
|
|
48
|
+
timestamp=data.get("timestamp", ""),
|
|
49
|
+
metadata=data.get("metadata"),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class ApprovalRecord:
|
|
55
|
+
"""Human-in-the-Loop approval record for held transactions."""
|
|
56
|
+
|
|
57
|
+
id: str
|
|
58
|
+
request_id: str
|
|
59
|
+
agent_id: str
|
|
60
|
+
policy_id: str
|
|
61
|
+
policy_version: int
|
|
62
|
+
tool: str
|
|
63
|
+
operation: str
|
|
64
|
+
amount_formatted: str
|
|
65
|
+
amount_cents: str
|
|
66
|
+
currency: str
|
|
67
|
+
merchant: str
|
|
68
|
+
description: str
|
|
69
|
+
status: str
|
|
70
|
+
risk_reasons: List[str] = field(default_factory=list)
|
|
71
|
+
server_authorization_token: Optional[str] = None
|
|
72
|
+
token_expires_at: Optional[str] = None
|
|
73
|
+
reviewed_by: Optional[str] = None
|
|
74
|
+
reviewed_at: Optional[str] = None
|
|
75
|
+
rejection_reason: Optional[str] = None
|
|
76
|
+
created_at: str = ""
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ApprovalRecord":
|
|
80
|
+
return cls(
|
|
81
|
+
id=data.get("id", ""),
|
|
82
|
+
request_id=data.get("requestId", ""),
|
|
83
|
+
agent_id=data.get("agentId", ""),
|
|
84
|
+
policy_id=data.get("policyId", ""),
|
|
85
|
+
policy_version=data.get("policyVersion", 1),
|
|
86
|
+
tool=data.get("tool", ""),
|
|
87
|
+
operation=data.get("operation", ""),
|
|
88
|
+
amount_formatted=data.get("amountFormatted", ""),
|
|
89
|
+
amount_cents=data.get("amountCents", "0"),
|
|
90
|
+
currency=data.get("currency", "USD"),
|
|
91
|
+
merchant=str(data.get("merchant", "")),
|
|
92
|
+
description=data.get("description", ""),
|
|
93
|
+
status=data.get("status", "PENDING"),
|
|
94
|
+
risk_reasons=data.get("riskReasons", []),
|
|
95
|
+
server_authorization_token=data.get("serverAuthorizationToken"),
|
|
96
|
+
token_expires_at=data.get("tokenExpiresAt"),
|
|
97
|
+
reviewed_by=data.get("reviewedBy"),
|
|
98
|
+
reviewed_at=data.get("reviewedAt"),
|
|
99
|
+
rejection_reason=data.get("rejectionReason"),
|
|
100
|
+
created_at=data.get("createdAt", ""),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class PromptScanResult:
|
|
106
|
+
"""Result of prompt injection heuristic scanning."""
|
|
107
|
+
|
|
108
|
+
is_threat_detected: bool
|
|
109
|
+
risk_score: float
|
|
110
|
+
threat_level: str
|
|
111
|
+
findings: List[Dict[str, Any]] = field(default_factory=list)
|
|
112
|
+
sanitized_input: str = ""
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_dict(cls, data: Dict[str, Any]) -> "PromptScanResult":
|
|
116
|
+
return cls(
|
|
117
|
+
is_threat_detected=bool(data.get("isThreatDetected", False)),
|
|
118
|
+
risk_score=float(data.get("riskScore", 0.0)),
|
|
119
|
+
threat_level=data.get("threatLevel", "NONE"),
|
|
120
|
+
findings=data.get("findings", []),
|
|
121
|
+
sanitized_input=data.get("sanitizedInput", ""),
|
|
122
|
+
)
|
agentguard/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561. The agentguard package uses inline type annotations.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CodeSec AI / AgentGuard Authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: codesec-agentguard
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the AgentGuard AI Agent Security Gateway
|
|
5
|
+
Home-page: https://github.com/altafpasha/agentguard
|
|
6
|
+
Author: CodeSec AI / AgentGuard Security Team
|
|
7
|
+
Author-email: security@agentguard.dev
|
|
8
|
+
License: MIT
|
|
9
|
+
Platform: UNKNOWN
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
|
|
14
|
+
# codesec-agentguard (Python SDK)
|
|
15
|
+
|
|
16
|
+
Official Python SDK for the **AgentGuard** AI Agent Security Gateway.
|
|
17
|
+
|
|
18
|
+
AgentGuard intercepts, evaluates, and verifies autonomous AI agent actions against deterministic spending limits, currency controls, capability whitelists, and adversarial prompt injection heuristic scans before downstream payment APIs (such as Razorpay or PayPal) are executed.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install codesec-agentguard
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install locally in editable mode:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install -e packages/python-sdk
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Quickstart
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import os
|
|
40
|
+
from agentguard import AgentGuard
|
|
41
|
+
|
|
42
|
+
# Initialize client
|
|
43
|
+
guard = AgentGuard(
|
|
44
|
+
base_url=os.environ.get("AGENTGUARD_URL", "http://localhost:3001"),
|
|
45
|
+
api_key=os.environ["AGENTGUARD_API_KEY"],
|
|
46
|
+
timeout=5.0,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Pre-flight evaluation
|
|
50
|
+
decision = guard.evaluate({
|
|
51
|
+
"action": "payment.create",
|
|
52
|
+
"amount": 1200.0,
|
|
53
|
+
"currency": "INR",
|
|
54
|
+
"recipient": "merchant@store.com",
|
|
55
|
+
"description": "Autonomous grocery restocking order",
|
|
56
|
+
"prompt_context": "User prompt: Restock organic coffee beans.",
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
if decision.status == "ALLOW":
|
|
60
|
+
print(f"Action permitted! Policy version: {decision.policy_version}")
|
|
61
|
+
# Safe to initiate Razorpay order
|
|
62
|
+
elif decision.status == "HOLD":
|
|
63
|
+
print(f"Action held for human review: {decision.pending_approval_id}")
|
|
64
|
+
# Wait for operator in AgentGuard dashboard
|
|
65
|
+
else:
|
|
66
|
+
print(f"Blocked by AgentGuard: {decision.reason}")
|
|
67
|
+
for violation in decision.violations:
|
|
68
|
+
print(f" - {violation}")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Context Manager Usage
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
with AgentGuard(base_url="http://localhost:3001", api_key="ag_live_...") as guard:
|
|
75
|
+
decision = guard.evaluate(action="payment.create", amount=25.0)
|
|
76
|
+
if decision.is_allowed:
|
|
77
|
+
pass
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Strict Fail-Closed Guarantee
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
# evaluate_safely catches all timeouts, DNS failures, and gateway crashes,
|
|
84
|
+
# strictly returning an authoritative BLOCK decision so your code never fails open.
|
|
85
|
+
decision = guard.evaluate_safely(action="payment.create", amount=50.0)
|
|
86
|
+
if not decision.is_allowed:
|
|
87
|
+
print(f"Transaction denied: {decision.reason}")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT © AgentGuard Security Team
|
|
95
|
+
|
|
96
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
agentguard/__init__.py,sha256=vp0-ZYB4knyzevaZOYP5IJB65ivk5KXI58qwGvH7GVc,766
|
|
2
|
+
agentguard/client.py,sha256=wmBVsmqtZ6kwE-JsYAHLHzgkOtJao4gzNVmmyA2UsRI,11338
|
|
3
|
+
agentguard/exceptions.py,sha256=yWHSVNmWXa95x2ZLEiCb03G4UYs4IxRko4aNJjYpzPg,4108
|
|
4
|
+
agentguard/models.py,sha256=6F9iFPIpOgGvFH97GNej98uVKvOAqXQjMR4wSj74qC8,4277
|
|
5
|
+
agentguard/py.typed,sha256=jKVcLoqUzJAhSwO-U3ztJHFft9oHMcj4681w24jTnac,80
|
|
6
|
+
codesec_agentguard-1.0.0.dist-info/LICENSE,sha256=wrpoyW6mMh07hQj-FUo87mBKRJMC-ekqVTR5t7NmMEc,1088
|
|
7
|
+
codesec_agentguard-1.0.0.dist-info/METADATA,sha256=yoFYTbByf70_Jw_EGO5TN9lv8lEQBSxgDyXf4_cIsK4,2561
|
|
8
|
+
codesec_agentguard-1.0.0.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92
|
|
9
|
+
codesec_agentguard-1.0.0.dist-info/top_level.txt,sha256=rEW5sAnjxXRs63ja2psmsMRMJJfi_3EAxf7crl1nHyw,11
|
|
10
|
+
codesec_agentguard-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agentguard
|