intermesh 0.3.0__py3-none-any.whl

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/cli.py ADDED
@@ -0,0 +1,453 @@
1
+ import argparse
2
+ import asyncio
3
+ import getpass
4
+ import http.server
5
+ import json
6
+ import os
7
+ import socketserver
8
+ import sys
9
+ import time
10
+
11
+ from intermesh import InterMeshAgent, InterMeshMessage, MessageType
12
+ from intermesh.crypto import generate_keypair, get_public_key_pem
13
+ from intermesh.identity import AgentIdentity
14
+
15
+
16
+ def print_banner():
17
+ # La version vient du paquet : une chaîne en dur ici finit toujours
18
+ # par mentir après quelques releases.
19
+ 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
+ """)
27
+
28
+
29
+ def run_security_check(args):
30
+ print("\033[33m🛡️ Exécution de l'audit de sécurité de votre installation locale...\033[0m\n")
31
+
32
+ score = 100
33
+ checks = []
34
+
35
+ # 1. Vérification du chiffrement E2E
36
+ checks.append(("Chiffrement E2E (RSA-2048 + AES-256-GCM)", True, "Activé par défaut dans le SDK"))
37
+
38
+ # 2. Vérification de l'isolation du Hub (127.0.0.1)
39
+ bind_ip = os.getenv("INTERMESH_BIND_IP", "127.0.0.1")
40
+ if bind_ip in ("127.0.0.1", "localhost"):
41
+ checks.append(("Isolation du Hub (Loopback 127.0.0.1)", True, "Le Hub est isolé sur votre machine locale"))
42
+ else:
43
+ checks.append(("Isolation du Hub", False, "Le Hub est exposé sur toutes les interfaces réseau (0.0.0.0)"))
44
+ score -= 20
45
+
46
+ # 3. Vérification du TLS / WSS
47
+ cert_path = os.getenv("INTERMESH_SSL_CERT")
48
+ if cert_path and os.path.exists(cert_path):
49
+ checks.append(("Transport Sécurisé TLS (wss://)", True, "Certificat SSL/TLS détecté"))
50
+ else:
51
+ checks.append(("Transport Sécurisé TLS (wss://)", False, "Attention: Utilisation de ws:// non chiffré sur le réseau local"))
52
+ score -= 10
53
+
54
+ # 4. Vérification des permissions du dossier .intermesh/
55
+ intermesh_dir = os.path.expanduser("~/.intermesh")
56
+ if os.path.exists(intermesh_dir):
57
+ mode = oct(os.stat(intermesh_dir).st_mode)[-3:]
58
+ if mode in ("700", "600"):
59
+ checks.append(("Permissions du dossier ~/.intermesh/", True, f"Permissions restrictives idéales ({mode})"))
60
+ else:
61
+ checks.append(("Permissions du dossier ~/.intermesh/", False, f"Permissions ouvertes ({mode}). Recommandé: 700"))
62
+ score -= 10
63
+ else:
64
+ checks.append(("Dossier de clés ~/.intermesh/", True, "Non créé pour l'instant"))
65
+
66
+ print(f"{'TEST DE SÉCURITÉ':<45} {'STATUT':<12} {'DÉTAILS'}")
67
+ print("-" * 75)
68
+ for title, status, detail in checks:
69
+ stat_str = "\033[32m[ PASSE ]\033[0m" if status else "\033[33m[ WARN ]\033[0m"
70
+ print(f"{title:<45} {stat_str:<12} {detail}")
71
+
72
+ print("\n" + "=" * 75)
73
+ grade = "A+" if score >= 90 else ("A" if score >= 80 else "B")
74
+ print(f" SCORE DE SÉCURITÉ LOCALE : \033[32m{score}/100 (GRADE: {grade})\033[0m")
75
+ print("=" * 75 + "\n")
76
+
77
+
78
+ def run_docs(args):
79
+ doc_type = args.topic
80
+ base_docs_path = os.path.expanduser("~/nexus/docs")
81
+ mapping = {
82
+ "rfc": os.path.join(base_docs_path, "RFC-001-CORE-PROTOCOL.md"),
83
+ "security": os.path.join(base_docs_path, "SECURITY-AND-ENCRYPTION.md"),
84
+ "api": os.path.join(base_docs_path, "API-REFERENCE.md"),
85
+ "adapters": os.path.join(base_docs_path, "ADAPTERS-AND-INTEGRATIONS.md"),
86
+ }
87
+ target_file = mapping.get(doc_type)
88
+ if target_file and os.path.exists(target_file):
89
+ with open(target_file, "r", encoding="utf-8") as f:
90
+ print(f"\033[33m--- DOCUMENTATION NEXUS : {doc_type.upper()} ---\033[0m\n")
91
+ print(f.read())
92
+ else:
93
+ print(f"\033[31mDocumentation introuvable pour '{doc_type}'.\033[0m")
94
+
95
+
96
+ async def run_discover(args):
97
+ agent = InterMeshAgent(name="cli_inspector", roles=["admin"], encrypt=False)
98
+ await agent.connect()
99
+ query = {}
100
+ if args.capability: query["capabilities"] = [args.capability]
101
+ if args.role: query["roles"] = [args.role]
102
+ print(f"\033[33m🔍 Recherche d'agents connectés...\033[0m")
103
+ result = await agent.discover(**query, timeout=4.0)
104
+ agents = result.get("agents", [])
105
+ print(f"\n\033[32m✓ {len(agents)} agent(s) trouvé(s) sur le réseau :\033[0m\n")
106
+ print(f"{'NOM':<20} {'RÔLES':<18} {'CAPACITÉS':<30} {'E2E'}")
107
+ print("-" * 75)
108
+ for a in agents:
109
+ caps = ", ".join(a.get("capabilities", [])) or "aucune"
110
+ roles = ", ".join(a.get("roles", [])) or "standard"
111
+ e2e = "🔒 Oui" if a.get("public_key") else "🔓 Non"
112
+ print(f"{a['name']:<20} {roles:<18} {caps:<30} {e2e}")
113
+ print()
114
+ await agent.ws.close()
115
+
116
+
117
+ def _parse_json_arg(raw: str, label: str):
118
+ try:
119
+ return json.loads(raw)
120
+ except json.JSONDecodeError as exc:
121
+ print(f"\033[31m✗ {label} n'est pas du JSON valide : {exc}\033[0m")
122
+ sys.exit(2)
123
+
124
+
125
+ async def run_ping(args):
126
+ """Vérifie qu'un agent est joignable et mesure le temps d'aller-retour."""
127
+ agent = InterMeshAgent(name="cli_ping", roles=["admin"], hub_url=args.hub, encrypt=False)
128
+ await agent.connect()
129
+ try:
130
+ started = time.monotonic()
131
+ # Un agent inconnu, ou d'une autre organisation, n'obtient aucune
132
+ # réponse du Hub : l'absence de réponse *est* le résultat.
133
+ try:
134
+ identity = await agent.who_is(args.agent, timeout=args.timeout)
135
+ except (TimeoutError, asyncio.TimeoutError):
136
+ identity = None
137
+ elapsed = (time.monotonic() - started) * 1000
138
+ if not identity:
139
+ print(f"\033[31m✗ '{args.agent}' est introuvable ou hors ligne.\033[0m")
140
+ await agent.close()
141
+ sys.exit(1)
142
+ caps = ", ".join(identity.get("capabilities", [])) or "aucune"
143
+ print(f"\n\033[32m✓ {args.agent} répond en {elapsed:.0f} ms\033[0m")
144
+ print(f" rôles : {', '.join(identity.get('roles', [])) or 'standard'}")
145
+ print(f" capacités : {caps}")
146
+ print(f" E2E : {'🔒 oui' if identity.get('public_key') else '🔓 non'}\n")
147
+ finally:
148
+ await agent.close()
149
+
150
+
151
+ async def run_ask(args):
152
+ """Pose une question à un agent et affiche sa réponse."""
153
+ content = _parse_json_arg(args.content, "Le contenu")
154
+ agent = InterMeshAgent(name="cli_asker", roles=["admin"], hub_url=args.hub)
155
+ await agent.connect()
156
+ try:
157
+ print(f"\033[33m💬 Question à '{args.agent}'...\033[0m")
158
+ reply = await agent.ask(to=args.agent, content=content, timeout=args.timeout)
159
+ print(f"\n\033[32m✓ Réponse :\033[0m\n{json.dumps(reply, indent=2, ensure_ascii=False)}\n")
160
+ finally:
161
+ await agent.close()
162
+
163
+
164
+ async def run_task(args):
165
+ """Délègue une tâche à un agent et attend son résultat."""
166
+ input_data = _parse_json_arg(args.input, "Les données d'entrée")
167
+ agent = InterMeshAgent(name="cli_orchestrator", roles=["admin"], hub_url=args.hub)
168
+ await agent.connect()
169
+ try:
170
+ print(f"\033[33m📝 Tâche '{args.title}' ➜ {args.assignee}...\033[0m")
171
+ result = await agent.submit_task(title=args.title, assignee=args.assignee,
172
+ input_data=input_data, timeout=args.timeout)
173
+ print(f"\n\033[32m✓ Résultat :\033[0m\n{json.dumps(result, indent=2, ensure_ascii=False)}\n")
174
+ finally:
175
+ await agent.close()
176
+
177
+
178
+ def run_keygen(args):
179
+ """Génère une paire de clés RSA pour un agent."""
180
+ private_key = generate_keypair()
181
+ public_pem = get_public_key_pem(private_key)
182
+
183
+ if not args.out:
184
+ print(f"\n\033[32m✓ Clé publique (RSA-2048) :\033[0m\n{public_pem}")
185
+ print("\033[33mℹ️ Sans --out, la clé privée n'est pas affichée ni conservée : "
186
+ "elle ne doit pas transiter par un terminal ou un historique.\033[0m\n")
187
+ return
188
+
189
+ from cryptography.hazmat.primitives import serialization
190
+
191
+ private_path = args.out
192
+ public_path = f"{args.out}.pub"
193
+ private_bytes = private_key.private_bytes(
194
+ encoding=serialization.Encoding.PEM,
195
+ format=serialization.PrivateFormat.PKCS8,
196
+ encryption_algorithm=serialization.NoEncryption(),
197
+ )
198
+ # 0600 dès la création : pas de fenêtre où la clé privée serait lisible.
199
+ fd = os.open(private_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
200
+ try:
201
+ os.write(fd, private_bytes)
202
+ finally:
203
+ os.close(fd)
204
+ with open(public_path, "w", encoding="utf-8") as handle:
205
+ handle.write(public_pem)
206
+
207
+ print(f"\n\033[32m✓ Clé privée : {private_path} (0600)\033[0m")
208
+ print(f"\033[32m✓ Clé publique : {public_path}\033[0m\n")
209
+
210
+
211
+ async def _admin_call(args, command: str, **params) -> dict:
212
+ """
213
+ Ouvre une session d'administration éphémère et exécute une commande.
214
+
215
+ L'administration exige une identité authentifiée par clé d'API : les
216
+ rôles déclarés à l'enregistrement ne suffisent pas (voir admin.py).
217
+ """
218
+ api_key = args.api_key or os.getenv("INTERMESH_API_KEY")
219
+ if not api_key:
220
+ print("\033[31m✗ Clé d'API requise : --api-key, ou la variable INTERMESH_API_KEY.\033[0m")
221
+ sys.exit(2)
222
+
223
+ agent = InterMeshAgent(name="cli_snapshot", api_key=api_key, encrypt=False)
224
+ await agent.connect()
225
+ try:
226
+ return await agent.admin(command, **params)
227
+ finally:
228
+ await agent.close()
229
+
230
+
231
+ def _snapshot_passphrase(args, *, confirm: bool) -> str | None:
232
+ """
233
+ Passphrase de chiffrement au repos, demandée interactivement.
234
+
235
+ Jamais lue depuis un argument de ligne de commande : `ps` et
236
+ l'historique du shell exposeraient un secret à toute la machine.
237
+ """
238
+ if not args.encrypt:
239
+ return None
240
+ first = getpass.getpass("Passphrase de l'instantané : ")
241
+ if not first:
242
+ print("\033[31m✗ Passphrase vide.\033[0m")
243
+ sys.exit(2)
244
+ if confirm and getpass.getpass("Confirmez la passphrase : ") != first:
245
+ print("\033[31m✗ Les passphrases ne correspondent pas.\033[0m")
246
+ sys.exit(2)
247
+ return first
248
+
249
+
250
+ async def run_snapshot(args):
251
+ action = args.action
252
+
253
+ if action == "list":
254
+ result = await _admin_call(args, "snapshot.list")
255
+ snapshots = result.get("snapshots", [])
256
+ print(f"\n\033[32m✓ {len(snapshots)} instantané(s) :\033[0m\n")
257
+ print(f"{'NOM':<28} {'DATE':<21} {'AGENTS':<8} {'TÂCHES':<8} {'CHIFFRÉ'}")
258
+ print("-" * 78)
259
+ for s in snapshots:
260
+ when = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(s.get("created_at", 0)))
261
+ counts = s.get("counts", {})
262
+ lock = "🔒 oui" if s.get("encrypted") else "🔓 non"
263
+ print(f"{s.get('name', '?'):<28} {when:<21} "
264
+ f"{counts.get('identities', 0):<8} {counts.get('tasks', 0):<8} {lock}")
265
+ print()
266
+ return
267
+
268
+ if action == "create":
269
+ passphrase = _snapshot_passphrase(args, confirm=True)
270
+ result = await _admin_call(args, "snapshot.create", name=args.name, passphrase=passphrase)
271
+ counts = result.get("counts", {})
272
+ print(f"\n\033[32m✓ Instantané '{result['name']}' créé.\033[0m")
273
+ print(f" {counts.get('identities', 0)} identité(s), {counts.get('tasks', 0)} tâche(s), "
274
+ f"{counts.get('api_keys', 0)} clé(s), {counts.get('escrow_holds', 0)} séquestre(s)")
275
+ if result.get("warning"):
276
+ print(f"\033[33m ⚠️ {result['warning']}\033[0m")
277
+ print()
278
+ return
279
+
280
+ if action == "restore":
281
+ passphrase = _snapshot_passphrase(args, confirm=False)
282
+ result = await _admin_call(args, "snapshot.restore", name=args.name, passphrase=passphrase)
283
+ print(f"\n\033[32m✓ État restauré depuis '{result['restored_from']}'.\033[0m")
284
+ print(f" {result.get('identities', 0)} identité(s), {result.get('tasks', 0)} tâche(s)")
285
+ print(f" Restauré : {', '.join(result.get('restored', [])) or 'rien'}")
286
+ if result.get("safety_snapshot"):
287
+ print(f" Filet de sécurité pris avant restauration : "
288
+ f"\033[36m{result['safety_snapshot']}\033[0m")
289
+ for skipped in result.get("skipped", []):
290
+ print(f"\033[33m ⚠️ Ignoré ({skipped['what']}) : {skipped['reason']}\033[0m")
291
+ if result.get("orphaned_online"):
292
+ print(f"\033[33m ⚠️ Connectés mais absents de l'instantané : "
293
+ f"{', '.join(result['orphaned_online'])}\033[0m")
294
+ print("\033[36m ℹ️ Le journal d'audit n'a pas été remplacé : la restauration "
295
+ "y est enregistrée.\033[0m\n")
296
+ return
297
+
298
+ if action == "delete":
299
+ result = await _admin_call(args, "snapshot.delete", name=args.name)
300
+ print(f"\n\033[32m✓ Instantané '{result['deleted']}' supprimé.\033[0m\n")
301
+
302
+
303
+ def run_dashboard(args):
304
+ port = args.port
305
+ dashboard_dir = os.path.expanduser("~/nexus/dashboard")
306
+ class Handler(http.server.SimpleHTTPRequestHandler):
307
+ def __init__(self, *args_h, **kwargs_h):
308
+ super().__init__(*args_h, directory=dashboard_dir, **kwargs_h)
309
+ def log_message(self, format, *log_args): pass
310
+
311
+ print(f"\033[32m✓ InterMesh Mission Control Dashboard actif sur http://localhost:{port}\033[0m")
312
+ with socketserver.TCPServer(("", port), Handler) as httpd:
313
+ try: httpd.serve_forever()
314
+ except KeyboardInterrupt: pass
315
+
316
+
317
+ def run_serve(args):
318
+ """Expose un programme externe comme agent, sans écrire une ligne de code."""
319
+ from intermesh.bridge import from_command, from_http
320
+
321
+ common = dict(
322
+ name=args.name,
323
+ capabilities=args.capability or ["compute"],
324
+ org_id=args.org,
325
+ hub_url=args.hub,
326
+ encrypt=not args.no_encrypt,
327
+ timeout=args.timeout,
328
+ )
329
+ if args.exec_command:
330
+ agent = from_command(args.exec_command, **common)
331
+ source = f"exec « {args.exec_command} »"
332
+ else:
333
+ agent = from_http(args.http_url, **common)
334
+ source = f"http {args.http_url}"
335
+
336
+ print(f"\033[32m✓ Agent '{args.name}' ({source}) → {args.hub}\033[0m")
337
+ print(f" capacités : {', '.join(common['capabilities'])}")
338
+ print(" Ctrl+C pour arrêter.\n")
339
+ agent.run()
340
+
341
+
342
+ def main():
343
+ print_banner()
344
+
345
+ # `hub` est interceptée avant argparse : le Hub a sa propre douzaine
346
+ # d'options (TLS, pairage, egress…) et les redéclarer ici garantirait
347
+ # qu'elles divergent tôt ou tard.
348
+ if len(sys.argv) > 1 and sys.argv[1] == "hub":
349
+ from intermesh.hub import main as hub_main
350
+ try:
351
+ asyncio.run(hub_main(sys.argv[2:]))
352
+ except KeyboardInterrupt:
353
+ print("\nHub arrêté.")
354
+ return
355
+
356
+ parser = argparse.ArgumentParser(description="InterMesh Protocol Developer CLI")
357
+ subparsers = parser.add_subparsers(dest="command", help="Commandes disponibles")
358
+
359
+ # Command: hub (traitée plus haut ; déclarée pour l'aide)
360
+ subparsers.add_parser("hub", add_help=False,
361
+ help="Démarrer un Hub (voir `intermesh hub --help`)")
362
+
363
+ # Command: security-check
364
+ subparsers.add_parser("security-check", help="Auditer la sécurité de votre installation locale")
365
+
366
+ # Command: docs
367
+ doc_parser = subparsers.add_parser("docs", help="Afficher documentation")
368
+ doc_parser.add_argument("topic", choices=["rfc", "security", "api", "adapters"], help="Thème")
369
+
370
+ # Command: discover
371
+ disc_parser = subparsers.add_parser("discover", help="Rechercher des agents")
372
+ disc_parser.add_argument("--capability", "-c", type=str, help="Capacité")
373
+ disc_parser.add_argument("--role", "-r", type=str, help="Rôle")
374
+
375
+ # Command: snapshot
376
+ snap_parser = subparsers.add_parser("snapshot", help="Sauvegarder / restaurer l'état du Hub")
377
+ snap_parser.add_argument("action", choices=["create", "list", "restore", "delete"])
378
+ snap_parser.add_argument("--name", "-n", type=str, help="Nom de l'instantané")
379
+ snap_parser.add_argument("--api-key", type=str, default=None,
380
+ help="Clé d'API admin (ou variable INTERMESH_API_KEY)")
381
+ snap_parser.add_argument("--encrypt", action="store_true",
382
+ help="Chiffrer/déchiffrer l'instantané (passphrase demandée)")
383
+
384
+ # Command: dashboard
385
+ dash_parser = subparsers.add_parser("dashboard", help="Lancer Dashboard")
386
+ dash_parser.add_argument("--port", type=int, default=8080, help="Port Web")
387
+
388
+ # Command: serve — intégration en une ligne, quel que soit le langage
389
+ serve_parser = subparsers.add_parser(
390
+ "serve", help="Exposer un programme existant comme agent InterMesh")
391
+ serve_parser.add_argument("--name", "-n", type=str, required=True, help="Nom de l'agent")
392
+ source = serve_parser.add_mutually_exclusive_group(required=True)
393
+ source.add_argument("--exec", dest="exec_command", type=str,
394
+ help="Commande à lancer par tâche (JSON sur stdin, JSON sur stdout)")
395
+ source.add_argument("--http", dest="http_url", type=str,
396
+ help="URL recevant la tâche en POST JSON")
397
+ serve_parser.add_argument("--capability", "-c", action="append", default=[],
398
+ help="Capacité annoncée (répétable)")
399
+ serve_parser.add_argument("--org", type=str, default="default", help="Organisation")
400
+ serve_parser.add_argument("--hub", type=str, default="ws://localhost:8765", help="URL du Hub")
401
+ serve_parser.add_argument("--timeout", type=float, default=30.0,
402
+ help="Délai maximal accordé au programme externe")
403
+ serve_parser.add_argument("--no-encrypt", action="store_true",
404
+ help="Désactive le chiffrement E2E (débogage)")
405
+
406
+ # Command: ping
407
+ ping_parser = subparsers.add_parser("ping", help="Vérifier qu'un agent répond")
408
+ ping_parser.add_argument("agent", type=str, help="Nom de l'agent")
409
+ ping_parser.add_argument("--hub", type=str, default="ws://localhost:8765")
410
+ ping_parser.add_argument("--timeout", type=float, default=5.0)
411
+
412
+ # Command: ask
413
+ ask_parser = subparsers.add_parser("ask", help="Poser une question à un agent")
414
+ ask_parser.add_argument("agent", type=str, help="Nom de l'agent")
415
+ ask_parser.add_argument("content", type=str, help="Contenu JSON de la question")
416
+ ask_parser.add_argument("--hub", type=str, default="ws://localhost:8765")
417
+ ask_parser.add_argument("--timeout", type=float, default=10.0)
418
+
419
+ # Command: task
420
+ task_parser = subparsers.add_parser("task", help="Déléguer une tâche à un agent")
421
+ task_parser.add_argument("assignee", type=str, help="Agent exécutant")
422
+ task_parser.add_argument("title", type=str, help="Intitulé de la tâche")
423
+ task_parser.add_argument("input", type=str, help="Données d'entrée en JSON")
424
+ task_parser.add_argument("--hub", type=str, default="ws://localhost:8765")
425
+ task_parser.add_argument("--timeout", type=float, default=15.0)
426
+
427
+ # Command: keygen
428
+ key_parser = subparsers.add_parser("keygen", help="Générer une paire de clés RSA")
429
+ key_parser.add_argument("--out", "-o", type=str, default=None,
430
+ help="Chemin du fichier de clé privée (la publique reçoit .pub)")
431
+
432
+ args = parser.parse_args()
433
+ if not args.command:
434
+ parser.print_help()
435
+ sys.exit(1)
436
+
437
+ if args.command == "snapshot" and args.action != "list" and not args.name:
438
+ parser.error("--name est requis pour 'snapshot " + args.action + "'.")
439
+
440
+ if args.command == "security-check": run_security_check(args)
441
+ elif args.command == "docs": run_docs(args)
442
+ elif args.command == "discover": asyncio.run(run_discover(args))
443
+ elif args.command == "snapshot": asyncio.run(run_snapshot(args))
444
+ elif args.command == "dashboard": run_dashboard(args)
445
+ elif args.command == "serve": run_serve(args)
446
+ elif args.command == "ping": asyncio.run(run_ping(args))
447
+ elif args.command == "ask": asyncio.run(run_ask(args))
448
+ elif args.command == "task": asyncio.run(run_task(args))
449
+ elif args.command == "keygen": run_keygen(args)
450
+
451
+
452
+ if __name__ == "__main__":
453
+ main()
intermesh/config.py ADDED
@@ -0,0 +1,145 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ def load_env_file(dotenv_path: Optional[Path] = None) -> None:
7
+ """Charge un fichier .env de maniere robuste et sans dependances externes."""
8
+ if dotenv_path is None:
9
+ intermesh_home = os.environ.get("INTERMESH_HOME")
10
+ if intermesh_home:
11
+ dotenv_path = Path(intermesh_home) / ".env"
12
+ else:
13
+ dotenv_path = Path.cwd() / ".env"
14
+ if not dotenv_path.is_file():
15
+ dotenv_path = Path(__file__).resolve().parents[2] / ".env"
16
+
17
+ if dotenv_path.is_file():
18
+ try:
19
+ with open(dotenv_path, "r", encoding="utf-8") as f:
20
+ for line in f:
21
+ line = line.strip()
22
+ if not line or line.startswith("#"):
23
+ continue
24
+ if "=" in line:
25
+ key, val = line.split("=", 1)
26
+ key = key.strip()
27
+ val = val.strip()
28
+ if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
29
+ val = val[1:-1]
30
+ if key not in os.environ:
31
+ os.environ[key] = val
32
+ except Exception as e:
33
+ print(f"Erreur lors du chargement de {dotenv_path} : {e}", file=sys.stderr)
34
+
35
+ load_env_file()
36
+
37
+ class Settings:
38
+ """Configuration unifiee et typee pour le Hub et les SDK InterMesh."""
39
+
40
+ @property
41
+ def intermesh_home(self) -> Path:
42
+ base = os.environ.get("INTERMESH_HOME")
43
+ return Path(base) if base else Path.home() / ".intermesh"
44
+
45
+ @property
46
+ def hub_host(self) -> str:
47
+ return os.environ.get("INTERMESH_HUB_HOST", "0.0.0.0")
48
+
49
+ @property
50
+ def hub_port(self) -> int:
51
+ try:
52
+ return int(os.environ.get("INTERMESH_HUB_PORT", "8765"))
53
+ except ValueError:
54
+ return 8765
55
+
56
+ @property
57
+ def default_org(self) -> str:
58
+ return os.environ.get("INTERMESH_DEFAULT_ORG", "default")
59
+
60
+ @property
61
+ def token_expiry(self) -> int:
62
+ try:
63
+ return int(os.environ.get("INTERMESH_TOKEN_EXPIRY", "3600"))
64
+ except ValueError:
65
+ return 3600
66
+
67
+ @property
68
+ def default_rate_limit(self) -> float:
69
+ try:
70
+ return float(os.environ.get("INTERMESH_RATE_LIMIT", "15.0"))
71
+ except ValueError:
72
+ return 15.0
73
+
74
+ @property
75
+ def default_rate_burst(self) -> float:
76
+ try:
77
+ return float(os.environ.get("INTERMESH_RATE_BURST", "20.0"))
78
+ except ValueError:
79
+ return 20.0
80
+
81
+ @property
82
+ def default_hub_url(self) -> str:
83
+ return os.environ.get("INTERMESH_HUB_URL", "ws://localhost:8765")
84
+
85
+ @property
86
+ def default_timeout_who_is(self) -> float:
87
+ try:
88
+ return float(os.environ.get("INTERMESH_TIMEOUT_WHO_IS", "5.0"))
89
+ except ValueError:
90
+ return 5.0
91
+
92
+ @property
93
+ def default_timeout_ask(self) -> float:
94
+ try:
95
+ return float(os.environ.get("INTERMESH_TIMEOUT_ASK", "10.0"))
96
+ except ValueError:
97
+ return 10.0
98
+
99
+ @property
100
+ def default_timeout_submit_task(self) -> float:
101
+ try:
102
+ return float(os.environ.get("INTERMESH_TIMEOUT_SUBMIT_TASK", "15.0"))
103
+ except ValueError:
104
+ return 15.0
105
+
106
+ @property
107
+ def default_timeout_discover(self) -> float:
108
+ try:
109
+ return float(os.environ.get("INTERMESH_TIMEOUT_DISCOVER", "5.0"))
110
+ except ValueError:
111
+ return 5.0
112
+
113
+ @property
114
+ def default_timeout_admin(self) -> float:
115
+ try:
116
+ return float(os.environ.get("INTERMESH_TIMEOUT_ADMIN", "10.0"))
117
+ except ValueError:
118
+ return 10.0
119
+
120
+ @property
121
+ def default_timeout_key_fetch(self) -> float:
122
+ try:
123
+ return float(os.environ.get("INTERMESH_TIMEOUT_KEY_FETCH", "3.0"))
124
+ except ValueError:
125
+ return 3.0
126
+
127
+ @property
128
+ def default_encrypt(self) -> bool:
129
+ val = os.environ.get("INTERMESH_ENCRYPT", "true").lower()
130
+ return val in ("true", "1", "yes", "on")
131
+
132
+ @property
133
+ def log_level(self) -> str:
134
+ return os.environ.get("INTERMESH_LOG_LEVEL", "INFO").upper()
135
+
136
+ @property
137
+ def log_format(self) -> str:
138
+ return os.environ.get("INTERMESH_LOG_FORMAT", "text").lower()
139
+
140
+ @property
141
+ def allow_cross_tenant(self) -> bool:
142
+ val = os.environ.get("INTERMESH_ALLOW_CROSS_TENANT", "false").lower()
143
+ return val in ("true", "1", "yes", "on")
144
+
145
+ settings = Settings()