agentic-fabriq-sdk 0.1.14__py3-none-any.whl → 0.1.16__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/commands/tools.py CHANGED
@@ -13,7 +13,7 @@ app = typer.Typer(help="Tool management commands")
13
13
 
14
14
  @app.command()
15
15
  def list(
16
- format: str = typer.Option("table", "--format", "-f", help="Output format"),
16
+ format: str = typer.Option("table", "--format", help="Output format"),
17
17
  ):
18
18
  """List your tool connections (configured and connected tools)."""
19
19
  try:
@@ -56,7 +56,7 @@ def list(
56
56
  @app.command()
57
57
  def get(
58
58
  connection_id: str = typer.Argument(..., help="Connection ID (e.g., 'google', 'slack')"),
59
- format: str = typer.Option("table", "--format", "-f", help="Output format"),
59
+ format: str = typer.Option("table", "--format", help="Output format"),
60
60
  ):
61
61
  """Get tool connection details."""
62
62
  try:
@@ -121,24 +121,98 @@ def get(
121
121
 
122
122
  @app.command()
123
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"),
124
+ tool_identifier: str = typer.Argument(..., help="Tool name (e.g., 'slack') or UUID"),
125
+ method: str = typer.Option(..., "--method", help="Tool method to invoke"),
126
+ params: str = typer.Option(None, "--params", help="JSON string of method parameters (e.g., '{\"channel\": \"test\", \"text\": \"Hello\"}')"),
127
+ format: str = typer.Option("json", "--format", help="Output format (json, table, yaml)"),
127
128
  ):
128
- """Invoke a tool."""
129
+ """Invoke a tool by name or UUID.
130
+
131
+ Examples:
132
+ afctl tools invoke slack --method get_channels
133
+ afctl tools invoke slack --method post_message --params '{"channel": "test", "text": "Hello!"}'
134
+ afctl tools invoke c17b55af-18f8-49be-b9e9-44b1d1be429e --method get_channels
135
+ """
129
136
  try:
137
+ # Parse parameters if provided
138
+ parameters = {}
139
+ if params:
140
+ import json as json_lib
141
+ try:
142
+ parameters = json_lib.loads(params)
143
+ except json_lib.JSONDecodeError as e:
144
+ error(f"Invalid JSON in --params: {e}")
145
+ raise typer.Exit(1)
146
+
130
147
  with get_client() as client:
148
+ # Try to resolve tool name to UUID if not already a UUID
149
+ tool_id = tool_identifier
150
+ try:
151
+ from uuid import UUID
152
+ UUID(tool_identifier)
153
+ # It's already a UUID, use it directly
154
+ except ValueError:
155
+ # Not a UUID, try to look up by name
156
+ info(f"Looking up tool '{tool_identifier}'...")
157
+ try:
158
+ tools_response = client.get("/api/v1/tools")
159
+
160
+ # Handle different response formats
161
+ if isinstance(tools_response, dict) and "tools" in tools_response:
162
+ tools = tools_response["tools"]
163
+ elif isinstance(tools_response, dict) and "items" in tools_response:
164
+ tools = tools_response["items"]
165
+ elif hasattr(tools_response, '__iter__') and not isinstance(tools_response, (str, dict)):
166
+ tools = list(tools_response)
167
+ else:
168
+ tools = []
169
+
170
+ # Find tool by name (case-insensitive)
171
+ matching_tools = [t for t in tools if isinstance(t, dict) and t.get("name", "").lower() == tool_identifier.lower()]
172
+
173
+ if not matching_tools:
174
+ error(f"Tool '{tool_identifier}' not found. Available tools:")
175
+ for t in tools:
176
+ if isinstance(t, dict):
177
+ print(f" - {t.get('name')} (ID: {t.get('id')})")
178
+ raise typer.Exit(1)
179
+
180
+ if len(matching_tools) > 1:
181
+ error(f"Multiple tools found with name '{tool_identifier}':")
182
+ for t in matching_tools:
183
+ print(f" - {t.get('name')} (ID: {t.get('id')})")
184
+ error("Please use the UUID instead.")
185
+ raise typer.Exit(1)
186
+
187
+ tool_id = matching_tools[0].get("id")
188
+ info(f"Resolved '{tool_identifier}' to tool ID: {tool_id}")
189
+ except Exception as lookup_error:
190
+ error(f"Failed to look up tool: {lookup_error}")
191
+ raise typer.Exit(1)
192
+
131
193
  data = {
132
194
  "method": method,
133
- "parameters": {},
195
+ "parameters": parameters,
134
196
  "context": {},
135
197
  }
136
198
 
137
- info(f"Invoking tool {tool_id} method {method}...")
199
+ info(f"Invoking tool {tool_identifier} method {method}...")
138
200
  response = client.post(f"/api/v1/tools/{tool_id}/invoke", data)
139
201
 
140
202
  success("Tool invoked successfully")
141
- print_output(response, format_type=format)
203
+
204
+ # For tool invocations, show the result in a more readable format
205
+ if format == "json":
206
+ import json as json_lib
207
+ print(json_lib.dumps(response, indent=2))
208
+ elif format == "yaml":
209
+ import yaml
210
+ print(yaml.dump(response, default_flow_style=False))
211
+ else:
212
+ # For table format, show just the result field nicely
213
+ result = response.get("result", {})
214
+ print("\n📊 Result:")
215
+ print_output(result, format_type="json")
142
216
 
143
217
  except Exception as e:
144
218
  error(f"Failed to invoke tool: {e}")
@@ -150,30 +224,28 @@ def add(
150
224
  tool: str = typer.Argument(..., help="Tool name (google_drive, google_slides, slack, notion, github, etc.)"),
151
225
  connection_id: str = typer.Option(..., "--connection-id", help="Unique connection ID"),
152
226
  display_name: str = typer.Option(None, "--display-name", help="Human-readable name"),
153
- method: str = typer.Option(..., "--method", help="Connection method: 'api', 'credentials', or 'oauth'"),
154
-
155
- # API method fields
156
- token: str = typer.Option(None, "--token", help="API token (for api method)"),
227
+ method: str = typer.Option(..., "--method", help="Connection method: 'api_credentials' or 'oauth'"),
157
228
 
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)"),
229
+ # API credentials method fields (can be either a token OR client_id/secret)
230
+ token: str = typer.Option(None, "--token", help="API token (for simple token-based auth like Notion, Slack bot)"),
231
+ client_id: str = typer.Option(None, "--client-id", help="OAuth client ID (for app-based auth like Google, Slack)"),
232
+ client_secret: str = typer.Option(None, "--client-secret", help="OAuth client secret (for app-based auth)"),
161
233
  redirect_uri: str = typer.Option(None, "--redirect-uri", help="OAuth redirect URI (optional, auto-generated)"),
162
234
  ):
163
235
  """
164
236
  Add a new tool connection with credentials.
165
237
 
166
238
  Examples:
167
- # Notion (api method - single token)
168
- afctl tools add notion --connection-id notion-work --method api --token "secret_abc123"
239
+ # Notion (api_credentials method - single token)
240
+ afctl tools add notion --connection-id notion-work --method api_credentials --token "secret_abc123"
169
241
 
170
- # Google (credentials method - OAuth app)
171
- afctl tools add google --connection-id google-work --method credentials \\
242
+ # Google (api_credentials method - OAuth app)
243
+ afctl tools add google_drive --connection-id google-work --method api_credentials \\
172
244
  --client-id "123.apps.googleusercontent.com" \\
173
245
  --client-secret "GOCSPX-abc123"
174
246
 
175
- # Slack bot (api method)
176
- afctl tools add slack --connection-id slack-bot --method api --token "xoxb-123..."
247
+ # Slack bot (api_credentials method - single token)
248
+ afctl tools add slack --connection-id slack-bot --method api_credentials --token "xoxb-123..."
177
249
  """
178
250
  try:
179
251
  from af_cli.core.config import get_config
@@ -202,23 +274,24 @@ def add(
202
274
  raise typer.Exit(1)
203
275
 
204
276
  # Validate method
205
- if method not in ["api", "credentials", "oauth"]:
206
- error("Method must be 'api', 'credentials', or 'oauth'")
277
+ if method not in ["api_credentials", "oauth"]:
278
+ error("Method must be 'api_credentials' or 'oauth'")
207
279
  raise typer.Exit(1)
208
280
 
209
- # Validate API method requirements
210
- if method == "api":
211
- if not token:
212
- error("API method requires --token")
213
- info(f"Example: afctl tools add {tool} --connection-id {connection_id} --method api --token YOUR_TOKEN")
214
- raise typer.Exit(1)
215
-
216
- # Validate credentials method requirements
217
- elif method == "credentials":
218
- if not client_id or not client_secret:
219
- error("Credentials method requires --client-id and --client-secret")
220
- info(f"Example: afctl tools add {tool} --connection-id {connection_id} --method credentials \\")
221
- info(f" --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET")
281
+ # Validate api_credentials method requirements
282
+ if method == "api_credentials":
283
+ # Must have either token OR (client_id + client_secret)
284
+ has_token = bool(token)
285
+ has_oauth_creds = bool(client_id and client_secret)
286
+
287
+ if not has_token and not has_oauth_creds:
288
+ error("api_credentials method requires either:")
289
+ info(" • --token (for simple token auth like Notion, Slack bot)")
290
+ info(" • --client-id and --client-secret (for OAuth app auth like Google)")
291
+ info("")
292
+ info(f"Examples:")
293
+ info(f" afctl tools add {tool} --connection-id {connection_id} --method api_credentials --token YOUR_TOKEN")
294
+ info(f" afctl tools add {tool} --connection-id {connection_id} --method api_credentials --client-id ID --client-secret SECRET")
222
295
  raise typer.Exit(1)
223
296
 
224
297
  info(f"Creating connection: {connection_id}")
@@ -236,51 +309,59 @@ def add(
236
309
  client.post("/api/v1/user-connections", data=connection_data)
237
310
  success(f"✅ Connection entry created: {connection_id}")
238
311
 
239
- # Step 2: Store credentials based on method
240
- if method == "credentials" or method == "oauth":
312
+ # Step 2: Store credentials based on what was provided
313
+ if method == "api_credentials":
241
314
  # Determine the API base tool name (Google tools all use "google")
242
315
  api_tool_name = "google" if (tool.startswith("google_") or tool == "gmail") else tool
243
316
 
244
- # Auto-generate redirect_uri if not provided
245
- if not redirect_uri:
246
- config = get_config()
247
- redirect_uri = f"{config.gateway_url}/api/v1/tools/{api_tool_name}/oauth/callback"
248
- info(f"Using default redirect URI: {redirect_uri}")
249
-
250
- # Store OAuth app config
251
- info("Storing OAuth app credentials...")
252
- config_payload = {
253
- "client_id": client_id,
254
- "client_secret": client_secret,
255
- }
256
- if redirect_uri:
257
- config_payload["redirect_uri"] = redirect_uri
258
-
259
- client.post(
260
- f"/api/v1/tools/{api_tool_name}/config?connection_id={connection_id}",
261
- data=config_payload
262
- )
263
- success("✅ OAuth app credentials stored")
264
- info("")
265
- info(f"Next: Run 'afctl tools connect {connection_id}' to complete OAuth setup")
266
-
267
- elif method == "api":
268
- # Store API token directly
269
- info("Storing API credentials...")
270
-
271
- # Tool-specific endpoint and payload mappings
272
- if tool == "notion":
273
- # Notion uses /config endpoint with integration_token field
274
- endpoint = f"/api/v1/tools/{tool}/config?connection_id={connection_id}"
275
- cred_payload = {"integration_token": token}
276
- else:
277
- # Generic tools use /connection endpoint with api_token field
278
- endpoint = f"/api/v1/tools/{tool}/connection?connection_id={connection_id}"
279
- cred_payload = {"api_token": token}
280
-
281
- client.post(endpoint, data=cred_payload)
282
- success("✅ API credentials stored")
283
- success(f"✅ Connection '{connection_id}' is ready to use!")
317
+ if token:
318
+ # Simple token-based auth (Notion, Slack bot, etc.)
319
+ info("Storing API token...")
320
+
321
+ # Tool-specific endpoint and payload mappings
322
+ if tool == "notion":
323
+ # Notion uses /config endpoint with integration_token field
324
+ endpoint = f"/api/v1/tools/{tool}/config?connection_id={connection_id}"
325
+ cred_payload = {"integration_token": token}
326
+ else:
327
+ # Generic tools use /connection endpoint with api_token field
328
+ endpoint = f"/api/v1/tools/{tool}/connection?connection_id={connection_id}"
329
+ cred_payload = {"api_token": token}
330
+
331
+ client.post(endpoint, data=cred_payload)
332
+ success("✅ API token stored")
333
+ success(f"✅ Connection '{connection_id}' is ready to use!")
334
+
335
+ elif client_id and client_secret:
336
+ # OAuth app credentials (Google, Slack app, etc.)
337
+ # Auto-generate redirect_uri if not provided
338
+ if not redirect_uri:
339
+ config = get_config()
340
+ redirect_uri = f"{config.gateway_url}/api/v1/tools/{api_tool_name}/oauth/callback"
341
+ info(f"Using default redirect URI: {redirect_uri}")
342
+
343
+ # Store OAuth app config
344
+ info("Storing OAuth app credentials...")
345
+ config_payload = {
346
+ "client_id": client_id,
347
+ "client_secret": client_secret,
348
+ }
349
+ if redirect_uri:
350
+ config_payload["redirect_uri"] = redirect_uri
351
+
352
+ client.post(
353
+ f"/api/v1/tools/{api_tool_name}/config?connection_id={connection_id}",
354
+ data=config_payload
355
+ )
356
+ success("✅ OAuth app credentials stored")
357
+ info("")
358
+ info(f"Next: Run 'afctl tools connect {connection_id}' to complete OAuth setup")
359
+
360
+ elif method == "oauth":
361
+ # OAuth flow (legacy, redirect to api_credentials)
362
+ error("The 'oauth' method is deprecated. Please use 'api_credentials' instead.")
363
+ info("All credential storage now uses the 'api_credentials' method.")
364
+ raise typer.Exit(1)
284
365
 
285
366
  # Show helpful info
286
367
  info("")
@@ -320,14 +401,7 @@ def connect(
320
401
  tool = connection["tool"]
321
402
  method = connection["method"]
322
403
 
323
- # Only credentials/oauth method needs OAuth completion
324
- if method not in ["credentials", "oauth"]:
325
- error(f"Connection '{connection_id}' uses '{method}' method")
326
- info("Only 'credentials' or 'oauth' method connections need OAuth setup")
327
- info("API connections are already connected after 'afctl tools add'")
328
- raise typer.Exit(1)
329
-
330
- # Check if already connected
404
+ # Check if connection is already set up (has credentials stored)
331
405
  if connection.get("connected"):
332
406
  warning(f"Connection '{connection_id}' is already connected")
333
407
  confirm = typer.confirm("Do you want to reconnect (re-authorize)?")
@@ -413,7 +487,7 @@ def connect(
413
487
  @app.command()
414
488
  def disconnect(
415
489
  connection_id: str = typer.Argument(..., help="Connection ID to disconnect"),
416
- force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
490
+ force: bool = typer.Option(False, "--force", help="Skip confirmation"),
417
491
  ):
418
492
  """Disconnect a tool (remove credentials but keep connection entry)."""
419
493
  try:
@@ -466,7 +540,7 @@ def disconnect(
466
540
  @app.command()
467
541
  def remove(
468
542
  connection_id: str = typer.Argument(..., help="Connection ID to remove"),
469
- force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
543
+ force: bool = typer.Option(False, "--force", help="Skip confirmation"),
470
544
  ):
471
545
  """Remove a tool connection completely (delete entry and credentials)."""
472
546
  try:
af_cli/main.py CHANGED
@@ -10,6 +10,7 @@ from rich.console import Console
10
10
  from rich.table import Table
11
11
 
12
12
  from af_cli.commands.agents import app as agents_app
13
+ from af_cli.commands.applications import app as applications_app
13
14
  from af_cli.commands.auth import app as auth_app
14
15
  from af_cli.commands.config import app as config_app
15
16
  from af_cli.commands.mcp_servers import app as mcp_servers_app
@@ -31,6 +32,7 @@ app.add_typer(auth_app, name="auth", help="Authentication commands")
31
32
  app.add_typer(config_app, name="config", help="Configuration commands")
32
33
  app.add_typer(agents_app, name="agents", help="Agent management commands")
33
34
  app.add_typer(tools_app, name="tools", help="Tool management commands")
35
+ app.add_typer(applications_app, name="applications", help="Application management commands")
34
36
  app.add_typer(mcp_servers_app, name="mcp-servers", help="MCP server management commands")
35
37
  app.add_typer(secrets_app, name="secrets", help="Secret management commands")
36
38
 
@@ -85,19 +87,16 @@ def init(
85
87
  gateway_url: str = typer.Option(
86
88
  "https://dashboard.agenticfabriq.com",
87
89
  "--gateway-url",
88
- "-g",
89
90
  help="Gateway URL"
90
91
  ),
91
92
  tenant_id: Optional[str] = typer.Option(
92
93
  None,
93
94
  "--tenant-id",
94
- "-t",
95
95
  help="Tenant ID"
96
96
  ),
97
97
  force: bool = typer.Option(
98
98
  False,
99
99
  "--force",
100
- "-f",
101
100
  help="Force initialization, overwrite existing config"
102
101
  ),
103
102
  ):
@@ -134,25 +133,21 @@ def main(
134
133
  config_file: Optional[str] = typer.Option(
135
134
  None,
136
135
  "--config",
137
- "-c",
138
136
  help="Path to configuration file"
139
137
  ),
140
138
  gateway_url: Optional[str] = typer.Option(
141
139
  None,
142
140
  "--gateway-url",
143
- "-g",
144
141
  help="Gateway URL"
145
142
  ),
146
143
  tenant_id: Optional[str] = typer.Option(
147
144
  None,
148
145
  "--tenant-id",
149
- "-t",
150
146
  help="Tenant ID"
151
147
  ),
152
148
  verbose: bool = typer.Option(
153
149
  False,
154
150
  "--verbose",
155
- "-v",
156
151
  help="Enable verbose output"
157
152
  ),
158
153
  ):
af_sdk/__init__.py CHANGED
@@ -23,6 +23,14 @@ from .models.types import (
23
23
  from .transport.http import HTTPClient
24
24
  from .fabriq_client import FabriqClient
25
25
  from .models.audit import AuditEvent
26
+ from .auth import (
27
+ get_application_client,
28
+ load_application_config,
29
+ save_application_config,
30
+ list_applications,
31
+ delete_application_config,
32
+ ApplicationNotFoundError,
33
+ )
26
34
 
27
35
  __version__ = "1.0.0"
28
36
 
@@ -44,6 +52,13 @@ __all__ = [
44
52
  "HTTPClient",
45
53
  "FabriqClient",
46
54
  "AuditEvent",
55
+ # Application auth helpers
56
+ "get_application_client",
57
+ "load_application_config",
58
+ "save_application_config",
59
+ "list_applications",
60
+ "delete_application_config",
61
+ "ApplicationNotFoundError",
47
62
  ]
48
63
 
49
64
  # Lazy expose dx submodule under af_sdk.dx
af_sdk/auth/__init__.py CHANGED
@@ -11,6 +11,15 @@ from .oauth import (
11
11
  TokenValidator,
12
12
  )
13
13
  from .token_cache import TokenManager, VaultClient
14
+ from .applications import (
15
+ get_application_client,
16
+ load_application_config,
17
+ save_application_config,
18
+ list_applications,
19
+ delete_application_config,
20
+ ApplicationNotFoundError,
21
+ AuthenticationError,
22
+ )
14
23
 
15
24
  # DPoP helper will be provided from af_sdk.auth.dpop
16
25
  try:
@@ -28,4 +37,11 @@ __all__ = [
28
37
  "TokenManager",
29
38
  "VaultClient",
30
39
  "create_dpop_proof",
40
+ "get_application_client",
41
+ "load_application_config",
42
+ "save_application_config",
43
+ "list_applications",
44
+ "delete_application_config",
45
+ "ApplicationNotFoundError",
46
+ "AuthenticationError",
31
47
  ]