agentic-fabriq-sdk 0.1.14__py3-none-any.whl → 0.1.15__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_cli/main.py CHANGED
@@ -10,6 +10,7 @@ from rich.console import Console
10
10
  from rich.table import Table
11
11
 
12
12
  from af_cli.commands.agents import app as agents_app
13
+ from af_cli.commands.applications import app as applications_app
13
14
  from af_cli.commands.auth import app as auth_app
14
15
  from af_cli.commands.config import app as config_app
15
16
  from af_cli.commands.mcp_servers import app as mcp_servers_app
@@ -31,6 +32,7 @@ app.add_typer(auth_app, name="auth", help="Authentication commands")
31
32
  app.add_typer(config_app, name="config", help="Configuration commands")
32
33
  app.add_typer(agents_app, name="agents", help="Agent management commands")
33
34
  app.add_typer(tools_app, name="tools", help="Tool management commands")
35
+ app.add_typer(applications_app, name="applications", help="Application management commands")
34
36
  app.add_typer(mcp_servers_app, name="mcp-servers", help="MCP server management commands")
35
37
  app.add_typer(secrets_app, name="secrets", help="Secret management commands")
36
38
 
@@ -85,19 +87,16 @@ def init(
85
87
  gateway_url: str = typer.Option(
86
88
  "https://dashboard.agenticfabriq.com",
87
89
  "--gateway-url",
88
- "-g",
89
90
  help="Gateway URL"
90
91
  ),
91
92
  tenant_id: Optional[str] = typer.Option(
92
93
  None,
93
94
  "--tenant-id",
94
- "-t",
95
95
  help="Tenant ID"
96
96
  ),
97
97
  force: bool = typer.Option(
98
98
  False,
99
99
  "--force",
100
- "-f",
101
100
  help="Force initialization, overwrite existing config"
102
101
  ),
103
102
  ):
@@ -134,25 +133,21 @@ def main(
134
133
  config_file: Optional[str] = typer.Option(
135
134
  None,
136
135
  "--config",
137
- "-c",
138
136
  help="Path to configuration file"
139
137
  ),
140
138
  gateway_url: Optional[str] = typer.Option(
141
139
  None,
142
140
  "--gateway-url",
143
- "-g",
144
141
  help="Gateway URL"
145
142
  ),
146
143
  tenant_id: Optional[str] = typer.Option(
147
144
  None,
148
145
  "--tenant-id",
149
- "-t",
150
146
  help="Tenant ID"
151
147
  ),
152
148
  verbose: bool = typer.Option(
153
149
  False,
154
150
  "--verbose",
155
- "-v",
156
151
  help="Enable verbose output"
157
152
  ),
158
153
  ):
af_sdk/__init__.py CHANGED
@@ -23,6 +23,14 @@ from .models.types import (
23
23
  from .transport.http import HTTPClient
24
24
  from .fabriq_client import FabriqClient
25
25
  from .models.audit import AuditEvent
26
+ from .auth import (
27
+ get_application_client,
28
+ load_application_config,
29
+ save_application_config,
30
+ list_applications,
31
+ delete_application_config,
32
+ ApplicationNotFoundError,
33
+ )
26
34
 
27
35
  __version__ = "1.0.0"
28
36
 
@@ -44,6 +52,13 @@ __all__ = [
44
52
  "HTTPClient",
45
53
  "FabriqClient",
46
54
  "AuditEvent",
55
+ # Application auth helpers
56
+ "get_application_client",
57
+ "load_application_config",
58
+ "save_application_config",
59
+ "list_applications",
60
+ "delete_application_config",
61
+ "ApplicationNotFoundError",
47
62
  ]
48
63
 
49
64
  # Lazy expose dx submodule under af_sdk.dx
af_sdk/auth/__init__.py CHANGED
@@ -11,6 +11,15 @@ from .oauth import (
11
11
  TokenValidator,
12
12
  )
13
13
  from .token_cache import TokenManager, VaultClient
14
+ from .applications import (
15
+ get_application_client,
16
+ load_application_config,
17
+ save_application_config,
18
+ list_applications,
19
+ delete_application_config,
20
+ ApplicationNotFoundError,
21
+ AuthenticationError,
22
+ )
14
23
 
15
24
  # DPoP helper will be provided from af_sdk.auth.dpop
16
25
  try:
@@ -28,4 +37,11 @@ __all__ = [
28
37
  "TokenManager",
29
38
  "VaultClient",
30
39
  "create_dpop_proof",
40
+ "get_application_client",
41
+ "load_application_config",
42
+ "save_application_config",
43
+ "list_applications",
44
+ "delete_application_config",
45
+ "ApplicationNotFoundError",
46
+ "AuthenticationError",
31
47
  ]
@@ -0,0 +1,264 @@
1
+ """
2
+ Authentication helpers for Agentic Fabric SDK.
3
+
4
+ Provides utilities for loading application credentials and creating
5
+ authenticated clients.
6
+ """
7
+
8
+ from pathlib import Path
9
+ import json
10
+ import httpx
11
+ from typing import Optional, List, Dict
12
+ import logging
13
+
14
+ from .fabriq_client import FabriqClient
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class ApplicationNotFoundError(Exception):
20
+ """Raised when an application configuration is not found."""
21
+ pass
22
+
23
+
24
+ class AuthenticationError(Exception):
25
+ """Raised when authentication fails."""
26
+ pass
27
+
28
+
29
+ async def get_application_client(
30
+ app_id: str,
31
+ config_dir: Optional[Path] = None,
32
+ gateway_url: Optional[str] = None,
33
+ ) -> FabriqClient:
34
+ """
35
+ Get authenticated FabriqClient for an application.
36
+
37
+ Automatically loads credentials from ~/.af/applications/{app_id}.json
38
+ and exchanges them for a JWT token.
39
+
40
+ Args:
41
+ app_id: Application identifier (e.g., "my-slack-bot")
42
+ config_dir: Optional custom config directory (default: ~/.af)
43
+ gateway_url: Optional gateway URL override (default: from app config)
44
+
45
+ Returns:
46
+ Authenticated FabriqClient instance
47
+
48
+ Raises:
49
+ ApplicationNotFoundError: If application config doesn't exist
50
+ AuthenticationError: If authentication fails
51
+
52
+ Example:
53
+ >>> client = await get_application_client("my-slack-bot")
54
+ >>> result = await client.invoke_tool("slack-uuid", "post_message", {...})
55
+ """
56
+ # 1. Load application config
57
+ try:
58
+ app_config = load_application_config(app_id, config_dir)
59
+ except FileNotFoundError as e:
60
+ raise ApplicationNotFoundError(
61
+ f"Application '{app_id}' not found. "
62
+ f"Register it first with: afctl applications create --app-id {app_id} ..."
63
+ ) from e
64
+
65
+ # Use provided gateway_url or fall back to config
66
+ base_url = gateway_url or app_config.get("gateway_url", "https://dashboard.agenticfabriq.com")
67
+
68
+ # 2. Exchange credentials for JWT token
69
+ try:
70
+ async with httpx.AsyncClient() as http:
71
+ response = await http.post(
72
+ f"{base_url}/api/v1/applications/token",
73
+ json={
74
+ "app_id": app_config["app_id"],
75
+ "secret_key": app_config["secret_key"]
76
+ },
77
+ timeout=30.0
78
+ )
79
+
80
+ if response.status_code != 200:
81
+ error_detail = response.text
82
+ try:
83
+ error_json = response.json()
84
+ error_detail = error_json.get("detail", response.text)
85
+ except:
86
+ pass
87
+
88
+ raise AuthenticationError(
89
+ f"Failed to authenticate application '{app_id}': {error_detail}"
90
+ )
91
+
92
+ token_data = response.json()
93
+ except httpx.HTTPError as e:
94
+ raise AuthenticationError(
95
+ f"Network error while authenticating application '{app_id}': {e}"
96
+ ) from e
97
+
98
+ # 3. Create and return authenticated client
99
+ client = FabriqClient(
100
+ base_url=base_url,
101
+ auth_token=token_data["access_token"]
102
+ )
103
+
104
+ # Store metadata for potential refresh
105
+ client._app_id = app_id
106
+ client._expires_in = token_data.get("expires_in", 86400)
107
+
108
+ logger.info(
109
+ f"Authenticated as application '{app_id}' "
110
+ f"(user_id={token_data.get('user_id')}, tenant_id={token_data.get('tenant_id')})"
111
+ )
112
+
113
+ return client
114
+
115
+
116
+ def load_application_config(
117
+ app_id: str,
118
+ config_dir: Optional[Path] = None
119
+ ) -> Dict:
120
+ """
121
+ Load application config from disk.
122
+
123
+ Args:
124
+ app_id: Application identifier
125
+ config_dir: Optional custom config directory (default: ~/.af)
126
+
127
+ Returns:
128
+ Application configuration dictionary
129
+
130
+ Raises:
131
+ FileNotFoundError: If application config doesn't exist
132
+
133
+ Example:
134
+ >>> config = load_application_config("my-slack-bot")
135
+ >>> print(config["app_id"], config["created_at"])
136
+ """
137
+ if config_dir is None:
138
+ config_dir = Path.home() / ".af"
139
+
140
+ app_file = config_dir / "applications" / f"{app_id}.json"
141
+
142
+ if not app_file.exists():
143
+ raise FileNotFoundError(
144
+ f"Application '{app_id}' not found at {app_file}. "
145
+ f"Register it with: afctl applications create --app-id {app_id}"
146
+ )
147
+
148
+ with open(app_file, "r") as f:
149
+ return json.load(f)
150
+
151
+
152
+ def save_application_config(
153
+ app_id: str,
154
+ config: Dict,
155
+ config_dir: Optional[Path] = None
156
+ ) -> Path:
157
+ """
158
+ Save application config to disk.
159
+
160
+ Args:
161
+ app_id: Application identifier
162
+ config: Application configuration dictionary
163
+ config_dir: Optional custom config directory (default: ~/.af)
164
+
165
+ Returns:
166
+ Path to saved config file
167
+
168
+ Example:
169
+ >>> config = {
170
+ ... "app_id": "my-bot",
171
+ ... "secret_key": "sk_...",
172
+ ... "gateway_url": "https://dashboard.agenticfabriq.com"
173
+ ... }
174
+ >>> path = save_application_config("my-bot", config)
175
+ """
176
+ if config_dir is None:
177
+ config_dir = Path.home() / ".af"
178
+
179
+ # Create applications directory if it doesn't exist
180
+ app_dir = config_dir / "applications"
181
+ app_dir.mkdir(parents=True, exist_ok=True)
182
+
183
+ # Write config file
184
+ app_file = app_dir / f"{app_id}.json"
185
+ with open(app_file, "w") as f:
186
+ json.dump(config, f, indent=2)
187
+
188
+ # Secure the file (user read/write only)
189
+ app_file.chmod(0o600)
190
+
191
+ logger.info(f"Saved application config to {app_file}")
192
+
193
+ return app_file
194
+
195
+
196
+ def list_applications(
197
+ config_dir: Optional[Path] = None
198
+ ) -> List[Dict]:
199
+ """
200
+ List all registered applications.
201
+
202
+ Args:
203
+ config_dir: Optional custom config directory (default: ~/.af)
204
+
205
+ Returns:
206
+ List of application configuration dictionaries
207
+
208
+ Example:
209
+ >>> apps = list_applications()
210
+ >>> for app in apps:
211
+ ... print(f"{app['app_id']}: {app.get('name', 'N/A')}")
212
+ """
213
+ if config_dir is None:
214
+ config_dir = Path.home() / ".af"
215
+
216
+ app_dir = config_dir / "applications"
217
+
218
+ if not app_dir.exists():
219
+ return []
220
+
221
+ apps = []
222
+ for app_file in sorted(app_dir.glob("*.json")):
223
+ try:
224
+ with open(app_file, "r") as f:
225
+ app_config = json.load(f)
226
+ apps.append(app_config)
227
+ except Exception as e:
228
+ logger.warning(f"Failed to load application config from {app_file}: {e}")
229
+
230
+ return apps
231
+
232
+
233
+ def delete_application_config(
234
+ app_id: str,
235
+ config_dir: Optional[Path] = None
236
+ ) -> bool:
237
+ """
238
+ Delete application config from disk.
239
+
240
+ Args:
241
+ app_id: Application identifier
242
+ config_dir: Optional custom config directory (default: ~/.af)
243
+
244
+ Returns:
245
+ True if deleted, False if not found
246
+
247
+ Example:
248
+ >>> deleted = delete_application_config("my-old-bot")
249
+ >>> if deleted:
250
+ ... print("Deleted successfully")
251
+ """
252
+ if config_dir is None:
253
+ config_dir = Path.home() / ".af"
254
+
255
+ app_file = config_dir / "applications" / f"{app_id}.json"
256
+
257
+ if not app_file.exists():
258
+ return False
259
+
260
+ app_file.unlink()
261
+ logger.info(f"Deleted application config: {app_file}")
262
+
263
+ return True
264
+
@@ -0,0 +1,264 @@
1
+ """
2
+ Authentication helpers for Agentic Fabric SDK.
3
+
4
+ Provides utilities for loading application credentials and creating
5
+ authenticated clients.
6
+ """
7
+
8
+ from pathlib import Path
9
+ import json
10
+ import httpx
11
+ from typing import Optional, List, Dict
12
+ import logging
13
+
14
+ from ..fabriq_client import FabriqClient
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class ApplicationNotFoundError(Exception):
20
+ """Raised when an application configuration is not found."""
21
+ pass
22
+
23
+
24
+ class AuthenticationError(Exception):
25
+ """Raised when authentication fails."""
26
+ pass
27
+
28
+
29
+ async def get_application_client(
30
+ app_id: str,
31
+ config_dir: Optional[Path] = None,
32
+ gateway_url: Optional[str] = None,
33
+ ) -> FabriqClient:
34
+ """
35
+ Get authenticated FabriqClient for an application.
36
+
37
+ Automatically loads credentials from ~/.af/applications/{app_id}.json
38
+ and exchanges them for a JWT token.
39
+
40
+ Args:
41
+ app_id: Application identifier (e.g., "my-slack-bot")
42
+ config_dir: Optional custom config directory (default: ~/.af)
43
+ gateway_url: Optional gateway URL override (default: from app config)
44
+
45
+ Returns:
46
+ Authenticated FabriqClient instance
47
+
48
+ Raises:
49
+ ApplicationNotFoundError: If application config doesn't exist
50
+ AuthenticationError: If authentication fails
51
+
52
+ Example:
53
+ >>> client = await get_application_client("my-slack-bot")
54
+ >>> result = await client.invoke_tool("slack-uuid", "post_message", {...})
55
+ """
56
+ # 1. Load application config
57
+ try:
58
+ app_config = load_application_config(app_id, config_dir)
59
+ except FileNotFoundError as e:
60
+ raise ApplicationNotFoundError(
61
+ f"Application '{app_id}' not found. "
62
+ f"Register it first with: afctl applications create --app-id {app_id} ..."
63
+ ) from e
64
+
65
+ # Use provided gateway_url or fall back to config
66
+ base_url = gateway_url or app_config.get("gateway_url", "https://dashboard.agenticfabriq.com")
67
+
68
+ # 2. Exchange credentials for JWT token
69
+ try:
70
+ async with httpx.AsyncClient() as http:
71
+ response = await http.post(
72
+ f"{base_url}/api/v1/applications/token",
73
+ json={
74
+ "app_id": app_config["app_id"],
75
+ "secret_key": app_config["secret_key"]
76
+ },
77
+ timeout=30.0
78
+ )
79
+
80
+ if response.status_code != 200:
81
+ error_detail = response.text
82
+ try:
83
+ error_json = response.json()
84
+ error_detail = error_json.get("detail", response.text)
85
+ except:
86
+ pass
87
+
88
+ raise AuthenticationError(
89
+ f"Failed to authenticate application '{app_id}': {error_detail}"
90
+ )
91
+
92
+ token_data = response.json()
93
+ except httpx.HTTPError as e:
94
+ raise AuthenticationError(
95
+ f"Network error while authenticating application '{app_id}': {e}"
96
+ ) from e
97
+
98
+ # 3. Create and return authenticated client
99
+ client = FabriqClient(
100
+ base_url=base_url,
101
+ auth_token=token_data["access_token"]
102
+ )
103
+
104
+ # Store metadata for potential refresh
105
+ client._app_id = app_id
106
+ client._expires_in = token_data.get("expires_in", 86400)
107
+
108
+ logger.info(
109
+ f"Authenticated as application '{app_id}' "
110
+ f"(user_id={token_data.get('user_id')}, tenant_id={token_data.get('tenant_id')})"
111
+ )
112
+
113
+ return client
114
+
115
+
116
+ def load_application_config(
117
+ app_id: str,
118
+ config_dir: Optional[Path] = None
119
+ ) -> Dict:
120
+ """
121
+ Load application config from disk.
122
+
123
+ Args:
124
+ app_id: Application identifier
125
+ config_dir: Optional custom config directory (default: ~/.af)
126
+
127
+ Returns:
128
+ Application configuration dictionary
129
+
130
+ Raises:
131
+ FileNotFoundError: If application config doesn't exist
132
+
133
+ Example:
134
+ >>> config = load_application_config("my-slack-bot")
135
+ >>> print(config["app_id"], config["created_at"])
136
+ """
137
+ if config_dir is None:
138
+ config_dir = Path.home() / ".af"
139
+
140
+ app_file = config_dir / "applications" / f"{app_id}.json"
141
+
142
+ if not app_file.exists():
143
+ raise FileNotFoundError(
144
+ f"Application '{app_id}' not found at {app_file}. "
145
+ f"Register it with: afctl applications create --app-id {app_id}"
146
+ )
147
+
148
+ with open(app_file, "r") as f:
149
+ return json.load(f)
150
+
151
+
152
+ def save_application_config(
153
+ app_id: str,
154
+ config: Dict,
155
+ config_dir: Optional[Path] = None
156
+ ) -> Path:
157
+ """
158
+ Save application config to disk.
159
+
160
+ Args:
161
+ app_id: Application identifier
162
+ config: Application configuration dictionary
163
+ config_dir: Optional custom config directory (default: ~/.af)
164
+
165
+ Returns:
166
+ Path to saved config file
167
+
168
+ Example:
169
+ >>> config = {
170
+ ... "app_id": "my-bot",
171
+ ... "secret_key": "sk_...",
172
+ ... "gateway_url": "https://dashboard.agenticfabriq.com"
173
+ ... }
174
+ >>> path = save_application_config("my-bot", config)
175
+ """
176
+ if config_dir is None:
177
+ config_dir = Path.home() / ".af"
178
+
179
+ # Create applications directory if it doesn't exist
180
+ app_dir = config_dir / "applications"
181
+ app_dir.mkdir(parents=True, exist_ok=True)
182
+
183
+ # Write config file
184
+ app_file = app_dir / f"{app_id}.json"
185
+ with open(app_file, "w") as f:
186
+ json.dump(config, f, indent=2)
187
+
188
+ # Secure the file (user read/write only)
189
+ app_file.chmod(0o600)
190
+
191
+ logger.info(f"Saved application config to {app_file}")
192
+
193
+ return app_file
194
+
195
+
196
+ def list_applications(
197
+ config_dir: Optional[Path] = None
198
+ ) -> List[Dict]:
199
+ """
200
+ List all registered applications.
201
+
202
+ Args:
203
+ config_dir: Optional custom config directory (default: ~/.af)
204
+
205
+ Returns:
206
+ List of application configuration dictionaries
207
+
208
+ Example:
209
+ >>> apps = list_applications()
210
+ >>> for app in apps:
211
+ ... print(f"{app['app_id']}: {app.get('name', 'N/A')}")
212
+ """
213
+ if config_dir is None:
214
+ config_dir = Path.home() / ".af"
215
+
216
+ app_dir = config_dir / "applications"
217
+
218
+ if not app_dir.exists():
219
+ return []
220
+
221
+ apps = []
222
+ for app_file in sorted(app_dir.glob("*.json")):
223
+ try:
224
+ with open(app_file, "r") as f:
225
+ app_config = json.load(f)
226
+ apps.append(app_config)
227
+ except Exception as e:
228
+ logger.warning(f"Failed to load application config from {app_file}: {e}")
229
+
230
+ return apps
231
+
232
+
233
+ def delete_application_config(
234
+ app_id: str,
235
+ config_dir: Optional[Path] = None
236
+ ) -> bool:
237
+ """
238
+ Delete application config from disk.
239
+
240
+ Args:
241
+ app_id: Application identifier
242
+ config_dir: Optional custom config directory (default: ~/.af)
243
+
244
+ Returns:
245
+ True if deleted, False if not found
246
+
247
+ Example:
248
+ >>> deleted = delete_application_config("my-old-bot")
249
+ >>> if deleted:
250
+ ... print("Deleted successfully")
251
+ """
252
+ if config_dir is None:
253
+ config_dir = Path.home() / ".af"
254
+
255
+ app_file = config_dir / "applications" / f"{app_id}.json"
256
+
257
+ if not app_file.exists():
258
+ return False
259
+
260
+ app_file.unlink()
261
+ logger.info(f"Deleted application config: {app_file}")
262
+
263
+ return True
264
+