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.
- {intermesh-0.4.3 → intermesh-0.4.5}/PKG-INFO +1 -1
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/__init__.py +1 -1
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/admin.py +28 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/agent.py +20 -2
- intermesh-0.4.5/intermesh/backpressure.py +238 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/cli.py +20 -3
- intermesh-0.4.5/intermesh/console/404.html +16 -0
- intermesh-0.4.5/intermesh/console/_next/static/8A7tou8DfWGFVRtP-NDfC/_buildManifest.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/8A7tou8DfWGFVRtP-NDfC/_ssgManifest.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/117-279de47ad86d55f2.js +2 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/283-7828913da424ef26.js +46 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/44530001-792177c4c52bbd93.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/451-d950d00746903c71.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/67.55c167dcccf37409.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/845-1b26226a3e7e86ee.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/agents/page-e2b539ca4d8e901f.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/billing/page-ad7e25cb466a1d75.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/dashboard/page-ef7c7b6a0300c304.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/keys/page-38e3537a90794291.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/layout-5acef3b51b22acab.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/resumes/page-b92a0a12233cda42.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/security/page-2b0ec08aad65bca2.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/settings/page-ecd261e5d7fcb349.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/(app)/topology/page-faa778b14d5de62f.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/_not-found/page-6d4614157fff2d5f.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/docs/page-9b599783e780a1ca.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/layout-904d511bcfb3dc49.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/page-a7b8793e2971aba6.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/pricing/page-0cf2650bbac40063.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/privacy/page-108901b94493a585.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/app/terms/page-9a0f507db0aa3753.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/fd9d1056-d17313c03983f67e.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/main-3dfb547d0081738b.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/main-app-bcf1b654d275aa86.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/chunks/webpack-881ca5d5b20426f9.js +1 -0
- intermesh-0.4.5/intermesh/console/_next/static/css/d3ada213244fbd00.css +3 -0
- intermesh-0.4.5/intermesh/console/agents.html +16 -0
- intermesh-0.4.5/intermesh/console/agents.txt +13 -0
- intermesh-0.4.5/intermesh/console/billing.html +16 -0
- intermesh-0.4.5/intermesh/console/billing.txt +13 -0
- intermesh-0.4.5/intermesh/console/dashboard.html +16 -0
- intermesh-0.4.5/intermesh/console/dashboard.txt +13 -0
- intermesh-0.4.5/intermesh/console/icon.png +0 -0
- intermesh-0.4.5/intermesh/console/index.html +16 -0
- intermesh-0.4.5/intermesh/console/intermesh-logo.png +0 -0
- intermesh-0.4.5/intermesh/console/keys.html +16 -0
- intermesh-0.4.5/intermesh/console/keys.txt +13 -0
- intermesh-0.4.5/intermesh/console/opengraph-image +0 -0
- intermesh-0.4.5/intermesh/console/resumes.html +16 -0
- intermesh-0.4.5/intermesh/console/resumes.txt +13 -0
- intermesh-0.4.5/intermesh/console/security.html +16 -0
- intermesh-0.4.5/intermesh/console/security.txt +13 -0
- intermesh-0.4.5/intermesh/console/settings.html +16 -0
- intermesh-0.4.5/intermesh/console/settings.txt +13 -0
- intermesh-0.4.5/intermesh/console/topology.html +16 -0
- intermesh-0.4.5/intermesh/console/topology.txt +13 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/hub.py +105 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh.egg-info/PKG-INFO +1 -1
- intermesh-0.4.5/intermesh.egg-info/SOURCES.txt +95 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/pyproject.toml +5 -2
- intermesh-0.4.3/intermesh/console/app.js +0 -601
- intermesh-0.4.3/intermesh/console/index.html +0 -427
- intermesh-0.4.3/intermesh.egg-info/SOURCES.txt +0 -42
- {intermesh-0.4.3 → intermesh-0.4.5}/README.md +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/adapters.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/apikeys.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/approval.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/audit.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/bridge.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/config.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/console/__init__.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/crypto.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/egress.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/escrow.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/guardrails.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/hardware.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/health.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/identity.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/logger.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/message.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/metrics.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/peering.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/pipeline.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/policy.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/ratelimit.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/schema.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/secret.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/signing.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/snapshot.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/store.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh/task.py +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh.egg-info/dependency_links.txt +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh.egg-info/entry_points.txt +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh.egg-info/requires.txt +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/intermesh.egg-info/top_level.txt +0 -0
- {intermesh-0.4.3 → intermesh-0.4.5}/setup.cfg +0 -0
|
@@ -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
|
-
|
|
469
|
-
|
|
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
|
|
323
|
-
|
|
324
|
-
print("
|
|
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,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";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()
|