intermesh 0.4.1__tar.gz → 0.4.3__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.
- {intermesh-0.4.1 → intermesh-0.4.3}/PKG-INFO +20 -13
- {intermesh-0.4.1 → intermesh-0.4.3}/README.md +19 -12
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/__init__.py +2 -2
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/agent.py +46 -8
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/cli.py +40 -10
- intermesh-0.4.3/intermesh/console/__init__.py +0 -0
- intermesh-0.4.3/intermesh/console/app.js +601 -0
- intermesh-0.4.3/intermesh/console/index.html +427 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/crypto.py +25 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/store.py +7 -1
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh.egg-info/PKG-INFO +20 -13
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh.egg-info/SOURCES.txt +4 -1
- {intermesh-0.4.1 → intermesh-0.4.3}/pyproject.toml +8 -1
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/adapters.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/admin.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/apikeys.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/approval.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/audit.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/bridge.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/config.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/egress.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/escrow.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/guardrails.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/hardware.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/health.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/hub.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/identity.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/logger.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/message.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/metrics.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/peering.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/pipeline.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/policy.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/ratelimit.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/schema.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/secret.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/signing.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/snapshot.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh/task.py +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh.egg-info/dependency_links.txt +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh.egg-info/entry_points.txt +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh.egg-info/requires.txt +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/intermesh.egg-info/top_level.txt +0 -0
- {intermesh-0.4.1 → intermesh-0.4.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: intermesh
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
56
|
+
__version__ = "0.4.3"
|
|
@@ -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,
|
|
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
|
-
|
|
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):
|
|
@@ -301,17 +307,41 @@ async def run_snapshot(args):
|
|
|
301
307
|
|
|
302
308
|
|
|
303
309
|
def run_dashboard(args):
|
|
310
|
+
"""Sert la console d'exploitation, celle qui voyage avec le paquet.
|
|
311
|
+
|
|
312
|
+
Le chemin était codé en dur sur `~/nexus/dashboard`, l'arborescence du
|
|
313
|
+
développeur — et les fichiers n'étaient même pas dans le paquet. Chez
|
|
314
|
+
tout autre utilisateur la commande annonçait « actif » puis répondait
|
|
315
|
+
404 sur tout. Elle sert désormais le dossier livré avec `intermesh`,
|
|
316
|
+
situé par le module lui-même.
|
|
317
|
+
"""
|
|
304
318
|
port = args.port
|
|
305
|
-
|
|
319
|
+
console_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "console")
|
|
320
|
+
|
|
321
|
+
if not os.path.isfile(os.path.join(console_dir, "index.html")):
|
|
322
|
+
print("\033[31m✗ Console introuvable dans le paquet installé "
|
|
323
|
+
f"({console_dir}).\033[0m")
|
|
324
|
+
print(" Réinstallez avec : pip install --force-reinstall intermesh")
|
|
325
|
+
return 1
|
|
326
|
+
|
|
306
327
|
class Handler(http.server.SimpleHTTPRequestHandler):
|
|
307
328
|
def __init__(self, *args_h, **kwargs_h):
|
|
308
|
-
super().__init__(*args_h, directory=
|
|
329
|
+
super().__init__(*args_h, directory=console_dir, **kwargs_h)
|
|
309
330
|
def log_message(self, format, *log_args): pass
|
|
310
331
|
|
|
311
|
-
print(f"\033[32m✓
|
|
332
|
+
print(f"\033[32m✓ Console d'exploitation sur http://localhost:{port}\033[0m")
|
|
333
|
+
# Dit ce que c'est, et ce que ce n'est pas : le premier utilisateur a
|
|
334
|
+
# cru voir « le mauvais dashboard » alors qu'il y en a deux, distincts
|
|
335
|
+
# et voulus.
|
|
336
|
+
print(" Interface locale, sans dépendance externe : elle se branche sur")
|
|
337
|
+
print(" le Hub que vous indiquez et n'a besoin d'aucun compte.")
|
|
338
|
+
print(" Le Control Plane hébergé (React, comptes et équipes) est un autre")
|
|
339
|
+
print(" produit, sur https://intermesh.site — il exige Supabase et ne")
|
|
340
|
+
print(" fonctionne pas hors ligne.\n")
|
|
312
341
|
with socketserver.TCPServer(("", port), Handler) as httpd:
|
|
313
342
|
try: httpd.serve_forever()
|
|
314
343
|
except KeyboardInterrupt: pass
|
|
344
|
+
return 0
|
|
315
345
|
|
|
316
346
|
|
|
317
347
|
def run_serve(args):
|
|
File without changes
|