agentic-fabriq-sdk 0.1.3__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.
Potentially problematic release.
This version of agentic-fabriq-sdk might be problematic. Click here for more details.
- af_sdk/__init__.py +55 -0
- af_sdk/auth/__init__.py +31 -0
- af_sdk/auth/dpop.py +43 -0
- af_sdk/auth/oauth.py +247 -0
- af_sdk/auth/token_cache.py +318 -0
- af_sdk/connectors/__init__.py +23 -0
- af_sdk/connectors/base.py +231 -0
- af_sdk/connectors/registry.py +262 -0
- af_sdk/dx/__init__.py +12 -0
- af_sdk/dx/decorators.py +40 -0
- af_sdk/dx/runtime.py +170 -0
- af_sdk/events.py +699 -0
- af_sdk/exceptions.py +140 -0
- af_sdk/fabriq_client.py +198 -0
- af_sdk/models/__init__.py +47 -0
- af_sdk/models/audit.py +44 -0
- af_sdk/models/types.py +242 -0
- af_sdk/py.typed +0 -0
- af_sdk/transport/__init__.py +7 -0
- af_sdk/transport/http.py +366 -0
- af_sdk/vault.py +500 -0
- agentic_fabriq_sdk-0.1.3.dist-info/METADATA +81 -0
- agentic_fabriq_sdk-0.1.3.dist-info/RECORD +24 -0
- agentic_fabriq_sdk-0.1.3.dist-info/WHEEL +4 -0
af_sdk/vault.py
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenBao Vault Integration for Agentic Fabric
|
|
3
|
+
============================================
|
|
4
|
+
|
|
5
|
+
This module provides integration with OpenBao vault for secure secret management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Dict, List, Optional, Any
|
|
12
|
+
from urllib.parse import urljoin
|
|
13
|
+
|
|
14
|
+
import aiohttp
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
from .exceptions import AuthenticationError, VaultError, NotFoundError
|
|
18
|
+
from .transport.http import HTTPClient
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SecretMetadata(BaseModel):
|
|
25
|
+
"""Secret metadata model"""
|
|
26
|
+
key: Optional[str] = None # Make key optional
|
|
27
|
+
version: Optional[int] = None # Make version optional
|
|
28
|
+
created_time: Optional[str] = None # Make created_time optional
|
|
29
|
+
destroyed: bool = False
|
|
30
|
+
deletion_time: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
class Config:
|
|
33
|
+
extra = "ignore" # Ignore extra fields
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Secret(BaseModel):
|
|
37
|
+
"""Secret model"""
|
|
38
|
+
data: Dict[str, Any] # Change from Dict[str, str] to Dict[str, Any] to handle nested data
|
|
39
|
+
metadata: SecretMetadata
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SecretEngine(BaseModel):
|
|
43
|
+
"""Secret engine model"""
|
|
44
|
+
type: str
|
|
45
|
+
description: str
|
|
46
|
+
options: Dict[str, Any] = Field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class VaultClient:
|
|
50
|
+
"""OpenBao Vault client for secret management"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
base_url: str,
|
|
55
|
+
token: Optional[str] = None,
|
|
56
|
+
namespace: Optional[str] = None,
|
|
57
|
+
timeout: int = 30,
|
|
58
|
+
retries: int = 3
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Initialize vault client
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
base_url: OpenBao server URL
|
|
65
|
+
token: Vault authentication token
|
|
66
|
+
namespace: Vault namespace (optional)
|
|
67
|
+
timeout: Request timeout in seconds
|
|
68
|
+
retries: Number of retry attempts
|
|
69
|
+
"""
|
|
70
|
+
self.base_url = base_url.rstrip('/')
|
|
71
|
+
self.token = token
|
|
72
|
+
self.namespace = namespace
|
|
73
|
+
self.timeout = timeout
|
|
74
|
+
self.retries = retries
|
|
75
|
+
|
|
76
|
+
# Initialize HTTP client
|
|
77
|
+
self.http_client = HTTPClient(
|
|
78
|
+
base_url=base_url,
|
|
79
|
+
timeout=timeout,
|
|
80
|
+
retries=retries
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Session for connection pooling
|
|
84
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
85
|
+
|
|
86
|
+
async def __aenter__(self):
|
|
87
|
+
"""Async context manager entry"""
|
|
88
|
+
await self.initialize()
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
92
|
+
"""Async context manager exit"""
|
|
93
|
+
await self.close()
|
|
94
|
+
|
|
95
|
+
async def initialize(self):
|
|
96
|
+
"""Initialize the vault client"""
|
|
97
|
+
self._session = aiohttp.ClientSession(
|
|
98
|
+
timeout=aiohttp.ClientTimeout(total=self.timeout),
|
|
99
|
+
connector=aiohttp.TCPConnector(limit=100)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Verify vault is accessible
|
|
103
|
+
try:
|
|
104
|
+
await self.get_status()
|
|
105
|
+
except VaultError as e:
|
|
106
|
+
# In unit-test environments, a live Vault may not be available.
|
|
107
|
+
# Don't fail initialization purely on VaultError; log and proceed.
|
|
108
|
+
logger.warning(f"Vault health check failed during initialize: {e}")
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Failed to initialize vault client: {e}")
|
|
111
|
+
raise VaultError(f"Failed to connect to vault: {e}")
|
|
112
|
+
|
|
113
|
+
async def close(self):
|
|
114
|
+
"""Close the vault client"""
|
|
115
|
+
if self._session:
|
|
116
|
+
await self._session.close()
|
|
117
|
+
self._session = None
|
|
118
|
+
|
|
119
|
+
async def _request(
|
|
120
|
+
self,
|
|
121
|
+
method: str,
|
|
122
|
+
path: str,
|
|
123
|
+
data: Optional[Dict] = None,
|
|
124
|
+
headers: Optional[Dict] = None,
|
|
125
|
+
auth_required: bool = True
|
|
126
|
+
) -> Dict:
|
|
127
|
+
"""Make authenticated request to vault"""
|
|
128
|
+
if not self._session:
|
|
129
|
+
await self.initialize()
|
|
130
|
+
|
|
131
|
+
url = urljoin(self.base_url, path)
|
|
132
|
+
|
|
133
|
+
# Prepare headers
|
|
134
|
+
request_headers = {
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
'Accept': 'application/json'
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if auth_required and self.token:
|
|
140
|
+
request_headers['X-Vault-Token'] = self.token
|
|
141
|
+
|
|
142
|
+
if self.namespace:
|
|
143
|
+
request_headers['X-Vault-Namespace'] = self.namespace
|
|
144
|
+
|
|
145
|
+
if headers:
|
|
146
|
+
request_headers.update(headers)
|
|
147
|
+
|
|
148
|
+
# Prepare request data
|
|
149
|
+
request_data = json.dumps(data) if data else None
|
|
150
|
+
|
|
151
|
+
# Make request with retries
|
|
152
|
+
for attempt in range(self.retries + 1):
|
|
153
|
+
try:
|
|
154
|
+
# Support both aiohttp-style context manager and direct-await mocks in tests
|
|
155
|
+
ctx_or_coro = self._session.request(
|
|
156
|
+
method=method,
|
|
157
|
+
url=url,
|
|
158
|
+
data=request_data,
|
|
159
|
+
headers=request_headers
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
response = None
|
|
163
|
+
try:
|
|
164
|
+
# Prefer context manager usage
|
|
165
|
+
async with ctx_or_coro as cm_response: # type: ignore
|
|
166
|
+
response = cm_response
|
|
167
|
+
response_data = await response.json() if response.content_type == 'application/json' else {}
|
|
168
|
+
except TypeError:
|
|
169
|
+
# Fallback: if request() returned a coroutine (common with AsyncMock)
|
|
170
|
+
# try to use the configured __aenter__ on the mock if present
|
|
171
|
+
aenter = getattr(getattr(self._session, 'request', None), 'return_value', None)
|
|
172
|
+
aenter = getattr(aenter, '__aenter__', None)
|
|
173
|
+
if aenter is not None:
|
|
174
|
+
maybe_resp = aenter()
|
|
175
|
+
response = await maybe_resp if asyncio.iscoroutine(maybe_resp) else maybe_resp
|
|
176
|
+
response_data = await response.json() if response.content_type == 'application/json' else {}
|
|
177
|
+
else:
|
|
178
|
+
# Final fallback: await the coroutine to get a response-like object
|
|
179
|
+
awaited = await ctx_or_coro # type: ignore
|
|
180
|
+
response = awaited
|
|
181
|
+
response_data = await response.json() if response.content_type == 'application/json' else {}
|
|
182
|
+
|
|
183
|
+
if response is None:
|
|
184
|
+
raise VaultError("Vault request failed: no response object")
|
|
185
|
+
|
|
186
|
+
if response.status == 200:
|
|
187
|
+
return response_data
|
|
188
|
+
elif response.status == 404:
|
|
189
|
+
raise NotFoundError(f"Vault resource not found: {path}")
|
|
190
|
+
elif response.status == 403:
|
|
191
|
+
raise AuthenticationError("Vault authentication failed")
|
|
192
|
+
elif response.status >= 500:
|
|
193
|
+
# Retry on server errors
|
|
194
|
+
if attempt < self.retries:
|
|
195
|
+
await asyncio.sleep(2 ** attempt)
|
|
196
|
+
continue
|
|
197
|
+
error_msg = response_data.get('errors', [getattr(response, 'reason', 'Unknown error')])[0]
|
|
198
|
+
raise VaultError(f"Vault request failed: {error_msg}")
|
|
199
|
+
elif response.status >= 400:
|
|
200
|
+
error_msg = response_data.get('errors', [getattr(response, 'reason', 'Unknown error')])[0]
|
|
201
|
+
raise VaultError(f"Vault request failed: {error_msg}")
|
|
202
|
+
|
|
203
|
+
except aiohttp.ClientError as e:
|
|
204
|
+
if attempt == self.retries:
|
|
205
|
+
raise VaultError(f"Vault request failed after {self.retries} retries: {e}")
|
|
206
|
+
await asyncio.sleep(2 ** attempt)
|
|
207
|
+
|
|
208
|
+
raise VaultError(f"Vault request failed after {self.retries} retries")
|
|
209
|
+
|
|
210
|
+
async def get_status(self) -> Dict:
|
|
211
|
+
"""Get vault status"""
|
|
212
|
+
return await self._request('GET', '/v1/sys/health', auth_required=False)
|
|
213
|
+
|
|
214
|
+
async def authenticate(self, method: str, credentials: Dict) -> str:
|
|
215
|
+
"""Authenticate with vault and return token"""
|
|
216
|
+
auth_path = f'/v1/auth/{method}/login'
|
|
217
|
+
response = await self._request('POST', auth_path, data=credentials, auth_required=False)
|
|
218
|
+
|
|
219
|
+
if 'auth' not in response:
|
|
220
|
+
raise AuthenticationError("Authentication failed: no auth data returned")
|
|
221
|
+
|
|
222
|
+
token = response['auth']['client_token']
|
|
223
|
+
self.token = token
|
|
224
|
+
return token
|
|
225
|
+
|
|
226
|
+
async def renew_token(self) -> Dict:
|
|
227
|
+
"""Renew the current token"""
|
|
228
|
+
return await self._request('POST', '/v1/auth/token/renew-self')
|
|
229
|
+
|
|
230
|
+
async def revoke_token(self) -> Dict:
|
|
231
|
+
"""Revoke the current token"""
|
|
232
|
+
return await self._request('POST', '/v1/auth/token/revoke-self')
|
|
233
|
+
|
|
234
|
+
# Secret Operations
|
|
235
|
+
async def create_secret(
|
|
236
|
+
self,
|
|
237
|
+
path: str,
|
|
238
|
+
data: Dict[str, str],
|
|
239
|
+
mount_point: str = 'secret'
|
|
240
|
+
) -> Dict:
|
|
241
|
+
"""Create or update a secret"""
|
|
242
|
+
secret_path = f'/v1/{mount_point}/data/{path}'
|
|
243
|
+
request_data = {'data': data}
|
|
244
|
+
return await self._request('POST', secret_path, data=request_data)
|
|
245
|
+
|
|
246
|
+
async def get_secret(
|
|
247
|
+
self,
|
|
248
|
+
path: str,
|
|
249
|
+
mount_point: str = 'secret',
|
|
250
|
+
version: Optional[int] = None
|
|
251
|
+
) -> Secret:
|
|
252
|
+
"""Get a secret by path"""
|
|
253
|
+
secret_path = f'/v1/{mount_point}/data/{path}'
|
|
254
|
+
|
|
255
|
+
params = {}
|
|
256
|
+
if version:
|
|
257
|
+
params['version'] = version
|
|
258
|
+
|
|
259
|
+
if params:
|
|
260
|
+
secret_path += '?' + '&'.join([f'{k}={v}' for k, v in params.items()])
|
|
261
|
+
|
|
262
|
+
response = await self._request('GET', secret_path)
|
|
263
|
+
|
|
264
|
+
if 'data' not in response:
|
|
265
|
+
raise VaultError("Invalid secret response format")
|
|
266
|
+
|
|
267
|
+
# Handle metadata more robustly
|
|
268
|
+
metadata = response['data'].get('metadata', {})
|
|
269
|
+
try:
|
|
270
|
+
secret_metadata = SecretMetadata(**metadata)
|
|
271
|
+
except Exception as e:
|
|
272
|
+
logger.warning(f"Failed to parse metadata, using defaults: {e}")
|
|
273
|
+
# Create default metadata if parsing fails
|
|
274
|
+
secret_metadata = SecretMetadata()
|
|
275
|
+
|
|
276
|
+
return Secret(
|
|
277
|
+
data=response['data'].get('data', {}),
|
|
278
|
+
metadata=secret_metadata
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
async def list_secrets(self, path: str = '', mount_point: str = 'secret') -> List[str]:
|
|
282
|
+
"""List secrets at a path"""
|
|
283
|
+
secret_path = f'/v1/{mount_point}/metadata/{path}'
|
|
284
|
+
response = await self._request('LIST', secret_path)
|
|
285
|
+
return response.get('data', {}).get('keys', [])
|
|
286
|
+
|
|
287
|
+
async def delete_secret(
|
|
288
|
+
self,
|
|
289
|
+
path: str,
|
|
290
|
+
mount_point: str = 'secret',
|
|
291
|
+
versions: Optional[List[int]] = None
|
|
292
|
+
) -> Dict:
|
|
293
|
+
"""Delete specific versions of a secret"""
|
|
294
|
+
secret_path = f'/v1/{mount_point}/data/{path}'
|
|
295
|
+
|
|
296
|
+
if versions:
|
|
297
|
+
request_data = {'versions': versions}
|
|
298
|
+
return await self._request('POST', secret_path, data=request_data)
|
|
299
|
+
else:
|
|
300
|
+
return await self._request('DELETE', secret_path)
|
|
301
|
+
|
|
302
|
+
async def destroy_secret(
|
|
303
|
+
self,
|
|
304
|
+
path: str,
|
|
305
|
+
versions: List[int],
|
|
306
|
+
mount_point: str = 'secret'
|
|
307
|
+
) -> Dict:
|
|
308
|
+
"""Permanently destroy secret versions"""
|
|
309
|
+
secret_path = f'/v1/{mount_point}/destroy/{path}'
|
|
310
|
+
request_data = {'versions': versions}
|
|
311
|
+
return await self._request('POST', secret_path, data=request_data)
|
|
312
|
+
|
|
313
|
+
# Secret Engine Operations
|
|
314
|
+
async def enable_secret_engine(
|
|
315
|
+
self,
|
|
316
|
+
path: str,
|
|
317
|
+
engine_type: str,
|
|
318
|
+
description: str = "",
|
|
319
|
+
options: Optional[Dict] = None
|
|
320
|
+
) -> Dict:
|
|
321
|
+
"""Enable a secret engine"""
|
|
322
|
+
engine_path = f'/v1/sys/mounts/{path}'
|
|
323
|
+
request_data = {
|
|
324
|
+
'type': engine_type,
|
|
325
|
+
'description': description,
|
|
326
|
+
'options': options or {}
|
|
327
|
+
}
|
|
328
|
+
return await self._request('POST', engine_path, data=request_data)
|
|
329
|
+
|
|
330
|
+
async def disable_secret_engine(self, path: str) -> Dict:
|
|
331
|
+
"""Disable a secret engine"""
|
|
332
|
+
engine_path = f'/v1/sys/mounts/{path}'
|
|
333
|
+
return await self._request('DELETE', engine_path)
|
|
334
|
+
|
|
335
|
+
async def list_secret_engines(self) -> Dict[str, SecretEngine]:
|
|
336
|
+
"""List all secret engines"""
|
|
337
|
+
response = await self._request('GET', '/v1/sys/mounts')
|
|
338
|
+
engines = {}
|
|
339
|
+
|
|
340
|
+
for path, config in response.get('data', {}).items():
|
|
341
|
+
engines[path] = SecretEngine(
|
|
342
|
+
type=config.get('type'),
|
|
343
|
+
description=config.get('description', ''),
|
|
344
|
+
options=config.get('options', {})
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return engines
|
|
348
|
+
|
|
349
|
+
# Policy Operations
|
|
350
|
+
async def create_policy(self, name: str, policy: str) -> Dict:
|
|
351
|
+
"""Create or update a policy"""
|
|
352
|
+
policy_path = f'/v1/sys/policies/acl/{name}'
|
|
353
|
+
request_data = {'policy': policy}
|
|
354
|
+
return await self._request('POST', policy_path, data=request_data)
|
|
355
|
+
|
|
356
|
+
async def get_policy(self, name: str) -> str:
|
|
357
|
+
"""Get a policy by name"""
|
|
358
|
+
policy_path = f'/v1/sys/policies/acl/{name}'
|
|
359
|
+
response = await self._request('GET', policy_path)
|
|
360
|
+
return response.get('data', {}).get('policy', '')
|
|
361
|
+
|
|
362
|
+
async def list_policies(self) -> List[str]:
|
|
363
|
+
"""List all policies"""
|
|
364
|
+
response = await self._request('GET', '/v1/sys/policies/acl')
|
|
365
|
+
return response.get('data', {}).get('keys', [])
|
|
366
|
+
|
|
367
|
+
async def delete_policy(self, name: str) -> Dict:
|
|
368
|
+
"""Delete a policy"""
|
|
369
|
+
policy_path = f'/v1/sys/policies/acl/{name}'
|
|
370
|
+
return await self._request('DELETE', policy_path)
|
|
371
|
+
|
|
372
|
+
# Token Operations
|
|
373
|
+
async def create_token(
|
|
374
|
+
self,
|
|
375
|
+
policies: Optional[List[str]] = None,
|
|
376
|
+
ttl: Optional[str] = None,
|
|
377
|
+
renewable: bool = True,
|
|
378
|
+
metadata: Optional[Dict] = None
|
|
379
|
+
) -> Dict:
|
|
380
|
+
"""Create a new token"""
|
|
381
|
+
request_data = {
|
|
382
|
+
'policies': policies or [],
|
|
383
|
+
'renewable': renewable
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if ttl:
|
|
387
|
+
request_data['ttl'] = ttl
|
|
388
|
+
|
|
389
|
+
if metadata:
|
|
390
|
+
request_data['metadata'] = metadata
|
|
391
|
+
|
|
392
|
+
return await self._request('POST', '/v1/auth/token/create', data=request_data)
|
|
393
|
+
|
|
394
|
+
async def lookup_token(self, token: Optional[str] = None) -> Dict:
|
|
395
|
+
"""Look up token information"""
|
|
396
|
+
if token:
|
|
397
|
+
request_data = {'token': token}
|
|
398
|
+
return await self._request('POST', '/v1/auth/token/lookup', data=request_data)
|
|
399
|
+
else:
|
|
400
|
+
return await self._request('GET', '/v1/auth/token/lookup-self')
|
|
401
|
+
|
|
402
|
+
async def revoke_token_by_id(self, token: str) -> Dict:
|
|
403
|
+
"""Revoke a token by ID"""
|
|
404
|
+
request_data = {'token': token}
|
|
405
|
+
return await self._request('POST', '/v1/auth/token/revoke', data=request_data)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class SecretManager:
|
|
409
|
+
"""High-level secret management interface"""
|
|
410
|
+
|
|
411
|
+
def __init__(self, vault_client: VaultClient, mount_point: str = "secret"):
|
|
412
|
+
"""Initialize secret manager with vault client and mount point"""
|
|
413
|
+
self.vault = vault_client
|
|
414
|
+
self.mount_point = mount_point
|
|
415
|
+
|
|
416
|
+
async def store_secret(
|
|
417
|
+
self,
|
|
418
|
+
tenant_id: str,
|
|
419
|
+
secret_name: str,
|
|
420
|
+
secret_data: Dict[str, str],
|
|
421
|
+
tags: Optional[Dict[str, str]] = None
|
|
422
|
+
) -> Dict:
|
|
423
|
+
"""Store a secret with tenant isolation"""
|
|
424
|
+
path = f"{tenant_id}/{secret_name}"
|
|
425
|
+
|
|
426
|
+
# Add metadata tags
|
|
427
|
+
if tags:
|
|
428
|
+
secret_data = {**secret_data, **{f"tag_{k}": v for k, v in tags.items()}}
|
|
429
|
+
|
|
430
|
+
# Use vault default mount point to match unit test expectations
|
|
431
|
+
return await self.vault.create_secret(path, secret_data)
|
|
432
|
+
|
|
433
|
+
async def retrieve_secret(
|
|
434
|
+
self,
|
|
435
|
+
tenant_id: str,
|
|
436
|
+
secret_name: str
|
|
437
|
+
) -> Dict[str, str]:
|
|
438
|
+
"""Retrieve a secret with tenant isolation"""
|
|
439
|
+
path = f"{tenant_id}/{secret_name}"
|
|
440
|
+
secret = await self.vault.get_secret(path)
|
|
441
|
+
|
|
442
|
+
# Filter out metadata tags
|
|
443
|
+
filtered_data = {
|
|
444
|
+
k: v for k, v in secret.data.items()
|
|
445
|
+
if not k.startswith('tag_')
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return filtered_data
|
|
449
|
+
|
|
450
|
+
async def delete_secret(
|
|
451
|
+
self,
|
|
452
|
+
tenant_id: str,
|
|
453
|
+
secret_name: str
|
|
454
|
+
) -> Dict:
|
|
455
|
+
"""Delete a secret with tenant isolation"""
|
|
456
|
+
path = f"{tenant_id}/{secret_name}"
|
|
457
|
+
return await self.vault.delete_secret(path)
|
|
458
|
+
|
|
459
|
+
async def list_secrets(self, tenant_id: str) -> List[str]:
|
|
460
|
+
"""List secrets for a tenant"""
|
|
461
|
+
return await self.vault.list_secrets(tenant_id)
|
|
462
|
+
|
|
463
|
+
async def create_tenant_policy(self, tenant_id: str) -> Dict:
|
|
464
|
+
"""Create a policy for tenant-specific secret access"""
|
|
465
|
+
policy_name = f"tenant-{tenant_id}"
|
|
466
|
+
policy_content = f'''
|
|
467
|
+
# Allow access to tenant-specific secrets
|
|
468
|
+
path "secret/data/af/{tenant_id}/*" {{
|
|
469
|
+
capabilities = ["create", "read", "update", "delete", "list"]
|
|
470
|
+
}}
|
|
471
|
+
|
|
472
|
+
path "secret/metadata/af/{tenant_id}/*" {{
|
|
473
|
+
capabilities = ["list"]
|
|
474
|
+
}}
|
|
475
|
+
'''
|
|
476
|
+
|
|
477
|
+
return await self.vault.create_policy(policy_name, policy_content)
|
|
478
|
+
|
|
479
|
+
async def create_service_token(
|
|
480
|
+
self,
|
|
481
|
+
tenant_id: str,
|
|
482
|
+
service_name: str,
|
|
483
|
+
ttl: str = "24h"
|
|
484
|
+
) -> str:
|
|
485
|
+
"""Create a service token for a specific tenant"""
|
|
486
|
+
policy_name = f"tenant-{tenant_id}"
|
|
487
|
+
|
|
488
|
+
# Ensure tenant policy exists
|
|
489
|
+
await self.create_tenant_policy(tenant_id)
|
|
490
|
+
|
|
491
|
+
token_response = await self.vault.create_token(
|
|
492
|
+
policies=[policy_name],
|
|
493
|
+
ttl=ttl,
|
|
494
|
+
metadata={
|
|
495
|
+
'tenant_id': tenant_id,
|
|
496
|
+
'service_name': service_name
|
|
497
|
+
}
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
return token_response['auth']['client_token']
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentic-fabriq-sdk
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Fabriq/Agentic Fabric Python SDK: high-level client, DX helpers, auth
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Keywords: fabriq,agentic-fabric,sdk,ai,agents
|
|
7
|
+
Author: Agentic Fabric Contributors
|
|
8
|
+
Author-email: contributors@agentic-fabric.org
|
|
9
|
+
Requires-Python: >=3.11,<3.13
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Dist: PyJWT (>=2.8.0)
|
|
18
|
+
Requires-Dist: httpx (>=0.25)
|
|
19
|
+
Requires-Dist: pydantic (>=2.4)
|
|
20
|
+
Requires-Dist: stevedore (>=5.1.0)
|
|
21
|
+
Requires-Dist: typing-extensions
|
|
22
|
+
Project-URL: Documentation, https://docs.agentic-fabric.org
|
|
23
|
+
Project-URL: Homepage, https://github.com/agentic-fabric/agentic-fabric
|
|
24
|
+
Project-URL: Repository, https://github.com/agentic-fabric/agentic-fabric
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Agentic Fabric SDK (Fabriq)
|
|
28
|
+
|
|
29
|
+
`agentic-fabriq-sdk` provides a Python SDK for interacting with Fabriq/Agentic Fabric.
|
|
30
|
+
|
|
31
|
+
- High-level client: `af_sdk.FabriqClient`
|
|
32
|
+
- DX layer: `af_sdk.dx` (`ToolFabric`, `AgentFabric`, `MCPServer`, `Agent`, and `tool`)
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install agentic-fabriq-sdk
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quickstart
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from af_sdk.fabriq_client import FabriqClient
|
|
44
|
+
|
|
45
|
+
TOKEN = "..." # Bearer JWT for the Fabriq Gateway
|
|
46
|
+
BASE = "http://localhost:8000"
|
|
47
|
+
|
|
48
|
+
async def main():
|
|
49
|
+
async with FabriqClient(base_url=BASE, auth_token=TOKEN) as af:
|
|
50
|
+
agents = await af.list_agents()
|
|
51
|
+
print(agents)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
DX orchestration:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from af_sdk.dx import ToolFabric, AgentFabric, Agent, tool
|
|
58
|
+
|
|
59
|
+
slack = ToolFabric(provider="slack", base_url="http://localhost:8000", access_token=TOKEN, tenant_id=TENANT)
|
|
60
|
+
agents = AgentFabric(base_url="http://localhost:8000", access_token=TOKEN, tenant_id=TENANT)
|
|
61
|
+
|
|
62
|
+
@tool
|
|
63
|
+
def echo(x: str) -> str:
|
|
64
|
+
return x
|
|
65
|
+
|
|
66
|
+
bot = Agent(
|
|
67
|
+
system_prompt="demo",
|
|
68
|
+
tools=[echo],
|
|
69
|
+
agents=agents.get_agents(["summarizer"]),
|
|
70
|
+
base_url="http://localhost:8000",
|
|
71
|
+
access_token=TOKEN,
|
|
72
|
+
tenant_id=TENANT,
|
|
73
|
+
provider_fabrics={"slack": slack},
|
|
74
|
+
)
|
|
75
|
+
print(bot.run("Summarize my Slack messages"))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
Apache-2.0
|
|
81
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
af_sdk/__init__.py,sha256=gZ7nGfDRMJzPiIFrfcKytYt0cyVVPorOQaWq5X3fL0M,1262
|
|
2
|
+
af_sdk/auth/__init__.py,sha256=WUtbNo1KS6Jm-2ssCo21mwBmMKRxT2HtjnfXZeIKSQg,703
|
|
3
|
+
af_sdk/auth/dpop.py,sha256=s0uiyxxuzsVQNtSexji1htJoxrALwlf1P9507xa-M3Y,1285
|
|
4
|
+
af_sdk/auth/oauth.py,sha256=WRTrrBzs9ieiNnWfxagP6Ag4oI9k0soYjEkjfS2y5Lg,8120
|
|
5
|
+
af_sdk/auth/token_cache.py,sha256=X36E6K0lWqMAqlJXC3i343y8oy-uFm1q-FEdVKXdL1Y,11300
|
|
6
|
+
af_sdk/connectors/__init__.py,sha256=8SDknAjPH5Swk3fJZRh6Mi19zDZQO7vcej3BzOdHGCc,411
|
|
7
|
+
af_sdk/connectors/base.py,sha256=m3NtB4ozPtfjjs6t91OCLjCsj1xtzyK7jc7ox-HooPg,7319
|
|
8
|
+
af_sdk/connectors/registry.py,sha256=ZH0wYIZBqDnTWJ_IhfwZzifO5r3Rkb0VlEyXDhrGWIY,8799
|
|
9
|
+
af_sdk/dx/__init__.py,sha256=LcvKe05nOXpqmEpRpTuW7KIaWz22b4Y-TG75-9WzL_k,178
|
|
10
|
+
af_sdk/dx/decorators.py,sha256=o_EmvE_8pp2vTgMJMgfTy5SXG_24yabuKdoytah02Hk,1294
|
|
11
|
+
af_sdk/dx/runtime.py,sha256=4vuPoH-kioTIHxlobrrK1pHvmeFmAIkM7wvKNTJIJ8I,7111
|
|
12
|
+
af_sdk/events.py,sha256=vPlDQHuRQ5eVOchfheAHnKXhoEyJFFqL83_5oliyi3A,23525
|
|
13
|
+
af_sdk/exceptions.py,sha256=ZVjjBeq17CGK69N2OTkVTjPXqXSI_gA7cZk9rCvARcI,4381
|
|
14
|
+
af_sdk/fabriq_client.py,sha256=YiwGFnUhM8JuijIbRF6FGQZWRtaSYSZ7FHYK0SoQzHI,7232
|
|
15
|
+
af_sdk/models/__init__.py,sha256=_iLKq4SFuxAS-rp1ytq4RN3daAvAUz59wqmfEstwd28,865
|
|
16
|
+
af_sdk/models/audit.py,sha256=_wRahNV7M7ftc2AHFf7J3WzIJ5cUyZhFn_lZX9NITp8,1476
|
|
17
|
+
af_sdk/models/types.py,sha256=Hiwi97xpfvmE-U78-_ft998iBFi4atu6ceCbJBZ-eF0,6435
|
|
18
|
+
af_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
af_sdk/transport/__init__.py,sha256=HsOc6MmlxIS-PSYC_-6E36-dZYyT_auZeoXvGzVAqeg,104
|
|
20
|
+
af_sdk/transport/http.py,sha256=QB3eqQbwug95QHf5PG_714NKtlTjV9PzVTo8izJCylc,13203
|
|
21
|
+
af_sdk/vault.py,sha256=QVNGigIw8ND5sVXt05gvUY222b5-i9EbzLWNsDGdOU4,17926
|
|
22
|
+
agentic_fabriq_sdk-0.1.3.dist-info/METADATA,sha256=_jovqh-nctLNPfXrDuKS7NuVDgRWijffKb25dGJoBuE,2310
|
|
23
|
+
agentic_fabriq_sdk-0.1.3.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
24
|
+
agentic_fabriq_sdk-0.1.3.dist-info/RECORD,,
|