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 +8 -0
- af_cli/commands/__init__.py +3 -0
- af_cli/commands/agents.py +238 -0
- af_cli/commands/auth.py +388 -0
- af_cli/commands/config.py +102 -0
- af_cli/commands/mcp_servers.py +83 -0
- af_cli/commands/secrets.py +109 -0
- af_cli/commands/tools.py +83 -0
- af_cli/core/__init__.py +3 -0
- af_cli/core/client.py +123 -0
- af_cli/core/config.py +200 -0
- af_cli/core/oauth.py +506 -0
- af_cli/core/output.py +180 -0
- af_cli/core/token_storage.py +263 -0
- af_cli/main.py +187 -0
- {agentic_fabriq_sdk-0.1.5.dist-info → agentic_fabriq_sdk-0.1.6.dist-info}/METADATA +37 -7
- {agentic_fabriq_sdk-0.1.5.dist-info → agentic_fabriq_sdk-0.1.6.dist-info}/RECORD +19 -3
- agentic_fabriq_sdk-0.1.6.dist-info/entry_points.txt +3 -0
- {agentic_fabriq_sdk-0.1.5.dist-info → agentic_fabriq_sdk-0.1.6.dist-info}/WHEEL +0 -0
af_cli/__init__.py
ADDED
|
@@ -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)
|
af_cli/commands/auth.py
ADDED
|
@@ -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()
|