agentic-fabriq-sdk 0.1.5__py3-none-any.whl → 0.1.6__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/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """
2
+ Agentic Fabric CLI Tool
3
+
4
+ A command-line interface for managing Agentic Fabric resources including
5
+ agents, tools, MCP servers, and administrative operations.
6
+ """
7
+
8
+ __version__ = "1.0.0"
@@ -0,0 +1,3 @@
1
+ """
2
+ CLI commands for the Agentic Fabric CLI.
3
+ """
@@ -0,0 +1,238 @@
1
+ """
2
+ Agent management commands for the Agentic Fabric CLI.
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+
9
+ from af_cli.core.client import get_client
10
+ from af_cli.core.output import error, info, print_output, success, warning, prompt_confirm
11
+
12
+ app = typer.Typer(help="Agent management commands")
13
+
14
+
15
+ @app.command()
16
+ def list(
17
+ page: int = typer.Option(1, "--page", "-p", help="Page number"),
18
+ page_size: int = typer.Option(20, "--page-size", "-s", help="Page size"),
19
+ search: Optional[str] = typer.Option(None, "--search", "-q", help="Search query"),
20
+ format: str = typer.Option("table", "--format", "-f", help="Output format"),
21
+ ):
22
+ """List agents."""
23
+ try:
24
+ with get_client() as client:
25
+ params = {
26
+ "page": page,
27
+ "page_size": page_size,
28
+ }
29
+
30
+ if search:
31
+ params["search"] = search
32
+
33
+ response = client.get("/api/v1/agents", params=params)
34
+
35
+ # Support both new (items/total) and legacy (agents/total) shapes
36
+ if isinstance(response, dict):
37
+ if "items" in response:
38
+ agents = response.get("items", [])
39
+ total = response.get("total", len(agents))
40
+ elif "agents" in response:
41
+ agents = response.get("agents", [])
42
+ total = response.get("total", len(agents))
43
+ else:
44
+ agents = []
45
+ total = 0
46
+ elif isinstance(response, list):
47
+ agents = response
48
+ total = len(agents)
49
+ else:
50
+ agents = []
51
+ total = 0
52
+
53
+ if not agents:
54
+ warning("No agents found")
55
+ return
56
+
57
+ # Format agent data for display
58
+ display_data = []
59
+ for agent in agents:
60
+ display_data.append({
61
+ "id": agent.get("id"),
62
+ "name": agent.get("name"),
63
+ "version": agent.get("version"),
64
+ "protocol": agent.get("protocol"),
65
+ "endpoint_url": agent.get("endpoint_url"),
66
+ "auth_method": agent.get("auth_method"),
67
+ "created_at": (agent.get("created_at") or "")[:19], # Trim microseconds if present
68
+ })
69
+
70
+ print_output(
71
+ display_data,
72
+ format_type=format,
73
+ columns=["id", "name", "version", "protocol", "endpoint_url", "auth_method", "created_at"],
74
+ title=f"Agents ({len(agents)}/{total})"
75
+ )
76
+
77
+ except Exception as e:
78
+ error(f"Failed to list agents: {e}")
79
+ raise typer.Exit(1)
80
+
81
+
82
+ @app.command()
83
+ def get(
84
+ agent_id: str = typer.Argument(..., help="Agent ID"),
85
+ format: str = typer.Option("table", "--format", "-f", help="Output format"),
86
+ ):
87
+ """Get agent details."""
88
+ try:
89
+ with get_client() as client:
90
+ agent = client.get(f"/api/v1/agents/{agent_id}")
91
+
92
+ print_output(
93
+ agent,
94
+ format_type=format,
95
+ title=f"Agent {agent_id}"
96
+ )
97
+
98
+ except Exception as e:
99
+ error(f"Failed to get agent: {e}")
100
+ raise typer.Exit(1)
101
+
102
+
103
+ @app.command()
104
+ def create(
105
+ name: str = typer.Option(..., "--name", "-n", help="Agent name"),
106
+ description: Optional[str] = typer.Option(None, "--description", "-d", help="Agent description"),
107
+ version: str = typer.Option("1.0.0", "--version", "-v", help="Agent version"),
108
+ protocol: str = typer.Option("HTTP", "--protocol", help="Agent protocol"),
109
+ endpoint_url: str = typer.Option(..., "--endpoint-url", "-u", help="Agent endpoint URL"),
110
+ auth_method: str = typer.Option("OAUTH2", "--auth-method", "-a", help="Authentication method"),
111
+ ):
112
+ """Create a new agent."""
113
+ try:
114
+ with get_client() as client:
115
+ data = {
116
+ "name": name,
117
+ "description": description,
118
+ "version": version,
119
+ "protocol": protocol,
120
+ "endpoint_url": endpoint_url,
121
+ "auth_method": auth_method,
122
+ }
123
+
124
+ agent = client.post("/api/v1/agents", data)
125
+
126
+ success(f"Agent created: {agent['id']}")
127
+ info(f"Name: {agent['name']}")
128
+ info(f"Endpoint: {agent['endpoint_url']}")
129
+
130
+ except Exception as e:
131
+ error(f"Failed to create agent: {e}")
132
+ raise typer.Exit(1)
133
+
134
+
135
+ @app.command()
136
+ def update(
137
+ agent_id: str = typer.Argument(..., help="Agent ID"),
138
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Agent name"),
139
+ description: Optional[str] = typer.Option(None, "--description", "-d", help="Agent description"),
140
+ version: Optional[str] = typer.Option(None, "--version", "-v", help="Agent version"),
141
+ protocol: Optional[str] = typer.Option(None, "--protocol", help="Agent protocol"),
142
+ endpoint_url: Optional[str] = typer.Option(None, "--endpoint-url", "-u", help="Agent endpoint URL"),
143
+ auth_method: Optional[str] = typer.Option(None, "--auth-method", "-a", help="Authentication method"),
144
+ ):
145
+ """Update an agent."""
146
+ try:
147
+ with get_client() as client:
148
+ data = {}
149
+
150
+ if name is not None:
151
+ data["name"] = name
152
+ if description is not None:
153
+ data["description"] = description
154
+ if version is not None:
155
+ data["version"] = version
156
+ if protocol is not None:
157
+ data["protocol"] = protocol
158
+ if endpoint_url is not None:
159
+ data["endpoint_url"] = endpoint_url
160
+ if auth_method is not None:
161
+ data["auth_method"] = auth_method
162
+
163
+ if not data:
164
+ error("No update data provided")
165
+ raise typer.Exit(1)
166
+
167
+ agent = client.put(f"/api/v1/agents/{agent_id}", data)
168
+
169
+ success(f"Agent updated: {agent['id']}")
170
+ info(f"Name: {agent['name']}")
171
+ info(f"Endpoint: {agent['endpoint_url']}")
172
+
173
+ except Exception as e:
174
+ error(f"Failed to update agent: {e}")
175
+ raise typer.Exit(1)
176
+
177
+
178
+ @app.command()
179
+ def delete(
180
+ agent_id: str = typer.Argument(..., help="Agent ID"),
181
+ force: bool = typer.Option(False, "--force", "-f", help="Force deletion without confirmation"),
182
+ ):
183
+ """Delete an agent."""
184
+ try:
185
+ if not force:
186
+ if not prompt_confirm(f"Are you sure you want to delete agent {agent_id}?"):
187
+ info("Deletion cancelled")
188
+ return
189
+
190
+ with get_client() as client:
191
+ client.delete(f"/api/v1/agents/{agent_id}")
192
+
193
+ success(f"Agent deleted: {agent_id}")
194
+
195
+ except Exception as e:
196
+ error(f"Failed to delete agent: {e}")
197
+ raise typer.Exit(1)
198
+
199
+
200
+ @app.command()
201
+ def invoke(
202
+ agent_id: str = typer.Argument(..., help="Agent ID"),
203
+ input_text: str = typer.Option(..., "--input", "-i", help="Input message for the agent"),
204
+ format: str = typer.Option("table", "--format", "-f", help="Output format"),
205
+ ):
206
+ """Invoke an agent."""
207
+ try:
208
+ with get_client() as client:
209
+ data = {
210
+ "input": input_text,
211
+ "parameters": {},
212
+ "context": {},
213
+ }
214
+
215
+ info(f"Invoking agent {agent_id}...")
216
+ response = client.post(f"/api/v1/agents/{agent_id}/invoke", data)
217
+
218
+ success("Agent invoked successfully")
219
+
220
+ # Display response
221
+ if format == "table":
222
+ info("Response:")
223
+ print(response["output"])
224
+
225
+ if response.get("metadata"):
226
+ info("\nMetadata:")
227
+ print_output(response["metadata"], format_type="yaml")
228
+
229
+ if response.get("logs"):
230
+ info("\nLogs:")
231
+ for log in response["logs"]:
232
+ print(f" {log}")
233
+ else:
234
+ print_output(response, format_type=format)
235
+
236
+ except Exception as e:
237
+ error(f"Failed to invoke agent: {e}")
238
+ raise typer.Exit(1)
@@ -0,0 +1,388 @@
1
+ """
2
+ Authentication commands for the Agentic Fabric CLI.
3
+
4
+ This module provides OAuth2/PKCE-based authentication commands for secure
5
+ login without requiring client secrets.
6
+ """
7
+
8
+ import time
9
+ from typing import Optional
10
+
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+
15
+ from af_cli.core.config import get_config
16
+ from af_cli.core.oauth import OAuth2Client
17
+ from af_cli.core.output import error, info, success, warning
18
+ from af_cli.core.token_storage import TokenData, get_token_storage
19
+
20
+ app = typer.Typer(help="Authentication commands")
21
+ console = Console()
22
+
23
+
24
+ def get_oauth_client(keycloak_url: Optional[str] = None) -> OAuth2Client:
25
+ """
26
+ Get configured OAuth2 client.
27
+
28
+ Args:
29
+ keycloak_url: Override Keycloak URL from config
30
+
31
+ Returns:
32
+ Configured OAuth2Client instance
33
+ """
34
+ config = get_config()
35
+
36
+ # Get Keycloak URL from environment or config
37
+ keycloak_url = keycloak_url or config.dict().get('keycloak_url', 'https://auth.agenticfabriq.com')
38
+ realm = config.dict().get('keycloak_realm', 'agentic-fabric')
39
+ client_id = config.dict().get('keycloak_client_id', 'agentic-fabriq-cli')
40
+
41
+ return OAuth2Client(
42
+ keycloak_url=keycloak_url,
43
+ realm=realm,
44
+ client_id=client_id,
45
+ scopes=['openid', 'profile', 'email']
46
+ )
47
+
48
+
49
+ @app.command()
50
+ def login(
51
+ tenant_id: Optional[str] = typer.Option(
52
+ None,
53
+ "--tenant-id",
54
+ "-t",
55
+ help="Tenant ID (optional, can be extracted from JWT)"
56
+ ),
57
+ browser: bool = typer.Option(
58
+ True,
59
+ "--browser/--no-browser",
60
+ help="Open browser for authentication"
61
+ ),
62
+ keycloak_url: Optional[str] = typer.Option(
63
+ None,
64
+ "--keycloak-url",
65
+ "-k",
66
+ help="Keycloak URL (default: https://auth.agenticfabriq.com or from config)"
67
+ ),
68
+ ):
69
+ """
70
+ Login to Agentic Fabric using OAuth2/PKCE flow.
71
+
72
+ This command will open your default browser and prompt you to authenticate
73
+ with your Keycloak credentials. Once authenticated, your tokens will be
74
+ securely stored for future use.
75
+ """
76
+ config = get_config()
77
+ token_storage = get_token_storage()
78
+
79
+ # Check if already authenticated
80
+ existing_token = token_storage.load()
81
+ if existing_token and not token_storage.is_token_expired(existing_token):
82
+ user_display = existing_token.email or existing_token.name or existing_token.user_id
83
+ info(f"Already authenticated as: {user_display}")
84
+ info(f"Tenant: {existing_token.tenant_id or 'Unknown'}")
85
+
86
+ if not typer.confirm("Do you want to login again?"):
87
+ return
88
+
89
+ try:
90
+ # Get OAuth2 client
91
+ oauth_client = get_oauth_client(keycloak_url)
92
+
93
+ # Perform login
94
+ console.print()
95
+ # Use direct localhost redirect until branded page is deployed
96
+ tokens = oauth_client.login(open_browser=browser, timeout=300, use_hosted_callback=False)
97
+
98
+ # Extract and save token data
99
+ token_data = token_storage.extract_token_info(tokens)
100
+
101
+ # Override tenant_id if provided
102
+ if tenant_id:
103
+ token_data.tenant_id = tenant_id
104
+
105
+ # Save tokens
106
+ token_storage.save(token_data)
107
+
108
+ # Update config
109
+ config.access_token = token_data.access_token
110
+ config.refresh_token = token_data.refresh_token
111
+ config.token_expires_at = token_data.expires_at
112
+ if token_data.tenant_id:
113
+ config.tenant_id = token_data.tenant_id
114
+ config.save()
115
+
116
+ # Display success message
117
+ console.print()
118
+ success("Successfully authenticated!")
119
+
120
+ if token_data.name or token_data.email:
121
+ user_display = f"{token_data.name}" if token_data.name else ""
122
+ if token_data.email:
123
+ user_display += f" ({token_data.email})" if user_display else token_data.email
124
+ info(f"User: {user_display}")
125
+
126
+ if token_data.tenant_id:
127
+ info(f"Tenant: {token_data.tenant_id}")
128
+
129
+ expires_in = token_data.expires_at - int(time.time())
130
+ info(f"Token expires in {expires_in // 60} minutes")
131
+
132
+ except Exception as e:
133
+ console.print()
134
+ error(f"Authentication failed: {e}")
135
+ raise typer.Exit(1)
136
+
137
+
138
+ @app.command()
139
+ def logout(
140
+ keycloak_url: Optional[str] = typer.Option(
141
+ None,
142
+ "--keycloak-url",
143
+ "-k",
144
+ help="Keycloak URL (default: https://auth.agenticfabriq.com or from config)"
145
+ ),
146
+ ):
147
+ """
148
+ Logout from Agentic Fabric.
149
+
150
+ This command will revoke your tokens and clear your local authentication state.
151
+ """
152
+ config = get_config()
153
+ token_storage = get_token_storage()
154
+
155
+ # Load tokens
156
+ token_data = token_storage.load()
157
+
158
+ if not token_data:
159
+ warning("Not authenticated")
160
+ return
161
+
162
+ try:
163
+ # Revoke tokens with Keycloak
164
+ if token_data.refresh_token:
165
+ oauth_client = get_oauth_client(keycloak_url)
166
+ oauth_client.logout(token_data.refresh_token)
167
+
168
+ except Exception as e:
169
+ warning(f"Server logout failed (continuing with local logout): {e}")
170
+
171
+ # Clear local tokens
172
+ token_storage.delete()
173
+ config.clear_auth()
174
+
175
+ success("Successfully logged out")
176
+
177
+
178
+ @app.command()
179
+ def status():
180
+ """
181
+ Show authentication status and token information.
182
+
183
+ Displays current authentication state, user information, and token expiration.
184
+ """
185
+ config = get_config()
186
+ token_storage = get_token_storage()
187
+
188
+ # Load token data
189
+ token_data = token_storage.load()
190
+
191
+ if not token_data:
192
+ warning("Not authenticated")
193
+ info("Run 'af auth login' to authenticate")
194
+ return
195
+
196
+ # Create status table
197
+ table = Table(title="Authentication Status", show_header=False)
198
+ table.add_column("Field", style="cyan", width=20)
199
+ table.add_column("Value", style="white")
200
+
201
+ # Authentication status
202
+ is_expired = token_storage.is_token_expired(token_data)
203
+ if is_expired:
204
+ table.add_row("Status", "[red]Expired[/red]")
205
+ else:
206
+ table.add_row("Status", "[green]✓ Authenticated[/green]")
207
+
208
+ # User information
209
+ if token_data.name:
210
+ table.add_row("Name", token_data.name)
211
+ if token_data.email:
212
+ table.add_row("Email", token_data.email)
213
+ if token_data.user_id:
214
+ table.add_row("User ID", token_data.user_id)
215
+ if token_data.tenant_id:
216
+ table.add_row("Tenant ID", token_data.tenant_id)
217
+
218
+ # Token expiration
219
+ if token_data.expires_at:
220
+ expires_in = token_data.expires_at - int(time.time())
221
+ if expires_in > 0:
222
+ minutes = expires_in // 60
223
+ seconds = expires_in % 60
224
+ table.add_row("Expires in", f"{minutes}m {seconds}s")
225
+ else:
226
+ table.add_row("Expired", f"{-expires_in // 60} minutes ago")
227
+
228
+ # Refresh token availability
229
+ if token_data.refresh_token:
230
+ table.add_row("Refresh Token", "[green]Available[/green]")
231
+ else:
232
+ table.add_row("Refresh Token", "[red]Not available[/red]")
233
+
234
+ # Gateway URL
235
+ if config.gateway_url:
236
+ table.add_row("Gateway URL", config.gateway_url)
237
+
238
+ console.print()
239
+ console.print(table)
240
+ console.print()
241
+
242
+ # Show recommendations
243
+ if is_expired:
244
+ if token_data.refresh_token:
245
+ info("Token has expired. Run 'af auth refresh' to get a new token")
246
+ else:
247
+ info("Token has expired. Run 'af auth login' to re-authenticate")
248
+
249
+
250
+ @app.command()
251
+ def refresh(
252
+ keycloak_url: Optional[str] = typer.Option(
253
+ None,
254
+ "--keycloak-url",
255
+ "-k",
256
+ help="Keycloak URL (default: https://auth.agenticfabriq.com or from config)"
257
+ ),
258
+ ):
259
+ """
260
+ Refresh authentication token.
261
+
262
+ Uses the refresh token to obtain a new access token without requiring
263
+ interactive login.
264
+ """
265
+ config = get_config()
266
+ token_storage = get_token_storage()
267
+
268
+ # Load tokens
269
+ token_data = token_storage.load()
270
+
271
+ if not token_data or not token_data.refresh_token:
272
+ error("No refresh token available")
273
+ info("Run 'af auth login' to authenticate")
274
+ raise typer.Exit(1)
275
+
276
+ try:
277
+ # Get OAuth2 client
278
+ oauth_client = get_oauth_client(keycloak_url)
279
+
280
+ # Refresh tokens
281
+ info("Refreshing token...")
282
+ new_tokens = oauth_client.refresh_token(token_data.refresh_token)
283
+
284
+ # Extract and save new token data
285
+ new_token_data = token_storage.extract_token_info(new_tokens)
286
+
287
+ # Preserve tenant_id if not in new token
288
+ if not new_token_data.tenant_id and token_data.tenant_id:
289
+ new_token_data.tenant_id = token_data.tenant_id
290
+
291
+ # Save new tokens
292
+ token_storage.save(new_token_data)
293
+
294
+ # Update config
295
+ config.access_token = new_token_data.access_token
296
+ config.refresh_token = new_token_data.refresh_token
297
+ config.token_expires_at = new_token_data.expires_at
298
+ config.save()
299
+
300
+ success("Token refreshed successfully")
301
+
302
+ expires_in = new_token_data.expires_at - int(time.time())
303
+ info(f"New token expires in {expires_in // 60} minutes")
304
+
305
+ except Exception as e:
306
+ error(f"Token refresh failed: {e}")
307
+ error("Please run 'af auth login' to re-authenticate")
308
+
309
+ # Clear invalid tokens
310
+ token_storage.delete()
311
+ config.clear_auth()
312
+
313
+ raise typer.Exit(1)
314
+
315
+
316
+ @app.command()
317
+ def token(
318
+ show_full: bool = typer.Option(
319
+ False,
320
+ "--full",
321
+ "-f",
322
+ help="Show full token (warning: sensitive information)"
323
+ ),
324
+ ):
325
+ """
326
+ Display current access token.
327
+
328
+ Use --full to see the complete token (warning: contains sensitive information).
329
+ """
330
+ config = get_config()
331
+ token_storage = get_token_storage()
332
+
333
+ # Load token data
334
+ token_data = token_storage.load()
335
+
336
+ if not token_data:
337
+ error("Not authenticated")
338
+ raise typer.Exit(1)
339
+
340
+ if show_full:
341
+ console.print("\n[bold yellow]Warning: Sensitive information below[/bold yellow]\n")
342
+ console.print(f"Access token: {token_data.access_token}\n")
343
+ else:
344
+ # Show truncated token
345
+ token_preview = token_data.access_token[:50] + "..." if len(token_data.access_token) > 50 else token_data.access_token
346
+ console.print(f"\nAccess token: {token_preview}\n")
347
+ info("Use --full to see complete token")
348
+
349
+ # Check expiration
350
+ if token_storage.is_token_expired(token_data):
351
+ warning("Token has expired")
352
+ else:
353
+ expires_in = token_data.expires_at - int(time.time())
354
+ info(f"Expires in: {expires_in // 60} minutes")
355
+
356
+
357
+ @app.command()
358
+ def whoami():
359
+ """
360
+ Display information about the currently authenticated user.
361
+ """
362
+ token_storage = get_token_storage()
363
+
364
+ # Load token data
365
+ token_data = token_storage.load()
366
+
367
+ if not token_data:
368
+ warning("Not authenticated")
369
+ info("Run 'af auth login' to authenticate")
370
+ return
371
+
372
+ # Create user info table
373
+ table = Table(title="Current User", show_header=False)
374
+ table.add_column("Field", style="cyan", width=15)
375
+ table.add_column("Value", style="white")
376
+
377
+ if token_data.name:
378
+ table.add_row("Name", token_data.name)
379
+ if token_data.email:
380
+ table.add_row("Email", token_data.email)
381
+ if token_data.user_id:
382
+ table.add_row("User ID", token_data.user_id)
383
+ if token_data.tenant_id:
384
+ table.add_row("Tenant", token_data.tenant_id)
385
+
386
+ console.print()
387
+ console.print(table)
388
+ console.print()