computeid-cli 1.0.0__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.
cli.py
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ComputeID CLI — Command line tool for ComputeID identity management
|
|
4
|
+
Every GPU needs a passport. Every AI agent needs an identity.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import requests
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
from rich import box
|
|
21
|
+
from rich.style import Style
|
|
22
|
+
RICH = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
RICH = False
|
|
25
|
+
|
|
26
|
+
API_URL = "https://api.aicomputeid.com"
|
|
27
|
+
CONFIG_FILE = Path.home() / ".computeid" / "config.json"
|
|
28
|
+
|
|
29
|
+
console = Console() if RICH else None
|
|
30
|
+
|
|
31
|
+
# ── HELPERS ───────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
def load_config():
|
|
34
|
+
if CONFIG_FILE.exists():
|
|
35
|
+
with open(CONFIG_FILE) as f:
|
|
36
|
+
return json.load(f)
|
|
37
|
+
return {}
|
|
38
|
+
|
|
39
|
+
def save_config(cfg):
|
|
40
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
with open(CONFIG_FILE, 'w') as f:
|
|
42
|
+
json.dump(cfg, f, indent=2)
|
|
43
|
+
|
|
44
|
+
def get_token():
|
|
45
|
+
cfg = load_config()
|
|
46
|
+
return cfg.get("token")
|
|
47
|
+
|
|
48
|
+
def get_api_url():
|
|
49
|
+
cfg = load_config()
|
|
50
|
+
return cfg.get("api_url", API_URL)
|
|
51
|
+
|
|
52
|
+
def auth_headers():
|
|
53
|
+
token = get_token()
|
|
54
|
+
if not token:
|
|
55
|
+
error("Not logged in. Run: computeid login")
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
return {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
"Authorization": f"Bearer {token}"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
def success(msg):
|
|
63
|
+
if RICH:
|
|
64
|
+
console.print(f"[bold green]✓[/bold green] {msg}")
|
|
65
|
+
else:
|
|
66
|
+
print(f"✓ {msg}")
|
|
67
|
+
|
|
68
|
+
def error(msg):
|
|
69
|
+
if RICH:
|
|
70
|
+
console.print(f"[bold red]✗[/bold red] {msg}")
|
|
71
|
+
else:
|
|
72
|
+
print(f"✗ {msg}")
|
|
73
|
+
|
|
74
|
+
def info(msg):
|
|
75
|
+
if RICH:
|
|
76
|
+
console.print(f"[bold cyan]→[/bold cyan] {msg}")
|
|
77
|
+
else:
|
|
78
|
+
print(f"→ {msg}")
|
|
79
|
+
|
|
80
|
+
def warn(msg):
|
|
81
|
+
if RICH:
|
|
82
|
+
console.print(f"[bold yellow]⚠[/bold yellow] {msg}")
|
|
83
|
+
else:
|
|
84
|
+
print(f"⚠ {msg}")
|
|
85
|
+
|
|
86
|
+
def print_header():
|
|
87
|
+
if RICH:
|
|
88
|
+
console.print()
|
|
89
|
+
console.print(Panel.fit(
|
|
90
|
+
"[bold white]COMPUTE[/bold white][bold cyan]ID[/bold cyan] [dim]Every GPU needs a passport. We issue them.[/dim]",
|
|
91
|
+
border_style="cyan",
|
|
92
|
+
padding=(0, 2)
|
|
93
|
+
))
|
|
94
|
+
console.print()
|
|
95
|
+
|
|
96
|
+
def fmt_status(status):
|
|
97
|
+
colors = {
|
|
98
|
+
"active": "[bold green]● active[/bold green]",
|
|
99
|
+
"pending": "[bold yellow]● pending[/bold yellow]",
|
|
100
|
+
"revoked": "[bold red]● revoked[/bold red]",
|
|
101
|
+
"expired": "[bold red]● expired[/bold red]",
|
|
102
|
+
}
|
|
103
|
+
return colors.get(status, status) if RICH else status
|
|
104
|
+
|
|
105
|
+
def fmt_time(ts):
|
|
106
|
+
try:
|
|
107
|
+
dt = datetime.fromisoformat(ts.replace("Z",""))
|
|
108
|
+
return dt.strftime("%Y-%m-%d %H:%M")
|
|
109
|
+
except:
|
|
110
|
+
return ts or "—"
|
|
111
|
+
|
|
112
|
+
# ── CLI ROOT ──────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
@click.group()
|
|
115
|
+
@click.version_option("1.0.0", prog_name="computeid")
|
|
116
|
+
def cli():
|
|
117
|
+
"""
|
|
118
|
+
ComputeID CLI — Cryptographic identity for AI compute infrastructure.
|
|
119
|
+
|
|
120
|
+
Every GPU needs a passport. Every AI agent needs an identity.
|
|
121
|
+
|
|
122
|
+
Docs: compute-id.com
|
|
123
|
+
"""
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
# ── STATUS ────────────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
@cli.command()
|
|
129
|
+
def status():
|
|
130
|
+
"""Check API health and connection status."""
|
|
131
|
+
print_header()
|
|
132
|
+
api = get_api_url()
|
|
133
|
+
info(f"Connecting to {api}...")
|
|
134
|
+
try:
|
|
135
|
+
r = requests.get(f"{api}/health", timeout=10)
|
|
136
|
+
if r.ok:
|
|
137
|
+
data = r.json()
|
|
138
|
+
success(f"API is online")
|
|
139
|
+
if RICH:
|
|
140
|
+
t = Table(box=box.ROUNDED, border_style="cyan", show_header=False)
|
|
141
|
+
t.add_column("Key", style="bold cyan", width=20)
|
|
142
|
+
t.add_column("Value", style="white")
|
|
143
|
+
t.add_row("API URL", api)
|
|
144
|
+
t.add_row("Status", data.get("status", "running"))
|
|
145
|
+
t.add_row("Time", fmt_time(data.get("time", "")))
|
|
146
|
+
t.add_row("Logged in", "Yes" if get_token() else "No — run: computeid login")
|
|
147
|
+
console.print(t)
|
|
148
|
+
else:
|
|
149
|
+
print(f" API URL: {api}")
|
|
150
|
+
print(f" Status: {data.get('status')}")
|
|
151
|
+
else:
|
|
152
|
+
error(f"API returned {r.status_code}")
|
|
153
|
+
except Exception as e:
|
|
154
|
+
error(f"Cannot reach API: {e}")
|
|
155
|
+
|
|
156
|
+
# ── LOGIN ─────────────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
@cli.command()
|
|
159
|
+
@click.option("--password", "-p", prompt=True, hide_input=True, help="Admin password")
|
|
160
|
+
def login(password):
|
|
161
|
+
"""Login to ComputeID with your admin password."""
|
|
162
|
+
api = get_api_url()
|
|
163
|
+
try:
|
|
164
|
+
r = requests.post(f"{api}/api/admin/login",
|
|
165
|
+
json={"password": password},
|
|
166
|
+
headers={"Content-Type": "application/json"},
|
|
167
|
+
timeout=10)
|
|
168
|
+
data = r.json()
|
|
169
|
+
if r.ok:
|
|
170
|
+
cfg = load_config()
|
|
171
|
+
cfg["token"] = data["token"]
|
|
172
|
+
cfg["api_url"] = api
|
|
173
|
+
save_config(cfg)
|
|
174
|
+
success("Logged in successfully!")
|
|
175
|
+
info("Token saved to ~/.computeid/config.json")
|
|
176
|
+
else:
|
|
177
|
+
error(f"Login failed: {data.get('error', 'Invalid password')}")
|
|
178
|
+
except Exception as e:
|
|
179
|
+
error(f"Login error: {e}")
|
|
180
|
+
|
|
181
|
+
@cli.command()
|
|
182
|
+
def logout():
|
|
183
|
+
"""Logout and clear saved credentials."""
|
|
184
|
+
cfg = load_config()
|
|
185
|
+
cfg.pop("token", None)
|
|
186
|
+
save_config(cfg)
|
|
187
|
+
success("Logged out successfully")
|
|
188
|
+
|
|
189
|
+
# ── DEVICE COMMANDS ───────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
@cli.group()
|
|
192
|
+
def device():
|
|
193
|
+
"""Manage DevicePassports for GPUs and servers."""
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
@device.command("list")
|
|
197
|
+
@click.option("--status", "-s", default=None, help="Filter by status: active, pending, revoked")
|
|
198
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
199
|
+
def device_list(status, as_json):
|
|
200
|
+
"""List all registered devices."""
|
|
201
|
+
try:
|
|
202
|
+
r = requests.get(f"{get_api_url()}/api/devices", headers=auth_headers(), timeout=10)
|
|
203
|
+
if not r.ok:
|
|
204
|
+
error(f"Failed to fetch devices: {r.json().get('error')}")
|
|
205
|
+
return
|
|
206
|
+
devices = r.json()
|
|
207
|
+
if status:
|
|
208
|
+
devices = [d for d in devices if d.get("status") == status]
|
|
209
|
+
if as_json:
|
|
210
|
+
click.echo(json.dumps(devices, indent=2))
|
|
211
|
+
return
|
|
212
|
+
if not devices:
|
|
213
|
+
warn("No devices found")
|
|
214
|
+
return
|
|
215
|
+
if RICH:
|
|
216
|
+
t = Table(title=f"[bold]Devices ({len(devices)})[/bold]",
|
|
217
|
+
box=box.ROUNDED, border_style="cyan", show_lines=True)
|
|
218
|
+
t.add_column("Code", style="bold cyan", width=12)
|
|
219
|
+
t.add_column("Name", style="white", width=22)
|
|
220
|
+
t.add_column("Type", style="dim", width=10)
|
|
221
|
+
t.add_column("IP Address", style="dim", width=16)
|
|
222
|
+
t.add_column("Status", width=14)
|
|
223
|
+
t.add_column("Certificate", width=12)
|
|
224
|
+
t.add_column("Registered", width=16)
|
|
225
|
+
for d in devices:
|
|
226
|
+
t.add_row(
|
|
227
|
+
d.get("device_code", "—"),
|
|
228
|
+
d.get("name", "—"),
|
|
229
|
+
d.get("type", "—"),
|
|
230
|
+
d.get("ip_address", "—"),
|
|
231
|
+
fmt_status(d.get("status", "—")),
|
|
232
|
+
"[green]✓ Issued[/green]" if d.get("public_key") else "[dim]None[/dim]",
|
|
233
|
+
fmt_time(d.get("created_at", ""))
|
|
234
|
+
)
|
|
235
|
+
console.print(t)
|
|
236
|
+
else:
|
|
237
|
+
print(f"\nDevices ({len(devices)}):")
|
|
238
|
+
for d in devices:
|
|
239
|
+
print(f" {d.get('device_code')} | {d.get('name')} | {d.get('status')} | {d.get('ip_address')}")
|
|
240
|
+
except Exception as e:
|
|
241
|
+
error(f"Error: {e}")
|
|
242
|
+
|
|
243
|
+
@device.command("register")
|
|
244
|
+
@click.option("--name", "-n", required=True, help="Device name e.g. 'NVIDIA A100'")
|
|
245
|
+
@click.option("--ip", "-i", required=True, help="Device IP address")
|
|
246
|
+
@click.option("--type", "-t", "dtype", default="GPU",
|
|
247
|
+
type=click.Choice(["GPU","Server","TPU","FPGA"], case_sensitive=False),
|
|
248
|
+
help="Device type")
|
|
249
|
+
def device_register(name, ip, dtype):
|
|
250
|
+
"""Register a new device and issue a DevicePassport."""
|
|
251
|
+
info(f"Registering {dtype}: {name} ({ip})...")
|
|
252
|
+
try:
|
|
253
|
+
r = requests.post(f"{get_api_url()}/api/devices/register",
|
|
254
|
+
json={"name": name, "type": dtype, "ip_address": ip},
|
|
255
|
+
headers={"Content-Type": "application/json"},
|
|
256
|
+
timeout=10)
|
|
257
|
+
data = r.json()
|
|
258
|
+
if r.ok:
|
|
259
|
+
success(f"Device registered successfully!")
|
|
260
|
+
if RICH:
|
|
261
|
+
t = Table(box=box.ROUNDED, border_style="green", show_header=False)
|
|
262
|
+
t.add_column("Key", style="bold cyan", width=20)
|
|
263
|
+
t.add_column("Value", style="white")
|
|
264
|
+
t.add_row("Device Code", data.get("device_code", "—"))
|
|
265
|
+
t.add_row("Name", data.get("name", "—"))
|
|
266
|
+
t.add_row("Type", data.get("type", "—"))
|
|
267
|
+
t.add_row("IP Address", data.get("ip_address", "—"))
|
|
268
|
+
t.add_row("Status", fmt_status(data.get("status", "pending")))
|
|
269
|
+
t.add_row("Certificate", "✓ Issued" if data.get("public_key") else "Pending approval")
|
|
270
|
+
console.print(t)
|
|
271
|
+
console.print()
|
|
272
|
+
warn("Device is pending admin approval. Run: computeid device approve " + data.get("device_code",""))
|
|
273
|
+
else:
|
|
274
|
+
print(f" Code: {data.get('device_code')}")
|
|
275
|
+
print(f" Status: pending — awaiting admin approval")
|
|
276
|
+
else:
|
|
277
|
+
error(f"Registration failed: {data.get('error')}")
|
|
278
|
+
except Exception as e:
|
|
279
|
+
error(f"Error: {e}")
|
|
280
|
+
|
|
281
|
+
@device.command("approve")
|
|
282
|
+
@click.argument("device_id")
|
|
283
|
+
def device_approve(device_id):
|
|
284
|
+
"""Approve a pending device."""
|
|
285
|
+
info(f"Approving device {device_id}...")
|
|
286
|
+
try:
|
|
287
|
+
r = requests.patch(f"{get_api_url()}/api/devices/{device_id}/approve",
|
|
288
|
+
headers=auth_headers(), timeout=10)
|
|
289
|
+
if r.ok:
|
|
290
|
+
success(f"Device {device_id} approved and activated!")
|
|
291
|
+
else:
|
|
292
|
+
error(f"Failed: {r.json().get('error')}")
|
|
293
|
+
except Exception as e:
|
|
294
|
+
error(f"Error: {e}")
|
|
295
|
+
|
|
296
|
+
@device.command("revoke")
|
|
297
|
+
@click.argument("device_id")
|
|
298
|
+
@click.option("--force", is_flag=True, help="Skip confirmation")
|
|
299
|
+
def device_revoke(device_id, force):
|
|
300
|
+
"""Revoke a device certificate immediately."""
|
|
301
|
+
if not force:
|
|
302
|
+
click.confirm(f"Revoke device {device_id}? This removes all access immediately.", abort=True)
|
|
303
|
+
info(f"Revoking device {device_id}...")
|
|
304
|
+
try:
|
|
305
|
+
r = requests.patch(f"{get_api_url()}/api/devices/{device_id}/revoke",
|
|
306
|
+
headers=auth_headers(), timeout=10)
|
|
307
|
+
if r.ok:
|
|
308
|
+
success(f"Device {device_id} revoked. Access removed immediately.")
|
|
309
|
+
else:
|
|
310
|
+
error(f"Failed: {r.json().get('error')}")
|
|
311
|
+
except Exception as e:
|
|
312
|
+
error(f"Error: {e}")
|
|
313
|
+
|
|
314
|
+
@device.command("authenticate")
|
|
315
|
+
@click.argument("device_code")
|
|
316
|
+
def device_authenticate(device_code):
|
|
317
|
+
"""Authenticate a device and get a JWT token."""
|
|
318
|
+
info(f"Authenticating device {device_code}...")
|
|
319
|
+
try:
|
|
320
|
+
r = requests.post(f"{get_api_url()}/api/devices/authenticate",
|
|
321
|
+
json={"device_code": device_code},
|
|
322
|
+
headers={"Content-Type": "application/json"},
|
|
323
|
+
timeout=10)
|
|
324
|
+
data = r.json()
|
|
325
|
+
if r.ok:
|
|
326
|
+
success("Device authenticated!")
|
|
327
|
+
token = data.get("access_token", "")
|
|
328
|
+
if RICH:
|
|
329
|
+
console.print(f"\n[bold cyan]Access Token:[/bold cyan]")
|
|
330
|
+
console.print(f"[dim]{token[:60]}...[/dim]")
|
|
331
|
+
console.print(f"\n[dim]Valid for 1 hour. Use in Authorization: Bearer header.[/dim]")
|
|
332
|
+
else:
|
|
333
|
+
print(f"Token: {token[:60]}...")
|
|
334
|
+
else:
|
|
335
|
+
error(f"Authentication failed: {data.get('error')}")
|
|
336
|
+
except Exception as e:
|
|
337
|
+
error(f"Error: {e}")
|
|
338
|
+
|
|
339
|
+
# ── AGENT COMMANDS ────────────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
@cli.group()
|
|
342
|
+
def agent():
|
|
343
|
+
"""Manage AgentPassports for AI agents."""
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
@agent.command("issue")
|
|
347
|
+
@click.option("--name", "-n", required=True, help="Agent name e.g. 'ResearchAgent'")
|
|
348
|
+
@click.option("--org", "-o", default="My Organisation", help="Organisation name")
|
|
349
|
+
@click.option("--email", "-e", default="admin@compute-id.com", help="Owner email")
|
|
350
|
+
@click.option("--trust", "-t", default="standard",
|
|
351
|
+
type=click.Choice(["restricted","standard","elevated","autonomous"], case_sensitive=False),
|
|
352
|
+
help="Trust level")
|
|
353
|
+
@click.option("--model", "-m", default="unknown", help="AI model e.g. claude-sonnet-4-5")
|
|
354
|
+
def agent_issue(name, org, email, trust, model):
|
|
355
|
+
"""Issue a new AgentPassport for an AI agent."""
|
|
356
|
+
try:
|
|
357
|
+
from computeid import issue_agent_passport
|
|
358
|
+
info(f"Issuing AgentPassport for {name}...")
|
|
359
|
+
passport = issue_agent_passport(
|
|
360
|
+
agent_name=name,
|
|
361
|
+
owner_org=org,
|
|
362
|
+
owner_email=email,
|
|
363
|
+
trust_level=trust,
|
|
364
|
+
model=model
|
|
365
|
+
)
|
|
366
|
+
success(f"AgentPassport issued successfully!")
|
|
367
|
+
if RICH:
|
|
368
|
+
t = Table(box=box.ROUNDED, border_style="cyan", show_header=False)
|
|
369
|
+
t.add_column("Key", style="bold cyan", width=20)
|
|
370
|
+
t.add_column("Value", style="white")
|
|
371
|
+
t.add_row("Agent ID", passport.agent_id[:16] + "...")
|
|
372
|
+
t.add_row("Name", passport.agent_name)
|
|
373
|
+
t.add_row("Organisation", passport.owner_org)
|
|
374
|
+
t.add_row("Trust Level", trust)
|
|
375
|
+
t.add_row("Model", model)
|
|
376
|
+
t.add_row("Status", fmt_status(passport.status))
|
|
377
|
+
t.add_row("Fingerprint", passport._fingerprint)
|
|
378
|
+
t.add_row("Issued At", fmt_time(passport.issued_at))
|
|
379
|
+
t.add_row("Expires At", fmt_time(passport.expires_at))
|
|
380
|
+
console.print(t)
|
|
381
|
+
console.print()
|
|
382
|
+
info("Export passport with: computeid agent export --id " + passport.agent_id[:8])
|
|
383
|
+
else:
|
|
384
|
+
print(f" ID: {passport.agent_id[:16]}...")
|
|
385
|
+
print(f" Trust: {trust}")
|
|
386
|
+
print(f" Status: {passport.status}")
|
|
387
|
+
except ImportError:
|
|
388
|
+
error("computeid-sdk not installed. Run: pip install computeid-sdk")
|
|
389
|
+
except Exception as e:
|
|
390
|
+
error(f"Error: {e}")
|
|
391
|
+
|
|
392
|
+
# ── LOGS ──────────────────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
@cli.command()
|
|
395
|
+
@click.option("--limit", "-l", default=20, help="Number of log entries to show")
|
|
396
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
397
|
+
def logs(limit, as_json):
|
|
398
|
+
"""View audit logs."""
|
|
399
|
+
try:
|
|
400
|
+
r = requests.get(f"{get_api_url()}/api/logs", headers=auth_headers(), timeout=10)
|
|
401
|
+
if not r.ok:
|
|
402
|
+
error(f"Failed: {r.json().get('error')}")
|
|
403
|
+
return
|
|
404
|
+
entries = r.json()[:limit]
|
|
405
|
+
if as_json:
|
|
406
|
+
click.echo(json.dumps(entries, indent=2))
|
|
407
|
+
return
|
|
408
|
+
if not entries:
|
|
409
|
+
warn("No audit logs found")
|
|
410
|
+
return
|
|
411
|
+
if RICH:
|
|
412
|
+
t = Table(title=f"[bold]Audit Logs (last {len(entries)})[/bold]",
|
|
413
|
+
box=box.ROUNDED, border_style="cyan", show_lines=True)
|
|
414
|
+
t.add_column("Time", style="dim", width=16)
|
|
415
|
+
t.add_column("Device ID", style="cyan", width=14)
|
|
416
|
+
t.add_column("Action", style="white", width=22)
|
|
417
|
+
t.add_column("Status", width=12)
|
|
418
|
+
t.add_column("IP", style="dim", width=16)
|
|
419
|
+
for e in entries:
|
|
420
|
+
action = e.get("action","").replace("_"," ").title()
|
|
421
|
+
s = e.get("status","")
|
|
422
|
+
sc = "[green]success[/green]" if s=="success" else "[red]denied[/red]" if s=="denied" else f"[yellow]{s}[/yellow]"
|
|
423
|
+
t.add_row(
|
|
424
|
+
fmt_time(e.get("created_at","")),
|
|
425
|
+
(e.get("device_id","") or "")[:10]+"...",
|
|
426
|
+
action,
|
|
427
|
+
sc,
|
|
428
|
+
e.get("ip_address","—") or "—"
|
|
429
|
+
)
|
|
430
|
+
console.print(t)
|
|
431
|
+
else:
|
|
432
|
+
print(f"\nAudit Logs ({len(entries)}):")
|
|
433
|
+
for e in entries:
|
|
434
|
+
print(f" {fmt_time(e.get('created_at',''))} | {e.get('action')} | {e.get('status')}")
|
|
435
|
+
except Exception as e:
|
|
436
|
+
error(f"Error: {e}")
|
|
437
|
+
|
|
438
|
+
# ── CONFIG ────────────────────────────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
@cli.group()
|
|
441
|
+
def config():
|
|
442
|
+
"""Manage CLI configuration."""
|
|
443
|
+
pass
|
|
444
|
+
|
|
445
|
+
@config.command("show")
|
|
446
|
+
def config_show():
|
|
447
|
+
"""Show current configuration."""
|
|
448
|
+
cfg = load_config()
|
|
449
|
+
if RICH:
|
|
450
|
+
t = Table(box=box.ROUNDED, border_style="cyan", show_header=False)
|
|
451
|
+
t.add_column("Key", style="bold cyan", width=20)
|
|
452
|
+
t.add_column("Value", style="white")
|
|
453
|
+
t.add_row("API URL", cfg.get("api_url", API_URL))
|
|
454
|
+
t.add_row("Logged In", "Yes ✓" if cfg.get("token") else "No")
|
|
455
|
+
t.add_row("Config File", str(CONFIG_FILE))
|
|
456
|
+
console.print(t)
|
|
457
|
+
else:
|
|
458
|
+
print(f"API URL: {cfg.get('api_url', API_URL)}")
|
|
459
|
+
print(f"Logged in: {'Yes' if cfg.get('token') else 'No'}")
|
|
460
|
+
|
|
461
|
+
@config.command("set-url")
|
|
462
|
+
@click.argument("url")
|
|
463
|
+
def config_set_url(url):
|
|
464
|
+
"""Set a custom API URL (for self-hosted ComputeID)."""
|
|
465
|
+
cfg = load_config()
|
|
466
|
+
cfg["api_url"] = url
|
|
467
|
+
save_config(cfg)
|
|
468
|
+
success(f"API URL set to {url}")
|
|
469
|
+
|
|
470
|
+
# ── QUICK START ───────────────────────────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
@cli.command()
|
|
473
|
+
def quickstart():
|
|
474
|
+
"""Interactive quickstart guide."""
|
|
475
|
+
print_header()
|
|
476
|
+
if RICH:
|
|
477
|
+
steps = [
|
|
478
|
+
("1", "Login", "computeid login"),
|
|
479
|
+
("2", "Check status", "computeid status"),
|
|
480
|
+
("3", "Register a GPU", "computeid device register --name 'NVIDIA A100' --ip 192.168.1.10"),
|
|
481
|
+
("4", "List devices", "computeid device list"),
|
|
482
|
+
("5", "Issue agent passport", "computeid agent issue --name 'MyAgent' --trust standard"),
|
|
483
|
+
("6", "View audit logs", "computeid logs"),
|
|
484
|
+
]
|
|
485
|
+
t = Table(title="[bold]ComputeID CLI Quick Start[/bold]",
|
|
486
|
+
box=box.ROUNDED, border_style="cyan")
|
|
487
|
+
t.add_column("Step", style="bold cyan", width=6)
|
|
488
|
+
t.add_column("Action", style="white", width=22)
|
|
489
|
+
t.add_column("Command", style="bold green", width=60)
|
|
490
|
+
for step, action, cmd in steps:
|
|
491
|
+
t.add_row(step, action, cmd)
|
|
492
|
+
console.print(t)
|
|
493
|
+
console.print()
|
|
494
|
+
console.print("[dim]Docs: compute-id.com | hello@compute-id.com[/dim]")
|
|
495
|
+
else:
|
|
496
|
+
print("ComputeID CLI Quick Start:")
|
|
497
|
+
print(" 1. computeid login")
|
|
498
|
+
print(" 2. computeid status")
|
|
499
|
+
print(" 3. computeid device register --name 'NVIDIA A100' --ip 192.168.1.10")
|
|
500
|
+
print(" 4. computeid device list")
|
|
501
|
+
print(" 5. computeid agent issue --name 'MyAgent' --trust standard")
|
|
502
|
+
print(" 6. computeid logs")
|
|
503
|
+
|
|
504
|
+
if __name__ == "__main__":
|
|
505
|
+
cli()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: computeid-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CLI tool for ComputeID — cryptographic identity for AI compute infrastructure
|
|
5
|
+
Home-page: https://github.com/trustedaicompute-ops/computeid-sdk
|
|
6
|
+
Author: ComputeID
|
|
7
|
+
Author-email: hello@compute-id.com
|
|
8
|
+
Keywords: gpu identity certificates quantum-safe ai agents security cli computeid
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Security :: Cryptography
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: click>=8.0.0
|
|
18
|
+
Requires-Dist: requests>=2.28.0
|
|
19
|
+
Requires-Dist: rich>=13.0.0
|
|
20
|
+
Dynamic: author
|
|
21
|
+
Dynamic: author-email
|
|
22
|
+
Dynamic: classifier
|
|
23
|
+
Dynamic: description
|
|
24
|
+
Dynamic: description-content-type
|
|
25
|
+
Dynamic: home-page
|
|
26
|
+
Dynamic: keywords
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
30
|
+
|
|
31
|
+
# ComputeID CLI
|
|
32
|
+
|
|
33
|
+
Command line tool for ComputeID — cryptographic identity for AI compute infrastructure.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install computeid-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Login
|
|
45
|
+
computeid login
|
|
46
|
+
|
|
47
|
+
# Check status
|
|
48
|
+
computeid status
|
|
49
|
+
|
|
50
|
+
# Register a GPU
|
|
51
|
+
computeid device register --name "NVIDIA A100" --ip 192.168.1.10
|
|
52
|
+
|
|
53
|
+
# List devices
|
|
54
|
+
computeid device list
|
|
55
|
+
|
|
56
|
+
# Approve a device
|
|
57
|
+
computeid device approve GPU-001
|
|
58
|
+
|
|
59
|
+
# Revoke a device
|
|
60
|
+
computeid device revoke GPU-001
|
|
61
|
+
|
|
62
|
+
# Issue an agent passport
|
|
63
|
+
computeid agent issue --name "ResearchAgent" --trust standard
|
|
64
|
+
|
|
65
|
+
# View audit logs
|
|
66
|
+
computeid logs
|
|
67
|
+
|
|
68
|
+
# Get help
|
|
69
|
+
computeid --help
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Commands
|
|
73
|
+
|
|
74
|
+
| Command | Description |
|
|
75
|
+
|---------|-------------|
|
|
76
|
+
| `computeid status` | Check API health |
|
|
77
|
+
| `computeid login` | Login with admin password |
|
|
78
|
+
| `computeid logout` | Logout |
|
|
79
|
+
| `computeid device list` | List all devices |
|
|
80
|
+
| `computeid device register` | Register a new GPU or server |
|
|
81
|
+
| `computeid device approve` | Approve a pending device |
|
|
82
|
+
| `computeid device revoke` | Revoke a device certificate |
|
|
83
|
+
| `computeid device authenticate` | Get JWT token for a device |
|
|
84
|
+
| `computeid agent issue` | Issue an AgentPassport |
|
|
85
|
+
| `computeid logs` | View audit logs |
|
|
86
|
+
| `computeid config show` | Show configuration |
|
|
87
|
+
| `computeid quickstart` | Interactive quick start guide |
|
|
88
|
+
|
|
89
|
+
## Docs
|
|
90
|
+
|
|
91
|
+
compute-id.com | hello@compute-id.com
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
cli.py,sha256=8utzmOQ54fa4MCkJPFnmpZcWln7eGOaaxFS3O_Szjws,19932
|
|
2
|
+
computeid_cli-1.0.0.dist-info/METADATA,sha256=KSakFHv7VRPmjBha8sBjmfoY3_f94tpVJC2FLiBii4o,2368
|
|
3
|
+
computeid_cli-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
4
|
+
computeid_cli-1.0.0.dist-info/entry_points.txt,sha256=ZqdabCC0NhFM63d7E14JbYd1sJXCKG_UDerjqiFigpg,38
|
|
5
|
+
computeid_cli-1.0.0.dist-info/top_level.txt,sha256=2ImG917oaVHlm0nP9oJE-Qrgs-fq_fGWgba2H1f8fpE,4
|
|
6
|
+
computeid_cli-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cli
|