Files
zooqueenandHanzo Dev b144973bfc fix(lint): resolve all ruff + black errors across all packages
Fix 35 ruff errors (E722, F841, B904, B905, B007, E741, S202, S310,
S306, S110, TID251, UP042, F401) and run black on 526 files.

Co-authored-by: Hanzo Dev <[email protected]>
2026-02-21 23:32:32 -08:00

385 lines
12 KiB
Python

"""Platform authentication utilities for Hanzo Platform MCP.
This module provides utilities for authenticating with Hanzo Platform,
including:
- Getting API keys
- Getting deployment tokens
- Setting up CI/CD credentials
Usage:
# Interactive login
hanzo-platform login
# Get deploy token for CI
hanzo-platform deploy-token --app console
# Check authentication status
hanzo-platform status
"""
import os
import sys
import json
import asyncio
import webbrowser
from typing import Any, Dict, Optional
from pathlib import Path
from dataclasses import dataclass
import httpx
@dataclass
class PlatformConfig:
"""Configuration for Platform API."""
base_url: str = "https://platform.hanzo.ai"
api_key: Optional[str] = None
org_id: Optional[str] = None
@classmethod
def load(cls) -> "PlatformConfig":
"""Load configuration from file and environment."""
config = cls()
# Load from file
config_file = Path.home() / ".hanzo" / "platform" / "config.json"
if config_file.exists():
try:
with open(config_file, "r") as f:
data = json.load(f)
config.base_url = data.get("base_url", config.base_url)
config.api_key = data.get("api_key")
config.org_id = data.get("org_id")
except Exception:
pass
# Environment overrides
if os.environ.get("PLATFORM_URL"):
config.base_url = os.environ["PLATFORM_URL"]
if os.environ.get("PLATFORM_API_KEY"):
config.api_key = os.environ["PLATFORM_API_KEY"]
if os.environ.get("PLATFORM_ORG_ID"):
config.org_id = os.environ["PLATFORM_ORG_ID"]
return config
def save(self):
"""Save configuration to file."""
config_file = Path.home() / ".hanzo" / "platform" / "config.json"
config_file.parent.mkdir(parents=True, exist_ok=True)
data = {
"base_url": self.base_url,
"api_key": self.api_key,
"org_id": self.org_id,
}
with open(config_file, "w") as f:
json.dump(data, f, indent=2)
# Set restrictive permissions
try:
import stat
config_file.chmod(stat.S_IRUSR | stat.S_IWUSR) # 0600
except Exception:
pass
class PlatformClient:
"""Client for Hanzo Platform API."""
def __init__(self, config: Optional[PlatformConfig] = None):
self.config = config or PlatformConfig.load()
self._client: Optional[httpx.AsyncClient] = None
async def __aenter__(self):
self._client = httpx.AsyncClient(
base_url=self.config.base_url,
headers=self._get_headers(),
timeout=30.0,
)
return self
async def __aexit__(self, *args):
if self._client:
await self._client.aclose()
def _get_headers(self) -> Dict[str, str]:
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
if self.config.api_key:
headers["x-api-key"] = self.config.api_key
return headers
async def check_auth(self) -> Dict[str, Any]:
"""Check if authentication is valid."""
if not self._client:
raise RuntimeError("Client not initialized")
try:
response = await self._client.get("/api/v1/user/me")
if response.status_code == 200:
return {"authenticated": True, "user": response.json()}
return {"authenticated": False, "error": "Invalid API key"}
except Exception as e:
return {"authenticated": False, "error": str(e)}
async def list_projects(self) -> Dict[str, Any]:
"""List all projects."""
if not self._client:
raise RuntimeError("Client not initialized")
response = await self._client.get("/api/v1/projects")
return response.json()
async def list_applications(self, project_id: str) -> Dict[str, Any]:
"""List applications in a project."""
if not self._client:
raise RuntimeError("Client not initialized")
response = await self._client.get(f"/api/v1/projects/{project_id}/applications")
return response.json()
async def get_deploy_token(self, app_id: str) -> Optional[str]:
"""Get or create a deploy token for an application."""
if not self._client:
raise RuntimeError("Client not initialized")
# Get application details including refresh token
response = await self._client.get(f"/api/v1/applications/{app_id}")
if response.status_code != 200:
return None
app_data = response.json()
return app_data.get("application", {}).get("refreshToken")
async def create_api_key(self, name: str = "hanzo-mcp") -> Optional[str]:
"""Create a new API key."""
if not self._client:
raise RuntimeError("Client not initialized")
response = await self._client.post(
"/api/v1/api-keys",
json={"name": name, "expiresAt": None},
)
if response.status_code == 201:
data = response.json()
return data.get("apiKey", {}).get("key")
return None
async def interactive_login() -> bool:
"""Interactive login flow."""
print("=== Hanzo Platform Login ===")
print()
config = PlatformConfig.load()
# Check if already logged in
if config.api_key:
async with PlatformClient(config) as client:
result = await client.check_auth()
if result.get("authenticated"):
user = result.get("user", {})
print(f"Already logged in as: {user.get('email', 'Unknown')}")
confirm = input("Re-authenticate? [y/N]: ").strip().lower()
if confirm != "y":
return True
print(f"Opening {config.base_url}/settings/api-keys in your browser...")
print()
# Try to open browser
try:
webbrowser.open(f"{config.base_url}/settings/api-keys")
except Exception:
print(
f"Could not open browser. Please visit: {config.base_url}/settings/api-keys"
)
print("1. Create a new API key in the Platform dashboard")
print("2. Copy the API key")
print()
api_key = input("Paste your API key here: ").strip()
if not api_key:
print("Error: No API key provided")
return False
# Validate the API key
config.api_key = api_key
async with PlatformClient(config) as client:
result = await client.check_auth()
if result.get("authenticated"):
user = result.get("user", {})
print(f"\n✅ Successfully authenticated as: {user.get('email')}")
# Save the configuration
config.save()
# Also set in environment for current session
os.environ["PLATFORM_API_KEY"] = api_key
print(f"\nConfiguration saved to: ~/.hanzo/platform/config.json")
print(f"For CI/CD, set: PLATFORM_API_KEY={api_key[:8]}...")
return True
else:
print(f"\n❌ Authentication failed: {result.get('error')}")
return False
async def get_deploy_token_cli(app_name: str) -> Optional[str]:
"""Get deploy token for an application."""
config = PlatformConfig.load()
if not config.api_key:
print("Error: Not logged in. Run 'hanzo-platform login' first.")
return None
async with PlatformClient(config) as client:
# List projects to find the app
projects = await client.list_projects()
for project in projects.get("projects", []):
apps = await client.list_applications(project["id"])
for app in apps.get("applications", []):
if app.get("name") == app_name or app.get("id") == app_name:
token = await client.get_deploy_token(app["id"])
if token:
return token
print(f"Error: Application '{app_name}' not found")
return None
async def status() -> Dict[str, Any]:
"""Check authentication status and available resources."""
config = PlatformConfig.load()
result = {
"configured": bool(config.api_key),
"base_url": config.base_url,
"authenticated": False,
"user": None,
"projects": [],
}
if not config.api_key:
return result
async with PlatformClient(config) as client:
auth_result = await client.check_auth()
result["authenticated"] = auth_result.get("authenticated", False)
result["user"] = auth_result.get("user")
if result["authenticated"]:
try:
projects = await client.list_projects()
result["projects"] = projects.get("projects", [])
except Exception:
pass
return result
def cli():
"""CLI entry point."""
import argparse
parser = argparse.ArgumentParser(
description="Hanzo Platform authentication",
prog="hanzo-platform",
)
subparsers = parser.add_subparsers(dest="command")
# Login command
subparsers.add_parser("login", help="Interactive login")
# Status command
subparsers.add_parser("status", help="Check authentication status")
# Deploy token command
deploy_parser = subparsers.add_parser(
"deploy-token", help="Get deploy token for app"
)
deploy_parser.add_argument("--app", required=True, help="Application name or ID")
deploy_parser.add_argument("--output", choices=["plain", "json"], default="plain")
# Config command
config_parser = subparsers.add_parser("config", help="Show/set configuration")
config_parser.add_argument("--set-url", help="Set Platform URL")
config_parser.add_argument("--set-key", help="Set API key")
args = parser.parse_args()
if args.command == "login":
success = asyncio.run(interactive_login())
sys.exit(0 if success else 1)
elif args.command == "status":
result = asyncio.run(status())
if result["authenticated"]:
user = result.get("user", {})
print(f"✅ Authenticated as: {user.get('email', 'Unknown')}")
print(f" Platform: {result['base_url']}")
print(f" Projects: {len(result.get('projects', []))}")
for project in result.get("projects", []):
print(f" - {project.get('name', 'Unknown')}")
else:
print("❌ Not authenticated")
print(f" Platform: {result['base_url']}")
print("\nRun 'hanzo-platform login' to authenticate")
sys.exit(0)
elif args.command == "deploy-token":
token = asyncio.run(get_deploy_token_cli(args.app))
if token:
if args.output == "json":
print(json.dumps({"token": token}))
else:
print(token)
sys.exit(0)
sys.exit(1)
elif args.command == "config":
config = PlatformConfig.load()
if args.set_url:
config.base_url = args.set_url
config.save()
print(f"Set Platform URL to: {args.set_url}")
if args.set_key:
config.api_key = args.set_key
config.save()
print(f"Set API key: {args.set_key[:8]}...")
if not args.set_url and not args.set_key:
print(f"Platform URL: {config.base_url}")
print(
f"API Key: {'****' + config.api_key[-4:] if config.api_key else 'Not set'}"
)
print(f"Org ID: {config.org_id or 'Not set'}")
sys.exit(0)
else:
parser.print_help()
sys.exit(1)
if __name__ == "__main__":
cli()