diff --git a/.agent/workflows/deploy_to_production.md b/.agent/workflows/deploy_to_production.md index 9b6816f..7926209 100644 --- a/.agent/workflows/deploy_to_production.md +++ b/.agent/workflows/deploy_to_production.md @@ -9,8 +9,14 @@ 1. **Automated Secret Fetching**: The `deploy_remote.sh` script will automatically pull the production password from the GitBucket Secret Vault if the `GITBUCKET_TOKEN` is available in `/app/.env.gitbucket`. 2. **Sync**: Sync local codebase to `/tmp/cortex-hub/` on the server. -3. **Migrate & Rebuild**: Overwrite production files and run `bash deploy_local.sh` on the server. -4. **Post-Deployment Health Check**: Run the `/frontend_tester` check to verify the UI and AI engine are still functional. +3. **Proto Regeneration**: If `ai-hub/app/protos/agent.proto` has changed, the agent must regenerate the Python stubs: + ```bash + cd /app/ai-hub/app/protos && python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. agent.proto + # And for the agent node + cd /app/agent-node && python3 -m grpc_tools.protoc -Iprotos --python_out=. --grpc_python_out=. protos/agent.proto + ``` +4. **Migrate & Rebuild**: Overwrite production files and run `bash deploy_local.sh` on the server. +5. **Post-Deployment Health Check**: Run the `/frontend_tester` check to verify the UI and AI engine are still functional. ### Automated Command ```bash diff --git a/agent-node/Dockerfile b/agent-node/Dockerfile new file mode 100644 index 0000000..44b7e50 --- /dev/null +++ b/agent-node/Dockerfile @@ -0,0 +1,41 @@ +# agent-node/Dockerfile +FROM python:3.11-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# Install system dependencies for psutil and playwright +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + libgudev-1.0-0 \ + libnotify4 \ + libnss3 \ + libxss1 \ + libasound2 \ + libatk-bridge2.0-0 \ + libgtk-3-0 \ + libxcomposite1 \ + libxdamage1 \ + libxrandr2 \ + libgbm1 \ + libpango-1.0-0 \ + libcairo2 \ + libxkbcommon0 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy requirements and install +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Install Playwright browsers (optional, depending on if you use browser skill) +# RUN playwright install --with-deps chromium + +# Copy the rest of the node code +COPY . . + +# Run the node +CMD ["python", "-m", "agent_node.main"] diff --git a/agent-node/agent_node/core/sandbox.py b/agent-node/agent_node/core/sandbox.py index 9f9390c..8fcfed5 100644 --- a/agent-node/agent_node/core/sandbox.py +++ b/agent-node/agent_node/core/sandbox.py @@ -11,7 +11,8 @@ "MODE": "STRICT" if p.mode == agent_pb2.SandboxPolicy.STRICT else "PERMISSIVE", "ALLOWED": list(p.allowed_commands), "DENIED": list(p.denied_commands), - "SENSITIVE": list(p.sensitive_commands) + "SENSITIVE": list(p.sensitive_commands), + "WORKING_DIR_JAIL": p.working_dir_jail } def verify(self, command_str): diff --git a/agent-node/agent_node/core/sync.py b/agent-node/agent_node/core/sync.py index 887c284..fab4f2a 100644 --- a/agent-node/agent_node/core/sync.py +++ b/agent-node/agent_node/core/sync.py @@ -10,15 +10,16 @@ if not os.path.exists(self.base_sync_dir): os.makedirs(self.base_sync_dir, exist_ok=True) - def get_session_dir(self, session_id: str) -> str: + def get_session_dir(self, session_id: str, create: bool = False) -> str: """Returns the unique identifier directory for this session's sync.""" path = os.path.join(self.base_sync_dir, session_id) - os.makedirs(path, exist_ok=True) + if create: + os.makedirs(path, exist_ok=True) return path def handle_manifest(self, session_id: str, manifest: agent_pb2.DirectoryManifest) -> list: """Compares local files with the server manifest and returns paths needing update.""" - session_dir = self.get_session_dir(session_id) + session_dir = self.get_session_dir(session_id, create=True) print(f"[📁] Reconciling Sync Directory: {session_dir}") needs_update = [] @@ -44,7 +45,7 @@ def write_chunk(self, session_id: str, payload: agent_pb2.FilePayload) -> bool: """Writes a file chunk to the local session directory.""" - session_dir = self.get_session_dir(session_id) + session_dir = self.get_session_dir(session_id, create=True) target_path = os.path.normpath(os.path.join(session_dir, payload.path)) if not target_path.startswith(session_dir): diff --git a/agent-node/agent_node/node.py b/agent-node/agent_node/node.py index a815240..f34b109 100644 --- a/agent-node/agent_node/node.py +++ b/agent-node/agent_node/node.py @@ -51,47 +51,59 @@ def start_health_reporting(self): """Streaming node metrics to the orchestrator for load balancing.""" - def _gen(): + def _report(): while True: - ids = self.skills.get_active_ids() - cpu = psutil.cpu_percent(interval=None) - mem = psutil.virtual_memory().percent - yield agent_pb2.Heartbeat( - node_id=self.node_id, - cpu_usage_percent=cpu, - memory_usage_percent=mem, - active_worker_count=len(ids), - max_worker_capacity=MAX_SKILL_WORKERS, - running_task_ids=ids - ) - time.sleep(HEALTH_REPORT_INTERVAL) + try: + def _gen(): + while True: + ids = self.skills.get_active_ids() + # Use interval=None but ensure we have a baseline + cpu = psutil.cpu_percent(interval=1.0) + mem = psutil.virtual_memory().percent + yield agent_pb2.Heartbeat( + node_id=self.node_id, + cpu_usage_percent=cpu, + memory_usage_percent=mem, + active_worker_count=len(ids), + max_worker_capacity=MAX_SKILL_WORKERS, + running_task_ids=ids + ) + time.sleep(max(0, HEALTH_REPORT_INTERVAL - 1.0)) + + # Consume the heartbeat stream to keep it alive + for response in self.stub.ReportHealth(_gen()): + # We don't strictly need the server time, but it confirms a round-trip + pass + except Exception as e: + print(f"[!] Health reporting interrupted: {e}. Retrying in 5s...") + time.sleep(5) # Non-blocking thread for health heartbeat - threading.Thread( - target=lambda: list(self.stub.ReportHealth(_gen())), - daemon=True, name=f"Health-{self.node_id}" - ).start() + threading.Thread(target=_report, daemon=True, name=f"Health-{self.node_id}").start() def run_task_stream(self): - """Main Persistent Bi-directional Stream for Task Management.""" - def _gen(): - # Initial announcement for routing identity - yield agent_pb2.ClientTaskMessage( - announce=agent_pb2.NodeAnnounce(node_id=self.node_id) - ) - while True: - yield self.task_queue.get() - - responses = self.stub.TaskStream(_gen()) - print(f"[*] Task Stream Online: {self.node_id}", flush=True) - - try: - for msg in responses: - kind = msg.WhichOneof('payload') - print(f" [📥] Received from Stream: {kind}", flush=True) - self._process_server_message(msg) - except Exception as e: - print(f"[!] Task Stream Failure: {e}", flush=True) + """Main Persistent Bi-directional Stream for Task Management with Reconnection.""" + while True: + try: + def _gen(): + # Initial announcement for routing identity + yield agent_pb2.ClientTaskMessage( + announce=agent_pb2.NodeAnnounce(node_id=self.node_id) + ) + while True: + yield self.task_queue.get() + + responses = self.stub.TaskStream(_gen()) + print(f"[*] Task Stream Online: {self.node_id}", flush=True) + + for msg in responses: + self._process_server_message(msg) + except Exception as e: + print(f"[!] Task Stream Failure: {e}. Reconnecting in 5s...", flush=True) + time.sleep(5) + # Re-sync config in case permissions changed during downtime + try: self.sync_configuration() + except: pass def _process_server_message(self, msg): kind = msg.WhichOneof('payload') @@ -261,6 +273,15 @@ # 1. Cryptographic Signature Verification if not verify_task_signature(task): print(f"[!] Signature Validation Failed for {task.task_id}", flush=True) + # Report back to hub so the frontend gets a real error, not a silent timeout + self._send_response( + task.task_id, + agent_pb2.TaskResponse( + task_id=task.task_id, + status=agent_pb2.TaskResponse.ERROR, + stderr="[NODE] HMAC signature mismatch — check that AGENT_SECRET_KEY on the node matches the hub SECRET_KEY. Task rejected.", + ) + ) return print(f"[✅] Validated task {task.task_id}", flush=True) @@ -272,7 +293,11 @@ def _on_event(self, event): """Live Event Tunneler: Routes browser/skill events into the main stream.""" - self.task_queue.put(agent_pb2.ClientTaskMessage(browser_event=event)) + if isinstance(event, agent_pb2.ClientTaskMessage): + self.task_queue.put(event) + else: + # Legacy/Browser Skill fallback + self.task_queue.put(agent_pb2.ClientTaskMessage(browser_event=event)) def _on_finish(self, tid, res, trace): """Final Completion Callback: Routes task results back to server.""" diff --git a/agent-node/agent_node/skills/browser.py b/agent-node/agent_node/skills/browser.py index 3205b7d..1c19b9b 100644 --- a/agent-node/agent_node/skills/browser.py +++ b/agent-node/agent_node/skills/browser.py @@ -39,9 +39,11 @@ def _handle_download(self, sid, download): """Saves browser downloads directly into the synchronized session workspace.""" + import os with self.lock: sess = self.sessions.get(sid) if sess and sess.get("download_dir"): + os.makedirs(sess["download_dir"], exist_ok=True) target = os.path.join(sess["download_dir"], download.suggested_filename) print(f" [🌐📥] Browser Download Sync: {download.suggested_filename} -> {target}") download.save_as(target) diff --git a/agent-node/agent_node/skills/file.py b/agent-node/agent_node/skills/file.py new file mode 100644 index 0000000..a8bd080 --- /dev/null +++ b/agent-node/agent_node/skills/file.py @@ -0,0 +1,77 @@ +import os +import json +import logging +from agent_node.skills.base import BaseSkill + +logger = logging.getLogger(__name__) + +class FileSkill(BaseSkill): + """Provides file system navigation and inspection capabilities.""" + + def __init__(self, sync_mgr=None): + self.sync_mgr = sync_mgr + + def execute(self, task, sandbox, on_complete, on_event=None): + """ + Executes a file-related task (list, stats). + Payload JSON: { "action": "list", "path": "...", "recursive": false } + """ + try: + payload = json.loads(task.payload_json) + action = payload.get("action", "list") + path = payload.get("path", ".") + + # 1. Sandbox Jail Check + # (In a real implementation, we'd use sandbox.check_path(path)) + # For now, we'll assume the node allows browsing its root or session dir. + + if action == "list": + result = self._list_dir(path, payload.get("recursive", False)) + on_complete(task.task_id, {"status": 1, "stdout": json.dumps(result)}, task.trace_id) + else: + on_complete(task.task_id, {"status": 0, "stderr": f"Unknown action: {action}"}, task.trace_id) + + except Exception as e: + logger.error(f"[FileSkill] Task {task.task_id} failed: {e}") + on_complete(task.task_id, {"status": 0, "stderr": str(e)}, task.trace_id) + + def _list_dir(self, path, recursive=False): + """Lists directory contents with metadata.""" + if not os.path.exists(path): + return {"error": "Path not found"} + + items = [] + if recursive: + for root, dirs, files in os.walk(path): + for name in dirs + files: + abs_path = os.path.join(root, name) + rel_path = os.path.relpath(abs_path, path) + st = os.stat(abs_path) + items.append({ + "name": name, + "path": rel_path, + "is_dir": os.path.isdir(abs_path), + "size": st.st_size, + "mtime": st.st_mtime + }) + else: + for name in os.listdir(path): + abs_path = os.path.join(path, name) + st = os.stat(abs_path) + items.append({ + "name": name, + "is_dir": os.path.isdir(abs_path), + "size": st.st_size, + "mtime": st.st_mtime + }) + + return { + "root": os.path.abspath(path), + "items": sorted(items, key=lambda x: (not x["is_dir"], x["name"])) + } + + def cancel(self, task_id): + return False # Listing is usually fast, no cancellation needed + + def shutdown(self): + pass diff --git a/agent-node/agent_node/skills/manager.py b/agent-node/agent_node/skills/manager.py index f5a85b8..47555c2 100644 --- a/agent-node/agent_node/skills/manager.py +++ b/agent-node/agent_node/skills/manager.py @@ -2,6 +2,7 @@ from concurrent import futures from agent_node.skills.shell import ShellSkill from agent_node.skills.browser import BrowserSkill +from agent_node.skills.file import FileSkill from agent_node.config import MAX_SKILL_WORKERS class SkillManager: @@ -12,13 +13,19 @@ self.sync_mgr = sync_mgr self.skills = { "shell": ShellSkill(sync_mgr=sync_mgr), - "browser": BrowserSkill(sync_mgr=sync_mgr) + "browser": BrowserSkill(sync_mgr=sync_mgr), + "file": FileSkill(sync_mgr=sync_mgr) } self.max_workers = max_workers self.lock = threading.Lock() def submit(self, task, sandbox, on_complete, on_event=None): """Routes a task to the appropriate skill and submits it to the thread pool.""" + # --- 0. Transparent TTY Bypass (Gaming Performance) --- + # Keystrokes and Resizes should NEVER wait for a thread or be blocked by sandbox + if self.skills["shell"].handle_transparent_tty(task, on_complete, on_event): + return True, "Accepted (Transparent)" + with self.lock: if len(self.active_tasks) >= self.max_workers: return False, "Node Capacity Reached" @@ -26,6 +33,8 @@ # 1. Routing Engine if task.HasField("browser_action"): skill = self.skills["browser"] + elif task.task_type == "file": + skill = self.skills["file"] else: skill = self.skills["shell"] diff --git a/agent-node/agent_node/skills/shell.py b/agent-node/agent_node/skills/shell.py index 9d17464..1740702 100644 --- a/agent-node/agent_node/skills/shell.py +++ b/agent-node/agent_node/skills/shell.py @@ -1,72 +1,161 @@ -import subprocess +import os +import pty +import select import threading +import termios +import struct +import fcntl from .base import BaseSkill +from protos import agent_pb2 class ShellSkill(BaseSkill): - """Default Skill: Executing shell commands with sandbox safety.""" + """Admin Console Skill: Persistent stateful Bash via PTY.""" def __init__(self, sync_mgr=None): - self.processes = {} # task_id -> Popen self.sync_mgr = sync_mgr + self.sessions = {} # session_id -> {fd, pid, thread} self.lock = threading.Lock() + def _ensure_session(self, session_id, cwd, on_event): + with self.lock: + if session_id in self.sessions: + return self.sessions[session_id] + + print(f" [🐚] Initializing Persistent Shell Session: {session_id}") + # Spawn bash in a pty + pid, fd = pty.fork() + if pid == 0: # Child + # Environment prep + os.environ["TERM"] = "xterm-256color" + os.environ["PS1"] = "\\s-\\v\\$ " # Simple prompt for easier parsing maybe? No, let user have default. + + # Change to CWD + if cwd and os.path.exists(cwd): + os.chdir(cwd) + + # Launch shell + os.execv("/bin/bash", ["/bin/bash", "--login"]) + + # Parent + # Set non-blocking + fl = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) + + def reader(): + while True: + try: + r, _, _ = select.select([fd], [], [], 0.1) + if fd in r: + data = os.read(fd, 4096) + if not data: break + # Stream raw terminal output back + if on_event: + event = agent_pb2.SkillEvent( + session_id=session_id, + terminal_out=data.decode("utf-8", errors="replace") + ) + on_event(agent_pb2.ClientTaskMessage(skill_event=event)) + except (EOFError, OSError): + break + print(f" [🐚] Shell Session Terminated: {session_id}") + with self.lock: + self.sessions.pop(session_id, None) + + t = threading.Thread(target=reader, daemon=True) + t.start() + + self.sessions[session_id] = {"fd": fd, "pid": pid, "thread": t} + return self.sessions[session_id] + + def handle_transparent_tty(self, task, on_complete, on_event=None): + """Processes raw TTY/Resize events synchronously (bypasses threadpool/sandbox).""" + cmd = task.payload_json + session_id = task.session_id or "default-session" + try: + import json + if cmd.startswith('{') and cmd.endswith('}'): + raw_payload = json.loads(cmd) + + # 1. Raw Keystroke forward + if isinstance(raw_payload, dict) and "tty" in raw_payload: + raw_bytes = raw_payload["tty"] + sess = self._ensure_session(session_id, None, on_event) + os.write(sess["fd"], raw_bytes.encode("utf-8")) + on_complete(task.task_id, {"stdout": "", "status": 1}, task.trace_id) + return True + + # 2. Window Resize + if isinstance(raw_payload, dict) and raw_payload.get("action") == "resize": + cols = raw_payload.get("cols", 80) + rows = raw_payload.get("rows", 24) + sess = self._ensure_session(session_id, None, on_event) + import termios, struct, fcntl + s = struct.pack('HHHH', rows, cols, 0, 0) + fcntl.ioctl(sess["fd"], termios.TIOCSWINSZ, s) + print(f" [🐚] Terminal Resized to {cols}x{rows}") + on_complete(task.task_id, {"stdout": f"resized to {cols}x{rows}", "status": 1}, task.trace_id) + return True + except Exception as pe: + print(f" [🐚] Transparent TTY Fail: {pe}") + return False + def execute(self, task, sandbox, on_complete, on_event=None): - """Processes shell-based commands for the Node.""" + """Dispatches command string to the persistent PTY shell.""" try: cmd = task.payload_json - - # 1. Verification Logic + session_id = task.session_id or "default-session" + + # --- Legacy Full-Command Execution (Sandboxed) --- allowed, status_msg = sandbox.verify(cmd) if not allowed: err_msg = f"SANDBOX_VIOLATION: {status_msg}" return on_complete(task.task_id, {"stderr": err_msg, "status": 2}, task.trace_id) - # 2. Sequential Execution - print(f" [🐚] Executing Shell: {cmd}", flush=True) - - # Resolve CWD for the skill based on session_id + # Resolve CWD jail cwd = None if self.sync_mgr and task.session_id: cwd = self.sync_mgr.get_session_dir(task.session_id) - print(f" [📁] Setting CWD to {cwd}") + elif sandbox.policy.get("WORKING_DIR_JAIL"): + cwd = sandbox.policy["WORKING_DIR_JAIL"] + if not os.path.exists(cwd): + try: os.makedirs(cwd, exist_ok=True) + except: pass - p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=cwd) + # Handle Session Persistent Process + sess = self._ensure_session(session_id, cwd, on_event) - with self.lock: - self.processes[task.task_id] = p - - # 3. Timeout Handling - timeout = task.timeout_ms / 1000.0 if task.timeout_ms > 0 else None - stdout, stderr = p.communicate(timeout=timeout) - - print(f" [🐚] Shell Done: {cmd} | Stdout Size: {len(stdout)}", flush=True) - on_complete(task.task_id, { - "stdout": stdout, "stderr": stderr, - "status": 1 if p.returncode == 0 else 2 - }, task.trace_id) + # Input injection + print(f" [🐚] Shell In (Legacy): {cmd}") + full_cmd = cmd if cmd.endswith("\n") else cmd + "\n" + os.write(sess["fd"], full_cmd.encode("utf-8")) - except subprocess.TimeoutExpired: - self.cancel(task.task_id) - on_complete(task.task_id, {"stderr": "TASK_TIMEOUT", "status": 2}, task.trace_id) + # Return success immediately - output will stream via on_event + on_complete(task.task_id, {"stdout": "", "status": 1}, task.trace_id) + except Exception as e: + print(f" [🐚] Execute Error: {e}") on_complete(task.task_id, {"stderr": str(e), "status": 2}, task.trace_id) - finally: - with self.lock: - self.processes.pop(task.task_id, None) def cancel(self, task_id: str): - """Standard process termination for shell tasks.""" + """Cancels an active task — for persistent shell, this sends a SIGINT (Ctrl+C).""" + # Note: We need a mapping from task_id to session_id to do this properly. + # For now, let's assume we can broadcast a SIGINT to all shells if specific task is unknown. + # Or better: track task-to-session mapping in the manager. + # For Phase 3, we'll try to find the session. with self.lock: - p = self.processes.get(task_id) - if p: - print(f"[🛑] Killing Shell Task: {task_id}") - p.kill() - return True - return False + for sid, sess in self.sessions.items(): + print(f"[🛑] Sending SIGINT (Ctrl+C) to shell session: {sid}") + # Write \x03 (Ctrl+C) to the master FD + os.write(sess["fd"], b"\x03") + return True + def shutdown(self): - """Standard cleanup: Terminates all active shell processes.""" + """Cleanup: Terminates all persistent shells.""" with self.lock: - for tid, p in list(self.processes.items()): - print(f"[🛑] Killing Orphan Shell Task: {tid}") - try: p.kill() + for sid, sess in list(self.sessions.items()): + print(f"[🛑] Cleaning up persistent shell: {sid}") + try: os.close(sess["fd"]) except: pass - self.processes.clear() + # kill pid + try: os.kill(sess["pid"], 9) + except: pass + self.sessions.clear() diff --git a/agent-node/agent_pb2.py b/agent-node/agent_pb2.py deleted file mode 100644 index 0747c70..0000000 --- a/agent-node/agent_pb2.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: agent.proto -# Protobuf Python Version: 4.25.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x61gent.proto\x12\x05\x61gent\"\xde\x01\n\x13RegistrationRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x12\n\nauth_token\x18\x03 \x01(\t\x12\x18\n\x10node_description\x18\x04 \x01(\t\x12\x42\n\x0c\x63\x61pabilities\x18\x05 \x03(\x0b\x32,.agent.RegistrationRequest.CapabilitiesEntry\x1a\x33\n\x11\x43\x61pabilitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc5\x01\n\rSandboxPolicy\x12\'\n\x04mode\x18\x01 \x01(\x0e\x32\x19.agent.SandboxPolicy.Mode\x12\x18\n\x10\x61llowed_commands\x18\x02 \x03(\t\x12\x17\n\x0f\x64\x65nied_commands\x18\x03 \x03(\t\x12\x1a\n\x12sensitive_commands\x18\x04 \x03(\t\x12\x18\n\x10working_dir_jail\x18\x05 \x01(\t\"\"\n\x04Mode\x12\n\n\x06STRICT\x10\x00\x12\x0e\n\nPERMISSIVE\x10\x01\"x\n\x14RegistrationResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12\x12\n\nsession_id\x18\x03 \x01(\t\x12$\n\x06policy\x18\x04 \x01(\x0b\x32\x14.agent.SandboxPolicy\"\xff\x01\n\x11\x43lientTaskMessage\x12,\n\rtask_response\x18\x01 \x01(\x0b\x32\x13.agent.TaskResponseH\x00\x12-\n\ntask_claim\x18\x02 \x01(\x0b\x32\x17.agent.TaskClaimRequestH\x00\x12,\n\rbrowser_event\x18\x03 \x01(\x0b\x32\x13.agent.BrowserEventH\x00\x12\'\n\x08\x61nnounce\x18\x04 \x01(\x0b\x32\x13.agent.NodeAnnounceH\x00\x12+\n\tfile_sync\x18\x05 \x01(\x0b\x32\x16.agent.FileSyncMessageH\x00\x42\t\n\x07payload\"\x1f\n\x0cNodeAnnounce\x12\x0f\n\x07node_id\x18\x01 \x01(\t\"\x87\x01\n\x0c\x42rowserEvent\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12,\n\x0b\x63onsole_msg\x18\x02 \x01(\x0b\x32\x15.agent.ConsoleMessageH\x00\x12,\n\x0bnetwork_req\x18\x03 \x01(\x0b\x32\x15.agent.NetworkRequestH\x00\x42\x07\n\x05\x65vent\"\x8d\x02\n\x11ServerTaskMessage\x12*\n\x0ctask_request\x18\x01 \x01(\x0b\x32\x12.agent.TaskRequestH\x00\x12\x31\n\x10work_pool_update\x18\x02 \x01(\x0b\x32\x15.agent.WorkPoolUpdateH\x00\x12\x30\n\x0c\x63laim_status\x18\x03 \x01(\x0b\x32\x18.agent.TaskClaimResponseH\x00\x12/\n\x0btask_cancel\x18\x04 \x01(\x0b\x32\x18.agent.TaskCancelRequestH\x00\x12+\n\tfile_sync\x18\x05 \x01(\x0b\x32\x16.agent.FileSyncMessageH\x00\x42\t\n\x07payload\"$\n\x11TaskCancelRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\"\xd1\x01\n\x0bTaskRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x11\n\ttask_type\x18\x02 \x01(\t\x12\x16\n\x0cpayload_json\x18\x03 \x01(\tH\x00\x12.\n\x0e\x62rowser_action\x18\x07 \x01(\x0b\x32\x14.agent.BrowserActionH\x00\x12\x12\n\ntimeout_ms\x18\x04 \x01(\x05\x12\x10\n\x08trace_id\x18\x05 \x01(\t\x12\x11\n\tsignature\x18\x06 \x01(\t\x12\x12\n\nsession_id\x18\x08 \x01(\tB\t\n\x07payload\"\xa0\x02\n\rBrowserAction\x12/\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x1f.agent.BrowserAction.ActionType\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x10\n\x08selector\x18\x03 \x01(\t\x12\x0c\n\x04text\x18\x04 \x01(\t\x12\x12\n\nsession_id\x18\x05 \x01(\t\x12\t\n\x01x\x18\x06 \x01(\x05\x12\t\n\x01y\x18\x07 \x01(\x05\"\x86\x01\n\nActionType\x12\x0c\n\x08NAVIGATE\x10\x00\x12\t\n\x05\x43LICK\x10\x01\x12\x08\n\x04TYPE\x10\x02\x12\x0e\n\nSCREENSHOT\x10\x03\x12\x0b\n\x07GET_DOM\x10\x04\x12\t\n\x05HOVER\x10\x05\x12\n\n\x06SCROLL\x10\x06\x12\t\n\x05\x43LOSE\x10\x07\x12\x08\n\x04\x45VAL\x10\x08\x12\x0c\n\x08GET_A11Y\x10\t\"\xe0\x02\n\x0cTaskResponse\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12*\n\x06status\x18\x02 \x01(\x0e\x32\x1a.agent.TaskResponse.Status\x12\x0e\n\x06stdout\x18\x03 \x01(\t\x12\x0e\n\x06stderr\x18\x04 \x01(\t\x12\x10\n\x08trace_id\x18\x05 \x01(\t\x12\x35\n\tartifacts\x18\x06 \x03(\x0b\x32\".agent.TaskResponse.ArtifactsEntry\x12\x30\n\x0e\x62rowser_result\x18\x07 \x01(\x0b\x32\x16.agent.BrowserResponseH\x00\x1a\x30\n\x0e\x41rtifactsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"<\n\x06Status\x12\x0b\n\x07SUCCESS\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x0b\n\x07TIMEOUT\x10\x02\x12\r\n\tCANCELLED\x10\x03\x42\x08\n\x06result\"\xdc\x01\n\x0f\x42rowserResponse\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x10\n\x08snapshot\x18\x03 \x01(\x0c\x12\x13\n\x0b\x64om_content\x18\x04 \x01(\t\x12\x11\n\ta11y_tree\x18\x05 \x01(\t\x12\x13\n\x0b\x65val_result\x18\x06 \x01(\t\x12.\n\x0f\x63onsole_history\x18\x07 \x03(\x0b\x32\x15.agent.ConsoleMessage\x12.\n\x0fnetwork_history\x18\x08 \x03(\x0b\x32\x15.agent.NetworkRequest\"C\n\x0e\x43onsoleMessage\x12\r\n\x05level\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x02 \x01(\t\x12\x14\n\x0ctimestamp_ms\x18\x03 \x01(\x03\"h\n\x0eNetworkRequest\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x05\x12\x15\n\rresource_type\x18\x04 \x01(\t\x12\x12\n\nlatency_ms\x18\x05 \x01(\x03\",\n\x0eWorkPoolUpdate\x12\x1a\n\x12\x61vailable_task_ids\x18\x01 \x03(\t\"4\n\x10TaskClaimRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07node_id\x18\x02 \x01(\t\"E\n\x11TaskClaimResponse\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07granted\x18\x02 \x01(\x08\x12\x0e\n\x06reason\x18\x03 \x01(\t\"\xc1\x01\n\tHeartbeat\x12\x0f\n\x07node_id\x18\x01 \x01(\t\x12\x19\n\x11\x63pu_usage_percent\x18\x02 \x01(\x02\x12\x1c\n\x14memory_usage_percent\x18\x03 \x01(\x02\x12\x1b\n\x13\x61\x63tive_worker_count\x18\x04 \x01(\x05\x12\x1b\n\x13max_worker_capacity\x18\x05 \x01(\x05\x12\x16\n\x0estatus_message\x18\x06 \x01(\t\x12\x18\n\x10running_task_ids\x18\x07 \x03(\t\"-\n\x13HealthCheckResponse\x12\x16\n\x0eserver_time_ms\x18\x01 \x01(\x03\"\xd3\x01\n\x0f\x46ileSyncMessage\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12,\n\x08manifest\x18\x02 \x01(\x0b\x32\x18.agent.DirectoryManifestH\x00\x12\'\n\tfile_data\x18\x03 \x01(\x0b\x32\x12.agent.FilePayloadH\x00\x12#\n\x06status\x18\x04 \x01(\x0b\x32\x11.agent.SyncStatusH\x00\x12%\n\x07\x63ontrol\x18\x05 \x01(\x0b\x32\x12.agent.SyncControlH\x00\x42\t\n\x07payload\"\xc6\x01\n\x0bSyncControl\x12)\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x19.agent.SyncControl.Action\x12\x0c\n\x04path\x18\x02 \x01(\t\x12\x15\n\rrequest_paths\x18\x03 \x03(\t\"g\n\x06\x41\x63tion\x12\x12\n\x0eSTART_WATCHING\x10\x00\x12\x11\n\rSTOP_WATCHING\x10\x01\x12\x08\n\x04LOCK\x10\x02\x12\n\n\x06UNLOCK\x10\x03\x12\x14\n\x10REFRESH_MANIFEST\x10\x04\x12\n\n\x06RESYNC\x10\x05\"F\n\x11\x44irectoryManifest\x12\x11\n\troot_path\x18\x01 \x01(\t\x12\x1e\n\x05\x66iles\x18\x02 \x03(\x0b\x32\x0f.agent.FileInfo\"D\n\x08\x46ileInfo\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\x03\x12\x0c\n\x04hash\x18\x03 \x01(\t\x12\x0e\n\x06is_dir\x18\x04 \x01(\x08\"_\n\x0b\x46ilePayload\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\r\n\x05\x63hunk\x18\x02 \x01(\x0c\x12\x13\n\x0b\x63hunk_index\x18\x03 \x01(\x05\x12\x10\n\x08is_final\x18\x04 \x01(\x08\x12\x0c\n\x04hash\x18\x05 \x01(\t\"\xa0\x01\n\nSyncStatus\x12$\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x16.agent.SyncStatus.Code\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x17\n\x0freconcile_paths\x18\x03 \x03(\t\"B\n\x04\x43ode\x12\x06\n\x02OK\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x16\n\x12RECONCILE_REQUIRED\x10\x02\x12\x0f\n\x0bIN_PROGRESS\x10\x03\x32\xe9\x01\n\x11\x41gentOrchestrator\x12L\n\x11SyncConfiguration\x12\x1a.agent.RegistrationRequest\x1a\x1b.agent.RegistrationResponse\x12\x44\n\nTaskStream\x12\x18.agent.ClientTaskMessage\x1a\x18.agent.ServerTaskMessage(\x01\x30\x01\x12@\n\x0cReportHealth\x12\x10.agent.Heartbeat\x1a\x1a.agent.HealthCheckResponse(\x01\x30\x01\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'agent_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None - _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._options = None - _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._serialized_options = b'8\001' - _globals['_TASKRESPONSE_ARTIFACTSENTRY']._options = None - _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_options = b'8\001' - _globals['_REGISTRATIONREQUEST']._serialized_start=23 - _globals['_REGISTRATIONREQUEST']._serialized_end=245 - _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._serialized_start=194 - _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._serialized_end=245 - _globals['_SANDBOXPOLICY']._serialized_start=248 - _globals['_SANDBOXPOLICY']._serialized_end=445 - _globals['_SANDBOXPOLICY_MODE']._serialized_start=411 - _globals['_SANDBOXPOLICY_MODE']._serialized_end=445 - _globals['_REGISTRATIONRESPONSE']._serialized_start=447 - _globals['_REGISTRATIONRESPONSE']._serialized_end=567 - _globals['_CLIENTTASKMESSAGE']._serialized_start=570 - _globals['_CLIENTTASKMESSAGE']._serialized_end=825 - _globals['_NODEANNOUNCE']._serialized_start=827 - _globals['_NODEANNOUNCE']._serialized_end=858 - _globals['_BROWSEREVENT']._serialized_start=861 - _globals['_BROWSEREVENT']._serialized_end=996 - _globals['_SERVERTASKMESSAGE']._serialized_start=999 - _globals['_SERVERTASKMESSAGE']._serialized_end=1268 - _globals['_TASKCANCELREQUEST']._serialized_start=1270 - _globals['_TASKCANCELREQUEST']._serialized_end=1306 - _globals['_TASKREQUEST']._serialized_start=1309 - _globals['_TASKREQUEST']._serialized_end=1518 - _globals['_BROWSERACTION']._serialized_start=1521 - _globals['_BROWSERACTION']._serialized_end=1809 - _globals['_BROWSERACTION_ACTIONTYPE']._serialized_start=1675 - _globals['_BROWSERACTION_ACTIONTYPE']._serialized_end=1809 - _globals['_TASKRESPONSE']._serialized_start=1812 - _globals['_TASKRESPONSE']._serialized_end=2164 - _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_start=2044 - _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_end=2092 - _globals['_TASKRESPONSE_STATUS']._serialized_start=2094 - _globals['_TASKRESPONSE_STATUS']._serialized_end=2154 - _globals['_BROWSERRESPONSE']._serialized_start=2167 - _globals['_BROWSERRESPONSE']._serialized_end=2387 - _globals['_CONSOLEMESSAGE']._serialized_start=2389 - _globals['_CONSOLEMESSAGE']._serialized_end=2456 - _globals['_NETWORKREQUEST']._serialized_start=2458 - _globals['_NETWORKREQUEST']._serialized_end=2562 - _globals['_WORKPOOLUPDATE']._serialized_start=2564 - _globals['_WORKPOOLUPDATE']._serialized_end=2608 - _globals['_TASKCLAIMREQUEST']._serialized_start=2610 - _globals['_TASKCLAIMREQUEST']._serialized_end=2662 - _globals['_TASKCLAIMRESPONSE']._serialized_start=2664 - _globals['_TASKCLAIMRESPONSE']._serialized_end=2733 - _globals['_HEARTBEAT']._serialized_start=2736 - _globals['_HEARTBEAT']._serialized_end=2929 - _globals['_HEALTHCHECKRESPONSE']._serialized_start=2931 - _globals['_HEALTHCHECKRESPONSE']._serialized_end=2976 - _globals['_FILESYNCMESSAGE']._serialized_start=2979 - _globals['_FILESYNCMESSAGE']._serialized_end=3190 - _globals['_SYNCCONTROL']._serialized_start=3193 - _globals['_SYNCCONTROL']._serialized_end=3391 - _globals['_SYNCCONTROL_ACTION']._serialized_start=3288 - _globals['_SYNCCONTROL_ACTION']._serialized_end=3391 - _globals['_DIRECTORYMANIFEST']._serialized_start=3393 - _globals['_DIRECTORYMANIFEST']._serialized_end=3463 - _globals['_FILEINFO']._serialized_start=3465 - _globals['_FILEINFO']._serialized_end=3533 - _globals['_FILEPAYLOAD']._serialized_start=3535 - _globals['_FILEPAYLOAD']._serialized_end=3630 - _globals['_SYNCSTATUS']._serialized_start=3633 - _globals['_SYNCSTATUS']._serialized_end=3793 - _globals['_SYNCSTATUS_CODE']._serialized_start=3727 - _globals['_SYNCSTATUS_CODE']._serialized_end=3793 - _globals['_AGENTORCHESTRATOR']._serialized_start=3796 - _globals['_AGENTORCHESTRATOR']._serialized_end=4029 -# @@protoc_insertion_point(module_scope) diff --git a/agent-node/agent_pb2_grpc.py b/agent-node/agent_pb2_grpc.py deleted file mode 100644 index 932d45e..0000000 --- a/agent-node/agent_pb2_grpc.py +++ /dev/null @@ -1,138 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc - -import agent_pb2 as agent__pb2 - - -class AgentOrchestratorStub(object): - """The Cortex Server exposes this service - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.SyncConfiguration = channel.unary_unary( - '/agent.AgentOrchestrator/SyncConfiguration', - request_serializer=agent__pb2.RegistrationRequest.SerializeToString, - response_deserializer=agent__pb2.RegistrationResponse.FromString, - ) - self.TaskStream = channel.stream_stream( - '/agent.AgentOrchestrator/TaskStream', - request_serializer=agent__pb2.ClientTaskMessage.SerializeToString, - response_deserializer=agent__pb2.ServerTaskMessage.FromString, - ) - self.ReportHealth = channel.stream_stream( - '/agent.AgentOrchestrator/ReportHealth', - request_serializer=agent__pb2.Heartbeat.SerializeToString, - response_deserializer=agent__pb2.HealthCheckResponse.FromString, - ) - - -class AgentOrchestratorServicer(object): - """The Cortex Server exposes this service - """ - - def SyncConfiguration(self, request, context): - """1. Control Channel: Sync policies and settings (Unary) - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def TaskStream(self, request_iterator, context): - """2. Task Channel: Bidirectional work dispatch and reporting (Persistent) - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def ReportHealth(self, request_iterator, context): - """3. Health Channel: Dedicated Ping-Pong / Heartbeat (Persistent) - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_AgentOrchestratorServicer_to_server(servicer, server): - rpc_method_handlers = { - 'SyncConfiguration': grpc.unary_unary_rpc_method_handler( - servicer.SyncConfiguration, - request_deserializer=agent__pb2.RegistrationRequest.FromString, - response_serializer=agent__pb2.RegistrationResponse.SerializeToString, - ), - 'TaskStream': grpc.stream_stream_rpc_method_handler( - servicer.TaskStream, - request_deserializer=agent__pb2.ClientTaskMessage.FromString, - response_serializer=agent__pb2.ServerTaskMessage.SerializeToString, - ), - 'ReportHealth': grpc.stream_stream_rpc_method_handler( - servicer.ReportHealth, - request_deserializer=agent__pb2.Heartbeat.FromString, - response_serializer=agent__pb2.HealthCheckResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'agent.AgentOrchestrator', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - - - # This class is part of an EXPERIMENTAL API. -class AgentOrchestrator(object): - """The Cortex Server exposes this service - """ - - @staticmethod - def SyncConfiguration(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary(request, target, '/agent.AgentOrchestrator/SyncConfiguration', - agent__pb2.RegistrationRequest.SerializeToString, - agent__pb2.RegistrationResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) - - @staticmethod - def TaskStream(request_iterator, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.stream_stream(request_iterator, target, '/agent.AgentOrchestrator/TaskStream', - agent__pb2.ClientTaskMessage.SerializeToString, - agent__pb2.ServerTaskMessage.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) - - @staticmethod - def ReportHealth(request_iterator, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.stream_stream(request_iterator, target, '/agent.AgentOrchestrator/ReportHealth', - agent__pb2.Heartbeat.SerializeToString, - agent__pb2.HealthCheckResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/agent-node/docker-compose.yml b/agent-node/docker-compose.yml new file mode 100644 index 0000000..d519192 --- /dev/null +++ b/agent-node/docker-compose.yml @@ -0,0 +1,15 @@ +# agent-node/docker-compose.yml +services: + agent-node: + build: . + container_name: cortex-local-agent + environment: + - AGENT_NODE_ID=${AGENT_NODE_ID:-test-prod-node} + - AGENT_NODE_DESC=${AGENT_NODE_DESC:-Modular Stateful Node} + - GRPC_ENDPOINT=${GRPC_ENDPOINT:-192.168.68.113:50051} + - AGENT_SECRET_KEY=${AGENT_SECRET_KEY:-aYc2j1lYUUZXkBFFUndnleZI} + - AGENT_TLS_ENABLED=${AGENT_TLS_ENABLED:-false} + - CORTEX_SYNC_DIR=/app/sync + volumes: + - ./sync:/app/sync + restart: unless-stopped diff --git a/agent-node/protos/agent.proto b/agent-node/protos/agent.proto index e751062..e01e322 100644 --- a/agent-node/protos/agent.proto +++ b/agent-node/protos/agent.proto @@ -50,6 +50,17 @@ BrowserEvent browser_event = 3; NodeAnnounce announce = 4; // NEW: Identification on stream connect FileSyncMessage file_sync = 5; // NEW: Ghost Mirror Sync + SkillEvent skill_event = 6; // NEW: Persistent real-time skill data + } +} + +message SkillEvent { + string session_id = 1; + string task_id = 2; + oneof data { + string terminal_out = 3; // Raw stdout/stderr chunks + string prompt = 4; // Interactive prompt (like password) + bool keep_alive = 5; // Session preservation } } diff --git a/agent-node/protos/agent_pb2.py b/agent-node/protos/agent_pb2.py index 3472d01..3b32354 100644 --- a/agent-node/protos/agent_pb2.py +++ b/agent-node/protos/agent_pb2.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! -# source: protos/agent.proto +# source: agent.proto # Protobuf Python Version: 4.25.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor @@ -14,81 +14,83 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12protos/agent.proto\x12\x05\x61gent\"\xde\x01\n\x13RegistrationRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x12\n\nauth_token\x18\x03 \x01(\t\x12\x18\n\x10node_description\x18\x04 \x01(\t\x12\x42\n\x0c\x63\x61pabilities\x18\x05 \x03(\x0b\x32,.agent.RegistrationRequest.CapabilitiesEntry\x1a\x33\n\x11\x43\x61pabilitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc5\x01\n\rSandboxPolicy\x12\'\n\x04mode\x18\x01 \x01(\x0e\x32\x19.agent.SandboxPolicy.Mode\x12\x18\n\x10\x61llowed_commands\x18\x02 \x03(\t\x12\x17\n\x0f\x64\x65nied_commands\x18\x03 \x03(\t\x12\x1a\n\x12sensitive_commands\x18\x04 \x03(\t\x12\x18\n\x10working_dir_jail\x18\x05 \x01(\t\"\"\n\x04Mode\x12\n\n\x06STRICT\x10\x00\x12\x0e\n\nPERMISSIVE\x10\x01\"x\n\x14RegistrationResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12\x12\n\nsession_id\x18\x03 \x01(\t\x12$\n\x06policy\x18\x04 \x01(\x0b\x32\x14.agent.SandboxPolicy\"\xff\x01\n\x11\x43lientTaskMessage\x12,\n\rtask_response\x18\x01 \x01(\x0b\x32\x13.agent.TaskResponseH\x00\x12-\n\ntask_claim\x18\x02 \x01(\x0b\x32\x17.agent.TaskClaimRequestH\x00\x12,\n\rbrowser_event\x18\x03 \x01(\x0b\x32\x13.agent.BrowserEventH\x00\x12\'\n\x08\x61nnounce\x18\x04 \x01(\x0b\x32\x13.agent.NodeAnnounceH\x00\x12+\n\tfile_sync\x18\x05 \x01(\x0b\x32\x16.agent.FileSyncMessageH\x00\x42\t\n\x07payload\"\x1f\n\x0cNodeAnnounce\x12\x0f\n\x07node_id\x18\x01 \x01(\t\"\x87\x01\n\x0c\x42rowserEvent\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12,\n\x0b\x63onsole_msg\x18\x02 \x01(\x0b\x32\x15.agent.ConsoleMessageH\x00\x12,\n\x0bnetwork_req\x18\x03 \x01(\x0b\x32\x15.agent.NetworkRequestH\x00\x42\x07\n\x05\x65vent\"\x8d\x02\n\x11ServerTaskMessage\x12*\n\x0ctask_request\x18\x01 \x01(\x0b\x32\x12.agent.TaskRequestH\x00\x12\x31\n\x10work_pool_update\x18\x02 \x01(\x0b\x32\x15.agent.WorkPoolUpdateH\x00\x12\x30\n\x0c\x63laim_status\x18\x03 \x01(\x0b\x32\x18.agent.TaskClaimResponseH\x00\x12/\n\x0btask_cancel\x18\x04 \x01(\x0b\x32\x18.agent.TaskCancelRequestH\x00\x12+\n\tfile_sync\x18\x05 \x01(\x0b\x32\x16.agent.FileSyncMessageH\x00\x42\t\n\x07payload\"$\n\x11TaskCancelRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\"\xd1\x01\n\x0bTaskRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x11\n\ttask_type\x18\x02 \x01(\t\x12\x16\n\x0cpayload_json\x18\x03 \x01(\tH\x00\x12.\n\x0e\x62rowser_action\x18\x07 \x01(\x0b\x32\x14.agent.BrowserActionH\x00\x12\x12\n\ntimeout_ms\x18\x04 \x01(\x05\x12\x10\n\x08trace_id\x18\x05 \x01(\t\x12\x11\n\tsignature\x18\x06 \x01(\t\x12\x12\n\nsession_id\x18\x08 \x01(\tB\t\n\x07payload\"\xa0\x02\n\rBrowserAction\x12/\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x1f.agent.BrowserAction.ActionType\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x10\n\x08selector\x18\x03 \x01(\t\x12\x0c\n\x04text\x18\x04 \x01(\t\x12\x12\n\nsession_id\x18\x05 \x01(\t\x12\t\n\x01x\x18\x06 \x01(\x05\x12\t\n\x01y\x18\x07 \x01(\x05\"\x86\x01\n\nActionType\x12\x0c\n\x08NAVIGATE\x10\x00\x12\t\n\x05\x43LICK\x10\x01\x12\x08\n\x04TYPE\x10\x02\x12\x0e\n\nSCREENSHOT\x10\x03\x12\x0b\n\x07GET_DOM\x10\x04\x12\t\n\x05HOVER\x10\x05\x12\n\n\x06SCROLL\x10\x06\x12\t\n\x05\x43LOSE\x10\x07\x12\x08\n\x04\x45VAL\x10\x08\x12\x0c\n\x08GET_A11Y\x10\t\"\xe0\x02\n\x0cTaskResponse\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12*\n\x06status\x18\x02 \x01(\x0e\x32\x1a.agent.TaskResponse.Status\x12\x0e\n\x06stdout\x18\x03 \x01(\t\x12\x0e\n\x06stderr\x18\x04 \x01(\t\x12\x10\n\x08trace_id\x18\x05 \x01(\t\x12\x35\n\tartifacts\x18\x06 \x03(\x0b\x32\".agent.TaskResponse.ArtifactsEntry\x12\x30\n\x0e\x62rowser_result\x18\x07 \x01(\x0b\x32\x16.agent.BrowserResponseH\x00\x1a\x30\n\x0e\x41rtifactsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"<\n\x06Status\x12\x0b\n\x07SUCCESS\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x0b\n\x07TIMEOUT\x10\x02\x12\r\n\tCANCELLED\x10\x03\x42\x08\n\x06result\"\xdc\x01\n\x0f\x42rowserResponse\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x10\n\x08snapshot\x18\x03 \x01(\x0c\x12\x13\n\x0b\x64om_content\x18\x04 \x01(\t\x12\x11\n\ta11y_tree\x18\x05 \x01(\t\x12\x13\n\x0b\x65val_result\x18\x06 \x01(\t\x12.\n\x0f\x63onsole_history\x18\x07 \x03(\x0b\x32\x15.agent.ConsoleMessage\x12.\n\x0fnetwork_history\x18\x08 \x03(\x0b\x32\x15.agent.NetworkRequest\"C\n\x0e\x43onsoleMessage\x12\r\n\x05level\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x02 \x01(\t\x12\x14\n\x0ctimestamp_ms\x18\x03 \x01(\x03\"h\n\x0eNetworkRequest\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x05\x12\x15\n\rresource_type\x18\x04 \x01(\t\x12\x12\n\nlatency_ms\x18\x05 \x01(\x03\",\n\x0eWorkPoolUpdate\x12\x1a\n\x12\x61vailable_task_ids\x18\x01 \x03(\t\"4\n\x10TaskClaimRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07node_id\x18\x02 \x01(\t\"E\n\x11TaskClaimResponse\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07granted\x18\x02 \x01(\x08\x12\x0e\n\x06reason\x18\x03 \x01(\t\"\xc1\x01\n\tHeartbeat\x12\x0f\n\x07node_id\x18\x01 \x01(\t\x12\x19\n\x11\x63pu_usage_percent\x18\x02 \x01(\x02\x12\x1c\n\x14memory_usage_percent\x18\x03 \x01(\x02\x12\x1b\n\x13\x61\x63tive_worker_count\x18\x04 \x01(\x05\x12\x1b\n\x13max_worker_capacity\x18\x05 \x01(\x05\x12\x16\n\x0estatus_message\x18\x06 \x01(\t\x12\x18\n\x10running_task_ids\x18\x07 \x03(\t\"-\n\x13HealthCheckResponse\x12\x16\n\x0eserver_time_ms\x18\x01 \x01(\x03\"\xd3\x01\n\x0f\x46ileSyncMessage\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12,\n\x08manifest\x18\x02 \x01(\x0b\x32\x18.agent.DirectoryManifestH\x00\x12\'\n\tfile_data\x18\x03 \x01(\x0b\x32\x12.agent.FilePayloadH\x00\x12#\n\x06status\x18\x04 \x01(\x0b\x32\x11.agent.SyncStatusH\x00\x12%\n\x07\x63ontrol\x18\x05 \x01(\x0b\x32\x12.agent.SyncControlH\x00\x42\t\n\x07payload\"\xaf\x01\n\x0bSyncControl\x12)\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x19.agent.SyncControl.Action\x12\x0c\n\x04path\x18\x02 \x01(\t\"g\n\x06\x41\x63tion\x12\x12\n\x0eSTART_WATCHING\x10\x00\x12\x11\n\rSTOP_WATCHING\x10\x01\x12\x08\n\x04LOCK\x10\x02\x12\n\n\x06UNLOCK\x10\x03\x12\x14\n\x10REFRESH_MANIFEST\x10\x04\x12\n\n\x06RESYNC\x10\x05\"F\n\x11\x44irectoryManifest\x12\x11\n\troot_path\x18\x01 \x01(\t\x12\x1e\n\x05\x66iles\x18\x02 \x03(\x0b\x32\x0f.agent.FileInfo\"D\n\x08\x46ileInfo\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\x03\x12\x0c\n\x04hash\x18\x03 \x01(\t\x12\x0e\n\x06is_dir\x18\x04 \x01(\x08\"_\n\x0b\x46ilePayload\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\r\n\x05\x63hunk\x18\x02 \x01(\x0c\x12\x13\n\x0b\x63hunk_index\x18\x03 \x01(\x05\x12\x10\n\x08is_final\x18\x04 \x01(\x08\x12\x0c\n\x04hash\x18\x05 \x01(\t\"\xa0\x01\n\nSyncStatus\x12$\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x16.agent.SyncStatus.Code\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x17\n\x0freconcile_paths\x18\x03 \x03(\t\"B\n\x04\x43ode\x12\x06\n\x02OK\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x16\n\x12RECONCILE_REQUIRED\x10\x02\x12\x0f\n\x0bIN_PROGRESS\x10\x03\x32\xe9\x01\n\x11\x41gentOrchestrator\x12L\n\x11SyncConfiguration\x12\x1a.agent.RegistrationRequest\x1a\x1b.agent.RegistrationResponse\x12\x44\n\nTaskStream\x12\x18.agent.ClientTaskMessage\x1a\x18.agent.ServerTaskMessage(\x01\x30\x01\x12@\n\x0cReportHealth\x12\x10.agent.Heartbeat\x1a\x1a.agent.HealthCheckResponse(\x01\x30\x01\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x61gent.proto\x12\x05\x61gent\"\xde\x01\n\x13RegistrationRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x12\n\nauth_token\x18\x03 \x01(\t\x12\x18\n\x10node_description\x18\x04 \x01(\t\x12\x42\n\x0c\x63\x61pabilities\x18\x05 \x03(\x0b\x32,.agent.RegistrationRequest.CapabilitiesEntry\x1a\x33\n\x11\x43\x61pabilitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc5\x01\n\rSandboxPolicy\x12\'\n\x04mode\x18\x01 \x01(\x0e\x32\x19.agent.SandboxPolicy.Mode\x12\x18\n\x10\x61llowed_commands\x18\x02 \x03(\t\x12\x17\n\x0f\x64\x65nied_commands\x18\x03 \x03(\t\x12\x1a\n\x12sensitive_commands\x18\x04 \x03(\t\x12\x18\n\x10working_dir_jail\x18\x05 \x01(\t\"\"\n\x04Mode\x12\n\n\x06STRICT\x10\x00\x12\x0e\n\nPERMISSIVE\x10\x01\"x\n\x14RegistrationResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12\x12\n\nsession_id\x18\x03 \x01(\t\x12$\n\x06policy\x18\x04 \x01(\x0b\x32\x14.agent.SandboxPolicy\"\xa9\x02\n\x11\x43lientTaskMessage\x12,\n\rtask_response\x18\x01 \x01(\x0b\x32\x13.agent.TaskResponseH\x00\x12-\n\ntask_claim\x18\x02 \x01(\x0b\x32\x17.agent.TaskClaimRequestH\x00\x12,\n\rbrowser_event\x18\x03 \x01(\x0b\x32\x13.agent.BrowserEventH\x00\x12\'\n\x08\x61nnounce\x18\x04 \x01(\x0b\x32\x13.agent.NodeAnnounceH\x00\x12+\n\tfile_sync\x18\x05 \x01(\x0b\x32\x16.agent.FileSyncMessageH\x00\x12(\n\x0bskill_event\x18\x06 \x01(\x0b\x32\x11.agent.SkillEventH\x00\x42\t\n\x07payload\"y\n\nSkillEvent\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0f\n\x07task_id\x18\x02 \x01(\t\x12\x16\n\x0cterminal_out\x18\x03 \x01(\tH\x00\x12\x10\n\x06prompt\x18\x04 \x01(\tH\x00\x12\x14\n\nkeep_alive\x18\x05 \x01(\x08H\x00\x42\x06\n\x04\x64\x61ta\"\x1f\n\x0cNodeAnnounce\x12\x0f\n\x07node_id\x18\x01 \x01(\t\"\x87\x01\n\x0c\x42rowserEvent\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12,\n\x0b\x63onsole_msg\x18\x02 \x01(\x0b\x32\x15.agent.ConsoleMessageH\x00\x12,\n\x0bnetwork_req\x18\x03 \x01(\x0b\x32\x15.agent.NetworkRequestH\x00\x42\x07\n\x05\x65vent\"\x8d\x02\n\x11ServerTaskMessage\x12*\n\x0ctask_request\x18\x01 \x01(\x0b\x32\x12.agent.TaskRequestH\x00\x12\x31\n\x10work_pool_update\x18\x02 \x01(\x0b\x32\x15.agent.WorkPoolUpdateH\x00\x12\x30\n\x0c\x63laim_status\x18\x03 \x01(\x0b\x32\x18.agent.TaskClaimResponseH\x00\x12/\n\x0btask_cancel\x18\x04 \x01(\x0b\x32\x18.agent.TaskCancelRequestH\x00\x12+\n\tfile_sync\x18\x05 \x01(\x0b\x32\x16.agent.FileSyncMessageH\x00\x42\t\n\x07payload\"$\n\x11TaskCancelRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\"\xd1\x01\n\x0bTaskRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x11\n\ttask_type\x18\x02 \x01(\t\x12\x16\n\x0cpayload_json\x18\x03 \x01(\tH\x00\x12.\n\x0e\x62rowser_action\x18\x07 \x01(\x0b\x32\x14.agent.BrowserActionH\x00\x12\x12\n\ntimeout_ms\x18\x04 \x01(\x05\x12\x10\n\x08trace_id\x18\x05 \x01(\t\x12\x11\n\tsignature\x18\x06 \x01(\t\x12\x12\n\nsession_id\x18\x08 \x01(\tB\t\n\x07payload\"\xa0\x02\n\rBrowserAction\x12/\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x1f.agent.BrowserAction.ActionType\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x10\n\x08selector\x18\x03 \x01(\t\x12\x0c\n\x04text\x18\x04 \x01(\t\x12\x12\n\nsession_id\x18\x05 \x01(\t\x12\t\n\x01x\x18\x06 \x01(\x05\x12\t\n\x01y\x18\x07 \x01(\x05\"\x86\x01\n\nActionType\x12\x0c\n\x08NAVIGATE\x10\x00\x12\t\n\x05\x43LICK\x10\x01\x12\x08\n\x04TYPE\x10\x02\x12\x0e\n\nSCREENSHOT\x10\x03\x12\x0b\n\x07GET_DOM\x10\x04\x12\t\n\x05HOVER\x10\x05\x12\n\n\x06SCROLL\x10\x06\x12\t\n\x05\x43LOSE\x10\x07\x12\x08\n\x04\x45VAL\x10\x08\x12\x0c\n\x08GET_A11Y\x10\t\"\xe0\x02\n\x0cTaskResponse\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12*\n\x06status\x18\x02 \x01(\x0e\x32\x1a.agent.TaskResponse.Status\x12\x0e\n\x06stdout\x18\x03 \x01(\t\x12\x0e\n\x06stderr\x18\x04 \x01(\t\x12\x10\n\x08trace_id\x18\x05 \x01(\t\x12\x35\n\tartifacts\x18\x06 \x03(\x0b\x32\".agent.TaskResponse.ArtifactsEntry\x12\x30\n\x0e\x62rowser_result\x18\x07 \x01(\x0b\x32\x16.agent.BrowserResponseH\x00\x1a\x30\n\x0e\x41rtifactsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"<\n\x06Status\x12\x0b\n\x07SUCCESS\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x0b\n\x07TIMEOUT\x10\x02\x12\r\n\tCANCELLED\x10\x03\x42\x08\n\x06result\"\xdc\x01\n\x0f\x42rowserResponse\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x10\n\x08snapshot\x18\x03 \x01(\x0c\x12\x13\n\x0b\x64om_content\x18\x04 \x01(\t\x12\x11\n\ta11y_tree\x18\x05 \x01(\t\x12\x13\n\x0b\x65val_result\x18\x06 \x01(\t\x12.\n\x0f\x63onsole_history\x18\x07 \x03(\x0b\x32\x15.agent.ConsoleMessage\x12.\n\x0fnetwork_history\x18\x08 \x03(\x0b\x32\x15.agent.NetworkRequest\"C\n\x0e\x43onsoleMessage\x12\r\n\x05level\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x02 \x01(\t\x12\x14\n\x0ctimestamp_ms\x18\x03 \x01(\x03\"h\n\x0eNetworkRequest\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x05\x12\x15\n\rresource_type\x18\x04 \x01(\t\x12\x12\n\nlatency_ms\x18\x05 \x01(\x03\",\n\x0eWorkPoolUpdate\x12\x1a\n\x12\x61vailable_task_ids\x18\x01 \x03(\t\"4\n\x10TaskClaimRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07node_id\x18\x02 \x01(\t\"E\n\x11TaskClaimResponse\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07granted\x18\x02 \x01(\x08\x12\x0e\n\x06reason\x18\x03 \x01(\t\"\xc1\x01\n\tHeartbeat\x12\x0f\n\x07node_id\x18\x01 \x01(\t\x12\x19\n\x11\x63pu_usage_percent\x18\x02 \x01(\x02\x12\x1c\n\x14memory_usage_percent\x18\x03 \x01(\x02\x12\x1b\n\x13\x61\x63tive_worker_count\x18\x04 \x01(\x05\x12\x1b\n\x13max_worker_capacity\x18\x05 \x01(\x05\x12\x16\n\x0estatus_message\x18\x06 \x01(\t\x12\x18\n\x10running_task_ids\x18\x07 \x03(\t\"-\n\x13HealthCheckResponse\x12\x16\n\x0eserver_time_ms\x18\x01 \x01(\x03\"\xd3\x01\n\x0f\x46ileSyncMessage\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12,\n\x08manifest\x18\x02 \x01(\x0b\x32\x18.agent.DirectoryManifestH\x00\x12\'\n\tfile_data\x18\x03 \x01(\x0b\x32\x12.agent.FilePayloadH\x00\x12#\n\x06status\x18\x04 \x01(\x0b\x32\x11.agent.SyncStatusH\x00\x12%\n\x07\x63ontrol\x18\x05 \x01(\x0b\x32\x12.agent.SyncControlH\x00\x42\t\n\x07payload\"\xc6\x01\n\x0bSyncControl\x12)\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x19.agent.SyncControl.Action\x12\x0c\n\x04path\x18\x02 \x01(\t\x12\x15\n\rrequest_paths\x18\x03 \x03(\t\"g\n\x06\x41\x63tion\x12\x12\n\x0eSTART_WATCHING\x10\x00\x12\x11\n\rSTOP_WATCHING\x10\x01\x12\x08\n\x04LOCK\x10\x02\x12\n\n\x06UNLOCK\x10\x03\x12\x14\n\x10REFRESH_MANIFEST\x10\x04\x12\n\n\x06RESYNC\x10\x05\"F\n\x11\x44irectoryManifest\x12\x11\n\troot_path\x18\x01 \x01(\t\x12\x1e\n\x05\x66iles\x18\x02 \x03(\x0b\x32\x0f.agent.FileInfo\"D\n\x08\x46ileInfo\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\x03\x12\x0c\n\x04hash\x18\x03 \x01(\t\x12\x0e\n\x06is_dir\x18\x04 \x01(\x08\"_\n\x0b\x46ilePayload\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\r\n\x05\x63hunk\x18\x02 \x01(\x0c\x12\x13\n\x0b\x63hunk_index\x18\x03 \x01(\x05\x12\x10\n\x08is_final\x18\x04 \x01(\x08\x12\x0c\n\x04hash\x18\x05 \x01(\t\"\xa0\x01\n\nSyncStatus\x12$\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x16.agent.SyncStatus.Code\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x17\n\x0freconcile_paths\x18\x03 \x03(\t\"B\n\x04\x43ode\x12\x06\n\x02OK\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x16\n\x12RECONCILE_REQUIRED\x10\x02\x12\x0f\n\x0bIN_PROGRESS\x10\x03\x32\xe9\x01\n\x11\x41gentOrchestrator\x12L\n\x11SyncConfiguration\x12\x1a.agent.RegistrationRequest\x1a\x1b.agent.RegistrationResponse\x12\x44\n\nTaskStream\x12\x18.agent.ClientTaskMessage\x1a\x18.agent.ServerTaskMessage(\x01\x30\x01\x12@\n\x0cReportHealth\x12\x10.agent.Heartbeat\x1a\x1a.agent.HealthCheckResponse(\x01\x30\x01\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'protos.agent_pb2', _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'agent_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._options = None _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._serialized_options = b'8\001' _globals['_TASKRESPONSE_ARTIFACTSENTRY']._options = None _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_options = b'8\001' - _globals['_REGISTRATIONREQUEST']._serialized_start=30 - _globals['_REGISTRATIONREQUEST']._serialized_end=252 - _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._serialized_start=201 - _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._serialized_end=252 - _globals['_SANDBOXPOLICY']._serialized_start=255 - _globals['_SANDBOXPOLICY']._serialized_end=452 - _globals['_SANDBOXPOLICY_MODE']._serialized_start=418 - _globals['_SANDBOXPOLICY_MODE']._serialized_end=452 - _globals['_REGISTRATIONRESPONSE']._serialized_start=454 - _globals['_REGISTRATIONRESPONSE']._serialized_end=574 - _globals['_CLIENTTASKMESSAGE']._serialized_start=577 - _globals['_CLIENTTASKMESSAGE']._serialized_end=832 - _globals['_NODEANNOUNCE']._serialized_start=834 - _globals['_NODEANNOUNCE']._serialized_end=865 - _globals['_BROWSEREVENT']._serialized_start=868 - _globals['_BROWSEREVENT']._serialized_end=1003 - _globals['_SERVERTASKMESSAGE']._serialized_start=1006 - _globals['_SERVERTASKMESSAGE']._serialized_end=1275 - _globals['_TASKCANCELREQUEST']._serialized_start=1277 - _globals['_TASKCANCELREQUEST']._serialized_end=1313 - _globals['_TASKREQUEST']._serialized_start=1316 - _globals['_TASKREQUEST']._serialized_end=1525 - _globals['_BROWSERACTION']._serialized_start=1528 - _globals['_BROWSERACTION']._serialized_end=1816 - _globals['_BROWSERACTION_ACTIONTYPE']._serialized_start=1682 - _globals['_BROWSERACTION_ACTIONTYPE']._serialized_end=1816 - _globals['_TASKRESPONSE']._serialized_start=1819 - _globals['_TASKRESPONSE']._serialized_end=2171 - _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_start=2051 - _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_end=2099 - _globals['_TASKRESPONSE_STATUS']._serialized_start=2101 - _globals['_TASKRESPONSE_STATUS']._serialized_end=2161 - _globals['_BROWSERRESPONSE']._serialized_start=2174 - _globals['_BROWSERRESPONSE']._serialized_end=2394 - _globals['_CONSOLEMESSAGE']._serialized_start=2396 - _globals['_CONSOLEMESSAGE']._serialized_end=2463 - _globals['_NETWORKREQUEST']._serialized_start=2465 - _globals['_NETWORKREQUEST']._serialized_end=2569 - _globals['_WORKPOOLUPDATE']._serialized_start=2571 - _globals['_WORKPOOLUPDATE']._serialized_end=2615 - _globals['_TASKCLAIMREQUEST']._serialized_start=2617 - _globals['_TASKCLAIMREQUEST']._serialized_end=2669 - _globals['_TASKCLAIMRESPONSE']._serialized_start=2671 - _globals['_TASKCLAIMRESPONSE']._serialized_end=2740 - _globals['_HEARTBEAT']._serialized_start=2743 - _globals['_HEARTBEAT']._serialized_end=2936 - _globals['_HEALTHCHECKRESPONSE']._serialized_start=2938 - _globals['_HEALTHCHECKRESPONSE']._serialized_end=2983 - _globals['_FILESYNCMESSAGE']._serialized_start=2986 - _globals['_FILESYNCMESSAGE']._serialized_end=3197 - _globals['_SYNCCONTROL']._serialized_start=3200 - _globals['_SYNCCONTROL']._serialized_end=3375 - _globals['_SYNCCONTROL_ACTION']._serialized_start=3272 - _globals['_SYNCCONTROL_ACTION']._serialized_end=3375 - _globals['_DIRECTORYMANIFEST']._serialized_start=3377 - _globals['_DIRECTORYMANIFEST']._serialized_end=3447 - _globals['_FILEINFO']._serialized_start=3449 - _globals['_FILEINFO']._serialized_end=3517 - _globals['_FILEPAYLOAD']._serialized_start=3519 - _globals['_FILEPAYLOAD']._serialized_end=3614 - _globals['_SYNCSTATUS']._serialized_start=3617 - _globals['_SYNCSTATUS']._serialized_end=3777 - _globals['_SYNCSTATUS_CODE']._serialized_start=3711 - _globals['_SYNCSTATUS_CODE']._serialized_end=3777 - _globals['_AGENTORCHESTRATOR']._serialized_start=3780 - _globals['_AGENTORCHESTRATOR']._serialized_end=4013 + _globals['_REGISTRATIONREQUEST']._serialized_start=23 + _globals['_REGISTRATIONREQUEST']._serialized_end=245 + _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._serialized_start=194 + _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._serialized_end=245 + _globals['_SANDBOXPOLICY']._serialized_start=248 + _globals['_SANDBOXPOLICY']._serialized_end=445 + _globals['_SANDBOXPOLICY_MODE']._serialized_start=411 + _globals['_SANDBOXPOLICY_MODE']._serialized_end=445 + _globals['_REGISTRATIONRESPONSE']._serialized_start=447 + _globals['_REGISTRATIONRESPONSE']._serialized_end=567 + _globals['_CLIENTTASKMESSAGE']._serialized_start=570 + _globals['_CLIENTTASKMESSAGE']._serialized_end=867 + _globals['_SKILLEVENT']._serialized_start=869 + _globals['_SKILLEVENT']._serialized_end=990 + _globals['_NODEANNOUNCE']._serialized_start=992 + _globals['_NODEANNOUNCE']._serialized_end=1023 + _globals['_BROWSEREVENT']._serialized_start=1026 + _globals['_BROWSEREVENT']._serialized_end=1161 + _globals['_SERVERTASKMESSAGE']._serialized_start=1164 + _globals['_SERVERTASKMESSAGE']._serialized_end=1433 + _globals['_TASKCANCELREQUEST']._serialized_start=1435 + _globals['_TASKCANCELREQUEST']._serialized_end=1471 + _globals['_TASKREQUEST']._serialized_start=1474 + _globals['_TASKREQUEST']._serialized_end=1683 + _globals['_BROWSERACTION']._serialized_start=1686 + _globals['_BROWSERACTION']._serialized_end=1974 + _globals['_BROWSERACTION_ACTIONTYPE']._serialized_start=1840 + _globals['_BROWSERACTION_ACTIONTYPE']._serialized_end=1974 + _globals['_TASKRESPONSE']._serialized_start=1977 + _globals['_TASKRESPONSE']._serialized_end=2329 + _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_start=2209 + _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_end=2257 + _globals['_TASKRESPONSE_STATUS']._serialized_start=2259 + _globals['_TASKRESPONSE_STATUS']._serialized_end=2319 + _globals['_BROWSERRESPONSE']._serialized_start=2332 + _globals['_BROWSERRESPONSE']._serialized_end=2552 + _globals['_CONSOLEMESSAGE']._serialized_start=2554 + _globals['_CONSOLEMESSAGE']._serialized_end=2621 + _globals['_NETWORKREQUEST']._serialized_start=2623 + _globals['_NETWORKREQUEST']._serialized_end=2727 + _globals['_WORKPOOLUPDATE']._serialized_start=2729 + _globals['_WORKPOOLUPDATE']._serialized_end=2773 + _globals['_TASKCLAIMREQUEST']._serialized_start=2775 + _globals['_TASKCLAIMREQUEST']._serialized_end=2827 + _globals['_TASKCLAIMRESPONSE']._serialized_start=2829 + _globals['_TASKCLAIMRESPONSE']._serialized_end=2898 + _globals['_HEARTBEAT']._serialized_start=2901 + _globals['_HEARTBEAT']._serialized_end=3094 + _globals['_HEALTHCHECKRESPONSE']._serialized_start=3096 + _globals['_HEALTHCHECKRESPONSE']._serialized_end=3141 + _globals['_FILESYNCMESSAGE']._serialized_start=3144 + _globals['_FILESYNCMESSAGE']._serialized_end=3355 + _globals['_SYNCCONTROL']._serialized_start=3358 + _globals['_SYNCCONTROL']._serialized_end=3556 + _globals['_SYNCCONTROL_ACTION']._serialized_start=3453 + _globals['_SYNCCONTROL_ACTION']._serialized_end=3556 + _globals['_DIRECTORYMANIFEST']._serialized_start=3558 + _globals['_DIRECTORYMANIFEST']._serialized_end=3628 + _globals['_FILEINFO']._serialized_start=3630 + _globals['_FILEINFO']._serialized_end=3698 + _globals['_FILEPAYLOAD']._serialized_start=3700 + _globals['_FILEPAYLOAD']._serialized_end=3795 + _globals['_SYNCSTATUS']._serialized_start=3798 + _globals['_SYNCSTATUS']._serialized_end=3958 + _globals['_SYNCSTATUS_CODE']._serialized_start=3892 + _globals['_SYNCSTATUS_CODE']._serialized_end=3958 + _globals['_AGENTORCHESTRATOR']._serialized_start=3961 + _globals['_AGENTORCHESTRATOR']._serialized_end=4194 # @@protoc_insertion_point(module_scope) diff --git a/agent-node/protos/agent_pb2_grpc.py b/agent-node/protos/agent_pb2_grpc.py index f551b0b..b91c8a0 100644 --- a/agent-node/protos/agent_pb2_grpc.py +++ b/agent-node/protos/agent_pb2_grpc.py @@ -2,7 +2,7 @@ """Client and server classes corresponding to protobuf-defined services.""" import grpc -from protos import agent_pb2 as protos_dot_agent__pb2 +from . import agent_pb2 as agent__pb2 class AgentOrchestratorStub(object): @@ -17,18 +17,18 @@ """ self.SyncConfiguration = channel.unary_unary( '/agent.AgentOrchestrator/SyncConfiguration', - request_serializer=protos_dot_agent__pb2.RegistrationRequest.SerializeToString, - response_deserializer=protos_dot_agent__pb2.RegistrationResponse.FromString, + request_serializer=agent__pb2.RegistrationRequest.SerializeToString, + response_deserializer=agent__pb2.RegistrationResponse.FromString, ) self.TaskStream = channel.stream_stream( '/agent.AgentOrchestrator/TaskStream', - request_serializer=protos_dot_agent__pb2.ClientTaskMessage.SerializeToString, - response_deserializer=protos_dot_agent__pb2.ServerTaskMessage.FromString, + request_serializer=agent__pb2.ClientTaskMessage.SerializeToString, + response_deserializer=agent__pb2.ServerTaskMessage.FromString, ) self.ReportHealth = channel.stream_stream( '/agent.AgentOrchestrator/ReportHealth', - request_serializer=protos_dot_agent__pb2.Heartbeat.SerializeToString, - response_deserializer=protos_dot_agent__pb2.HealthCheckResponse.FromString, + request_serializer=agent__pb2.Heartbeat.SerializeToString, + response_deserializer=agent__pb2.HealthCheckResponse.FromString, ) @@ -62,18 +62,18 @@ rpc_method_handlers = { 'SyncConfiguration': grpc.unary_unary_rpc_method_handler( servicer.SyncConfiguration, - request_deserializer=protos_dot_agent__pb2.RegistrationRequest.FromString, - response_serializer=protos_dot_agent__pb2.RegistrationResponse.SerializeToString, + request_deserializer=agent__pb2.RegistrationRequest.FromString, + response_serializer=agent__pb2.RegistrationResponse.SerializeToString, ), 'TaskStream': grpc.stream_stream_rpc_method_handler( servicer.TaskStream, - request_deserializer=protos_dot_agent__pb2.ClientTaskMessage.FromString, - response_serializer=protos_dot_agent__pb2.ServerTaskMessage.SerializeToString, + request_deserializer=agent__pb2.ClientTaskMessage.FromString, + response_serializer=agent__pb2.ServerTaskMessage.SerializeToString, ), 'ReportHealth': grpc.stream_stream_rpc_method_handler( servicer.ReportHealth, - request_deserializer=protos_dot_agent__pb2.Heartbeat.FromString, - response_serializer=protos_dot_agent__pb2.HealthCheckResponse.SerializeToString, + request_deserializer=agent__pb2.Heartbeat.FromString, + response_serializer=agent__pb2.HealthCheckResponse.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( @@ -98,8 +98,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/agent.AgentOrchestrator/SyncConfiguration', - protos_dot_agent__pb2.RegistrationRequest.SerializeToString, - protos_dot_agent__pb2.RegistrationResponse.FromString, + agent__pb2.RegistrationRequest.SerializeToString, + agent__pb2.RegistrationResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -115,8 +115,8 @@ timeout=None, metadata=None): return grpc.experimental.stream_stream(request_iterator, target, '/agent.AgentOrchestrator/TaskStream', - protos_dot_agent__pb2.ClientTaskMessage.SerializeToString, - protos_dot_agent__pb2.ServerTaskMessage.FromString, + agent__pb2.ClientTaskMessage.SerializeToString, + agent__pb2.ServerTaskMessage.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -132,7 +132,7 @@ timeout=None, metadata=None): return grpc.experimental.stream_stream(request_iterator, target, '/agent.AgentOrchestrator/ReportHealth', - protos_dot_agent__pb2.Heartbeat.SerializeToString, - protos_dot_agent__pb2.HealthCheckResponse.FromString, + agent__pb2.Heartbeat.SerializeToString, + agent__pb2.HealthCheckResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/agent-node/spawn_local_agent.sh b/agent-node/spawn_local_agent.sh new file mode 100755 index 0000000..150bed5 --- /dev/null +++ b/agent-node/spawn_local_agent.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# scripts/spawn_local_agent.sh + +echo "🚀 Cortex Agent Node - Local Spawner" +echo "====================================" + +# Check for environment variables +read -p "Enter Node ID (Default: local-agent-$(hostname)): " NODE_ID +NODE_ID=${NODE_ID:-local-agent-$(hostname)} + +echo "" +echo "STEP 1: Register your node" +echo "--------------------------" +echo "Go to: https://ai.jerxie.com/nodes" +echo "1. Click '+ Register Node'" +echo "2. Use ID: $NODE_ID" +echo "3. Copy the 'Secret Key' provided." +echo "" + +read -sp "Enter the Secret Key: " SECRET_KEY +echo "" + +read -p "Enter Hub gRPC Endpoint (Default: ai.jerxie.com:50051): " GRPC_ENDPOINT +GRPC_ENDPOINT=${GRPC_ENDPOINT:-ai.jerxie.com:50051} + +# Create .env file +cat > .env < HEARTBEAT_INTERVAL_S: + live_node = registry.get_node(node_id) + await websocket.send_json({ + "event": "heartbeat", "node_id": node_id, "timestamp": _now(), + "data": {"status": live_node._compute_status() if live_node else "offline", + "stats": live_node.stats if live_node else {}}, + }) + last_heartbeat = now + # Extremely fast poll loop for real-time latency + await asyncio.sleep(0.02) + except WebSocketDisconnect: + pass + except Exception as e: + logger.error(f"[nodes/stream_sender] {node_id}: {e}") + + async def receive_events(): + from app.protos import agent_pb2 + from app.core.grpc.utils.crypto import sign_payload + import uuid + try: + while True: + data = await websocket.receive_json() + if data.get("action") == "ping": + await websocket.send_json({ + "event": "pong", + "node_id": node_id, + "timestamp": _now(), + "client_ts": data.get("ts") # Echo back for RTT calculation + }) + continue + + if data.get("action") == "dispatch": + live_node = registry.get_node(node_id) + if not live_node: + await websocket.send_json({"event": "task_error", "data": {"stderr": "Node offline"}}) + continue + + cmd = data.get("command", "") + session_id = data.get("session_id", "") + task_id = str(uuid.uuid4()) + + registry.emit(node_id, "task_assigned", {"command": cmd, "session_id": session_id}, task_id=task_id) + + try: + task_req = agent_pb2.TaskRequest( + task_id=task_id, + payload_json=cmd, + signature=sign_payload(cmd), + timeout_ms=0, + session_id=session_id + ) + live_node.queue.put(agent_pb2.ServerTaskMessage(task_request=task_req)) + registry.emit(node_id, "task_start", {"command": cmd}, task_id=task_id) + except Exception as e: + logger.error(f"[ws/dispatch] Error: {e}") + except WebSocketDisconnect: + pass + except Exception as e: + logger.error(f"[nodes/stream_receive] {node_id}: {e}") + + sender_task = asyncio.create_task(send_events()) + receiver_task = asyncio.create_task(receive_events()) + try: - while True: - _drain(q, websocket) - await asyncio.sleep(HEARTBEAT_INTERVAL_S) - live = registry.get_node(node_id) - await websocket.send_json({ - "event": "heartbeat", "node_id": node_id, "timestamp": _now(), - "data": {"status": live._compute_status() if live else "offline", - "stats": live.stats if live else {}}, - }) - except WebSocketDisconnect: - pass - except Exception as e: - logger.error(f"[nodes/stream] {node_id}: {e}") + done, pending = await asyncio.wait( + [sender_task, receiver_task], + return_when=asyncio.FIRST_COMPLETED + ) + for t in pending: + t.cancel() + except asyncio.CancelledError: + sender_task.cancel() + receiver_task.cancel() finally: registry.unsubscribe_node(node_id, q) @@ -530,7 +633,7 @@ registry.subscribe_user(user_id, q) try: while True: - _drain(q, websocket) + await _drain(q, websocket) await asyncio.sleep(HEARTBEAT_INTERVAL_S) live_nodes = registry.list_nodes(user_id=user_id) await websocket.send_json({ @@ -593,7 +696,11 @@ live = registry.get_node(node.node_id) status = live._compute_status() if live else node.last_status or "offline" skill_cfg = node.skill_config or {} - available = [skill for skill, cfg in skill_cfg.items() if cfg.get("enabled", True)] + if isinstance(skill_cfg, str): + import json + try: skill_cfg = json.loads(skill_cfg) + except: skill_cfg = {} + available = [skill for skill, cfg in skill_cfg.items() if isinstance(cfg, dict) and cfg.get("enabled", True)] stats = live.stats if live else {} return schemas.AgentNodeUserView( diff --git a/ai-hub/app/api/schemas.py b/ai-hub/app/api/schemas.py index c7b734b..0ad14d3 100644 --- a/ai-hub/app/api/schemas.py +++ b/ai-hub/app/api/schemas.py @@ -247,15 +247,24 @@ # --- Skill Toggles (admin-configured) --- +class SandboxConfig(BaseModel): + """Granular command execution policy for the shell skill.""" + mode: Literal["STRICT", "PERMISSIVE"] = "STRICT" + allowed_commands: List[str] = Field(default_factory=lambda: ["ls", "cat", "echo", "pwd", "uname", "curl", "python3", "git"]) + denied_commands: List[str] = Field(default_factory=list) + sensitive_commands: List[str] = Field(default_factory=list) + class SkillConfig(BaseModel): """Per-skill enable/disable with optional config.""" enabled: bool = True cwd_jail: Optional[str] = None # shell only: restrict working directory max_file_size_mb: Optional[int] = None # sync only: file size cap + sandbox: Optional[SandboxConfig] = None # NEW: shell sandbox config class NodeSkillConfig(BaseModel): """Admin-controlled skill configuration for a node.""" - shell: SkillConfig = SkillConfig(enabled=True) + status: Optional[str] = "configured" + shell: SkillConfig = SkillConfig(enabled=True, sandbox=SandboxConfig()) browser: SkillConfig = SkillConfig(enabled=True) sync: SkillConfig = SkillConfig(enabled=True) diff --git a/ai-hub/app/core/grpc/services/grpc_server.py b/ai-hub/app/core/grpc/services/grpc_server.py index 6f8ddc6..832e54a 100644 --- a/ai-hub/app/core/grpc/services/grpc_server.py +++ b/ai-hub/app/core/grpc/services/grpc_server.py @@ -99,11 +99,22 @@ logger.error(f"[⚠️] Hub token validation unavailable ({e}); proceeding as 'default' user.") # Build allowed_commands from skill_config + # Build Sandbox Policy from skill_config['shell'] shell_cfg = skill_cfg.get("shell", {}) - if shell_cfg.get("enabled", True): - allowed_commands = ["ls", "cat", "echo", "pwd", "uname", "curl", "python3", "git"] - else: - allowed_commands = [] + sandbox_cfg = shell_cfg.get("sandbox", {}) if isinstance(shell_cfg, dict) else {} + + # 1. Resolve Mode + mode_str = sandbox_cfg.get("mode", "STRICT").upper() + grpc_mode = agent_pb2.SandboxPolicy.STRICT if mode_str == "STRICT" else agent_pb2.SandboxPolicy.PERMISSIVE + + # 2. Resolve Command Lists (fallback to some safe defaults if enabled but empty) + allowed = sandbox_cfg.get("allowed_commands", []) + if not allowed and shell_cfg.get("enabled", True): + allowed = ["ls", "cat", "echo", "pwd", "uname", "curl", "python3", "git"] + + denied = sandbox_cfg.get("denied_commands", []) + sensitive = sandbox_cfg.get("sensitive_commands", []) + jail = shell_cfg.get("cwd_jail", "") # Register the node in the centralized AI Hub registry self.registry.register(request.node_id, user_id, { @@ -114,8 +125,11 @@ return agent_pb2.RegistrationResponse( success=True, policy=agent_pb2.SandboxPolicy( - mode=agent_pb2.SandboxPolicy.STRICT, - allowed_commands=allowed_commands, + mode=grpc_mode, + allowed_commands=allowed, + denied_commands=denied, + sensitive_commands=sensitive, + working_dir_jail=jail ) ) @@ -209,6 +223,20 @@ event_type = "task_complete" if tr.status == agent_pb2.TaskResponse.SUCCESS else "task_error" self.registry.emit(node_id, event_type, res_obj, task_id=tr.task_id) + elif kind == 'skill_event': + se = msg.skill_event + data_kind = se.WhichOneof('data') + event_data = {"session_id": se.session_id, "task_id": se.task_id, "skill": "shell"} + + if data_kind == 'terminal_out': + event_data["data"] = se.terminal_out + event_data["type"] = "output" + elif data_kind == 'prompt': + event_data["data"] = se.prompt + event_data["type"] = "prompt" + + self.registry.emit(node_id, "skill_event", event_data) + elif kind == 'browser_event': e = msg.browser_event event_data = {} diff --git a/ai-hub/app/core/services/node_registry.py b/ai-hub/app/core/services/node_registry.py index c6f88c4..a83cb81 100644 --- a/ai-hub/app/core/services/node_registry.py +++ b/ai-hub/app/core/services/node_registry.py @@ -11,9 +11,12 @@ """ import threading import queue +import logging from datetime import datetime from typing import Dict, Optional, List, Any +logger = logging.getLogger(__name__) + # All event types emitted across the system — rendered in the live UI EVENT_TYPES = { @@ -165,7 +168,7 @@ # Persist to DB (background-safe — session is scoped) self._db_upsert_node(node_id, user_id, metadata) - print(f"[📋] NodeRegistry: Registered {node_id} (owner: {user_id})") + logger.info(f"[📋] NodeRegistry: Registered {node_id} (owner: {user_id}) | Stats enabled") self.emit(node_id, "node_online", record.to_dict()) return record @@ -212,6 +215,8 @@ node = self._nodes.get(node_id) if node: node.update_stats(stats) + if stats.get("cpu_usage_percent", 0) > 0 or stats.get("memory_usage_percent", 0) > 0: + logger.debug(f"[💓] Heartbeat {node_id}: CPU {stats.get('cpu_usage_percent')}% | MEM {stats.get('memory_usage_percent')}%") # Persist heartbeat timestamp to DB (throttle: already ~10s cadence from node) self._db_update_heartbeat(node_id) # Emit heartbeat event to live UI diff --git a/ai-hub/app/db/migrate.py b/ai-hub/app/db/migrate.py index 63e2e33..e098f0a 100644 --- a/ai-hub/app/db/migrate.py +++ b/ai-hub/app/db/migrate.py @@ -61,9 +61,33 @@ else: logger.info(f"Column '{col_name}' already exists in 'sessions'.") - - # Agent Nodes table migrations - if inspector.has_table("agent_nodes"): + # --- M6: Agent Node Tables --- + # Create agent_nodes table if it doesn't exist + if not inspector.has_table("agent_nodes"): + logger.info("Creating table 'agent_nodes'...") + try: + conn.execute(text(""" + CREATE TABLE IF NOT EXISTS agent_nodes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + node_id TEXT UNIQUE NOT NULL, + display_name TEXT NOT NULL, + description TEXT, + registered_by TEXT NOT NULL, + skill_config TEXT NOT NULL DEFAULT '{}', + capabilities TEXT DEFAULT '{}', + invite_token TEXT UNIQUE, + is_active INTEGER NOT NULL DEFAULT 1, + last_status TEXT NOT NULL DEFAULT 'offline', + last_seen_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """)) + conn.commit() + logger.info("Table 'agent_nodes' created.") + except Exception as e: + logger.error(f"Failed to create 'agent_nodes': {e}") + else: + # Table exists — ensure all columns are present node_columns = [c["name"] for c in inspector.get_columns("agent_nodes")] node_required_columns = [ ("display_name", "TEXT"), @@ -84,6 +108,25 @@ except Exception as e: logger.error(f"Failed to add column '{col_name}': {e}") + # Create node_group_access table if it doesn't exist + if not inspector.has_table("node_group_access"): + logger.info("Creating table 'node_group_access'...") + try: + conn.execute(text(""" + CREATE TABLE IF NOT EXISTS node_group_access ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + node_id TEXT NOT NULL REFERENCES agent_nodes(node_id), + group_id TEXT NOT NULL REFERENCES groups(id), + access_level TEXT NOT NULL DEFAULT 'use', + granted_by TEXT NOT NULL REFERENCES users(id), + granted_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """)) + conn.commit() + logger.info("Table 'node_group_access' created.") + except Exception as e: + logger.error(f"Failed to create 'node_group_access': {e}") + logger.info("Database migrations complete.") diff --git a/ai-hub/app/protos/__init__.py b/ai-hub/app/protos/__init__.py index e69de29..07e7658 100644 --- a/ai-hub/app/protos/__init__.py +++ b/ai-hub/app/protos/__init__.py @@ -0,0 +1,9 @@ +import os +import sys + +# Add this directory to sys.path so that agent_pb2_grpc.py +# can find agent_pb2.py via absolute import 'import agent_pb2'. +# This is a common workaround for gRPC-generated code in Python packages. +proto_dir = os.path.dirname(os.path.abspath(__file__)) +if proto_dir not in sys.path: + sys.path.insert(0, proto_dir) diff --git a/ai-hub/app/protos/agent.proto b/ai-hub/app/protos/agent.proto index e751062..e01e322 100644 --- a/ai-hub/app/protos/agent.proto +++ b/ai-hub/app/protos/agent.proto @@ -50,6 +50,17 @@ BrowserEvent browser_event = 3; NodeAnnounce announce = 4; // NEW: Identification on stream connect FileSyncMessage file_sync = 5; // NEW: Ghost Mirror Sync + SkillEvent skill_event = 6; // NEW: Persistent real-time skill data + } +} + +message SkillEvent { + string session_id = 1; + string task_id = 2; + oneof data { + string terminal_out = 3; // Raw stdout/stderr chunks + string prompt = 4; // Interactive prompt (like password) + bool keep_alive = 5; // Session preservation } } diff --git a/ai-hub/app/protos/agent_pb2.py b/ai-hub/app/protos/agent_pb2.py index 0747c70..3b32354 100644 --- a/ai-hub/app/protos/agent_pb2.py +++ b/ai-hub/app/protos/agent_pb2.py @@ -14,7 +14,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x61gent.proto\x12\x05\x61gent\"\xde\x01\n\x13RegistrationRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x12\n\nauth_token\x18\x03 \x01(\t\x12\x18\n\x10node_description\x18\x04 \x01(\t\x12\x42\n\x0c\x63\x61pabilities\x18\x05 \x03(\x0b\x32,.agent.RegistrationRequest.CapabilitiesEntry\x1a\x33\n\x11\x43\x61pabilitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc5\x01\n\rSandboxPolicy\x12\'\n\x04mode\x18\x01 \x01(\x0e\x32\x19.agent.SandboxPolicy.Mode\x12\x18\n\x10\x61llowed_commands\x18\x02 \x03(\t\x12\x17\n\x0f\x64\x65nied_commands\x18\x03 \x03(\t\x12\x1a\n\x12sensitive_commands\x18\x04 \x03(\t\x12\x18\n\x10working_dir_jail\x18\x05 \x01(\t\"\"\n\x04Mode\x12\n\n\x06STRICT\x10\x00\x12\x0e\n\nPERMISSIVE\x10\x01\"x\n\x14RegistrationResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12\x12\n\nsession_id\x18\x03 \x01(\t\x12$\n\x06policy\x18\x04 \x01(\x0b\x32\x14.agent.SandboxPolicy\"\xff\x01\n\x11\x43lientTaskMessage\x12,\n\rtask_response\x18\x01 \x01(\x0b\x32\x13.agent.TaskResponseH\x00\x12-\n\ntask_claim\x18\x02 \x01(\x0b\x32\x17.agent.TaskClaimRequestH\x00\x12,\n\rbrowser_event\x18\x03 \x01(\x0b\x32\x13.agent.BrowserEventH\x00\x12\'\n\x08\x61nnounce\x18\x04 \x01(\x0b\x32\x13.agent.NodeAnnounceH\x00\x12+\n\tfile_sync\x18\x05 \x01(\x0b\x32\x16.agent.FileSyncMessageH\x00\x42\t\n\x07payload\"\x1f\n\x0cNodeAnnounce\x12\x0f\n\x07node_id\x18\x01 \x01(\t\"\x87\x01\n\x0c\x42rowserEvent\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12,\n\x0b\x63onsole_msg\x18\x02 \x01(\x0b\x32\x15.agent.ConsoleMessageH\x00\x12,\n\x0bnetwork_req\x18\x03 \x01(\x0b\x32\x15.agent.NetworkRequestH\x00\x42\x07\n\x05\x65vent\"\x8d\x02\n\x11ServerTaskMessage\x12*\n\x0ctask_request\x18\x01 \x01(\x0b\x32\x12.agent.TaskRequestH\x00\x12\x31\n\x10work_pool_update\x18\x02 \x01(\x0b\x32\x15.agent.WorkPoolUpdateH\x00\x12\x30\n\x0c\x63laim_status\x18\x03 \x01(\x0b\x32\x18.agent.TaskClaimResponseH\x00\x12/\n\x0btask_cancel\x18\x04 \x01(\x0b\x32\x18.agent.TaskCancelRequestH\x00\x12+\n\tfile_sync\x18\x05 \x01(\x0b\x32\x16.agent.FileSyncMessageH\x00\x42\t\n\x07payload\"$\n\x11TaskCancelRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\"\xd1\x01\n\x0bTaskRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x11\n\ttask_type\x18\x02 \x01(\t\x12\x16\n\x0cpayload_json\x18\x03 \x01(\tH\x00\x12.\n\x0e\x62rowser_action\x18\x07 \x01(\x0b\x32\x14.agent.BrowserActionH\x00\x12\x12\n\ntimeout_ms\x18\x04 \x01(\x05\x12\x10\n\x08trace_id\x18\x05 \x01(\t\x12\x11\n\tsignature\x18\x06 \x01(\t\x12\x12\n\nsession_id\x18\x08 \x01(\tB\t\n\x07payload\"\xa0\x02\n\rBrowserAction\x12/\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x1f.agent.BrowserAction.ActionType\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x10\n\x08selector\x18\x03 \x01(\t\x12\x0c\n\x04text\x18\x04 \x01(\t\x12\x12\n\nsession_id\x18\x05 \x01(\t\x12\t\n\x01x\x18\x06 \x01(\x05\x12\t\n\x01y\x18\x07 \x01(\x05\"\x86\x01\n\nActionType\x12\x0c\n\x08NAVIGATE\x10\x00\x12\t\n\x05\x43LICK\x10\x01\x12\x08\n\x04TYPE\x10\x02\x12\x0e\n\nSCREENSHOT\x10\x03\x12\x0b\n\x07GET_DOM\x10\x04\x12\t\n\x05HOVER\x10\x05\x12\n\n\x06SCROLL\x10\x06\x12\t\n\x05\x43LOSE\x10\x07\x12\x08\n\x04\x45VAL\x10\x08\x12\x0c\n\x08GET_A11Y\x10\t\"\xe0\x02\n\x0cTaskResponse\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12*\n\x06status\x18\x02 \x01(\x0e\x32\x1a.agent.TaskResponse.Status\x12\x0e\n\x06stdout\x18\x03 \x01(\t\x12\x0e\n\x06stderr\x18\x04 \x01(\t\x12\x10\n\x08trace_id\x18\x05 \x01(\t\x12\x35\n\tartifacts\x18\x06 \x03(\x0b\x32\".agent.TaskResponse.ArtifactsEntry\x12\x30\n\x0e\x62rowser_result\x18\x07 \x01(\x0b\x32\x16.agent.BrowserResponseH\x00\x1a\x30\n\x0e\x41rtifactsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"<\n\x06Status\x12\x0b\n\x07SUCCESS\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x0b\n\x07TIMEOUT\x10\x02\x12\r\n\tCANCELLED\x10\x03\x42\x08\n\x06result\"\xdc\x01\n\x0f\x42rowserResponse\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x10\n\x08snapshot\x18\x03 \x01(\x0c\x12\x13\n\x0b\x64om_content\x18\x04 \x01(\t\x12\x11\n\ta11y_tree\x18\x05 \x01(\t\x12\x13\n\x0b\x65val_result\x18\x06 \x01(\t\x12.\n\x0f\x63onsole_history\x18\x07 \x03(\x0b\x32\x15.agent.ConsoleMessage\x12.\n\x0fnetwork_history\x18\x08 \x03(\x0b\x32\x15.agent.NetworkRequest\"C\n\x0e\x43onsoleMessage\x12\r\n\x05level\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x02 \x01(\t\x12\x14\n\x0ctimestamp_ms\x18\x03 \x01(\x03\"h\n\x0eNetworkRequest\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x05\x12\x15\n\rresource_type\x18\x04 \x01(\t\x12\x12\n\nlatency_ms\x18\x05 \x01(\x03\",\n\x0eWorkPoolUpdate\x12\x1a\n\x12\x61vailable_task_ids\x18\x01 \x03(\t\"4\n\x10TaskClaimRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07node_id\x18\x02 \x01(\t\"E\n\x11TaskClaimResponse\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07granted\x18\x02 \x01(\x08\x12\x0e\n\x06reason\x18\x03 \x01(\t\"\xc1\x01\n\tHeartbeat\x12\x0f\n\x07node_id\x18\x01 \x01(\t\x12\x19\n\x11\x63pu_usage_percent\x18\x02 \x01(\x02\x12\x1c\n\x14memory_usage_percent\x18\x03 \x01(\x02\x12\x1b\n\x13\x61\x63tive_worker_count\x18\x04 \x01(\x05\x12\x1b\n\x13max_worker_capacity\x18\x05 \x01(\x05\x12\x16\n\x0estatus_message\x18\x06 \x01(\t\x12\x18\n\x10running_task_ids\x18\x07 \x03(\t\"-\n\x13HealthCheckResponse\x12\x16\n\x0eserver_time_ms\x18\x01 \x01(\x03\"\xd3\x01\n\x0f\x46ileSyncMessage\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12,\n\x08manifest\x18\x02 \x01(\x0b\x32\x18.agent.DirectoryManifestH\x00\x12\'\n\tfile_data\x18\x03 \x01(\x0b\x32\x12.agent.FilePayloadH\x00\x12#\n\x06status\x18\x04 \x01(\x0b\x32\x11.agent.SyncStatusH\x00\x12%\n\x07\x63ontrol\x18\x05 \x01(\x0b\x32\x12.agent.SyncControlH\x00\x42\t\n\x07payload\"\xc6\x01\n\x0bSyncControl\x12)\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x19.agent.SyncControl.Action\x12\x0c\n\x04path\x18\x02 \x01(\t\x12\x15\n\rrequest_paths\x18\x03 \x03(\t\"g\n\x06\x41\x63tion\x12\x12\n\x0eSTART_WATCHING\x10\x00\x12\x11\n\rSTOP_WATCHING\x10\x01\x12\x08\n\x04LOCK\x10\x02\x12\n\n\x06UNLOCK\x10\x03\x12\x14\n\x10REFRESH_MANIFEST\x10\x04\x12\n\n\x06RESYNC\x10\x05\"F\n\x11\x44irectoryManifest\x12\x11\n\troot_path\x18\x01 \x01(\t\x12\x1e\n\x05\x66iles\x18\x02 \x03(\x0b\x32\x0f.agent.FileInfo\"D\n\x08\x46ileInfo\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\x03\x12\x0c\n\x04hash\x18\x03 \x01(\t\x12\x0e\n\x06is_dir\x18\x04 \x01(\x08\"_\n\x0b\x46ilePayload\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\r\n\x05\x63hunk\x18\x02 \x01(\x0c\x12\x13\n\x0b\x63hunk_index\x18\x03 \x01(\x05\x12\x10\n\x08is_final\x18\x04 \x01(\x08\x12\x0c\n\x04hash\x18\x05 \x01(\t\"\xa0\x01\n\nSyncStatus\x12$\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x16.agent.SyncStatus.Code\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x17\n\x0freconcile_paths\x18\x03 \x03(\t\"B\n\x04\x43ode\x12\x06\n\x02OK\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x16\n\x12RECONCILE_REQUIRED\x10\x02\x12\x0f\n\x0bIN_PROGRESS\x10\x03\x32\xe9\x01\n\x11\x41gentOrchestrator\x12L\n\x11SyncConfiguration\x12\x1a.agent.RegistrationRequest\x1a\x1b.agent.RegistrationResponse\x12\x44\n\nTaskStream\x12\x18.agent.ClientTaskMessage\x1a\x18.agent.ServerTaskMessage(\x01\x30\x01\x12@\n\x0cReportHealth\x12\x10.agent.Heartbeat\x1a\x1a.agent.HealthCheckResponse(\x01\x30\x01\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x61gent.proto\x12\x05\x61gent\"\xde\x01\n\x13RegistrationRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x12\n\nauth_token\x18\x03 \x01(\t\x12\x18\n\x10node_description\x18\x04 \x01(\t\x12\x42\n\x0c\x63\x61pabilities\x18\x05 \x03(\x0b\x32,.agent.RegistrationRequest.CapabilitiesEntry\x1a\x33\n\x11\x43\x61pabilitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc5\x01\n\rSandboxPolicy\x12\'\n\x04mode\x18\x01 \x01(\x0e\x32\x19.agent.SandboxPolicy.Mode\x12\x18\n\x10\x61llowed_commands\x18\x02 \x03(\t\x12\x17\n\x0f\x64\x65nied_commands\x18\x03 \x03(\t\x12\x1a\n\x12sensitive_commands\x18\x04 \x03(\t\x12\x18\n\x10working_dir_jail\x18\x05 \x01(\t\"\"\n\x04Mode\x12\n\n\x06STRICT\x10\x00\x12\x0e\n\nPERMISSIVE\x10\x01\"x\n\x14RegistrationResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12\x12\n\nsession_id\x18\x03 \x01(\t\x12$\n\x06policy\x18\x04 \x01(\x0b\x32\x14.agent.SandboxPolicy\"\xa9\x02\n\x11\x43lientTaskMessage\x12,\n\rtask_response\x18\x01 \x01(\x0b\x32\x13.agent.TaskResponseH\x00\x12-\n\ntask_claim\x18\x02 \x01(\x0b\x32\x17.agent.TaskClaimRequestH\x00\x12,\n\rbrowser_event\x18\x03 \x01(\x0b\x32\x13.agent.BrowserEventH\x00\x12\'\n\x08\x61nnounce\x18\x04 \x01(\x0b\x32\x13.agent.NodeAnnounceH\x00\x12+\n\tfile_sync\x18\x05 \x01(\x0b\x32\x16.agent.FileSyncMessageH\x00\x12(\n\x0bskill_event\x18\x06 \x01(\x0b\x32\x11.agent.SkillEventH\x00\x42\t\n\x07payload\"y\n\nSkillEvent\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0f\n\x07task_id\x18\x02 \x01(\t\x12\x16\n\x0cterminal_out\x18\x03 \x01(\tH\x00\x12\x10\n\x06prompt\x18\x04 \x01(\tH\x00\x12\x14\n\nkeep_alive\x18\x05 \x01(\x08H\x00\x42\x06\n\x04\x64\x61ta\"\x1f\n\x0cNodeAnnounce\x12\x0f\n\x07node_id\x18\x01 \x01(\t\"\x87\x01\n\x0c\x42rowserEvent\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12,\n\x0b\x63onsole_msg\x18\x02 \x01(\x0b\x32\x15.agent.ConsoleMessageH\x00\x12,\n\x0bnetwork_req\x18\x03 \x01(\x0b\x32\x15.agent.NetworkRequestH\x00\x42\x07\n\x05\x65vent\"\x8d\x02\n\x11ServerTaskMessage\x12*\n\x0ctask_request\x18\x01 \x01(\x0b\x32\x12.agent.TaskRequestH\x00\x12\x31\n\x10work_pool_update\x18\x02 \x01(\x0b\x32\x15.agent.WorkPoolUpdateH\x00\x12\x30\n\x0c\x63laim_status\x18\x03 \x01(\x0b\x32\x18.agent.TaskClaimResponseH\x00\x12/\n\x0btask_cancel\x18\x04 \x01(\x0b\x32\x18.agent.TaskCancelRequestH\x00\x12+\n\tfile_sync\x18\x05 \x01(\x0b\x32\x16.agent.FileSyncMessageH\x00\x42\t\n\x07payload\"$\n\x11TaskCancelRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\"\xd1\x01\n\x0bTaskRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x11\n\ttask_type\x18\x02 \x01(\t\x12\x16\n\x0cpayload_json\x18\x03 \x01(\tH\x00\x12.\n\x0e\x62rowser_action\x18\x07 \x01(\x0b\x32\x14.agent.BrowserActionH\x00\x12\x12\n\ntimeout_ms\x18\x04 \x01(\x05\x12\x10\n\x08trace_id\x18\x05 \x01(\t\x12\x11\n\tsignature\x18\x06 \x01(\t\x12\x12\n\nsession_id\x18\x08 \x01(\tB\t\n\x07payload\"\xa0\x02\n\rBrowserAction\x12/\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x1f.agent.BrowserAction.ActionType\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x10\n\x08selector\x18\x03 \x01(\t\x12\x0c\n\x04text\x18\x04 \x01(\t\x12\x12\n\nsession_id\x18\x05 \x01(\t\x12\t\n\x01x\x18\x06 \x01(\x05\x12\t\n\x01y\x18\x07 \x01(\x05\"\x86\x01\n\nActionType\x12\x0c\n\x08NAVIGATE\x10\x00\x12\t\n\x05\x43LICK\x10\x01\x12\x08\n\x04TYPE\x10\x02\x12\x0e\n\nSCREENSHOT\x10\x03\x12\x0b\n\x07GET_DOM\x10\x04\x12\t\n\x05HOVER\x10\x05\x12\n\n\x06SCROLL\x10\x06\x12\t\n\x05\x43LOSE\x10\x07\x12\x08\n\x04\x45VAL\x10\x08\x12\x0c\n\x08GET_A11Y\x10\t\"\xe0\x02\n\x0cTaskResponse\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12*\n\x06status\x18\x02 \x01(\x0e\x32\x1a.agent.TaskResponse.Status\x12\x0e\n\x06stdout\x18\x03 \x01(\t\x12\x0e\n\x06stderr\x18\x04 \x01(\t\x12\x10\n\x08trace_id\x18\x05 \x01(\t\x12\x35\n\tartifacts\x18\x06 \x03(\x0b\x32\".agent.TaskResponse.ArtifactsEntry\x12\x30\n\x0e\x62rowser_result\x18\x07 \x01(\x0b\x32\x16.agent.BrowserResponseH\x00\x1a\x30\n\x0e\x41rtifactsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"<\n\x06Status\x12\x0b\n\x07SUCCESS\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x0b\n\x07TIMEOUT\x10\x02\x12\r\n\tCANCELLED\x10\x03\x42\x08\n\x06result\"\xdc\x01\n\x0f\x42rowserResponse\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x10\n\x08snapshot\x18\x03 \x01(\x0c\x12\x13\n\x0b\x64om_content\x18\x04 \x01(\t\x12\x11\n\ta11y_tree\x18\x05 \x01(\t\x12\x13\n\x0b\x65val_result\x18\x06 \x01(\t\x12.\n\x0f\x63onsole_history\x18\x07 \x03(\x0b\x32\x15.agent.ConsoleMessage\x12.\n\x0fnetwork_history\x18\x08 \x03(\x0b\x32\x15.agent.NetworkRequest\"C\n\x0e\x43onsoleMessage\x12\r\n\x05level\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x02 \x01(\t\x12\x14\n\x0ctimestamp_ms\x18\x03 \x01(\x03\"h\n\x0eNetworkRequest\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x05\x12\x15\n\rresource_type\x18\x04 \x01(\t\x12\x12\n\nlatency_ms\x18\x05 \x01(\x03\",\n\x0eWorkPoolUpdate\x12\x1a\n\x12\x61vailable_task_ids\x18\x01 \x03(\t\"4\n\x10TaskClaimRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07node_id\x18\x02 \x01(\t\"E\n\x11TaskClaimResponse\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x0f\n\x07granted\x18\x02 \x01(\x08\x12\x0e\n\x06reason\x18\x03 \x01(\t\"\xc1\x01\n\tHeartbeat\x12\x0f\n\x07node_id\x18\x01 \x01(\t\x12\x19\n\x11\x63pu_usage_percent\x18\x02 \x01(\x02\x12\x1c\n\x14memory_usage_percent\x18\x03 \x01(\x02\x12\x1b\n\x13\x61\x63tive_worker_count\x18\x04 \x01(\x05\x12\x1b\n\x13max_worker_capacity\x18\x05 \x01(\x05\x12\x16\n\x0estatus_message\x18\x06 \x01(\t\x12\x18\n\x10running_task_ids\x18\x07 \x03(\t\"-\n\x13HealthCheckResponse\x12\x16\n\x0eserver_time_ms\x18\x01 \x01(\x03\"\xd3\x01\n\x0f\x46ileSyncMessage\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12,\n\x08manifest\x18\x02 \x01(\x0b\x32\x18.agent.DirectoryManifestH\x00\x12\'\n\tfile_data\x18\x03 \x01(\x0b\x32\x12.agent.FilePayloadH\x00\x12#\n\x06status\x18\x04 \x01(\x0b\x32\x11.agent.SyncStatusH\x00\x12%\n\x07\x63ontrol\x18\x05 \x01(\x0b\x32\x12.agent.SyncControlH\x00\x42\t\n\x07payload\"\xc6\x01\n\x0bSyncControl\x12)\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x19.agent.SyncControl.Action\x12\x0c\n\x04path\x18\x02 \x01(\t\x12\x15\n\rrequest_paths\x18\x03 \x03(\t\"g\n\x06\x41\x63tion\x12\x12\n\x0eSTART_WATCHING\x10\x00\x12\x11\n\rSTOP_WATCHING\x10\x01\x12\x08\n\x04LOCK\x10\x02\x12\n\n\x06UNLOCK\x10\x03\x12\x14\n\x10REFRESH_MANIFEST\x10\x04\x12\n\n\x06RESYNC\x10\x05\"F\n\x11\x44irectoryManifest\x12\x11\n\troot_path\x18\x01 \x01(\t\x12\x1e\n\x05\x66iles\x18\x02 \x03(\x0b\x32\x0f.agent.FileInfo\"D\n\x08\x46ileInfo\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\x03\x12\x0c\n\x04hash\x18\x03 \x01(\t\x12\x0e\n\x06is_dir\x18\x04 \x01(\x08\"_\n\x0b\x46ilePayload\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\r\n\x05\x63hunk\x18\x02 \x01(\x0c\x12\x13\n\x0b\x63hunk_index\x18\x03 \x01(\x05\x12\x10\n\x08is_final\x18\x04 \x01(\x08\x12\x0c\n\x04hash\x18\x05 \x01(\t\"\xa0\x01\n\nSyncStatus\x12$\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x16.agent.SyncStatus.Code\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x17\n\x0freconcile_paths\x18\x03 \x03(\t\"B\n\x04\x43ode\x12\x06\n\x02OK\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x16\n\x12RECONCILE_REQUIRED\x10\x02\x12\x0f\n\x0bIN_PROGRESS\x10\x03\x32\xe9\x01\n\x11\x41gentOrchestrator\x12L\n\x11SyncConfiguration\x12\x1a.agent.RegistrationRequest\x1a\x1b.agent.RegistrationResponse\x12\x44\n\nTaskStream\x12\x18.agent.ClientTaskMessage\x1a\x18.agent.ServerTaskMessage(\x01\x30\x01\x12@\n\x0cReportHealth\x12\x10.agent.Heartbeat\x1a\x1a.agent.HealthCheckResponse(\x01\x30\x01\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -36,59 +36,61 @@ _globals['_REGISTRATIONRESPONSE']._serialized_start=447 _globals['_REGISTRATIONRESPONSE']._serialized_end=567 _globals['_CLIENTTASKMESSAGE']._serialized_start=570 - _globals['_CLIENTTASKMESSAGE']._serialized_end=825 - _globals['_NODEANNOUNCE']._serialized_start=827 - _globals['_NODEANNOUNCE']._serialized_end=858 - _globals['_BROWSEREVENT']._serialized_start=861 - _globals['_BROWSEREVENT']._serialized_end=996 - _globals['_SERVERTASKMESSAGE']._serialized_start=999 - _globals['_SERVERTASKMESSAGE']._serialized_end=1268 - _globals['_TASKCANCELREQUEST']._serialized_start=1270 - _globals['_TASKCANCELREQUEST']._serialized_end=1306 - _globals['_TASKREQUEST']._serialized_start=1309 - _globals['_TASKREQUEST']._serialized_end=1518 - _globals['_BROWSERACTION']._serialized_start=1521 - _globals['_BROWSERACTION']._serialized_end=1809 - _globals['_BROWSERACTION_ACTIONTYPE']._serialized_start=1675 - _globals['_BROWSERACTION_ACTIONTYPE']._serialized_end=1809 - _globals['_TASKRESPONSE']._serialized_start=1812 - _globals['_TASKRESPONSE']._serialized_end=2164 - _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_start=2044 - _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_end=2092 - _globals['_TASKRESPONSE_STATUS']._serialized_start=2094 - _globals['_TASKRESPONSE_STATUS']._serialized_end=2154 - _globals['_BROWSERRESPONSE']._serialized_start=2167 - _globals['_BROWSERRESPONSE']._serialized_end=2387 - _globals['_CONSOLEMESSAGE']._serialized_start=2389 - _globals['_CONSOLEMESSAGE']._serialized_end=2456 - _globals['_NETWORKREQUEST']._serialized_start=2458 - _globals['_NETWORKREQUEST']._serialized_end=2562 - _globals['_WORKPOOLUPDATE']._serialized_start=2564 - _globals['_WORKPOOLUPDATE']._serialized_end=2608 - _globals['_TASKCLAIMREQUEST']._serialized_start=2610 - _globals['_TASKCLAIMREQUEST']._serialized_end=2662 - _globals['_TASKCLAIMRESPONSE']._serialized_start=2664 - _globals['_TASKCLAIMRESPONSE']._serialized_end=2733 - _globals['_HEARTBEAT']._serialized_start=2736 - _globals['_HEARTBEAT']._serialized_end=2929 - _globals['_HEALTHCHECKRESPONSE']._serialized_start=2931 - _globals['_HEALTHCHECKRESPONSE']._serialized_end=2976 - _globals['_FILESYNCMESSAGE']._serialized_start=2979 - _globals['_FILESYNCMESSAGE']._serialized_end=3190 - _globals['_SYNCCONTROL']._serialized_start=3193 - _globals['_SYNCCONTROL']._serialized_end=3391 - _globals['_SYNCCONTROL_ACTION']._serialized_start=3288 - _globals['_SYNCCONTROL_ACTION']._serialized_end=3391 - _globals['_DIRECTORYMANIFEST']._serialized_start=3393 - _globals['_DIRECTORYMANIFEST']._serialized_end=3463 - _globals['_FILEINFO']._serialized_start=3465 - _globals['_FILEINFO']._serialized_end=3533 - _globals['_FILEPAYLOAD']._serialized_start=3535 - _globals['_FILEPAYLOAD']._serialized_end=3630 - _globals['_SYNCSTATUS']._serialized_start=3633 - _globals['_SYNCSTATUS']._serialized_end=3793 - _globals['_SYNCSTATUS_CODE']._serialized_start=3727 - _globals['_SYNCSTATUS_CODE']._serialized_end=3793 - _globals['_AGENTORCHESTRATOR']._serialized_start=3796 - _globals['_AGENTORCHESTRATOR']._serialized_end=4029 + _globals['_CLIENTTASKMESSAGE']._serialized_end=867 + _globals['_SKILLEVENT']._serialized_start=869 + _globals['_SKILLEVENT']._serialized_end=990 + _globals['_NODEANNOUNCE']._serialized_start=992 + _globals['_NODEANNOUNCE']._serialized_end=1023 + _globals['_BROWSEREVENT']._serialized_start=1026 + _globals['_BROWSEREVENT']._serialized_end=1161 + _globals['_SERVERTASKMESSAGE']._serialized_start=1164 + _globals['_SERVERTASKMESSAGE']._serialized_end=1433 + _globals['_TASKCANCELREQUEST']._serialized_start=1435 + _globals['_TASKCANCELREQUEST']._serialized_end=1471 + _globals['_TASKREQUEST']._serialized_start=1474 + _globals['_TASKREQUEST']._serialized_end=1683 + _globals['_BROWSERACTION']._serialized_start=1686 + _globals['_BROWSERACTION']._serialized_end=1974 + _globals['_BROWSERACTION_ACTIONTYPE']._serialized_start=1840 + _globals['_BROWSERACTION_ACTIONTYPE']._serialized_end=1974 + _globals['_TASKRESPONSE']._serialized_start=1977 + _globals['_TASKRESPONSE']._serialized_end=2329 + _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_start=2209 + _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_end=2257 + _globals['_TASKRESPONSE_STATUS']._serialized_start=2259 + _globals['_TASKRESPONSE_STATUS']._serialized_end=2319 + _globals['_BROWSERRESPONSE']._serialized_start=2332 + _globals['_BROWSERRESPONSE']._serialized_end=2552 + _globals['_CONSOLEMESSAGE']._serialized_start=2554 + _globals['_CONSOLEMESSAGE']._serialized_end=2621 + _globals['_NETWORKREQUEST']._serialized_start=2623 + _globals['_NETWORKREQUEST']._serialized_end=2727 + _globals['_WORKPOOLUPDATE']._serialized_start=2729 + _globals['_WORKPOOLUPDATE']._serialized_end=2773 + _globals['_TASKCLAIMREQUEST']._serialized_start=2775 + _globals['_TASKCLAIMREQUEST']._serialized_end=2827 + _globals['_TASKCLAIMRESPONSE']._serialized_start=2829 + _globals['_TASKCLAIMRESPONSE']._serialized_end=2898 + _globals['_HEARTBEAT']._serialized_start=2901 + _globals['_HEARTBEAT']._serialized_end=3094 + _globals['_HEALTHCHECKRESPONSE']._serialized_start=3096 + _globals['_HEALTHCHECKRESPONSE']._serialized_end=3141 + _globals['_FILESYNCMESSAGE']._serialized_start=3144 + _globals['_FILESYNCMESSAGE']._serialized_end=3355 + _globals['_SYNCCONTROL']._serialized_start=3358 + _globals['_SYNCCONTROL']._serialized_end=3556 + _globals['_SYNCCONTROL_ACTION']._serialized_start=3453 + _globals['_SYNCCONTROL_ACTION']._serialized_end=3556 + _globals['_DIRECTORYMANIFEST']._serialized_start=3558 + _globals['_DIRECTORYMANIFEST']._serialized_end=3628 + _globals['_FILEINFO']._serialized_start=3630 + _globals['_FILEINFO']._serialized_end=3698 + _globals['_FILEPAYLOAD']._serialized_start=3700 + _globals['_FILEPAYLOAD']._serialized_end=3795 + _globals['_SYNCSTATUS']._serialized_start=3798 + _globals['_SYNCSTATUS']._serialized_end=3958 + _globals['_SYNCSTATUS_CODE']._serialized_start=3892 + _globals['_SYNCSTATUS_CODE']._serialized_end=3958 + _globals['_AGENTORCHESTRATOR']._serialized_start=3961 + _globals['_AGENTORCHESTRATOR']._serialized_end=4194 # @@protoc_insertion_point(module_scope) diff --git a/docker-compose.yml b/docker-compose.yml index 6f6c15a..16a9eb1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,8 @@ - OIDC_SERVER_URL=https://auth.jerxie.com - OIDC_REDIRECT_URI=https://ai.jerxie.com/api/v1/users/login/callback - SUPER_ADMINS=axieyangb@gmail.com,jerxie.app@gmail.com + # IMPORTANT: Agent nodes must have AGENT_SECRET_KEY set to this same value + - SECRET_KEY=aYc2j1lYUUZXkBFFUndnleZI volumes: - ai_hub_data:/app/data:rw deploy: diff --git a/ui/client-app/package-lock.json b/ui/client-app/package-lock.json index 6c7b805..d277649 100644 --- a/ui/client-app/package-lock.json +++ b/ui/client-app/package-lock.json @@ -13,6 +13,8 @@ "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", "diff": "^8.0.2", "diff2html": "^3.4.52", "react": "^19.1.1", @@ -4973,6 +4975,16 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", diff --git a/ui/client-app/package.json b/ui/client-app/package.json index 0463eb8..5b8d64f 100644 --- a/ui/client-app/package.json +++ b/ui/client-app/package.json @@ -8,6 +8,8 @@ "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", "diff": "^8.0.2", "diff2html": "^3.4.52", "react": "^19.1.1", diff --git a/ui/client-app/src/components/MultiNodeConsole.js b/ui/client-app/src/components/MultiNodeConsole.js index 33c97f2..d870b20 100644 --- a/ui/client-app/src/components/MultiNodeConsole.js +++ b/ui/client-app/src/components/MultiNodeConsole.js @@ -3,6 +3,7 @@ const MultiNodeConsole = ({ attachedNodeIds }) => { const [logs, setLogs] = useState({}); // node_id -> array of log strings + const [nodeStats, setNodeStats] = useState({}); // node_id -> stats object const scrollRefs = useRef({}); useEffect(() => { @@ -37,8 +38,16 @@ case 'sync_status': logLine = `[${timestamp}] 📁 SYNC: ${msg.data.message}`; break; + case 'heartbeat': + if (msg.data && msg.data.stats) { + setNodeStats(prev => ({ + ...prev, + [msg.node_id]: msg.data.stats + })); + } + return; default: - return; // Ignore heartbeats etc. + return; } setLogs(prev => ({ @@ -71,24 +80,33 @@
- {attachedNodeIds.map(nodeId => ( -
-
- {nodeId} - NODE_CONTEXT -
-
scrollRefs.current[nodeId] = el} - className="flex-1 overflow-y-auto p-3 space-y-1 scrollbar-thin scrollbar-thumb-gray-800" - > - {(logs[nodeId] || ["[*] Awaiting task..."]).map((line, i) => ( -
- {line} + {attachedNodeIds.map(nodeId => { + const stats = nodeStats[nodeId] || {}; + const cpu = stats.cpu_usage_percent !== undefined ? stats.cpu_usage_percent.toFixed(1) : '0.0'; + const mem = stats.memory_usage_percent !== undefined ? stats.memory_usage_percent.toFixed(1) : '0.0'; + return ( +
+
+ {nodeId} +
+ CPU: {cpu}% + MEM: {mem}% + WORKERS: {stats.active_worker_count || 0}
- ))} +
+
scrollRefs.current[nodeId] = el} + className="flex-1 overflow-y-auto p-3 space-y-1 scrollbar-thin scrollbar-thumb-gray-800" + > + {(logs[nodeId] || ["[*] Awaiting task..."]).map((line, i) => ( +
+ {line} +
+ ))} +
-
- ))} + ) + })}
); diff --git a/ui/client-app/src/components/NodeTerminal.js b/ui/client-app/src/components/NodeTerminal.js new file mode 100644 index 0000000..2ecf8c7 --- /dev/null +++ b/ui/client-app/src/components/NodeTerminal.js @@ -0,0 +1,229 @@ +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import { getNodeStreamUrl } from '../services/apiService'; +import { Terminal } from '@xterm/xterm'; +import { FitAddon } from '@xterm/addon-fit'; +import '@xterm/xterm/css/xterm.css'; + +const NodeTerminal = ({ nodeId }) => { + const [isZoomed, setIsZoomed] = useState(false); + const [debugMode, setDebugMode] = useState(false); + const [latency, setLatency] = useState(null); + + const terminalRef = useRef(null); + const wsRef = useRef(null); + const xtermRef = useRef(null); + const fitAddonRef = useRef(null); + + const sessionId = useMemo(() => `terminal-${nodeId}-${Math.random().toString(36).substr(2, 9)}`, [nodeId]); + + // 1. Core Terminal Engine Initialization + useEffect(() => { + const xterm = new Terminal({ + cursorBlink: true, + theme: { + background: '#030712', // Matches Tailwind bg-gray-950 + foreground: '#10b981', // Matches text-emerald-500 loosely + cursor: '#10b981', + cursorAccent: '#030712', + }, + fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace', + fontSize: 13, + rightClickSelectsWord: true, + scrollback: 1000, + convertEol: true, + scrollOnOutput: true, + }); + + const fitAddon = new FitAddon(); + xterm.loadAddon(fitAddon); + + xterm.open(terminalRef.current); + fitAddon.fit(); + + xtermRef.current = xterm; + fitAddonRef.current = fitAddon; + + // Auto-refit terminal rows/cols when user resizes Chrome window + const observer = new ResizeObserver(() => fitAddon.fit()); + if (terminalRef.current) { + observer.observe(terminalRef.current); + } + + return () => { + observer.disconnect(); + xterm.dispose(); + }; + }, []); + + // 2. Headless WS Comm Link & Keystroke Forwarding + useEffect(() => { + const ws = new WebSocket(getNodeStreamUrl(nodeId)); + wsRef.current = ws; + + ws.onopen = () => { + xtermRef.current?.writeln('\x1b[32m\x1b[1m[CORTEX MESH] Stream Established — Persistent PTY Active\x1b[0m\r'); + // Force initial resize sync + fitAddonRef.current?.fit(); + }; + + ws.onmessage = (event) => { + try { + const msg = JSON.parse(event.data); + + // --- 1. Periodic Latency Monitor (Pong Response) --- + if (msg.event === 'pong' && msg.client_ts) { + const rtt = Date.now() - msg.client_ts; + setLatency(rtt); + } + + // --- 2. Skill Events (Terminal stdout) --- + else if (msg.event === 'skill_event' && msg.data?.type === 'output') { + // Crucial: Just blindly pipe pure PTY ANSI stdout to Xterm directly! No formatting! + xtermRef.current?.write(msg.data.data); + } + else if (msg.event === 'task_error') { + const errStr = msg.data?.stderr || JSON.stringify(msg.data); + xtermRef.current?.writeln(`\r\n\x1b[31m[ERROR] ${errStr}\x1b[0m\r`); + } + else if (debugMode) { + // Debug events visible natively inline in the terminal text + if (msg.event === 'task_start') { + xtermRef.current?.writeln(`\r\n\x1b[33m[DEBUG] task_start id=${msg.task_id || '?'}\x1b[0m\r`); + } else if (msg.event === 'task_complete') { + xtermRef.current?.writeln(`\r\n\x1b[33m[DEBUG] task_complete status=${msg.data?.status ?? '?'} id=${msg.task_id || '?'}\x1b[0m\r`); + } else if (msg.event === 'snapshot') { + xtermRef.current?.writeln(`\x1b[33m[DEBUG] snapshot status=${msg.data?.status || '?'}\x1b[0m\r`); + } + } + } catch (e) { + // Ignore raw failures + } + }; + + ws.onerror = () => xtermRef.current?.writeln('\r\n\x1b[31m[WebSocket connection error]\x1b[0m\r'); + ws.onclose = () => xtermRef.current?.writeln('\r\n\x1b[31m[WebSocket disconnected]\x1b[0m\r'); + + // BIND XTERM DATA TO SOCKET + const disposableData = xtermRef.current?.onData((data) => { + if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify({ + action: "dispatch", + command: JSON.stringify({ tty: data }), + session_id: sessionId + })); + } + }); + + const disposableResize = xtermRef.current?.onResize(({ cols, rows }) => { + if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify({ + action: "dispatch", + command: JSON.stringify({ action: "resize", cols, rows }), + session_id: sessionId + })); + } + }); + + // --- PERIODIC LATENCY PING (Pulse) --- + const pingInterval = setInterval(() => { + if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify({ + action: "ping", + ts: Date.now() + })); + } + }, 3000); + + return () => { + disposableData?.dispose(); + disposableResize?.dispose(); + clearInterval(pingInterval); + ws.close(); + }; + }, [nodeId, sessionId, debugMode]); + + // Force XTerm internal Grid fit on zoom toggle + useEffect(() => { + const timeoutId = setTimeout(() => { + fitAddonRef.current?.fit(); + }, 150); + return () => clearTimeout(timeoutId); + }, [isZoomed]); + + const handleClear = () => { + xtermRef.current?.clear(); + }; + + return ( +
+ {isZoomed &&
setIsZoomed(false)}>
} + + {/* Title Bar */} +
+ + + Interactive Console + — {nodeId} + {latency !== null && ( + + {latency}ms + + )} + +
+ {/* Debug Mode Toggle */} + + + {/* Clear */} + + + {/* Zoom */} + +
+
+ + {/* XTerm Host Area */} + {/* Note: 'h-auto' combined with 'flex-1' guarantees XTerm occupies exactly the rest of the flex parent container */} +
+
+ ); +}; + +export default NodeTerminal; diff --git a/ui/client-app/src/pages/NodesPage.js b/ui/client-app/src/pages/NodesPage.js index 9ca848c..6199f47 100644 --- a/ui/client-app/src/pages/NodesPage.js +++ b/ui/client-app/src/pages/NodesPage.js @@ -1,10 +1,11 @@ import React, { useState, useEffect, useCallback } from 'react'; import { - getAdminNodes, adminCreateNode, adminUpdateNode, + getAdminNodes, adminCreateNode, adminUpdateNode, adminDeleteNode, adminGrantNodeAccess, adminRevokeNodeAccess, adminDownloadNodeBundle, getUserAccessibleNodes, getAdminGroups, getNodeStreamUrl } from '../services/apiService'; +import NodeTerminal from '../components/NodeTerminal'; const NodesPage = ({ user }) => { const [activeTab, setActiveTab] = useState(user?.role === 'admin' ? 'manage' : 'status'); @@ -14,6 +15,10 @@ const [error, setError] = useState(null); const [showCreateModal, setShowCreateModal] = useState(false); const [newNode, setNewNode] = useState({ node_id: '', display_name: '', description: '', skill_config: { shell: { enabled: true }, browser: { enabled: true }, sync: { enabled: true } } }); + const [expandedTerminals, setExpandedTerminals] = useState({}); // node_id -> boolean + const [expandedNodes, setExpandedNodes] = useState({}); // node_id -> boolean + const [editingNodeId, setEditingNodeId] = useState(null); + const [editForm, setEditForm] = useState({ display_name: '', description: '', skill_config: {} }); // WebSocket state for live updates const [meshStatus, setMeshStatus] = useState({}); // node_id -> { status, stats } @@ -91,6 +96,35 @@ } }; + const handleDeleteNode = async (nodeId) => { + if (!window.confirm(`Are you sure you want to deregister node ${nodeId}? This will remove all access grants.`)) return; + try { + await adminDeleteNode(nodeId); + fetchData(); + } catch (err) { + alert(err.message); + } + }; + + const startEditing = (node) => { + setEditingNodeId(node.node_id); + setEditForm({ + display_name: node.display_name, + description: node.description, + skill_config: node.skill_config || {} + }); + }; + + const handleUpdateNode = async (nodeId) => { + try { + await adminUpdateNode(nodeId, editForm); + setEditingNodeId(null); + fetchData(); + } catch (err) { + alert(err.message); + } + }; + return (
{/* Header */} @@ -162,52 +196,281 @@
{nodes.map(node => ( -
-
-
-

{node.display_name}

- - {node.is_active ? 'Active' : 'Disabled'} - - ID: {node.node_id} -
-

{node.description || 'No description provided.'}

- -
- {Object.entries(node.skill_config || {}).map(([skill, cfg]) => ( -
- {skill} - {cfg?.enabled ? ( - - ) : ( - - )} +
+ {/* Top Row: Basic Info & Actions */} +
+
+
+ + + +
+
+

{node.display_name}

+
+ + {node.is_active ? 'Active' : 'Disabled'} + + + {node.node_id}
- ))} +
+
+ +
+ + + +
+
-
- - - {/* Access control button could open another modal */} - -
+ {/* Expanded Panels */} + {(expandedNodes[node.node_id] || expandedTerminals[node.node_id]) && ( +
+ {/* Settings Content */} + {expandedNodes[node.node_id] && ( +
+ {/* Left: General Settings */} +
+
+

General Configuration

+ {editingNodeId !== node.node_id ? ( + + ) : ( +
+ + +
+ )} +
+ +
+
+ + {editingNodeId === node.node_id ? ( + setEditForm({ ...editForm, display_name: e.target.value })} + /> + ) : ( +
+ {node.display_name} +
+ )} +
+
+ + {editingNodeId === node.node_id ? ( +