Skip to content

Commit f8ce180

Browse files
authored
Feature: simple login to prime (#35)
* simple login to prime using `prime login`
1 parent 6fc435b commit f8ce180

File tree

4 files changed

+165
-10
lines changed

4 files changed

+165
-10
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ dependencies = [
1212
"typer[all]>=0.9.0",
1313
"requests>=2.31.0",
1414
"rich>=13.3.1",
15-
"pydantic>=2.0.0"
15+
"pydantic>=2.0.0",
16+
"cryptography>=41.0.0"
1617
]
1718
keywords = ["cli", "gpu", "cloud", "compute"]
1819
classifiers = [

src/prime_cli/commands/login.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import base64
2+
import time
3+
from typing import Optional
4+
5+
import requests
6+
import typer
7+
from cryptography.hazmat.primitives import hashes, serialization
8+
from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
9+
from cryptography.hazmat.primitives.asymmetric import rsa
10+
from rich.console import Console
11+
12+
from ..config import Config
13+
14+
app = typer.Typer(help="Login to Prime Intellect")
15+
console = Console()
16+
17+
18+
def generate_ephemeral_keypair() -> tuple[rsa.RSAPrivateKey, str]:
19+
"""Generate a temporary RSA key pair for secure communication"""
20+
try:
21+
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
22+
public_key = private_key.public_key()
23+
24+
# Serialize public key to PEM format
25+
public_pem = public_key.public_bytes(
26+
encoding=serialization.Encoding.PEM,
27+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
28+
).decode("utf-8")
29+
30+
return private_key, public_pem
31+
except Exception as e:
32+
console.print(f"[red]Error generating keypair: {str(e)}[/red]")
33+
raise typer.Exit(1)
34+
35+
36+
def decrypt_challenge_response(
37+
private_key: rsa.RSAPrivateKey, encrypted_response: bytes
38+
) -> Optional[bytes]:
39+
"""Decrypt the challenge response using the private key"""
40+
try:
41+
decrypted: bytes = private_key.decrypt(
42+
encrypted_response,
43+
asym_padding.OAEP(
44+
mgf=asym_padding.MGF1(algorithm=hashes.SHA256()),
45+
algorithm=hashes.SHA256(),
46+
label=None,
47+
),
48+
)
49+
return decrypted
50+
except Exception as e:
51+
console.print(f"[red]Error decrypting response: {str(e)}[/red]")
52+
return None
53+
54+
55+
@app.callback(invoke_without_command=True)
56+
def login() -> None:
57+
"""Login to Prime Intellect"""
58+
config = Config()
59+
settings = config.view()
60+
61+
if not settings["base_url"]:
62+
console.print(
63+
"[red]Base URL not configured.",
64+
"Please run 'prime config set-base-url' first.",
65+
)
66+
raise typer.Exit(1)
67+
68+
private_key = None
69+
try:
70+
# Generate secure keypair
71+
private_key, public_pem = generate_ephemeral_keypair()
72+
73+
response = requests.post(
74+
f"{settings['base_url']}/api/v1/auth_challenge/generate",
75+
json={
76+
"encryptionPublicKey": public_pem,
77+
},
78+
)
79+
80+
if response.status_code != 200:
81+
console.print(
82+
"[red]Failed to generate challenge:",
83+
f"{response.json().get('detail', 'Unknown error')}[/red]",
84+
)
85+
raise typer.Exit(1)
86+
87+
challenge_response = response.json()
88+
89+
console.print("\n[bold blue]To login, please follow these steps:[/bold blue]")
90+
console.print(
91+
"1. Open ",
92+
"[link]https://app.primeintellect.ai/dashboard/tokens/challenge[/link]",
93+
)
94+
console.print(
95+
"2. Enter this code:",
96+
f"[bold green]{challenge_response['challenge']}[/bold green]",
97+
)
98+
console.print("\nWaiting for authentication...")
99+
100+
challenge_auth_header = f"Bearer {challenge_response['status_auth_token']}"
101+
while True:
102+
try:
103+
status_response = requests.get(
104+
f"{settings['base_url']}/api/v1/auth_challenge/status",
105+
params={"challenge": challenge_response["challenge"]},
106+
headers={"Authorization": challenge_auth_header},
107+
)
108+
109+
if status_response.status_code == 404:
110+
console.print("[red]Challenge expired[/red]")
111+
break
112+
113+
status_data = status_response.json()
114+
if status_data.get("result"):
115+
# Decrypt the result
116+
encrypted_result = base64.b64decode(status_data["result"])
117+
decrypted_result = decrypt_challenge_response(
118+
private_key, encrypted_result
119+
)
120+
if decrypted_result:
121+
# Update config with decrypted token
122+
config.set_api_key(decrypted_result.decode())
123+
console.print("[green]Successfully logged in![/green]")
124+
else:
125+
console.print(
126+
"[red]Failed to decrypt authentication token[/red]"
127+
)
128+
break
129+
130+
time.sleep(5)
131+
except requests.exceptions.RequestException:
132+
console.print("[red]Failed to connect to server. Retrying...[/red]")
133+
time.sleep(5)
134+
continue
135+
136+
except KeyboardInterrupt:
137+
console.print("\n[yellow]Login cancelled by user[/yellow]")
138+
raise typer.Exit(1)
139+
except Exception as e:
140+
console.print(f"[red]An error occurred: {str(e)}[/red]")
141+
raise typer.Exit(1)
142+
finally:
143+
# Ensure private key is securely wiped
144+
if private_key:
145+
del private_key

src/prime_cli/commands/pods.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import os
44
import subprocess
5+
import time
56
from datetime import datetime
67
from typing import Dict, List, Optional, Tuple, Union
78

@@ -752,16 +753,22 @@ def connect(pod_id: str) -> None:
752753
base_client = APIClient()
753754
pods_client = PodsClient(base_client)
754755

755-
# Get pod status to check SSH connection details
756-
statuses = pods_client.get_status([pod_id])
757-
if not statuses:
758-
console.print(f"[red]No status found for pod {pod_id}[/red]")
759-
raise typer.Exit(1)
756+
# Keep trying until SSH connection is available
757+
status_message = "[bold blue]Waiting for SSH connection to become available..."
758+
with console.status(status_message, spinner="dots"):
759+
while True:
760+
# Get pod status to check SSH connection details
761+
statuses = pods_client.get_status([pod_id])
762+
if not statuses:
763+
console.print(f"[red]No status found for pod {pod_id}[/red]")
764+
raise typer.Exit(1)
760765

761-
status = statuses[0]
762-
if not status.ssh_connection:
763-
console.print(f"[red]SSH connection not available for pod {pod_id}[/red]")
764-
raise typer.Exit(1)
766+
status = statuses[0]
767+
if status.ssh_connection:
768+
break
769+
770+
# Wait before retrying
771+
time.sleep(5) # Wait 5 seconds before retrying
765772

766773
# Get SSH key path from config
767774
ssh_key_path = config.ssh_key_path

src/prime_cli/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from .commands.availability import app as availability_app
66
from .commands.config import app as config_app
7+
from .commands.login import app as login_app
78
from .commands.pods import app as pods_app
89

910
__version__ = version("prime-cli")
@@ -13,6 +14,7 @@
1314
app.add_typer(availability_app, name="availability")
1415
app.add_typer(config_app, name="config")
1516
app.add_typer(pods_app, name="pods")
17+
app.add_typer(login_app, name="login")
1618

1719

1820
@app.callback(invoke_without_command=True)

0 commit comments

Comments
 (0)