agentic-fabriq-sdk 0.1.5__py3-none-any.whl → 0.1.7__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,390 @@
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, otherwise use default for Keycloak JWTs
102
+ if tenant_id:
103
+ token_data.tenant_id = tenant_id
104
+ elif not token_data.tenant_id:
105
+ # Keycloak JWTs don't have tenant_id, use the same default as the backend
106
+ token_data.tenant_id = "550e8400-e29b-41d4-a716-446655440000"
107
+
108
+ # Save tokens
109
+ token_storage.save(token_data)
110
+
111
+ # Update config
112
+ config.access_token = token_data.access_token
113
+ config.refresh_token = token_data.refresh_token
114
+ config.token_expires_at = token_data.expires_at
115
+ config.tenant_id = token_data.tenant_id # Always set tenant_id
116
+ config.save()
117
+
118
+ # Display success message
119
+ console.print()
120
+ success("Successfully authenticated!")
121
+
122
+ if token_data.name or token_data.email:
123
+ user_display = f"{token_data.name}" if token_data.name else ""
124
+ if token_data.email:
125
+ user_display += f" ({token_data.email})" if user_display else token_data.email
126
+ info(f"User: {user_display}")
127
+
128
+ if token_data.tenant_id:
129
+ info(f"Tenant: {token_data.tenant_id}")
130
+
131
+ expires_in = token_data.expires_at - int(time.time())
132
+ info(f"Token expires in {expires_in // 60} minutes")
133
+
134
+ except Exception as e:
135
+ console.print()
136
+ error(f"Authentication failed: {e}")
137
+ raise typer.Exit(1)
138
+
139
+
140
+ @app.command()
141
+ def logout(
142
+ keycloak_url: Optional[str] = typer.Option(
143
+ None,
144
+ "--keycloak-url",
145
+ "-k",
146
+ help="Keycloak URL (default: https://auth.agenticfabriq.com or from config)"
147
+ ),
148
+ ):
149
+ """
150
+ Logout from Agentic Fabric.
151
+
152
+ This command will revoke your tokens and clear your local authentication state.
153
+ """
154
+ config = get_config()
155
+ token_storage = get_token_storage()
156
+
157
+ # Load tokens
158
+ token_data = token_storage.load()
159
+
160
+ if not token_data:
161
+ warning("Not authenticated")
162
+ return
163
+
164
+ try:
165
+ # Revoke tokens with Keycloak
166
+ if token_data.refresh_token:
167
+ oauth_client = get_oauth_client(keycloak_url)
168
+ oauth_client.logout(token_data.refresh_token)
169
+
170
+ except Exception as e:
171
+ warning(f"Server logout failed (continuing with local logout): {e}")
172
+
173
+ # Clear local tokens
174
+ token_storage.delete()
175
+ config.clear_auth()
176
+
177
+ success("Successfully logged out")
178
+
179
+
180
+ @app.command()
181
+ def status():
182
+ """
183
+ Show authentication status and token information.
184
+
185
+ Displays current authentication state, user information, and token expiration.
186
+ """
187
+ config = get_config()
188
+ token_storage = get_token_storage()
189
+
190
+ # Load token data
191
+ token_data = token_storage.load()
192
+
193
+ if not token_data:
194
+ warning("Not authenticated")
195
+ info("Run 'afctl auth login' to authenticate")
196
+ return
197
+
198
+ # Create status table
199
+ table = Table(title="Authentication Status", show_header=False)
200
+ table.add_column("Field", style="cyan", width=20)
201
+ table.add_column("Value", style="white")
202
+
203
+ # Authentication status
204
+ is_expired = token_storage.is_token_expired(token_data)
205
+ if is_expired:
206
+ table.add_row("Status", "[red]Expired[/red]")
207
+ else:
208
+ table.add_row("Status", "[green]✓ Authenticated[/green]")
209
+
210
+ # User information
211
+ if token_data.name:
212
+ table.add_row("Name", token_data.name)
213
+ if token_data.email:
214
+ table.add_row("Email", token_data.email)
215
+ if token_data.user_id:
216
+ table.add_row("User ID", token_data.user_id)
217
+ if token_data.tenant_id:
218
+ table.add_row("Tenant ID", token_data.tenant_id)
219
+
220
+ # Token expiration
221
+ if token_data.expires_at:
222
+ expires_in = token_data.expires_at - int(time.time())
223
+ if expires_in > 0:
224
+ minutes = expires_in // 60
225
+ seconds = expires_in % 60
226
+ table.add_row("Expires in", f"{minutes}m {seconds}s")
227
+ else:
228
+ table.add_row("Expired", f"{-expires_in // 60} minutes ago")
229
+
230
+ # Refresh token availability
231
+ if token_data.refresh_token:
232
+ table.add_row("Refresh Token", "[green]Available[/green]")
233
+ else:
234
+ table.add_row("Refresh Token", "[red]Not available[/red]")
235
+
236
+ # Gateway URL
237
+ if config.gateway_url:
238
+ table.add_row("Gateway URL", config.gateway_url)
239
+
240
+ console.print()
241
+ console.print(table)
242
+ console.print()
243
+
244
+ # Show recommendations
245
+ if is_expired:
246
+ if token_data.refresh_token:
247
+ info("Token has expired. Run 'afctl auth refresh' to get a new token")
248
+ else:
249
+ info("Token has expired. Run 'afctl auth login' to re-authenticate")
250
+
251
+
252
+ @app.command()
253
+ def refresh(
254
+ keycloak_url: Optional[str] = typer.Option(
255
+ None,
256
+ "--keycloak-url",
257
+ "-k",
258
+ help="Keycloak URL (default: https://auth.agenticfabriq.com or from config)"
259
+ ),
260
+ ):
261
+ """
262
+ Refresh authentication token.
263
+
264
+ Uses the refresh token to obtain a new access token without requiring
265
+ interactive login.
266
+ """
267
+ config = get_config()
268
+ token_storage = get_token_storage()
269
+
270
+ # Load tokens
271
+ token_data = token_storage.load()
272
+
273
+ if not token_data or not token_data.refresh_token:
274
+ error("No refresh token available")
275
+ info("Run 'afctl auth login' to authenticate")
276
+ raise typer.Exit(1)
277
+
278
+ try:
279
+ # Get OAuth2 client
280
+ oauth_client = get_oauth_client(keycloak_url)
281
+
282
+ # Refresh tokens
283
+ info("Refreshing token...")
284
+ new_tokens = oauth_client.refresh_token(token_data.refresh_token)
285
+
286
+ # Extract and save new token data
287
+ new_token_data = token_storage.extract_token_info(new_tokens)
288
+
289
+ # Preserve tenant_id if not in new token
290
+ if not new_token_data.tenant_id and token_data.tenant_id:
291
+ new_token_data.tenant_id = token_data.tenant_id
292
+
293
+ # Save new tokens
294
+ token_storage.save(new_token_data)
295
+
296
+ # Update config
297
+ config.access_token = new_token_data.access_token
298
+ config.refresh_token = new_token_data.refresh_token
299
+ config.token_expires_at = new_token_data.expires_at
300
+ config.save()
301
+
302
+ success("Token refreshed successfully")
303
+
304
+ expires_in = new_token_data.expires_at - int(time.time())
305
+ info(f"New token expires in {expires_in // 60} minutes")
306
+
307
+ except Exception as e:
308
+ error(f"Token refresh failed: {e}")
309
+ error("Please run 'afctl auth login' to re-authenticate")
310
+
311
+ # Clear invalid tokens
312
+ token_storage.delete()
313
+ config.clear_auth()
314
+
315
+ raise typer.Exit(1)
316
+
317
+
318
+ @app.command()
319
+ def token(
320
+ show_full: bool = typer.Option(
321
+ False,
322
+ "--full",
323
+ "-f",
324
+ help="Show full token (warning: sensitive information)"
325
+ ),
326
+ ):
327
+ """
328
+ Display current access token.
329
+
330
+ Use --full to see the complete token (warning: contains sensitive information).
331
+ """
332
+ config = get_config()
333
+ token_storage = get_token_storage()
334
+
335
+ # Load token data
336
+ token_data = token_storage.load()
337
+
338
+ if not token_data:
339
+ error("Not authenticated")
340
+ raise typer.Exit(1)
341
+
342
+ if show_full:
343
+ console.print("\n[bold yellow]Warning: Sensitive information below[/bold yellow]\n")
344
+ console.print(f"Access token: {token_data.access_token}\n")
345
+ else:
346
+ # Show truncated token
347
+ token_preview = token_data.access_token[:50] + "..." if len(token_data.access_token) > 50 else token_data.access_token
348
+ console.print(f"\nAccess token: {token_preview}\n")
349
+ info("Use --full to see complete token")
350
+
351
+ # Check expiration
352
+ if token_storage.is_token_expired(token_data):
353
+ warning("Token has expired")
354
+ else:
355
+ expires_in = token_data.expires_at - int(time.time())
356
+ info(f"Expires in: {expires_in // 60} minutes")
357
+
358
+
359
+ @app.command()
360
+ def whoami():
361
+ """
362
+ Display information about the currently authenticated user.
363
+ """
364
+ token_storage = get_token_storage()
365
+
366
+ # Load token data
367
+ token_data = token_storage.load()
368
+
369
+ if not token_data:
370
+ warning("Not authenticated")
371
+ info("Run 'afctl auth login' to authenticate")
372
+ return
373
+
374
+ # Create user info table
375
+ table = Table(title="Current User", show_header=False)
376
+ table.add_column("Field", style="cyan", width=15)
377
+ table.add_column("Value", style="white")
378
+
379
+ if token_data.name:
380
+ table.add_row("Name", token_data.name)
381
+ if token_data.email:
382
+ table.add_row("Email", token_data.email)
383
+ if token_data.user_id:
384
+ table.add_row("User ID", token_data.user_id)
385
+ if token_data.tenant_id:
386
+ table.add_row("Tenant", token_data.tenant_id)
387
+
388
+ console.print()
389
+ console.print(table)
390
+ console.print()