zetro-sentinel-sdk 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.
- zetro_sentinel_sdk/__init__.py +63 -0
- zetro_sentinel_sdk/cli.py +201 -0
- zetro_sentinel_sdk/client.py +890 -0
- zetro_sentinel_sdk/exceptions.py +44 -0
- zetro_sentinel_sdk/models.py +174 -0
- zetro_sentinel_sdk/skills/__init__.py +1 -0
- zetro_sentinel_sdk/skills/setup-sentinel.md +386 -0
- zetro_sentinel_sdk-0.3.0.dist-info/METADATA +223 -0
- zetro_sentinel_sdk-0.3.0.dist-info/RECORD +12 -0
- zetro_sentinel_sdk-0.3.0.dist-info/WHEEL +5 -0
- zetro_sentinel_sdk-0.3.0.dist-info/entry_points.txt +2 -0
- zetro_sentinel_sdk-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
"""AI Sentinel SDK Client."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from zetro_sentinel_sdk.exceptions import (
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
NetworkError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
SentinelError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
)
|
|
14
|
+
from zetro_sentinel_sdk.models import (
|
|
15
|
+
ActionSourceResult,
|
|
16
|
+
AuthorizeResult,
|
|
17
|
+
HierarchyResult,
|
|
18
|
+
Incident,
|
|
19
|
+
IncidentList,
|
|
20
|
+
Policy,
|
|
21
|
+
RateLimitResult,
|
|
22
|
+
ScanResult,
|
|
23
|
+
ToolExecution,
|
|
24
|
+
ToolExecutionList,
|
|
25
|
+
ToolResultScanResult,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Sentinel:
|
|
30
|
+
"""
|
|
31
|
+
Synchronous client for AI Sentinel API.
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
sentinel = Sentinel(api_key="your-api-key")
|
|
35
|
+
|
|
36
|
+
# Scan user input
|
|
37
|
+
result = sentinel.scan_input("Hello world", agent_id="my-agent")
|
|
38
|
+
if not result.allowed:
|
|
39
|
+
print(f"Blocked: {result.reason}")
|
|
40
|
+
|
|
41
|
+
# Authorize tool call
|
|
42
|
+
auth = sentinel.authorize_tool(
|
|
43
|
+
agent_id="my-agent",
|
|
44
|
+
tool_name="send_email",
|
|
45
|
+
user_role="USER",
|
|
46
|
+
user_id="user-123"
|
|
47
|
+
)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
DEFAULT_BASE_URL = "https://api.aisentinel.io"
|
|
51
|
+
DEFAULT_TIMEOUT = 30.0
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
api_key: str,
|
|
56
|
+
base_url: str = None,
|
|
57
|
+
timeout: float = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Initialize the Sentinel client.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
api_key: Your AI Sentinel API key
|
|
64
|
+
base_url: API base URL (defaults to production)
|
|
65
|
+
timeout: Request timeout in seconds
|
|
66
|
+
"""
|
|
67
|
+
self.api_key = api_key
|
|
68
|
+
self.base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
|
|
69
|
+
self.timeout = timeout or self.DEFAULT_TIMEOUT
|
|
70
|
+
|
|
71
|
+
self._client = httpx.Client(
|
|
72
|
+
base_url=self.base_url,
|
|
73
|
+
headers={
|
|
74
|
+
"X-API-Key": api_key,
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
"User-Agent": "zetro-sentinel-sdk/0.3.0",
|
|
77
|
+
},
|
|
78
|
+
timeout=self.timeout,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def _handle_response(self, response: httpx.Response) -> Dict[str, Any]:
|
|
82
|
+
"""Handle API response and raise appropriate exceptions."""
|
|
83
|
+
if response.status_code == 401:
|
|
84
|
+
raise AuthenticationError(
|
|
85
|
+
"Invalid API key",
|
|
86
|
+
status_code=401,
|
|
87
|
+
response=response.json() if response.content else None,
|
|
88
|
+
)
|
|
89
|
+
elif response.status_code == 429:
|
|
90
|
+
retry_after = response.headers.get("Retry-After")
|
|
91
|
+
raise RateLimitError(
|
|
92
|
+
"Rate limit exceeded",
|
|
93
|
+
retry_after=int(retry_after) if retry_after else None,
|
|
94
|
+
response=response.json() if response.content else None,
|
|
95
|
+
)
|
|
96
|
+
elif response.status_code == 422:
|
|
97
|
+
data = response.json() if response.content else {}
|
|
98
|
+
raise ValidationError(
|
|
99
|
+
"Validation error",
|
|
100
|
+
errors=data.get("detail", []),
|
|
101
|
+
response=data,
|
|
102
|
+
)
|
|
103
|
+
elif response.status_code >= 400:
|
|
104
|
+
raise SentinelError(
|
|
105
|
+
f"API error: {response.status_code}",
|
|
106
|
+
status_code=response.status_code,
|
|
107
|
+
response=response.json() if response.content else None,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return response.json()
|
|
111
|
+
|
|
112
|
+
def _request(self, method: str, path: str, **kwargs) -> Dict[str, Any]:
|
|
113
|
+
"""Make an API request."""
|
|
114
|
+
try:
|
|
115
|
+
response = self._client.request(method, f"/v1{path}", **kwargs)
|
|
116
|
+
return self._handle_response(response)
|
|
117
|
+
except httpx.RequestError as e:
|
|
118
|
+
raise NetworkError(f"Network error: {str(e)}")
|
|
119
|
+
|
|
120
|
+
# =========================================================================
|
|
121
|
+
# SCANNING
|
|
122
|
+
# =========================================================================
|
|
123
|
+
|
|
124
|
+
def scan_input(
|
|
125
|
+
self,
|
|
126
|
+
text: str,
|
|
127
|
+
agent_id: str = "default",
|
|
128
|
+
session_id: str = None,
|
|
129
|
+
) -> ScanResult:
|
|
130
|
+
"""
|
|
131
|
+
Scan user input for prompt injection and policy violations.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
text: User input text to scan
|
|
135
|
+
agent_id: Agent identifier (for agent-specific policies)
|
|
136
|
+
session_id: Optional session ID for logging
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
ScanResult with allowed status and detection details
|
|
140
|
+
"""
|
|
141
|
+
data = self._request(
|
|
142
|
+
"POST",
|
|
143
|
+
"/scan/input",
|
|
144
|
+
json={
|
|
145
|
+
"text": text,
|
|
146
|
+
"agent_id": agent_id,
|
|
147
|
+
"session_id": session_id,
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
return ScanResult(**data)
|
|
151
|
+
|
|
152
|
+
def scan_output(
|
|
153
|
+
self,
|
|
154
|
+
text: str,
|
|
155
|
+
agent_id: str = "default",
|
|
156
|
+
session_id: str = None,
|
|
157
|
+
) -> ScanResult:
|
|
158
|
+
"""
|
|
159
|
+
Scan agent output for sensitive data leaks.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
text: Output text to scan
|
|
163
|
+
agent_id: Agent identifier
|
|
164
|
+
session_id: Optional session ID for logging
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
ScanResult with allowed status and any detected patterns
|
|
168
|
+
"""
|
|
169
|
+
data = self._request(
|
|
170
|
+
"POST",
|
|
171
|
+
"/scan/output",
|
|
172
|
+
json={
|
|
173
|
+
"text": text,
|
|
174
|
+
"agent_id": agent_id,
|
|
175
|
+
"session_id": session_id,
|
|
176
|
+
},
|
|
177
|
+
)
|
|
178
|
+
return ScanResult(**data)
|
|
179
|
+
|
|
180
|
+
def scan_tool_result(
|
|
181
|
+
self,
|
|
182
|
+
text: str,
|
|
183
|
+
tool_name: str,
|
|
184
|
+
agent_id: str = "default",
|
|
185
|
+
session_id: str = None,
|
|
186
|
+
) -> ToolResultScanResult:
|
|
187
|
+
"""
|
|
188
|
+
Scan tool result for indirect injection patterns.
|
|
189
|
+
|
|
190
|
+
Use this after executing external tool calls to detect
|
|
191
|
+
instruction patterns embedded in the response.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
text: Tool result text to scan
|
|
195
|
+
tool_name: Name of the tool that produced the result
|
|
196
|
+
agent_id: Agent identifier
|
|
197
|
+
session_id: Optional session ID for logging
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
ToolResultScanResult with detection details
|
|
201
|
+
"""
|
|
202
|
+
data = self._request(
|
|
203
|
+
"POST",
|
|
204
|
+
"/scan/tool-result",
|
|
205
|
+
json={
|
|
206
|
+
"text": text,
|
|
207
|
+
"tool_name": tool_name,
|
|
208
|
+
"agent_id": agent_id,
|
|
209
|
+
"session_id": session_id,
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
return ToolResultScanResult(**data)
|
|
213
|
+
|
|
214
|
+
# =========================================================================
|
|
215
|
+
# AUTHORIZATION
|
|
216
|
+
# =========================================================================
|
|
217
|
+
|
|
218
|
+
def authorize_tool(
|
|
219
|
+
self,
|
|
220
|
+
agent_id: str,
|
|
221
|
+
tool_name: str,
|
|
222
|
+
user_role: str,
|
|
223
|
+
user_id: str,
|
|
224
|
+
is_resource_owner: bool = True,
|
|
225
|
+
arguments: Dict[str, Any] = None,
|
|
226
|
+
session_id: str = None,
|
|
227
|
+
) -> AuthorizeResult:
|
|
228
|
+
"""
|
|
229
|
+
Authorize a tool call based on policy.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
agent_id: Agent identifier
|
|
233
|
+
tool_name: Tool to authorize
|
|
234
|
+
user_role: User's role (e.g., "USER", "ADMIN")
|
|
235
|
+
user_id: User identifier
|
|
236
|
+
is_resource_owner: Whether user owns the resource
|
|
237
|
+
arguments: Tool arguments (for HITL hash verification)
|
|
238
|
+
session_id: Optional session ID
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
AuthorizeResult with allowed status and approval details
|
|
242
|
+
"""
|
|
243
|
+
data = self._request(
|
|
244
|
+
"POST",
|
|
245
|
+
"/authorize/tool",
|
|
246
|
+
json={
|
|
247
|
+
"agent_id": agent_id,
|
|
248
|
+
"tool_name": tool_name,
|
|
249
|
+
"user_role": user_role,
|
|
250
|
+
"user_id": user_id,
|
|
251
|
+
"is_resource_owner": is_resource_owner,
|
|
252
|
+
"arguments": arguments,
|
|
253
|
+
"session_id": session_id,
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
return AuthorizeResult(**data)
|
|
257
|
+
|
|
258
|
+
def check_rate_limit(
|
|
259
|
+
self,
|
|
260
|
+
agent_id: str,
|
|
261
|
+
tool_name: str,
|
|
262
|
+
user_id: str,
|
|
263
|
+
) -> RateLimitResult:
|
|
264
|
+
"""
|
|
265
|
+
Check if a tool call is within rate limits.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
agent_id: Agent identifier
|
|
269
|
+
tool_name: Tool being called
|
|
270
|
+
user_id: User identifier
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
RateLimitResult with current counts and limits
|
|
274
|
+
"""
|
|
275
|
+
data = self._request(
|
|
276
|
+
"POST",
|
|
277
|
+
"/authorize/rate-limit",
|
|
278
|
+
json={
|
|
279
|
+
"agent_id": agent_id,
|
|
280
|
+
"tool_name": tool_name,
|
|
281
|
+
"user_id": user_id,
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
return RateLimitResult(**data)
|
|
285
|
+
|
|
286
|
+
def evaluate_action_source(
|
|
287
|
+
self,
|
|
288
|
+
agent_id: str,
|
|
289
|
+
user_message: str,
|
|
290
|
+
tool_name: str,
|
|
291
|
+
tool_arguments: Dict[str, Any],
|
|
292
|
+
tool_results: List[Dict] = None,
|
|
293
|
+
) -> ActionSourceResult:
|
|
294
|
+
"""
|
|
295
|
+
Evaluate whether an action was directly requested or data-derived.
|
|
296
|
+
|
|
297
|
+
Use this for indirect injection defense to determine if
|
|
298
|
+
a proposed action came from the user or external data.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
agent_id: Agent identifier
|
|
302
|
+
user_message: Original user request
|
|
303
|
+
tool_name: Tool being called
|
|
304
|
+
tool_arguments: Arguments to the tool
|
|
305
|
+
tool_results: Previous tool results with provenance
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
ActionSourceResult with source classification
|
|
309
|
+
"""
|
|
310
|
+
data = self._request(
|
|
311
|
+
"POST",
|
|
312
|
+
"/authorize/action-source",
|
|
313
|
+
json={
|
|
314
|
+
"agent_id": agent_id,
|
|
315
|
+
"user_message": user_message,
|
|
316
|
+
"tool_name": tool_name,
|
|
317
|
+
"tool_arguments": tool_arguments,
|
|
318
|
+
"tool_results": tool_results or [],
|
|
319
|
+
},
|
|
320
|
+
)
|
|
321
|
+
return ActionSourceResult(**data)
|
|
322
|
+
|
|
323
|
+
def check_hierarchy(
|
|
324
|
+
self,
|
|
325
|
+
agent_id: str,
|
|
326
|
+
user_message: str,
|
|
327
|
+
proposed_action: Dict[str, Any],
|
|
328
|
+
tool_results_with_instructions: List[Dict],
|
|
329
|
+
) -> HierarchyResult:
|
|
330
|
+
"""
|
|
331
|
+
Check if proposed action respects instruction hierarchy.
|
|
332
|
+
|
|
333
|
+
Verifies that external data is not overriding user instructions.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
agent_id: Agent identifier
|
|
337
|
+
user_message: Original user request
|
|
338
|
+
proposed_action: Action LLM wants to take
|
|
339
|
+
tool_results_with_instructions: Tool results flagged with instructions
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
HierarchyResult with violation details if any
|
|
343
|
+
"""
|
|
344
|
+
data = self._request(
|
|
345
|
+
"POST",
|
|
346
|
+
"/authorize/hierarchy",
|
|
347
|
+
json={
|
|
348
|
+
"agent_id": agent_id,
|
|
349
|
+
"user_message": user_message,
|
|
350
|
+
"proposed_action": proposed_action,
|
|
351
|
+
"tool_results_with_instructions": tool_results_with_instructions,
|
|
352
|
+
},
|
|
353
|
+
)
|
|
354
|
+
return HierarchyResult(**data)
|
|
355
|
+
|
|
356
|
+
# =========================================================================
|
|
357
|
+
# INCIDENTS
|
|
358
|
+
# =========================================================================
|
|
359
|
+
|
|
360
|
+
def list_incidents(
|
|
361
|
+
self,
|
|
362
|
+
page: int = 1,
|
|
363
|
+
page_size: int = 20,
|
|
364
|
+
severity: str = None,
|
|
365
|
+
category: str = None,
|
|
366
|
+
agent_id: str = None,
|
|
367
|
+
resolved: bool = None,
|
|
368
|
+
) -> IncidentList:
|
|
369
|
+
"""
|
|
370
|
+
List security incidents with filtering.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
page: Page number (1-indexed)
|
|
374
|
+
page_size: Items per page (max 100)
|
|
375
|
+
severity: Filter by severity
|
|
376
|
+
category: Filter by category
|
|
377
|
+
agent_id: Filter by agent
|
|
378
|
+
resolved: Filter by resolution status
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
IncidentList with paginated incidents
|
|
382
|
+
"""
|
|
383
|
+
params = {"page": page, "page_size": page_size}
|
|
384
|
+
if severity:
|
|
385
|
+
params["severity"] = severity
|
|
386
|
+
if category:
|
|
387
|
+
params["category"] = category
|
|
388
|
+
if agent_id:
|
|
389
|
+
params["agent_id"] = agent_id
|
|
390
|
+
if resolved is not None:
|
|
391
|
+
params["resolved"] = resolved
|
|
392
|
+
|
|
393
|
+
data = self._request("GET", "/incidents", params=params)
|
|
394
|
+
return IncidentList(**data)
|
|
395
|
+
|
|
396
|
+
def get_incident(self, incident_id: str) -> Incident:
|
|
397
|
+
"""
|
|
398
|
+
Get a specific incident by ID.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
incident_id: Incident ID
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Incident details
|
|
405
|
+
"""
|
|
406
|
+
data = self._request("GET", f"/incidents/{incident_id}")
|
|
407
|
+
return Incident(**data)
|
|
408
|
+
|
|
409
|
+
# =========================================================================
|
|
410
|
+
# POLICIES
|
|
411
|
+
# =========================================================================
|
|
412
|
+
|
|
413
|
+
def get_policy(self) -> Policy:
|
|
414
|
+
"""
|
|
415
|
+
Get the current security policy.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Policy configuration
|
|
419
|
+
"""
|
|
420
|
+
data = self._request("GET", "/policies")
|
|
421
|
+
return Policy(**data)
|
|
422
|
+
|
|
423
|
+
def toggle_agent(self, agent_id: str, enabled: bool, reason: str = None) -> Dict:
|
|
424
|
+
"""
|
|
425
|
+
Toggle the kill switch for an agent.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
agent_id: Agent identifier
|
|
429
|
+
enabled: Whether to enable or disable
|
|
430
|
+
reason: Reason for the change
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
Confirmation dict
|
|
434
|
+
"""
|
|
435
|
+
return self._request(
|
|
436
|
+
"POST",
|
|
437
|
+
f"/policies/kill-switch/agent/{agent_id}",
|
|
438
|
+
json={"enabled": enabled, "reason": reason},
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def toggle_tool(
|
|
442
|
+
self, agent_id: str, tool_name: str, enabled: bool, reason: str = None
|
|
443
|
+
) -> Dict:
|
|
444
|
+
"""
|
|
445
|
+
Toggle the kill switch for a specific tool.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
agent_id: Agent identifier
|
|
449
|
+
tool_name: Tool name
|
|
450
|
+
enabled: Whether to enable or disable
|
|
451
|
+
reason: Reason for the change
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Confirmation dict
|
|
455
|
+
"""
|
|
456
|
+
return self._request(
|
|
457
|
+
"POST",
|
|
458
|
+
f"/policies/kill-switch/tool/{agent_id}/{tool_name}",
|
|
459
|
+
json={"enabled": enabled, "reason": reason},
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# =========================================================================
|
|
463
|
+
# TOOL EXECUTIONS
|
|
464
|
+
# =========================================================================
|
|
465
|
+
|
|
466
|
+
def create_execution(
|
|
467
|
+
self,
|
|
468
|
+
agent_id: str,
|
|
469
|
+
tool_name: str,
|
|
470
|
+
user_id: str = None,
|
|
471
|
+
session_id: str = None,
|
|
472
|
+
tool_arguments: Dict[str, Any] = None,
|
|
473
|
+
argument_hash: str = None,
|
|
474
|
+
action_source: str = None,
|
|
475
|
+
) -> ToolExecution:
|
|
476
|
+
"""
|
|
477
|
+
Create a tool execution record to track a tool call.
|
|
478
|
+
|
|
479
|
+
Call this when starting a tool execution to begin tracking.
|
|
480
|
+
Use complete_execution() when the tool call finishes.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
agent_id: Agent making the tool call
|
|
484
|
+
tool_name: Name of the tool being called
|
|
485
|
+
user_id: Optional user identifier
|
|
486
|
+
session_id: Optional session identifier
|
|
487
|
+
tool_arguments: Arguments passed to the tool
|
|
488
|
+
argument_hash: Hash of arguments (for verification)
|
|
489
|
+
action_source: DIRECT_REQUEST, DATA_DERIVED, or HYBRID
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
ToolExecution with ID to use for completion
|
|
493
|
+
|
|
494
|
+
Example:
|
|
495
|
+
execution = sentinel.create_execution(
|
|
496
|
+
agent_id="my-agent",
|
|
497
|
+
tool_name="send_email",
|
|
498
|
+
user_id="user-123",
|
|
499
|
+
tool_arguments={"to": "user@example.com"}
|
|
500
|
+
)
|
|
501
|
+
try:
|
|
502
|
+
result = execute_tool(...)
|
|
503
|
+
sentinel.complete_execution(execution.id, "SUCCESS", result=result)
|
|
504
|
+
except Exception as e:
|
|
505
|
+
sentinel.complete_execution(execution.id, "FAILED", error=str(e))
|
|
506
|
+
"""
|
|
507
|
+
data = self._request(
|
|
508
|
+
"POST",
|
|
509
|
+
"/tool-executions",
|
|
510
|
+
json={
|
|
511
|
+
"agent_id": agent_id,
|
|
512
|
+
"tool_name": tool_name,
|
|
513
|
+
"user_id": user_id,
|
|
514
|
+
"session_id": session_id,
|
|
515
|
+
"tool_arguments": tool_arguments,
|
|
516
|
+
"argument_hash": argument_hash,
|
|
517
|
+
"action_source": action_source,
|
|
518
|
+
},
|
|
519
|
+
)
|
|
520
|
+
return ToolExecution(**data)
|
|
521
|
+
|
|
522
|
+
def complete_execution(
|
|
523
|
+
self,
|
|
524
|
+
execution_id: str,
|
|
525
|
+
status: str,
|
|
526
|
+
result: Dict[str, Any] = None,
|
|
527
|
+
error: str = None,
|
|
528
|
+
error_type: str = None,
|
|
529
|
+
) -> ToolExecution:
|
|
530
|
+
"""
|
|
531
|
+
Complete a tool execution record.
|
|
532
|
+
|
|
533
|
+
Call this when a tool call finishes to record the outcome.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
execution_id: Execution ID from create_execution()
|
|
537
|
+
status: Final status - one of:
|
|
538
|
+
- SUCCESS: Tool completed successfully
|
|
539
|
+
- FAILED: Tool encountered an error
|
|
540
|
+
- DENIED: Tool was denied by policy
|
|
541
|
+
- TIMEOUT: Tool execution timed out
|
|
542
|
+
- CANCELLED: Tool execution was cancelled
|
|
543
|
+
result: Optional result data (for SUCCESS)
|
|
544
|
+
error: Error message (for FAILED/TIMEOUT)
|
|
545
|
+
error_type: Type of error (e.g., "ValidationError")
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
Updated ToolExecution with completion details
|
|
549
|
+
"""
|
|
550
|
+
data = self._request(
|
|
551
|
+
"POST",
|
|
552
|
+
f"/tool-executions/{execution_id}/complete",
|
|
553
|
+
json={
|
|
554
|
+
"status": status,
|
|
555
|
+
"result": result,
|
|
556
|
+
"error": error,
|
|
557
|
+
"error_type": error_type,
|
|
558
|
+
},
|
|
559
|
+
)
|
|
560
|
+
return ToolExecution(**data)
|
|
561
|
+
|
|
562
|
+
def list_executions(
|
|
563
|
+
self,
|
|
564
|
+
page: int = 1,
|
|
565
|
+
page_size: int = 20,
|
|
566
|
+
tool_name: str = None,
|
|
567
|
+
status: str = None,
|
|
568
|
+
agent_id: str = None,
|
|
569
|
+
user_id: str = None,
|
|
570
|
+
session_id: str = None,
|
|
571
|
+
) -> ToolExecutionList:
|
|
572
|
+
"""
|
|
573
|
+
List tool executions with filtering.
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
page: Page number (1-indexed)
|
|
577
|
+
page_size: Items per page (max 100)
|
|
578
|
+
tool_name: Filter by tool name
|
|
579
|
+
status: Filter by status
|
|
580
|
+
agent_id: Filter by agent
|
|
581
|
+
user_id: Filter by user
|
|
582
|
+
session_id: Filter by session
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
ToolExecutionList with paginated executions
|
|
586
|
+
"""
|
|
587
|
+
params = {"page": page, "page_size": page_size}
|
|
588
|
+
if tool_name:
|
|
589
|
+
params["tool_name"] = tool_name
|
|
590
|
+
if status:
|
|
591
|
+
params["status"] = status
|
|
592
|
+
if agent_id:
|
|
593
|
+
params["agent_id"] = agent_id
|
|
594
|
+
if user_id:
|
|
595
|
+
params["user_id"] = user_id
|
|
596
|
+
if session_id:
|
|
597
|
+
params["session_id"] = session_id
|
|
598
|
+
|
|
599
|
+
data = self._request("GET", "/tool-executions", params=params)
|
|
600
|
+
return ToolExecutionList(**data)
|
|
601
|
+
|
|
602
|
+
def get_execution(self, execution_id: str) -> ToolExecution:
|
|
603
|
+
"""
|
|
604
|
+
Get a specific tool execution by ID.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
execution_id: Execution ID
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
ToolExecution details
|
|
611
|
+
"""
|
|
612
|
+
data = self._request("GET", f"/tool-executions/{execution_id}")
|
|
613
|
+
return ToolExecution(**data)
|
|
614
|
+
|
|
615
|
+
def close(self) -> None:
|
|
616
|
+
"""Close the HTTP client."""
|
|
617
|
+
self._client.close()
|
|
618
|
+
|
|
619
|
+
def __enter__(self):
|
|
620
|
+
return self
|
|
621
|
+
|
|
622
|
+
def __exit__(self, *args):
|
|
623
|
+
self.close()
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
class AsyncSentinel:
|
|
627
|
+
"""
|
|
628
|
+
Asynchronous client for AI Sentinel API.
|
|
629
|
+
|
|
630
|
+
Usage:
|
|
631
|
+
async with AsyncSentinel(api_key="your-api-key") as sentinel:
|
|
632
|
+
result = await sentinel.scan_input("Hello world")
|
|
633
|
+
"""
|
|
634
|
+
|
|
635
|
+
DEFAULT_BASE_URL = "https://api.aisentinel.io"
|
|
636
|
+
DEFAULT_TIMEOUT = 30.0
|
|
637
|
+
|
|
638
|
+
def __init__(
|
|
639
|
+
self,
|
|
640
|
+
api_key: str,
|
|
641
|
+
base_url: str = None,
|
|
642
|
+
timeout: float = None,
|
|
643
|
+
) -> None:
|
|
644
|
+
self.api_key = api_key
|
|
645
|
+
self.base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
|
|
646
|
+
self.timeout = timeout or self.DEFAULT_TIMEOUT
|
|
647
|
+
|
|
648
|
+
self._client = httpx.AsyncClient(
|
|
649
|
+
base_url=self.base_url,
|
|
650
|
+
headers={
|
|
651
|
+
"X-API-Key": api_key,
|
|
652
|
+
"Content-Type": "application/json",
|
|
653
|
+
"User-Agent": "zetro-sentinel-sdk/0.3.0",
|
|
654
|
+
},
|
|
655
|
+
timeout=self.timeout,
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
async def _handle_response(self, response: httpx.Response) -> Dict[str, Any]:
|
|
659
|
+
"""Handle API response and raise appropriate exceptions."""
|
|
660
|
+
if response.status_code == 401:
|
|
661
|
+
raise AuthenticationError("Invalid API key", status_code=401)
|
|
662
|
+
elif response.status_code == 429:
|
|
663
|
+
retry_after = response.headers.get("Retry-After")
|
|
664
|
+
raise RateLimitError(
|
|
665
|
+
"Rate limit exceeded",
|
|
666
|
+
retry_after=int(retry_after) if retry_after else None,
|
|
667
|
+
)
|
|
668
|
+
elif response.status_code == 422:
|
|
669
|
+
data = response.json() if response.content else {}
|
|
670
|
+
raise ValidationError("Validation error", errors=data.get("detail", []))
|
|
671
|
+
elif response.status_code >= 400:
|
|
672
|
+
raise SentinelError(f"API error: {response.status_code}", status_code=response.status_code)
|
|
673
|
+
|
|
674
|
+
return response.json()
|
|
675
|
+
|
|
676
|
+
async def _request(self, method: str, path: str, **kwargs) -> Dict[str, Any]:
|
|
677
|
+
"""Make an async API request."""
|
|
678
|
+
try:
|
|
679
|
+
response = await self._client.request(method, f"/v1{path}", **kwargs)
|
|
680
|
+
return await self._handle_response(response)
|
|
681
|
+
except httpx.RequestError as e:
|
|
682
|
+
raise NetworkError(f"Network error: {str(e)}")
|
|
683
|
+
|
|
684
|
+
async def scan_input(
|
|
685
|
+
self,
|
|
686
|
+
text: str,
|
|
687
|
+
agent_id: str = "default",
|
|
688
|
+
session_id: str = None,
|
|
689
|
+
) -> ScanResult:
|
|
690
|
+
"""Async version of scan_input."""
|
|
691
|
+
data = await self._request(
|
|
692
|
+
"POST",
|
|
693
|
+
"/scan/input",
|
|
694
|
+
json={"text": text, "agent_id": agent_id, "session_id": session_id},
|
|
695
|
+
)
|
|
696
|
+
return ScanResult(**data)
|
|
697
|
+
|
|
698
|
+
async def scan_output(
|
|
699
|
+
self,
|
|
700
|
+
text: str,
|
|
701
|
+
agent_id: str = "default",
|
|
702
|
+
session_id: str = None,
|
|
703
|
+
) -> ScanResult:
|
|
704
|
+
"""Async version of scan_output."""
|
|
705
|
+
data = await self._request(
|
|
706
|
+
"POST",
|
|
707
|
+
"/scan/output",
|
|
708
|
+
json={"text": text, "agent_id": agent_id, "session_id": session_id},
|
|
709
|
+
)
|
|
710
|
+
return ScanResult(**data)
|
|
711
|
+
|
|
712
|
+
async def scan_tool_result(
|
|
713
|
+
self,
|
|
714
|
+
text: str,
|
|
715
|
+
tool_name: str,
|
|
716
|
+
agent_id: str = "default",
|
|
717
|
+
session_id: str = None,
|
|
718
|
+
) -> ToolResultScanResult:
|
|
719
|
+
"""Async version of scan_tool_result."""
|
|
720
|
+
data = await self._request(
|
|
721
|
+
"POST",
|
|
722
|
+
"/scan/tool-result",
|
|
723
|
+
json={
|
|
724
|
+
"text": text,
|
|
725
|
+
"tool_name": tool_name,
|
|
726
|
+
"agent_id": agent_id,
|
|
727
|
+
"session_id": session_id,
|
|
728
|
+
},
|
|
729
|
+
)
|
|
730
|
+
return ToolResultScanResult(**data)
|
|
731
|
+
|
|
732
|
+
async def authorize_tool(
|
|
733
|
+
self,
|
|
734
|
+
agent_id: str,
|
|
735
|
+
tool_name: str,
|
|
736
|
+
user_role: str,
|
|
737
|
+
user_id: str,
|
|
738
|
+
is_resource_owner: bool = True,
|
|
739
|
+
arguments: Dict[str, Any] = None,
|
|
740
|
+
session_id: str = None,
|
|
741
|
+
) -> AuthorizeResult:
|
|
742
|
+
"""Async version of authorize_tool."""
|
|
743
|
+
data = await self._request(
|
|
744
|
+
"POST",
|
|
745
|
+
"/authorize/tool",
|
|
746
|
+
json={
|
|
747
|
+
"agent_id": agent_id,
|
|
748
|
+
"tool_name": tool_name,
|
|
749
|
+
"user_role": user_role,
|
|
750
|
+
"user_id": user_id,
|
|
751
|
+
"is_resource_owner": is_resource_owner,
|
|
752
|
+
"arguments": arguments,
|
|
753
|
+
"session_id": session_id,
|
|
754
|
+
},
|
|
755
|
+
)
|
|
756
|
+
return AuthorizeResult(**data)
|
|
757
|
+
|
|
758
|
+
async def evaluate_action_source(
|
|
759
|
+
self,
|
|
760
|
+
agent_id: str,
|
|
761
|
+
user_message: str,
|
|
762
|
+
tool_name: str,
|
|
763
|
+
tool_arguments: Dict[str, Any],
|
|
764
|
+
tool_results: List[Dict] = None,
|
|
765
|
+
) -> ActionSourceResult:
|
|
766
|
+
"""Async version of evaluate_action_source."""
|
|
767
|
+
data = await self._request(
|
|
768
|
+
"POST",
|
|
769
|
+
"/authorize/action-source",
|
|
770
|
+
json={
|
|
771
|
+
"agent_id": agent_id,
|
|
772
|
+
"user_message": user_message,
|
|
773
|
+
"tool_name": tool_name,
|
|
774
|
+
"tool_arguments": tool_arguments,
|
|
775
|
+
"tool_results": tool_results or [],
|
|
776
|
+
},
|
|
777
|
+
)
|
|
778
|
+
return ActionSourceResult(**data)
|
|
779
|
+
|
|
780
|
+
async def check_hierarchy(
|
|
781
|
+
self,
|
|
782
|
+
agent_id: str,
|
|
783
|
+
user_message: str,
|
|
784
|
+
proposed_action: Dict[str, Any],
|
|
785
|
+
tool_results_with_instructions: List[Dict],
|
|
786
|
+
) -> HierarchyResult:
|
|
787
|
+
"""Async version of check_hierarchy."""
|
|
788
|
+
data = await self._request(
|
|
789
|
+
"POST",
|
|
790
|
+
"/authorize/hierarchy",
|
|
791
|
+
json={
|
|
792
|
+
"agent_id": agent_id,
|
|
793
|
+
"user_message": user_message,
|
|
794
|
+
"proposed_action": proposed_action,
|
|
795
|
+
"tool_results_with_instructions": tool_results_with_instructions,
|
|
796
|
+
},
|
|
797
|
+
)
|
|
798
|
+
return HierarchyResult(**data)
|
|
799
|
+
|
|
800
|
+
# =========================================================================
|
|
801
|
+
# TOOL EXECUTIONS
|
|
802
|
+
# =========================================================================
|
|
803
|
+
|
|
804
|
+
async def create_execution(
|
|
805
|
+
self,
|
|
806
|
+
agent_id: str,
|
|
807
|
+
tool_name: str,
|
|
808
|
+
user_id: str = None,
|
|
809
|
+
session_id: str = None,
|
|
810
|
+
tool_arguments: Dict[str, Any] = None,
|
|
811
|
+
argument_hash: str = None,
|
|
812
|
+
action_source: str = None,
|
|
813
|
+
) -> ToolExecution:
|
|
814
|
+
"""Async version of create_execution."""
|
|
815
|
+
data = await self._request(
|
|
816
|
+
"POST",
|
|
817
|
+
"/tool-executions",
|
|
818
|
+
json={
|
|
819
|
+
"agent_id": agent_id,
|
|
820
|
+
"tool_name": tool_name,
|
|
821
|
+
"user_id": user_id,
|
|
822
|
+
"session_id": session_id,
|
|
823
|
+
"tool_arguments": tool_arguments,
|
|
824
|
+
"argument_hash": argument_hash,
|
|
825
|
+
"action_source": action_source,
|
|
826
|
+
},
|
|
827
|
+
)
|
|
828
|
+
return ToolExecution(**data)
|
|
829
|
+
|
|
830
|
+
async def complete_execution(
|
|
831
|
+
self,
|
|
832
|
+
execution_id: str,
|
|
833
|
+
status: str,
|
|
834
|
+
result: Dict[str, Any] = None,
|
|
835
|
+
error: str = None,
|
|
836
|
+
error_type: str = None,
|
|
837
|
+
) -> ToolExecution:
|
|
838
|
+
"""Async version of complete_execution."""
|
|
839
|
+
data = await self._request(
|
|
840
|
+
"POST",
|
|
841
|
+
f"/tool-executions/{execution_id}/complete",
|
|
842
|
+
json={
|
|
843
|
+
"status": status,
|
|
844
|
+
"result": result,
|
|
845
|
+
"error": error,
|
|
846
|
+
"error_type": error_type,
|
|
847
|
+
},
|
|
848
|
+
)
|
|
849
|
+
return ToolExecution(**data)
|
|
850
|
+
|
|
851
|
+
async def list_executions(
|
|
852
|
+
self,
|
|
853
|
+
page: int = 1,
|
|
854
|
+
page_size: int = 20,
|
|
855
|
+
tool_name: str = None,
|
|
856
|
+
status: str = None,
|
|
857
|
+
agent_id: str = None,
|
|
858
|
+
user_id: str = None,
|
|
859
|
+
session_id: str = None,
|
|
860
|
+
) -> ToolExecutionList:
|
|
861
|
+
"""Async version of list_executions."""
|
|
862
|
+
params = {"page": page, "page_size": page_size}
|
|
863
|
+
if tool_name:
|
|
864
|
+
params["tool_name"] = tool_name
|
|
865
|
+
if status:
|
|
866
|
+
params["status"] = status
|
|
867
|
+
if agent_id:
|
|
868
|
+
params["agent_id"] = agent_id
|
|
869
|
+
if user_id:
|
|
870
|
+
params["user_id"] = user_id
|
|
871
|
+
if session_id:
|
|
872
|
+
params["session_id"] = session_id
|
|
873
|
+
|
|
874
|
+
data = await self._request("GET", "/tool-executions", params=params)
|
|
875
|
+
return ToolExecutionList(**data)
|
|
876
|
+
|
|
877
|
+
async def get_execution(self, execution_id: str) -> ToolExecution:
|
|
878
|
+
"""Async version of get_execution."""
|
|
879
|
+
data = await self._request("GET", f"/tool-executions/{execution_id}")
|
|
880
|
+
return ToolExecution(**data)
|
|
881
|
+
|
|
882
|
+
async def close(self) -> None:
|
|
883
|
+
"""Close the HTTP client."""
|
|
884
|
+
await self._client.aclose()
|
|
885
|
+
|
|
886
|
+
async def __aenter__(self):
|
|
887
|
+
return self
|
|
888
|
+
|
|
889
|
+
async def __aexit__(self, *args):
|
|
890
|
+
await self.close()
|