diff --git a/poc-grpc-agent/agent_pb2.py b/poc-grpc-agent/agent_pb2.py index a0eacb3..f51d6f2 100644 --- a/poc-grpc-agent/agent_pb2.py +++ b/poc-grpc-agent/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\"{\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\x42\t\n\x07payload\"\xe0\x01\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\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\"\xda\x01\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\"W\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\x05\x43LOSE\x10\x07\"\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\"T\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\",\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\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\"\xd2\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\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\"\xe0\x01\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\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\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) @@ -35,36 +35,44 @@ _globals['_SANDBOXPOLICY_MODE']._serialized_end=445 _globals['_REGISTRATIONRESPONSE']._serialized_start=447 _globals['_REGISTRATIONRESPONSE']._serialized_end=567 - _globals['_CLIENTTASKMESSAGE']._serialized_start=569 - _globals['_CLIENTTASKMESSAGE']._serialized_end=692 - _globals['_SERVERTASKMESSAGE']._serialized_start=695 - _globals['_SERVERTASKMESSAGE']._serialized_end=919 - _globals['_TASKCANCELREQUEST']._serialized_start=921 - _globals['_TASKCANCELREQUEST']._serialized_end=957 - _globals['_TASKREQUEST']._serialized_start=960 - _globals['_TASKREQUEST']._serialized_end=1149 - _globals['_BROWSERACTION']._serialized_start=1152 - _globals['_BROWSERACTION']._serialized_end=1370 - _globals['_BROWSERACTION_ACTIONTYPE']._serialized_start=1283 - _globals['_BROWSERACTION_ACTIONTYPE']._serialized_end=1370 - _globals['_TASKRESPONSE']._serialized_start=1373 - _globals['_TASKRESPONSE']._serialized_end=1725 - _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_start=1605 - _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_end=1653 - _globals['_TASKRESPONSE_STATUS']._serialized_start=1655 - _globals['_TASKRESPONSE_STATUS']._serialized_end=1715 - _globals['_BROWSERRESPONSE']._serialized_start=1727 - _globals['_BROWSERRESPONSE']._serialized_end=1811 - _globals['_WORKPOOLUPDATE']._serialized_start=1813 - _globals['_WORKPOOLUPDATE']._serialized_end=1857 - _globals['_TASKCLAIMREQUEST']._serialized_start=1859 - _globals['_TASKCLAIMREQUEST']._serialized_end=1911 - _globals['_TASKCLAIMRESPONSE']._serialized_start=1913 - _globals['_TASKCLAIMRESPONSE']._serialized_end=1982 - _globals['_HEARTBEAT']._serialized_start=1985 - _globals['_HEARTBEAT']._serialized_end=2178 - _globals['_HEALTHCHECKRESPONSE']._serialized_start=2180 - _globals['_HEALTHCHECKRESPONSE']._serialized_end=2225 - _globals['_AGENTORCHESTRATOR']._serialized_start=2228 - _globals['_AGENTORCHESTRATOR']._serialized_end=2461 + _globals['_CLIENTTASKMESSAGE']._serialized_start=570 + _globals['_CLIENTTASKMESSAGE']._serialized_end=780 + _globals['_NODEANNOUNCE']._serialized_start=782 + _globals['_NODEANNOUNCE']._serialized_end=813 + _globals['_BROWSEREVENT']._serialized_start=816 + _globals['_BROWSEREVENT']._serialized_end=951 + _globals['_SERVERTASKMESSAGE']._serialized_start=954 + _globals['_SERVERTASKMESSAGE']._serialized_end=1178 + _globals['_TASKCANCELREQUEST']._serialized_start=1180 + _globals['_TASKCANCELREQUEST']._serialized_end=1216 + _globals['_TASKREQUEST']._serialized_start=1219 + _globals['_TASKREQUEST']._serialized_end=1408 + _globals['_BROWSERACTION']._serialized_start=1411 + _globals['_BROWSERACTION']._serialized_end=1699 + _globals['_BROWSERACTION_ACTIONTYPE']._serialized_start=1565 + _globals['_BROWSERACTION_ACTIONTYPE']._serialized_end=1699 + _globals['_TASKRESPONSE']._serialized_start=1702 + _globals['_TASKRESPONSE']._serialized_end=2054 + _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_start=1934 + _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_end=1982 + _globals['_TASKRESPONSE_STATUS']._serialized_start=1984 + _globals['_TASKRESPONSE_STATUS']._serialized_end=2044 + _globals['_BROWSERRESPONSE']._serialized_start=2057 + _globals['_BROWSERRESPONSE']._serialized_end=2277 + _globals['_CONSOLEMESSAGE']._serialized_start=2279 + _globals['_CONSOLEMESSAGE']._serialized_end=2346 + _globals['_NETWORKREQUEST']._serialized_start=2348 + _globals['_NETWORKREQUEST']._serialized_end=2452 + _globals['_WORKPOOLUPDATE']._serialized_start=2454 + _globals['_WORKPOOLUPDATE']._serialized_end=2498 + _globals['_TASKCLAIMREQUEST']._serialized_start=2500 + _globals['_TASKCLAIMREQUEST']._serialized_end=2552 + _globals['_TASKCLAIMRESPONSE']._serialized_start=2554 + _globals['_TASKCLAIMRESPONSE']._serialized_end=2623 + _globals['_HEARTBEAT']._serialized_start=2626 + _globals['_HEARTBEAT']._serialized_end=2819 + _globals['_HEALTHCHECKRESPONSE']._serialized_start=2821 + _globals['_HEALTHCHECKRESPONSE']._serialized_end=2866 + _globals['_AGENTORCHESTRATOR']._serialized_start=2869 + _globals['_AGENTORCHESTRATOR']._serialized_end=3102 # @@protoc_insertion_point(module_scope) diff --git a/poc-grpc-agent/client.py b/poc-grpc-agent/client.py index a4950a8..33c782f 100644 --- a/poc-grpc-agent/client.py +++ b/poc-grpc-agent/client.py @@ -20,7 +20,7 @@ class BaseSkill: """Interface for pluggable node capabilities.""" - def execute(self, task, sandbox, on_complete): + def execute(self, task, sandbox, on_complete, on_event=None): raise NotImplementedError def cancel(self, task_id): @@ -32,7 +32,7 @@ self.processes = {} # task_id -> Popen self.lock = threading.Lock() - def execute(self, task, sandbox, on_complete): + def execute(self, task, sandbox, on_complete, on_event=None): try: cmd = task.payload_json @@ -40,13 +40,14 @@ if not allowed: return on_complete(task.task_id, {"stderr": f"SANDBOX_VIOLATION: {status_msg}", "status": 2}, task.trace_id) - print(f" [🐚] Executing Shell: {cmd}") + print(f" [🐚] Executing Shell: {cmd}", flush=True) p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) with self.lock: self.processes[task.task_id] = p 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) except subprocess.TimeoutExpired: @@ -57,72 +58,6 @@ finally: with self.lock: self.processes.pop(task.task_id, None) -class BrowserSkill(BaseSkill): - """The 'Antigravity Bridge': Persistent Browser Skill.""" - def __init__(self): - self.lock = threading.Lock() - self.pw = None - self.browser = None - self.sessions = {} # session_id -> { "context": Context, "page": Page } - - def _ensure_browser(self): - if not self.pw: - self.pw = sync_playwright().start() - self.browser = self.pw.chromium.launch(headless=True) - - def execute(self, task, sandbox, on_complete): - try: - self._ensure_browser() - action = task.browser_action - sid = action.session_id or "default" - - with self.lock: - if sid not in self.sessions: - context = self.browser.new_context() - page = context.new_page() - self.sessions[sid] = {"context": context, "page": page} - - page = self.sessions[sid]["page"] - - print(f" [🌐] Browser Action: {agent_pb2.BrowserAction.ActionType.Name(action.action)} | Session: {sid}") - - res_data = {"url": page.url, "title": page.title()} - - if action.action == agent_pb2.BrowserAction.NAVIGATE: - page.goto(action.url, wait_until="domcontentloaded") - elif action.action == agent_pb2.BrowserAction.CLICK: - page.click(action.selector) - elif action.action == agent_pb2.BrowserAction.TYPE: - page.fill(action.selector, action.text) - elif action.action == agent_pb2.BrowserAction.SCREENSHOT: - res_data["snapshot"] = page.screenshot() - elif action.action == agent_pb2.BrowserAction.GET_DOM: - res_data["dom_content"] = page.content() - elif action.action == agent_pb2.BrowserAction.CLOSE: - with self.lock: - sess = self.sessions.pop(sid, None) - if sess: sess["context"].close() - - # Refresh metadata after action - res_data["url"] = page.url - res_data["title"] = page.title() - - browser_res = agent_pb2.BrowserResponse( - url=res_data["url"], title=res_data["title"], - snapshot=res_data.get("snapshot", b""), - dom_content=res_data.get("dom_content", "") - ) - on_complete(task.task_id, {"status": 1, "browser_result": browser_res}, task.trace_id) - - except Exception as e: - print(f" [!] Browser Error: {e}") - on_complete(task.task_id, {"stderr": str(e), "status": 2}, task.trace_id) - - def cancel(self, task_id): - # Playwright sync actions are typically blocking in the thread, - # but we can close the contexts if needed. For now, we trust the timeout. - return False - def cancel(self, task_id): with self.lock: p = self.processes.get(task_id) @@ -132,6 +67,98 @@ return True return False +class BrowserSkill(BaseSkill): + """The 'Antigravity Bridge': Persistent Browser Skill using a dedicated Actor thread.""" + def __init__(self): + self.task_queue = queue.Queue() + self.sessions = {} # session_id -> { "context": Context, "page": Page } + self.lock = threading.Lock() + threading.Thread(target=self._browser_actor, daemon=True, name="BrowserActor").start() + + def _setup_listeners(self, sid, page, on_event): + if not on_event: return + page.on("console", lambda msg: on_event(agent_pb2.BrowserEvent( + session_id=sid, console_msg=agent_pb2.ConsoleMessage( + level=msg.type, text=msg.text, timestamp_ms=int(time.time()*1000) + ) + ))) + page.on("requestfinished", lambda req: on_event(agent_pb2.BrowserEvent( + session_id=sid, network_req=agent_pb2.NetworkRequest( + method=req.method, url=req.url, status=req.response().status if req.response() else 0, + resource_type=req.resource_type, latency_ms=0 + ) + ))) + + def _browser_actor(self): + print("[🌐] Browser Actor Starting...", flush=True) + try: + pw = sync_playwright().start() + browser = pw.chromium.launch(headless=True, args=[ + '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu' + ]) + print("[🌐] Browser Engine Online.", flush=True) + except Exception as e: + print(f"[!] Browser Actor Startup Fail: {e}", flush=True) + return + + while True: + try: + task, sandbox, on_complete, on_event = self.task_queue.get() + action = task.browser_action + sid = action.session_id or "default" + + with self.lock: + if sid not in self.sessions: + context = browser.new_context() + page = context.new_page() + self._setup_listeners(sid, page, on_event) + self.sessions[sid] = {"context": context, "page": page} + page = self.sessions[sid]["page"] + + print(f" [🌐] Browser Actor Processing: {agent_pb2.BrowserAction.ActionType.Name(action.action)} | Session: {sid}", flush=True) + + res_data = {} + if action.action == agent_pb2.BrowserAction.NAVIGATE: + page.goto(action.url, wait_until="commit") + elif action.action == agent_pb2.BrowserAction.CLICK: + page.click(action.selector) + elif action.action == agent_pb2.BrowserAction.TYPE: + page.fill(action.selector, action.text) + elif action.action == agent_pb2.BrowserAction.SCREENSHOT: + res_data["snapshot"] = page.screenshot() + elif action.action == agent_pb2.BrowserAction.GET_DOM: + res_data["dom_content"] = page.content() + elif action.action == agent_pb2.BrowserAction.HOVER: + page.hover(action.selector) + elif action.action == agent_pb2.BrowserAction.SCROLL: + page.mouse.wheel(x=0, y=action.y) + elif action.action == agent_pb2.BrowserAction.EVAL: + res_data["eval_result"] = str(page.evaluate(action.text)) + elif action.action == agent_pb2.BrowserAction.GET_A11Y: + res_data["a11y_tree"] = json.dumps(page.accessibility.snapshot()) + elif action.action == agent_pb2.BrowserAction.CLOSE: + with self.lock: + sess = self.sessions.pop(sid, None) + if sess: sess["context"].close() + + # Refresh metadata After + br_res = agent_pb2.BrowserResponse( + url=page.url, title=page.title(), + snapshot=res_data.get("snapshot", b""), + dom_content=res_data.get("dom_content", ""), + a11y_tree=res_data.get("a11y_tree", ""), + eval_result=res_data.get("eval_result", "") + ) + on_complete(task.task_id, {"status": 1, "browser_result": br_res}, task.trace_id) + except Exception as e: + print(f" [!] Browser Actor Error: {e}", flush=True) + on_complete(task.task_id, {"stderr": str(e), "status": 2}, task.trace_id) + + def execute(self, task, sandbox, on_complete, on_event=None): + self.task_queue.put((task, sandbox, on_complete, on_event)) + + def cancel(self, task_id): return False + class SkillManager: """Orchestrates multiple skills and manages the worker thread pool.""" def __init__(self, max_workers=5): @@ -144,7 +171,7 @@ self.max_workers = max_workers self.lock = threading.Lock() - def submit(self, task, sandbox, on_complete): + def submit(self, task, sandbox, on_complete, on_event=None): with self.lock: if len(self.active_tasks) >= self.max_workers: return False, "Node Capacity Reached" @@ -155,7 +182,7 @@ else: skill = self.skills["shell"] - future = self.executor.submit(skill.execute, task, sandbox, on_complete) + future = self.executor.submit(skill.execute, task, sandbox, on_complete, on_event) self.active_tasks[task.task_id] = future # Cleanup hook @@ -232,37 +259,48 @@ def run_task_stream(self): def _gen(): + 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()) - for msg in responses: - kind = msg.WhichOneof('payload') - if kind == 'task_request': - self._handle_task(msg.task_request) - elif kind == 'task_cancel': - if self.skills.cancel(msg.task_cancel.task_id): - self.task_queue.put(agent_pb2.ClientTaskMessage(task_response=agent_pb2.TaskResponse( - task_id=msg.task_cancel.task_id, status=agent_pb2.TaskResponse.CANCELLED))) - elif kind == 'work_pool_update': - for tid in msg.work_pool_update.available_task_ids: - if len(self.skills.get_active_ids()) < self.skills.max_workers: - self.task_queue.put(agent_pb2.ClientTaskMessage(task_claim=agent_pb2.TaskClaimRequest(task_id=tid, node_id=self.node_id))) + print(f"[*] Stream processing started: {self.node_id}", flush=True) + try: + for msg in responses: + kind = msg.WhichOneof('payload') + print(f"[*] Received message from server: {kind}", flush=True) + if kind == 'task_request': + self._handle_task(msg.task_request) + elif kind == 'task_cancel': + if self.skills.cancel(msg.task_cancel.task_id): + self.task_queue.put(agent_pb2.ClientTaskMessage(task_response=agent_pb2.TaskResponse( + task_id=msg.task_cancel.task_id, status=agent_pb2.TaskResponse.CANCELLED))) + elif kind == 'work_pool_update': + for tid in msg.work_pool_update.available_task_ids: + if len(self.skills.get_active_ids()) < self.skills.max_workers: + self.task_queue.put(agent_pb2.ClientTaskMessage(task_claim=agent_pb2.TaskClaimRequest(task_id=tid, node_id=self.node_id))) + except Exception as e: + print(f"[!] Stream Error: {e}", flush=True) def _handle_task(self, task): + print(f"[*] Handling Task: {task.task_id}", flush=True) # Sig Verify logic based on payload type if task.HasField("browser_action"): a = task.browser_action sign_base = f"{a.action}:{a.url}:{a.session_id}".encode() else: sign_base = task.payload_json.encode() - + expected_sig = hmac.new(SECRET_KEY.encode(), sign_base, hashlib.sha256).hexdigest() if not hmac.compare_digest(task.signature, expected_sig): - return print(f"[!] Sig Fail for {task.task_id}") + return print(f"[!] Sig Fail for {task.task_id} | Raw: {sign_base}", flush=True) - self.skills.submit(task, self.sandbox, self._on_finish) + print(f"[✅] Signature Verified for {task.task_id}", flush=True) + self.skills.submit(task, self.sandbox, self._on_finish, self._on_event) + + def _on_event(self, event): + self.task_queue.put(agent_pb2.ClientTaskMessage(browser_event=event)) def _on_finish(self, tid, res, trace): - print(f"[*] Task {tid} finished.") + print(f"[*] Task {tid} finished.", flush=True) status = agent_pb2.TaskResponse.SUCCESS if res['status'] == 1 else agent_pb2.TaskResponse.ERROR tr = agent_pb2.TaskResponse( diff --git a/poc-grpc-agent/protos/agent.proto b/poc-grpc-agent/protos/agent.proto index 3855527..35226bd 100644 --- a/poc-grpc-agent/protos/agent.proto +++ b/poc-grpc-agent/protos/agent.proto @@ -44,10 +44,24 @@ // --- Channel 2: Tasks & Collaboration --- message ClientTaskMessage { - oneof payload { - TaskResponse task_response = 1; - TaskClaimRequest task_claim = 2; - } + oneof payload { + TaskResponse task_response = 1; + TaskClaimRequest task_claim = 2; + BrowserEvent browser_event = 3; + NodeAnnounce announce = 4; // NEW: Identification on stream connect + } +} + +message NodeAnnounce { + string node_id = 1; +} + +message BrowserEvent { + string session_id = 1; + oneof event { + ConsoleMessage console_msg = 2; + NetworkRequest network_req = 3; + } } message ServerTaskMessage { @@ -82,13 +96,19 @@ TYPE = 2; SCREENSHOT = 3; GET_DOM = 4; + HOVER = 5; + SCROLL = 6; CLOSE = 7; + EVAL = 8; + GET_A11Y = 9; } ActionType action = 1; string url = 2; string selector = 3; string text = 4; - string session_id = 5; // For persistent "Antigravity Bridge" continuity + string session_id = 5; + int32 x = 6; + int32 y = 7; } message TaskResponse { @@ -114,8 +134,26 @@ message BrowserResponse { string url = 1; string title = 2; - bytes snapshot = 3; // Small snapshots; large ones use 'artifacts' chunking + bytes snapshot = 3; string dom_content = 4; + string a11y_tree = 5; + string eval_result = 6; + repeated ConsoleMessage console_history = 7; + repeated NetworkRequest network_history = 8; +} + +message ConsoleMessage { + string level = 1; + string text = 2; + int64 timestamp_ms = 3; +} + +message NetworkRequest { + string method = 1; + string url = 2; + int32 status = 3; + string resource_type = 4; + int64 latency_ms = 5; } message WorkPoolUpdate { diff --git a/poc-grpc-agent/server.py b/poc-grpc-agent/server.py index 2280a72..c82fb75 100644 --- a/poc-grpc-agent/server.py +++ b/poc-grpc-agent/server.py @@ -75,7 +75,7 @@ class GlobalWorkPool: def __init__(self): self.lock = threading.Lock() - self.available = {"shared-001": '{"command": "uname -a"}', "shared-002": '{"command": "uptime"}'} + self.available = {"shared-001": "uname -a", "shared-002": "uptime"} def claim(self, task_id, node_id): with self.lock: @@ -98,10 +98,13 @@ tid = f"task-{int(time.time()*1000)}" event = self.journal.register(tid, node_id) - sig = hmac.new(SECRET_KEY.encode(), json.dumps({"command": cmd}).encode(), hashlib.sha256).hexdigest() + # Don't wrap in JSON, use raw cmd for shells + msg_json = json.dumps({"command": cmd}) # Simulation legacy check + sig = hmac.new(SECRET_KEY.encode(), cmd.encode(), hashlib.sha256).hexdigest() req = agent_pb2.ServerTaskMessage(task_request=agent_pb2.TaskRequest( - task_id=tid, payload_json=json.dumps({"command": cmd}), signature=sig)) + task_id=tid, payload_json=cmd, signature=sig)) + print(f"[📤] Dispatching {tid} to {node_id}") node["queue"].put(req) if event.wait(timeout): @@ -148,29 +151,63 @@ allowed_commands=["ls", "uname", "echo", "sleep"])) def TaskStream(self, request_iterator, context): - node_id = None - - def _read(): - nonlocal node_id - for msg in request_iterator: - kind = msg.WhichOneof('payload') - if kind == 'task_claim': - node_id = msg.task_claim.node_id - success, payload = self.pool.claim(msg.task_claim.task_id, node_id) - if success: - sig = hmac.new(SECRET_KEY.encode(), payload.encode(), hashlib.sha256).hexdigest() - self.registry.get_node(node_id)["queue"].put(agent_pb2.ServerTaskMessage( - task_request=agent_pb2.TaskRequest(task_id=msg.task_claim.task_id, payload_json=payload, signature=sig))) - elif kind == 'task_response': - res_obj = {"stdout": msg.task_response.stdout, "status": msg.task_response.status} - if msg.task_response.HasField("browser_result"): - br = msg.task_response.browser_result - res_obj["browser"] = {"url": br.url, "title": br.title, "has_snapshot": len(br.snapshot) > 0} - self.journal.fulfill(msg.task_response.task_id, res_obj) - - threading.Thread(target=_read, daemon=True).start() + try: + # 1. Blocking wait for identity + first_msg = next(request_iterator) + if first_msg.WhichOneof('payload') != 'announce': + print("[!] Stream rejected: No NodeAnnounce") + return + + node_id = first_msg.announce.node_id + node = self.registry.get_node(node_id) + if not node: + print(f"[!] Stream rejected: Node {node_id} not registered via SyncConfiguration") + return + + print(f"[📶] Stream established for {node_id}") - while context.is_active(): + # 2. Results Listener + def _read_results(): + for msg in request_iterator: + kind = msg.WhichOneof('payload') + if kind == 'task_claim': + success, payload = self.pool.claim(msg.task_claim.task_id, node_id) + if success: + sig = hmac.new(SECRET_KEY.encode(), payload.encode(), hashlib.sha256).hexdigest() + node["queue"].put(agent_pb2.ServerTaskMessage( + task_request=agent_pb2.TaskRequest(task_id=msg.task_claim.task_id, payload_json=payload, signature=sig))) + elif kind == 'task_response': + res_obj = {"stdout": msg.task_response.stdout, "status": msg.task_response.status} + if msg.task_response.HasField("browser_result"): + br = msg.task_response.browser_result + res_obj["browser"] = { + "url": br.url, "title": br.title, "has_snapshot": len(br.snapshot) > 0, + "a11y": br.a11y_tree[:100] + "..." if br.a11y_tree else None, + "eval": br.eval_result + } + self.journal.fulfill(msg.task_response.task_id, res_obj) + elif kind == 'browser_event': + e = msg.browser_event + if e.HasField("console_msg"): + print(f" [🖥️] Live Browser Console: {e.console_msg.text}", flush=True) + elif e.HasField("network_req"): + print(f" [🌐] Live Network Request: {e.network_req.method} {e.network_req.url}", flush=True) + + threading.Thread(target=_read_results, daemon=True).start() + + # 3. Work Dispatcher (Main Stream) + while context.is_active(): + try: + msg = node["queue"].get(timeout=1.0) + yield msg + print(f"[🚀] Pushed message from queue to stream: {node_id}") + except queue.Empty: + continue + + except StopIteration: + pass + except Exception as e: + print(f"[!] TaskStream Error: {e}") # Broadcast pool if self.pool.available: yield agent_pb2.ServerTaskMessage(work_pool_update=agent_pb2.WorkPoolUpdate(available_task_ids=list(self.pool.available.keys()))) @@ -203,8 +240,9 @@ # Simple AI Simulation loop time.sleep(10) - print("\n[🧠] AI Simulation Start...") - print(f" Whoami: {orch.assistant.dispatch_single('agent-node-007', 'whoami')}") + print("\n[🧠] AI Simulation Start...", flush=True) + res_single = orch.assistant.dispatch_single('agent-node-007', 'uname -a') + print(f" Uname: {res_single}", flush=True) # NEW: Browser Phase print("\n[🧠] AI Phase 4: Navigating Browser (Antigravity Bridge)...") @@ -224,6 +262,32 @@ res_snap = orch.assistant.dispatch_browser("agent-node-007", snap_action) print(f" Snap Result: {res_snap.get('browser', {}).get('title')} | Snapshot captured: {res_snap.get('browser', {}).get('has_snapshot')}") + # NEW: Phase 4 Pro Features + print("\n[🧠] AI Phase 4 Pro: Perception & Advanced Logic...") + a11y_action = agent_pb2.BrowserAction( + action=agent_pb2.BrowserAction.GET_A11Y, + session_id="antigravity-session-1" + ) + res_a11y = orch.assistant.dispatch_browser("agent-node-007", a11y_action) + print(f" A11y Result: {res_a11y.get('browser', {}).get('a11y')}") + + eval_action = agent_pb2.BrowserAction( + action=agent_pb2.BrowserAction.EVAL, + text="window.performance.now()", + session_id="antigravity-session-1" + ) + res_eval = orch.assistant.dispatch_browser("agent-node-007", eval_action) + print(f" Eval Result (Timestamp): {res_eval.get('browser', {}).get('eval')}") + + # NEW: Phase 4 Pro Features - Real-time Events + print("\n[🧠] AI Phase 4 Pro: Triggering Real-time Events (Tunneling)...") + trigger_action = agent_pb2.BrowserAction( + action=agent_pb2.BrowserAction.EVAL, + text="console.log('Hello from Antigravity Bridge!'); fetch('https://example.com/api/ping');", + session_id="antigravity-session-1" + ) + orch.assistant.dispatch_browser("agent-node-007", trigger_action) + server.wait_for_termination() if __name__ == '__main__':