diff --git a/agent-node/VERSION b/agent-node/VERSION index f9ef507..9084fa2 100644 --- a/agent-node/VERSION +++ b/agent-node/VERSION @@ -1 +1 @@ -1.0.78 +1.1.0 diff --git a/agent-node/src/agent_node/node.py b/agent-node/src/agent_node/node.py index 492d27b..f14787c 100644 --- a/agent-node/src/agent_node/node.py +++ b/agent-node/src/agent_node/node.py @@ -321,6 +321,8 @@ print(f"[*] [gRPC-Stream] Connection closed by server.", flush=True) except Exception as e: + import traceback + traceback.print_exc() err_desc = self._format_grpc_error(e) print(f"[!] Task Stream Failure: {err_desc}. Reconnecting in 5s...", flush=True) # Force refresh stub on reconnection, closing old channel diff --git a/agent-node/src/agent_node/utils/auth.py b/agent-node/src/agent_node/utils/auth.py index b06710c..d5fb7b5 100644 --- a/agent-node/src/agent_node/utils/auth.py +++ b/agent-node/src/agent_node/utils/auth.py @@ -3,7 +3,7 @@ import hmac import hashlib from protos import agent_pb2 -from agent_node.config import SECRET_KEY +from agent_node import config def create_auth_token(node_id: str) -> str: """Creates a JWT for node authentication.""" @@ -12,20 +12,24 @@ "iat": datetime.datetime.utcnow(), "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=10) } - return jwt.encode(payload, SECRET_KEY, algorithm="HS256") + return jwt.encode(payload, config.SECRET_KEY, algorithm="HS256") -def verify_task_signature(task, secret=SECRET_KEY) -> bool: +def verify_task_signature(task, secret=None) -> bool: """Verifies HMAC signature for task payloads.""" + if secret is None: + secret = config.SECRET_KEY sign_base = task.payload_json expected_sig = hmac.new(secret.encode(), sign_base.encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(task.signature, expected_sig) -def verify_server_message_signature(msg: agent_pb2.ServerTaskMessage, secret=SECRET_KEY) -> bool: +def verify_server_message_signature(msg: agent_pb2.ServerTaskMessage, secret=None) -> bool: """Verifies HMAC signature for ServerTaskMessage.""" + if secret is None: + secret = config.SECRET_KEY sig = msg.signature msg.signature = "" - msg_bytes = msg.SerializeToString() + msg_bytes = msg.SerializeToString(deterministic=True) msg.signature = sig # Restore it expected_sig = hmac.new(secret.encode(), msg_bytes, hashlib.sha256).hexdigest() diff --git a/agent-node/src/agent_pb2.py b/agent-node/src/agent_pb2.py index 0bdab29..450935e 100644 --- a/agent-node/src/agent_pb2.py +++ b/agent-node/src/agent_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: agent.proto -# Protobuf Python Version: 6.31.1 +# Protobuf Python Version: 5.29.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, + 5, + 29, + 0, '', 'agent.proto' ) diff --git a/agent-node/src/agent_pb2_grpc.py b/agent-node/src/agent_pb2_grpc.py index e4fe22b..e972a62 100644 --- a/agent-node/src/agent_pb2_grpc.py +++ b/agent-node/src/agent_pb2_grpc.py @@ -5,7 +5,7 @@ import agent_pb2 as agent__pb2 -GRPC_GENERATED_VERSION = '1.80.0' +GRPC_GENERATED_VERSION = '1.71.2' GRPC_VERSION = grpc.__version__ _version_not_supported = False @@ -18,7 +18,7 @@ if _version_not_supported: raise RuntimeError( f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in agent_pb2_grpc.py depends on' + + f' but the generated code in agent_pb2_grpc.py depends on' + f' grpcio>={GRPC_GENERATED_VERSION}.' + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' diff --git a/agent-node/src/protos/agent_pb2.py b/agent-node/src/protos/agent_pb2.py index 981686e..450935e 100644 --- a/agent-node/src/protos/agent_pb2.py +++ b/agent-node/src/protos/agent_pb2.py @@ -1,12 +1,22 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: agent.proto -# Protobuf Python Version: 4.25.1 +# Protobuf Python Version: 5.29.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 0, + '', + 'agent.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -14,16 +24,16 @@ -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\"\xe0\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\x12\x19\n\x11skill_config_json\x18\x06 \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\"\xfb\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\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\"\xbc\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\x12-\n\rpolicy_update\x18\x06 \x01(\x0b\x32\x14.agent.SandboxPolicyH\x00\x42\t\n\x07payload\"$\n\x11TaskCancelRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\"\xa1\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\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\"\xa4\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\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\",\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\"\xe6\x02\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\x12\x11\n\tcpu_count\x18\x08 \x01(\x05\x12\x16\n\x0ememory_used_gb\x18\t \x01(\x02\x12\x17\n\x0fmemory_total_gb\x18\n \x01(\x02\x12\x1a\n\x12\x63pu_usage_per_core\x18\x0b \x03(\x02\x12\x14\n\x0c\x63pu_freq_mhz\x18\x0c \x01(\x02\x12\x1b\n\x13memory_available_gb\x18\r \x01(\x02\x12\x10\n\x08load_avg\x18\x0e \x03(\x02\"-\n\x13HealthCheckResponse\x12\x16\n\x0eserver_time_ms\x18\x01 \x01(\x03\"\xe4\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\x12\x0f\n\x07task_id\x18\x06 \x01(\tB\t\n\x07payload\"\xab\x02\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\x12\x0f\n\x07\x63ontent\x18\x04 \x01(\x0c\x12\x0e\n\x06is_dir\x18\x05 \x01(\x08\"\xaa\x01\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\x12\x08\n\x04LIST\x10\x06\x12\x08\n\x04READ\x10\x07\x12\t\n\x05WRITE\x10\x08\x12\n\n\x06\x44\x45LETE\x10\t\x12\t\n\x05PURGE\x10\n\x12\x0b\n\x07\x43LEANUP\x10\x0b\"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\"\xad\x01\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\x12\x0e\n\x06offset\x18\x06 \x01(\x03\x12\x12\n\ncompressed\x18\x07 \x01(\x08\x12\x14\n\x0ctotal_chunks\x18\x08 \x01(\x05\x12\x12\n\ntotal_size\x18\t \x01(\x03\"\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\"\xe0\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\x12\x19\n\x11skill_config_json\x18\x06 \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\"\xfb\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\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\"\xcf\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\x12-\n\rpolicy_update\x18\x06 \x01(\x0b\x32\x14.agent.SandboxPolicyH\x00\x12\x11\n\tsignature\x18\x07 \x01(\tB\t\n\x07payload\"$\n\x11TaskCancelRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\"\xa1\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\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\"\xa4\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\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\",\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\"\xe6\x02\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\x12\x11\n\tcpu_count\x18\x08 \x01(\x05\x12\x16\n\x0ememory_used_gb\x18\t \x01(\x02\x12\x17\n\x0fmemory_total_gb\x18\n \x01(\x02\x12\x1a\n\x12\x63pu_usage_per_core\x18\x0b \x03(\x02\x12\x14\n\x0c\x63pu_freq_mhz\x18\x0c \x01(\x02\x12\x1b\n\x13memory_available_gb\x18\r \x01(\x02\x12\x10\n\x08load_avg\x18\x0e \x03(\x02\"-\n\x13HealthCheckResponse\x12\x16\n\x0eserver_time_ms\x18\x01 \x01(\x03\"\xe4\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\x12\x0f\n\x07task_id\x18\x06 \x01(\tB\t\n\x07payload\"\xab\x02\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\x12\x0f\n\x07\x63ontent\x18\x04 \x01(\x0c\x12\x0e\n\x06is_dir\x18\x05 \x01(\x08\"\xaa\x01\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\x12\x08\n\x04LIST\x10\x06\x12\x08\n\x04READ\x10\x07\x12\t\n\x05WRITE\x10\x08\x12\n\n\x06\x44\x45LETE\x10\t\x12\t\n\x05PURGE\x10\n\x12\x0b\n\x07\x43LEANUP\x10\x0b\"m\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\x12\x13\n\x0b\x63hunk_index\x18\x03 \x01(\x05\x12\x10\n\x08is_final\x18\x04 \x01(\x08\"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\"\xad\x01\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\x12\x0e\n\x06offset\x18\x06 \x01(\x03\x12\x12\n\ncompressed\x18\x07 \x01(\x08\x12\x14\n\x0ctotal_chunks\x18\x08 \x01(\x05\x12\x12\n\ntotal_size\x18\t \x01(\x03\"\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 +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._loaded_options = None _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._serialized_options = b'8\001' - _globals['_TASKRESPONSE_ARTIFACTSENTRY']._options = None + _globals['_TASKRESPONSE_ARTIFACTSENTRY']._loaded_options = None _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_options = b'8\001' _globals['_REGISTRATIONREQUEST']._serialized_start=23 _globals['_REGISTRATIONREQUEST']._serialized_end=245 @@ -42,43 +52,43 @@ _globals['_NODEANNOUNCE']._serialized_start=973 _globals['_NODEANNOUNCE']._serialized_end=1004 _globals['_SERVERTASKMESSAGE']._serialized_start=1007 - _globals['_SERVERTASKMESSAGE']._serialized_end=1323 - _globals['_TASKCANCELREQUEST']._serialized_start=1325 - _globals['_TASKCANCELREQUEST']._serialized_end=1361 - _globals['_TASKREQUEST']._serialized_start=1364 - _globals['_TASKREQUEST']._serialized_end=1525 - _globals['_TASKRESPONSE']._serialized_start=1528 - _globals['_TASKRESPONSE']._serialized_end=1820 - _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_start=1710 - _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_end=1758 - _globals['_TASKRESPONSE_STATUS']._serialized_start=1760 - _globals['_TASKRESPONSE_STATUS']._serialized_end=1820 - _globals['_WORKPOOLUPDATE']._serialized_start=1822 - _globals['_WORKPOOLUPDATE']._serialized_end=1866 - _globals['_TASKCLAIMREQUEST']._serialized_start=1868 - _globals['_TASKCLAIMREQUEST']._serialized_end=1920 - _globals['_TASKCLAIMRESPONSE']._serialized_start=1922 - _globals['_TASKCLAIMRESPONSE']._serialized_end=1991 - _globals['_HEARTBEAT']._serialized_start=1994 - _globals['_HEARTBEAT']._serialized_end=2352 - _globals['_HEALTHCHECKRESPONSE']._serialized_start=2354 - _globals['_HEALTHCHECKRESPONSE']._serialized_end=2399 - _globals['_FILESYNCMESSAGE']._serialized_start=2402 - _globals['_FILESYNCMESSAGE']._serialized_end=2630 - _globals['_SYNCCONTROL']._serialized_start=2633 - _globals['_SYNCCONTROL']._serialized_end=2932 - _globals['_SYNCCONTROL_ACTION']._serialized_start=2762 - _globals['_SYNCCONTROL_ACTION']._serialized_end=2932 - _globals['_DIRECTORYMANIFEST']._serialized_start=2934 - _globals['_DIRECTORYMANIFEST']._serialized_end=3004 - _globals['_FILEINFO']._serialized_start=3006 - _globals['_FILEINFO']._serialized_end=3074 - _globals['_FILEPAYLOAD']._serialized_start=3077 - _globals['_FILEPAYLOAD']._serialized_end=3250 - _globals['_SYNCSTATUS']._serialized_start=3253 - _globals['_SYNCSTATUS']._serialized_end=3413 - _globals['_SYNCSTATUS_CODE']._serialized_start=3347 - _globals['_SYNCSTATUS_CODE']._serialized_end=3413 - _globals['_AGENTORCHESTRATOR']._serialized_start=3416 - _globals['_AGENTORCHESTRATOR']._serialized_end=3649 + _globals['_SERVERTASKMESSAGE']._serialized_end=1342 + _globals['_TASKCANCELREQUEST']._serialized_start=1344 + _globals['_TASKCANCELREQUEST']._serialized_end=1380 + _globals['_TASKREQUEST']._serialized_start=1383 + _globals['_TASKREQUEST']._serialized_end=1544 + _globals['_TASKRESPONSE']._serialized_start=1547 + _globals['_TASKRESPONSE']._serialized_end=1839 + _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_start=1729 + _globals['_TASKRESPONSE_ARTIFACTSENTRY']._serialized_end=1777 + _globals['_TASKRESPONSE_STATUS']._serialized_start=1779 + _globals['_TASKRESPONSE_STATUS']._serialized_end=1839 + _globals['_WORKPOOLUPDATE']._serialized_start=1841 + _globals['_WORKPOOLUPDATE']._serialized_end=1885 + _globals['_TASKCLAIMREQUEST']._serialized_start=1887 + _globals['_TASKCLAIMREQUEST']._serialized_end=1939 + _globals['_TASKCLAIMRESPONSE']._serialized_start=1941 + _globals['_TASKCLAIMRESPONSE']._serialized_end=2010 + _globals['_HEARTBEAT']._serialized_start=2013 + _globals['_HEARTBEAT']._serialized_end=2371 + _globals['_HEALTHCHECKRESPONSE']._serialized_start=2373 + _globals['_HEALTHCHECKRESPONSE']._serialized_end=2418 + _globals['_FILESYNCMESSAGE']._serialized_start=2421 + _globals['_FILESYNCMESSAGE']._serialized_end=2649 + _globals['_SYNCCONTROL']._serialized_start=2652 + _globals['_SYNCCONTROL']._serialized_end=2951 + _globals['_SYNCCONTROL_ACTION']._serialized_start=2781 + _globals['_SYNCCONTROL_ACTION']._serialized_end=2951 + _globals['_DIRECTORYMANIFEST']._serialized_start=2953 + _globals['_DIRECTORYMANIFEST']._serialized_end=3062 + _globals['_FILEINFO']._serialized_start=3064 + _globals['_FILEINFO']._serialized_end=3132 + _globals['_FILEPAYLOAD']._serialized_start=3135 + _globals['_FILEPAYLOAD']._serialized_end=3308 + _globals['_SYNCSTATUS']._serialized_start=3311 + _globals['_SYNCSTATUS']._serialized_end=3471 + _globals['_SYNCSTATUS_CODE']._serialized_start=3405 + _globals['_SYNCSTATUS_CODE']._serialized_end=3471 + _globals['_AGENTORCHESTRATOR']._serialized_start=3474 + _globals['_AGENTORCHESTRATOR']._serialized_end=3707 # @@protoc_insertion_point(module_scope) diff --git a/agent-node/src/protos/agent_pb2_grpc.py b/agent-node/src/protos/agent_pb2_grpc.py index 1e19ad3..e972a62 100644 --- a/agent-node/src/protos/agent_pb2_grpc.py +++ b/agent-node/src/protos/agent_pb2_grpc.py @@ -1,8 +1,28 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc +import warnings -from protos import agent_pb2 as agent__pb2 +import agent_pb2 as agent__pb2 + +GRPC_GENERATED_VERSION = '1.71.2' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in agent_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) class AgentOrchestratorStub(object): @@ -19,17 +39,17 @@ '/agent.AgentOrchestrator/SyncConfiguration', request_serializer=agent__pb2.RegistrationRequest.SerializeToString, response_deserializer=agent__pb2.RegistrationResponse.FromString, - ) + _registered_method=True) self.TaskStream = channel.stream_stream( '/agent.AgentOrchestrator/TaskStream', request_serializer=agent__pb2.ClientTaskMessage.SerializeToString, response_deserializer=agent__pb2.ServerTaskMessage.FromString, - ) + _registered_method=True) self.ReportHealth = channel.stream_stream( '/agent.AgentOrchestrator/ReportHealth', request_serializer=agent__pb2.Heartbeat.SerializeToString, response_deserializer=agent__pb2.HealthCheckResponse.FromString, - ) + _registered_method=True) class AgentOrchestratorServicer(object): @@ -79,6 +99,7 @@ generic_handler = grpc.method_handlers_generic_handler( 'agent.AgentOrchestrator', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('agent.AgentOrchestrator', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -97,11 +118,21 @@ wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/agent.AgentOrchestrator/SyncConfiguration', + 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) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def TaskStream(request_iterator, @@ -114,11 +145,21 @@ wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.stream_stream(request_iterator, target, '/agent.AgentOrchestrator/TaskStream', + 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) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def ReportHealth(request_iterator, @@ -131,8 +172,18 @@ wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.stream_stream(request_iterator, target, '/agent.AgentOrchestrator/ReportHealth', + 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) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/ai-hub/app/api/routes/user.py b/ai-hub/app/api/routes/user.py index e10d3e1..b1bcff7 100644 --- a/ai-hub/app/api/routes/user.py +++ b/ai-hub/app/api/routes/user.py @@ -49,7 +49,7 @@ """ Initiates the OIDC authentication flow. """ - auth_url = services.auth_service.generate_login_url(frontend_callback_uri) + auth_url = await services.auth_service.generate_login_url(frontend_callback_uri) return redirect(url=auth_url) @router.get("/login/callback", summary="Handle OIDC Login Callback") diff --git a/ai-hub/app/config.py b/ai-hub/app/config.py index d405903..678d540 100644 --- a/ai-hub/app/config.py +++ b/ai-hub/app/config.py @@ -2,11 +2,14 @@ import yaml from enum import Enum from typing import Optional +from pathlib import Path from dotenv import load_dotenv from pydantic import BaseModel, Field, SecretStr # Load environment variables from .env file -load_dotenv() +# M6: Use absolute path relative to this file to ensure it's found across different deployment methods +env_path = Path(__file__).parent / ".env" +load_dotenv(dotenv_path=env_path) # --- 1. Define the Configuration Schema --- @@ -114,10 +117,6 @@ self.SUPER_ADMINS: list[str] = [x.strip() for x in super_admins_env.split(",")] if super_admins_env else \ get_from_yaml(["application", "super_admins"]) or \ config_from_pydantic.application.super_admins - self.ALLOW_PASSWORD_LOGIN: bool = str(os.getenv("ALLOW_PASSWORD_LOGIN") if os.getenv("ALLOW_PASSWORD_LOGIN") is not None else - get_from_yaml(["application", "allow_password_login"]) if get_from_yaml(["application", "allow_password_login"]) is not None else - config_from_pydantic.application.allow_password_login).lower() == "true" - # --- OIDC Settings --- self.OIDC_ENABLED: bool = str(os.getenv("OIDC_ENABLED") if os.getenv("OIDC_ENABLED") is not None else get_from_yaml(["oidc", "enabled"]) if get_from_yaml(["oidc", "enabled"]) is not None else @@ -137,6 +136,10 @@ self.ALLOW_OIDC_LOGIN: bool = str(os.getenv("ALLOW_OIDC_LOGIN") if os.getenv("ALLOW_OIDC_LOGIN") is not None else get_from_yaml(["oidc", "allow_oidc_login"]) if get_from_yaml(["oidc", "allow_oidc_login"]) is not None else config_from_pydantic.oidc.allow_oidc_login).lower() == "true" + + self.ALLOW_PASSWORD_LOGIN: bool = (str(os.getenv("ALLOW_PASSWORD_LOGIN") if os.getenv("ALLOW_PASSWORD_LOGIN") is not None else + get_from_yaml(["application", "allow_password_login"]) if get_from_yaml(["application", "allow_password_login"]) is not None else + config_from_pydantic.application.allow_password_login).lower() == "true") or (not self.OIDC_ENABLED) # --- Swarm Settings --- self.GRPC_EXTERNAL_ENDPOINT: Optional[str] = os.getenv("GRPC_EXTERNAL_ENDPOINT") or \ diff --git a/ai-hub/app/core/grpc/services/grpc_server.py b/ai-hub/app/core/grpc/services/grpc_server.py index e42d1e5..f13588d 100644 --- a/ai-hub/app/core/grpc/services/grpc_server.py +++ b/ai-hub/app/core/grpc/services/grpc_server.py @@ -15,7 +15,7 @@ from app.core.grpc.core.pool import GlobalWorkPool from app.core.grpc.core.mirror import GhostMirrorManager from app.core.grpc.services.assistant import TaskAssistant -from app.core.grpc.utils.crypto import sign_payload +from app.core.grpc.utils.crypto import sign_payload, sign_bytes from app.config import settings logger = logging.getLogger(__name__) @@ -280,9 +280,12 @@ if (now - last_keepalive) > 10.0: last_keepalive = now # Yield a NOP ping (work pool update) to keep proxy connections hot - yield agent_pb2.ServerTaskMessage( + msg = agent_pb2.ServerTaskMessage( work_pool_update=agent_pb2.WorkPoolUpdate(available_task_ids=self.pool.list_available()) ) + msg_bytes = msg.SerializeToString(deterministic=True) + msg.signature = sign_bytes(msg_bytes) + yield msg continue except StopIteration: logger.info(f"[📶] gRPC Stream StopIteration for {node_id}") diff --git a/ai-hub/app/core/grpc/utils/crypto.py b/ai-hub/app/core/grpc/utils/crypto.py index b973bac..3b6dd5c 100644 --- a/ai-hub/app/core/grpc/utils/crypto.py +++ b/ai-hub/app/core/grpc/utils/crypto.py @@ -2,15 +2,13 @@ import hashlib from app.config import settings -SECRET_KEY = settings.SECRET_KEY - def sign_payload(payload: str) -> str: """Signs a string payload using HMAC-SHA256.""" - return hmac.new(SECRET_KEY.encode(), payload.encode(), hashlib.sha256).hexdigest() + return hmac.new(settings.SECRET_KEY.encode(), payload.encode(), hashlib.sha256).hexdigest() def sign_bytes(data: bytes) -> str: """Signs bytes using HMAC-SHA256.""" - return hmac.new(SECRET_KEY.encode(), data, hashlib.sha256).hexdigest() + return hmac.new(settings.SECRET_KEY.encode(), data, hashlib.sha256).hexdigest() def verify_signature(payload: str, signature: str) -> bool: """Verifies a signature against a payload using HMAC-SHA256.""" diff --git a/ai-hub/app/core/services/auth.py b/ai-hub/app/core/services/auth.py index e5c900f..4573f3c 100644 --- a/ai-hub/app/core/services/auth.py +++ b/ai-hub/app/core/services/auth.py @@ -1,6 +1,7 @@ import httpx import jwt import urllib.parse +import time from fastapi import HTTPException import logging from app.config import settings @@ -9,19 +10,37 @@ logger = logging.getLogger(__name__) class AuthService: + _discovery_cache = None + _discovery_expiry = 0 + def __init__(self, services): self.services = services - def get_oidc_urls(self) -> Dict[str, str]: - server_url = settings.OIDC_SERVER_URL.rstrip("/") - return { - "auth": f"{server_url}/auth", - "token": f"{server_url}/token", - "userinfo": f"{server_url}/userinfo" - } + async def get_discovery(self) -> Dict[str, Any]: + if self._discovery_cache and time.time() < self._discovery_expiry: + return self._discovery_cache - def generate_login_url(self, frontend_callback_uri: Optional[str]) -> str: - oidc_urls = self.get_oidc_urls() + discovery_url = f"{settings.OIDC_SERVER_URL.rstrip('/')}/.well-known/openid-configuration" + try: + async with httpx.AsyncClient() as client: + response = await client.get(discovery_url, timeout=10.0) + response.raise_for_status() + self._discovery_cache = response.json() + self._discovery_expiry = time.time() + 3600 # Cache for 1 hour + return self._discovery_cache + except Exception as e: + logger.error(f"Failed to fetch OIDC discovery from {discovery_url}: {e}") + # Fallback to defaults if discovery fails, for resiliency + server_url = settings.OIDC_SERVER_URL.rstrip("/") + return { + "authorization_endpoint": f"{server_url}/auth", + "token_endpoint": f"{server_url}/token", + "jwks_uri": f"{server_url}/keys", + "userinfo_endpoint": f"{server_url}/userinfo" + } + + async def generate_login_url(self, frontend_callback_uri: Optional[str]) -> str: + discovery = await self.get_discovery() params = { "response_type": "code", "scope": "openid profile email", @@ -29,10 +48,12 @@ "redirect_uri": settings.OIDC_REDIRECT_URI, "state": frontend_callback_uri or "" } - return f"{oidc_urls['auth']}?{urllib.parse.urlencode(params)}" + auth_endpoint = discovery.get("authorization_endpoint") + return f"{auth_endpoint}?{urllib.parse.urlencode(params)}" async def handle_callback(self, code: str, db) -> Dict[str, Any]: - oidc_urls = self.get_oidc_urls() + discovery = await self.get_discovery() + token_endpoint = discovery.get("token_endpoint") token_data = { "grant_type": "authorization_code", "code": code, @@ -43,7 +64,7 @@ try: async with httpx.AsyncClient() as client: - token_response = await client.post(oidc_urls['token'], data=token_data, timeout=30.0) + token_response = await client.post(token_endpoint, data=token_data, timeout=30.0) token_response.raise_for_status() response_json = token_response.json() id_token = response_json.get("id_token") @@ -56,9 +77,7 @@ raise HTTPException(status_code=500, detail=f"Failed to communicate with OIDC provider: {e}") # 1. Fetch JWKS (Public Keys) to verify signature - # Standard OIDC path - normally found in .well-known/openid-configuration - # For efficiency in a production environment, these should be cached. - jwks_url = f"{settings.OIDC_SERVER_URL.rstrip('/')}/jwks" + jwks_url = discovery.get("jwks_uri") try: async with httpx.AsyncClient() as client: jwks_response = await client.get(jwks_url, timeout=10.0) diff --git a/ai-hub/app/core/services/node_registry.py b/ai-hub/app/core/services/node_registry.py index 7d178cd..38eb650 100644 --- a/ai-hub/app/core/services/node_registry.py +++ b/ai-hub/app/core/services/node_registry.py @@ -90,7 +90,7 @@ if isinstance(msg, agent_pb2.ServerTaskMessage): msg.signature = "" - msg_bytes = msg.SerializeToString() + msg_bytes = msg.SerializeToString(deterministic=True) msg.signature = sign_bytes(msg_bytes) item = (priority, time.time(), msg)