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`);
+};