elo-node 0.4.2__tar.gz → 0.4.4__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.2
3
+ Version: 0.4.4
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.2"
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.2",
81
+ version: str = "0.4.4",
82
82
  identity: EphemeralIdentity | None = None,
83
83
  verify_peers: bool = True,
84
84
  heartbeat_interval_s: int = 30,
@@ -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,56 @@ 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 with empty capability.
334
+
335
+ Sends a broadcast QUERY for an empty capability (catch-all) and aggregates
336
+ responses for the given timeout period. Returns list of discovered peer dicts.
337
+
338
+ Args:
339
+ timeout: seconds to wait for responses (default 5.0)
340
+
341
+ Returns:
342
+ List of dicts with 'addr' and optionally 'node_id'
343
+ """
344
+ query_id = str(uuid.uuid4())[:8]
345
+ future: asyncio.Future = asyncio.get_event_loop().create_future()
346
+ self._pending_queries[query_id] = (future, time.time())
347
+ discovered: dict[str, dict[str, Any]] = {}
348
+
349
+ # Broadcast QUERY for any capability
350
+ await self._tcp.broadcast(query_msg("", query_id, ttl=3))
351
+
352
+ try:
353
+ result_addr = await asyncio.wait_for(future, timeout=timeout)
354
+ if result_addr:
355
+ discovered[result_addr] = {"addr": result_addr, "connected": False, "caps": [], "via": "network"}
356
+ except asyncio.TimeoutError:
357
+ pass
358
+ finally:
359
+ self._pending_queries.pop(query_id, None)
360
+
361
+ # Merge with locally known peers
362
+ local = await self.get_known_peers_local()
363
+ for entry in local:
364
+ addr = entry["addr"]
365
+ if addr not in discovered:
366
+ discovered[addr] = entry
367
+ else:
368
+ discovered[addr].update(entry)
369
+
370
+ return list(discovered.values())
371
+
317
372
  def get_known_peers(self) -> list[dict[str, Any]]:
318
373
  """Return peers registered in InterestTable (completed HELLO handshake).
319
374
 
@@ -355,6 +410,18 @@ class Node:
355
410
  await self._on_hello(peer_addr, msg)
356
411
  elif msg_type == MessageType.HELLO_ACK:
357
412
  await self._on_hello(peer_addr, msg)
413
+ # Bug 1: Se HELLO_ACK contém known_peers, conectar-se a eles
414
+ known_peers = msg.get("known_peers")
415
+ if known_peers:
416
+ hello = hello_msg(self._node_id, self._tracker.get_public_caps(),
417
+ list(self._routing.local_interests),
418
+ self._tracker.visibility, self._version)
419
+ for peer_info in known_peers:
420
+ addr = peer_info.get("addr", "")
421
+ if addr and addr != peer_addr and addr not in self._tcp.peer_addresses:
422
+ asyncio.create_task(
423
+ self._tcp.connect_to_peer(addr, hello_payload=hello)
424
+ )
358
425
  elif msg_type == MessageType.QUERY:
359
426
  await self._on_query(peer_addr, msg)
360
427
  elif msg_type == MessageType.QUERY_RESP:
@@ -376,8 +443,14 @@ class Node:
376
443
  interests = msg.get("interests", [])
377
444
  self._routing.register_peer(peer_addr, caps, interests)
378
445
 
446
+ # Se for tracker (public/private), inclui peers conhecidos no ACK
447
+ known_peers = None
448
+ if msg.get("type") == MessageType.HELLO and self._tracker.visibility in ("public", "private"):
449
+ known_peers = self.get_known_peers()
450
+
379
451
  ack = hello_ack_msg(self._node_id, self._tracker.get_caps_for_peer(node_id),
380
- list(self._routing.local_interests), self._tracker.visibility)
452
+ list(self._routing.local_interests), self._tracker.visibility,
453
+ known_peers=known_peers)
381
454
  try:
382
455
  await self._tcp.send_to(peer_addr, ack)
383
456
  except Exception:
@@ -479,6 +552,48 @@ class Node:
479
552
  async def _on_bye(self, peer_addr: str, msg: dict) -> None:
480
553
  self._routing.remove_peer(peer_addr)
481
554
 
555
+ # ── relay via tracker ───────────────────────────────────
556
+
557
+ async def send_task_via_tracker(
558
+ self,
559
+ tracker_node: str,
560
+ target: str,
561
+ capability: str,
562
+ payload: dict[str, Any],
563
+ *,
564
+ ttl_s: int = 60,
565
+ ) -> Result:
566
+ """Send a task via tracker relay (for peers behind NAT/Docker).
567
+
568
+ Args:
569
+ tracker_node: node_id or addr of tracker (empty = auto)
570
+ target: node_id prefix or name of the destination
571
+ capability: capability to invoke on destination
572
+ payload: task payload
573
+ """
574
+ task_id = str(uuid.uuid4())
575
+ relay_payload = {
576
+ "target": target,
577
+ "capability": capability,
578
+ "payload": payload,
579
+ "ttl_s": ttl_s,
580
+ }
581
+ task_dict = p2p_task_msg(
582
+ task_id, tracker_node or "", self._node_id, "relay", relay_payload
583
+ )
584
+ task_dict.pop("signature", None)
585
+ task_dict["signature"] = self._identity.sign(task_dict)
586
+
587
+ peer = tracker_node or self._routing.find_peer_for("relay")
588
+ if peer:
589
+ try:
590
+ await self._tcp.send_to(peer, task_dict)
591
+ return await self._wait_for_result(task_id, peer)
592
+ except Exception as e:
593
+ return Result.make_error(task_id, "RELAY_ERROR", str(e))
594
+
595
+ return Result.make_error(task_id, "NO_TRACKER", "No tracker peer found")
596
+
482
597
  # ── helpers ───────────────────────────────────────────────
483
598
 
484
599
  async def _wait_for_result(self, task_id: str, peer_addr: str) -> Result:
@@ -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.2
3
+ Version: 0.4.4
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.2"
7
+ version = "0.4.4"
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