elo-node 0.4.3__tar.gz → 0.4.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elo-node
3
- Version: 0.4.3
3
+ Version: 0.4.5
4
4
  Summary: Elo — malha P2P de mensagens para agentes de IA. Zero infraestrutura.
5
5
  Author: Elo Contributors
6
6
  License: MIT
@@ -26,4 +26,4 @@ __all__ = [
26
26
  "Node", "Task", "Result", "Event", "NodeInfo", "Capabilities",
27
27
  "EphemeralIdentity", "generate_and_save_identity", "load_identity",
28
28
  ]
29
- __version__ = "0.4.3"
29
+ __version__ = "0.4.4"
@@ -78,7 +78,7 @@ class Node:
78
78
  peers: list[str] | None = None,
79
79
  tracker: str = "public",
80
80
  allowlist: list[str] | None = None,
81
- version: str = "0.4.3",
81
+ version: str = "0.4.5",
82
82
  identity: EphemeralIdentity | None = None,
83
83
  verify_peers: bool = True,
84
84
  heartbeat_interval_s: int = 30,
@@ -110,7 +110,7 @@ class Node:
110
110
  self._task_handler: Callable[[Task], Awaitable[dict[str, Any]]] | None = None
111
111
  self._shutdown_event = asyncio.Event()
112
112
  self._pending_results: dict[str, asyncio.Future] = {}
113
- self._pending_queries: dict[str, tuple[asyncio.Future, float]] = {}
113
+ self._pending_queries: dict[str, tuple[list[dict[str, Any]], asyncio.Event, float]] = {}
114
114
 
115
115
  # Cache de pubkeys para verificação de assinatura
116
116
  self._pubkey_cache: dict[str, tuple[Any, float]] = {}
@@ -253,6 +253,10 @@ class Node:
253
253
  except Exception as e:
254
254
  return Result.make_error(task_id, "SEND_ERROR", str(e))
255
255
 
256
+ # Bug 2: Fallback via tracker antes de desistir
257
+ if not peer:
258
+ return await self.send_task_via_tracker("", target_node, capability, payload, ttl_s=ttl_s)
259
+
256
260
  return Result.make_error(task_id, "NO_PEER", f"No peer for: {capability}")
257
261
 
258
262
  async def send_task_async(self, target_node: str, capability: str,
@@ -292,10 +296,11 @@ class Node:
292
296
 
293
297
  # ── descoberta ─────────────────────────────────────────────
294
298
 
295
- async def discover_peers(self) -> list[dict[str, Any]]:
296
- """Return all known peers with capabilities.
299
+ async def get_known_peers_local(self) -> list[dict[str, Any]]:
300
+ """Return all locally-known peers with capabilities (no network discovery).
297
301
 
298
302
  Merges data from TCP connections (live) and InterestTable (registered).
303
+ Does NOT perform any network queries — only returns what's already known.
299
304
  """
300
305
  result: dict[str, dict[str, Any]] = {}
301
306
 
@@ -314,6 +319,63 @@ class Node:
314
319
 
315
320
  return list(result.values())
316
321
 
322
+ # Alias de compatibilidade — o método anterior chamava-se discover_peers()
323
+ async def discover_peers(self) -> list[dict[str, Any]]:
324
+ """[DEPRECATED] Use get_known_peers_local() ou discover_peers_network().
325
+
326
+ Este método apenas consolida peers localmente conhecidos (TCP + InterestTable).
327
+ Não faz descoberta ativa de rede. Prefira discover_peers_network() para
328
+ descoberta via broadcast, ou get_known_peers() para peers com handshake completo.
329
+ """
330
+ return await self.get_known_peers_local()
331
+
332
+ async def discover_peers_network(self, timeout: float = 5.0) -> list[dict[str, Any]]:
333
+ """Discover peers on the network via QUERY broadcast.
334
+
335
+ Envia QUERY com capability vazia e agrega todas as respostas
336
+ dentro do timeout. Modelo BitTorrent: o tracker retorna todos
337
+ os peers conhecidos, não só quem matcha a capability.
338
+ """
339
+ query_id = str(uuid.uuid4())[:8]
340
+ collected: list[dict[str, Any]] = []
341
+ event = asyncio.Event()
342
+ self._pending_queries[query_id] = (collected, event, time.time())
343
+ discovered: dict[str, dict[str, Any]] = {}
344
+
345
+ # Broadcast QUERY for any capability
346
+ await self._tcp.broadcast(query_msg("", query_id, ttl=3))
347
+
348
+ try:
349
+ await asyncio.wait_for(event.wait(), timeout=timeout)
350
+ except asyncio.TimeoutError:
351
+ pass
352
+ finally:
353
+ self._pending_queries.pop(query_id, None)
354
+
355
+ # Merge collected peers from all responses
356
+ known_addrs: set[str] = set()
357
+ for entry in collected:
358
+ addr = entry.get("addr", "")
359
+ if addr and addr not in discovered:
360
+ discovered[addr] = {
361
+ "addr": addr,
362
+ "caps": entry.get("caps", []),
363
+ "connected": False,
364
+ "via": "network",
365
+ }
366
+ known_addrs.add(addr)
367
+
368
+ # Merge with locally known peers
369
+ local = await self.get_known_peers_local()
370
+ for entry in local:
371
+ addr = entry["addr"]
372
+ if addr not in discovered:
373
+ discovered[addr] = entry
374
+ else:
375
+ discovered[addr].update(entry)
376
+
377
+ return list(discovered.values())
378
+
317
379
  def get_known_peers(self) -> list[dict[str, Any]]:
318
380
  """Return peers registered in InterestTable (completed HELLO handshake).
319
381
 
@@ -355,6 +417,18 @@ class Node:
355
417
  await self._on_hello(peer_addr, msg)
356
418
  elif msg_type == MessageType.HELLO_ACK:
357
419
  await self._on_hello(peer_addr, msg)
420
+ # Bug 1: Se HELLO_ACK contém known_peers, conectar-se a eles
421
+ known_peers = msg.get("known_peers")
422
+ if known_peers:
423
+ hello = hello_msg(self._node_id, self._tracker.get_public_caps(),
424
+ list(self._routing.local_interests),
425
+ self._tracker.visibility, self._version)
426
+ for peer_info in known_peers:
427
+ addr = peer_info.get("addr", "")
428
+ if addr and addr != peer_addr and addr not in self._tcp.peer_addresses:
429
+ asyncio.create_task(
430
+ self._tcp.connect_to_peer(addr, hello_payload=hello)
431
+ )
358
432
  elif msg_type == MessageType.QUERY:
359
433
  await self._on_query(peer_addr, msg)
360
434
  elif msg_type == MessageType.QUERY_RESP:
@@ -376,8 +450,14 @@ class Node:
376
450
  interests = msg.get("interests", [])
377
451
  self._routing.register_peer(peer_addr, caps, interests)
378
452
 
453
+ # Se for tracker (public/private), inclui peers conhecidos no ACK
454
+ known_peers = None
455
+ if msg.get("type") == MessageType.HELLO and self._tracker.visibility in ("public", "private"):
456
+ known_peers = self.get_known_peers()
457
+
379
458
  ack = hello_ack_msg(self._node_id, self._tracker.get_caps_for_peer(node_id),
380
- list(self._routing.local_interests), self._tracker.visibility)
459
+ list(self._routing.local_interests), self._tracker.visibility,
460
+ known_peers=known_peers)
381
461
  try:
382
462
  await self._tcp.send_to(peer_addr, ack)
383
463
  except Exception:
@@ -387,12 +467,23 @@ class Node:
387
467
  capability = msg.get("capability", "")
388
468
  query_id = msg.get("id", "")
389
469
  ttl = msg.get("ttl", 5)
390
- nodes = []
470
+ nodes: list[dict[str, Any]] = []
471
+
472
+ # Modelo BitTorrent: tracker retorna SEMPRE todos os peers conhecidos,
473
+ # não só quem matcha a capability. Isso permite que qualquer nó
474
+ # descubra a mesh completa com discover_peers_network().
475
+ for addr in self._routing.known_peers:
476
+ info = self._routing.get_peer_caps(addr)
477
+ if addr != peer_addr:
478
+ nodes.append({
479
+ "addr": addr,
480
+ "caps": list(info.get("caps", set())),
481
+ })
482
+
391
483
  if self._tracker.has_capability(capability):
392
484
  # Usa peer_addr (addr real do TCP) em vez de localhost — necessário para WAN/Tailscale
393
485
  nodes.append({"node_id": self._node_id[:12], "addr": peer_addr})
394
- for p in self._routing.find_all_peers_for(capability):
395
- nodes.append({"addr": p})
486
+
396
487
  if nodes:
397
488
  try:
398
489
  await self._tcp.send_to(peer_addr, query_resp_msg(query_id, nodes))
@@ -404,10 +495,11 @@ class Node:
404
495
  async def _on_query_resp(self, peer_addr: str, msg: dict) -> None:
405
496
  query_id = msg.get("id", "")
406
497
  if query_id in self._pending_queries:
407
- future, _ = self._pending_queries[query_id]
498
+ collected, event, _ts = self._pending_queries[query_id]
408
499
  nodes = msg.get("nodes", [])
409
- if nodes and not future.done():
410
- future.set_result(nodes[0].get("addr", ""))
500
+ if nodes:
501
+ collected.extend(nodes)
502
+ event.set()
411
503
 
412
504
  async def _on_interest_update(self, peer_addr: str, msg: dict) -> None:
413
505
  existing = self._routing.get_peer_caps(peer_addr)
@@ -536,15 +628,19 @@ class Node:
536
628
  async def _query_capability(self, capability: str, ttl: int = 5,
537
629
  timeout: float = 5.0) -> str | None:
538
630
  query_id = str(uuid.uuid4())[:8]
539
- future: asyncio.Future = asyncio.get_event_loop().create_future()
540
- self._pending_queries[query_id] = (future, time.time())
631
+ collected: list[dict[str, Any]] = []
632
+ event = asyncio.Event()
633
+ self._pending_queries[query_id] = (collected, event, time.time())
541
634
  await self._tcp.broadcast(query_msg(capability, query_id, ttl))
542
635
  try:
543
- return await asyncio.wait_for(future, timeout=timeout)
636
+ await asyncio.wait_for(event.wait(), timeout=timeout)
544
637
  except asyncio.TimeoutError:
545
- return None
638
+ pass
546
639
  finally:
547
640
  self._pending_queries.pop(query_id, None)
641
+ if collected:
642
+ return collected[0].get("addr", None)
643
+ return None
548
644
 
549
645
  async def _get_caller_pubkey(self, node_id: str) -> Any | None:
550
646
  now = time.time()
@@ -89,14 +89,18 @@ def hello_msg(node_id: str, caps: dict, interests: list[str],
89
89
 
90
90
 
91
91
  def hello_ack_msg(node_id: str, caps: dict, interests: list[str],
92
- tracker: str = "public") -> dict:
93
- return {
92
+ tracker: str = "public",
93
+ known_peers: list[dict] | None = None) -> dict:
94
+ msg: dict[str, Any] = {
94
95
  "type": MessageType.HELLO_ACK,
95
96
  "node_id": node_id,
96
97
  "caps": caps,
97
98
  "interests": interests,
98
99
  "tracker": tracker,
99
100
  }
101
+ if known_peers is not None:
102
+ msg["known_peers"] = known_peers
103
+ return msg
100
104
 
101
105
 
102
106
  def query_msg(capability: str, query_id: str, ttl: int = 5) -> dict:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elo-node
3
- Version: 0.4.3
3
+ Version: 0.4.5
4
4
  Summary: Elo — malha P2P de mensagens para agentes de IA. Zero infraestrutura.
5
5
  Author: Elo Contributors
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "elo-node"
7
- version = "0.4.3"
7
+ version = "0.4.5"
8
8
  description = "Elo — malha P2P de mensagens para agentes de IA. Zero infraestrutura."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -136,7 +136,7 @@ class TestNodeConstruction:
136
136
  node = Node("test-node")
137
137
  assert node._name == "test-node"
138
138
  assert node._port == 7878
139
- assert node._version == "0.4.0"
139
+ assert node._version == "0.4.4"
140
140
  assert node.connected is False
141
141
  assert node.node_id is not None
142
142
  assert len(node.node_id) > 20
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes