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.
@@ -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()