diff --git a/docs/architecture/cortex_project_todo.md b/docs/architecture/cortex_project_todo.md index 2ee2ecf..275c3d4 100644 --- a/docs/architecture/cortex_project_todo.md +++ b/docs/architecture/cortex_project_todo.md @@ -38,8 +38,8 @@ - **Description**: Migrate `NodeRegistry` and `WorkPool` from in-memory to a persistent backend (Postgres/Redis). - **Priority**: Deferred until **Full System Integration** phase. -### [ ] Workspace Mirroring & Efficient File Sync - ✅ [PHASE 2 COMPLETE](file:///app/docs/architecture/workspace_mirroring_design.md) -- **Description**: Real-time bidirectional sync with multi-node broadcast propagation established. +### [x] Workspace Mirroring & Efficient File Sync (Phase 5: Distributed Conflict Resolution) +- Ghost Mirror server-side patterns, real-time sync, isolation, multi-node sync, ignore filters, lock management, and browser skill integration. ### [ ] Real-time gRPC Log Streaming - **Description**: Bidirectional stream for live `stdout/stderr`. diff --git a/poc-grpc-agent/agent_node/node.py b/poc-grpc-agent/agent_node/node.py index d547ad6..c92d7a5 100644 --- a/poc-grpc-agent/agent_node/node.py +++ b/poc-grpc-agent/agent_node/node.py @@ -16,9 +16,9 @@ """The 'Agent Core': Orchestrates Local Skills and Maintains gRPC Connection.""" def __init__(self, node_id=NODE_ID): self.node_id = node_id - self.skills = SkillManager(max_workers=MAX_SKILL_WORKERS) self.sandbox = SandboxEngine() self.sync_mgr = NodeSyncManager() + self.skills = SkillManager(max_workers=MAX_SKILL_WORKERS, sync_mgr=self.sync_mgr) self.watcher = WorkspaceWatcher(self._on_sync_delta) self.task_queue = queue.Queue() self.stub = get_secure_stub() diff --git a/poc-grpc-agent/agent_node/skills/browser.py b/poc-grpc-agent/agent_node/skills/browser.py index 32ded63..3205b7d 100644 --- a/poc-grpc-agent/agent_node/skills/browser.py +++ b/poc-grpc-agent/agent_node/skills/browser.py @@ -8,9 +8,10 @@ class BrowserSkill(BaseSkill): """The 'Antigravity Bridge': Persistent Browser Skill using a dedicated Actor thread.""" - def __init__(self): + def __init__(self, sync_mgr=None): self.task_queue = queue.Queue() self.sessions = {} # session_id -> { "context": Context, "page": Page } + self.sync_mgr = sync_mgr self.lock = threading.Lock() threading.Thread(target=self._browser_actor, daemon=True, name="BrowserActor").start() @@ -33,6 +34,18 @@ ) ))) + # Live Download Redirector + page.on("download", lambda download: self._handle_download(sid, download)) + + def _handle_download(self, sid, download): + """Saves browser downloads directly into the synchronized session workspace.""" + with self.lock: + sess = self.sessions.get(sid) + if sess and sess.get("download_dir"): + target = os.path.join(sess["download_dir"], download.suggested_filename) + print(f" [🌐📥] Browser Download Sync: {download.suggested_filename} -> {target}") + download.save_as(target) + def _browser_actor(self): """Serializes all Playwright operations on a single dedicated thread.""" print("[🌐] Browser Actor Starting...", flush=True) @@ -63,10 +76,16 @@ with self.lock: if sid not in self.sessions: - ctx = browser.new_context() + # Phase 4: Mount workspace for downloads/uploads + download_dir = None + if self.sync_mgr and task.session_id: + download_dir = self.sync_mgr.get_session_dir(task.session_id) + print(f" [🌐📁] Mapping Browser Context to: {download_dir}") + + ctx = browser.new_context(accept_downloads=True) pg = ctx.new_page() self._setup_listeners(sid, pg, on_event) - self.sessions[sid] = {"context": ctx, "page": pg} + self.sessions[sid] = {"context": ctx, "page": pg, "download_dir": download_dir} page = self.sessions[sid]["page"] print(f" [🌐] Browser Actor Processing: {agent_pb2.BrowserAction.ActionType.Name(action.action)} | Session: {sid}", flush=True) diff --git a/poc-grpc-agent/agent_node/skills/manager.py b/poc-grpc-agent/agent_node/skills/manager.py index 616a4ea..f5a85b8 100644 --- a/poc-grpc-agent/agent_node/skills/manager.py +++ b/poc-grpc-agent/agent_node/skills/manager.py @@ -6,12 +6,13 @@ class SkillManager: """Orchestrates multiple modular skills and manages the task worker pool.""" - def __init__(self, max_workers=MAX_SKILL_WORKERS): + def __init__(self, max_workers=MAX_SKILL_WORKERS, sync_mgr=None): self.executor = futures.ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix="skill-worker") self.active_tasks = {} # task_id -> future + self.sync_mgr = sync_mgr self.skills = { - "shell": ShellSkill(), - "browser": BrowserSkill() + "shell": ShellSkill(sync_mgr=sync_mgr), + "browser": BrowserSkill(sync_mgr=sync_mgr) } self.max_workers = max_workers self.lock = threading.Lock() diff --git a/poc-grpc-agent/agent_node/skills/shell.py b/poc-grpc-agent/agent_node/skills/shell.py index 8b4b1e0..9d17464 100644 --- a/poc-grpc-agent/agent_node/skills/shell.py +++ b/poc-grpc-agent/agent_node/skills/shell.py @@ -4,8 +4,9 @@ class ShellSkill(BaseSkill): """Default Skill: Executing shell commands with sandbox safety.""" - def __init__(self): + def __init__(self, sync_mgr=None): self.processes = {} # task_id -> Popen + self.sync_mgr = sync_mgr self.lock = threading.Lock() def execute(self, task, sandbox, on_complete, on_event=None): @@ -21,7 +22,14 @@ # 2. Sequential Execution print(f" [🐚] Executing Shell: {cmd}", flush=True) - p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + # Resolve CWD for the skill based on session_id + 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}") + + p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=cwd) with self.lock: self.processes[task.task_id] = p diff --git a/poc-grpc-agent/orchestrator/app.py b/poc-grpc-agent/orchestrator/app.py index 34ac451..dfd5fda 100644 --- a/poc-grpc-agent/orchestrator/app.py +++ b/poc-grpc-agent/orchestrator/app.py @@ -58,11 +58,6 @@ return target_node = active_nodes[0] - # Phase 1: Direct Shell Task - print(f"\n[📤] Dispatching shell task to {target_node}") - res_single = orch.assistant.dispatch_single(target_node, 'uname -a') - print(f" Uname Output: {res_single}", flush=True) - # Ghost Mirror Sync Phase 1 & 2 print("\n[🧠] AI Phase: Ghost Mirror Workspace Sync (Multi-Node Broadcast)...") for node_id in active_nodes: @@ -72,6 +67,11 @@ # Start watching only on the first node to test broadcast to others orch.assistant.control_sync(active_nodes[0], "test-session-001", action="START") + # Phase 3: Context-Aware Skills (Shell + Browser) + print("\n[🧠] AI Phase 3: Executing Context-Aware Shell Task...") + res_single = orch.assistant.dispatch_single(target_node, 'ls -la', session_id="test-session-001") + print(f" CWD Listing Output: {res_single}", flush=True) + # Phase 3: LOCK Test (Simulate an AI edit phase where user edits are blocked) time.sleep(10) print("\n[🔒] Orchestrator: Locking Node 0 to prevent user interference (Phase 3)...") @@ -82,9 +82,9 @@ nav_action = agent_pb2.BrowserAction( action=agent_pb2.BrowserAction.NAVIGATE, url="https://google.com", - session_id="br-session-456" + session_id="test-session-001" ) - res_browser = orch.assistant.dispatch_browser(target_node, nav_action) + res_browser = orch.assistant.dispatch_browser(target_node, nav_action, session_id="test-session-001") print(f" Browser Result: {res_browser}", flush=True) # Stay alive for diagnostics diff --git a/poc-grpc-agent/orchestrator/services/assistant.py b/poc-grpc-agent/orchestrator/services/assistant.py index fad8643..26bad63 100644 --- a/poc-grpc-agent/orchestrator/services/assistant.py +++ b/poc-grpc-agent/orchestrator/services/assistant.py @@ -121,7 +121,7 @@ ) )) - def dispatch_single(self, node_id, cmd, timeout=30): + def dispatch_single(self, node_id, cmd, timeout=30, session_id=None): """Dispatches a shell command to a specific node.""" node = self.registry.get_node(node_id) if not node: return {"error": f"Node {node_id} Offline"} @@ -132,7 +132,7 @@ # 12-Factor Signing Logic sig = sign_payload(cmd) req = agent_pb2.ServerTaskMessage(task_request=agent_pb2.TaskRequest( - task_id=tid, payload_json=cmd, signature=sig)) + task_id=tid, payload_json=cmd, signature=sig, session_id=session_id)) print(f"[📤] Dispatching shell {tid} to {node_id}") node["queue"].put(req) @@ -144,7 +144,7 @@ self.journal.pop(tid) return {"error": "Timeout"} - def dispatch_browser(self, node_id, action, timeout=60): + def dispatch_browser(self, node_id, action, timeout=60, session_id=None): """Dispatches a browser action to a directed session node.""" node = self.registry.get_node(node_id) if not node: return {"error": f"Node {node_id} Offline"} @@ -160,7 +160,7 @@ ) req = agent_pb2.ServerTaskMessage(task_request=agent_pb2.TaskRequest( - task_id=tid, browser_action=action, signature=sig)) + task_id=tid, browser_action=action, signature=sig, session_id=session_id)) print(f"[🌐📤] Dispatching browser {tid} to {node_id}") node["queue"].put(req) diff --git a/poc-grpc-agent/protos/agent.proto b/poc-grpc-agent/protos/agent.proto index adb9d8b..4ab3d8c 100644 --- a/poc-grpc-agent/protos/agent.proto +++ b/poc-grpc-agent/protos/agent.proto @@ -89,6 +89,7 @@ int32 timeout_ms = 4; string trace_id = 5; string signature = 6; + string session_id = 8; // NEW: Map execution to a sync workspace } message BrowserAction { diff --git a/poc-grpc-agent/protos/agent_pb2.py b/poc-grpc-agent/protos/agent_pb2.py index 691a6ee..35cea2a 100644 --- a/poc-grpc-agent/protos/agent_pb2.py +++ b/poc-grpc-agent/protos/agent_pb2.py @@ -14,7 +14,7 @@ -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\"\xbd\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(\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\"\xa3\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\"[\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\"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\"\x87\x01\n\nSyncStatus\x12$\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x16.agent.SyncStatus.Code\x12\x0f\n\x07message\x18\x02 \x01(\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\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\"\xa3\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\"[\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\"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\"\x87\x01\n\nSyncStatus\x12$\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x16.agent.SyncStatus.Code\x12\x0f\n\x07message\x18\x02 \x01(\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) @@ -46,49 +46,49 @@ _globals['_TASKCANCELREQUEST']._serialized_start=1277 _globals['_TASKCANCELREQUEST']._serialized_end=1313 _globals['_TASKREQUEST']._serialized_start=1316 - _globals['_TASKREQUEST']._serialized_end=1505 - _globals['_BROWSERACTION']._serialized_start=1508 - _globals['_BROWSERACTION']._serialized_end=1796 - _globals['_BROWSERACTION_ACTIONTYPE']._serialized_start=1662 - _globals['_BROWSERACTION_ACTIONTYPE']._serialized_end=1796 - _globals['_TASKRESPONSE']._serialized_start=1799 - _globals['_TASKRESPONSE']._serialized_end=2151 - _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_start=2031 - _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_end=2079 - _globals['_TASKRESPONSE_STATUS']._serialized_start=2081 - _globals['_TASKRESPONSE_STATUS']._serialized_end=2141 - _globals['_BROWSERRESPONSE']._serialized_start=2154 - _globals['_BROWSERRESPONSE']._serialized_end=2374 - _globals['_CONSOLEMESSAGE']._serialized_start=2376 - _globals['_CONSOLEMESSAGE']._serialized_end=2443 - _globals['_NETWORKREQUEST']._serialized_start=2445 - _globals['_NETWORKREQUEST']._serialized_end=2549 - _globals['_WORKPOOLUPDATE']._serialized_start=2551 - _globals['_WORKPOOLUPDATE']._serialized_end=2595 - _globals['_TASKCLAIMREQUEST']._serialized_start=2597 - _globals['_TASKCLAIMREQUEST']._serialized_end=2649 - _globals['_TASKCLAIMRESPONSE']._serialized_start=2651 - _globals['_TASKCLAIMRESPONSE']._serialized_end=2720 - _globals['_HEARTBEAT']._serialized_start=2723 - _globals['_HEARTBEAT']._serialized_end=2916 - _globals['_HEALTHCHECKRESPONSE']._serialized_start=2918 - _globals['_HEALTHCHECKRESPONSE']._serialized_end=2963 - _globals['_FILESYNCMESSAGE']._serialized_start=2966 - _globals['_FILESYNCMESSAGE']._serialized_end=3177 - _globals['_SYNCCONTROL']._serialized_start=3180 - _globals['_SYNCCONTROL']._serialized_end=3343 - _globals['_SYNCCONTROL_ACTION']._serialized_start=3252 - _globals['_SYNCCONTROL_ACTION']._serialized_end=3343 - _globals['_DIRECTORYMANIFEST']._serialized_start=3345 - _globals['_DIRECTORYMANIFEST']._serialized_end=3415 - _globals['_FILEINFO']._serialized_start=3417 - _globals['_FILEINFO']._serialized_end=3485 - _globals['_FILEPAYLOAD']._serialized_start=3487 - _globals['_FILEPAYLOAD']._serialized_end=3582 - _globals['_SYNCSTATUS']._serialized_start=3585 - _globals['_SYNCSTATUS']._serialized_end=3720 - _globals['_SYNCSTATUS_CODE']._serialized_start=3654 - _globals['_SYNCSTATUS_CODE']._serialized_end=3720 - _globals['_AGENTORCHESTRATOR']._serialized_start=3723 - _globals['_AGENTORCHESTRATOR']._serialized_end=3956 + _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=3363 + _globals['_SYNCCONTROL_ACTION']._serialized_start=3272 + _globals['_SYNCCONTROL_ACTION']._serialized_end=3363 + _globals['_DIRECTORYMANIFEST']._serialized_start=3365 + _globals['_DIRECTORYMANIFEST']._serialized_end=3435 + _globals['_FILEINFO']._serialized_start=3437 + _globals['_FILEINFO']._serialized_end=3505 + _globals['_FILEPAYLOAD']._serialized_start=3507 + _globals['_FILEPAYLOAD']._serialized_end=3602 + _globals['_SYNCSTATUS']._serialized_start=3605 + _globals['_SYNCSTATUS']._serialized_end=3740 + _globals['_SYNCSTATUS_CODE']._serialized_start=3674 + _globals['_SYNCSTATUS_CODE']._serialized_end=3740 + _globals['_AGENTORCHESTRATOR']._serialized_start=3743 + _globals['_AGENTORCHESTRATOR']._serialized_end=3976 # @@protoc_insertion_point(module_scope) diff --git a/poc-grpc-agent/test_mesh.py b/poc-grpc-agent/test_mesh.py new file mode 100644 index 0000000..012cf8e --- /dev/null +++ b/poc-grpc-agent/test_mesh.py @@ -0,0 +1,107 @@ + +import time +import subprocess +import os +import signal + +def run_mesh_test(): + print("[🚀] Starting Collaborative Mesh Test...") + print("[🛡️] Orchestrator: Starting...") + + # 1. Start Orchestrator + orchestrator = subprocess.Popen( + ["python3", "-m", "orchestrator.app"], + cwd="/app/poc-grpc-agent", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1 + ) + + time.sleep(3) # Wait for start + + print("[🤖] Node Alpha: Starting...") + # 2. Start Agent Node 1 + node1 = subprocess.Popen( + ["python3", "-m", "agent_node.main"], + cwd="/app/poc-grpc-agent", + env={**os.environ, "AGENT_NODE_ID": "node-alpha", "CORTEX_SYNC_DIR": "/tmp/cortex-sync-alpha"}, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1 + ) + + print("[🤖] Node Beta: Starting...") + # 3. Start Agent Node 2 + node2 = subprocess.Popen( + ["python3", "-m", "agent_node.main"], + cwd="/app/poc-grpc-agent", + env={**os.environ, "AGENT_NODE_ID": "node-beta", "CORTEX_SYNC_DIR": "/tmp/cortex-sync-beta"}, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1 + ) + + print("[⏳] Running simulation for 60 seconds...") + start_time = time.time() + + # Simple thread to print outputs in real-time + import threading + def pipe_output(name, pipe): + for line in pipe: + print(f"[{name}] {line.strip()}") + + threading.Thread(target=pipe_output, args=("ORCH", orchestrator.stdout), daemon=True).start() + threading.Thread(target=pipe_output, args=("N1", node1.stdout), daemon=True).start() + threading.Thread(target=pipe_output, args=("N2", node2.stdout), daemon=True).start() + + # Simulate a local edit on Node Alpha (N1) after a delay to test real-time sync + def simulate_local_edit(): + time.sleep(22) + root_alpha = "/tmp/cortex-sync-alpha/test-session-001" + os.makedirs(root_alpha, exist_ok=True) + + # 1. Create .cortexignore + print(f"\n[📝] User Sim: Creating .cortexignore on Node Alpha...") + with open(os.path.join(root_alpha, ".cortexignore"), "w") as f: + f.write("*.tmp\nsecret.txt\n") + + # 2. Edit hello.py (Should Sync) + sync_file = os.path.join(root_alpha, "hello.py") + print(f"[📝] User Sim: Editing {sync_file} (Should Sync)...") + with open(sync_file, "a") as f: + f.write("\n# Phase 3: Regular edit\n") + + # 3. Create secret.txt (Should be IGNORED) + secret_file = os.path.join(root_alpha, "secret.txt") + print(f"[📝] User Sim: Creating {secret_file} (Should be IGNORED)...") + with open(secret_file, "w") as f: + f.write("THIS SHOULD NOT SYNC") + + time.sleep(20) # Wait for Lock reliably + # 4. Workspace LOCK Test + print(f"\n[🔒] User Sim: Node Alpha should be LOCKED by Orchestrator now...") + locked_file = os.path.join(root_alpha, "hello.py") + with open(locked_file, "a") as f: + f.write("\n# USER TRYING TO EDIT WHILE LOCKED\n") + + threading.Thread(target=simulate_local_edit, daemon=True).start() + + time.sleep(60) + + # 4. Cleanup + print("\n[🛑] Test Finished. Terminating processes...") + orchestrator.terminate() + node1.terminate() + node2.terminate() + + time.sleep(2) + orchestrator.kill() + node1.kill() + node2.kill() + print("[✅] Done.") + +if __name__ == "__main__": + run_mesh_test()