diff --git a/ai-hub/app/api/routes/nodes.py b/ai-hub/app/api/routes/nodes.py index b256ddd..7670dc3 100644 --- a/ai-hub/app/api/routes/nodes.py +++ b/ai-hub/app/api/routes/nodes.py @@ -498,11 +498,14 @@ # Add pre-configured yaml zip_file.writestr("agent_config.yaml", config_yaml) - # Add a quick start script for comfort - quick_start = f"#!/bin/bash\n# Cortex Agent Quick Start\n./cortex-agent\n" + # Add a daemon install script for comfort + start_sh_script = services.mesh_service.get_template_content("provisioning/start_daemon.sh.j2") + if not start_sh_script: + start_sh_script = "#!/bin/bash\n./cortex-agent\n" + script_info = zipfile.ZipInfo("start.sh") script_info.external_attr = 0o100755 << 16 - zip_file.writestr(script_info, quick_start) + zip_file.writestr(script_info, start_sh_script) zip_buffer.seek(0) return StreamingResponse( @@ -511,6 +514,17 @@ headers={"Content-Disposition": f"attachment; filename=cortex-node-{node_id}-{arch}.zip"} ) + @router.get("/provision/binaries/status", summary="Check binary availability status") + def get_binaries_status(db: Session = Depends(get_db)): + from app.api.routes.agent_update import _AGENT_NODE_DIR + dist_dir = os.path.join(_AGENT_NODE_DIR, "dist") + available = [] + if os.path.exists(dist_dir): + for arch in os.listdir(dist_dir): + if os.path.exists(os.path.join(dist_dir, arch, "cortex-agent")): + available.append(arch) + return {"available_architectures": available} + @router.get("/admin/{node_id}/download", summary="[Admin] Download Agent Node Bundle (ZIP)") def admin_download_bundle( node_id: str, diff --git a/ai-hub/app/core/templates/provisioning/start_daemon.sh.j2 b/ai-hub/app/core/templates/provisioning/start_daemon.sh.j2 new file mode 100644 index 0000000..25b4a93 --- /dev/null +++ b/ai-hub/app/core/templates/provisioning/start_daemon.sh.j2 @@ -0,0 +1,81 @@ +#!/bin/bash +set -e + +echo "🚀 Installing Cortex Agent Daemon from Bundle..." + +# Determine OS +OS="$(uname -s | tr '[:upper:]' '[:lower:]')" + +# 1. Create .cortex/agent-node directory +INSTALL_DIR="$HOME/.cortex/agent-node" +mkdir -p "$INSTALL_DIR" + +# 2. Copy binary and config into installation directory +cp cortex-agent "$INSTALL_DIR/cortex-agent" +cp agent_config.yaml "$INSTALL_DIR/agent_config.yaml" +chmod +x "$INSTALL_DIR/cortex-agent" + +# 3. Bootstrap daemon installation +echo "[*] Bootstrapping agent background daemon..." + +if [ "$OS" = "darwin" ]; then + echo "Installing macOS launchd service..." + AGENTS_DIR="$HOME/Library/LaunchAgents" + mkdir -p "$AGENTS_DIR" + PLIST_PATH="$AGENTS_DIR/com.jerxie.cortex.agent.plist" + cat > "$PLIST_PATH" << EOF + + + + + Label + com.jerxie.cortex.agent + ProgramArguments + + $INSTALL_DIR/cortex-agent + + WorkingDirectory + $INSTALL_DIR + RunAtLoad + + KeepAlive + + StandardErrorPath + $INSTALL_DIR/agent.err.log + StandardOutPath + $INSTALL_DIR/agent.out.log + + +EOF + launchctl unload "$PLIST_PATH" 2>/dev/null || true + launchctl load "$PLIST_PATH" + echo "✅ macOS Agent Daemon installed and started!" + echo "Logs are available at: $INSTALL_DIR/agent.out.log" +else + # Try systemd fallback + echo "Installing Linux systemd service..." + SERVICE_DIR="$HOME/.config/systemd/user" + mkdir -p "$SERVICE_DIR" + SERVICE_PATH="$SERVICE_DIR/cortex-agent.service" + cat > "$SERVICE_PATH" << EOF +[Unit] +Description=Cortex Agent Node +After=network.target + +[Service] +ExecStart=$INSTALL_DIR/cortex-agent +WorkingDirectory=$INSTALL_DIR +Restart=always +RestartSec=5 + +[Install] +WantedBy=default.target +EOF + systemctl --user daemon-reload + systemctl --user enable --now cortex-agent.service + echo "✅ Linux Agent Daemon installed and started!" + echo "To view logs: journalctl --user -u cortex-agent -f" +fi + +echo "" +echo "🎉 Agent successfully daemonized! You may now close this terminal securely." diff --git a/frontend/src/features/nodes/pages/NodesPage.js b/frontend/src/features/nodes/pages/NodesPage.js index 4d66a8b..806c105 100644 --- a/frontend/src/features/nodes/pages/NodesPage.js +++ b/frontend/src/features/nodes/pages/NodesPage.js @@ -2,13 +2,14 @@ import { getAdminNodes, adminCreateNode, adminUpdateNode, adminDeleteNode, adminDownloadNodeBundle, getUserAccessibleNodes, - getAdminGroups, getNodeStreamUrl + getAdminGroups, getNodeStreamUrl, getBinaryStatus } from '../../../services/apiService'; import NodeTerminal from "../components/NodeTerminal"; import { FileSystemNavigator } from "../../../shared/components"; const NodesPage = ({ user }) => { const [nodes, setNodes] = useState([]); + const [availableBinaries, setAvailableBinaries] = useState([]); const [groups, setGroups] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -41,6 +42,9 @@ const fetchData = useCallback(async () => { setLoading(true); try { + const statusRes = await getBinaryStatus().catch(() => ({ available_architectures: [] })); + setAvailableBinaries(statusRes.available_architectures || []); + if (isAdmin) { const [nodesData, groupsData] = await Promise.all([getAdminNodes(), getAdminGroups()]); setNodes(nodesData); @@ -717,29 +721,37 @@ Source (.tar.gz) { if(!availableBinaries.includes('linux_amd64')) e.preventDefault(); }} + title={!availableBinaries.includes('linux_amd64') ? "Binary not compiled on hub" : ""} + className={`flex-1 border px-3 py-1.5 rounded text-[11px] font-bold shadow-sm transition-all flex items-center justify-center whitespace-nowrap ${availableBinaries.includes('linux_amd64') ? 'bg-white dark:bg-gray-700 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 active:scale-95' : 'bg-gray-100 dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-400 dark:text-gray-600 cursor-not-allowed opacity-60'}`} > Linux (AMD64) { if(!availableBinaries.includes('linux_arm64')) e.preventDefault(); }} + title={!availableBinaries.includes('linux_arm64') ? "Binary not compiled on hub" : ""} + className={`flex-1 border px-3 py-1.5 rounded text-[11px] font-bold shadow-sm transition-all flex items-center justify-center whitespace-nowrap ${availableBinaries.includes('linux_arm64') ? 'bg-white dark:bg-gray-700 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 active:scale-95' : 'bg-gray-100 dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-400 dark:text-gray-600 cursor-not-allowed opacity-60'}`} > Linux (ARM64) { if(!availableBinaries.includes('darwin_arm64')) e.preventDefault(); }} + title={!availableBinaries.includes('darwin_arm64') ? "Binary not compiled on hub" : ""} + className={`flex-1 border px-3 py-1.5 rounded text-[11px] font-bold shadow-sm transition-all flex items-center justify-center whitespace-nowrap ${availableBinaries.includes('darwin_arm64') ? 'bg-white dark:bg-gray-700 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 active:scale-95' : 'bg-gray-100 dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-400 dark:text-gray-600 cursor-not-allowed opacity-60'}`} > macOS (ARM64) { if(!availableBinaries.includes('darwin_amd64')) e.preventDefault(); }} + title={!availableBinaries.includes('darwin_amd64') ? "Binary not compiled on hub" : ""} + className={`flex-1 border px-3 py-1.5 rounded text-[11px] font-bold shadow-sm transition-all flex items-center justify-center whitespace-nowrap ${availableBinaries.includes('darwin_amd64') ? 'bg-white dark:bg-gray-700 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 active:scale-95' : 'bg-gray-100 dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-400 dark:text-gray-600 cursor-not-allowed opacity-60'}`} > macOS (Intel) diff --git a/frontend/src/services/api/nodeService.js b/frontend/src/services/api/nodeService.js index 355f6b1..39c1c7e 100644 --- a/frontend/src/services/api/nodeService.js +++ b/frontend/src/services/api/nodeService.js @@ -161,3 +161,10 @@ body: { path, session_id: sessionId } }); }; + +/** + * Fetch status of pre-compiled binaries on the central hub. + */ +export const getBinaryStatus = async () => { + return await fetchWithAuth(`/nodes/provision/binaries/status`); +};