agentic-fabriq-sdk 0.1.3__tar.gz

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.

@@ -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,54 @@
1
+ # Agentic Fabric SDK (Fabriq)
2
+
3
+ `agentic-fabriq-sdk` provides a Python SDK for interacting with Fabriq/Agentic Fabric.
4
+
5
+ - High-level client: `af_sdk.FabriqClient`
6
+ - DX layer: `af_sdk.dx` (`ToolFabric`, `AgentFabric`, `MCPServer`, `Agent`, and `tool`)
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install agentic-fabriq-sdk
12
+ ```
13
+
14
+ ## Quickstart
15
+
16
+ ```python
17
+ from af_sdk.fabriq_client import FabriqClient
18
+
19
+ TOKEN = "..." # Bearer JWT for the Fabriq Gateway
20
+ BASE = "http://localhost:8000"
21
+
22
+ async def main():
23
+ async with FabriqClient(base_url=BASE, auth_token=TOKEN) as af:
24
+ agents = await af.list_agents()
25
+ print(agents)
26
+ ```
27
+
28
+ DX orchestration:
29
+
30
+ ```python
31
+ from af_sdk.dx import ToolFabric, AgentFabric, Agent, tool
32
+
33
+ slack = ToolFabric(provider="slack", base_url="http://localhost:8000", access_token=TOKEN, tenant_id=TENANT)
34
+ agents = AgentFabric(base_url="http://localhost:8000", access_token=TOKEN, tenant_id=TENANT)
35
+
36
+ @tool
37
+ def echo(x: str) -> str:
38
+ return x
39
+
40
+ bot = Agent(
41
+ system_prompt="demo",
42
+ tools=[echo],
43
+ agents=agents.get_agents(["summarizer"]),
44
+ base_url="http://localhost:8000",
45
+ access_token=TOKEN,
46
+ tenant_id=TENANT,
47
+ provider_fabrics={"slack": slack},
48
+ )
49
+ print(bot.run("Summarize my Slack messages"))
50
+ ```
51
+
52
+ ## License
53
+
54
+ Apache-2.0
@@ -0,0 +1,55 @@
1
+ """
2
+ Agentic Fabric SDK
3
+
4
+ Official Python SDK for building connectors and interacting with Agentic Fabric.
5
+ """
6
+
7
+ from .auth.oauth import oauth_required
8
+ from .connectors.base import AgentConnector, ConnectorContext, ToolConnector
9
+ from .exceptions import (
10
+ AFError,
11
+ AuthenticationError,
12
+ AuthorizationError,
13
+ ConnectorError,
14
+ NotFoundError,
15
+ ValidationError,
16
+ )
17
+ from .models.types import (
18
+ AgentInvokeRequest,
19
+ AgentInvokeResult,
20
+ ToolInvokeRequest,
21
+ ToolInvokeResult,
22
+ )
23
+ from .transport.http import HTTPClient
24
+ from .fabriq_client import FabriqClient
25
+ from .models.audit import AuditEvent
26
+
27
+ __version__ = "1.0.0"
28
+
29
+ __all__ = [
30
+ "oauth_required",
31
+ "ToolConnector",
32
+ "AgentConnector",
33
+ "ConnectorContext",
34
+ "AFError",
35
+ "AuthenticationError",
36
+ "AuthorizationError",
37
+ "ConnectorError",
38
+ "NotFoundError",
39
+ "ValidationError",
40
+ "AgentInvokeRequest",
41
+ "AgentInvokeResult",
42
+ "ToolInvokeRequest",
43
+ "ToolInvokeResult",
44
+ "HTTPClient",
45
+ "FabriqClient",
46
+ "AuditEvent",
47
+ ]
48
+
49
+ # Lazy expose dx submodule under af_sdk.dx
50
+ from importlib import import_module as _import_module # noqa: E402
51
+
52
+ def __getattr__(name):
53
+ if name == "dx":
54
+ return _import_module("af_sdk.dx")
55
+ raise AttributeError(name)
@@ -0,0 +1,31 @@
1
+ """
2
+ Authentication and authorization utilities for Agentic Fabric SDK.
3
+ """
4
+
5
+ from .oauth import (
6
+ api_key_required,
7
+ mtls_required,
8
+ no_auth_required,
9
+ oauth_required,
10
+ ScopeValidator,
11
+ TokenValidator,
12
+ )
13
+ from .token_cache import TokenManager, VaultClient
14
+
15
+ # DPoP helper will be provided from af_sdk.auth.dpop
16
+ try:
17
+ from .dpop import create_dpop_proof
18
+ except Exception: # pragma: no cover - optional import if file missing
19
+ create_dpop_proof = None # type: ignore
20
+
21
+ __all__ = [
22
+ "oauth_required",
23
+ "api_key_required",
24
+ "mtls_required",
25
+ "no_auth_required",
26
+ "ScopeValidator",
27
+ "TokenValidator",
28
+ "TokenManager",
29
+ "VaultClient",
30
+ "create_dpop_proof",
31
+ ]
@@ -0,0 +1,43 @@
1
+ """
2
+ Client-side DPoP (Proof of Possession) helper for AF SDK.
3
+
4
+ This provides a simple HMAC-signed JWT for development that matches the
5
+ Gateway's mock PoP verifier semantics. In production, replace with a
6
+ DPoP JWT signed using a private key corresponding to the client cert
7
+ thumbprint per RFC 9449.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import time
13
+ from typing import Dict, Optional
14
+
15
+ import jwt
16
+
17
+
18
+ def create_dpop_proof(*, method: str, url: str, thumbprint: str = "dev-thumbprint", lifetime_s: int = 60, secret: Optional[str] = None) -> str:
19
+ """Create a development DPoP-like JWT for AF mock endpoints.
20
+
21
+ Args:
22
+ method: HTTP method, e.g., "POST".
23
+ url: Full request URL.
24
+ thumbprint: x5t#S256 thumbprint string (dev default).
25
+ lifetime_s: Token lifetime in seconds.
26
+ secret: HMAC secret for signing (dev only). If not provided, uses a default.
27
+
28
+ Returns:
29
+ A compact JWT string to send in the DPoP header.
30
+ """
31
+ now = int(time.time())
32
+ payload: Dict = {
33
+ "htm": method.upper(),
34
+ "htu": url,
35
+ "iat": now,
36
+ "exp": now + lifetime_s,
37
+ "cnf": {"x5t#S256": thumbprint},
38
+ "typ": "pop",
39
+ }
40
+ key = secret or "af-dev-pop-secret"
41
+ return jwt.encode(payload, key, algorithm="HS256")
42
+
43
+
@@ -0,0 +1,247 @@
1
+ """
2
+ OAuth authentication decorator and helpers.
3
+ """
4
+
5
+ import time
6
+ from functools import wraps
7
+ from typing import Any, Awaitable, Callable, List, Optional
8
+
9
+ from ..exceptions import AuthenticationError, TokenRefreshError
10
+
11
+
12
+ def oauth_required(*, scopes: List[str], refresh_if_expired: bool = True):
13
+ """
14
+ Decorator that injects a valid OAuth2 Bearer token for the current user.
15
+
16
+ Automatically:
17
+ 1. Pulls access token from TokenManager (refreshes if expired)
18
+ 2. Populates `Authorization` header
19
+ 3. Enforces that requested scopes ⊆ granted scopes
20
+
21
+ Args:
22
+ scopes: List of required OAuth scopes
23
+ refresh_if_expired: Whether to attempt token refresh if expired
24
+
25
+ Raises:
26
+ AuthenticationError: If token is invalid or missing
27
+ TokenRefreshError: If token refresh fails
28
+ """
29
+
30
+ def decorator(fn: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
31
+ @wraps(fn)
32
+ async def wrapper(self, *args, **kwargs):
33
+ ctx = getattr(self, "ctx", None)
34
+ if not ctx:
35
+ raise AuthenticationError("Connector context not available")
36
+
37
+ tool_id = getattr(self, "TOOL_ID", None)
38
+ if not tool_id:
39
+ raise AuthenticationError("TOOL_ID not set in connector")
40
+
41
+ try:
42
+ # Get OAuth token from token manager
43
+ token = await ctx.token_manager.get_oauth_token(
44
+ tool_id=tool_id,
45
+ user_id=ctx.user_id,
46
+ scopes=scopes,
47
+ refresh_if_expired=refresh_if_expired,
48
+ )
49
+
50
+ # Inject Authorization header
51
+ headers = kwargs.setdefault("headers", {})
52
+ headers.setdefault("Authorization", f"Bearer {token}")
53
+
54
+ # Log the request (without token)
55
+ ctx.logger.info(
56
+ f"Making OAuth request to {tool_id}",
57
+ extra={"scopes": scopes, "user_id": ctx.user_id},
58
+ )
59
+
60
+ return await fn(self, *args, **kwargs)
61
+
62
+ except Exception as e:
63
+ ctx.logger.error(f"OAuth authentication failed: {e}")
64
+ if isinstance(e, (AuthenticationError, TokenRefreshError)):
65
+ raise
66
+ raise AuthenticationError(f"OAuth authentication failed: {e}")
67
+
68
+ return wrapper
69
+
70
+ return decorator
71
+
72
+
73
+ def api_key_required(*, key_name: str = "api_key", header_name: str = "X-API-Key"):
74
+ """
75
+ Decorator that injects an API key for the current user.
76
+
77
+ Args:
78
+ key_name: Name of the API key in the vault
79
+ header_name: HTTP header name for the API key
80
+
81
+ Raises:
82
+ AuthenticationError: If API key is missing or invalid
83
+ """
84
+
85
+ def decorator(fn: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
86
+ @wraps(fn)
87
+ async def wrapper(self, *args, **kwargs):
88
+ ctx = getattr(self, "ctx", None)
89
+ if not ctx:
90
+ raise AuthenticationError("Connector context not available")
91
+
92
+ tool_id = getattr(self, "TOOL_ID", None)
93
+ if not tool_id:
94
+ raise AuthenticationError("TOOL_ID not set in connector")
95
+
96
+ try:
97
+ # Get API key from vault
98
+ secret_path = f"af/{ctx.tenant_id}/{ctx.user_id}/api_keys/{tool_id}/{key_name}"
99
+ secret = await ctx.token_manager.vault_client.read_secret(secret_path)
100
+
101
+ if not secret or "value" not in secret:
102
+ raise AuthenticationError(f"API key not found: {key_name}")
103
+
104
+ api_key = secret["value"]
105
+
106
+ # Inject API key header
107
+ headers = kwargs.setdefault("headers", {})
108
+ headers.setdefault(header_name, api_key)
109
+
110
+ # Log the request
111
+ ctx.logger.info(
112
+ f"Making API key request to {tool_id}",
113
+ extra={"key_name": key_name, "user_id": ctx.user_id},
114
+ )
115
+
116
+ return await fn(self, *args, **kwargs)
117
+
118
+ except Exception as e:
119
+ ctx.logger.error(f"API key authentication failed: {e}")
120
+ if isinstance(e, AuthenticationError):
121
+ raise
122
+ raise AuthenticationError(f"API key authentication failed: {e}")
123
+
124
+ return wrapper
125
+
126
+ return decorator
127
+
128
+
129
+ def mtls_required(*, cert_path: Optional[str] = None, key_path: Optional[str] = None):
130
+ """
131
+ Decorator that configures mutual TLS authentication.
132
+
133
+ Args:
134
+ cert_path: Path to client certificate (optional, uses default if not provided)
135
+ key_path: Path to client private key (optional, uses default if not provided)
136
+
137
+ Raises:
138
+ AuthenticationError: If mTLS configuration fails
139
+ """
140
+
141
+ def decorator(fn: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
142
+ @wraps(fn)
143
+ async def wrapper(self, *args, **kwargs):
144
+ ctx = getattr(self, "ctx", None)
145
+ if not ctx:
146
+ raise AuthenticationError("Connector context not available")
147
+
148
+ try:
149
+ # Configure mTLS for the HTTP client
150
+ # This would typically be done at the session level
151
+ # For now, we'll add it to the kwargs
152
+ cert_config = (cert_path, key_path) if cert_path and key_path else None
153
+ kwargs.setdefault("cert", cert_config)
154
+
155
+ ctx.logger.info(
156
+ "Making mTLS request",
157
+ extra={"cert_path": cert_path, "user_id": ctx.user_id},
158
+ )
159
+
160
+ return await fn(self, *args, **kwargs)
161
+
162
+ except Exception as e:
163
+ ctx.logger.error(f"mTLS authentication failed: {e}")
164
+ raise AuthenticationError(f"mTLS authentication failed: {e}")
165
+
166
+ return wrapper
167
+
168
+ return decorator
169
+
170
+
171
+ def no_auth_required(fn: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
172
+ """
173
+ Decorator that marks a method as not requiring authentication.
174
+ Useful for public endpoints or health checks.
175
+ """
176
+
177
+ @wraps(fn)
178
+ async def wrapper(self, *args, **kwargs):
179
+ ctx = getattr(self, "ctx", None)
180
+ if ctx:
181
+ ctx.logger.info("Making unauthenticated request")
182
+ return await fn(self, *args, **kwargs)
183
+
184
+ return wrapper
185
+
186
+
187
+ class ScopeValidator:
188
+ """Helper class for validating OAuth scopes."""
189
+
190
+ @staticmethod
191
+ def validate_scopes(required_scopes: List[str], granted_scopes: List[str]) -> bool:
192
+ """
193
+ Validate that all required scopes are granted.
194
+
195
+ Args:
196
+ required_scopes: List of required scopes
197
+ granted_scopes: List of granted scopes
198
+
199
+ Returns:
200
+ True if all required scopes are granted, False otherwise
201
+ """
202
+ return set(required_scopes).issubset(set(granted_scopes))
203
+
204
+ @staticmethod
205
+ def missing_scopes(required_scopes: List[str], granted_scopes: List[str]) -> List[str]:
206
+ """
207
+ Get list of missing scopes.
208
+
209
+ Args:
210
+ required_scopes: List of required scopes
211
+ granted_scopes: List of granted scopes
212
+
213
+ Returns:
214
+ List of missing scopes
215
+ """
216
+ return list(set(required_scopes) - set(granted_scopes))
217
+
218
+
219
+ class TokenValidator:
220
+ """Helper class for validating OAuth tokens."""
221
+
222
+ @staticmethod
223
+ def is_expired(expires_at: float) -> bool:
224
+ """
225
+ Check if a token is expired.
226
+
227
+ Args:
228
+ expires_at: Expiration timestamp
229
+
230
+ Returns:
231
+ True if token is expired, False otherwise
232
+ """
233
+ return time.time() >= expires_at
234
+
235
+ @staticmethod
236
+ def expires_soon(expires_at: float, buffer_seconds: int = 300) -> bool:
237
+ """
238
+ Check if a token expires soon.
239
+
240
+ Args:
241
+ expires_at: Expiration timestamp
242
+ buffer_seconds: Buffer time in seconds
243
+
244
+ Returns:
245
+ True if token expires within buffer time, False otherwise
246
+ """
247
+ return time.time() + buffer_seconds >= expires_at