diff --git a/agent-node/src/agent_node/skills/shell_bridge.py b/agent-node/src/agent_node/skills/shell_bridge.py index f805571..b71fd22 100644 --- a/agent-node/src/agent_node/skills/shell_bridge.py +++ b/agent-node/src/agent_node/skills/shell_bridge.py @@ -298,7 +298,11 @@ with sess["write_lock"]: if cmd.startswith("!RAW:"): if not tid.startswith("task-"): - sess["backend"].write((cmd[5:] + "\n").encode("utf-8")) + raw_payload = cmd[5:] + # Only append newline if it's a full command (len > 1) and lacks one + if len(raw_payload) > 1 and not (raw_payload.endswith("\n") or raw_payload.endswith("\r")): + raw_payload += "\n" + sess["backend"].write(raw_payload.encode("utf-8")) return on_complete(tid, {"stdout": "INJECTED", "status": 0}, task.trace_id) cmd = cmd[5:] diff --git a/agent-node/src/agent_node/skills/terminal_backends.py b/agent-node/src/agent_node/skills/terminal_backends.py index a934a8e..73116f5 100644 --- a/agent-node/src/agent_node/skills/terminal_backends.py +++ b/agent-node/src/agent_node/skills/terminal_backends.py @@ -39,40 +39,45 @@ class PosixTerminal(BaseTerminal): - """POSIX implementation using subprocess.Popen for stability.""" + """POSIX implementation using pty.fork for a true PTY.""" def __init__(self): - self.proc = None + self.pid = None self.fd = None def spawn(self, cwd=None, env=None): - import subprocess + import pty import os + import fcntl shell_path = "/bin/bash" if not os.path.exists(shell_path): shell_path = "/bin/sh" - self.proc = subprocess.Popen( - [shell_path], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - cwd=cwd, - env=env, - bufsize=0 - ) - self.fd = self.proc.stdout.fileno() - - import fcntl - fl = fcntl.fcntl(self.fd, fcntl.F_GETFL) - fcntl.fcntl(self.fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) + if env is None: + env = os.environ.copy() + if "TERM" not in env: + env["TERM"] = "xterm-256color" + + pid, fd = pty.fork() + if pid == 0: + if cwd: + try: os.chdir(cwd) + except: pass + os.execvpe(shell_path, [shell_path], env) + else: + self.pid = pid + self.fd = fd + + fl = fcntl.fcntl(self.fd, fcntl.F_GETFL) + fcntl.fcntl(self.fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) def read(self, size=4096) -> bytes: if self.fd is None: return b"" try: import select + import os r, _, _ = select.select([self.fd], [], [], 0.05) if self.fd in r: return os.read(self.fd, size) @@ -81,30 +86,47 @@ return b"" def write(self, data: bytes): - if self.proc and self.proc.stdin: + if self.fd is not None: try: - self.proc.stdin.write(data) - self.proc.stdin.flush() - except BrokenPipeError: + import os + os.write(self.fd, data) + except OSError: pass def resize(self, cols: int, rows: int): - pass + if self.fd is not None: + try: + import fcntl + import termios + import struct + winsize = struct.pack("HHHH", rows, cols, 0, 0) + fcntl.ioctl(self.fd, termios.TIOCSWINSZ, winsize) + except Exception: + pass def kill(self): - if self.proc: + if self.pid: + import os + import signal try: - self.proc.kill() - self.proc.wait(timeout=1) - except: + os.kill(self.pid, signal.SIGKILL) + os.waitpid(self.pid, 0) + except Exception: pass - self.proc = None + self.pid = None self.fd = None def is_alive(self) -> bool: - if self.proc is None: + if self.pid is None: return False - return self.proc.poll() is None + import os + try: + pid, status = os.waitpid(self.pid, os.WNOHANG) + if pid == self.pid: + return False + return True + except ChildProcessError: + return False class WindowsTerminal(BaseTerminal): @@ -124,15 +146,23 @@ # M7: Force TERM=dumb to suppress complex ANSI sequences that clobber rendering os.environ["TERM"] = "dumb" + if env is not None: + env["TERM"] = "dumb" + # pywinpty expects env to be a string formatted as "KEY=VALUE\0...KEY=VALUE\0\0" + if isinstance(env, dict): + env_str = "".join(f"{k}={v}\0" for k, v in env.items()) + "\0" + else: + env_str = env + self.pty = PTY(140, 40) try: - self.pty.spawn(shell_cmd, cwd=cwd, env=env) + self.pty.spawn(shell_cmd, cwd=cwd, env=env_str) except Exception as e: # Fallback for cwd issues on Windows if cwd: print(f"[!] Warning: Failed to spawn shell in {cwd} ({e}). Retrying in root...") - self.pty.spawn(shell_cmd, cwd=None, env=env) + self.pty.spawn(shell_cmd, cwd=None, env=env_str) else: raise e @@ -158,6 +188,8 @@ import time # pywinpty expects strings for input text = data.decode('utf-8', errors='replace') + # Fix backspace key: Xterm sends DEL (\x7f), but Windows expects Backspace (\x08) + text = text.replace('\x7f', '\x08') # Chunk writes to prevent PyWinPTY/ConHost input buffer saturation drops on Windows # Conhost is highly sensitive to rapid buffer writes over 120 bytes. chunk_size = 32 diff --git a/deployment/test-nodes/docker-compose.test-nodes.yml b/deployment/test-nodes/docker-compose.test-nodes.yml index 732f6b9..f8b9746 100644 --- a/deployment/test-nodes/docker-compose.test-nodes.yml +++ b/deployment/test-nodes/docker-compose.test-nodes.yml @@ -16,12 +16,15 @@ - AGENT_AUTH_TOKEN=cortex-secret-shared-key - AGENT_TLS_ENABLED=false - DEBUG_GRPC=true + - PYTHONPATH=/app:/app/mesh-sdk restart: always cap_add: - NET_ADMIN privileged: true volumes: - ./skills:/app/node_skills:ro + - ./mesh-sdk:/app/mesh-sdk:ro + - ./agent-node/src/agent_node:/app/src/agent_node:ro deploy: resources: limits: @@ -40,12 +43,15 @@ - AGENT_AUTH_TOKEN=ysHjZIRXeWo-YYK6EWtBsIgJ4uNBihSnZMtt0BQW3eI - AGENT_TLS_ENABLED=false - DEBUG_GRPC=true + - PYTHONPATH=/app:/app/mesh-sdk restart: always cap_add: - NET_ADMIN privileged: true volumes: - ./skills:/app/node_skills:ro + - ./mesh-sdk:/app/mesh-sdk:ro + - ./agent-node/src/agent_node:/app/src/agent_node:ro deploy: resources: limits: diff --git a/docker-compose.yml b/docker-compose.yml index 998126a..9c6fe6f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ - "${FRONTEND_PORT:-8002}:80" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./frontend/build:/usr/share/nginx/html:ro deploy: resources: limits: diff --git a/frontend/src/features/chat/components/ChatWindow.css b/frontend/src/features/chat/components/ChatWindow.css index a75cc4b..0055a03 100644 --- a/frontend/src/features/chat/components/ChatWindow.css +++ b/frontend/src/features/chat/components/ChatWindow.css @@ -263,7 +263,29 @@ vertical-align: top !important; } +.dark .markdown-preview td { + border-bottom-color: rgba(255, 255, 255, 0.05) !important; +} + .dark .markdown-preview th { background: rgba(255, 255, 255, 0.03) !important; color: #818cf8 !important; } + +/* Clear Hyperlink Styling */ +.markdown-preview a { + color: #4f46e5 !important; + text-decoration: underline !important; + font-weight: 600 !important; + transition: opacity 0.2s; + word-break: break-all; +} + +.markdown-preview a:hover { + opacity: 0.8 !important; +} + +.dark .markdown-preview a { + color: #818cf8 !important; +} + diff --git a/mesh-sdk/tests/test_mesh_robustness.py b/mesh-sdk/tests/test_mesh_robustness.py index a70e3b1..4966997 100644 --- a/mesh-sdk/tests/test_mesh_robustness.py +++ b/mesh-sdk/tests/test_mesh_robustness.py @@ -24,17 +24,13 @@ sys.modules['mesh_core.models'] = MagicMock() sys.modules['mesh_core.models.agent_pb2'] = mock_pb2 -# Add mesh_core dir directly -_mesh_core_path = os.path.join(_root, "mesh_core") -sys.path.append(_mesh_core_path) -sys.path.append(os.path.join(_mesh_core_path, "engines")) -sys.path.append(os.path.join(_mesh_core_path, "transport")) -sys.path.append(os.path.join(_mesh_core_path, "utils")) +# Add mesh-sdk dir +sys.path.append(_root) # Now import the modules directly -import server as server_mod -import node as node_mod -import data as data_mod +import mesh_core.engines.server as server_mod +import mesh_core.engines.node as node_mod +import mesh_core.utils.data as data_mod MeshServerCore = server_mod.MeshServerCore MeshNodeCore = node_mod.MeshNodeCore @@ -85,5 +81,22 @@ self.assertEqual(recovered, ["t1", "t3"]) print("✅ Recovery Logic Passed") + def test_windows_terminal_compatibility(self): + """Verifies Windows terminal backspace and env mapping logic.""" + # 1. Backspace check + input_data = b"dir\x7f" + text = input_data.decode('utf-8', errors='replace') + text = text.replace('\x7f', '\x08') + self.assertEqual(text, "dir\x08") + + # 2. Env Block conversion check + env = {"TERM": "dumb", "PATH": "C:\\Windows"} + if isinstance(env, dict): + env_str = "".join(f"{k}={v}\0" for k, v in env.items()) + "\0" + + expected_str = "TERM=dumb\0PATH=C:\\Windows\0\0" + self.assertEqual(env_str, expected_str) + print("✅ Windows Compatibility Checks Passed") + if __name__ == "__main__": unittest.main() diff --git a/mesh-sdk/tools/fake_nodes/windows_client.py b/mesh-sdk/tools/fake_nodes/windows_client.py new file mode 100644 index 0000000..39eda51 --- /dev/null +++ b/mesh-sdk/tools/fake_nodes/windows_client.py @@ -0,0 +1,156 @@ +import sys +import os +import time +import logging +import platform + +# Mock platform BEFORE anything else is loaded +original_system = platform.system +platform.system = lambda: "Windows" + +# Add paths to use mesh-sdk +_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(_root) + +from mesh_core.engines.node import MeshNodeCore +from mesh_core.transport.grpc import GrpcMeshTransport +from mesh_core import agent_pb2, agent_pb2_grpc +import grpc + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("FakedWindowsNode") + +def get_stub(): + target = os.getenv("GRPC_ENDPOINT", "192.168.68.113:50051") + logger.info(f"Connecting to {target}...") + channel = grpc.insecure_channel(target) + stub = agent_pb2_grpc.AgentOrchestratorStub(channel) + return stub, channel + +class FakedWindowsNode(MeshNodeCore): + def __init__(self, node_id: str, auth_token: str): + self.transport = GrpcMeshTransport(node_id, get_stub, auth_token=auth_token) + super().__init__(node_id, self.transport) + + self.on_task = self.handle_task + self.on_sync = self.handle_sync + self.on_ready = lambda _: logger.info(f"[*] FakedWindowsNode '{node_id}' is AUTHORIZED and ONLINE.") + self.on_disconnect = lambda: logger.warning("[!] Lost connection to Hub.") + + def handle_task(self, task): + logger.info(f"[Task] Executing: {task.task_id} (Type: {task.task_type})") + payload = task.payload_json + logger.info(f"[Task] Payload: {payload}") + + # Simulate Terminal output stream if it's an interactive task + if "tty" in payload or task.task_type == "interactive_shell": + import json + try: + cmd_data = json.loads(payload) if isinstance(payload, str) else payload + tty_input = cmd_data.get("tty", "") + if tty_input: + # Echo back the input and a prompt + logger.info(f"[PTY] Echoing input: {repr(tty_input)}") + resp = agent_pb2.TaskResponse( + task_id=task.task_id, + stdout=tty_input + "\r\nC:\\FakeWindows> ", + status=0 + ) + self.send_message(agent_pb2.ClientTaskMessage(task_response=resp)) + except Exception as e: + pass + else: + response = agent_pb2.TaskResponse( + task_id=task.task_id, + stdout=f"C:\\> Faked Windows Execution Success!\nReceived command: {payload}", + status=0 + ) + self.send_message(agent_pb2.ClientTaskMessage(task_response=response)) + + def handle_sync(self, sync_msg): + logger.info(f"[Sync] Received Sync Message: {sync_msg.WhichOneof('payload')}") + if sync_msg.HasField('control'): + ctrl = sync_msg.control + if ctrl.action == agent_pb2.SyncControl.LIST: + logger.info(f"[Sync] Answering LIST command for path: {ctrl.path}") + # Hardcode a fake Windows directory response + manifest = agent_pb2.DirectoryManifest( + root_path=ctrl.path, + files=[ + agent_pb2.FileInfo(path="fake_windows_folder", size=0, is_dir=True), + agent_pb2.FileInfo(path="boot.ini", size=1024, is_dir=False), + agent_pb2.FileInfo(path="autoexec.bat", size=256, is_dir=False) + ], + is_final=True + ) + response = agent_pb2.ClientTaskMessage( + file_sync=agent_pb2.FileSyncMessage( + session_id=sync_msg.session_id, + task_id=sync_msg.task_id, + manifest=manifest + ) + ) + self.send_message(response) + + + def start_health_reporting(self): + import threading + def _report(): + while not self._stop_event.is_set(): + if self.transport.is_connected(): + hb = agent_pb2.Heartbeat( + node_id=self.node_id, + cpu_usage_percent=12.5, + memory_usage_percent=45.0, + active_worker_count=0, + max_worker_capacity=5, + status_message="Windows node running fine", + running_task_ids=[], + cpu_count=8, + memory_used_gb=16.0, + memory_total_gb=32.0 + ) + self.transport.send_health(hb) + time.sleep(5) + threading.Thread(target=_report, daemon=True, name="HealthReporter").start() + +if __name__ == "__main__": + import requests + + API_URL = "http://192.168.68.113:8002/api/v1" + HEADERS = { + "X-User-ID": "37471c66-9da0-42a5-8b00-8b2f5bb46baa", # Auto-bypass user + "X-Proxy-Secret": "aYc2j1lYUUZXkBFFUndnleZI" + } + NODE_ID = "faked-windows-node" + + # 1. Register the node to get an invite token + logger.info(f"Registering node '{NODE_ID}' via REST API...") + try: + # First check if it exists and delete it, or just create it + requests.delete(f"{API_URL}/nodes/admin/{NODE_ID}", headers=HEADERS) + + res = requests.post(f"{API_URL}/nodes/admin", headers=HEADERS, json={ + "node_id": NODE_ID, + "display_name": "Faked Windows Node", + "description": "Mocked Windows client for debugging" + }) + res.raise_for_status() + auth_token = res.json().get("invite_token") + logger.info(f"Successfully registered. Invite Token: {auth_token[:8]}...") + except Exception as e: + logger.error(f"Failed to register node: {e}") + if hasattr(e, 'response') and e.response is not None: + logger.error(f"Response: {e.response.text}") + sys.exit(1) + + # 2. Start the Mesh Node + node = FakedWindowsNode(NODE_ID, auth_token) + + if node.start(): + node.start_health_reporting() + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + node.stop() diff --git a/scripts/deploy_to_prod.exp b/scripts/deploy_to_prod.exp index 84ec12e..21cdd8e 100755 --- a/scripts/deploy_to_prod.exp +++ b/scripts/deploy_to_prod.exp @@ -5,7 +5,7 @@ set env(DEPLOYMENT_SNIPPET_ID) "de6bc89a046776eb3c87544d2c06b39f" set env(REMOTE_PASS) "a6163484a" set env(PATH) "/opt/homebrew/bin:$env(PATH)" -set env(LOCAL_APP_DIR) "/Users/axieyangb/Project/CortexAI" +set env(LOCAL_APP_DIR) "/Users/axieyangb/Project/cortex-hub" spawn bash scripts/remote_deploy.sh expect { "password:" {