intermesh 0.4.1__tar.gz → 0.4.2__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.
Files changed (41) hide show
  1. {intermesh-0.4.1 → intermesh-0.4.2}/PKG-INFO +20 -13
  2. {intermesh-0.4.1 → intermesh-0.4.2}/README.md +19 -12
  3. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/__init__.py +2 -2
  4. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/agent.py +46 -8
  5. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/cli.py +13 -7
  6. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/crypto.py +25 -0
  7. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh.egg-info/PKG-INFO +20 -13
  8. {intermesh-0.4.1 → intermesh-0.4.2}/pyproject.toml +1 -1
  9. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/adapters.py +0 -0
  10. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/admin.py +0 -0
  11. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/apikeys.py +0 -0
  12. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/approval.py +0 -0
  13. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/audit.py +0 -0
  14. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/bridge.py +0 -0
  15. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/config.py +0 -0
  16. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/egress.py +0 -0
  17. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/escrow.py +0 -0
  18. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/guardrails.py +0 -0
  19. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/hardware.py +0 -0
  20. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/health.py +0 -0
  21. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/hub.py +0 -0
  22. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/identity.py +0 -0
  23. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/logger.py +0 -0
  24. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/message.py +0 -0
  25. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/metrics.py +0 -0
  26. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/peering.py +0 -0
  27. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/pipeline.py +0 -0
  28. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/policy.py +0 -0
  29. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/ratelimit.py +0 -0
  30. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/schema.py +0 -0
  31. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/secret.py +0 -0
  32. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/signing.py +0 -0
  33. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/snapshot.py +0 -0
  34. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/store.py +0 -0
  35. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh/task.py +0 -0
  36. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh.egg-info/SOURCES.txt +0 -0
  37. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh.egg-info/dependency_links.txt +0 -0
  38. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh.egg-info/entry_points.txt +0 -0
  39. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh.egg-info/requires.txt +0 -0
  40. {intermesh-0.4.1 → intermesh-0.4.2}/intermesh.egg-info/top_level.txt +0 -0
  41. {intermesh-0.4.1 → intermesh-0.4.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: intermesh
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Universal coordination protocol for AI agents — E2E encrypted, cross-language, federated
5
5
  Author: InterMesh Protocol Authors
6
6
  License: Apache-2.0
@@ -78,19 +78,26 @@ asyncio.run(main())
78
78
  **Delegate work from an orchestrator:**
79
79
 
80
80
  ```python
81
+ import asyncio
81
82
  from intermesh import InterMeshAgent
82
83
 
83
- orchestrator = InterMeshAgent(name="lead", roles=["admin"])
84
- await orchestrator.connect()
84
+ # `await` is not valid at module level in Python, so this lives in a
85
+ # coroutine. Copy-pasting it without the wrapper is a SyntaxError, not a
86
+ # runtime error — it fails before anything runs.
87
+ async def main():
88
+ orchestrator = InterMeshAgent(name="lead", roles=["admin"])
89
+ await orchestrator.connect()
90
+
91
+ # Find an agent by capability, then hand it a task
92
+ found = await orchestrator.discover(capabilities=["calculate"])
93
+ result = await orchestrator.submit_task(
94
+ title="Add two numbers",
95
+ assignee=found["agents"][0]["name"],
96
+ input_data={"a": 20, "b": 22},
97
+ )
98
+ print(result) # {"result": 42} — encrypted end-to-end in transit
85
99
 
86
- # Find an agent by capability, then hand it a task
87
- found = await orchestrator.discover(capabilities=["calculate"])
88
- result = await orchestrator.submit_task(
89
- title="Add two numbers",
90
- assignee=found["agents"][0]["name"],
91
- input_data={"a": 20, "b": 22},
92
- )
93
- # {"result": 42} — encrypted end-to-end in transit
100
+ asyncio.run(main())
94
101
  ```
95
102
 
96
103
  ---
@@ -106,7 +113,7 @@ A LangChain runnable:
106
113
  from intermesh import from_langchain
107
114
 
108
115
  agent = from_langchain(my_chain, name="analyst", capabilities=["market_analysis"])
109
- await agent.connect()
116
+ agent.run() # connects, then stays in service
110
117
  ```
111
118
 
112
119
  Anything else — a CrewAI crew, an AutoGen agent, a LlamaIndex engine, or a
@@ -122,7 +129,7 @@ async def run(data):
122
129
  return await asyncio.to_thread(lambda: my_crew.kickoff(inputs=data))
123
130
 
124
131
  agent = from_callable(run, name="research", capabilities=["research"])
125
- await agent.connect()
132
+ agent.run()
126
133
  ```
127
134
 
128
135
  Or as a decorator:
@@ -47,19 +47,26 @@ asyncio.run(main())
47
47
  **Delegate work from an orchestrator:**
48
48
 
49
49
  ```python
50
+ import asyncio
50
51
  from intermesh import InterMeshAgent
51
52
 
52
- orchestrator = InterMeshAgent(name="lead", roles=["admin"])
53
- await orchestrator.connect()
53
+ # `await` is not valid at module level in Python, so this lives in a
54
+ # coroutine. Copy-pasting it without the wrapper is a SyntaxError, not a
55
+ # runtime error — it fails before anything runs.
56
+ async def main():
57
+ orchestrator = InterMeshAgent(name="lead", roles=["admin"])
58
+ await orchestrator.connect()
59
+
60
+ # Find an agent by capability, then hand it a task
61
+ found = await orchestrator.discover(capabilities=["calculate"])
62
+ result = await orchestrator.submit_task(
63
+ title="Add two numbers",
64
+ assignee=found["agents"][0]["name"],
65
+ input_data={"a": 20, "b": 22},
66
+ )
67
+ print(result) # {"result": 42} — encrypted end-to-end in transit
54
68
 
55
- # Find an agent by capability, then hand it a task
56
- found = await orchestrator.discover(capabilities=["calculate"])
57
- result = await orchestrator.submit_task(
58
- title="Add two numbers",
59
- assignee=found["agents"][0]["name"],
60
- input_data={"a": 20, "b": 22},
61
- )
62
- # {"result": 42} — encrypted end-to-end in transit
69
+ asyncio.run(main())
63
70
  ```
64
71
 
65
72
  ---
@@ -75,7 +82,7 @@ A LangChain runnable:
75
82
  from intermesh import from_langchain
76
83
 
77
84
  agent = from_langchain(my_chain, name="analyst", capabilities=["market_analysis"])
78
- await agent.connect()
85
+ agent.run() # connects, then stays in service
79
86
  ```
80
87
 
81
88
  Anything else — a CrewAI crew, an AutoGen agent, a LlamaIndex engine, or a
@@ -91,7 +98,7 @@ async def run(data):
91
98
  return await asyncio.to_thread(lambda: my_crew.kickoff(inputs=data))
92
99
 
93
100
  agent = from_callable(run, name="research", capabilities=["research"])
94
- await agent.connect()
101
+ agent.run()
95
102
  ```
96
103
 
97
104
  Or as a decorator:
@@ -1,4 +1,4 @@
1
- from intermesh.agent import InterMeshAgent
1
+ from intermesh.agent import InterMeshAgent, EncryptionMismatch
2
2
  from intermesh.message import MessageType, InterMeshMessage
3
3
  from intermesh.identity import AgentIdentity
4
4
  from intermesh.task import InterMeshTask, TaskStatus
@@ -53,4 +53,4 @@ try:
53
53
  except ImportError:
54
54
  pass
55
55
 
56
- __version__ = "0.4.1"
56
+ __version__ = "0.4.2"
@@ -10,8 +10,17 @@ import websockets
10
10
  from intermesh.message import MessageType, InterMeshMessage
11
11
  from intermesh.identity import AgentIdentity
12
12
  from intermesh.task import InterMeshTask, TaskStatus
13
- from intermesh.crypto import generate_keypair, get_public_key_pem, encrypt_for, decrypt_with
13
+ from intermesh.crypto import (generate_keypair, get_public_key_pem, encrypt_for,
14
+ decrypt_with, looks_encrypted)
14
15
  from intermesh.egress import EgressPolicy, apply_egress
16
+
17
+
18
+ class EncryptionMismatch(ValueError):
19
+ """Les deux côtés ne s'accordent pas sur le chiffrement.
20
+
21
+ Levée plutôt que de rendre du texte chiffré à un gestionnaire, qui le
22
+ traiterait comme des données et rendrait un résultat faux.
23
+ """
15
24
  from intermesh.schema import SchemaRegistry, default_registry
16
25
 
17
26
 
@@ -329,10 +338,41 @@ class InterMeshAgent:
329
338
  decrypted = decrypt_with(self._private_key, encrypted_content)
330
339
  try: return json.loads(decrypted)
331
340
  except json.JSONDecodeError: return decrypted
332
- except Exception:
341
+ except Exception as exc:
342
+ # Une charge reconnaissable comme chiffrée mais qu'on n'arrive pas
343
+ # à ouvrir n'est pas une chaîne ordinaire : la rendre telle quelle
344
+ # la ferait traiter comme des données.
345
+ if looks_encrypted(encrypted_content):
346
+ raise EncryptionMismatch(
347
+ "Charge chiffrée illisible : elle a été chiffrée pour une autre "
348
+ "clé publique que celle de cet agent. Un agent qui se reconnecte "
349
+ "sous le même nom change de clé ; l'émetteur doit alors la "
350
+ "redemander (WHO_IS) avant de chiffrer."
351
+ ) from exc
333
352
  try: return json.loads(encrypted_content)
334
353
  except json.JSONDecodeError: return encrypted_content
335
354
 
355
+ def _unwrap_incoming(self, value, what: str):
356
+ """Rend la charge exploitable, ou refuse franchement.
357
+
358
+ Le piège que ceci ferme : avec `encrypt=False`, aucun déchiffrement
359
+ n'était tenté et le texte chiffré arrivait tel quel au gestionnaire,
360
+ qui le traitait comme des données. La tâche « réussissait » et
361
+ rendait un résultat faux — le pire mode de défaillance pour un
362
+ produit dont l'argument est le chiffrement de bout en bout.
363
+ """
364
+ if self.encrypt:
365
+ return self._decrypt_content(value) if isinstance(value, str) else value
366
+
367
+ if looks_encrypted(value):
368
+ raise EncryptionMismatch(
369
+ f"{what} chiffré reçu alors que le chiffrement est désactivé sur cet "
370
+ f"agent (encrypt=False). Activez-le ici, ou désactivez-le chez "
371
+ f"l'émetteur : les deux côtés doivent s'accorder. Sans ce refus, le "
372
+ f"texte chiffré serait traité comme des données."
373
+ )
374
+ return value
375
+
336
376
  async def _respond_to_request(self, msg) -> None:
337
377
  """Exécute le gestionnaire de requêtes et renvoie la réponse.
338
378
 
@@ -410,8 +450,7 @@ class InterMeshAgent:
410
450
  asyncio.create_task(self._respond_to_request(msg))
411
451
 
412
452
  elif msg.type == MessageType.MESSAGE:
413
- content = msg.content
414
- if self.encrypt and isinstance(content, str): content = self._decrypt_content(content)
453
+ content = self._unwrap_incoming(msg.content, "Message")
415
454
  msg.content = content
416
455
  if self._message_handler:
417
456
  if inspect.iscoroutinefunction(self._message_handler): await self._message_handler(msg)
@@ -448,11 +487,10 @@ class InterMeshAgent:
448
487
  task.update_status(TaskStatus.RUNNING)
449
488
  await self.ws.send(InterMeshMessage(type=MessageType.TASK_UPDATE, sender=self.qualified_name, content=task.to_dict(), token=self.token).to_json())
450
489
 
451
- decrypted_input = task.input_data
452
- if self.encrypt and isinstance(decrypted_input, str):
453
- decrypted_input = self._decrypt_content(decrypted_input)
454
-
455
490
  try:
491
+ # Dans le bloc : un refus de déchiffrement doit rapporter la tâche
492
+ # en échec avec sa raison, pas remonter jusqu'à la boucle d'écoute.
493
+ decrypted_input = self._unwrap_incoming(task.input_data, "Contenu de tâche")
456
494
  if self._task_handler:
457
495
  output = await self._task_handler(decrypted_input, task) if inspect.iscoroutinefunction(self._task_handler) else self._task_handler(decrypted_input, task)
458
496
  else:
@@ -13,17 +13,23 @@ from intermesh.crypto import generate_keypair, get_public_key_pem
13
13
  from intermesh.identity import AgentIdentity
14
14
 
15
15
 
16
+ # Le dessin vit dans une chaîne brute : ses antislashs sont des traits,
17
+ # pas des séquences d'échappement. Les laisser dans une f-string produisait
18
+ # un SyntaxWarning à chaque appel de la CLI — un avertissement affiché à
19
+ # chaque commande coûte plus cher que le dessin ne rapporte.
20
+ _LOGO = r""" _____ __________________ __ ______________ __ __
21
+ / _/ | / /_ __/ ____/ __ \/ |/ / ____/ ___// / / /
22
+ / // |/ / / / / __/ / /_/ / /|_/ / __/ \__ \/ /_/ /
23
+ _/ // /| / / / / /___/ _, _/ / / / /___ ___/ / __ /
24
+ /___/_/ |_/ /_/ /_____/_/ |_/_/ /_/_____//____/_/ /_/"""
25
+
26
+
16
27
  def print_banner():
17
28
  # La version vient du paquet : une chaîne en dur ici finit toujours
18
29
  # par mentir après quelques releases.
19
30
  from intermesh import __version__
20
- print(f"""
21
- \033[36m _ ___________ _______
22
- / | / / ____/ |/ _/ | INTERMESH PROTOCOL
23
- / |/ / __/ / /| |/ // /| | Universal Agent Infrastructure
24
- / /| / /___/ ___ / // ___ | CLI Developer Tool v{__version__}
25
- /_/ |_/_____/_/ |_/___/_/ |_|\033[0m
26
- """)
31
+ print(f"\n\033[36m{_LOGO}\033[0m\n")
32
+ print(f" Universal Agent Infrastructure · CLI v{__version__}\n")
27
33
 
28
34
 
29
35
  def run_security_check(args):
@@ -60,6 +60,31 @@ def encrypt_for(recipient_public_key_pem: str, plaintext: str) -> str:
60
60
  return base64.b64encode(json.dumps(envelope).encode()).decode()
61
61
 
62
62
 
63
+ ENVELOPE_KEYS = {"ek", "n", "ct"}
64
+
65
+
66
+ def looks_encrypted(value) -> bool:
67
+ """Reconnaît une charge produite par `encrypt_for`, sans la déchiffrer.
68
+
69
+ Sert à distinguer « cet agent reçoit du texte chiffré qu'il ne sait pas
70
+ ouvrir » de « cet agent reçoit une chaîne ordinaire ». Sans cette
71
+ distinction, un agent configuré sans chiffrement traitait le texte
72
+ chiffré comme des données : la tâche paraissait réussir et rendait un
73
+ résultat faux, ce qui est pire qu'un échec.
74
+
75
+ Le test est structurel — base64 d'un objet JSON portant exactement les
76
+ trois champs de l'enveloppe — et n'exige aucune clé. Une chaîne
77
+ ordinaire, même en base64, ne produit pas cette forme.
78
+ """
79
+ if not isinstance(value, str) or len(value) < 32:
80
+ return False
81
+ try:
82
+ envelope = json.loads(base64.b64decode(value, validate=True))
83
+ except Exception:
84
+ return False
85
+ return isinstance(envelope, dict) and set(envelope) == ENVELOPE_KEYS
86
+
87
+
63
88
  def decrypt_with(private_key, encrypted_b64: str) -> str:
64
89
  """Déchiffrement hybride RSA-OAEP + AES-256-GCM."""
65
90
  envelope = json.loads(base64.b64decode(encrypted_b64))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: intermesh
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Universal coordination protocol for AI agents — E2E encrypted, cross-language, federated
5
5
  Author: InterMesh Protocol Authors
6
6
  License: Apache-2.0
@@ -78,19 +78,26 @@ asyncio.run(main())
78
78
  **Delegate work from an orchestrator:**
79
79
 
80
80
  ```python
81
+ import asyncio
81
82
  from intermesh import InterMeshAgent
82
83
 
83
- orchestrator = InterMeshAgent(name="lead", roles=["admin"])
84
- await orchestrator.connect()
84
+ # `await` is not valid at module level in Python, so this lives in a
85
+ # coroutine. Copy-pasting it without the wrapper is a SyntaxError, not a
86
+ # runtime error — it fails before anything runs.
87
+ async def main():
88
+ orchestrator = InterMeshAgent(name="lead", roles=["admin"])
89
+ await orchestrator.connect()
90
+
91
+ # Find an agent by capability, then hand it a task
92
+ found = await orchestrator.discover(capabilities=["calculate"])
93
+ result = await orchestrator.submit_task(
94
+ title="Add two numbers",
95
+ assignee=found["agents"][0]["name"],
96
+ input_data={"a": 20, "b": 22},
97
+ )
98
+ print(result) # {"result": 42} — encrypted end-to-end in transit
85
99
 
86
- # Find an agent by capability, then hand it a task
87
- found = await orchestrator.discover(capabilities=["calculate"])
88
- result = await orchestrator.submit_task(
89
- title="Add two numbers",
90
- assignee=found["agents"][0]["name"],
91
- input_data={"a": 20, "b": 22},
92
- )
93
- # {"result": 42} — encrypted end-to-end in transit
100
+ asyncio.run(main())
94
101
  ```
95
102
 
96
103
  ---
@@ -106,7 +113,7 @@ A LangChain runnable:
106
113
  from intermesh import from_langchain
107
114
 
108
115
  agent = from_langchain(my_chain, name="analyst", capabilities=["market_analysis"])
109
- await agent.connect()
116
+ agent.run() # connects, then stays in service
110
117
  ```
111
118
 
112
119
  Anything else — a CrewAI crew, an AutoGen agent, a LlamaIndex engine, or a
@@ -122,7 +129,7 @@ async def run(data):
122
129
  return await asyncio.to_thread(lambda: my_crew.kickoff(inputs=data))
123
130
 
124
131
  agent = from_callable(run, name="research", capabilities=["research"])
125
- await agent.connect()
132
+ agent.run()
126
133
  ```
127
134
 
128
135
  Or as a decorator:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "intermesh"
7
- version = "0.4.1"
7
+ version = "0.4.2"
8
8
  description = "Universal coordination protocol for AI agents — E2E encrypted, cross-language, federated"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes