intermesh 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.
Files changed (100) hide show
  1. {intermesh-0.4.3 → intermesh-0.4.5}/PKG-INFO +1 -1
  2. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/__init__.py +1 -1
  3. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/admin.py +28 -0
  4. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/agent.py +20 -2
  5. intermesh-0.4.5/intermesh/backpressure.py +238 -0
  6. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/cli.py +20 -3
  7. intermesh-0.4.5/intermesh/console/404.html +16 -0
  8. intermesh-0.4.5/intermesh/console/_next/static/8A7tou8DfWGFVRtP-NDfC/_buildManifest.js +1 -0
  9. intermesh-0.4.5/intermesh/console/_next/static/8A7tou8DfWGFVRtP-NDfC/_ssgManifest.js +1 -0
  10. intermesh-0.4.5/intermesh/console/_next/static/chunks/117-279de47ad86d55f2.js +2 -0
  11. intermesh-0.4.5/intermesh/console/_next/static/chunks/283-7828913da424ef26.js +46 -0
  12. intermesh-0.4.5/intermesh/console/_next/static/chunks/44530001-792177c4c52bbd93.js +1 -0
  13. intermesh-0.4.5/intermesh/console/_next/static/chunks/451-d950d00746903c71.js +1 -0
  14. intermesh-0.4.5/intermesh/console/_next/static/chunks/67.55c167dcccf37409.js +1 -0
  15. intermesh-0.4.5/intermesh/console/_next/static/chunks/845-1b26226a3e7e86ee.js +1 -0
  16. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/agents/page-e2b539ca4d8e901f.js +1 -0
  17. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/billing/page-ad7e25cb466a1d75.js +1 -0
  18. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/dashboard/page-ef7c7b6a0300c304.js +1 -0
  19. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/keys/page-38e3537a90794291.js +1 -0
  20. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/layout-5acef3b51b22acab.js +1 -0
  21. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/resumes/page-b92a0a12233cda42.js +1 -0
  22. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/security/page-2b0ec08aad65bca2.js +1 -0
  23. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/settings/page-ecd261e5d7fcb349.js +1 -0
  24. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/topology/page-faa778b14d5de62f.js +1 -0
  25. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/_not-found/page-6d4614157fff2d5f.js +1 -0
  26. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/docs/page-9b599783e780a1ca.js +1 -0
  27. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/layout-904d511bcfb3dc49.js +1 -0
  28. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/page-a7b8793e2971aba6.js +1 -0
  29. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/pricing/page-0cf2650bbac40063.js +1 -0
  30. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/privacy/page-108901b94493a585.js +1 -0
  31. intermesh-0.4.5/intermesh/console/_next/static/chunks/app/terms/page-9a0f507db0aa3753.js +1 -0
  32. intermesh-0.4.5/intermesh/console/_next/static/chunks/fd9d1056-d17313c03983f67e.js +1 -0
  33. intermesh-0.4.5/intermesh/console/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
  34. intermesh-0.4.5/intermesh/console/_next/static/chunks/main-3dfb547d0081738b.js +1 -0
  35. intermesh-0.4.5/intermesh/console/_next/static/chunks/main-app-bcf1b654d275aa86.js +1 -0
  36. intermesh-0.4.5/intermesh/console/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
  37. intermesh-0.4.5/intermesh/console/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
  38. intermesh-0.4.5/intermesh/console/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  39. intermesh-0.4.5/intermesh/console/_next/static/chunks/webpack-881ca5d5b20426f9.js +1 -0
  40. intermesh-0.4.5/intermesh/console/_next/static/css/d3ada213244fbd00.css +3 -0
  41. intermesh-0.4.5/intermesh/console/agents.html +16 -0
  42. intermesh-0.4.5/intermesh/console/agents.txt +13 -0
  43. intermesh-0.4.5/intermesh/console/billing.html +16 -0
  44. intermesh-0.4.5/intermesh/console/billing.txt +13 -0
  45. intermesh-0.4.5/intermesh/console/dashboard.html +16 -0
  46. intermesh-0.4.5/intermesh/console/dashboard.txt +13 -0
  47. intermesh-0.4.5/intermesh/console/icon.png +0 -0
  48. intermesh-0.4.5/intermesh/console/index.html +16 -0
  49. intermesh-0.4.5/intermesh/console/intermesh-logo.png +0 -0
  50. intermesh-0.4.5/intermesh/console/keys.html +16 -0
  51. intermesh-0.4.5/intermesh/console/keys.txt +13 -0
  52. intermesh-0.4.5/intermesh/console/opengraph-image +0 -0
  53. intermesh-0.4.5/intermesh/console/resumes.html +16 -0
  54. intermesh-0.4.5/intermesh/console/resumes.txt +13 -0
  55. intermesh-0.4.5/intermesh/console/security.html +16 -0
  56. intermesh-0.4.5/intermesh/console/security.txt +13 -0
  57. intermesh-0.4.5/intermesh/console/settings.html +16 -0
  58. intermesh-0.4.5/intermesh/console/settings.txt +13 -0
  59. intermesh-0.4.5/intermesh/console/topology.html +16 -0
  60. intermesh-0.4.5/intermesh/console/topology.txt +13 -0
  61. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/hub.py +105 -0
  62. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh.egg-info/PKG-INFO +1 -1
  63. intermesh-0.4.5/intermesh.egg-info/SOURCES.txt +95 -0
  64. {intermesh-0.4.3 → intermesh-0.4.5}/pyproject.toml +5 -2
  65. intermesh-0.4.3/intermesh/console/app.js +0 -601
  66. intermesh-0.4.3/intermesh/console/index.html +0 -427
  67. intermesh-0.4.3/intermesh.egg-info/SOURCES.txt +0 -42
  68. {intermesh-0.4.3 → intermesh-0.4.5}/README.md +0 -0
  69. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/adapters.py +0 -0
  70. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/apikeys.py +0 -0
  71. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/approval.py +0 -0
  72. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/audit.py +0 -0
  73. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/bridge.py +0 -0
  74. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/config.py +0 -0
  75. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/console/__init__.py +0 -0
  76. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/crypto.py +0 -0
  77. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/egress.py +0 -0
  78. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/escrow.py +0 -0
  79. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/guardrails.py +0 -0
  80. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/hardware.py +0 -0
  81. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/health.py +0 -0
  82. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/identity.py +0 -0
  83. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/logger.py +0 -0
  84. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/message.py +0 -0
  85. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/metrics.py +0 -0
  86. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/peering.py +0 -0
  87. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/pipeline.py +0 -0
  88. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/policy.py +0 -0
  89. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/ratelimit.py +0 -0
  90. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/schema.py +0 -0
  91. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/secret.py +0 -0
  92. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/signing.py +0 -0
  93. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/snapshot.py +0 -0
  94. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/store.py +0 -0
  95. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/task.py +0 -0
  96. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh.egg-info/dependency_links.txt +0 -0
  97. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh.egg-info/entry_points.txt +0 -0
  98. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh.egg-info/requires.txt +0 -0
  99. {intermesh-0.4.3 → intermesh-0.4.5}/intermesh.egg-info/top_level.txt +0 -0
  100. {intermesh-0.4.3 → intermesh-0.4.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: intermesh
3
- Version: 0.4.3
3
+ Version: 0.4.5
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
@@ -53,4 +53,4 @@ try:
53
53
  except ImportError:
54
54
  pass
55
55
 
56
- __version__ = "0.4.3"
56
+ __version__ = "0.4.5"
@@ -97,6 +97,7 @@ class AdminContext:
97
97
  snapshot_dir: Any = None,
98
98
  pending_approvals: Any = None,
99
99
  finalize_task: Callable | None = None,
100
+ backpressure: Any = None,
100
101
  ):
101
102
  # Tâches retenues en attente d'arbitrage humain, et le moyen de les
102
103
  # relâcher. `finalize_task` est le même chemin que celui d'une
@@ -116,6 +117,10 @@ class AdminContext:
116
117
  self.my_org = my_org
117
118
  self.remember_task = remember_task
118
119
  self.send_to_agent = send_to_agent
120
+ # Portail de contre-pression, pour exposer la saturation : sans ce
121
+ # chiffre, un exploitant ne voit la saturation qu'au moment où les
122
+ # clients tombent en délai d'attente.
123
+ self.backpressure = backpressure
119
124
  self.hub_version = hub_version
120
125
  # Organisation du porteur du token, et `True` si son rôle est
121
126
  # `org_admin` — auquel cas chaque commande doit filtrer sa vue et
@@ -216,10 +221,33 @@ def _hub_info(ctx: AdminContext, params: dict) -> dict:
216
221
  "api_keys": api_keys_count,
217
222
  "api_keys_source": ctx.api_keys.source,
218
223
  "api_keys_mutable": ctx.api_keys.mutable,
224
+ "load": _load_snapshot(ctx),
219
225
  "server_time": time.time(),
220
226
  }
221
227
 
222
228
 
229
+ def _load_snapshot(ctx: AdminContext) -> dict:
230
+ """Charge et saturation, ou `None` hors contre-pression.
231
+
232
+ Rendu dans `hub.info` plutôt que dans une commande à part : c'est la
233
+ première chose qu'un exploitant consulte, et l'y cacher derrière un
234
+ second appel revient à ne pas l'exposer.
235
+ """
236
+ gate = getattr(ctx, "backpressure", None)
237
+ if gate is None:
238
+ return {"enabled": False, "note": "contre-pression non configurée"}
239
+
240
+ in_flight = 0
241
+ queued = 0
242
+ for task in ctx.task_registry.values():
243
+ if task.status.value in ("completed", "failed"):
244
+ continue
245
+ in_flight += 1
246
+ if task.status.value == "pending":
247
+ queued += 1
248
+ return gate.snapshot(in_flight, queued)
249
+
250
+
223
251
  def _agents_list(ctx: AdminContext, params: dict) -> dict:
224
252
  """
225
253
  Inventaire complet : les agents connectés, mais aussi ceux connus et
@@ -465,9 +465,27 @@ class InterMeshAgent:
465
465
  future.set_exception(PermissionError(msg.content))
466
466
  else:
467
467
  future.set_exception(RuntimeError(msg.content))
468
- for tid, fut in list(self._pending_tasks.items()):
469
- if not fut.done():
468
+ # Un refus qui nomme sa tâche ne concerne qu'elle. Faire
469
+ # échouer toutes les tâches en vol sur un refus ciblé
470
+ # était sans conséquence tant que les erreurs étaient
471
+ # rares ; sous contre-pression elles sont fréquentes, et
472
+ # un seul refus aurait anéanti tout le travail légitime
473
+ # du même orchestrateur.
474
+ named = (msg.content.get("task_id")
475
+ if isinstance(msg.content, dict) else None)
476
+ if named:
477
+ fut = self._pending_tasks.pop(named, None)
478
+ if fut is not None and not fut.done():
470
479
  fut.set_exception(RuntimeError(msg.content))
480
+ else:
481
+ # Erreur qui ne nomme aucune tâche : escrow refusé,
482
+ # garde-fou, rupture de lien. On ne peut pas savoir
483
+ # laquelle est visée, donc toutes tombent — c'est le
484
+ # comportement historique, et le restreindre ici
485
+ # laissait ces refus sans effet.
486
+ for tid, fut in list(self._pending_tasks.items()):
487
+ if not fut.done():
488
+ fut.set_exception(RuntimeError(msg.content))
471
489
  self._pending_tasks.pop(tid)
472
490
 
473
491
  except websockets.ConnectionClosed:
@@ -0,0 +1,238 @@
1
+ """
2
+ Contre-pression : dire non quand le Hub est saturé, plutôt que d'accepter.
3
+
4
+ Le garde-fou existant plafonne les soumissions **par agent** (60/minute).
5
+ Il protège d'un agent devenu fou, pas de la saturation : cent agents
6
+ sages, à un tiers de leur quota chacun, dépassent largement ce qu'un Hub
7
+ soutient.
8
+
9
+ Ce que le banc de mesure a montré, et qu'il ne faut pas contredire sans
10
+ nouvelle mesure :
11
+
12
+ * plafond observé sur un Hub : environ 11 tâches/s, indépendant du
13
+ nombre d'agents (78 comme 234) — c'est le Hub qui borne, pas la flotte ;
14
+ * zone saine : jusqu'à 8 tâches/s avec chiffrement, p50 sous la seconde ;
15
+ * au-delà, le Hub acceptait tout, mettait en file, la latence explosait,
16
+ et les clients mouraient sur un délai de ping WebSocket. Le processus
17
+ survivait, mais **aucun signal ne disait que le Hub était saturé**.
18
+
19
+ Un refus explicite vaut mieux qu'une acceptation qui n'aboutira pas :
20
+ l'émetteur peut réessayer plus tard, réduire sa cadence, ou basculer
21
+ ailleurs. Une file qui gonfle en silence ne lui laisse que le délai
22
+ d'attente, c'est-à-dire l'ignorance.
23
+
24
+ Ce module ne connaît rien du Hub : il reçoit les compteurs et rend une
25
+ décision. C'est ce qui le rend vérifiable sans réseau.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import os
31
+ import time
32
+ from dataclasses import dataclass, field
33
+
34
+
35
+ def _env_int(name: str, default: int) -> int:
36
+ raw = os.environ.get(name)
37
+ if raw is None or not str(raw).strip():
38
+ return default
39
+ try:
40
+ value = int(str(raw).strip())
41
+ except ValueError:
42
+ return default
43
+ return value if value >= 0 else default
44
+
45
+
46
+ def _env_bool(name: str, default: bool) -> bool:
47
+ raw = os.environ.get(name)
48
+ if raw is None:
49
+ return default
50
+ return str(raw).strip().lower() not in ("0", "false", "no", "off", "")
51
+
52
+
53
+ @dataclass
54
+ class BackpressureLimits:
55
+ """Les trois plafonds, et l'interrupteur.
56
+
57
+ Les défauts viennent de la mesure, pas d'une intuition : 10 tâches/s
58
+ est le plafond observé arrondi, et laisse la zone saine (8/s) intacte.
59
+ Un déploiement qui mesure autre chose sur son propre matériel doit les
60
+ relever — d'où des réglages, et non des constantes.
61
+ """
62
+
63
+ max_tasks_per_sec: int = 10
64
+ max_in_flight: int = 200
65
+ max_queue_depth: int = 100
66
+ enabled: bool = True
67
+
68
+ @classmethod
69
+ def from_env(cls) -> "BackpressureLimits":
70
+ return cls(
71
+ max_tasks_per_sec=_env_int("INTERMESH_MAX_TASKS_PER_SEC", 10),
72
+ max_in_flight=_env_int("INTERMESH_MAX_TASKS_IN_FLIGHT", 200),
73
+ max_queue_depth=_env_int("INTERMESH_MAX_TASK_QUEUE_DEPTH", 100),
74
+ enabled=_env_bool("INTERMESH_BACKPRESSURE_ENABLED", True),
75
+ )
76
+
77
+
78
+ @dataclass
79
+ class Rejection:
80
+ """Un refus, avec de quoi agir : pourquoi, et dans combien de temps."""
81
+
82
+ reason: str # rate_limited | in_flight_limit | task_queue_full
83
+ retry_after_ms: int
84
+ queue_depth: int
85
+ in_flight: int
86
+ limits: BackpressureLimits
87
+
88
+ code: str = "HUB_SATURATED"
89
+
90
+ def to_dict(self) -> dict:
91
+ return {
92
+ "code": self.code,
93
+ "reason": self.reason,
94
+ "retry_after_ms": self.retry_after_ms,
95
+ "queue_depth": self.queue_depth,
96
+ "in_flight": self.in_flight,
97
+ "hub_tasks_per_sec_limit": self.limits.max_tasks_per_sec,
98
+ "hub_in_flight_limit": self.limits.max_in_flight,
99
+ "hub_queue_depth_limit": self.limits.max_queue_depth,
100
+ }
101
+
102
+ def __str__(self) -> str:
103
+ return (f"HUB_SATURATED ({self.reason}) : le Hub refuse cette tâche pour "
104
+ f"ne pas la mettre dans une file qui n'avancera pas. "
105
+ f"Réessayez dans {self.retry_after_ms} ms. "
106
+ f"File {self.queue_depth}/{self.limits.max_queue_depth}, "
107
+ f"en vol {self.in_flight}/{self.limits.max_in_flight}, "
108
+ f"plafond {self.limits.max_tasks_per_sec} tâches/s.")
109
+
110
+
111
+ class BackpressureGate:
112
+ """Décide d'accepter ou de refuser une soumission.
113
+
114
+ Le débit est contrôlé par un seau à jetons plutôt qu'une fenêtre
115
+ glissante : à cadence régulière les deux se valent, mais le seau
116
+ absorbe une rafale courte sans la refuser, ce qui correspond à ce que
117
+ fait un orchestrateur qui découpe un travail en lots.
118
+ """
119
+
120
+ def __init__(self, limits: BackpressureLimits | None = None,
121
+ now: float | None = None):
122
+ self.limits = limits or BackpressureLimits()
123
+ # Le seau démarre plein : un Hub qui vient de démarrer ne doit pas
124
+ # refuser la première rafale au motif qu'il n'a pas encore
125
+ # accumulé de jetons.
126
+ self._tokens = float(self.limits.max_tasks_per_sec)
127
+ self._last = now if now is not None else time.monotonic()
128
+
129
+ self.accepted = 0
130
+ self.rejected = 0
131
+ self.rejected_by_reason: dict[str, int] = {}
132
+ self._last_logged: float | None = None
133
+
134
+ # ------------------------------------------------------------------
135
+
136
+ def _refill(self, now: float) -> None:
137
+ elapsed = max(0.0, now - self._last)
138
+ self._last = now
139
+ capacity = float(self.limits.max_tasks_per_sec)
140
+ self._tokens = min(capacity, self._tokens + elapsed * capacity)
141
+
142
+ def _retry_after_ms(self) -> int:
143
+ """Délai avant qu'un jeton soit à nouveau disponible.
144
+
145
+ Renvoyé au client pour qu'il attende la bonne durée plutôt que de
146
+ réessayer aussitôt — un réessai immédiat aggrave la saturation
147
+ qu'il vient de rencontrer.
148
+ """
149
+ rate = self.limits.max_tasks_per_sec
150
+ if rate <= 0:
151
+ return 1000
152
+ missing = max(0.0, 1.0 - self._tokens)
153
+ return max(1, int(round((missing / rate) * 1000)))
154
+
155
+ # ------------------------------------------------------------------
156
+
157
+ def admit(self, in_flight: int, queue_depth: int,
158
+ now: float | None = None) -> Rejection | None:
159
+ """Renvoie None si la tâche est acceptée, sinon le refus.
160
+
161
+ L'ordre des contrôles n'est pas indifférent : les plafonds d'état
162
+ (en vol, file) passent avant le débit, parce qu'un Hub déjà plein
163
+ doit refuser même s'il lui reste des jetons — sinon on accepterait
164
+ une tâche dont on sait déjà qu'elle attendra.
165
+ """
166
+ if not self.limits.enabled:
167
+ self.accepted += 1
168
+ return None
169
+
170
+ now = time.monotonic() if now is None else now
171
+
172
+ if self.limits.max_in_flight and in_flight >= self.limits.max_in_flight:
173
+ return self._refuse("in_flight_limit", 1000, in_flight, queue_depth)
174
+
175
+ if self.limits.max_queue_depth and queue_depth >= self.limits.max_queue_depth:
176
+ return self._refuse("task_queue_full", 1000, in_flight, queue_depth)
177
+
178
+ self._refill(now)
179
+ if self.limits.max_tasks_per_sec and self._tokens < 1.0:
180
+ return self._refuse("rate_limited", self._retry_after_ms(),
181
+ in_flight, queue_depth)
182
+
183
+ self._tokens -= 1.0
184
+ self.accepted += 1
185
+ return None
186
+
187
+ def _refuse(self, reason: str, retry_after_ms: int,
188
+ in_flight: int, queue_depth: int) -> Rejection:
189
+ self.rejected += 1
190
+ self.rejected_by_reason[reason] = self.rejected_by_reason.get(reason, 0) + 1
191
+ return Rejection(
192
+ reason=reason, retry_after_ms=retry_after_ms,
193
+ queue_depth=queue_depth, in_flight=in_flight, limits=self.limits,
194
+ )
195
+
196
+ # ------------------------------------------------------------------
197
+
198
+ def note_rejection(self, now: float, window_s: float = 10.0) -> bool:
199
+ """Faut-il journaliser ce refus ? Une fois par période, pas plus.
200
+
201
+ Écrire chaque refus dans la chaîne d'audit ajoute un hachage Merkle
202
+ par refus — donc du travail proportionnel à la saturation, au pire
203
+ moment. Le compte exact reste disponible dans les compteurs ; ce
204
+ qu'un exploitant a besoin de voir dans l'audit, c'est *quand* le Hub
205
+ est entré en saturation, pas les milliers de refus qui suivent.
206
+ """
207
+ if self._last_logged is None or (now - self._last_logged) >= window_s:
208
+ self._last_logged = now
209
+ return True
210
+ return False
211
+
212
+ def snapshot(self, in_flight: int, queue_depth: int) -> dict:
213
+ """État lisible par un exploitant ou une sonde."""
214
+ return {
215
+ "enabled": self.limits.enabled,
216
+ "in_flight": in_flight,
217
+ "queue_depth": queue_depth,
218
+ "accepted_total": self.accepted,
219
+ "rejected_total": self.rejected,
220
+ "rejected_by_reason": dict(self.rejected_by_reason),
221
+ "tokens_available": round(self._tokens, 2),
222
+ "limits": {
223
+ "tasks_per_sec": self.limits.max_tasks_per_sec,
224
+ "in_flight": self.limits.max_in_flight,
225
+ "queue_depth": self.limits.max_queue_depth,
226
+ },
227
+ # Ce que l'exploitant regarde en premier : suis-je proche du mur ?
228
+ "saturation": {
229
+ "in_flight_pct": _pct(in_flight, self.limits.max_in_flight),
230
+ "queue_depth_pct": _pct(queue_depth, self.limits.max_queue_depth),
231
+ },
232
+ }
233
+
234
+
235
+ def _pct(value: int, limit: int) -> int:
236
+ if not limit:
237
+ return 0
238
+ return min(100, int(round(100.0 * value / limit)))
@@ -319,14 +319,31 @@ def run_dashboard(args):
319
319
  console_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "console")
320
320
 
321
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")
322
+ print(f"\033[31m✗ Console introuvable ({console_dir}).\033[0m")
323
+ print(" Depuis un paquet installé : pip install --force-reinstall intermesh")
324
+ print(" Depuis une copie du dépôt : ./scripts/build_console.sh")
325
+ print(" La console est un export généré : elle n'est pas versionnée.")
325
326
  return 1
326
327
 
327
328
  class Handler(http.server.SimpleHTTPRequestHandler):
328
329
  def __init__(self, *args_h, **kwargs_h):
329
330
  super().__init__(*args_h, directory=console_dir, **kwargs_h)
331
+
332
+ def translate_path(self, path):
333
+ """Fait correspondre /agents au fichier agents.html.
334
+
335
+ Un export statique nomme ses pages `agents.html`, alors que la
336
+ navigation côté client demande `/agents`. Sans cette
337
+ correspondance, tout lien interne rend 404 — la console
338
+ s'ouvrirait puis casserait au premier clic.
339
+ """
340
+ local = super().translate_path(path)
341
+ if not os.path.exists(local) and not local.endswith(".html"):
342
+ candidate = local.rstrip("/") + ".html"
343
+ if os.path.isfile(candidate):
344
+ return candidate
345
+ return local
346
+
330
347
  def log_message(self, format, *log_args): pass
331
348
 
332
349
  print(f"\033[32m✓ Console d'exploitation sur http://localhost:{port}\033[0m")
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html><html lang="en" class=""><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/d3ada213244fbd00.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-881ca5d5b20426f9.js"/><script src="/_next/static/chunks/fd9d1056-d17313c03983f67e.js" async=""></script><script src="/_next/static/chunks/117-279de47ad86d55f2.js" async=""></script><script src="/_next/static/chunks/main-app-bcf1b654d275aa86.js" async=""></script><meta name="robots" content="noindex"/><title>InterMesh — Control Plane</title><title>InterMesh — Control Plane</title><meta name="description" content="Open-source protocol that lets AI agents find each other, delegate work, and negotiate across organizations — with end-to-end encryption and a verifiable audit log."/><meta name="keywords" content="AI agents,agent protocol,multi-agent,agent coordination,open source,MCP,A2A"/><meta name="robots" content="index, follow"/><link rel="canonical" href="https://intermesh.site"/><meta property="og:title" content="InterMesh — The coordination protocol for AI agents"/><meta property="og:description" content="Let AI agents discover each other, talk, and delegate work — across teams, and across the boundary between two organizations that do not trust each other."/><meta property="og:url" content="https://intermesh.site"/><meta property="og:site_name" content="InterMesh"/><meta property="og:type" content="website"/><meta name="twitter:card" content="summary_large_image"/><meta name="twitter:title" content="InterMesh — The coordination protocol for AI agents"/><meta name="twitter:description" content="Open-source protocol for AI agents that work together, across organizations. End-to-end encrypted, with a verifiable audit log."/><script>
2
+ (function () {
3
+ try {
4
+ var stored = localStorage.getItem('intermesh-theme');
5
+ var dark = stored
6
+ ? stored === 'dark'
7
+ : window.matchMedia('(prefers-color-scheme: dark)').matches;
8
+ document.documentElement.classList.toggle('dark', dark);
9
+ } catch (e) {
10
+ document.documentElement.classList.add('dark');
11
+ }
12
+ requestAnimationFrame(function () {
13
+ document.documentElement.classList.add('theme-ready');
14
+ });
15
+ })();
16
+ </script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="min-h-screen bg-white font-sans text-slate-900 antialiased selection:bg-cyan-500/25 dark:bg-[#08080A] dark:text-slate-100"><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><script src="/_next/static/chunks/webpack-881ca5d5b20426f9.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/_next/static/css/d3ada213244fbd00.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"2:I[2846,[],\"\"]\n4:I[4707,[],\"\"]\n5:I[6423,[],\"\"]\nb:I[1060,[],\"\"]\n6:{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"}\n7:{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"}\n8:{\"display\":\"inline-block\"}\n9:{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0}\nc:[]\n"])</script><script>self.__next_f.push([1,"0:[\"$\",\"$L2\",null,{\"buildId\":\"8A7tou8DfWGFVRtP-NDfC\",\"assetPrefix\":\"\",\"urlParts\":[\"\",\"_not-found\"],\"initialTree\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{},[[\"$L3\",[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null],null],null]},[null,[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"/_not-found\",\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\"}]],null]},[[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/d3ada213244fbd00.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"\",\"suppressHydrationWarning\":true,\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"\\n(function () {\\n try {\\n var stored = localStorage.getItem('intermesh-theme');\\n var dark = stored\\n ? stored === 'dark'\\n : window.matchMedia('(prefers-color-scheme: dark)').matches;\\n document.documentElement.classList.toggle('dark', dark);\\n } catch (e) {\\n document.documentElement.classList.add('dark');\\n }\\n requestAnimationFrame(function () {\\n document.documentElement.classList.add('theme-ready');\\n });\\n})();\\n\"}}]}],[\"$\",\"body\",null,{\"className\":\"min-h-screen bg-white font-sans text-slate-900 antialiased selection:bg-cyan-500/25 dark:bg-[#08080A] dark:text-slate-100\",\"suppressHydrationWarning\":true,\"children\":[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":\"$6\",\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":\"$7\",\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":\"$8\",\"children\":[\"$\",\"h2\",null,{\"style\":\"$9\",\"children\":\"This page could not be found.\"}]}]]}]}]],\"notFoundStyles\":[]}]}]]}]],null],null],\"couldBeIntercepted\":false,\"initialHead\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],\"$La\"],\"globalErrorComponent\":\"$b\",\"missingSlots\":\"$Wc\"}]\n"])</script><script>self.__next_f.push([1,"a:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"InterMesh — The coordination protocol for AI agents\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"Open-source protocol that lets AI agents find each other, delegate work, and negotiate across organizations — with end-to-end encryption and a verifiable audit log.\"}],[\"$\",\"meta\",\"4\",{\"name\":\"keywords\",\"content\":\"AI agents,agent protocol,multi-agent,agent coordination,open source,MCP,A2A\"}],[\"$\",\"meta\",\"5\",{\"name\":\"robots\",\"content\":\"index, follow\"}],[\"$\",\"link\",\"6\",{\"rel\":\"canonical\",\"href\":\"https://intermesh.site\"}],[\"$\",\"meta\",\"7\",{\"property\":\"og:title\",\"content\":\"InterMesh — The coordination protocol for AI agents\"}],[\"$\",\"meta\",\"8\",{\"property\":\"og:description\",\"content\":\"Let AI agents discover each other, talk, and delegate work — across teams, and across the boundary between two organizations that do not trust each other.\"}],[\"$\",\"meta\",\"9\",{\"property\":\"og:url\",\"content\":\"https://intermesh.site\"}],[\"$\",\"meta\",\"10\",{\"property\":\"og:site_name\",\"content\":\"InterMesh\"}],[\"$\",\"meta\",\"11\",{\"property\":\"og:type\",\"content\":\"website\"}],[\"$\",\"meta\",\"12\",{\"name\":\"twitter:card\",\"content\":\"summary_large_image\"}],[\"$\",\"meta\",\"13\",{\"name\":\"twitter:title\",\"content\":\"InterMesh — The coordination protocol for AI agents\"}],[\"$\",\"meta\",\"14\",{\"name\":\"twitter:description\",\"content\":\"Open-source protocol for AI agents that work together, across organizations. End-to-end encrypted, with a verifiable audit log.\"}]]\n3:null\n"])</script></body></html>
@@ -0,0 +1 @@
1
+ self.__BUILD_MANIFEST={__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/_error":["static/chunks/pages/_error-7ba65e1336b92748.js"],sortedPages:["/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
@@ -0,0 +1 @@
1
+ self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()