proxima-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.
- prox/__init__.py +3 -0
- prox/api.py +107 -0
- prox/auth.py +157 -0
- prox/cli.py +121 -0
- prox/commands/__init__.py +0 -0
- prox/commands/agent.py +345 -0
- prox/commands/catalog.py +242 -0
- prox/commands/governance.py +63 -0
- prox/commands/knowledge.py +141 -0
- prox/commands/model.py +69 -0
- prox/commands/ontology.py +209 -0
- prox/commands/platform.py +50 -0
- prox/commands/routine.py +84 -0
- prox/commands/secret.py +59 -0
- prox/commands/team.py +82 -0
- prox/commands/toolbox.py +136 -0
- prox/commands/workflow.py +129 -0
- prox/config.py +98 -0
- proxima_cli-1.0.0.dist-info/METADATA +115 -0
- proxima_cli-1.0.0.dist-info/RECORD +22 -0
- proxima_cli-1.0.0.dist-info/WHEEL +4 -0
- proxima_cli-1.0.0.dist-info/entry_points.txt +2 -0
prox/commands/agent.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""prox agent — manage agents on the platform."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import tarfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
import yaml
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from ..api import gateway_get, gateway_post, gateway_put, gateway_delete, APIError
|
|
15
|
+
from ..config import get_value
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(help="Manage platform agents.")
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command("list")
|
|
22
|
+
def list_agents(
|
|
23
|
+
domain: Optional[str] = typer.Option(None, "--domain", "-d", help="Filter by domain"),
|
|
24
|
+
status: Optional[str] = typer.Option(None, "--status", "-s", help="Filter by status (active/draft)"),
|
|
25
|
+
):
|
|
26
|
+
"""List all agents on the platform."""
|
|
27
|
+
try:
|
|
28
|
+
params = {}
|
|
29
|
+
if domain:
|
|
30
|
+
params["domain"] = domain
|
|
31
|
+
if status:
|
|
32
|
+
params["status"] = status
|
|
33
|
+
data = gateway_get("/build/agents", params=params)
|
|
34
|
+
except APIError as e:
|
|
35
|
+
console.print(f"[red]Error:[/red] {e.detail}")
|
|
36
|
+
raise typer.Exit(1)
|
|
37
|
+
|
|
38
|
+
agents = data.get("agents", [])
|
|
39
|
+
if not agents:
|
|
40
|
+
console.print("[dim]No agents found.[/dim]")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
table = Table(title=f"Agents ({len(agents)})")
|
|
44
|
+
table.add_column("ID", style="bold")
|
|
45
|
+
table.add_column("Name")
|
|
46
|
+
table.add_column("Domain")
|
|
47
|
+
table.add_column("Status")
|
|
48
|
+
table.add_column("Version", justify="right")
|
|
49
|
+
|
|
50
|
+
for a in agents:
|
|
51
|
+
st = a.get("status", "draft")
|
|
52
|
+
st_style = "green" if st == "active" else "yellow" if st == "draft" else "dim"
|
|
53
|
+
table.add_row(
|
|
54
|
+
a.get("id", ""),
|
|
55
|
+
a.get("name", ""),
|
|
56
|
+
a.get("domain", ""),
|
|
57
|
+
f"[{st_style}]{st}[/{st_style}]",
|
|
58
|
+
str(a.get("version", 1)),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
console.print(table)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command("get")
|
|
65
|
+
def get_agent(agent_id: str = typer.Argument(help="Agent ID")):
|
|
66
|
+
"""Get agent details."""
|
|
67
|
+
try:
|
|
68
|
+
data = gateway_get(f"/build/agents/{agent_id}")
|
|
69
|
+
except APIError as e:
|
|
70
|
+
console.print(f"[red]Error:[/red] {e.detail}")
|
|
71
|
+
raise typer.Exit(1)
|
|
72
|
+
|
|
73
|
+
agent = data.get("agent", data)
|
|
74
|
+
console.print(f"\n[bold]{agent.get('name', agent_id)}[/bold]")
|
|
75
|
+
console.print(f" ID: {agent.get('id')}")
|
|
76
|
+
console.print(f" Codename: {agent.get('codename', '—')}")
|
|
77
|
+
console.print(f" Domain: {agent.get('domain', '—')}")
|
|
78
|
+
console.print(f" Role: {agent.get('role', 'agent')}")
|
|
79
|
+
console.print(f" Status: {agent.get('status', '—')}")
|
|
80
|
+
console.print(f" Version: {agent.get('version', 1)}")
|
|
81
|
+
console.print(f" Model: {agent.get('llm', {}).get('model', '—')}")
|
|
82
|
+
if agent.get("toolboxes"):
|
|
83
|
+
console.print(f" Toolboxes: {', '.join(agent['toolboxes'])}")
|
|
84
|
+
if agent.get("knowledge_bases"):
|
|
85
|
+
console.print(f" Knowledge: {', '.join(agent['knowledge_bases'])}")
|
|
86
|
+
console.print()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command("create")
|
|
90
|
+
def create_agent(
|
|
91
|
+
name: str = typer.Option(None, "--name", "-n", help="Agent name"),
|
|
92
|
+
codename: str = typer.Option(None, "--codename", help="Agent codename"),
|
|
93
|
+
domain: str = typer.Option(None, "--domain", "-d", help="Domain"),
|
|
94
|
+
role: str = typer.Option("agent", "--role", help="Role: agent or orchestrator"),
|
|
95
|
+
from_file: Optional[Path] = typer.Option(None, "--from", help="Create from agent.yaml file"),
|
|
96
|
+
):
|
|
97
|
+
"""Create a new agent."""
|
|
98
|
+
if from_file:
|
|
99
|
+
if not from_file.exists():
|
|
100
|
+
console.print(f"[red]Error:[/red] File not found: {from_file}")
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
config = yaml.safe_load(from_file.read_text())
|
|
103
|
+
else:
|
|
104
|
+
if not name:
|
|
105
|
+
console.print("[red]Error:[/red] Provide --name or --from")
|
|
106
|
+
raise typer.Exit(1)
|
|
107
|
+
agent_id = name.lower().replace(" ", "-").replace("_", "-")
|
|
108
|
+
config = {"id": agent_id, "name": name, "codename": codename or "", "domain": domain or "", "role": role}
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
data = gateway_post("/build/agents", config)
|
|
112
|
+
except APIError as e:
|
|
113
|
+
console.print(f"[red]Error:[/red] {e.detail}")
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
|
|
116
|
+
console.print(f"[green]✓[/green] Agent created: {config.get('id', config.get('name'))}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.command("update")
|
|
120
|
+
def update_agent(
|
|
121
|
+
agent_id: str = typer.Argument(help="Agent ID"),
|
|
122
|
+
prompt: Optional[str] = typer.Option(None, "--prompt", help="System prompt"),
|
|
123
|
+
model: Optional[str] = typer.Option(None, "--model", help="Model ID"),
|
|
124
|
+
from_file: Optional[Path] = typer.Option(None, "--from", help="Update from agent.yaml"),
|
|
125
|
+
):
|
|
126
|
+
"""Update an agent's configuration."""
|
|
127
|
+
if from_file:
|
|
128
|
+
if not from_file.exists():
|
|
129
|
+
console.print(f"[red]Error:[/red] File not found: {from_file}")
|
|
130
|
+
raise typer.Exit(1)
|
|
131
|
+
config = yaml.safe_load(from_file.read_text())
|
|
132
|
+
else:
|
|
133
|
+
config = {}
|
|
134
|
+
if prompt:
|
|
135
|
+
config["system_prompt"] = prompt
|
|
136
|
+
if model:
|
|
137
|
+
config["llm"] = {"model": model}
|
|
138
|
+
if not config:
|
|
139
|
+
console.print("[red]Error:[/red] Provide --prompt, --model, or --from")
|
|
140
|
+
raise typer.Exit(1)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
gateway_put(f"/build/agents/{agent_id}", config)
|
|
144
|
+
except APIError as e:
|
|
145
|
+
console.print(f"[red]Error:[/red] {e.detail}")
|
|
146
|
+
raise typer.Exit(1)
|
|
147
|
+
|
|
148
|
+
console.print(f"[green]✓[/green] Agent updated: {agent_id}")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@app.command("publish")
|
|
152
|
+
def publish_agent(agent_id: str = typer.Argument(help="Agent ID to publish")):
|
|
153
|
+
"""Publish an agent (draft → active)."""
|
|
154
|
+
try:
|
|
155
|
+
gateway_post(f"/build/agents/{agent_id}/publish")
|
|
156
|
+
except APIError as e:
|
|
157
|
+
console.print(f"[red]Error:[/red] {e.detail}")
|
|
158
|
+
raise typer.Exit(1)
|
|
159
|
+
|
|
160
|
+
console.print(f"[green]✓[/green] Published: {agent_id}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@app.command("delete")
|
|
164
|
+
def delete_agent(
|
|
165
|
+
agent_id: str = typer.Argument(help="Agent ID to delete"),
|
|
166
|
+
confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
167
|
+
):
|
|
168
|
+
"""Delete an agent."""
|
|
169
|
+
if not confirm:
|
|
170
|
+
typer.confirm(f"Delete agent '{agent_id}'? This cannot be undone", abort=True)
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
gateway_delete(f"/build/agents/{agent_id}")
|
|
174
|
+
except APIError as e:
|
|
175
|
+
console.print(f"[red]Error:[/red] {e.detail}")
|
|
176
|
+
raise typer.Exit(1)
|
|
177
|
+
|
|
178
|
+
console.print(f"[green]✓[/green] Deleted: {agent_id}")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@app.command("deploy")
|
|
182
|
+
def deploy_agent(
|
|
183
|
+
from_path: Path = typer.Option(..., "--from", help="Path to agent package directory"),
|
|
184
|
+
):
|
|
185
|
+
"""Full agent deploy: build toolbox → register → configure → publish.
|
|
186
|
+
|
|
187
|
+
Expects directory structure:
|
|
188
|
+
agent.yaml, toolbox/, workspace.json, knowledge.yaml, ontology.yaml, data/synthetic/
|
|
189
|
+
"""
|
|
190
|
+
if not from_path.exists():
|
|
191
|
+
console.print(f"[red]Error:[/red] Path not found: {from_path}")
|
|
192
|
+
raise typer.Exit(1)
|
|
193
|
+
|
|
194
|
+
agent_yaml = from_path / "agent.yaml"
|
|
195
|
+
if not agent_yaml.exists():
|
|
196
|
+
console.print(f"[red]Error:[/red] No agent.yaml found in {from_path}")
|
|
197
|
+
raise typer.Exit(1)
|
|
198
|
+
|
|
199
|
+
config = yaml.safe_load(agent_yaml.read_text())
|
|
200
|
+
agent_id = config["id"]
|
|
201
|
+
console.print(f"\n[bold]Deploying:[/bold] {config.get('name', agent_id)} ({agent_id})\n")
|
|
202
|
+
|
|
203
|
+
# Step 1: Build and push toolbox (if exists)
|
|
204
|
+
toolbox_dir = from_path / "toolbox"
|
|
205
|
+
if toolbox_dir.exists() and (toolbox_dir / "Dockerfile").exists():
|
|
206
|
+
_deploy_toolbox(agent_id, toolbox_dir, config)
|
|
207
|
+
|
|
208
|
+
# Step 2: Upload knowledge data (if exists)
|
|
209
|
+
knowledge_yaml = from_path / "knowledge.yaml"
|
|
210
|
+
if knowledge_yaml.exists():
|
|
211
|
+
_deploy_knowledge(agent_id, knowledge_yaml, from_path)
|
|
212
|
+
|
|
213
|
+
# Step 2b: Deploy ontology (if exists)
|
|
214
|
+
ontology_yaml = from_path / "ontology.yaml"
|
|
215
|
+
if ontology_yaml.exists():
|
|
216
|
+
console.print(" [bold]2b.[/bold] Deploying ontology...")
|
|
217
|
+
try:
|
|
218
|
+
ont_data = yaml.safe_load(ontology_yaml.read_text())
|
|
219
|
+
ont_id = ont_data.get("id", f"{agent_id}-ontology")
|
|
220
|
+
gateway_post("/build/ontology", ont_data)
|
|
221
|
+
# Connect ontology to agent config
|
|
222
|
+
config["ontology"] = {"id": ont_id}
|
|
223
|
+
console.print(f" [green]\u2713[/green] Ontology deployed: {ont_id}")
|
|
224
|
+
except APIError:
|
|
225
|
+
# May already exist — try update
|
|
226
|
+
try:
|
|
227
|
+
gateway_put(f"/build/ontology/{ont_data.get('id', '')}", ont_data)
|
|
228
|
+
config["ontology"] = {"id": ont_data.get('id', '')}
|
|
229
|
+
console.print(f" [green]\u2713[/green] Ontology updated: {ont_data.get('id', '')}")
|
|
230
|
+
except APIError as e:
|
|
231
|
+
console.print(f" [yellow]\u26a0[/yellow] Ontology: {e.detail}")
|
|
232
|
+
|
|
233
|
+
# Step 3: Create/update agent in registry
|
|
234
|
+
console.print(" [bold]3.[/bold] Registering agent...")
|
|
235
|
+
try:
|
|
236
|
+
gateway_put(f"/build/agents/{agent_id}", config)
|
|
237
|
+
console.print(" [green]✓[/green] Agent registered")
|
|
238
|
+
except APIError as e:
|
|
239
|
+
console.print(f" [red]✗[/red] {e.detail}")
|
|
240
|
+
raise typer.Exit(1)
|
|
241
|
+
|
|
242
|
+
# Step 4: Push workspace (if exists)
|
|
243
|
+
workspace_json = from_path / "workspace.json"
|
|
244
|
+
if workspace_json.exists():
|
|
245
|
+
console.print(" [bold]4.[/bold] Pushing workspace...")
|
|
246
|
+
try:
|
|
247
|
+
ws = json.loads(workspace_json.read_text())
|
|
248
|
+
gateway_put(f"/workspaces/{agent_id}", ws)
|
|
249
|
+
console.print(" [green]✓[/green] Workspace pushed")
|
|
250
|
+
except APIError as e:
|
|
251
|
+
console.print(f" [yellow]⚠[/yellow] Workspace push failed: {e.detail}")
|
|
252
|
+
|
|
253
|
+
# Step 5: Publish
|
|
254
|
+
console.print(" [bold]5.[/bold] Publishing...")
|
|
255
|
+
try:
|
|
256
|
+
gateway_post(f"/build/agents/{agent_id}/publish")
|
|
257
|
+
console.print(" [green]✓[/green] Published (status: active)")
|
|
258
|
+
except APIError as e:
|
|
259
|
+
console.print(f" [yellow]⚠[/yellow] Publish failed: {e.detail}")
|
|
260
|
+
|
|
261
|
+
# Step 6: Health check
|
|
262
|
+
console.print(" [bold]6.[/bold] Health check...")
|
|
263
|
+
try:
|
|
264
|
+
health = gateway_get(f"/agents/{agent_id}/health")
|
|
265
|
+
console.print(f" [green]✓[/green] Healthy")
|
|
266
|
+
except APIError:
|
|
267
|
+
console.print(" [yellow]⚠[/yellow] Health check unavailable")
|
|
268
|
+
|
|
269
|
+
console.print(f"\n[green]✓ Deploy complete:[/green] {agent_id}\n")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _deploy_toolbox(agent_id: str, toolbox_dir: Path, config: dict):
|
|
273
|
+
"""Build Docker image, push to registry, register toolbox."""
|
|
274
|
+
registry = get_value("registry")
|
|
275
|
+
if not registry:
|
|
276
|
+
console.print(" [bold]1.[/bold] Toolbox — [yellow]skipped[/yellow] (no registry configured)")
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
image = f"{registry}/{agent_id}-toolbox:latest"
|
|
280
|
+
console.print(f" [bold]1.[/bold] Building toolbox → {image}")
|
|
281
|
+
|
|
282
|
+
# Build
|
|
283
|
+
result = subprocess.run(
|
|
284
|
+
["docker", "build", "--platform", "linux/amd64", "-t", image, "."],
|
|
285
|
+
cwd=toolbox_dir, capture_output=True, text=True,
|
|
286
|
+
)
|
|
287
|
+
if result.returncode != 0:
|
|
288
|
+
console.print(f" [red]✗[/red] Docker build failed:\n{result.stderr[:300]}")
|
|
289
|
+
raise typer.Exit(1)
|
|
290
|
+
console.print(" [green]✓[/green] Image built")
|
|
291
|
+
|
|
292
|
+
# Push
|
|
293
|
+
result = subprocess.run(
|
|
294
|
+
["docker", "push", image],
|
|
295
|
+
capture_output=True, text=True,
|
|
296
|
+
)
|
|
297
|
+
if result.returncode != 0:
|
|
298
|
+
console.print(f" [red]✗[/red] Docker push failed:\n{result.stderr[:300]}")
|
|
299
|
+
raise typer.Exit(1)
|
|
300
|
+
console.print(" [green]✓[/green] Image pushed")
|
|
301
|
+
|
|
302
|
+
# Register toolbox with gateway
|
|
303
|
+
toolbox_id = config.get("toolboxes", [None])[0] or f"{agent_id}-toolbox"
|
|
304
|
+
console.print(f" [bold]2.[/bold] Registering toolbox: {toolbox_id}")
|
|
305
|
+
try:
|
|
306
|
+
gateway_post("/build/toolboxes", {
|
|
307
|
+
"id": toolbox_id,
|
|
308
|
+
"name": f"{config.get('name', agent_id)} Toolbox",
|
|
309
|
+
"endpoint": f"http://{agent_id}:8001",
|
|
310
|
+
"type": "openapi",
|
|
311
|
+
})
|
|
312
|
+
# Discover tools
|
|
313
|
+
gateway_post("/build/toolboxes/discover", {"toolbox_id": toolbox_id})
|
|
314
|
+
console.print(" [green]✓[/green] Toolbox registered + tools discovered")
|
|
315
|
+
except APIError as e:
|
|
316
|
+
console.print(f" [yellow]⚠[/yellow] Toolbox registration: {e.detail}")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _deploy_knowledge(agent_id: str, knowledge_yaml: Path, base_path: Path):
|
|
320
|
+
"""Create knowledge sources and bases from knowledge.yaml."""
|
|
321
|
+
console.print(" [bold]2b.[/bold] Setting up knowledge...")
|
|
322
|
+
kconfig = yaml.safe_load(knowledge_yaml.read_text())
|
|
323
|
+
|
|
324
|
+
# Create sources
|
|
325
|
+
for source in kconfig.get("sources", []):
|
|
326
|
+
try:
|
|
327
|
+
gateway_post("/build/knowledge/sources", source)
|
|
328
|
+
console.print(f" [green]✓[/green] Source: {source.get('id', '?')}")
|
|
329
|
+
except APIError:
|
|
330
|
+
pass # May already exist
|
|
331
|
+
|
|
332
|
+
# Create bases
|
|
333
|
+
for base in kconfig.get("bases", []):
|
|
334
|
+
try:
|
|
335
|
+
gateway_post("/build/knowledge/bases", base)
|
|
336
|
+
console.print(f" [green]✓[/green] Base: {base.get('id', '?')}")
|
|
337
|
+
except APIError:
|
|
338
|
+
pass # May already exist
|
|
339
|
+
|
|
340
|
+
# Upload synthetic data if present
|
|
341
|
+
data_dir = base_path / "data" / "synthetic"
|
|
342
|
+
if data_dir.exists():
|
|
343
|
+
parquets = list(data_dir.glob("*.parquet"))
|
|
344
|
+
if parquets:
|
|
345
|
+
console.print(f" [dim]({len(parquets)} parquet files — upload via knowledge source config)[/dim]")
|
prox/commands/catalog.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""prox catalog — browse, pull, and push agent packages."""
|
|
2
|
+
|
|
3
|
+
import tarfile
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from ..api import catalog_get, catalog_post, catalog_download, APIError
|
|
13
|
+
from ..config import PACKAGES_DIR, get_license_key, get_master_key
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="Browse and manage the Proxima Agent Catalog.")
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("list")
|
|
20
|
+
def list_agents(
|
|
21
|
+
domain: Optional[str] = typer.Option(None, "--domain", "-d", help="Filter by domain"),
|
|
22
|
+
format: str = typer.Option("table", "--format", "-f", help="Output format: table, json"),
|
|
23
|
+
):
|
|
24
|
+
"""List all agents in the catalog."""
|
|
25
|
+
try:
|
|
26
|
+
params = {}
|
|
27
|
+
if domain:
|
|
28
|
+
params["domain"] = domain
|
|
29
|
+
data = catalog_get("/catalog/agents", params=params)
|
|
30
|
+
except APIError as e:
|
|
31
|
+
console.print(f"[red]Error:[/red] {e.detail}")
|
|
32
|
+
raise typer.Exit(1)
|
|
33
|
+
|
|
34
|
+
agents = data.get("agents", [])
|
|
35
|
+
if not agents:
|
|
36
|
+
console.print("[dim]No agents found.[/dim]")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
if format == "json":
|
|
40
|
+
import json
|
|
41
|
+
console.print(json.dumps(agents, indent=2))
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
# Check license to mark available agents
|
|
45
|
+
licensed_ids = set()
|
|
46
|
+
try:
|
|
47
|
+
lic = catalog_get("/catalog/license")
|
|
48
|
+
licensed_ids = set(lic.get("agents", []))
|
|
49
|
+
except APIError:
|
|
50
|
+
pass # No license configured — show all without marks
|
|
51
|
+
|
|
52
|
+
table = Table(title=f"Proxima Catalog ({len(agents)} agents)")
|
|
53
|
+
table.add_column("Agent", style="bold")
|
|
54
|
+
table.add_column("Codename", style="cyan")
|
|
55
|
+
table.add_column("Domain")
|
|
56
|
+
table.add_column("Tools", justify="right")
|
|
57
|
+
table.add_column("Version")
|
|
58
|
+
table.add_column("Access", justify="center")
|
|
59
|
+
|
|
60
|
+
for agent in agents:
|
|
61
|
+
aid = agent["id"]
|
|
62
|
+
access = "✓" if aid in licensed_ids or get_master_key() else "○"
|
|
63
|
+
access_style = "green" if access == "✓" else "dim"
|
|
64
|
+
table.add_row(
|
|
65
|
+
agent.get("name", aid),
|
|
66
|
+
agent.get("codename", ""),
|
|
67
|
+
agent.get("domain", ""),
|
|
68
|
+
str(agent.get("tools", 0)),
|
|
69
|
+
agent.get("latest_version", "—"),
|
|
70
|
+
f"[{access_style}]{access}[/{access_style}]",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
console.print(table)
|
|
74
|
+
if licensed_ids:
|
|
75
|
+
console.print(f"\n[green]✓[/green] = licensed [dim]○[/dim] = contact Proxima")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.command("show")
|
|
79
|
+
def show_agent(agent_id: str = typer.Argument(help="Agent ID")):
|
|
80
|
+
"""Show detailed information about a catalog agent."""
|
|
81
|
+
try:
|
|
82
|
+
data = catalog_get(f"/catalog/agents/{agent_id}")
|
|
83
|
+
except APIError as e:
|
|
84
|
+
console.print(f"[red]Error:[/red] {e.detail}")
|
|
85
|
+
raise typer.Exit(1)
|
|
86
|
+
|
|
87
|
+
console.print(f"\n[bold]{data.get('name', agent_id)}[/bold] ({data.get('codename', '')})")
|
|
88
|
+
console.print(f"[dim]{data.get('description', '')}[/dim]\n")
|
|
89
|
+
console.print(f" Domain: {data.get('domain', '—')}")
|
|
90
|
+
console.print(f" Tools: {data.get('tools', 0)}")
|
|
91
|
+
console.print(f" Version: {data.get('latest_version', '—')}")
|
|
92
|
+
console.print(f" Updated: {data.get('updated_at', '—')}")
|
|
93
|
+
|
|
94
|
+
if data.get("tool_names"):
|
|
95
|
+
console.print(f"\n [bold]Tools:[/bold]")
|
|
96
|
+
for t in data["tool_names"]:
|
|
97
|
+
console.print(f" • {t}")
|
|
98
|
+
|
|
99
|
+
if data.get("industries"):
|
|
100
|
+
console.print(f"\n [bold]Industries:[/bold] {', '.join(data['industries'])}")
|
|
101
|
+
if data.get("functions"):
|
|
102
|
+
console.print(f" [bold]Functions:[/bold] {', '.join(data['functions'])}")
|
|
103
|
+
console.print()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.command("pull")
|
|
107
|
+
def pull_agent(
|
|
108
|
+
agent_id: str = typer.Argument(help="Agent ID to pull"),
|
|
109
|
+
version: Optional[str] = typer.Option(None, "--version", "-v", help="Specific version (default: latest)"),
|
|
110
|
+
):
|
|
111
|
+
"""Pull an agent package from the catalog."""
|
|
112
|
+
if not get_license_key() and not get_master_key():
|
|
113
|
+
console.print("[red]Error:[/red] No license key configured. Run: prox login --license-key <key>")
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
|
|
116
|
+
console.print(f"[bold]Pulling[/bold] {agent_id}" + (f"@{version}" if version else " (latest)") + "...")
|
|
117
|
+
|
|
118
|
+
# Get download URL / validate access
|
|
119
|
+
try:
|
|
120
|
+
params = {"version": version} if version else {}
|
|
121
|
+
meta = catalog_get(f"/catalog/agents/{agent_id}", params=params)
|
|
122
|
+
except APIError as e:
|
|
123
|
+
if e.status == 403:
|
|
124
|
+
console.print(f"[red]Access denied:[/red] '{agent_id}' is not in your license. Contact Proxima to upgrade.")
|
|
125
|
+
else:
|
|
126
|
+
console.print(f"[red]Error:[/red] {e.detail}")
|
|
127
|
+
raise typer.Exit(1)
|
|
128
|
+
|
|
129
|
+
actual_version = version or meta.get("latest_version", "latest")
|
|
130
|
+
dest_dir = PACKAGES_DIR / agent_id
|
|
131
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
tarball = dest_dir / f"{agent_id}-{actual_version}.tar.gz"
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
pull_path = f"/catalog/agents/{agent_id}/pull"
|
|
136
|
+
if version:
|
|
137
|
+
pull_path += f"?version={version}"
|
|
138
|
+
catalog_download(pull_path, str(tarball))
|
|
139
|
+
except APIError as e:
|
|
140
|
+
console.print(f"[red]Download failed:[/red] {e.detail}")
|
|
141
|
+
raise typer.Exit(1)
|
|
142
|
+
|
|
143
|
+
# Extract
|
|
144
|
+
extract_dir = dest_dir / actual_version
|
|
145
|
+
extract_dir.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
with tarfile.open(tarball, "r:gz") as tar:
|
|
147
|
+
tar.extractall(path=extract_dir)
|
|
148
|
+
|
|
149
|
+
console.print(f"[green]✓[/green] Pulled {agent_id}@{actual_version} → {extract_dir}")
|
|
150
|
+
console.print(f"\n Deploy with: [bold]prox agent deploy --from {extract_dir}[/bold]")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@app.command("push")
|
|
154
|
+
def push_agent(
|
|
155
|
+
from_path: Path = typer.Option(..., "--from", help="Path to agent directory"),
|
|
156
|
+
version: str = typer.Option(..., "--version", "-v", help="Version to publish"),
|
|
157
|
+
):
|
|
158
|
+
"""Push an agent package to the catalog (Proxima team only)."""
|
|
159
|
+
if not get_master_key():
|
|
160
|
+
console.print("[red]Error:[/red] Master key required. Run: prox login --master-key <key>")
|
|
161
|
+
raise typer.Exit(1)
|
|
162
|
+
|
|
163
|
+
if not from_path.exists():
|
|
164
|
+
console.print(f"[red]Error:[/red] Path not found: {from_path}")
|
|
165
|
+
raise typer.Exit(1)
|
|
166
|
+
|
|
167
|
+
# Read manifest or agent.yaml for the agent ID
|
|
168
|
+
manifest = from_path / "manifest.json"
|
|
169
|
+
agent_yaml = from_path / "agent.yaml"
|
|
170
|
+
if manifest.exists():
|
|
171
|
+
import json
|
|
172
|
+
meta = json.loads(manifest.read_text())
|
|
173
|
+
agent_id = meta["id"]
|
|
174
|
+
elif agent_yaml.exists():
|
|
175
|
+
import yaml
|
|
176
|
+
meta = yaml.safe_load(agent_yaml.read_text())
|
|
177
|
+
agent_id = meta["id"]
|
|
178
|
+
else:
|
|
179
|
+
console.print("[red]Error:[/red] No manifest.json or agent.yaml found in source directory.")
|
|
180
|
+
raise typer.Exit(1)
|
|
181
|
+
|
|
182
|
+
console.print(f"[bold]Packaging[/bold] {agent_id}@{version}...")
|
|
183
|
+
|
|
184
|
+
# Create tarball
|
|
185
|
+
tarball_path = Path(tempfile.mktemp(suffix=".tar.gz"))
|
|
186
|
+
with tarfile.open(tarball_path, "w:gz") as tar:
|
|
187
|
+
tar.add(from_path, arcname=".")
|
|
188
|
+
|
|
189
|
+
size_mb = tarball_path.stat().st_size / (1024 * 1024)
|
|
190
|
+
console.print(f" Package size: {size_mb:.1f} MB")
|
|
191
|
+
console.print(f"[bold]Pushing[/bold] to catalog...")
|
|
192
|
+
|
|
193
|
+
# Upload
|
|
194
|
+
try:
|
|
195
|
+
import httpx
|
|
196
|
+
from ..config import get_value
|
|
197
|
+
catalog_url = (get_value("catalog") or "https://catalog.proximaintel.com").rstrip("/")
|
|
198
|
+
with open(tarball_path, "rb") as f:
|
|
199
|
+
r = httpx.post(
|
|
200
|
+
f"{catalog_url}/catalog/agents/{agent_id}/push?version={version}",
|
|
201
|
+
headers={"X-Master-Key": get_master_key()},
|
|
202
|
+
content=f.read(),
|
|
203
|
+
timeout=300,
|
|
204
|
+
)
|
|
205
|
+
if r.status_code >= 400:
|
|
206
|
+
console.print(f"[red]Push failed:[/red] {r.text[:300]}")
|
|
207
|
+
raise typer.Exit(1)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
console.print(f"[red]Push failed:[/red] {e}")
|
|
210
|
+
raise typer.Exit(1)
|
|
211
|
+
finally:
|
|
212
|
+
tarball_path.unlink(missing_ok=True)
|
|
213
|
+
|
|
214
|
+
console.print(f"[green]✓[/green] Pushed {agent_id}@{version} to catalog")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@app.command("license")
|
|
218
|
+
def check_license():
|
|
219
|
+
"""Check current license entitlements."""
|
|
220
|
+
if not get_license_key() and not get_master_key():
|
|
221
|
+
console.print("[yellow]No license key configured.[/yellow]")
|
|
222
|
+
console.print(" Run: prox login --license-key <key>")
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
if get_master_key():
|
|
226
|
+
console.print("[bold green]Master key active[/bold green] — full catalog access")
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
data = catalog_get("/catalog/license")
|
|
231
|
+
except APIError as e:
|
|
232
|
+
console.print(f"[red]Error:[/red] {e.detail}")
|
|
233
|
+
raise typer.Exit(1)
|
|
234
|
+
|
|
235
|
+
console.print(f"\n[bold]License:[/bold] {data.get('client_id', '—')}")
|
|
236
|
+
console.print(f" Tier: {data.get('tier', '—')}")
|
|
237
|
+
console.print(f" Expires: {data.get('expires', '—')}")
|
|
238
|
+
console.print(f" Valid: {'[green]Yes[/green]' if data.get('valid') else '[red]No[/red]'}")
|
|
239
|
+
console.print(f"\n [bold]Licensed agents ({len(data.get('agents', []))}):[/bold]")
|
|
240
|
+
for a in data.get("agents", []):
|
|
241
|
+
console.print(f" • {a}")
|
|
242
|
+
console.print()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""prox governance — audit logs and platform metrics."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from ..api import gateway_get, APIError
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="View governance logs and stats.")
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command("logs")
|
|
14
|
+
def logs(
|
|
15
|
+
domain: Optional[str] = typer.Option(None, "--domain", "-d"),
|
|
16
|
+
limit: int = typer.Option(20, "--limit", "-n"),
|
|
17
|
+
):
|
|
18
|
+
"""View audit logs."""
|
|
19
|
+
try:
|
|
20
|
+
params = {"limit": limit}
|
|
21
|
+
if domain:
|
|
22
|
+
params["domain"] = domain
|
|
23
|
+
data = gateway_get("/governance/logs", params=params)
|
|
24
|
+
entries = data.get("logs", data.get("entries", []))
|
|
25
|
+
if not entries:
|
|
26
|
+
console.print("[dim]No logs.[/dim]")
|
|
27
|
+
return
|
|
28
|
+
table = Table(title=f"Governance Logs (last {limit})")
|
|
29
|
+
table.add_column("Time", style="dim")
|
|
30
|
+
table.add_column("Type")
|
|
31
|
+
table.add_column("Agent")
|
|
32
|
+
table.add_column("User")
|
|
33
|
+
table.add_column("Tokens", justify="right")
|
|
34
|
+
for e in entries[:limit]:
|
|
35
|
+
table.add_row(
|
|
36
|
+
e.get("timestamp", "")[:19],
|
|
37
|
+
e.get("type", "query"),
|
|
38
|
+
e.get("agent_id", ""),
|
|
39
|
+
e.get("user", ""),
|
|
40
|
+
str(e.get("tokens_used", "")),
|
|
41
|
+
)
|
|
42
|
+
console.print(table)
|
|
43
|
+
except APIError as e:
|
|
44
|
+
console.print(f"[red]Error:[/red] {e.detail}")
|
|
45
|
+
raise typer.Exit(1)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.command("stats")
|
|
49
|
+
def stats(domain: Optional[str] = typer.Option(None, "--domain", "-d")):
|
|
50
|
+
"""View platform stats."""
|
|
51
|
+
try:
|
|
52
|
+
params = {"domain": domain} if domain else {}
|
|
53
|
+
data = gateway_get("/governance/stats", params=params)
|
|
54
|
+
console.print(f"\n [bold]Platform Stats[/bold]{' (' + domain + ')' if domain else ''}\n")
|
|
55
|
+
console.print(f" Total queries: {data.get('total_queries', 0)}")
|
|
56
|
+
console.print(f" Total tokens: {data.get('total_tokens', 0):,}")
|
|
57
|
+
console.print(f" Total cost: ${data.get('total_cost', 0):.2f}")
|
|
58
|
+
console.print(f" Avg duration: {data.get('avg_duration_ms', 0):.0f}ms")
|
|
59
|
+
console.print(f" Agents active: {data.get('agents_active', 0)}")
|
|
60
|
+
console.print()
|
|
61
|
+
except APIError as e:
|
|
62
|
+
console.print(f"[red]Error:[/red] {e.detail}")
|
|
63
|
+
raise typer.Exit(1)
|