diff --git a/agent-node/VERSION b/agent-node/VERSION index 2ac9634..a970716 100644 --- a/agent-node/VERSION +++ b/agent-node/VERSION @@ -1 +1 @@ -1.0.13 +1.0.15 diff --git a/agent-node/bootstrap_installer.py b/agent-node/bootstrap_installer.py index e32c278..74a1845 100644 --- a/agent-node/bootstrap_installer.py +++ b/agent-node/bootstrap_installer.py @@ -23,11 +23,12 @@ import tempfile import argparse import subprocess +import socket import urllib.request import urllib.error # ── Minimal defaults — overridden by CLI args or agent_config.yaml ──────────── -DEFAULT_HUB = "https://ai.jerxie.com" +DEFAULT_HUB = "" # Must be provided via --hub or config INSTALL_DIR = os.path.join(os.path.expanduser("~"), ".cortex", "agent-node") @@ -106,7 +107,7 @@ shutil.rmtree(tmp_dir, ignore_errors=True) -def _install_deps(install_dir: str): +def _install_deps(install_dir: str, skip_browsers: bool = False): req_file = os.path.join(install_dir, "requirements.txt") if not os.path.exists(req_file): _print("No requirements.txt found — skipping dependency install.") @@ -143,10 +144,16 @@ _print("Dependencies installed.") # New: Auto-install playwright browsers if the package is present + if skip_browsers: + _print("Skipping Playwright browser installation as requested.") + return + try: import playwright _print("Playwright detected. Installing chromium browser...") - subprocess.run([sys.executable, "-m", "playwright", "install", "chromium"], check=True) + # We add --with-deps but check for root first + cmd = [sys.executable, "-m", "playwright", "install", "chromium"] + subprocess.run(cmd, check=True) _print("Playwright browsers installed.") except ImportError: pass # No playwright needed @@ -227,6 +234,7 @@ parser.add_argument("--install-dir", default=INSTALL_DIR, help=f"Install path (default: {INSTALL_DIR})") parser.add_argument("--update-only", action="store_true", help="Only pull latest code, don't re-launch") parser.add_argument("--daemon", action="store_true", help="Install and run as a persistent background daemon (macOS/Linux)") + parser.add_argument("--skip-browsers", action="store_true", help="Skip automatic Playwright browser installation") args = parser.parse_args() # Try loading existing config for defaults @@ -269,7 +277,9 @@ # This is the node-specific invite_token. node_token = args.token or existing_config.get("auth_token") or os.getenv("AGENT_AUTH_TOKEN", "") - node_id = args.node_id or existing_config.get("node_id", "cortex-node-001") + node_id = args.node_id or existing_config.get("node_id") + if not node_id: + node_id = socket.gethostname() or "cortex-node" # Ensure grpc endpoint has a port grpc = args.grpc or existing_config.get("grpc_endpoint") @@ -280,6 +290,10 @@ install_dir = args.install_dir + if not hub_url: + _print("ERROR: --hub is required (or set in agent_config.yaml)") + sys.exit(1) + if not hub_token: _print("ERROR: --token is required (or set AGENT_AUTH_TOKEN env var)") sys.exit(1) @@ -300,8 +314,10 @@ _print(f"Updating {local_version} → {remote_version}") + skip_browsers = args.skip_browsers or existing_config.get("skip_browsers", False) + _install(hub_url, hub_token, install_dir) - _install_deps(install_dir) + _install_deps(install_dir, skip_browsers=skip_browsers) _write_config(install_dir, node_id, hub_url, node_token, grpc, secret_key=hub_token) if args.update_only: diff --git a/ai-hub/app/api/routes/agent_update.py b/ai-hub/app/api/routes/agent_update.py index 69aff9b..2b0923d 100644 --- a/ai-hub/app/api/routes/agent_update.py +++ b/ai-hub/app/api/routes/agent_update.py @@ -52,7 +52,18 @@ """Validates the agent auth token from the request header.""" from app.config import settings token = request.headers.get("X-Agent-Token", "") - return token == settings.SECRET_KEY + if token == settings.SECRET_KEY: + return True + + # M4 Fallback: Also allow any valid invite_token currently in the DB + from app.api.deps import get_db + from app.models.agent_node import AgentNode + db = next(get_db()) + try: + exists = db.query(AgentNode).filter(AgentNode.invite_token == token, AgentNode.is_active == True).first() + return exists is not None + finally: + db.close() def _should_exclude(path: str) -> bool: diff --git a/ai-hub/app/api/routes/nodes.py b/ai-hub/app/api/routes/nodes.py index 4288407..bdf96a5 100644 --- a/ai-hub/app/api/routes/nodes.py +++ b/ai-hub/app/api/routes/nodes.py @@ -28,7 +28,7 @@ import secrets from typing import Optional, Annotated import logging -from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect, Depends, Query, Header +from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect, Depends, Query, Header, Request from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session @@ -566,7 +566,7 @@ 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)): + def provision_node(node_id: str, token: str, request: Request, skip_browsers: bool = Query(False), db: Session = Depends(get_db)): """ Returns a Python script that can be piped into python3 to automatically install and start the agent node. @@ -578,12 +578,14 @@ 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) + skill_overrides = {} + if skip_browsers: + skill_overrides["browser"] = {"enabled": False} + config_yaml = _generate_node_config_yaml(node, skill_overrides=skill_overrides) # 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" + # Dynamically determine the hub URL from the request itself + base_url = f"{request.url.scheme}://{request.url.netloc}" script = f"""# Cortex Agent One-Liner Provisioner import os @@ -610,7 +612,10 @@ # 4. Run installer with --daemon (or --non-interactive) print("[*] Bootstrapping agent...") -subprocess.run([sys.executable, "bootstrap_installer.py", "--daemon"]) +cmd = [sys.executable, "bootstrap_installer.py", "--daemon"] +if {skip_browsers}: + cmd.append("--skip-browsers") +subprocess.run(cmd) print("✅ Provisioning complete! Node should be online in the Mesh Dashboard shortly.") """ @@ -1122,7 +1127,7 @@ # Helpers # =========================================================================== -def _generate_node_config_yaml(node: models.AgentNode) -> str: +def _generate_node_config_yaml(node: models.AgentNode, skill_overrides: dict = None) -> str: """Helper to generate the agent_config.yaml content.""" hub_url = os.getenv("HUB_PUBLIC_URL", "https://ai.jerxie.com") # Preserve the gRPC endpoint as-is from environment @@ -1137,6 +1142,12 @@ except Exception: skill_cfg = {} + if skill_overrides: + for skill, cfg in skill_overrides.items(): + if skill not in skill_cfg: + skill_cfg[skill] = {} + skill_cfg[skill].update(cfg) + lines = [ "# Cortex Hub — Agent Node Configuration", f"# Generated for node '{node.node_id}' — keep this file secret.", diff --git a/frontend/src/pages/NodesPage.js b/frontend/src/pages/NodesPage.js index 18e2bc7..114431b 100644 --- a/frontend/src/pages/NodesPage.js +++ b/frontend/src/pages/NodesPage.js @@ -19,6 +19,7 @@ const [expandedNodes, setExpandedNodes] = useState({}); // node_id -> boolean const [expandedFiles, setExpandedFiles] = useState({}); // node_id -> boolean const [editingNodeId, setEditingNodeId] = useState(null); + const [provisionIncludeBrowsers, setProvisionIncludeBrowsers] = useState(true); const [editForm, setEditForm] = useState({ display_name: '', description: '', @@ -714,16 +715,27 @@ - {/* One-Liner Provisioning */}
- curl -sSL 'https://ai.jerxie.com/api/v1/nodes/provision/{node.node_id}?token={node.invite_token}' | python3
+ curl -sSL 'https://ai.jerxie.com/api/v1/nodes/provision/{node.node_id}?token={node.invite_token}{provisionIncludeBrowsers ? "" : "&skip_browsers=true"}' | python3