diff --git a/ai-hub/app/api/routes/agent_update.py b/ai-hub/app/api/routes/agent_update.py index 3da1d79..69aff9b 100644 --- a/ai-hub/app/api/routes/agent_update.py +++ b/ai-hub/app/api/routes/agent_update.py @@ -144,4 +144,23 @@ headers={"Content-Disposition": f"attachment; filename={filename}"} ) + @router.get("/installer", summary="Download bootstrap_installer.py") + def download_installer(): + """ + Returns the bootstrap_installer.py script itself. + Used for initial one-liner provisioning on headless nodes. + """ + installer_path = os.path.join(_AGENT_NODE_DIR, "bootstrap_installer.py") + if not os.path.exists(installer_path): + raise HTTPException(status_code=404, detail="Installer script not found on hub.") + + with open(installer_path, "rb") as f: + content = f.read() + + return StreamingResponse( + iter([content]), + media_type="text/x-python", + headers={"Content-Disposition": "attachment; filename=bootstrap_installer.py"} + ) + return router diff --git a/ai-hub/app/api/routes/nodes.py b/ai-hub/app/api/routes/nodes.py index 718dce0..4288407 100644 --- a/ai-hub/app/api/routes/nodes.py +++ b/ai-hub/app/api/routes/nodes.py @@ -565,6 +565,57 @@ config_yaml = _generate_node_config_yaml(node) return schemas.NodeConfigYamlResponse(node_id=node_id, config_yaml=config_yaml) + @router.get("/provision/{node_id}", summary="Headless Provisioning Script") + def provision_node(node_id: str, token: str, db: Session = Depends(get_db)): + """ + Returns a Python script that can be piped into python3 to automatically + install and start the agent node. + + Usage: curl -sSL https://.../provision/{node_id}?token={token} | python3 + """ + from fastapi.responses import PlainTextResponse + node = db.query(models.AgentNode).filter(models.AgentNode.node_id == node_id).first() + if not node or node.invite_token != token: + raise HTTPException(status_code=403, detail="Invalid node or token.") + + config_yaml = _generate_node_config_yaml(node) + + # We need the hub's base URL. We can try to infer it from the request or use settings. + # For simplicity in this env, we'll use a placeholder or try to get it from request. + # But usually ai.jerxie.com is the hardcoded entrypoint for production. + base_url = "https://ai.jerxie.com" + + script = f"""# Cortex Agent One-Liner Provisioner +import os +import sys +import urllib.request +import subprocess + +print("🚀 Starting Cortex Agent Provisioning for node: {node_id}") + +# 1. Create .cortex/agent-node directory +install_dir = os.path.expanduser("~/.cortex/agent-node") +os.makedirs(install_dir, exist_ok=True) +os.chdir(install_dir) + +# 2. Write agent_config.yaml +print("[*] Writing configuration...") +with open("agent_config.yaml", "w") as f: + f.write(\"\"\"{config_yaml}\"\"\") + +# 3. Download bootstrap_installer.py +print("[*] Downloading installer...") +installer_url = "{base_url}/api/v1/agent/installer" +urllib.request.urlretrieve(installer_url, "bootstrap_installer.py") + +# 4. Run installer with --daemon (or --non-interactive) +print("[*] Bootstrapping agent...") +subprocess.run([sys.executable, "bootstrap_installer.py", "--daemon"]) + +print("✅ Provisioning complete! Node should be online in the Mesh Dashboard shortly.") +""" + return PlainTextResponse(script) + @router.get("/admin/{node_id}/download", summary="[Admin] Download Agent Node Bundle (ZIP)") def admin_download_bundle( node_id: str, diff --git a/frontend/src/pages/NodesPage.js b/frontend/src/pages/NodesPage.js index 7caf16a..559bd56 100644 --- a/frontend/src/pages/NodesPage.js +++ b/frontend/src/pages/NodesPage.js @@ -703,13 +703,39 @@
{isAdmin && ( - +
+
+ +
+ + {/* One-Liner Provisioning */} +
+ +
+ + curl -sSL https://ai.jerxie.com/api/v1/nodes/provision/{node.node_id}?token={node.invite_token} | python3 + + +
+

Best for terminal-only servers. Installs agent as a persistent service.

+
+
)}