agentic-fabriq-sdk 0.1.4__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/core/output.py ADDED
@@ -0,0 +1,180 @@
1
+ """
2
+ Output formatting utilities for the Agentic Fabric CLI.
3
+ """
4
+
5
+ import json
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ import yaml
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ from af_cli.core.config import get_config
13
+
14
+ console = Console()
15
+
16
+
17
+ def success(message: str) -> None:
18
+ """Print success message."""
19
+ console.print(f"✅ {message}", style="green")
20
+
21
+
22
+ def error(message: str) -> None:
23
+ """Print error message."""
24
+ console.print(f"❌ {message}", style="red")
25
+
26
+
27
+ def warning(message: str) -> None:
28
+ """Print warning message."""
29
+ console.print(f"⚠️ {message}", style="yellow")
30
+
31
+
32
+ def info(message: str) -> None:
33
+ """Print info message."""
34
+ console.print(f"ℹ️ {message}", style="blue")
35
+
36
+
37
+ def debug(message: str) -> None:
38
+ """Print debug message if verbose mode is enabled."""
39
+ config = get_config()
40
+ if config.verbose:
41
+ console.print(f"🔍 {message}", style="dim")
42
+
43
+
44
+ def print_table(
45
+ data: List[Dict[str, Any]],
46
+ columns: Optional[List[str]] = None,
47
+ title: Optional[str] = None,
48
+ ) -> None:
49
+ """Print data as a table."""
50
+ if not data:
51
+ warning("No data to display")
52
+ return
53
+
54
+ # Use provided columns or infer from first row
55
+ if columns is None:
56
+ columns = list(data[0].keys())
57
+
58
+ # Create table
59
+ table = Table(title=title)
60
+
61
+ # Add columns
62
+ for column in columns:
63
+ table.add_column(column.replace("_", " ").title(), style="cyan")
64
+
65
+ # Add rows
66
+ for row in data:
67
+ table.add_row(*[str(row.get(col, "")) for col in columns])
68
+
69
+ console.print(table)
70
+
71
+
72
+ def print_json(data: Any) -> None:
73
+ """Print data as JSON."""
74
+ console.print_json(json.dumps(data, indent=2, default=str))
75
+
76
+
77
+ def print_yaml(data: Any) -> None:
78
+ """Print data as YAML."""
79
+ console.print(yaml.dump(data, default_flow_style=False))
80
+
81
+
82
+ def print_output(
83
+ data: Any,
84
+ format_type: Optional[str] = None,
85
+ columns: Optional[List[str]] = None,
86
+ title: Optional[str] = None,
87
+ ) -> None:
88
+ """Print output in the specified format."""
89
+ config = get_config()
90
+ format_type = format_type or config.output_format
91
+
92
+ if format_type == "json":
93
+ print_json(data)
94
+ elif format_type == "yaml":
95
+ print_yaml(data)
96
+ elif format_type == "table":
97
+ if isinstance(data, list):
98
+ print_table(data, columns, title)
99
+ else:
100
+ # Convert single item to table format
101
+ if isinstance(data, dict):
102
+ table_data = [{"Field": k, "Value": v} for k, v in data.items()]
103
+ print_table(table_data, ["Field", "Value"], title)
104
+ else:
105
+ console.print(str(data))
106
+ else:
107
+ console.print(str(data))
108
+
109
+
110
+ def print_status(
111
+ resource_type: str,
112
+ resource_id: str,
113
+ status: str,
114
+ details: Optional[Dict[str, Any]] = None,
115
+ ) -> None:
116
+ """Print resource status."""
117
+ status_color = {
118
+ "created": "green",
119
+ "updated": "blue",
120
+ "deleted": "red",
121
+ "error": "red",
122
+ "warning": "yellow",
123
+ }.get(status, "white")
124
+
125
+ message = f"{resource_type} {resource_id} {status}"
126
+ console.print(message, style=status_color)
127
+
128
+ if details and get_config().verbose:
129
+ for key, value in details.items():
130
+ console.print(f" {key}: {value}", style="dim")
131
+
132
+
133
+ def prompt_confirm(message: str, default: bool = False) -> bool:
134
+ """Prompt for confirmation."""
135
+ default_text = "Y/n" if default else "y/N"
136
+ response = console.input(f"{message} [{default_text}]: ")
137
+
138
+ if not response:
139
+ return default
140
+
141
+ return response.lower() in ["y", "yes"]
142
+
143
+
144
+ def prompt_input(message: str, default: Optional[str] = None) -> str:
145
+ """Prompt for input."""
146
+ if default:
147
+ message = f"{message} [{default}]"
148
+
149
+ response = console.input(f"{message}: ")
150
+
151
+ if not response and default:
152
+ return default
153
+
154
+ return response
155
+
156
+
157
+ def format_timestamp(timestamp: str) -> str:
158
+ """Format timestamp for display."""
159
+ try:
160
+ from datetime import datetime
161
+ dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
162
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
163
+ except:
164
+ return timestamp
165
+
166
+
167
+ def format_size(size: int) -> str:
168
+ """Format file size for display."""
169
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
170
+ if size < 1024.0:
171
+ return f"{size:.1f}{unit}"
172
+ size /= 1024.0
173
+ return f"{size:.1f}PB"
174
+
175
+
176
+ def truncate_text(text: str, max_length: int = 50) -> str:
177
+ """Truncate text for display."""
178
+ if len(text) <= max_length:
179
+ return text
180
+ return text[:max_length - 3] + "..."
@@ -0,0 +1,263 @@
1
+ """
2
+ Secure token storage for Agentic Fabric CLI.
3
+
4
+ This module provides secure storage for authentication tokens using the system keychain
5
+ (macOS Keychain, Windows Credential Manager, Linux Secret Service) with fallback to
6
+ encrypted file storage.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Dict, Optional
14
+
15
+ try:
16
+ import keyring
17
+ KEYRING_AVAILABLE = True
18
+ except ImportError:
19
+ KEYRING_AVAILABLE = False
20
+ keyring = None
21
+
22
+ import jwt
23
+ from cryptography.fernet import Fernet
24
+ from pydantic import BaseModel
25
+
26
+
27
+ class TokenData(BaseModel):
28
+ """Token data model."""
29
+
30
+ access_token: str
31
+ refresh_token: Optional[str] = None
32
+ expires_at: int # Unix timestamp
33
+ tenant_id: Optional[str] = None
34
+ user_id: Optional[str] = None
35
+ email: Optional[str] = None
36
+ name: Optional[str] = None
37
+
38
+
39
+ class TokenStorage:
40
+ """Secure token storage using system keychain or encrypted files."""
41
+
42
+ SERVICE_NAME = "agentic-fabriq-cli"
43
+ ACCOUNT_NAME = "default"
44
+
45
+ def __init__(self, use_keyring: bool = True):
46
+ """
47
+ Initialize token storage.
48
+
49
+ Args:
50
+ use_keyring: Whether to use system keyring (falls back to file if unavailable)
51
+ """
52
+ self.use_keyring = use_keyring and KEYRING_AVAILABLE
53
+ self.config_dir = Path.home() / ".af"
54
+ self.token_file = self.config_dir / "tokens.enc"
55
+ self.key_file = self.config_dir / ".key"
56
+
57
+ # Ensure config directory exists
58
+ self.config_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
59
+
60
+ def _get_encryption_key(self) -> bytes:
61
+ """
62
+ Get or create encryption key for file storage.
63
+
64
+ Returns:
65
+ Fernet encryption key
66
+ """
67
+ if self.key_file.exists():
68
+ with open(self.key_file, 'rb') as f:
69
+ return f.read()
70
+ else:
71
+ # Generate new key
72
+ key = Fernet.generate_key()
73
+ with open(self.key_file, 'wb') as f:
74
+ f.write(key)
75
+ # Set restrictive permissions
76
+ os.chmod(self.key_file, 0o600)
77
+ return key
78
+
79
+ def save(self, token_data: TokenData) -> None:
80
+ """
81
+ Save token data securely.
82
+
83
+ Args:
84
+ token_data: Token data to save
85
+ """
86
+ # Serialize token data
87
+ token_json = token_data.json()
88
+
89
+ if self.use_keyring:
90
+ # Use system keyring
91
+ try:
92
+ keyring.set_password(
93
+ self.SERVICE_NAME,
94
+ self.ACCOUNT_NAME,
95
+ token_json
96
+ )
97
+ return
98
+ except Exception as e:
99
+ # Fall back to file storage
100
+ print(f"Warning: Keyring storage failed, using file storage: {e}")
101
+ self.use_keyring = False
102
+
103
+ # Fall back to encrypted file storage
104
+ key = self._get_encryption_key()
105
+ fernet = Fernet(key)
106
+
107
+ encrypted_data = fernet.encrypt(token_json.encode('utf-8'))
108
+
109
+ with open(self.token_file, 'wb') as f:
110
+ f.write(encrypted_data)
111
+
112
+ # Set restrictive permissions
113
+ os.chmod(self.token_file, 0o600)
114
+
115
+ def load(self) -> Optional[TokenData]:
116
+ """
117
+ Load token data.
118
+
119
+ Returns:
120
+ Token data if available, None otherwise
121
+ """
122
+ token_json = None
123
+
124
+ if self.use_keyring:
125
+ # Try system keyring first
126
+ try:
127
+ token_json = keyring.get_password(
128
+ self.SERVICE_NAME,
129
+ self.ACCOUNT_NAME
130
+ )
131
+ except Exception:
132
+ # Fall back to file storage
133
+ self.use_keyring = False
134
+
135
+ if not token_json and self.token_file.exists():
136
+ # Try encrypted file storage
137
+ try:
138
+ key = self._get_encryption_key()
139
+ fernet = Fernet(key)
140
+
141
+ with open(self.token_file, 'rb') as f:
142
+ encrypted_data = f.read()
143
+
144
+ token_json = fernet.decrypt(encrypted_data).decode('utf-8')
145
+ except Exception as e:
146
+ print(f"Warning: Failed to load tokens from file: {e}")
147
+ return None
148
+
149
+ if token_json:
150
+ try:
151
+ return TokenData.parse_raw(token_json)
152
+ except Exception as e:
153
+ print(f"Warning: Failed to parse token data: {e}")
154
+ return None
155
+
156
+ return None
157
+
158
+ def delete(self) -> None:
159
+ """Delete stored token data."""
160
+ if self.use_keyring:
161
+ # Delete from keyring
162
+ try:
163
+ keyring.delete_password(
164
+ self.SERVICE_NAME,
165
+ self.ACCOUNT_NAME
166
+ )
167
+ except Exception:
168
+ pass
169
+
170
+ # Delete file storage
171
+ if self.token_file.exists():
172
+ try:
173
+ self.token_file.unlink()
174
+ except Exception:
175
+ pass
176
+
177
+ def is_token_expired(self, token_data: Optional[TokenData] = None) -> bool:
178
+ """
179
+ Check if token is expired.
180
+
181
+ Args:
182
+ token_data: Token data to check (loads from storage if not provided)
183
+
184
+ Returns:
185
+ True if token is expired or invalid
186
+ """
187
+ if token_data is None:
188
+ token_data = self.load()
189
+
190
+ if not token_data:
191
+ return True
192
+
193
+ # Add 60 second buffer to account for clock skew
194
+ return time.time() >= (token_data.expires_at - 60)
195
+
196
+ def parse_jwt_claims(self, access_token: str) -> Dict:
197
+ """
198
+ Parse JWT token claims without validation.
199
+
200
+ Args:
201
+ access_token: JWT access token
202
+
203
+ Returns:
204
+ Dictionary of claims
205
+ """
206
+ try:
207
+ # Decode without verification (we trust the token from Keycloak)
208
+ claims = jwt.decode(
209
+ access_token,
210
+ options={"verify_signature": False}
211
+ )
212
+ return claims
213
+ except Exception:
214
+ return {}
215
+
216
+ def extract_token_info(self, tokens: Dict) -> TokenData:
217
+ """
218
+ Extract and parse token information.
219
+
220
+ Args:
221
+ tokens: Token response from OAuth endpoint
222
+
223
+ Returns:
224
+ Structured token data
225
+ """
226
+ access_token = tokens['access_token']
227
+ refresh_token = tokens.get('refresh_token')
228
+ expires_in = tokens.get('expires_in', 3600)
229
+
230
+ # Calculate expiration time
231
+ expires_at = int(time.time()) + expires_in
232
+
233
+ # Parse JWT claims
234
+ claims = self.parse_jwt_claims(access_token)
235
+
236
+ # Extract user info from claims
237
+ tenant_id = claims.get('tenant_id') or claims.get('tenant')
238
+ user_id = claims.get('sub')
239
+ email = claims.get('email')
240
+ name = claims.get('name') or claims.get('preferred_username')
241
+
242
+ return TokenData(
243
+ access_token=access_token,
244
+ refresh_token=refresh_token,
245
+ expires_at=expires_at,
246
+ tenant_id=tenant_id,
247
+ user_id=user_id,
248
+ email=email,
249
+ name=name
250
+ )
251
+
252
+
253
+ # Global token storage instance
254
+ _token_storage: Optional[TokenStorage] = None
255
+
256
+
257
+ def get_token_storage() -> TokenStorage:
258
+ """Get the global token storage instance."""
259
+ global _token_storage
260
+ if _token_storage is None:
261
+ _token_storage = TokenStorage()
262
+ return _token_storage
263
+
af_cli/main.py ADDED
@@ -0,0 +1,187 @@
1
+ """
2
+ Main CLI application for Agentic Fabric.
3
+ """
4
+
5
+ import os
6
+ from typing import Optional
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ from af_cli.commands.agents import app as agents_app
13
+ from af_cli.commands.auth import app as auth_app
14
+ from af_cli.commands.config import app as config_app
15
+ from af_cli.commands.mcp_servers import app as mcp_servers_app
16
+ from af_cli.commands.secrets import app as secrets_app
17
+ from af_cli.commands.tools import app as tools_app
18
+ from af_cli.core.config import get_config
19
+ from af_cli.core.output import success, error, info
20
+
21
+ app = typer.Typer(
22
+ name="af",
23
+ help="Agentic Fabric CLI - Manage your connectivity hub",
24
+ add_completion=False,
25
+ )
26
+
27
+ console = Console()
28
+
29
+ # Add subcommands
30
+ app.add_typer(auth_app, name="auth", help="Authentication commands")
31
+ app.add_typer(config_app, name="config", help="Configuration commands")
32
+ app.add_typer(agents_app, name="agents", help="Agent management commands")
33
+ app.add_typer(tools_app, name="tools", help="Tool management commands")
34
+ app.add_typer(mcp_servers_app, name="mcp-servers", help="MCP server management commands")
35
+ app.add_typer(secrets_app, name="secrets", help="Secret management commands")
36
+
37
+
38
+ @app.command()
39
+ def version():
40
+ """Show version information."""
41
+ from af_cli import __version__
42
+ console.print(f"Agentic Fabric CLI v{__version__}")
43
+
44
+
45
+ @app.command()
46
+ def status():
47
+ """Show system status and configuration."""
48
+ config = get_config()
49
+
50
+ # Create status table
51
+ table = Table(title="Agentic Fabric Status")
52
+ table.add_column("Component", style="cyan")
53
+ table.add_column("Status", style="green")
54
+ table.add_column("Details", style="yellow")
55
+
56
+ # Check gateway connection
57
+ try:
58
+ import httpx
59
+ response = httpx.get(f"{config.gateway_url}/health", timeout=5.0)
60
+ if response.status_code == 200:
61
+ table.add_row("Gateway", "✅ Online", config.gateway_url)
62
+ else:
63
+ table.add_row("Gateway", "❌ Error", f"Status: {response.status_code}")
64
+ except Exception as e:
65
+ table.add_row("Gateway", "❌ Offline", str(e))
66
+
67
+ # Check authentication
68
+ if config.access_token:
69
+ table.add_row("Authentication", "✅ Authenticated", f"Tenant: {config.tenant_id}")
70
+ else:
71
+ table.add_row("Authentication", "❌ Not authenticated", "Run 'af auth login'")
72
+
73
+ # Check configuration
74
+ config_path = config.config_file
75
+ if os.path.exists(config_path):
76
+ table.add_row("Configuration", "✅ Found", config_path)
77
+ else:
78
+ table.add_row("Configuration", "❌ Not found", config_path)
79
+
80
+ console.print(table)
81
+
82
+
83
+ @app.command()
84
+ def init(
85
+ gateway_url: str = typer.Option(
86
+ "http://localhost:8000",
87
+ "--gateway-url",
88
+ "-g",
89
+ help="Gateway URL"
90
+ ),
91
+ tenant_id: Optional[str] = typer.Option(
92
+ None,
93
+ "--tenant-id",
94
+ "-t",
95
+ help="Tenant ID"
96
+ ),
97
+ force: bool = typer.Option(
98
+ False,
99
+ "--force",
100
+ "-f",
101
+ help="Force initialization, overwrite existing config"
102
+ ),
103
+ ):
104
+ """Initialize CLI configuration."""
105
+ config = get_config()
106
+
107
+ # Check if config exists
108
+ if os.path.exists(config.config_file) and not force:
109
+ error(f"Configuration already exists at {config.config_file}")
110
+ error("Use --force to overwrite")
111
+ raise typer.Exit(1)
112
+
113
+ # Create config directory if it doesn't exist
114
+ os.makedirs(os.path.dirname(config.config_file), exist_ok=True)
115
+
116
+ # Save configuration
117
+ config.gateway_url = gateway_url
118
+ if tenant_id:
119
+ config.tenant_id = tenant_id
120
+
121
+ config.save()
122
+
123
+ success(f"Configuration initialized at {config.config_file}")
124
+ info(f"Gateway URL: {gateway_url}")
125
+ if tenant_id:
126
+ info(f"Tenant ID: {tenant_id}")
127
+ else:
128
+ info("Run 'af auth login' to authenticate")
129
+
130
+
131
+ @app.callback()
132
+ def main(
133
+ ctx: typer.Context,
134
+ config_file: Optional[str] = typer.Option(
135
+ None,
136
+ "--config",
137
+ "-c",
138
+ help="Path to configuration file"
139
+ ),
140
+ gateway_url: Optional[str] = typer.Option(
141
+ None,
142
+ "--gateway-url",
143
+ "-g",
144
+ help="Gateway URL"
145
+ ),
146
+ tenant_id: Optional[str] = typer.Option(
147
+ None,
148
+ "--tenant-id",
149
+ "-t",
150
+ help="Tenant ID"
151
+ ),
152
+ verbose: bool = typer.Option(
153
+ False,
154
+ "--verbose",
155
+ "-v",
156
+ help="Enable verbose output"
157
+ ),
158
+ ):
159
+ """
160
+ Agentic Fabric CLI - Manage your connectivity hub.
161
+
162
+ The CLI provides commands for managing agents, tools, MCP servers, and secrets
163
+ in your Agentic Fabric deployment.
164
+ """
165
+ # Configure global options
166
+ config = get_config()
167
+
168
+ if config_file:
169
+ config.config_file = config_file
170
+
171
+ # Load configuration
172
+ config.load()
173
+
174
+ # Override with command line options
175
+ if gateway_url:
176
+ config.gateway_url = gateway_url
177
+ if tenant_id:
178
+ config.tenant_id = tenant_id
179
+
180
+ config.verbose = verbose
181
+
182
+ # Store config in context
183
+ ctx.obj = config
184
+
185
+
186
+ if __name__ == "__main__":
187
+ app()