axonflow 0.4.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.
- axonflow/__init__.py +140 -0
- axonflow/client.py +1612 -0
- axonflow/exceptions.py +103 -0
- axonflow/interceptors/__init__.py +20 -0
- axonflow/interceptors/anthropic.py +184 -0
- axonflow/interceptors/base.py +58 -0
- axonflow/interceptors/bedrock.py +231 -0
- axonflow/interceptors/gemini.py +281 -0
- axonflow/interceptors/ollama.py +253 -0
- axonflow/interceptors/openai.py +160 -0
- axonflow/policies.py +289 -0
- axonflow/py.typed +0 -0
- axonflow/types.py +214 -0
- axonflow/utils/__init__.py +12 -0
- axonflow/utils/cache.py +102 -0
- axonflow/utils/logging.py +89 -0
- axonflow/utils/retry.py +111 -0
- axonflow-0.4.0.dist-info/METADATA +316 -0
- axonflow-0.4.0.dist-info/RECORD +22 -0
- axonflow-0.4.0.dist-info/WHEEL +5 -0
- axonflow-0.4.0.dist-info/licenses/LICENSE +21 -0
- axonflow-0.4.0.dist-info/top_level.txt +1 -0
axonflow/client.py
ADDED
|
@@ -0,0 +1,1612 @@
|
|
|
1
|
+
"""AxonFlow SDK Main Client.
|
|
2
|
+
|
|
3
|
+
The primary interface for interacting with AxonFlow governance platform.
|
|
4
|
+
Supports both async and sync usage patterns.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from axonflow import AxonFlow
|
|
8
|
+
>>>
|
|
9
|
+
>>> # Async usage
|
|
10
|
+
>>> async with AxonFlow(agent_url="...", client_id="...", client_secret="...") as client:
|
|
11
|
+
... result = await client.execute_query("user-token", "What is AI?", "chat")
|
|
12
|
+
... print(result.data)
|
|
13
|
+
>>>
|
|
14
|
+
>>> # Sync usage
|
|
15
|
+
>>> client = AxonFlow.sync(agent_url="...", client_id="...", client_secret="...")
|
|
16
|
+
>>> result = client.execute_query("user-token", "What is AI?", "chat")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import hashlib
|
|
23
|
+
import re
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from typing import TYPE_CHECKING, Any
|
|
26
|
+
|
|
27
|
+
import httpx
|
|
28
|
+
import structlog
|
|
29
|
+
from cachetools import TTLCache
|
|
30
|
+
from tenacity import (
|
|
31
|
+
retry,
|
|
32
|
+
retry_if_exception_type,
|
|
33
|
+
stop_after_attempt,
|
|
34
|
+
wait_exponential,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from axonflow.exceptions import (
|
|
38
|
+
AuthenticationError,
|
|
39
|
+
AxonFlowError,
|
|
40
|
+
ConnectionError,
|
|
41
|
+
PolicyViolationError,
|
|
42
|
+
TimeoutError,
|
|
43
|
+
)
|
|
44
|
+
from axonflow.policies import (
|
|
45
|
+
CreateDynamicPolicyRequest,
|
|
46
|
+
CreatePolicyOverrideRequest,
|
|
47
|
+
CreateStaticPolicyRequest,
|
|
48
|
+
DynamicPolicy,
|
|
49
|
+
EffectivePoliciesOptions,
|
|
50
|
+
ListDynamicPoliciesOptions,
|
|
51
|
+
ListStaticPoliciesOptions,
|
|
52
|
+
PolicyCategory, # noqa: F401 - used in docstrings
|
|
53
|
+
PolicyOverride,
|
|
54
|
+
PolicyTier, # noqa: F401 - used in docstrings
|
|
55
|
+
PolicyVersion,
|
|
56
|
+
StaticPolicy,
|
|
57
|
+
TestPatternResult,
|
|
58
|
+
UpdateDynamicPolicyRequest,
|
|
59
|
+
UpdateStaticPolicyRequest,
|
|
60
|
+
)
|
|
61
|
+
from axonflow.types import (
|
|
62
|
+
AuditResult,
|
|
63
|
+
AxonFlowConfig,
|
|
64
|
+
CacheConfig,
|
|
65
|
+
ClientRequest,
|
|
66
|
+
ClientResponse,
|
|
67
|
+
ConnectorInstallRequest,
|
|
68
|
+
ConnectorMetadata,
|
|
69
|
+
ConnectorResponse,
|
|
70
|
+
Mode,
|
|
71
|
+
PlanExecutionResponse,
|
|
72
|
+
PlanResponse,
|
|
73
|
+
PlanStep,
|
|
74
|
+
PolicyApprovalResult,
|
|
75
|
+
RateLimitInfo,
|
|
76
|
+
RetryConfig,
|
|
77
|
+
TokenUsage,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if TYPE_CHECKING:
|
|
81
|
+
from types import TracebackType
|
|
82
|
+
|
|
83
|
+
logger = structlog.get_logger(__name__)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _parse_datetime(value: str) -> datetime:
|
|
87
|
+
"""Parse ISO format datetime string.
|
|
88
|
+
|
|
89
|
+
Python 3.9's fromisoformat() doesn't handle 'Z' suffix for UTC.
|
|
90
|
+
This helper replaces 'Z' with '+00:00' for compatibility.
|
|
91
|
+
|
|
92
|
+
Also handles nanosecond precision (9 digits) by truncating to microseconds (6 digits)
|
|
93
|
+
since Python's fromisoformat() only supports up to 6 fractional digits.
|
|
94
|
+
"""
|
|
95
|
+
if value.endswith("Z"):
|
|
96
|
+
value = value[:-1] + "+00:00"
|
|
97
|
+
|
|
98
|
+
# Python's fromisoformat only supports up to 6 fractional digits (microseconds)
|
|
99
|
+
# Truncate nanoseconds (9 digits) to microseconds (6 digits) if needed
|
|
100
|
+
value = re.sub(r"(\.\d{6})\d+", r"\1", value)
|
|
101
|
+
|
|
102
|
+
return datetime.fromisoformat(value)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class AxonFlow:
|
|
106
|
+
"""Main AxonFlow client for AI governance.
|
|
107
|
+
|
|
108
|
+
This client provides async-first API for interacting with AxonFlow Agent.
|
|
109
|
+
All methods are async by default, with sync wrappers available via `.sync()`.
|
|
110
|
+
|
|
111
|
+
Attributes:
|
|
112
|
+
config: Client configuration
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
__slots__ = ("_config", "_http_client", "_map_http_client", "_cache", "_logger")
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
agent_url: str,
|
|
120
|
+
client_id: str,
|
|
121
|
+
client_secret: str,
|
|
122
|
+
*,
|
|
123
|
+
license_key: str | None = None,
|
|
124
|
+
mode: Mode | str = Mode.PRODUCTION,
|
|
125
|
+
debug: bool = False,
|
|
126
|
+
timeout: float = 60.0,
|
|
127
|
+
map_timeout: float = 120.0,
|
|
128
|
+
insecure_skip_verify: bool = False,
|
|
129
|
+
retry_config: RetryConfig | None = None,
|
|
130
|
+
cache_enabled: bool = True,
|
|
131
|
+
cache_ttl: float = 60.0,
|
|
132
|
+
cache_max_size: int = 1000,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Initialize AxonFlow client.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
agent_url: AxonFlow Agent URL
|
|
138
|
+
client_id: Client ID for authentication
|
|
139
|
+
client_secret: Client secret for authentication
|
|
140
|
+
license_key: Optional license key for organization-level auth
|
|
141
|
+
mode: Operation mode (production or sandbox)
|
|
142
|
+
debug: Enable debug logging
|
|
143
|
+
timeout: Request timeout in seconds
|
|
144
|
+
map_timeout: Timeout for MAP operations in seconds (default: 120s)
|
|
145
|
+
MAP operations involve multiple LLM calls and need longer timeouts
|
|
146
|
+
insecure_skip_verify: Skip TLS verification (dev only)
|
|
147
|
+
retry_config: Retry configuration
|
|
148
|
+
cache_enabled: Enable response caching
|
|
149
|
+
cache_ttl: Cache TTL in seconds
|
|
150
|
+
cache_max_size: Maximum cache entries
|
|
151
|
+
"""
|
|
152
|
+
if isinstance(mode, str):
|
|
153
|
+
mode = Mode(mode)
|
|
154
|
+
|
|
155
|
+
self._config = AxonFlowConfig(
|
|
156
|
+
agent_url=agent_url.rstrip("/"),
|
|
157
|
+
client_id=client_id,
|
|
158
|
+
client_secret=client_secret,
|
|
159
|
+
license_key=license_key,
|
|
160
|
+
mode=mode,
|
|
161
|
+
debug=debug,
|
|
162
|
+
timeout=timeout,
|
|
163
|
+
map_timeout=map_timeout,
|
|
164
|
+
insecure_skip_verify=insecure_skip_verify,
|
|
165
|
+
retry=retry_config or RetryConfig(),
|
|
166
|
+
cache=CacheConfig(enabled=cache_enabled, ttl=cache_ttl, max_size=cache_max_size),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Configure SSL verification
|
|
170
|
+
verify_ssl: bool = not insecure_skip_verify
|
|
171
|
+
|
|
172
|
+
# Build headers
|
|
173
|
+
headers: dict[str, str] = {
|
|
174
|
+
"Content-Type": "application/json",
|
|
175
|
+
"X-Client-Secret": client_secret,
|
|
176
|
+
"X-Tenant-ID": client_id, # client_id is used as tenant ID for policy APIs
|
|
177
|
+
}
|
|
178
|
+
if license_key:
|
|
179
|
+
headers["X-License-Key"] = license_key
|
|
180
|
+
|
|
181
|
+
# Initialize HTTP client
|
|
182
|
+
self._http_client = httpx.AsyncClient(
|
|
183
|
+
timeout=httpx.Timeout(timeout),
|
|
184
|
+
verify=verify_ssl,
|
|
185
|
+
headers=headers,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Initialize MAP HTTP client with longer timeout
|
|
189
|
+
self._map_http_client = httpx.AsyncClient(
|
|
190
|
+
timeout=httpx.Timeout(map_timeout),
|
|
191
|
+
verify=verify_ssl,
|
|
192
|
+
headers=headers,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Initialize cache
|
|
196
|
+
self._cache: TTLCache[str, ClientResponse] | None = None
|
|
197
|
+
if cache_enabled:
|
|
198
|
+
self._cache = TTLCache(maxsize=cache_max_size, ttl=cache_ttl)
|
|
199
|
+
|
|
200
|
+
# Initialize logger
|
|
201
|
+
self._logger = structlog.get_logger(__name__).bind(
|
|
202
|
+
client_id=client_id,
|
|
203
|
+
mode=mode.value,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if debug:
|
|
207
|
+
self._logger.info(
|
|
208
|
+
"AxonFlow client initialized",
|
|
209
|
+
agent_url=agent_url,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def config(self) -> AxonFlowConfig:
|
|
214
|
+
"""Get client configuration."""
|
|
215
|
+
return self._config
|
|
216
|
+
|
|
217
|
+
async def __aenter__(self) -> AxonFlow:
|
|
218
|
+
"""Async context manager entry."""
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
async def __aexit__(
|
|
222
|
+
self,
|
|
223
|
+
exc_type: type[BaseException] | None,
|
|
224
|
+
exc_val: BaseException | None,
|
|
225
|
+
exc_tb: TracebackType | None,
|
|
226
|
+
) -> None:
|
|
227
|
+
"""Async context manager exit."""
|
|
228
|
+
await self.close()
|
|
229
|
+
|
|
230
|
+
async def close(self) -> None:
|
|
231
|
+
"""Close the HTTP clients."""
|
|
232
|
+
await self._http_client.aclose()
|
|
233
|
+
await self._map_http_client.aclose()
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
def sync(
|
|
237
|
+
cls,
|
|
238
|
+
agent_url: str,
|
|
239
|
+
client_id: str,
|
|
240
|
+
client_secret: str,
|
|
241
|
+
**kwargs: Any,
|
|
242
|
+
) -> SyncAxonFlow:
|
|
243
|
+
"""Create a synchronous client wrapper.
|
|
244
|
+
|
|
245
|
+
Example:
|
|
246
|
+
>>> client = AxonFlow.sync(agent_url="...", client_id="...", client_secret="...")
|
|
247
|
+
>>> result = client.execute_query("token", "query", "chat")
|
|
248
|
+
"""
|
|
249
|
+
return SyncAxonFlow(cls(agent_url, client_id, client_secret, **kwargs))
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def sandbox(cls, api_key: str = "demo-key") -> AxonFlow:
|
|
253
|
+
"""Create a sandbox client for testing.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
api_key: Optional API key (defaults to demo-key)
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Configured AxonFlow client for sandbox environment
|
|
260
|
+
"""
|
|
261
|
+
return cls(
|
|
262
|
+
agent_url="https://staging-eu.getaxonflow.com",
|
|
263
|
+
client_id=api_key,
|
|
264
|
+
client_secret=api_key,
|
|
265
|
+
mode=Mode.SANDBOX,
|
|
266
|
+
debug=True,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def _get_cache_key(self, request_type: str, query: str, user_token: str) -> str:
|
|
270
|
+
"""Generate cache key for a request."""
|
|
271
|
+
key = f"{request_type}:{query}:{user_token}"
|
|
272
|
+
return hashlib.sha256(key.encode()).hexdigest()[:32]
|
|
273
|
+
|
|
274
|
+
async def _request(
|
|
275
|
+
self,
|
|
276
|
+
method: str,
|
|
277
|
+
path: str,
|
|
278
|
+
*,
|
|
279
|
+
json_data: dict[str, Any] | None = None,
|
|
280
|
+
) -> dict[str, Any]:
|
|
281
|
+
"""Make HTTP request to Agent."""
|
|
282
|
+
url = f"{self._config.agent_url}{path}"
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
if self._config.retry.enabled:
|
|
286
|
+
response = await self._request_with_retry(method, url, json_data)
|
|
287
|
+
else:
|
|
288
|
+
response = await self._http_client.request(method, url, json=json_data)
|
|
289
|
+
|
|
290
|
+
response.raise_for_status()
|
|
291
|
+
# Handle 204 No Content (e.g., DELETE responses)
|
|
292
|
+
if response.status_code == 204: # noqa: PLR2004
|
|
293
|
+
return None # type: ignore[return-value]
|
|
294
|
+
return response.json() # type: ignore[no-any-return]
|
|
295
|
+
|
|
296
|
+
except httpx.ConnectError as e:
|
|
297
|
+
msg = f"Failed to connect to AxonFlow Agent: {e}"
|
|
298
|
+
raise ConnectionError(msg) from e
|
|
299
|
+
except httpx.TimeoutException as e:
|
|
300
|
+
msg = f"Request timed out: {e}"
|
|
301
|
+
raise TimeoutError(msg) from e
|
|
302
|
+
except httpx.HTTPStatusError as e:
|
|
303
|
+
if e.response.status_code == 401: # noqa: PLR2004
|
|
304
|
+
msg = "Invalid credentials"
|
|
305
|
+
raise AuthenticationError(msg) from e
|
|
306
|
+
if e.response.status_code == 403: # noqa: PLR2004
|
|
307
|
+
body = e.response.json()
|
|
308
|
+
# Extract policy from policy_info if available
|
|
309
|
+
policy = body.get("policy")
|
|
310
|
+
if not policy:
|
|
311
|
+
policy_info = body.get("policy_info")
|
|
312
|
+
if policy_info and policy_info.get("policies_evaluated"):
|
|
313
|
+
policy = policy_info["policies_evaluated"][0]
|
|
314
|
+
raise PolicyViolationError(
|
|
315
|
+
body.get("block_reason") or body.get("message", "Request blocked by policy"),
|
|
316
|
+
policy=policy,
|
|
317
|
+
block_reason=body.get("block_reason"),
|
|
318
|
+
) from e
|
|
319
|
+
msg = f"HTTP {e.response.status_code}: {e.response.text}"
|
|
320
|
+
raise AxonFlowError(msg) from e
|
|
321
|
+
|
|
322
|
+
async def _request_with_retry(
|
|
323
|
+
self,
|
|
324
|
+
method: str,
|
|
325
|
+
url: str,
|
|
326
|
+
json_data: dict[str, Any] | None,
|
|
327
|
+
) -> httpx.Response:
|
|
328
|
+
"""Make request with retry logic."""
|
|
329
|
+
|
|
330
|
+
@retry(
|
|
331
|
+
stop=stop_after_attempt(self._config.retry.max_attempts),
|
|
332
|
+
wait=wait_exponential(
|
|
333
|
+
multiplier=self._config.retry.initial_delay,
|
|
334
|
+
max=self._config.retry.max_delay,
|
|
335
|
+
exp_base=self._config.retry.exponential_base,
|
|
336
|
+
),
|
|
337
|
+
retry=retry_if_exception_type((httpx.ConnectError, httpx.TimeoutException)),
|
|
338
|
+
reraise=True,
|
|
339
|
+
)
|
|
340
|
+
async def _do_request() -> httpx.Response:
|
|
341
|
+
return await self._http_client.request(method, url, json=json_data)
|
|
342
|
+
|
|
343
|
+
return await _do_request()
|
|
344
|
+
|
|
345
|
+
async def _map_request(
|
|
346
|
+
self,
|
|
347
|
+
method: str,
|
|
348
|
+
path: str,
|
|
349
|
+
*,
|
|
350
|
+
json_data: dict[str, Any] | None = None,
|
|
351
|
+
) -> dict[str, Any]:
|
|
352
|
+
"""Make HTTP request to Agent using MAP timeout.
|
|
353
|
+
|
|
354
|
+
This uses the longer map_timeout for MAP operations that involve
|
|
355
|
+
multiple LLM calls and can take 30-60+ seconds.
|
|
356
|
+
"""
|
|
357
|
+
url = f"{self._config.agent_url}{path}"
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
if self._config.debug:
|
|
361
|
+
self._logger.debug(
|
|
362
|
+
"MAP request",
|
|
363
|
+
url=url,
|
|
364
|
+
timeout=self._config.map_timeout,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
response = await self._map_http_client.request(method, url, json=json_data)
|
|
368
|
+
response.raise_for_status()
|
|
369
|
+
return response.json() # type: ignore[no-any-return]
|
|
370
|
+
|
|
371
|
+
except httpx.ConnectError as e:
|
|
372
|
+
msg = f"Failed to connect to AxonFlow Agent: {e}"
|
|
373
|
+
raise ConnectionError(msg) from e
|
|
374
|
+
except httpx.TimeoutException as e:
|
|
375
|
+
msg = f"MAP request timed out after {self._config.map_timeout}s: {e}"
|
|
376
|
+
raise TimeoutError(msg) from e
|
|
377
|
+
except httpx.HTTPStatusError as e:
|
|
378
|
+
if e.response.status_code == 401: # noqa: PLR2004
|
|
379
|
+
msg = "Invalid credentials"
|
|
380
|
+
raise AuthenticationError(msg) from e
|
|
381
|
+
if e.response.status_code == 403: # noqa: PLR2004
|
|
382
|
+
body = e.response.json()
|
|
383
|
+
policy = body.get("policy")
|
|
384
|
+
if not policy:
|
|
385
|
+
policy_info = body.get("policy_info")
|
|
386
|
+
if policy_info and policy_info.get("policies_evaluated"):
|
|
387
|
+
policy = policy_info["policies_evaluated"][0]
|
|
388
|
+
raise PolicyViolationError(
|
|
389
|
+
body.get("block_reason") or body.get("message", "Request blocked by policy"),
|
|
390
|
+
policy=policy,
|
|
391
|
+
block_reason=body.get("block_reason"),
|
|
392
|
+
) from e
|
|
393
|
+
msg = f"HTTP {e.response.status_code}: {e.response.text}"
|
|
394
|
+
raise AxonFlowError(msg) from e
|
|
395
|
+
|
|
396
|
+
async def health_check(self) -> bool:
|
|
397
|
+
"""Check if AxonFlow Agent is healthy.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
True if agent is healthy, False otherwise
|
|
401
|
+
"""
|
|
402
|
+
try:
|
|
403
|
+
response = await self._request("GET", "/health")
|
|
404
|
+
return response.get("status") == "healthy"
|
|
405
|
+
except AxonFlowError:
|
|
406
|
+
return False
|
|
407
|
+
|
|
408
|
+
async def execute_query(
|
|
409
|
+
self,
|
|
410
|
+
user_token: str,
|
|
411
|
+
query: str,
|
|
412
|
+
request_type: str,
|
|
413
|
+
context: dict[str, Any] | None = None,
|
|
414
|
+
) -> ClientResponse:
|
|
415
|
+
"""Execute a query through AxonFlow with policy enforcement.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
user_token: User authentication token
|
|
419
|
+
query: The query or prompt
|
|
420
|
+
request_type: Type of request (chat, sql, mcp-query, multi-agent-plan)
|
|
421
|
+
context: Optional additional context
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
ClientResponse with results or error
|
|
425
|
+
|
|
426
|
+
Raises:
|
|
427
|
+
PolicyViolationError: If request is blocked by policy
|
|
428
|
+
AuthenticationError: If credentials are invalid
|
|
429
|
+
TimeoutError: If request times out
|
|
430
|
+
"""
|
|
431
|
+
# Check cache
|
|
432
|
+
if self._cache is not None:
|
|
433
|
+
cache_key = self._get_cache_key(request_type, query, user_token)
|
|
434
|
+
if cache_key in self._cache:
|
|
435
|
+
if self._config.debug:
|
|
436
|
+
self._logger.debug("Cache hit", query=query[:50])
|
|
437
|
+
cached_result: ClientResponse = self._cache[cache_key]
|
|
438
|
+
return cached_result
|
|
439
|
+
else:
|
|
440
|
+
cache_key = ""
|
|
441
|
+
|
|
442
|
+
request = ClientRequest(
|
|
443
|
+
query=query,
|
|
444
|
+
user_token=user_token,
|
|
445
|
+
client_id=self._config.client_id,
|
|
446
|
+
request_type=request_type,
|
|
447
|
+
context=context or {},
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
if self._config.debug:
|
|
451
|
+
self._logger.debug(
|
|
452
|
+
"Executing query",
|
|
453
|
+
request_type=request_type,
|
|
454
|
+
query=query[:50] if query else "",
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
response_data = await self._request(
|
|
458
|
+
"POST",
|
|
459
|
+
"/api/request",
|
|
460
|
+
json_data=request.model_dump(),
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
response = ClientResponse.model_validate(response_data)
|
|
464
|
+
|
|
465
|
+
# Check for policy violation
|
|
466
|
+
if response.blocked:
|
|
467
|
+
# Extract policy name from policy_info if available
|
|
468
|
+
policy = None
|
|
469
|
+
if response.policy_info and response.policy_info.policies_evaluated:
|
|
470
|
+
policy = response.policy_info.policies_evaluated[0]
|
|
471
|
+
raise PolicyViolationError(
|
|
472
|
+
response.block_reason or "Request blocked by policy",
|
|
473
|
+
policy=policy,
|
|
474
|
+
block_reason=response.block_reason,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Cache successful responses
|
|
478
|
+
if self._cache is not None and response.success and cache_key:
|
|
479
|
+
self._cache[cache_key] = response
|
|
480
|
+
|
|
481
|
+
return response
|
|
482
|
+
|
|
483
|
+
async def list_connectors(self) -> list[ConnectorMetadata]:
|
|
484
|
+
"""List all available MCP connectors.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
List of connector metadata
|
|
488
|
+
"""
|
|
489
|
+
response = await self._request("GET", "/api/connectors")
|
|
490
|
+
return [ConnectorMetadata.model_validate(c) for c in response]
|
|
491
|
+
|
|
492
|
+
async def install_connector(self, request: ConnectorInstallRequest) -> None:
|
|
493
|
+
"""Install an MCP connector.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
request: Connector installation request
|
|
497
|
+
"""
|
|
498
|
+
await self._request(
|
|
499
|
+
"POST",
|
|
500
|
+
"/api/connectors/install",
|
|
501
|
+
json_data=request.model_dump(),
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
if self._config.debug:
|
|
505
|
+
self._logger.info("Connector installed", name=request.name)
|
|
506
|
+
|
|
507
|
+
async def query_connector(
|
|
508
|
+
self,
|
|
509
|
+
user_token: str,
|
|
510
|
+
connector_name: str,
|
|
511
|
+
operation: str,
|
|
512
|
+
params: dict[str, Any] | None = None,
|
|
513
|
+
) -> ConnectorResponse:
|
|
514
|
+
"""Query an MCP connector directly.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
user_token: User authentication token
|
|
518
|
+
connector_name: Name of the connector
|
|
519
|
+
operation: Operation to perform
|
|
520
|
+
params: Operation parameters
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
ConnectorResponse with results
|
|
524
|
+
"""
|
|
525
|
+
request_data: dict[str, Any] = {
|
|
526
|
+
"client_id": self._config.client_id,
|
|
527
|
+
"user_token": user_token,
|
|
528
|
+
"connector": connector_name,
|
|
529
|
+
"operation": operation,
|
|
530
|
+
"parameters": params or {},
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if self._config.license_key:
|
|
534
|
+
request_data["license_key"] = self._config.license_key
|
|
535
|
+
|
|
536
|
+
response = await self._request(
|
|
537
|
+
"POST",
|
|
538
|
+
"/mcp/resources/query",
|
|
539
|
+
json_data=request_data,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
return ConnectorResponse.model_validate(response)
|
|
543
|
+
|
|
544
|
+
async def generate_plan(
|
|
545
|
+
self,
|
|
546
|
+
query: str,
|
|
547
|
+
domain: str | None = None,
|
|
548
|
+
user_token: str | None = None,
|
|
549
|
+
) -> PlanResponse:
|
|
550
|
+
"""Generate a multi-agent execution plan.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
query: Natural language query describing the task
|
|
554
|
+
domain: Optional domain hint (travel, healthcare, etc.)
|
|
555
|
+
user_token: Optional user token for authentication (defaults to client_id)
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
PlanResponse with generated plan
|
|
559
|
+
|
|
560
|
+
Note:
|
|
561
|
+
This uses map_timeout (default 120s) as MAP operations involve
|
|
562
|
+
multiple LLM calls and can take 30-60+ seconds.
|
|
563
|
+
"""
|
|
564
|
+
context = {"domain": domain} if domain else {}
|
|
565
|
+
|
|
566
|
+
request = ClientRequest(
|
|
567
|
+
query=query,
|
|
568
|
+
user_token=user_token or self._config.client_id,
|
|
569
|
+
client_id=self._config.client_id,
|
|
570
|
+
request_type="multi-agent-plan",
|
|
571
|
+
context=context,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
if self._config.debug:
|
|
575
|
+
self._logger.debug(
|
|
576
|
+
"Generating plan",
|
|
577
|
+
query=query[:50] if query else "",
|
|
578
|
+
domain=domain,
|
|
579
|
+
timeout=self._config.map_timeout,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
# Use MAP request with longer timeout
|
|
583
|
+
response_data = await self._map_request(
|
|
584
|
+
"POST",
|
|
585
|
+
"/api/request",
|
|
586
|
+
json_data=request.model_dump(),
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
response = ClientResponse.model_validate(response_data)
|
|
590
|
+
|
|
591
|
+
if not response.success:
|
|
592
|
+
msg = f"Plan generation failed: {response.error}"
|
|
593
|
+
raise AxonFlowError(msg)
|
|
594
|
+
|
|
595
|
+
# Extract steps from response data
|
|
596
|
+
steps: list[PlanStep] = []
|
|
597
|
+
if response.data and isinstance(response.data, dict):
|
|
598
|
+
steps_data = response.data.get("steps", [])
|
|
599
|
+
steps = [PlanStep.model_validate(s) for s in steps_data]
|
|
600
|
+
# Also check for plan_id in data
|
|
601
|
+
if not response.plan_id and response.data.get("plan_id"):
|
|
602
|
+
response = ClientResponse.model_validate(
|
|
603
|
+
{
|
|
604
|
+
**response_data,
|
|
605
|
+
"plan_id": response.data.get("plan_id"),
|
|
606
|
+
}
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
plan_id = response.plan_id or (
|
|
610
|
+
response.data.get("plan_id", "") if isinstance(response.data, dict) else ""
|
|
611
|
+
)
|
|
612
|
+
return PlanResponse(
|
|
613
|
+
plan_id=plan_id,
|
|
614
|
+
steps=steps,
|
|
615
|
+
domain=response.data.get("domain", domain or "generic")
|
|
616
|
+
if response.data and isinstance(response.data, dict)
|
|
617
|
+
else (domain or "generic"),
|
|
618
|
+
complexity=response.data.get("complexity", 0)
|
|
619
|
+
if response.data and isinstance(response.data, dict)
|
|
620
|
+
else 0,
|
|
621
|
+
parallel=response.data.get("parallel", False)
|
|
622
|
+
if response.data and isinstance(response.data, dict)
|
|
623
|
+
else False,
|
|
624
|
+
metadata=response.metadata,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
async def execute_plan(
|
|
628
|
+
self,
|
|
629
|
+
plan_id: str,
|
|
630
|
+
user_token: str | None = None,
|
|
631
|
+
) -> PlanExecutionResponse:
|
|
632
|
+
"""Execute a previously generated plan.
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
plan_id: ID of the plan to execute
|
|
636
|
+
user_token: Optional user token for authentication (defaults to client_id)
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
PlanExecutionResponse with results
|
|
640
|
+
|
|
641
|
+
Note:
|
|
642
|
+
This uses map_timeout (default 120s) as plan execution involves
|
|
643
|
+
multiple LLM calls and can take 30-60+ seconds.
|
|
644
|
+
"""
|
|
645
|
+
request = ClientRequest(
|
|
646
|
+
query="",
|
|
647
|
+
user_token=user_token or self._config.client_id,
|
|
648
|
+
client_id=self._config.client_id,
|
|
649
|
+
request_type="execute-plan",
|
|
650
|
+
context={"plan_id": plan_id},
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
if self._config.debug:
|
|
654
|
+
self._logger.debug(
|
|
655
|
+
"Executing plan",
|
|
656
|
+
plan_id=plan_id,
|
|
657
|
+
timeout=self._config.map_timeout,
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
# Use MAP request with longer timeout
|
|
661
|
+
response_data = await self._map_request(
|
|
662
|
+
"POST",
|
|
663
|
+
"/api/request",
|
|
664
|
+
json_data=request.model_dump(),
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
response = ClientResponse.model_validate(response_data)
|
|
668
|
+
|
|
669
|
+
return PlanExecutionResponse(
|
|
670
|
+
plan_id=plan_id,
|
|
671
|
+
status="completed" if response.success else "failed",
|
|
672
|
+
result=response.result,
|
|
673
|
+
step_results=response.metadata.get("step_results", {}),
|
|
674
|
+
error=response.error,
|
|
675
|
+
duration=response.metadata.get("duration"),
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
async def get_plan_status(self, plan_id: str) -> PlanExecutionResponse:
|
|
679
|
+
"""Get status of a running or completed plan.
|
|
680
|
+
|
|
681
|
+
Args:
|
|
682
|
+
plan_id: ID of the plan
|
|
683
|
+
|
|
684
|
+
Returns:
|
|
685
|
+
PlanExecutionResponse with current status
|
|
686
|
+
"""
|
|
687
|
+
response = await self._request("GET", f"/api/plans/{plan_id}")
|
|
688
|
+
return PlanExecutionResponse.model_validate(response)
|
|
689
|
+
|
|
690
|
+
# =========================================================================
|
|
691
|
+
# Gateway Mode Methods
|
|
692
|
+
# =========================================================================
|
|
693
|
+
|
|
694
|
+
async def get_policy_approved_context(
|
|
695
|
+
self,
|
|
696
|
+
user_token: str,
|
|
697
|
+
query: str,
|
|
698
|
+
data_sources: list[str] | None = None,
|
|
699
|
+
context: dict[str, Any] | None = None,
|
|
700
|
+
) -> PolicyApprovalResult:
|
|
701
|
+
"""Perform policy pre-check before making LLM call.
|
|
702
|
+
|
|
703
|
+
This is the first step in Gateway Mode. Call this before making your
|
|
704
|
+
LLM call to ensure policy compliance.
|
|
705
|
+
|
|
706
|
+
Args:
|
|
707
|
+
user_token: JWT token for the user making the request
|
|
708
|
+
query: The query/prompt that will be sent to the LLM
|
|
709
|
+
data_sources: Optional list of MCP connectors to fetch data from
|
|
710
|
+
context: Optional additional context for policy evaluation
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
PolicyApprovalResult with context ID and approved data
|
|
714
|
+
|
|
715
|
+
Raises:
|
|
716
|
+
AuthenticationError: If user token is invalid
|
|
717
|
+
ConnectionError: If unable to reach AxonFlow Agent
|
|
718
|
+
TimeoutError: If request times out
|
|
719
|
+
|
|
720
|
+
Example:
|
|
721
|
+
>>> result = await client.get_policy_approved_context(
|
|
722
|
+
... user_token="user-jwt",
|
|
723
|
+
... query="Find patients with diabetes",
|
|
724
|
+
... data_sources=["postgres"]
|
|
725
|
+
... )
|
|
726
|
+
>>> if not result.approved:
|
|
727
|
+
... raise PolicyViolationError(result.block_reason)
|
|
728
|
+
"""
|
|
729
|
+
request_body = {
|
|
730
|
+
"user_token": user_token,
|
|
731
|
+
"client_id": self._config.client_id,
|
|
732
|
+
"query": query,
|
|
733
|
+
"data_sources": data_sources or [],
|
|
734
|
+
"context": context or {},
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if self._config.debug:
|
|
738
|
+
self._logger.debug(
|
|
739
|
+
"Gateway pre-check request",
|
|
740
|
+
query=query[:50] if query else "",
|
|
741
|
+
data_sources=data_sources,
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
response = await self._request(
|
|
745
|
+
"POST",
|
|
746
|
+
"/api/policy/pre-check",
|
|
747
|
+
json_data=request_body,
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
if self._config.debug:
|
|
751
|
+
self._logger.debug(
|
|
752
|
+
"Gateway pre-check complete",
|
|
753
|
+
context_id=response.get("context_id"),
|
|
754
|
+
approved=response.get("approved"),
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
rate_limit = None
|
|
758
|
+
if response.get("rate_limit"):
|
|
759
|
+
rate_limit = RateLimitInfo(
|
|
760
|
+
limit=response["rate_limit"]["limit"],
|
|
761
|
+
remaining=response["rate_limit"]["remaining"],
|
|
762
|
+
reset_at=_parse_datetime(response["rate_limit"]["reset_at"]),
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
return PolicyApprovalResult(
|
|
766
|
+
context_id=response["context_id"],
|
|
767
|
+
approved=response["approved"],
|
|
768
|
+
approved_data=response.get("approved_data", {}),
|
|
769
|
+
policies=response.get("policies", []),
|
|
770
|
+
rate_limit_info=rate_limit,
|
|
771
|
+
expires_at=_parse_datetime(response["expires_at"]),
|
|
772
|
+
block_reason=response.get("block_reason"),
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
async def audit_llm_call(
|
|
776
|
+
self,
|
|
777
|
+
context_id: str,
|
|
778
|
+
response_summary: str,
|
|
779
|
+
provider: str,
|
|
780
|
+
model: str,
|
|
781
|
+
token_usage: TokenUsage,
|
|
782
|
+
latency_ms: int,
|
|
783
|
+
metadata: dict[str, Any] | None = None,
|
|
784
|
+
) -> AuditResult:
|
|
785
|
+
"""Report LLM call details for audit logging.
|
|
786
|
+
|
|
787
|
+
This is the second step in Gateway Mode. Call this after making your
|
|
788
|
+
LLM call to record it in the audit trail.
|
|
789
|
+
|
|
790
|
+
Args:
|
|
791
|
+
context_id: Context ID from get_policy_approved_context()
|
|
792
|
+
response_summary: Brief summary of the LLM response (not full response)
|
|
793
|
+
provider: LLM provider name (openai, anthropic, bedrock, ollama)
|
|
794
|
+
model: Model name (gpt-4, claude-3-sonnet, etc.)
|
|
795
|
+
token_usage: Token counts from the LLM response
|
|
796
|
+
latency_ms: Time taken for the LLM call in milliseconds
|
|
797
|
+
metadata: Optional additional metadata to log
|
|
798
|
+
|
|
799
|
+
Returns:
|
|
800
|
+
AuditResult confirming the audit was recorded
|
|
801
|
+
|
|
802
|
+
Raises:
|
|
803
|
+
AxonFlowError: If audit recording fails
|
|
804
|
+
|
|
805
|
+
Example:
|
|
806
|
+
>>> result = await client.audit_llm_call(
|
|
807
|
+
... context_id=ctx.context_id,
|
|
808
|
+
... response_summary="Found 5 patients with recent lab results",
|
|
809
|
+
... provider="openai",
|
|
810
|
+
... model="gpt-4",
|
|
811
|
+
... token_usage=TokenUsage(
|
|
812
|
+
... prompt_tokens=100,
|
|
813
|
+
... completion_tokens=50,
|
|
814
|
+
... total_tokens=150
|
|
815
|
+
... ),
|
|
816
|
+
... latency_ms=250
|
|
817
|
+
... )
|
|
818
|
+
"""
|
|
819
|
+
request_body = {
|
|
820
|
+
"context_id": context_id,
|
|
821
|
+
"client_id": self._config.client_id,
|
|
822
|
+
"response_summary": response_summary,
|
|
823
|
+
"provider": provider,
|
|
824
|
+
"model": model,
|
|
825
|
+
"token_usage": {
|
|
826
|
+
"prompt_tokens": token_usage.prompt_tokens,
|
|
827
|
+
"completion_tokens": token_usage.completion_tokens,
|
|
828
|
+
"total_tokens": token_usage.total_tokens,
|
|
829
|
+
},
|
|
830
|
+
"latency_ms": latency_ms,
|
|
831
|
+
"metadata": metadata or {},
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if self._config.debug:
|
|
835
|
+
self._logger.debug(
|
|
836
|
+
"Gateway audit request",
|
|
837
|
+
context_id=context_id,
|
|
838
|
+
provider=provider,
|
|
839
|
+
model=model,
|
|
840
|
+
tokens=token_usage.total_tokens,
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
response = await self._request(
|
|
844
|
+
"POST",
|
|
845
|
+
"/api/audit/llm-call",
|
|
846
|
+
json_data=request_body,
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
if self._config.debug:
|
|
850
|
+
self._logger.debug(
|
|
851
|
+
"Gateway audit complete",
|
|
852
|
+
audit_id=response.get("audit_id"),
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
return AuditResult(
|
|
856
|
+
success=response["success"],
|
|
857
|
+
audit_id=response["audit_id"],
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
# =========================================================================
|
|
861
|
+
# Policy CRUD Methods - Static Policies
|
|
862
|
+
# =========================================================================
|
|
863
|
+
|
|
864
|
+
async def list_static_policies(
|
|
865
|
+
self,
|
|
866
|
+
options: ListStaticPoliciesOptions | None = None,
|
|
867
|
+
) -> list[StaticPolicy]:
|
|
868
|
+
"""List all static policies with optional filtering.
|
|
869
|
+
|
|
870
|
+
Args:
|
|
871
|
+
options: Filtering and pagination options
|
|
872
|
+
|
|
873
|
+
Returns:
|
|
874
|
+
List of static policies
|
|
875
|
+
|
|
876
|
+
Example:
|
|
877
|
+
>>> policies = await client.list_static_policies(
|
|
878
|
+
... ListStaticPoliciesOptions(category=PolicyCategory.SECURITY_SQLI)
|
|
879
|
+
... )
|
|
880
|
+
"""
|
|
881
|
+
params: list[str] = []
|
|
882
|
+
if options:
|
|
883
|
+
if options.category:
|
|
884
|
+
params.append(f"category={options.category.value}")
|
|
885
|
+
if options.tier:
|
|
886
|
+
params.append(f"tier={options.tier.value}")
|
|
887
|
+
if options.enabled is not None:
|
|
888
|
+
params.append(f"enabled={str(options.enabled).lower()}")
|
|
889
|
+
if options.limit:
|
|
890
|
+
params.append(f"limit={options.limit}")
|
|
891
|
+
if options.offset:
|
|
892
|
+
params.append(f"offset={options.offset}")
|
|
893
|
+
if options.sort_by:
|
|
894
|
+
params.append(f"sort_by={options.sort_by}")
|
|
895
|
+
if options.sort_order:
|
|
896
|
+
params.append(f"sort_order={options.sort_order}")
|
|
897
|
+
if options.search:
|
|
898
|
+
params.append(f"search={options.search}")
|
|
899
|
+
|
|
900
|
+
path = "/api/v1/static-policies"
|
|
901
|
+
if params:
|
|
902
|
+
path = f"{path}?{'&'.join(params)}"
|
|
903
|
+
|
|
904
|
+
if self._config.debug:
|
|
905
|
+
self._logger.debug("Listing static policies", path=path)
|
|
906
|
+
|
|
907
|
+
response = await self._request("GET", path)
|
|
908
|
+
# Backend returns { policies: [], pagination: {} }, extract the policies array
|
|
909
|
+
policies = response.get("policies", []) if isinstance(response, dict) else response
|
|
910
|
+
return [StaticPolicy.model_validate(p) for p in policies]
|
|
911
|
+
|
|
912
|
+
async def get_static_policy(self, policy_id: str) -> StaticPolicy:
|
|
913
|
+
"""Get a specific static policy by ID.
|
|
914
|
+
|
|
915
|
+
Args:
|
|
916
|
+
policy_id: Policy ID
|
|
917
|
+
|
|
918
|
+
Returns:
|
|
919
|
+
The static policy
|
|
920
|
+
"""
|
|
921
|
+
if self._config.debug:
|
|
922
|
+
self._logger.debug("Getting static policy", policy_id=policy_id)
|
|
923
|
+
|
|
924
|
+
response = await self._request("GET", f"/api/v1/static-policies/{policy_id}")
|
|
925
|
+
return StaticPolicy.model_validate(response)
|
|
926
|
+
|
|
927
|
+
async def create_static_policy(
|
|
928
|
+
self,
|
|
929
|
+
request: CreateStaticPolicyRequest,
|
|
930
|
+
) -> StaticPolicy:
|
|
931
|
+
"""Create a new static policy.
|
|
932
|
+
|
|
933
|
+
Args:
|
|
934
|
+
request: Policy creation request
|
|
935
|
+
|
|
936
|
+
Returns:
|
|
937
|
+
The created policy
|
|
938
|
+
|
|
939
|
+
Example:
|
|
940
|
+
>>> policy = await client.create_static_policy(
|
|
941
|
+
... CreateStaticPolicyRequest(
|
|
942
|
+
... name="Block Credit Cards",
|
|
943
|
+
... category=PolicyCategory.PII_GLOBAL,
|
|
944
|
+
... pattern=r"\\b(?:\\d{4}[- ]?){3}\\d{4}\\b",
|
|
945
|
+
... severity=8
|
|
946
|
+
... )
|
|
947
|
+
... )
|
|
948
|
+
"""
|
|
949
|
+
if self._config.debug:
|
|
950
|
+
self._logger.debug("Creating static policy", name=request.name)
|
|
951
|
+
|
|
952
|
+
response = await self._request(
|
|
953
|
+
"POST",
|
|
954
|
+
"/api/v1/static-policies",
|
|
955
|
+
json_data=request.model_dump(exclude_none=True, by_alias=True),
|
|
956
|
+
)
|
|
957
|
+
return StaticPolicy.model_validate(response)
|
|
958
|
+
|
|
959
|
+
async def update_static_policy(
|
|
960
|
+
self,
|
|
961
|
+
policy_id: str,
|
|
962
|
+
request: UpdateStaticPolicyRequest,
|
|
963
|
+
) -> StaticPolicy:
|
|
964
|
+
"""Update an existing static policy.
|
|
965
|
+
|
|
966
|
+
Args:
|
|
967
|
+
policy_id: Policy ID
|
|
968
|
+
request: Fields to update
|
|
969
|
+
|
|
970
|
+
Returns:
|
|
971
|
+
The updated policy
|
|
972
|
+
"""
|
|
973
|
+
if self._config.debug:
|
|
974
|
+
self._logger.debug("Updating static policy", policy_id=policy_id)
|
|
975
|
+
|
|
976
|
+
response = await self._request(
|
|
977
|
+
"PUT",
|
|
978
|
+
f"/api/v1/static-policies/{policy_id}",
|
|
979
|
+
json_data=request.model_dump(exclude_none=True, by_alias=True),
|
|
980
|
+
)
|
|
981
|
+
return StaticPolicy.model_validate(response)
|
|
982
|
+
|
|
983
|
+
async def delete_static_policy(self, policy_id: str) -> None:
|
|
984
|
+
"""Delete a static policy.
|
|
985
|
+
|
|
986
|
+
Args:
|
|
987
|
+
policy_id: Policy ID
|
|
988
|
+
"""
|
|
989
|
+
if self._config.debug:
|
|
990
|
+
self._logger.debug("Deleting static policy", policy_id=policy_id)
|
|
991
|
+
|
|
992
|
+
await self._request("DELETE", f"/api/v1/static-policies/{policy_id}")
|
|
993
|
+
|
|
994
|
+
async def toggle_static_policy(
|
|
995
|
+
self,
|
|
996
|
+
policy_id: str,
|
|
997
|
+
enabled: bool,
|
|
998
|
+
) -> StaticPolicy:
|
|
999
|
+
"""Toggle a static policy's enabled status.
|
|
1000
|
+
|
|
1001
|
+
Args:
|
|
1002
|
+
policy_id: Policy ID
|
|
1003
|
+
enabled: Whether the policy should be enabled
|
|
1004
|
+
|
|
1005
|
+
Returns:
|
|
1006
|
+
The updated policy
|
|
1007
|
+
"""
|
|
1008
|
+
if self._config.debug:
|
|
1009
|
+
self._logger.debug("Toggling static policy", policy_id=policy_id, enabled=enabled)
|
|
1010
|
+
|
|
1011
|
+
response = await self._request(
|
|
1012
|
+
"PATCH",
|
|
1013
|
+
f"/api/v1/static-policies/{policy_id}",
|
|
1014
|
+
json_data={"enabled": enabled},
|
|
1015
|
+
)
|
|
1016
|
+
return StaticPolicy.model_validate(response)
|
|
1017
|
+
|
|
1018
|
+
async def get_effective_static_policies(
|
|
1019
|
+
self,
|
|
1020
|
+
options: EffectivePoliciesOptions | None = None,
|
|
1021
|
+
) -> list[StaticPolicy]:
|
|
1022
|
+
"""Get effective static policies with tier inheritance applied.
|
|
1023
|
+
|
|
1024
|
+
Args:
|
|
1025
|
+
options: Filtering options
|
|
1026
|
+
|
|
1027
|
+
Returns:
|
|
1028
|
+
List of effective policies
|
|
1029
|
+
"""
|
|
1030
|
+
query_params: list[str] = []
|
|
1031
|
+
if options:
|
|
1032
|
+
if options.category:
|
|
1033
|
+
query_params.append(f"category={options.category.value}")
|
|
1034
|
+
if options.include_disabled:
|
|
1035
|
+
query_params.append("include_disabled=true")
|
|
1036
|
+
if options.include_overridden:
|
|
1037
|
+
query_params.append("include_overridden=true")
|
|
1038
|
+
|
|
1039
|
+
path = "/api/v1/static-policies/effective"
|
|
1040
|
+
if query_params:
|
|
1041
|
+
path = f"{path}?{'&'.join(query_params)}"
|
|
1042
|
+
|
|
1043
|
+
if self._config.debug:
|
|
1044
|
+
self._logger.debug("Getting effective static policies", path=path)
|
|
1045
|
+
|
|
1046
|
+
response = await self._request("GET", path)
|
|
1047
|
+
# Backend returns { static: [], dynamic: [], ... }, extract the static array
|
|
1048
|
+
policies = response.get("static", []) if isinstance(response, dict) else response
|
|
1049
|
+
return [StaticPolicy.model_validate(p) for p in policies]
|
|
1050
|
+
|
|
1051
|
+
async def test_pattern(
|
|
1052
|
+
self,
|
|
1053
|
+
pattern: str,
|
|
1054
|
+
test_inputs: list[str],
|
|
1055
|
+
) -> TestPatternResult:
|
|
1056
|
+
"""Test a regex pattern against sample inputs.
|
|
1057
|
+
|
|
1058
|
+
Args:
|
|
1059
|
+
pattern: Regex pattern to test
|
|
1060
|
+
test_inputs: Array of strings to test against
|
|
1061
|
+
|
|
1062
|
+
Returns:
|
|
1063
|
+
Test results showing matches
|
|
1064
|
+
|
|
1065
|
+
Example:
|
|
1066
|
+
>>> result = await client.test_pattern(
|
|
1067
|
+
... r"\\b\\d{3}-\\d{2}-\\d{4}\\b",
|
|
1068
|
+
... ["SSN: 123-45-6789", "No SSN here"]
|
|
1069
|
+
... )
|
|
1070
|
+
"""
|
|
1071
|
+
if self._config.debug:
|
|
1072
|
+
self._logger.debug(
|
|
1073
|
+
"Testing pattern",
|
|
1074
|
+
pattern=pattern,
|
|
1075
|
+
input_count=len(test_inputs),
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
response = await self._request(
|
|
1079
|
+
"POST",
|
|
1080
|
+
"/api/v1/static-policies/test",
|
|
1081
|
+
json_data={"pattern": pattern, "inputs": test_inputs},
|
|
1082
|
+
)
|
|
1083
|
+
return TestPatternResult.model_validate(response)
|
|
1084
|
+
|
|
1085
|
+
async def get_static_policy_versions(
|
|
1086
|
+
self,
|
|
1087
|
+
policy_id: str,
|
|
1088
|
+
) -> list[PolicyVersion]:
|
|
1089
|
+
"""Get version history for a static policy.
|
|
1090
|
+
|
|
1091
|
+
Args:
|
|
1092
|
+
policy_id: Policy ID
|
|
1093
|
+
|
|
1094
|
+
Returns:
|
|
1095
|
+
Array of version history entries
|
|
1096
|
+
"""
|
|
1097
|
+
if self._config.debug:
|
|
1098
|
+
self._logger.debug("Getting static policy versions", policy_id=policy_id)
|
|
1099
|
+
|
|
1100
|
+
response = await self._request(
|
|
1101
|
+
"GET",
|
|
1102
|
+
f"/api/v1/static-policies/{policy_id}/versions",
|
|
1103
|
+
)
|
|
1104
|
+
return [PolicyVersion.model_validate(v) for v in response]
|
|
1105
|
+
|
|
1106
|
+
# =========================================================================
|
|
1107
|
+
# Policy Override Methods (Enterprise)
|
|
1108
|
+
# =========================================================================
|
|
1109
|
+
|
|
1110
|
+
async def create_policy_override(
|
|
1111
|
+
self,
|
|
1112
|
+
policy_id: str,
|
|
1113
|
+
request: CreatePolicyOverrideRequest,
|
|
1114
|
+
) -> PolicyOverride:
|
|
1115
|
+
"""Create an override for a static policy.
|
|
1116
|
+
|
|
1117
|
+
Args:
|
|
1118
|
+
policy_id: ID of the policy to override
|
|
1119
|
+
request: Override configuration
|
|
1120
|
+
|
|
1121
|
+
Returns:
|
|
1122
|
+
The created override
|
|
1123
|
+
|
|
1124
|
+
Example:
|
|
1125
|
+
>>> override = await client.create_policy_override(
|
|
1126
|
+
... "pol_123",
|
|
1127
|
+
... CreatePolicyOverrideRequest(
|
|
1128
|
+
... action=OverrideAction.WARN,
|
|
1129
|
+
... reason="Temporarily relaxing for migration"
|
|
1130
|
+
... )
|
|
1131
|
+
... )
|
|
1132
|
+
"""
|
|
1133
|
+
if self._config.debug:
|
|
1134
|
+
self._logger.debug(
|
|
1135
|
+
"Creating policy override",
|
|
1136
|
+
policy_id=policy_id,
|
|
1137
|
+
action=request.action.value,
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
response = await self._request(
|
|
1141
|
+
"POST",
|
|
1142
|
+
f"/api/v1/static-policies/{policy_id}/override",
|
|
1143
|
+
json_data=request.model_dump(exclude_none=True, by_alias=True),
|
|
1144
|
+
)
|
|
1145
|
+
return PolicyOverride.model_validate(response)
|
|
1146
|
+
|
|
1147
|
+
async def delete_policy_override(self, policy_id: str) -> None:
|
|
1148
|
+
"""Delete an override for a static policy.
|
|
1149
|
+
|
|
1150
|
+
Args:
|
|
1151
|
+
policy_id: ID of the policy whose override to delete
|
|
1152
|
+
"""
|
|
1153
|
+
if self._config.debug:
|
|
1154
|
+
self._logger.debug("Deleting policy override", policy_id=policy_id)
|
|
1155
|
+
|
|
1156
|
+
await self._request("DELETE", f"/api/v1/static-policies/{policy_id}/override")
|
|
1157
|
+
|
|
1158
|
+
# =========================================================================
|
|
1159
|
+
# Dynamic Policy Methods
|
|
1160
|
+
# =========================================================================
|
|
1161
|
+
|
|
1162
|
+
async def list_dynamic_policies(
|
|
1163
|
+
self,
|
|
1164
|
+
options: ListDynamicPoliciesOptions | None = None,
|
|
1165
|
+
) -> list[DynamicPolicy]:
|
|
1166
|
+
"""List all dynamic policies with optional filtering.
|
|
1167
|
+
|
|
1168
|
+
Args:
|
|
1169
|
+
options: Filtering and pagination options
|
|
1170
|
+
|
|
1171
|
+
Returns:
|
|
1172
|
+
List of dynamic policies
|
|
1173
|
+
"""
|
|
1174
|
+
params: list[str] = []
|
|
1175
|
+
if options:
|
|
1176
|
+
if options.category:
|
|
1177
|
+
params.append(f"category={options.category.value}")
|
|
1178
|
+
if options.tier:
|
|
1179
|
+
params.append(f"tier={options.tier.value}")
|
|
1180
|
+
if options.enabled is not None:
|
|
1181
|
+
params.append(f"enabled={str(options.enabled).lower()}")
|
|
1182
|
+
if options.limit:
|
|
1183
|
+
params.append(f"limit={options.limit}")
|
|
1184
|
+
if options.offset:
|
|
1185
|
+
params.append(f"offset={options.offset}")
|
|
1186
|
+
if options.sort_by:
|
|
1187
|
+
params.append(f"sort_by={options.sort_by}")
|
|
1188
|
+
if options.sort_order:
|
|
1189
|
+
params.append(f"sort_order={options.sort_order}")
|
|
1190
|
+
if options.search:
|
|
1191
|
+
params.append(f"search={options.search}")
|
|
1192
|
+
|
|
1193
|
+
path = "/api/v1/policies"
|
|
1194
|
+
if params:
|
|
1195
|
+
path = f"{path}?{'&'.join(params)}"
|
|
1196
|
+
|
|
1197
|
+
if self._config.debug:
|
|
1198
|
+
self._logger.debug("Listing dynamic policies", path=path)
|
|
1199
|
+
|
|
1200
|
+
response = await self._request("GET", path)
|
|
1201
|
+
return [DynamicPolicy.model_validate(p) for p in response]
|
|
1202
|
+
|
|
1203
|
+
async def get_dynamic_policy(self, policy_id: str) -> DynamicPolicy:
|
|
1204
|
+
"""Get a specific dynamic policy by ID.
|
|
1205
|
+
|
|
1206
|
+
Args:
|
|
1207
|
+
policy_id: Policy ID
|
|
1208
|
+
|
|
1209
|
+
Returns:
|
|
1210
|
+
The dynamic policy
|
|
1211
|
+
"""
|
|
1212
|
+
if self._config.debug:
|
|
1213
|
+
self._logger.debug("Getting dynamic policy", policy_id=policy_id)
|
|
1214
|
+
|
|
1215
|
+
response = await self._request("GET", f"/api/v1/policies/{policy_id}")
|
|
1216
|
+
return DynamicPolicy.model_validate(response)
|
|
1217
|
+
|
|
1218
|
+
async def create_dynamic_policy(
|
|
1219
|
+
self,
|
|
1220
|
+
request: CreateDynamicPolicyRequest,
|
|
1221
|
+
) -> DynamicPolicy:
|
|
1222
|
+
"""Create a new dynamic policy.
|
|
1223
|
+
|
|
1224
|
+
Args:
|
|
1225
|
+
request: Policy creation request
|
|
1226
|
+
|
|
1227
|
+
Returns:
|
|
1228
|
+
The created policy
|
|
1229
|
+
"""
|
|
1230
|
+
if self._config.debug:
|
|
1231
|
+
self._logger.debug("Creating dynamic policy", name=request.name)
|
|
1232
|
+
|
|
1233
|
+
response = await self._request(
|
|
1234
|
+
"POST",
|
|
1235
|
+
"/api/v1/policies",
|
|
1236
|
+
json_data=request.model_dump(exclude_none=True, by_alias=True),
|
|
1237
|
+
)
|
|
1238
|
+
return DynamicPolicy.model_validate(response)
|
|
1239
|
+
|
|
1240
|
+
async def update_dynamic_policy(
|
|
1241
|
+
self,
|
|
1242
|
+
policy_id: str,
|
|
1243
|
+
request: UpdateDynamicPolicyRequest,
|
|
1244
|
+
) -> DynamicPolicy:
|
|
1245
|
+
"""Update an existing dynamic policy.
|
|
1246
|
+
|
|
1247
|
+
Args:
|
|
1248
|
+
policy_id: Policy ID
|
|
1249
|
+
request: Fields to update
|
|
1250
|
+
|
|
1251
|
+
Returns:
|
|
1252
|
+
The updated policy
|
|
1253
|
+
"""
|
|
1254
|
+
if self._config.debug:
|
|
1255
|
+
self._logger.debug("Updating dynamic policy", policy_id=policy_id)
|
|
1256
|
+
|
|
1257
|
+
response = await self._request(
|
|
1258
|
+
"PUT",
|
|
1259
|
+
f"/api/v1/policies/{policy_id}",
|
|
1260
|
+
json_data=request.model_dump(exclude_none=True, by_alias=True),
|
|
1261
|
+
)
|
|
1262
|
+
return DynamicPolicy.model_validate(response)
|
|
1263
|
+
|
|
1264
|
+
async def delete_dynamic_policy(self, policy_id: str) -> None:
|
|
1265
|
+
"""Delete a dynamic policy.
|
|
1266
|
+
|
|
1267
|
+
Args:
|
|
1268
|
+
policy_id: Policy ID
|
|
1269
|
+
"""
|
|
1270
|
+
if self._config.debug:
|
|
1271
|
+
self._logger.debug("Deleting dynamic policy", policy_id=policy_id)
|
|
1272
|
+
|
|
1273
|
+
await self._request("DELETE", f"/api/v1/policies/{policy_id}")
|
|
1274
|
+
|
|
1275
|
+
async def toggle_dynamic_policy(
|
|
1276
|
+
self,
|
|
1277
|
+
policy_id: str,
|
|
1278
|
+
enabled: bool,
|
|
1279
|
+
) -> DynamicPolicy:
|
|
1280
|
+
"""Toggle a dynamic policy's enabled status.
|
|
1281
|
+
|
|
1282
|
+
Args:
|
|
1283
|
+
policy_id: Policy ID
|
|
1284
|
+
enabled: Whether the policy should be enabled
|
|
1285
|
+
|
|
1286
|
+
Returns:
|
|
1287
|
+
The updated policy
|
|
1288
|
+
"""
|
|
1289
|
+
if self._config.debug:
|
|
1290
|
+
self._logger.debug("Toggling dynamic policy", policy_id=policy_id, enabled=enabled)
|
|
1291
|
+
|
|
1292
|
+
response = await self._request(
|
|
1293
|
+
"PATCH",
|
|
1294
|
+
f"/api/v1/policies/{policy_id}",
|
|
1295
|
+
json_data={"enabled": enabled},
|
|
1296
|
+
)
|
|
1297
|
+
return DynamicPolicy.model_validate(response)
|
|
1298
|
+
|
|
1299
|
+
async def get_effective_dynamic_policies(
|
|
1300
|
+
self,
|
|
1301
|
+
options: EffectivePoliciesOptions | None = None,
|
|
1302
|
+
) -> list[DynamicPolicy]:
|
|
1303
|
+
"""Get effective dynamic policies with tier inheritance applied.
|
|
1304
|
+
|
|
1305
|
+
Args:
|
|
1306
|
+
options: Filtering options
|
|
1307
|
+
|
|
1308
|
+
Returns:
|
|
1309
|
+
List of effective dynamic policies
|
|
1310
|
+
"""
|
|
1311
|
+
query_params: list[str] = []
|
|
1312
|
+
if options:
|
|
1313
|
+
if options.category:
|
|
1314
|
+
query_params.append(f"category={options.category.value}")
|
|
1315
|
+
if options.include_disabled:
|
|
1316
|
+
query_params.append("include_disabled=true")
|
|
1317
|
+
|
|
1318
|
+
path = "/api/v1/policies/effective"
|
|
1319
|
+
if query_params:
|
|
1320
|
+
path = f"{path}?{'&'.join(query_params)}"
|
|
1321
|
+
|
|
1322
|
+
if self._config.debug:
|
|
1323
|
+
self._logger.debug("Getting effective dynamic policies", path=path)
|
|
1324
|
+
|
|
1325
|
+
response = await self._request("GET", path)
|
|
1326
|
+
return [DynamicPolicy.model_validate(p) for p in response]
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
class SyncAxonFlow:
|
|
1330
|
+
"""Synchronous wrapper for AxonFlow client.
|
|
1331
|
+
|
|
1332
|
+
Wraps all async methods for synchronous usage.
|
|
1333
|
+
"""
|
|
1334
|
+
|
|
1335
|
+
__slots__ = ("_async_client", "_loop")
|
|
1336
|
+
|
|
1337
|
+
def __init__(self, async_client: AxonFlow) -> None:
|
|
1338
|
+
self._async_client = async_client
|
|
1339
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
1340
|
+
|
|
1341
|
+
def _get_loop(self) -> asyncio.AbstractEventLoop:
|
|
1342
|
+
"""Get or create event loop."""
|
|
1343
|
+
if self._loop is None or self._loop.is_closed():
|
|
1344
|
+
try:
|
|
1345
|
+
self._loop = asyncio.get_event_loop()
|
|
1346
|
+
except RuntimeError:
|
|
1347
|
+
self._loop = asyncio.new_event_loop()
|
|
1348
|
+
asyncio.set_event_loop(self._loop)
|
|
1349
|
+
return self._loop
|
|
1350
|
+
|
|
1351
|
+
def __enter__(self) -> SyncAxonFlow:
|
|
1352
|
+
return self
|
|
1353
|
+
|
|
1354
|
+
def __exit__(
|
|
1355
|
+
self,
|
|
1356
|
+
exc_type: type[BaseException] | None,
|
|
1357
|
+
exc_val: BaseException | None,
|
|
1358
|
+
exc_tb: TracebackType | None,
|
|
1359
|
+
) -> None:
|
|
1360
|
+
self.close()
|
|
1361
|
+
|
|
1362
|
+
def close(self) -> None:
|
|
1363
|
+
"""Close the client."""
|
|
1364
|
+
self._get_loop().run_until_complete(self._async_client.close())
|
|
1365
|
+
|
|
1366
|
+
@property
|
|
1367
|
+
def config(self) -> AxonFlowConfig:
|
|
1368
|
+
"""Get client configuration."""
|
|
1369
|
+
return self._async_client.config
|
|
1370
|
+
|
|
1371
|
+
def health_check(self) -> bool:
|
|
1372
|
+
"""Check if AxonFlow Agent is healthy."""
|
|
1373
|
+
return self._get_loop().run_until_complete(self._async_client.health_check())
|
|
1374
|
+
|
|
1375
|
+
def execute_query(
|
|
1376
|
+
self,
|
|
1377
|
+
user_token: str,
|
|
1378
|
+
query: str,
|
|
1379
|
+
request_type: str,
|
|
1380
|
+
context: dict[str, Any] | None = None,
|
|
1381
|
+
) -> ClientResponse:
|
|
1382
|
+
"""Execute a query through AxonFlow."""
|
|
1383
|
+
return self._get_loop().run_until_complete(
|
|
1384
|
+
self._async_client.execute_query(user_token, query, request_type, context)
|
|
1385
|
+
)
|
|
1386
|
+
|
|
1387
|
+
def list_connectors(self) -> list[ConnectorMetadata]:
|
|
1388
|
+
"""List all available MCP connectors."""
|
|
1389
|
+
return self._get_loop().run_until_complete(self._async_client.list_connectors())
|
|
1390
|
+
|
|
1391
|
+
def install_connector(self, request: ConnectorInstallRequest) -> None:
|
|
1392
|
+
"""Install an MCP connector."""
|
|
1393
|
+
return self._get_loop().run_until_complete(self._async_client.install_connector(request))
|
|
1394
|
+
|
|
1395
|
+
def query_connector(
|
|
1396
|
+
self,
|
|
1397
|
+
user_token: str,
|
|
1398
|
+
connector_name: str,
|
|
1399
|
+
operation: str,
|
|
1400
|
+
params: dict[str, Any] | None = None,
|
|
1401
|
+
) -> ConnectorResponse:
|
|
1402
|
+
"""Query an MCP connector directly."""
|
|
1403
|
+
return self._get_loop().run_until_complete(
|
|
1404
|
+
self._async_client.query_connector(user_token, connector_name, operation, params)
|
|
1405
|
+
)
|
|
1406
|
+
|
|
1407
|
+
def generate_plan(
|
|
1408
|
+
self,
|
|
1409
|
+
query: str,
|
|
1410
|
+
domain: str | None = None,
|
|
1411
|
+
user_token: str | None = None,
|
|
1412
|
+
) -> PlanResponse:
|
|
1413
|
+
"""Generate a multi-agent execution plan."""
|
|
1414
|
+
return self._get_loop().run_until_complete(
|
|
1415
|
+
self._async_client.generate_plan(query, domain, user_token)
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
def execute_plan(
|
|
1419
|
+
self,
|
|
1420
|
+
plan_id: str,
|
|
1421
|
+
user_token: str | None = None,
|
|
1422
|
+
) -> PlanExecutionResponse:
|
|
1423
|
+
"""Execute a previously generated plan."""
|
|
1424
|
+
return self._get_loop().run_until_complete(
|
|
1425
|
+
self._async_client.execute_plan(plan_id, user_token)
|
|
1426
|
+
)
|
|
1427
|
+
|
|
1428
|
+
def get_plan_status(self, plan_id: str) -> PlanExecutionResponse:
|
|
1429
|
+
"""Get status of a running or completed plan."""
|
|
1430
|
+
return self._get_loop().run_until_complete(self._async_client.get_plan_status(plan_id))
|
|
1431
|
+
|
|
1432
|
+
# Gateway Mode sync wrappers
|
|
1433
|
+
|
|
1434
|
+
def get_policy_approved_context(
|
|
1435
|
+
self,
|
|
1436
|
+
user_token: str,
|
|
1437
|
+
query: str,
|
|
1438
|
+
data_sources: list[str] | None = None,
|
|
1439
|
+
context: dict[str, Any] | None = None,
|
|
1440
|
+
) -> PolicyApprovalResult:
|
|
1441
|
+
"""Perform policy pre-check before making LLM call."""
|
|
1442
|
+
return self._get_loop().run_until_complete(
|
|
1443
|
+
self._async_client.get_policy_approved_context(user_token, query, data_sources, context)
|
|
1444
|
+
)
|
|
1445
|
+
|
|
1446
|
+
def audit_llm_call(
|
|
1447
|
+
self,
|
|
1448
|
+
context_id: str,
|
|
1449
|
+
response_summary: str,
|
|
1450
|
+
provider: str,
|
|
1451
|
+
model: str,
|
|
1452
|
+
token_usage: TokenUsage,
|
|
1453
|
+
latency_ms: int,
|
|
1454
|
+
metadata: dict[str, Any] | None = None,
|
|
1455
|
+
) -> AuditResult:
|
|
1456
|
+
"""Report LLM call details for audit logging."""
|
|
1457
|
+
return self._get_loop().run_until_complete(
|
|
1458
|
+
self._async_client.audit_llm_call(
|
|
1459
|
+
context_id, response_summary, provider, model, token_usage, latency_ms, metadata
|
|
1460
|
+
)
|
|
1461
|
+
)
|
|
1462
|
+
|
|
1463
|
+
# Policy CRUD sync wrappers
|
|
1464
|
+
|
|
1465
|
+
def list_static_policies(
|
|
1466
|
+
self,
|
|
1467
|
+
options: ListStaticPoliciesOptions | None = None,
|
|
1468
|
+
) -> list[StaticPolicy]:
|
|
1469
|
+
"""List all static policies with optional filtering."""
|
|
1470
|
+
return self._get_loop().run_until_complete(self._async_client.list_static_policies(options))
|
|
1471
|
+
|
|
1472
|
+
def get_static_policy(self, policy_id: str) -> StaticPolicy:
|
|
1473
|
+
"""Get a specific static policy by ID."""
|
|
1474
|
+
return self._get_loop().run_until_complete(self._async_client.get_static_policy(policy_id))
|
|
1475
|
+
|
|
1476
|
+
def create_static_policy(
|
|
1477
|
+
self,
|
|
1478
|
+
request: CreateStaticPolicyRequest,
|
|
1479
|
+
) -> StaticPolicy:
|
|
1480
|
+
"""Create a new static policy."""
|
|
1481
|
+
return self._get_loop().run_until_complete(self._async_client.create_static_policy(request))
|
|
1482
|
+
|
|
1483
|
+
def update_static_policy(
|
|
1484
|
+
self,
|
|
1485
|
+
policy_id: str,
|
|
1486
|
+
request: UpdateStaticPolicyRequest,
|
|
1487
|
+
) -> StaticPolicy:
|
|
1488
|
+
"""Update an existing static policy."""
|
|
1489
|
+
return self._get_loop().run_until_complete(
|
|
1490
|
+
self._async_client.update_static_policy(policy_id, request)
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
def delete_static_policy(self, policy_id: str) -> None:
|
|
1494
|
+
"""Delete a static policy."""
|
|
1495
|
+
return self._get_loop().run_until_complete(
|
|
1496
|
+
self._async_client.delete_static_policy(policy_id)
|
|
1497
|
+
)
|
|
1498
|
+
|
|
1499
|
+
def toggle_static_policy(
|
|
1500
|
+
self,
|
|
1501
|
+
policy_id: str,
|
|
1502
|
+
enabled: bool,
|
|
1503
|
+
) -> StaticPolicy:
|
|
1504
|
+
"""Toggle a static policy's enabled status."""
|
|
1505
|
+
return self._get_loop().run_until_complete(
|
|
1506
|
+
self._async_client.toggle_static_policy(policy_id, enabled)
|
|
1507
|
+
)
|
|
1508
|
+
|
|
1509
|
+
def get_effective_static_policies(
|
|
1510
|
+
self,
|
|
1511
|
+
options: EffectivePoliciesOptions | None = None,
|
|
1512
|
+
) -> list[StaticPolicy]:
|
|
1513
|
+
"""Get effective static policies with tier inheritance applied."""
|
|
1514
|
+
return self._get_loop().run_until_complete(
|
|
1515
|
+
self._async_client.get_effective_static_policies(options)
|
|
1516
|
+
)
|
|
1517
|
+
|
|
1518
|
+
def test_pattern(
|
|
1519
|
+
self,
|
|
1520
|
+
pattern: str,
|
|
1521
|
+
test_inputs: list[str],
|
|
1522
|
+
) -> TestPatternResult:
|
|
1523
|
+
"""Test a regex pattern against sample inputs."""
|
|
1524
|
+
return self._get_loop().run_until_complete(
|
|
1525
|
+
self._async_client.test_pattern(pattern, test_inputs)
|
|
1526
|
+
)
|
|
1527
|
+
|
|
1528
|
+
def get_static_policy_versions(
|
|
1529
|
+
self,
|
|
1530
|
+
policy_id: str,
|
|
1531
|
+
) -> list[PolicyVersion]:
|
|
1532
|
+
"""Get version history for a static policy."""
|
|
1533
|
+
return self._get_loop().run_until_complete(
|
|
1534
|
+
self._async_client.get_static_policy_versions(policy_id)
|
|
1535
|
+
)
|
|
1536
|
+
|
|
1537
|
+
# Policy override sync wrappers
|
|
1538
|
+
|
|
1539
|
+
def create_policy_override(
|
|
1540
|
+
self,
|
|
1541
|
+
policy_id: str,
|
|
1542
|
+
request: CreatePolicyOverrideRequest,
|
|
1543
|
+
) -> PolicyOverride:
|
|
1544
|
+
"""Create an override for a static policy."""
|
|
1545
|
+
return self._get_loop().run_until_complete(
|
|
1546
|
+
self._async_client.create_policy_override(policy_id, request)
|
|
1547
|
+
)
|
|
1548
|
+
|
|
1549
|
+
def delete_policy_override(self, policy_id: str) -> None:
|
|
1550
|
+
"""Delete an override for a static policy."""
|
|
1551
|
+
return self._get_loop().run_until_complete(
|
|
1552
|
+
self._async_client.delete_policy_override(policy_id)
|
|
1553
|
+
)
|
|
1554
|
+
|
|
1555
|
+
# Dynamic policy sync wrappers
|
|
1556
|
+
|
|
1557
|
+
def list_dynamic_policies(
|
|
1558
|
+
self,
|
|
1559
|
+
options: ListDynamicPoliciesOptions | None = None,
|
|
1560
|
+
) -> list[DynamicPolicy]:
|
|
1561
|
+
"""List all dynamic policies with optional filtering."""
|
|
1562
|
+
return self._get_loop().run_until_complete(
|
|
1563
|
+
self._async_client.list_dynamic_policies(options)
|
|
1564
|
+
)
|
|
1565
|
+
|
|
1566
|
+
def get_dynamic_policy(self, policy_id: str) -> DynamicPolicy:
|
|
1567
|
+
"""Get a specific dynamic policy by ID."""
|
|
1568
|
+
return self._get_loop().run_until_complete(self._async_client.get_dynamic_policy(policy_id))
|
|
1569
|
+
|
|
1570
|
+
def create_dynamic_policy(
|
|
1571
|
+
self,
|
|
1572
|
+
request: CreateDynamicPolicyRequest,
|
|
1573
|
+
) -> DynamicPolicy:
|
|
1574
|
+
"""Create a new dynamic policy."""
|
|
1575
|
+
return self._get_loop().run_until_complete(
|
|
1576
|
+
self._async_client.create_dynamic_policy(request)
|
|
1577
|
+
)
|
|
1578
|
+
|
|
1579
|
+
def update_dynamic_policy(
|
|
1580
|
+
self,
|
|
1581
|
+
policy_id: str,
|
|
1582
|
+
request: UpdateDynamicPolicyRequest,
|
|
1583
|
+
) -> DynamicPolicy:
|
|
1584
|
+
"""Update an existing dynamic policy."""
|
|
1585
|
+
return self._get_loop().run_until_complete(
|
|
1586
|
+
self._async_client.update_dynamic_policy(policy_id, request)
|
|
1587
|
+
)
|
|
1588
|
+
|
|
1589
|
+
def delete_dynamic_policy(self, policy_id: str) -> None:
|
|
1590
|
+
"""Delete a dynamic policy."""
|
|
1591
|
+
return self._get_loop().run_until_complete(
|
|
1592
|
+
self._async_client.delete_dynamic_policy(policy_id)
|
|
1593
|
+
)
|
|
1594
|
+
|
|
1595
|
+
def toggle_dynamic_policy(
|
|
1596
|
+
self,
|
|
1597
|
+
policy_id: str,
|
|
1598
|
+
enabled: bool,
|
|
1599
|
+
) -> DynamicPolicy:
|
|
1600
|
+
"""Toggle a dynamic policy's enabled status."""
|
|
1601
|
+
return self._get_loop().run_until_complete(
|
|
1602
|
+
self._async_client.toggle_dynamic_policy(policy_id, enabled)
|
|
1603
|
+
)
|
|
1604
|
+
|
|
1605
|
+
def get_effective_dynamic_policies(
|
|
1606
|
+
self,
|
|
1607
|
+
options: EffectivePoliciesOptions | None = None,
|
|
1608
|
+
) -> list[DynamicPolicy]:
|
|
1609
|
+
"""Get effective dynamic policies with tier inheritance applied."""
|
|
1610
|
+
return self._get_loop().run_until_complete(
|
|
1611
|
+
self._async_client.get_effective_dynamic_policies(options)
|
|
1612
|
+
)
|