GitHub Action commited on
Commit
ab3306e
·
1 Parent(s): bcc313a

Sync from GitHub with Git LFS

Browse files
agents/bootstrap.txt CHANGED
@@ -1,4 +0,0 @@
1
- did:hmp:ac0e063e-8609-4ef9-75f8-d7dcafc65977 ["tcp://node1.mesh.local:8000","udp://node1.mesh.local:8030"]
2
- did:hmp:ac0e063e-8709-4ef9-75f8-d7dcafc65977 ["tcp://node2.mesh.local:8010"]
3
- did:hmp:ac0e063e-8609-4ff9-75f8-d7dc46c65977 ["tcp://node3.mesh.local:8020"]
4
- did:hmp:ac0f053e-8609-4ef9-75f8-d7dcafc65977 ["any://node4.mesh.local:8000"]
 
 
 
 
 
agents/examples/bootstrap.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ did:hmp:ac0e063e-8609-4ef9-75f8-d7dcafc65977 ["tcp://node1.mesh.local:8000","udp://node1.mesh.local:8030"] "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA+J7pfdbWotIo31Omd0b49bIFAc6nCZpriQc9BdbZ2TQ=\n-----END PUBLIC KEY-----\n" 12166 "0000967e52fdbeb393ec9e4376cf32605209da69fabc590607cea0574273e470"
2
+ did:hmp:ac0e063e-8709-4ef9-75f8-d7dcafc65977 ["tcp://node2.mesh.local:8010"] "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA6OZO4dJ9IZethuM+8f+m8WVRXwMsCPOYaNrwcaA1a08=\n-----END PUBLIC KEY-----\n" 82022 "00004a512b13ca5084fa913e890578befe1068ac06d3dba541589822a90a36c6"
3
+ did:hmp:ac0e063e-8609-4ff9-75f8-d7dc46c65977 ["tcp://node3.mesh.local:8020"] "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAAGTXI+KznuhV1N9+sb9/FHQndgmw2Ro2dAvwpRBrG78=\n-----END PUBLIC KEY-----\n" 47803 "0000beb657e738dabc9731eb911f45f6706fba7a8bc342583d022b004ce97921"
4
+ did:hmp:ac0f053e-8609-4ef9-75f8-d7dcafc65977 ["any://node4.mesh.local:8000"] "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA3HR98+9G+MbrAuMKx5mhGKccOlWdre9A+OKE1W0gMa4=\n-----END PUBLIC KEY-----\n" 13549 "00008b71f46aa0f0c9e3f15a6b32a24ef58a1642936e324a548b5aac97e989d6"
agents/init.py CHANGED
@@ -48,11 +48,7 @@ def init_identity(storage, config):
48
  privkey, pubkey = generate_keypair(method="ed25519")
49
  privkey, pubkey = privkey.decode(), pubkey.decode()
50
 
51
- # 3. Выполнить PoW (для начальных адресов можно пока [] или config["local_addresses"])
52
- addresses = config.get("local_addresses", [])
53
- nonce, pow_hash = storage.generate_pow(identity_id, pubkey, addresses, difficulty=4)
54
-
55
- # 4. Создать запись в identity
56
  identity = {
57
  "id": identity_id,
58
  "name": config.get("agent_name", "Unnamed"),
@@ -64,16 +60,14 @@ def init_identity(storage, config):
64
  }
65
  storage.add_identity(identity)
66
 
67
- # 5. Записать в config
68
  config["agent_id"] = did
69
  config["identity_agent"] = identity_id
70
  config["pubkey"] = pubkey
71
  config["privkey"] = privkey
72
- config["pow_nonce"] = nonce
73
- config["pow_hash"] = pow_hash
74
 
75
  save_config(CONFIG_PATH, config)
76
- print(f"[+] Создана личность: {identity_id}, PoW: {pow_hash[:8]}...")
77
  else:
78
  print("[=] agent_id уже задан, пропускаем генерацию DiD.")
79
 
@@ -132,10 +126,65 @@ def init_config_table(storage, config):
132
  else:
133
  flat_config[addr_key] = expand_addresses(value)
134
 
 
135
  for key, value in flat_config.items():
136
  storage.set_config(key, json.dumps(value))
137
  print("[+] Конфигурация сохранена в БД.")
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  def init_prompts_and_ethics():
140
  folder = os.path.dirname(__file__)
141
  prompt_files = [
@@ -246,6 +295,7 @@ def ensure_db_initialized():
246
  init_user(storage, config)
247
  init_llm_backends(storage, config)
248
  init_config_table(storage, config)
 
249
  save_config(CONFIG_PATH, config)
250
  init_prompts_and_ethics()
251
  except Exception as e:
 
48
  privkey, pubkey = generate_keypair(method="ed25519")
49
  privkey, pubkey = privkey.decode(), pubkey.decode()
50
 
51
+ # 3. Создать запись в identity
 
 
 
 
52
  identity = {
53
  "id": identity_id,
54
  "name": config.get("agent_name", "Unnamed"),
 
60
  }
61
  storage.add_identity(identity)
62
 
63
+ # 4. Записать в config
64
  config["agent_id"] = did
65
  config["identity_agent"] = identity_id
66
  config["pubkey"] = pubkey
67
  config["privkey"] = privkey
 
 
68
 
69
  save_config(CONFIG_PATH, config)
70
+ print(f"[+] Создана личность: {identity_id}.")
71
  else:
72
  print("[=] agent_id уже задан, пропускаем генерацию DiD.")
73
 
 
126
  else:
127
  flat_config[addr_key] = expand_addresses(value)
128
 
129
+ # Сохраняем конфиг в БД
130
  for key, value in flat_config.items():
131
  storage.set_config(key, json.dumps(value))
132
  print("[+] Конфигурация сохранена в БД.")
133
 
134
+ def update_pow_for_addresses(storage, difficulty=4):
135
+ raw_id = storage.get_config_value("agent_id")
136
+ if not raw_id:
137
+ print("[-] Нет agent_id в config — пропуск обновления PoW.")
138
+ return
139
+ agent_id = storage.normalize_did(raw_id)
140
+ pubkey = storage.get_config_value("pubkey")
141
+
142
+ if not agent_id or not pubkey:
143
+ print("[-] Нет agent_id/pubkey в config — пропуск обновления PoW.")
144
+ return
145
+
146
+ for addr_key in ("local_addresses", "global_addresses"):
147
+ raw = storage.get_config_value(addr_key)
148
+ if not raw:
149
+ continue
150
+
151
+ # raw может быть либо JSON-строкой, либо уже list
152
+ if isinstance(raw, str):
153
+ try:
154
+ addresses = json.loads(raw)
155
+ except Exception as e:
156
+ print(f"[!] Ошибка при чтении {addr_key}: {e}")
157
+ continue
158
+ else:
159
+ addresses = raw
160
+
161
+ enriched = []
162
+ for addr in addresses:
163
+ # если уже dict → PoW записан
164
+ if isinstance(addr, dict):
165
+ enriched.append(addr)
166
+ continue
167
+
168
+ nonce, hash_value = storage.generate_pow(
169
+ peer_id=agent_id,
170
+ pubkey=pubkey,
171
+ address=addr,
172
+ difficulty=difficulty
173
+ )
174
+ enriched.append({
175
+ "address": addr,
176
+ "expires": "",
177
+ "pow": {
178
+ "nonce": nonce,
179
+ "hash": hash_value,
180
+ "difficulty": difficulty
181
+ }
182
+ })
183
+
184
+ storage.set_config(addr_key, json.dumps(enriched))
185
+
186
+ print("[+] Адреса обновлены с PoW.")
187
+
188
  def init_prompts_and_ethics():
189
  folder = os.path.dirname(__file__)
190
  prompt_files = [
 
295
  init_user(storage, config)
296
  init_llm_backends(storage, config)
297
  init_config_table(storage, config)
298
+ update_pow_for_addresses(storage, difficulty=4)
299
  save_config(CONFIG_PATH, config)
300
  init_prompts_and_ethics()
301
  except Exception as e:
agents/peer_sync.py CHANGED
@@ -24,13 +24,35 @@ global_addresses = storage.get_config_value("global_addresses", [])
24
  all_addresses = local_addresses + global_addresses # один раз
25
 
26
  # Получаем уникальные локальные порты
27
- def get_local_ports():
28
- ports = set()
29
- for addr in local_addresses:
30
- _, port = storage.parse_hostport(addr.split("://", 1)[1])
31
- if port:
32
- ports.add(port)
33
- return sorted(ports)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  local_ports = get_local_ports()
36
  print(f"[PeerSync] Local ports: {local_ports}")
@@ -38,12 +60,7 @@ print(f"[PeerSync] Local ports: {local_ports}")
38
  # ---------------------------
39
  # Загрузка bootstrap
40
  # ---------------------------
41
-
42
  def load_bootstrap_peers(filename="bootstrap.txt"):
43
- """
44
- Читает bootstrap.txt и добавляет узлы в storage.
45
- Формат строки: did [JSON-список адресов]
46
- """
47
  try:
48
  with open(filename, "r", encoding="utf-8") as f:
49
  lines = f.readlines()
@@ -56,19 +73,57 @@ def load_bootstrap_peers(filename="bootstrap.txt"):
56
  if not line or line.startswith("#"):
57
  continue
58
 
59
- match = re.match(r"^(did:hmp:[\w-]+)\s+(.+)$", line)
60
- if not match:
61
- print(f"[Bootstrap] Invalid line format: {line}")
 
62
  continue
63
-
64
- did, addresses_json = match.groups()
 
 
 
 
 
 
 
 
65
  try:
66
  addresses = json.loads(addresses_json)
67
  except Exception as e:
68
- print(f"[Bootstrap] Invalid JSON addresses for {did}: {e}")
69
  continue
 
70
 
71
- # Разворачиваем any:// в tcp:// и udp://
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  expanded_addresses = []
73
  for addr in addresses:
74
  if addr.startswith("any://"):
@@ -78,8 +133,18 @@ def load_bootstrap_peers(filename="bootstrap.txt"):
78
  else:
79
  expanded_addresses.append(addr)
80
 
81
- storage.add_or_update_peer(did, name=None, addresses=expanded_addresses,
82
- source="bootstrap", status="offline")
 
 
 
 
 
 
 
 
 
 
83
  print(f"[Bootstrap] Loaded peer {did} -> {expanded_addresses}")
84
 
85
  # ---------------------------
 
24
  all_addresses = local_addresses + global_addresses # один раз
25
 
26
  # Получаем уникальные локальные порты
27
+ def get_local_ports(storage):
28
+ """
29
+ Возвращает список портов для всех локальных адресов.
30
+ Формат конфигурации: список dict {"addr": str, "nonce": int, "pow_hash": str, "expires": ...}
31
+ """
32
+ local_addrs_json = storage.get_config("local_addresses")
33
+ if not local_addrs_json:
34
+ return []
35
+
36
+ try:
37
+ local_addrs = json.loads(local_addrs_json)
38
+ except:
39
+ print("[WARN] Не удалось разобрать local_addresses из БД")
40
+ return []
41
+
42
+ ports = []
43
+ for entry in local_addrs:
44
+ # Если entry — словарь, берём поле addr, иначе предполагаем строку
45
+ addr_str = entry["addr"] if isinstance(entry, dict) else entry
46
+
47
+ # Разбираем протокол и host:port
48
+ try:
49
+ proto, hostport = addr_str.split("://", 1)
50
+ _, port = storage.parse_hostport(hostport)
51
+ ports.append(port)
52
+ except Exception as e:
53
+ print(f"[WARN] Не удалось разобрать адрес {addr_str}: {e}")
54
+
55
+ return ports
56
 
57
  local_ports = get_local_ports()
58
  print(f"[PeerSync] Local ports: {local_ports}")
 
60
  # ---------------------------
61
  # Загрузка bootstrap
62
  # ---------------------------
 
63
  def load_bootstrap_peers(filename="bootstrap.txt"):
 
 
 
 
64
  try:
65
  with open(filename, "r", encoding="utf-8") as f:
66
  lines = f.readlines()
 
73
  if not line or line.startswith("#"):
74
  continue
75
 
76
+ # 1. DID
77
+ did_end = line.find(" ")
78
+ if did_end == -1:
79
+ print(f"[Bootstrap] Invalid line (no DID): {line}")
80
  continue
81
+ did = line[:did_end]
82
+ rest = line[did_end + 1:].strip()
83
+
84
+ # 2. JSON-адреса
85
+ addr_start = rest.find("[")
86
+ addr_end = rest.find("]") + 1
87
+ if addr_start == -1 or addr_end == 0:
88
+ print(f"[Bootstrap] Invalid JSON addresses: {line}")
89
+ continue
90
+ addresses_json = rest[addr_start:addr_end]
91
  try:
92
  addresses = json.loads(addresses_json)
93
  except Exception as e:
94
+ print(f"[Bootstrap] Failed to parse JSON addresses: {line} ({e})")
95
  continue
96
+ rest = rest[addr_end:].strip()
97
 
98
+ # 3. pubkey (в кавычках)
99
+ pub_start = rest.find('"')
100
+ pub_end = rest.find('"', pub_start + 1)
101
+ if pub_start == -1 or pub_end == -1:
102
+ print(f"[Bootstrap] Invalid pubkey: {line}")
103
+ continue
104
+ pubkey = rest[pub_start + 1:pub_end].replace("\\n", "\n")
105
+ rest = rest[pub_end + 1:].strip()
106
+
107
+ # 4. pow_nonce
108
+ nonce_end = rest.find(" ")
109
+ if nonce_end == -1:
110
+ print(f"[Bootstrap] Invalid pow_nonce: {line}")
111
+ continue
112
+ try:
113
+ pow_nonce = int(rest[:nonce_end])
114
+ except ValueError:
115
+ print(f"[Bootstrap] Invalid pow_nonce: {rest[:nonce_end]} in line: {line}")
116
+ continue
117
+ rest = rest[nonce_end:].strip()
118
+
119
+ # 5. pow_hash (в кавычках)
120
+ if rest.startswith('"') and rest.endswith('"'):
121
+ pow_hash = rest[1:-1]
122
+ else:
123
+ print(f"[Bootstrap] Invalid pow_hash: {line}")
124
+ continue
125
+
126
+ # Разворачиваем any://
127
  expanded_addresses = []
128
  for addr in addresses:
129
  if addr.startswith("any://"):
 
133
  else:
134
  expanded_addresses.append(addr)
135
 
136
+ storage.add_or_update_peer(
137
+ peer_id=did,
138
+ name=None,
139
+ addresses=expanded_addresses,
140
+ source="bootstrap",
141
+ status="offline",
142
+ pubkey=pubkey,
143
+ capabilities=None,
144
+ pow_nonce=pow_nonce,
145
+ pow_hash=pow_hash
146
+ )
147
+
148
  print(f"[Bootstrap] Loaded peer {did} -> {expanded_addresses}")
149
 
150
  # ---------------------------
agents/tools/db_structure.sql CHANGED
@@ -151,7 +151,7 @@ CREATE TABLE IF NOT EXISTS llm_recent_responses (
151
  CREATE TABLE IF NOT EXISTS agent_peers (
152
  id TEXT PRIMARY KEY, -- Уникальный идентификатор (UUID или псевдоним)
153
  name TEXT, -- Имя агента
154
- addresses TEXT, -- Адреса для связи (JSON)
155
  tags TEXT, -- Теги (Postman, Friend и т.д.)
156
  status TEXT DEFAULT 'unknown', -- online | offline | untrusted | blacklisted и др.
157
  source TEXT, -- bootstrap | discovery | exchange
@@ -159,8 +159,6 @@ CREATE TABLE IF NOT EXISTS agent_peers (
159
  description TEXT, -- Описание агента
160
  capabilities TEXT, -- Возможности (JSON)
161
  pubkey TEXT, -- Публичный ключ
162
- pow_nonce INTEGER, -- Nonce для PoW
163
- pow_hash TEXT, -- Контрольный хеш PoW (например, sha256(pubkey + addresses + nonce))
164
  heard_from TEXT, -- JSON список DID, от кого агент о нем узнал
165
  software_info TEXT, -- Информация о ПО агента (JSON)
166
  registered_at DATETIME DEFAULT CURRENT_TIMESTAMP -- Время регистрации
 
151
  CREATE TABLE IF NOT EXISTS agent_peers (
152
  id TEXT PRIMARY KEY, -- Уникальный идентификатор (UUID или псевдоним)
153
  name TEXT, -- Имя агента
154
+ addresses TEXT, -- Адреса для связи (JSON), каждый адрес содержит addr, nonce, pow_hash, expiries
155
  tags TEXT, -- Теги (Postman, Friend и т.д.)
156
  status TEXT DEFAULT 'unknown', -- online | offline | untrusted | blacklisted и др.
157
  source TEXT, -- bootstrap | discovery | exchange
 
159
  description TEXT, -- Описание агента
160
  capabilities TEXT, -- Возможности (JSON)
161
  pubkey TEXT, -- Публичный ключ
 
 
162
  heard_from TEXT, -- JSON список DID, от кого агент о нем узнал
163
  software_info TEXT, -- Информация о ПО агента (JSON)
164
  registered_at DATETIME DEFAULT CURRENT_TIMESTAMP -- Время регистрации
agents/tools/storage.py CHANGED
@@ -677,11 +677,15 @@ class Storage:
677
  self.conn.commit()
678
  return cursor.lastrowid
679
 
680
- def generate_pow(self, peer_id, pubkey, addresses, difficulty=4):
 
 
 
681
  nonce = 0
682
  prefix = "0" * difficulty
 
683
  while True:
684
- base = f"{peer_id}{pubkey}{''.join(addresses)}{nonce}".encode()
685
  h = hashlib.sha256(base).hexdigest()
686
  if h.startswith(prefix):
687
  return nonce, h
@@ -963,9 +967,19 @@ class Storage:
963
 
964
  return f"{proto}://{host}:{port}" if port else f"{proto}://{host}"
965
 
 
 
 
 
 
966
  # Работа с пирам (agent_peers)
967
- def verify_pow(peer_id, pubkey, addresses, nonce, pow_hash, difficulty=4):
968
- base = f"{peer_id}{pubkey}{''.join(addresses)}{nonce}".encode()
 
 
 
 
 
969
  h = hashlib.sha256(base).hexdigest()
970
  return h == pow_hash and h.startswith("0" * difficulty)
971
 
@@ -973,14 +987,24 @@ class Storage:
973
  self, peer_id, name, addresses,
974
  source="discovery", status="unknown",
975
  pubkey=None, capabilities=None,
976
- pow_nonce=None, pow_hash=None,
977
  heard_from=None
978
  ):
979
  c = self.conn.cursor()
980
 
981
- # нормализация адресов
982
- addresses = list({self.normalize_address(a) for a in (addresses or []) if a and a.strip()})
983
-
 
 
 
 
 
 
 
 
 
 
 
984
  existing_addresses = []
985
  existing_pubkey = None
986
  existing_capabilities = {}
@@ -1004,37 +1028,42 @@ class Storage:
1004
  except:
1005
  existing_heard_from = []
1006
 
1007
- # объединяем адреса
1008
- combined_addresses = list({self.normalize_address(a) for a in (*existing_addresses, *addresses)})
1009
- final_pubkey = pubkey or existing_pubkey
1010
  final_capabilities = capabilities or existing_capabilities
1011
-
1012
- # обновляем heard_from
1013
  combined_heard_from = list(set(existing_heard_from + (heard_from or [])))
1014
 
1015
- # TODO: Проверка PoW (например, pow_hash.startswith("0000"))
1016
- if pow_hash and pow_nonce:
1017
- # простейшая проверка PoW
1018
- import hashlib
1019
- base = f"{peer_id}{final_pubkey}{''.join(combined_addresses)}{pow_nonce}".encode()
1020
- calc_hash = hashlib.sha256(base).hexdigest()
1021
- if calc_hash != pow_hash:
1022
- print(f"[WARN] Peer {peer_id} failed PoW validation")
1023
- return # не вставляем
1024
-
 
 
 
 
 
 
 
 
 
 
 
 
 
1025
  c.execute("""
1026
- INSERT INTO agent_peers (id, name, addresses, source, status, last_seen, pubkey, capabilities, pow_nonce, pow_hash, heard_from)
1027
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1028
  ON CONFLICT(id) DO UPDATE SET
1029
  name=excluded.name,
1030
  addresses=excluded.addresses,
1031
  source=excluded.source,
1032
  status=excluded.status,
1033
  last_seen=excluded.last_seen,
1034
- pubkey=excluded.pubkey,
1035
  capabilities=excluded.capabilities,
1036
- pow_nonce=excluded.pow_nonce,
1037
- pow_hash=excluded.pow_hash,
1038
  heard_from=excluded.heard_from
1039
  """, (
1040
  peer_id,
@@ -1045,8 +1074,6 @@ class Storage:
1045
  datetime.now(UTC).isoformat(),
1046
  final_pubkey,
1047
  json.dumps(final_capabilities),
1048
- pow_nonce,
1049
- pow_hash,
1050
  json.dumps(combined_heard_from)
1051
  ))
1052
  self.conn.commit()
 
677
  self.conn.commit()
678
  return cursor.lastrowid
679
 
680
+ def generate_pow(self, peer_id, pubkey, address, expires=None, difficulty=4):
681
+ """
682
+ Генерирует PoW для одной пары (peer_id + pubkey + address + expires).
683
+ """
684
  nonce = 0
685
  prefix = "0" * difficulty
686
+ expires_str = str(expires) if expires is not None else ""
687
  while True:
688
+ base = f"{peer_id}{pubkey}{address}{expires_str}{nonce}".encode()
689
  h = hashlib.sha256(base).hexdigest()
690
  if h.startswith(prefix):
691
  return nonce, h
 
967
 
968
  return f"{proto}://{host}:{port}" if port else f"{proto}://{host}"
969
 
970
+ # Нормализация DID
971
+ @staticmethod
972
+ def normalize_did(did: str) -> str:
973
+ return did.strip().strip('"').strip("'")
974
+
975
  # Работа с пирам (agent_peers)
976
+ @staticmethod
977
+ def verify_pow(peer_id, pubkey, address, nonce, pow_hash, expires=None, difficulty=4):
978
+ """
979
+ Проверяет PoW для одной пары (peer_id + pubkey + address + expires).
980
+ """
981
+ expires_str = str(expires) if expires is not None else ""
982
+ base = f"{peer_id}{pubkey}{address}{expires_str}{nonce}".encode()
983
  h = hashlib.sha256(base).hexdigest()
984
  return h == pow_hash and h.startswith("0" * difficulty)
985
 
 
987
  self, peer_id, name, addresses,
988
  source="discovery", status="unknown",
989
  pubkey=None, capabilities=None,
 
990
  heard_from=None
991
  ):
992
  c = self.conn.cursor()
993
 
994
+ # нормализуем входные адреса
995
+ norm_addresses = []
996
+ for a in (addresses or []):
997
+ if isinstance(a, dict) and "addr" in a:
998
+ norm_addresses.append({
999
+ "addr": self.normalize_address(a["addr"]),
1000
+ "nonce": a.get("nonce"),
1001
+ "pow_hash": a.get("pow_hash"),
1002
+ "expires": a.get("expires")
1003
+ })
1004
+ elif isinstance(a, str):
1005
+ norm_addresses.append({"addr": self.normalize_address(a), "nonce": None, "pow_hash": None, "expires": None})
1006
+
1007
+ # получаем существующую запись
1008
  existing_addresses = []
1009
  existing_pubkey = None
1010
  existing_capabilities = {}
 
1028
  except:
1029
  existing_heard_from = []
1030
 
 
 
 
1031
  final_capabilities = capabilities or existing_capabilities
 
 
1032
  combined_heard_from = list(set(existing_heard_from + (heard_from or [])))
1033
 
1034
+ # Проверка неизменности pubkey
1035
+ if existing_pubkey and pubkey and existing_pubkey != pubkey:
1036
+ print(f"[WARN] Peer {peer_id} pubkey mismatch! Possible impersonation attempt.")
1037
+ return
1038
+ final_pubkey = existing_pubkey or pubkey
1039
+
1040
+ # Объединяем адреса по addr, проверяем PoW
1041
+ addr_map = {a["addr"]: a for a in existing_addresses if isinstance(a, dict)}
1042
+ for a in norm_addresses:
1043
+ addr = a["addr"]
1044
+ nonce = a.get("nonce")
1045
+ pow_hash = a.get("pow_hash")
1046
+ expires = a.get("expires")
1047
+ # проверка PoW
1048
+ if nonce is not None and pow_hash is not None:
1049
+ if not self.verify_pow(peer_id, final_pubkey, addr, nonce, pow_hash, expires):
1050
+ print(f"[WARN] Peer {peer_id} address {addr} failed PoW validation")
1051
+ continue
1052
+ addr_map[addr] = {"addr": addr, "nonce": nonce, "pow_hash": pow_hash, "expires": expires}
1053
+
1054
+ combined_addresses = list(addr_map.values())
1055
+
1056
+ # Вставка/обновление записи
1057
  c.execute("""
1058
+ INSERT INTO agent_peers (id, name, addresses, source, status, last_seen, pubkey, capabilities, heard_from)
1059
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1060
  ON CONFLICT(id) DO UPDATE SET
1061
  name=excluded.name,
1062
  addresses=excluded.addresses,
1063
  source=excluded.source,
1064
  status=excluded.status,
1065
  last_seen=excluded.last_seen,
 
1066
  capabilities=excluded.capabilities,
 
 
1067
  heard_from=excluded.heard_from
1068
  """, (
1069
  peer_id,
 
1074
  datetime.now(UTC).isoformat(),
1075
  final_pubkey,
1076
  json.dumps(final_capabilities),
 
 
1077
  json.dumps(combined_heard_from)
1078
  ))
1079
  self.conn.commit()