agentic-fabriq-sdk 0.1.6__tar.gz → 0.1.7__tar.gz
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.
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/PKG-INFO +4 -4
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/README.md +3 -3
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_cli/commands/auth.py +11 -9
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_cli/commands/config.py +1 -1
- agentic_fabriq_sdk-0.1.7/af_cli/commands/tools.py +474 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_cli/core/client.py +9 -5
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_cli/core/output.py +4 -4
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_cli/main.py +3 -3
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/pyproject.toml +1 -1
- agentic_fabriq_sdk-0.1.6/af_cli/commands/tools.py +0 -83
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_cli/__init__.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_cli/commands/__init__.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_cli/commands/agents.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_cli/commands/mcp_servers.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_cli/commands/secrets.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_cli/core/__init__.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_cli/core/config.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_cli/core/oauth.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_cli/core/token_storage.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/__init__.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/auth/__init__.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/auth/dpop.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/auth/oauth.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/auth/token_cache.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/connectors/__init__.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/connectors/base.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/connectors/registry.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/dx/__init__.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/dx/decorators.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/dx/runtime.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/events.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/exceptions.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/fabriq_client.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/models/__init__.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/models/audit.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/models/types.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/py.typed +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/transport/__init__.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/transport/http.py +0 -0
- {agentic_fabriq_sdk-0.1.6 → agentic_fabriq_sdk-0.1.7}/af_sdk/vault.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-fabriq-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Agentic Fabriq SDK: high-level client, CLI tool, DX helpers, and auth for AI agents
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Keywords: fabriq,agentic-fabriq,sdk,ai,agents,agentic,fabric,cli
|
|
@@ -90,8 +90,8 @@ DX orchestration:
|
|
|
90
90
|
```python
|
|
91
91
|
from af_sdk.dx import ToolFabric, AgentFabric, Agent, tool
|
|
92
92
|
|
|
93
|
-
slack = ToolFabric(provider="slack", base_url="
|
|
94
|
-
agents = AgentFabric(base_url="
|
|
93
|
+
slack = ToolFabric(provider="slack", base_url="https://dashboard.agenticfabriq.com", access_token=TOKEN, tenant_id=TENANT)
|
|
94
|
+
agents = AgentFabric(base_url="https://dashboard.agenticfabriq.com", access_token=TOKEN, tenant_id=TENANT)
|
|
95
95
|
|
|
96
96
|
@tool
|
|
97
97
|
def echo(x: str) -> str:
|
|
@@ -101,7 +101,7 @@ bot = Agent(
|
|
|
101
101
|
system_prompt="demo",
|
|
102
102
|
tools=[echo],
|
|
103
103
|
agents=agents.get_agents(["summarizer"]),
|
|
104
|
-
base_url="
|
|
104
|
+
base_url="https://dashboard.agenticfabriq.com",
|
|
105
105
|
access_token=TOKEN,
|
|
106
106
|
tenant_id=TENANT,
|
|
107
107
|
provider_fabrics={"slack": slack},
|
|
@@ -57,8 +57,8 @@ DX orchestration:
|
|
|
57
57
|
```python
|
|
58
58
|
from af_sdk.dx import ToolFabric, AgentFabric, Agent, tool
|
|
59
59
|
|
|
60
|
-
slack = ToolFabric(provider="slack", base_url="
|
|
61
|
-
agents = AgentFabric(base_url="
|
|
60
|
+
slack = ToolFabric(provider="slack", base_url="https://dashboard.agenticfabriq.com", access_token=TOKEN, tenant_id=TENANT)
|
|
61
|
+
agents = AgentFabric(base_url="https://dashboard.agenticfabriq.com", access_token=TOKEN, tenant_id=TENANT)
|
|
62
62
|
|
|
63
63
|
@tool
|
|
64
64
|
def echo(x: str) -> str:
|
|
@@ -68,7 +68,7 @@ bot = Agent(
|
|
|
68
68
|
system_prompt="demo",
|
|
69
69
|
tools=[echo],
|
|
70
70
|
agents=agents.get_agents(["summarizer"]),
|
|
71
|
-
base_url="
|
|
71
|
+
base_url="https://dashboard.agenticfabriq.com",
|
|
72
72
|
access_token=TOKEN,
|
|
73
73
|
tenant_id=TENANT,
|
|
74
74
|
provider_fabrics={"slack": slack},
|
|
@@ -98,9 +98,12 @@ def login(
|
|
|
98
98
|
# Extract and save token data
|
|
99
99
|
token_data = token_storage.extract_token_info(tokens)
|
|
100
100
|
|
|
101
|
-
# Override tenant_id if provided
|
|
101
|
+
# Override tenant_id if provided, otherwise use default for Keycloak JWTs
|
|
102
102
|
if tenant_id:
|
|
103
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"
|
|
104
107
|
|
|
105
108
|
# Save tokens
|
|
106
109
|
token_storage.save(token_data)
|
|
@@ -109,8 +112,7 @@ def login(
|
|
|
109
112
|
config.access_token = token_data.access_token
|
|
110
113
|
config.refresh_token = token_data.refresh_token
|
|
111
114
|
config.token_expires_at = token_data.expires_at
|
|
112
|
-
|
|
113
|
-
config.tenant_id = token_data.tenant_id
|
|
115
|
+
config.tenant_id = token_data.tenant_id # Always set tenant_id
|
|
114
116
|
config.save()
|
|
115
117
|
|
|
116
118
|
# Display success message
|
|
@@ -190,7 +192,7 @@ def status():
|
|
|
190
192
|
|
|
191
193
|
if not token_data:
|
|
192
194
|
warning("Not authenticated")
|
|
193
|
-
info("Run '
|
|
195
|
+
info("Run 'afctl auth login' to authenticate")
|
|
194
196
|
return
|
|
195
197
|
|
|
196
198
|
# Create status table
|
|
@@ -242,9 +244,9 @@ def status():
|
|
|
242
244
|
# Show recommendations
|
|
243
245
|
if is_expired:
|
|
244
246
|
if token_data.refresh_token:
|
|
245
|
-
info("Token has expired. Run '
|
|
247
|
+
info("Token has expired. Run 'afctl auth refresh' to get a new token")
|
|
246
248
|
else:
|
|
247
|
-
info("Token has expired. Run '
|
|
249
|
+
info("Token has expired. Run 'afctl auth login' to re-authenticate")
|
|
248
250
|
|
|
249
251
|
|
|
250
252
|
@app.command()
|
|
@@ -270,7 +272,7 @@ def refresh(
|
|
|
270
272
|
|
|
271
273
|
if not token_data or not token_data.refresh_token:
|
|
272
274
|
error("No refresh token available")
|
|
273
|
-
info("Run '
|
|
275
|
+
info("Run 'afctl auth login' to authenticate")
|
|
274
276
|
raise typer.Exit(1)
|
|
275
277
|
|
|
276
278
|
try:
|
|
@@ -304,7 +306,7 @@ def refresh(
|
|
|
304
306
|
|
|
305
307
|
except Exception as e:
|
|
306
308
|
error(f"Token refresh failed: {e}")
|
|
307
|
-
error("Please run '
|
|
309
|
+
error("Please run 'afctl auth login' to re-authenticate")
|
|
308
310
|
|
|
309
311
|
# Clear invalid tokens
|
|
310
312
|
token_storage.delete()
|
|
@@ -366,7 +368,7 @@ def whoami():
|
|
|
366
368
|
|
|
367
369
|
if not token_data:
|
|
368
370
|
warning("Not authenticated")
|
|
369
|
-
info("Run '
|
|
371
|
+
info("Run 'afctl auth login' to authenticate")
|
|
370
372
|
return
|
|
371
373
|
|
|
372
374
|
# Create user info table
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool management commands for the Agentic Fabric CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from af_cli.core.client import get_client
|
|
9
|
+
from af_cli.core.output import debug, error, info, print_output, success, warning
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Tool management commands")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def list(
|
|
16
|
+
format: str = typer.Option("table", "--format", "-f", help="Output format"),
|
|
17
|
+
):
|
|
18
|
+
"""List your tool connections (configured and connected tools)."""
|
|
19
|
+
try:
|
|
20
|
+
with get_client() as client:
|
|
21
|
+
connections = client.get("/api/v1/user-connections")
|
|
22
|
+
|
|
23
|
+
if not connections:
|
|
24
|
+
warning("No tool connections found. Add connections in the dashboard UI.")
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
# Format for better display
|
|
28
|
+
display_data = []
|
|
29
|
+
for conn in connections:
|
|
30
|
+
# Format tool name nicely (e.g., "google_docs" -> "Google Docs")
|
|
31
|
+
tool_name = conn.get("tool", "N/A").replace("_", " ").title()
|
|
32
|
+
|
|
33
|
+
# Status indicator
|
|
34
|
+
status = "✓ Connected" if conn.get("connected") else "○ Configured"
|
|
35
|
+
|
|
36
|
+
display_data.append({
|
|
37
|
+
"Tool": tool_name,
|
|
38
|
+
"ID": conn.get("connection_id", "N/A"),
|
|
39
|
+
"Name": conn.get("display_name") or conn.get("connection_id", "N/A"),
|
|
40
|
+
"Status": status,
|
|
41
|
+
"Method": conn.get("method", "oauth"),
|
|
42
|
+
"Added": conn.get("created_at", "N/A")[:10] if conn.get("created_at") else "N/A",
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
print_output(
|
|
46
|
+
display_data,
|
|
47
|
+
format_type=format,
|
|
48
|
+
title="Your Tool Connections"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
except Exception as e:
|
|
52
|
+
error(f"Failed to list tool connections: {e}")
|
|
53
|
+
raise typer.Exit(1)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command()
|
|
57
|
+
def get(
|
|
58
|
+
connection_id: str = typer.Argument(..., help="Connection ID (e.g., 'google', 'slack')"),
|
|
59
|
+
format: str = typer.Option("table", "--format", "-f", help="Output format"),
|
|
60
|
+
):
|
|
61
|
+
"""Get tool connection details."""
|
|
62
|
+
try:
|
|
63
|
+
with get_client() as client:
|
|
64
|
+
# Get all user connections and find the matching one
|
|
65
|
+
connections = client.get("/api/v1/user-connections")
|
|
66
|
+
|
|
67
|
+
# Find the specific connection
|
|
68
|
+
connection = None
|
|
69
|
+
for conn in connections:
|
|
70
|
+
if conn.get("connection_id") == connection_id or conn.get("tool") == connection_id:
|
|
71
|
+
connection = conn
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
if not connection:
|
|
75
|
+
error(f"Connection '{connection_id}' not found")
|
|
76
|
+
info("Available connections:")
|
|
77
|
+
for conn in connections:
|
|
78
|
+
info(f" - {conn.get('tool')} (ID: {conn.get('connection_id')})")
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
|
|
81
|
+
# Format tool name nicely
|
|
82
|
+
tool_name = connection.get("tool", "N/A").replace("_", " ").title()
|
|
83
|
+
|
|
84
|
+
# Format the connection details for display
|
|
85
|
+
details = {
|
|
86
|
+
"Tool": tool_name,
|
|
87
|
+
"Connection ID": connection.get("connection_id", "N/A"),
|
|
88
|
+
"Display Name": connection.get("display_name") or connection.get("connection_id", "N/A"),
|
|
89
|
+
"Status": "✓ Connected" if connection.get("connected") else "○ Configured",
|
|
90
|
+
"Method": connection.get("method", "oauth"),
|
|
91
|
+
"Created": connection.get("created_at", "N/A"),
|
|
92
|
+
"Updated": connection.get("updated_at", "N/A"),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Add tool-specific fields if present
|
|
96
|
+
if connection.get("team_name"):
|
|
97
|
+
details["Team Name"] = connection.get("team_name")
|
|
98
|
+
if connection.get("team_id"):
|
|
99
|
+
details["Team ID"] = connection.get("team_id")
|
|
100
|
+
if connection.get("bot_user_id"):
|
|
101
|
+
details["Bot User ID"] = connection.get("bot_user_id")
|
|
102
|
+
if connection.get("email"):
|
|
103
|
+
details["Email"] = connection.get("email")
|
|
104
|
+
if connection.get("login"):
|
|
105
|
+
details["GitHub Login"] = connection.get("login")
|
|
106
|
+
if connection.get("workspace_name"):
|
|
107
|
+
details["Workspace Name"] = connection.get("workspace_name")
|
|
108
|
+
if connection.get("scopes"):
|
|
109
|
+
details["Scopes"] = ", ".join(connection.get("scopes", []))
|
|
110
|
+
|
|
111
|
+
print_output(
|
|
112
|
+
details,
|
|
113
|
+
format_type=format,
|
|
114
|
+
title=f"{tool_name} Connection Details"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
error(f"Failed to get tool connection: {e}")
|
|
119
|
+
raise typer.Exit(1)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@app.command()
|
|
123
|
+
def invoke(
|
|
124
|
+
tool_id: str = typer.Argument(..., help="Tool ID"),
|
|
125
|
+
method: str = typer.Option(..., "--method", "-m", help="Tool method to invoke"),
|
|
126
|
+
format: str = typer.Option("table", "--format", "-f", help="Output format"),
|
|
127
|
+
):
|
|
128
|
+
"""Invoke a tool."""
|
|
129
|
+
try:
|
|
130
|
+
with get_client() as client:
|
|
131
|
+
data = {
|
|
132
|
+
"method": method,
|
|
133
|
+
"parameters": {},
|
|
134
|
+
"context": {},
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
info(f"Invoking tool {tool_id} method {method}...")
|
|
138
|
+
response = client.post(f"/api/v1/tools/{tool_id}/invoke", data)
|
|
139
|
+
|
|
140
|
+
success("Tool invoked successfully")
|
|
141
|
+
print_output(response, format_type=format)
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
error(f"Failed to invoke tool: {e}")
|
|
145
|
+
raise typer.Exit(1)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.command()
|
|
149
|
+
def add(
|
|
150
|
+
tool: str = typer.Argument(..., help="Tool name (google, slack, notion, github, etc.)"),
|
|
151
|
+
connection_id: str = typer.Option(..., "--connection-id", help="Unique connection ID"),
|
|
152
|
+
display_name: str = typer.Option(None, "--display-name", help="Human-readable name"),
|
|
153
|
+
method: str = typer.Option(..., "--method", help="Connection method: 'api' or 'credentials'"),
|
|
154
|
+
|
|
155
|
+
# API method fields
|
|
156
|
+
token: str = typer.Option(None, "--token", help="API token (for api method)"),
|
|
157
|
+
|
|
158
|
+
# Credentials method fields
|
|
159
|
+
client_id: str = typer.Option(None, "--client-id", help="OAuth client ID (for credentials method)"),
|
|
160
|
+
client_secret: str = typer.Option(None, "--client-secret", help="OAuth client secret (for credentials method)"),
|
|
161
|
+
redirect_uri: str = typer.Option(None, "--redirect-uri", help="OAuth redirect URI (optional, auto-generated)"),
|
|
162
|
+
):
|
|
163
|
+
"""
|
|
164
|
+
Add a new tool connection with credentials.
|
|
165
|
+
|
|
166
|
+
Examples:
|
|
167
|
+
# Notion (api method - single token)
|
|
168
|
+
afctl tools add notion --connection-id notion-work --method api --token "secret_abc123"
|
|
169
|
+
|
|
170
|
+
# Google (credentials method - OAuth app)
|
|
171
|
+
afctl tools add google --connection-id google-work --method credentials \\
|
|
172
|
+
--client-id "123.apps.googleusercontent.com" \\
|
|
173
|
+
--client-secret "GOCSPX-abc123"
|
|
174
|
+
|
|
175
|
+
# Slack bot (api method)
|
|
176
|
+
afctl tools add slack --connection-id slack-bot --method api --token "xoxb-123..."
|
|
177
|
+
"""
|
|
178
|
+
try:
|
|
179
|
+
from af_cli.core.config import get_config
|
|
180
|
+
|
|
181
|
+
with get_client() as client:
|
|
182
|
+
# Validate method
|
|
183
|
+
if method not in ["api", "credentials"]:
|
|
184
|
+
error("Method must be 'api' or 'credentials'")
|
|
185
|
+
raise typer.Exit(1)
|
|
186
|
+
|
|
187
|
+
# Validate API method requirements
|
|
188
|
+
if method == "api":
|
|
189
|
+
if not token:
|
|
190
|
+
error("API method requires --token")
|
|
191
|
+
info(f"Example: afctl tools add {tool} --connection-id {connection_id} --method api --token YOUR_TOKEN")
|
|
192
|
+
raise typer.Exit(1)
|
|
193
|
+
|
|
194
|
+
# Validate credentials method requirements
|
|
195
|
+
elif method == "credentials":
|
|
196
|
+
if not client_id or not client_secret:
|
|
197
|
+
error("Credentials method requires --client-id and --client-secret")
|
|
198
|
+
info(f"Example: afctl tools add {tool} --connection-id {connection_id} --method credentials \\")
|
|
199
|
+
info(f" --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET")
|
|
200
|
+
raise typer.Exit(1)
|
|
201
|
+
|
|
202
|
+
info(f"Creating connection: {connection_id}")
|
|
203
|
+
info(f"Tool: {tool}")
|
|
204
|
+
info(f"Method: {method}")
|
|
205
|
+
|
|
206
|
+
# Step 1: Create connection metadata
|
|
207
|
+
connection_data = {
|
|
208
|
+
"tool": tool,
|
|
209
|
+
"connection_id": connection_id,
|
|
210
|
+
"display_name": display_name or connection_id,
|
|
211
|
+
"method": method,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
client.post("/api/v1/user-connections", data=connection_data)
|
|
215
|
+
success(f"✅ Connection entry created: {connection_id}")
|
|
216
|
+
|
|
217
|
+
# Step 2: Store credentials based on method
|
|
218
|
+
if method == "credentials":
|
|
219
|
+
# Auto-generate redirect_uri if not provided
|
|
220
|
+
if not redirect_uri:
|
|
221
|
+
config = get_config()
|
|
222
|
+
redirect_uri = f"{config.gateway_url}/api/v1/tools/{tool}/oauth/callback"
|
|
223
|
+
info(f"Using default redirect URI: {redirect_uri}")
|
|
224
|
+
|
|
225
|
+
# Store OAuth app config
|
|
226
|
+
info("Storing OAuth app credentials...")
|
|
227
|
+
config_payload = {
|
|
228
|
+
"client_id": client_id,
|
|
229
|
+
"client_secret": client_secret,
|
|
230
|
+
}
|
|
231
|
+
if redirect_uri:
|
|
232
|
+
config_payload["redirect_uri"] = redirect_uri
|
|
233
|
+
|
|
234
|
+
client.post(
|
|
235
|
+
f"/api/v1/tools/{tool}/config?connection_id={connection_id}",
|
|
236
|
+
data=config_payload
|
|
237
|
+
)
|
|
238
|
+
success("✅ OAuth app credentials stored")
|
|
239
|
+
info("")
|
|
240
|
+
info(f"Next: Run 'afctl tools connect {connection_id}' to complete OAuth setup")
|
|
241
|
+
|
|
242
|
+
elif method == "api":
|
|
243
|
+
# Store API token directly
|
|
244
|
+
info("Storing API credentials...")
|
|
245
|
+
|
|
246
|
+
# Tool-specific endpoint and payload mappings
|
|
247
|
+
if tool == "notion":
|
|
248
|
+
# Notion uses /config endpoint with integration_token field
|
|
249
|
+
endpoint = f"/api/v1/tools/{tool}/config?connection_id={connection_id}"
|
|
250
|
+
cred_payload = {"integration_token": token}
|
|
251
|
+
else:
|
|
252
|
+
# Generic tools use /connection endpoint with api_token field
|
|
253
|
+
endpoint = f"/api/v1/tools/{tool}/connection?connection_id={connection_id}"
|
|
254
|
+
cred_payload = {"api_token": token}
|
|
255
|
+
|
|
256
|
+
client.post(endpoint, data=cred_payload)
|
|
257
|
+
success("✅ API credentials stored")
|
|
258
|
+
success(f"✅ Connection '{connection_id}' is ready to use!")
|
|
259
|
+
|
|
260
|
+
# Show helpful info
|
|
261
|
+
info("")
|
|
262
|
+
info("View your connections:")
|
|
263
|
+
info(f" • List all: afctl tools list")
|
|
264
|
+
info(f" • View details: afctl tools get {connection_id}")
|
|
265
|
+
|
|
266
|
+
except Exception as e:
|
|
267
|
+
error(f"Failed to add connection: {e}")
|
|
268
|
+
raise typer.Exit(1)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@app.command()
|
|
272
|
+
def connect(
|
|
273
|
+
connection_id: str = typer.Argument(..., help="Connection ID to connect"),
|
|
274
|
+
):
|
|
275
|
+
"""Complete OAuth connection (open browser for authorization)."""
|
|
276
|
+
try:
|
|
277
|
+
import webbrowser
|
|
278
|
+
import time
|
|
279
|
+
|
|
280
|
+
with get_client() as client:
|
|
281
|
+
# Get connection info
|
|
282
|
+
connections = client.get("/api/v1/user-connections")
|
|
283
|
+
|
|
284
|
+
connection = None
|
|
285
|
+
for conn in connections:
|
|
286
|
+
if conn.get("connection_id") == connection_id:
|
|
287
|
+
connection = conn
|
|
288
|
+
break
|
|
289
|
+
|
|
290
|
+
if not connection:
|
|
291
|
+
error(f"Connection '{connection_id}' not found")
|
|
292
|
+
info("Run 'afctl tools list' to see available connections")
|
|
293
|
+
raise typer.Exit(1)
|
|
294
|
+
|
|
295
|
+
tool = connection["tool"]
|
|
296
|
+
method = connection["method"]
|
|
297
|
+
|
|
298
|
+
# Only credentials method needs OAuth completion
|
|
299
|
+
if method != "credentials":
|
|
300
|
+
error(f"Connection '{connection_id}' uses '{method}' method")
|
|
301
|
+
info("Only 'credentials' method connections need OAuth setup")
|
|
302
|
+
info("API connections are already connected after 'afctl tools add'")
|
|
303
|
+
raise typer.Exit(1)
|
|
304
|
+
|
|
305
|
+
# Check if already connected
|
|
306
|
+
if connection.get("connected"):
|
|
307
|
+
warning(f"Connection '{connection_id}' is already connected")
|
|
308
|
+
confirm = typer.confirm("Do you want to reconnect (re-authorize)?")
|
|
309
|
+
if not confirm:
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
# Initiate OAuth flow
|
|
313
|
+
info(f"Initiating OAuth for {tool}...")
|
|
314
|
+
|
|
315
|
+
result = client.post(
|
|
316
|
+
f"/api/v1/tools/{tool}/connect/initiate?connection_id={connection_id}",
|
|
317
|
+
data={}
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
debug(f"Backend response: {result}")
|
|
321
|
+
|
|
322
|
+
# Different tools use different field names for the auth URL
|
|
323
|
+
auth_url = (
|
|
324
|
+
result.get("authorization_url") or
|
|
325
|
+
result.get("auth_url") or
|
|
326
|
+
result.get("oauth_url")
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if not auth_url:
|
|
330
|
+
error("Failed to get authorization URL from backend")
|
|
331
|
+
error(f"Response keys: {list(result.keys())}")
|
|
332
|
+
debug(f"Full response: {result}")
|
|
333
|
+
raise typer.Exit(1)
|
|
334
|
+
|
|
335
|
+
info("Opening browser for authentication...")
|
|
336
|
+
info("")
|
|
337
|
+
info(f"If browser doesn't open, visit: {auth_url}")
|
|
338
|
+
|
|
339
|
+
# Open browser
|
|
340
|
+
webbrowser.open(auth_url)
|
|
341
|
+
|
|
342
|
+
info("")
|
|
343
|
+
info("Waiting for authorization...")
|
|
344
|
+
info("(Complete the login in your browser)")
|
|
345
|
+
|
|
346
|
+
# Poll for connection completion
|
|
347
|
+
max_attempts = 120 # 2 minutes
|
|
348
|
+
for attempt in range(max_attempts):
|
|
349
|
+
time.sleep(1)
|
|
350
|
+
|
|
351
|
+
# Check connection status
|
|
352
|
+
connections = client.get("/api/v1/user-connections")
|
|
353
|
+
for conn in connections:
|
|
354
|
+
if conn.get("connection_id") == connection_id:
|
|
355
|
+
if conn.get("connected"):
|
|
356
|
+
info("")
|
|
357
|
+
success(f"✅ Successfully connected to {tool}!")
|
|
358
|
+
|
|
359
|
+
# Show connection details
|
|
360
|
+
info(f"Connection ID: {connection_id}")
|
|
361
|
+
if conn.get("email"):
|
|
362
|
+
info(f"Email: {conn['email']}")
|
|
363
|
+
if conn.get("team_name"):
|
|
364
|
+
info(f"Team: {conn['team_name']}")
|
|
365
|
+
if conn.get("login"):
|
|
366
|
+
info(f"GitHub: {conn['login']}")
|
|
367
|
+
|
|
368
|
+
return
|
|
369
|
+
break
|
|
370
|
+
|
|
371
|
+
# Timeout
|
|
372
|
+
error("")
|
|
373
|
+
error("Timeout: Authorization not completed within 2 minutes")
|
|
374
|
+
info("Please try again or check your browser")
|
|
375
|
+
raise typer.Exit(1)
|
|
376
|
+
|
|
377
|
+
except Exception as e:
|
|
378
|
+
error(f"Failed to connect: {e}")
|
|
379
|
+
raise typer.Exit(1)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@app.command()
|
|
383
|
+
def disconnect(
|
|
384
|
+
connection_id: str = typer.Argument(..., help="Connection ID to disconnect"),
|
|
385
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
386
|
+
):
|
|
387
|
+
"""Disconnect a tool (remove credentials but keep connection entry)."""
|
|
388
|
+
try:
|
|
389
|
+
with get_client() as client:
|
|
390
|
+
# Get connection info
|
|
391
|
+
connections = client.get("/api/v1/user-connections")
|
|
392
|
+
|
|
393
|
+
connection = None
|
|
394
|
+
for conn in connections:
|
|
395
|
+
if conn.get("connection_id") == connection_id:
|
|
396
|
+
connection = conn
|
|
397
|
+
break
|
|
398
|
+
|
|
399
|
+
if not connection:
|
|
400
|
+
error(f"Connection '{connection_id}' not found")
|
|
401
|
+
raise typer.Exit(1)
|
|
402
|
+
|
|
403
|
+
tool = connection["tool"]
|
|
404
|
+
tool_display = connection.get("display_name") or connection_id
|
|
405
|
+
|
|
406
|
+
# Check if connected
|
|
407
|
+
if not connection.get("connected"):
|
|
408
|
+
error(f"Connection '{connection_id}' is already disconnected")
|
|
409
|
+
info(f"Use 'afctl tools get {connection_id}' to view status")
|
|
410
|
+
raise typer.Exit(1)
|
|
411
|
+
|
|
412
|
+
# Confirm
|
|
413
|
+
if not force:
|
|
414
|
+
warning(f"This will remove OAuth tokens/credentials for '{tool_display}'")
|
|
415
|
+
info("You can reconnect later with 'afctl tools connect'")
|
|
416
|
+
confirm = typer.confirm(f"Disconnect {tool} connection '{connection_id}'?")
|
|
417
|
+
if not confirm:
|
|
418
|
+
info("Cancelled")
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
# Delete connection credentials
|
|
422
|
+
client.delete(
|
|
423
|
+
f"/api/v1/tools/{tool}/connection?connection_id={connection_id}"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
success(f"✅ Disconnected: {connection_id}")
|
|
427
|
+
info("Connection entry preserved.")
|
|
428
|
+
info(f"Run 'afctl tools connect {connection_id}' to reconnect.")
|
|
429
|
+
|
|
430
|
+
except Exception as e:
|
|
431
|
+
error(f"Failed to disconnect: {e}")
|
|
432
|
+
raise typer.Exit(1)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@app.command()
|
|
436
|
+
def remove(
|
|
437
|
+
connection_id: str = typer.Argument(..., help="Connection ID to remove"),
|
|
438
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
439
|
+
):
|
|
440
|
+
"""Remove a tool connection completely (delete entry and credentials)."""
|
|
441
|
+
try:
|
|
442
|
+
with get_client() as client:
|
|
443
|
+
# Get connection info
|
|
444
|
+
connections = client.get("/api/v1/user-connections")
|
|
445
|
+
|
|
446
|
+
connection = None
|
|
447
|
+
for conn in connections:
|
|
448
|
+
if conn.get("connection_id") == connection_id:
|
|
449
|
+
connection = conn
|
|
450
|
+
break
|
|
451
|
+
|
|
452
|
+
if not connection:
|
|
453
|
+
error(f"Connection '{connection_id}' not found")
|
|
454
|
+
raise typer.Exit(1)
|
|
455
|
+
|
|
456
|
+
tool = connection["tool"]
|
|
457
|
+
tool_display = connection.get("display_name") or connection_id
|
|
458
|
+
|
|
459
|
+
# Confirm
|
|
460
|
+
if not force:
|
|
461
|
+
warning("⚠️ This will permanently delete the connection and credentials")
|
|
462
|
+
confirm = typer.confirm(f"Remove {tool} connection '{tool_display}'?")
|
|
463
|
+
if not confirm:
|
|
464
|
+
info("Cancelled")
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
# Delete connection entry (backend will cascade delete credentials)
|
|
468
|
+
client.delete(f"/api/v1/user-connections/{connection_id}")
|
|
469
|
+
|
|
470
|
+
success(f"✅ Removed: {connection_id}")
|
|
471
|
+
|
|
472
|
+
except Exception as e:
|
|
473
|
+
error(f"Failed to remove: {e}")
|
|
474
|
+
raise typer.Exit(1)
|
|
@@ -33,7 +33,7 @@ class AFClient:
|
|
|
33
33
|
debug(f"Response: {response.status_code} {response.url}")
|
|
34
34
|
|
|
35
35
|
if response.status_code == 401:
|
|
36
|
-
error("Authentication failed. Please run '
|
|
36
|
+
error("Authentication failed. Please run 'afctl auth login'")
|
|
37
37
|
raise typer.Exit(1)
|
|
38
38
|
|
|
39
39
|
if response.status_code == 403:
|
|
@@ -43,12 +43,16 @@ class AFClient:
|
|
|
43
43
|
if response.status_code >= 400:
|
|
44
44
|
try:
|
|
45
45
|
error_data = response.json()
|
|
46
|
-
|
|
46
|
+
# Try different error message fields (FastAPI uses "detail")
|
|
47
|
+
error_message = error_data.get("detail") or error_data.get("message") or "Unknown error"
|
|
47
48
|
error(f"API Error: {error_message}")
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
# Always show full response for debugging
|
|
50
|
+
debug(f"Response status: {response.status_code}")
|
|
51
|
+
debug(f"Request URL: {response.url}")
|
|
52
|
+
debug(f"Full response: {json.dumps(error_data, indent=2)}")
|
|
50
53
|
except:
|
|
51
|
-
error(f"HTTP Error: {response.status_code}
|
|
54
|
+
error(f"HTTP Error: {response.status_code}")
|
|
55
|
+
debug(f"Response text: {response.text}")
|
|
52
56
|
raise typer.Exit(1)
|
|
53
57
|
|
|
54
58
|
try:
|
|
@@ -55,12 +55,12 @@ def print_table(
|
|
|
55
55
|
if columns is None:
|
|
56
56
|
columns = list(data[0].keys())
|
|
57
57
|
|
|
58
|
-
# Create table
|
|
59
|
-
table = Table(title=title)
|
|
58
|
+
# Create table with expand to fill terminal width and show grid lines
|
|
59
|
+
table = Table(title=title, expand=True, show_lines=True)
|
|
60
60
|
|
|
61
|
-
# Add columns
|
|
61
|
+
# Add columns with no_wrap to prevent text wrapping
|
|
62
62
|
for column in columns:
|
|
63
|
-
table.add_column(column.replace("_", " ").title(), style="cyan")
|
|
63
|
+
table.add_column(column.replace("_", " ").title(), style="cyan", no_wrap=True)
|
|
64
64
|
|
|
65
65
|
# Add rows
|
|
66
66
|
for row in data:
|
|
@@ -68,7 +68,7 @@ def status():
|
|
|
68
68
|
if config.access_token:
|
|
69
69
|
table.add_row("Authentication", "✅ Authenticated", f"Tenant: {config.tenant_id}")
|
|
70
70
|
else:
|
|
71
|
-
table.add_row("Authentication", "❌ Not authenticated", "Run '
|
|
71
|
+
table.add_row("Authentication", "❌ Not authenticated", "Run 'afctl auth login'")
|
|
72
72
|
|
|
73
73
|
# Check configuration
|
|
74
74
|
config_path = config.config_file
|
|
@@ -83,7 +83,7 @@ def status():
|
|
|
83
83
|
@app.command()
|
|
84
84
|
def init(
|
|
85
85
|
gateway_url: str = typer.Option(
|
|
86
|
-
"
|
|
86
|
+
"https://dashboard.agenticfabriq.com",
|
|
87
87
|
"--gateway-url",
|
|
88
88
|
"-g",
|
|
89
89
|
help="Gateway URL"
|
|
@@ -125,7 +125,7 @@ def init(
|
|
|
125
125
|
if tenant_id:
|
|
126
126
|
info(f"Tenant ID: {tenant_id}")
|
|
127
127
|
else:
|
|
128
|
-
info("Run '
|
|
128
|
+
info("Run 'afctl auth login' to authenticate")
|
|
129
129
|
|
|
130
130
|
|
|
131
131
|
@app.callback()
|
|
@@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
5
5
|
|
|
6
6
|
[tool.poetry]
|
|
7
7
|
name = "agentic-fabriq-sdk"
|
|
8
|
-
version = "0.1.
|
|
8
|
+
version = "0.1.7"
|
|
9
9
|
description = "Agentic Fabriq SDK: high-level client, CLI tool, DX helpers, and auth for AI agents"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
license = "Apache-2.0"
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tool management commands for the Agentic Fabric CLI.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import typer
|
|
7
|
-
|
|
8
|
-
from af_cli.core.client import get_client
|
|
9
|
-
from af_cli.core.output import error, info, print_output, success, warning
|
|
10
|
-
|
|
11
|
-
app = typer.Typer(help="Tool management commands")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@app.command()
|
|
15
|
-
def list(
|
|
16
|
-
format: str = typer.Option("table", "--format", "-f", help="Output format"),
|
|
17
|
-
):
|
|
18
|
-
"""List tools."""
|
|
19
|
-
try:
|
|
20
|
-
with get_client() as client:
|
|
21
|
-
response = client.get("/api/v1/tools")
|
|
22
|
-
tools = response["tools"]
|
|
23
|
-
|
|
24
|
-
if not tools:
|
|
25
|
-
warning("No tools found")
|
|
26
|
-
return
|
|
27
|
-
|
|
28
|
-
print_output(
|
|
29
|
-
tools,
|
|
30
|
-
format_type=format,
|
|
31
|
-
title="Tools"
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
except Exception as e:
|
|
35
|
-
error(f"Failed to list tools: {e}")
|
|
36
|
-
raise typer.Exit(1)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@app.command()
|
|
40
|
-
def get(
|
|
41
|
-
tool_id: str = typer.Argument(..., help="Tool ID"),
|
|
42
|
-
format: str = typer.Option("table", "--format", "-f", help="Output format"),
|
|
43
|
-
):
|
|
44
|
-
"""Get tool details."""
|
|
45
|
-
try:
|
|
46
|
-
with get_client() as client:
|
|
47
|
-
tool = client.get(f"/api/v1/tools/{tool_id}")
|
|
48
|
-
|
|
49
|
-
print_output(
|
|
50
|
-
tool,
|
|
51
|
-
format_type=format,
|
|
52
|
-
title=f"Tool {tool_id}"
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
except Exception as e:
|
|
56
|
-
error(f"Failed to get tool: {e}")
|
|
57
|
-
raise typer.Exit(1)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@app.command()
|
|
61
|
-
def invoke(
|
|
62
|
-
tool_id: str = typer.Argument(..., help="Tool ID"),
|
|
63
|
-
method: str = typer.Option(..., "--method", "-m", help="Tool method to invoke"),
|
|
64
|
-
format: str = typer.Option("table", "--format", "-f", help="Output format"),
|
|
65
|
-
):
|
|
66
|
-
"""Invoke a tool."""
|
|
67
|
-
try:
|
|
68
|
-
with get_client() as client:
|
|
69
|
-
data = {
|
|
70
|
-
"method": method,
|
|
71
|
-
"parameters": {},
|
|
72
|
-
"context": {},
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
info(f"Invoking tool {tool_id} method {method}...")
|
|
76
|
-
response = client.post(f"/api/v1/tools/{tool_id}/invoke", data)
|
|
77
|
-
|
|
78
|
-
success("Tool invoked successfully")
|
|
79
|
-
print_output(response, format_type=format)
|
|
80
|
-
|
|
81
|
-
except Exception as e:
|
|
82
|
-
error(f"Failed to invoke tool: {e}")
|
|
83
|
-
raise typer.Exit(1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|