|
| 1 | +""" |
| 2 | +AgentMesh Trust Score Dashboard (stdlib-only) |
| 3 | +============================================= |
| 4 | +Serves a self-contained HTML dashboard that visualises trust scores, |
| 5 | +score history, and trust-tier distribution for registered agents. |
| 6 | +
|
| 7 | +Usage: |
| 8 | + python dashboard.py [--port PORT] |
| 9 | +
|
| 10 | +The page auto-refreshes every 5 seconds by polling ``/api/data``. |
| 11 | +""" |
| 12 | + |
| 13 | +from __future__ import annotations |
| 14 | + |
| 15 | +import argparse |
| 16 | +import json |
| 17 | +import threading |
| 18 | +from http.server import HTTPServer, BaseHTTPRequestHandler |
| 19 | +from typing import Any |
| 20 | + |
| 21 | +# --------------------------------------------------------------------------- |
| 22 | +# Shared data store — mutate via ``update_data()`` |
| 23 | +# --------------------------------------------------------------------------- |
| 24 | + |
| 25 | +_lock = threading.Lock() |
| 26 | +_data: dict[str, Any] = { |
| 27 | + "agents": {}, # name -> {score, protocol, did} |
| 28 | + "history": {}, # name -> [(timestamp_iso, score), ...] |
| 29 | + "tiers": { # tier_name -> count |
| 30 | + "Verified Partner": 0, |
| 31 | + "Trusted": 0, |
| 32 | + "Standard": 0, |
| 33 | + "Probationary": 0, |
| 34 | + "Untrusted": 0, |
| 35 | + }, |
| 36 | +} |
| 37 | + |
| 38 | +TIER_RANGES = [ |
| 39 | + ("Verified Partner", 900, 1000), |
| 40 | + ("Trusted", 700, 899), |
| 41 | + ("Standard", 500, 699), |
| 42 | + ("Probationary", 300, 499), |
| 43 | + ("Untrusted", 0, 299), |
| 44 | +] |
| 45 | + |
| 46 | + |
| 47 | +def _tier_for_score(score: int) -> str: |
| 48 | + for name, lo, hi in TIER_RANGES: |
| 49 | + if lo <= score <= hi: |
| 50 | + return name |
| 51 | + return "Untrusted" |
| 52 | + |
| 53 | + |
| 54 | +def _recompute_tiers() -> None: |
| 55 | + counts = {name: 0 for name, _, _ in TIER_RANGES} |
| 56 | + for info in _data["agents"].values(): |
| 57 | + counts[_tier_for_score(info["score"])] += 1 |
| 58 | + _data["tiers"] = counts |
| 59 | + |
| 60 | + |
| 61 | +def update_data( |
| 62 | + agents: dict[str, dict] | None = None, |
| 63 | + history: dict[str, list] | None = None, |
| 64 | +) -> None: |
| 65 | + """Thread-safe update of the shared data store.""" |
| 66 | + with _lock: |
| 67 | + if agents is not None: |
| 68 | + _data["agents"] = agents |
| 69 | + _recompute_tiers() |
| 70 | + if history is not None: |
| 71 | + _data["history"] = history |
| 72 | + |
| 73 | + |
| 74 | +def get_data() -> dict[str, Any]: |
| 75 | + """Return a snapshot of the current data.""" |
| 76 | + with _lock: |
| 77 | + return json.loads(json.dumps(_data)) |
| 78 | + |
| 79 | + |
| 80 | +# --------------------------------------------------------------------------- |
| 81 | +# HTML page (embedded) |
| 82 | +# --------------------------------------------------------------------------- |
| 83 | + |
| 84 | +_HTML_PAGE = r"""<!DOCTYPE html> |
| 85 | +<html lang="en"> |
| 86 | +<head> |
| 87 | +<meta charset="utf-8" /> |
| 88 | +<title>AgentMesh Trust Dashboard</title> |
| 89 | +<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script> |
| 90 | +<style> |
| 91 | + :root{--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#c9d1d9; |
| 92 | + --accent:#58a6ff} |
| 93 | + *{box-sizing:border-box;margin:0;padding:0} |
| 94 | + body{font-family:system-ui,sans-serif;background:var(--bg);color:var(--text); |
| 95 | + padding:24px} |
| 96 | + h1{text-align:center;margin-bottom:24px;color:var(--accent)} |
| 97 | + .grid{display:grid;gap:20px} |
| 98 | + .two{grid-template-columns:1fr 1fr} |
| 99 | + .card{background:var(--card);border:1px solid var(--border);border-radius:10px; |
| 100 | + padding:20px} |
| 101 | + .card h2{font-size:1rem;margin-bottom:12px;color:var(--accent)} |
| 102 | + canvas{width:100%!important} |
| 103 | + .kpi-row{display:flex;gap:16px;justify-content:center;margin-bottom:20px; |
| 104 | + flex-wrap:wrap} |
| 105 | + .kpi{background:var(--card);border:1px solid var(--border);border-radius:8px; |
| 106 | + padding:14px 28px;text-align:center;min-width:140px} |
| 107 | + .kpi .value{font-size:1.6rem;font-weight:700;color:var(--accent)} |
| 108 | + .kpi .label{font-size:.75rem;color:#8b949e;margin-top:4px} |
| 109 | + .tier-badge{display:inline-block;padding:2px 8px;border-radius:4px; |
| 110 | + font-size:.75rem;font-weight:600;margin-left:6px} |
| 111 | + .tier-verified{background:#1a7f37;color:#fff} |
| 112 | + .tier-trusted{background:#2ea043;color:#fff} |
| 113 | + .tier-standard{background:#d29922;color:#fff} |
| 114 | + .tier-probationary{background:#db6d28;color:#fff} |
| 115 | + .tier-untrusted{background:#da3633;color:#fff} |
| 116 | + #agent-table{width:100%;border-collapse:collapse;font-size:.85rem} |
| 117 | + #agent-table th,#agent-table td{padding:8px 12px;text-align:left; |
| 118 | + border-bottom:1px solid var(--border)} |
| 119 | + #agent-table th{color:#8b949e;font-weight:600} |
| 120 | + .bar-cell{position:relative;height:20px;background:#21262d;border-radius:4px; |
| 121 | + overflow:hidden} |
| 122 | + .bar-fill{height:100%;border-radius:4px;transition:width .4s} |
| 123 | + @media(max-width:860px){.two{grid-template-columns:1fr}} |
| 124 | +</style> |
| 125 | +</head> |
| 126 | +<body> |
| 127 | +<h1>🛡 AgentMesh Trust Dashboard</h1> |
| 128 | +<div class="kpi-row" id="kpis"></div> |
| 129 | +<div class="card" style="margin-bottom:20px;overflow-x:auto"> |
| 130 | + <h2>Registered Agents</h2> |
| 131 | + <table id="agent-table"><thead><tr> |
| 132 | + <th>Agent</th><th>Protocol</th><th>Score</th><th>Tier</th><th></th> |
| 133 | + </tr></thead><tbody id="agent-tbody"></tbody></table> |
| 134 | +</div> |
| 135 | +<div class="grid two"> |
| 136 | + <div class="card"><h2>Trust Score History</h2> |
| 137 | + <canvas id="historyChart"></canvas></div> |
| 138 | + <div class="card"><h2>Trust Tier Distribution</h2> |
| 139 | + <canvas id="tierChart"></canvas></div> |
| 140 | +</div> |
| 141 | +<script> |
| 142 | +const TIER_COLORS={ |
| 143 | + "Verified Partner":"#1a7f37","Trusted":"#2ea043", |
| 144 | + "Standard":"#d29922","Probationary":"#db6d28","Untrusted":"#da3633"}; |
| 145 | +const TIER_CSS={ |
| 146 | + "Verified Partner":"verified","Trusted":"trusted","Standard":"standard", |
| 147 | + "Probationary":"probationary","Untrusted":"untrusted"}; |
| 148 | +const LINE_COLORS=[ |
| 149 | + "#58a6ff","#f78166","#3fb950","#d2a8ff","#79c0ff", |
| 150 | + "#ffa657","#7ee787","#ff7b72","#a5d6ff","#d29922"]; |
| 151 | +
|
| 152 | +function tierFor(s){ |
| 153 | + if(s>=900) return "Verified Partner"; |
| 154 | + if(s>=700) return "Trusted"; |
| 155 | + if(s>=500) return "Standard"; |
| 156 | + if(s>=300) return "Probationary"; |
| 157 | + return "Untrusted"; |
| 158 | +} |
| 159 | +function barColor(s){return TIER_COLORS[tierFor(s)];} |
| 160 | +
|
| 161 | +let histChart=null, tierChart=null; |
| 162 | +
|
| 163 | +function renderKPIs(agents){ |
| 164 | + const scores=Object.values(agents).map(a=>a.score); |
| 165 | + const n=scores.length, avg=n? (scores.reduce((a,b)=>a+b,0)/n).toFixed(0) :0; |
| 166 | + const mn=n? Math.min(...scores):0, mx=n? Math.max(...scores):0; |
| 167 | + document.getElementById("kpis").innerHTML= |
| 168 | + kpi("Agents",n)+kpi("Avg Score",avg)+kpi("Min",mn)+kpi("Max",mx); |
| 169 | +} |
| 170 | +function kpi(label,value){ |
| 171 | + return `<div class="kpi"><div class="value">${value}</div><div class="label">${label}</div></div>`; |
| 172 | +} |
| 173 | +
|
| 174 | +function renderTable(agents){ |
| 175 | + const tbody=document.getElementById("agent-tbody"); |
| 176 | + const sorted=Object.entries(agents).sort((a,b)=>b[1].score-a[1].score); |
| 177 | + tbody.innerHTML=sorted.map(([name,info])=>{ |
| 178 | + const tier=tierFor(info.score); |
| 179 | + const pct=(info.score/1000*100).toFixed(1); |
| 180 | + return `<tr><td><b>${name}</b></td><td>${info.protocol||""}</td> |
| 181 | + <td>${info.score}</td> |
| 182 | + <td><span class="tier-badge tier-${TIER_CSS[tier]}">${tier}</span></td> |
| 183 | + <td style="width:30%"><div class="bar-cell"><div class="bar-fill" |
| 184 | + style="width:${pct}%;background:${barColor(info.score)}"></div></div></td></tr>`; |
| 185 | + }).join(""); |
| 186 | +} |
| 187 | +
|
| 188 | +function renderHistory(history){ |
| 189 | + const datasets=[]; |
| 190 | + let idx=0; |
| 191 | + for(const[name,pts] of Object.entries(history)){ |
| 192 | + datasets.push({ |
| 193 | + label:name, |
| 194 | + data:pts.map(p=>({x:p[0],y:p[1]})), |
| 195 | + borderColor:LINE_COLORS[idx%LINE_COLORS.length], |
| 196 | + borderWidth:2,fill:false,tension:.3,pointRadius:0 |
| 197 | + }); |
| 198 | + idx++; |
| 199 | + } |
| 200 | + if(histChart){histChart.data.datasets=datasets;histChart.update();} |
| 201 | + else{ |
| 202 | + histChart=new Chart(document.getElementById("historyChart"),{ |
| 203 | + type:"line",data:{datasets}, |
| 204 | + options:{responsive:true, |
| 205 | + scales:{x:{type:"category",ticks:{maxTicksToShow:10,color:"#8b949e"}}, |
| 206 | + y:{min:0,max:1000,ticks:{color:"#8b949e"},grid:{color:"#21262d"}}}, |
| 207 | + plugins:{legend:{labels:{color:"#c9d1d9",boxWidth:12}}}} |
| 208 | + }); |
| 209 | + } |
| 210 | +} |
| 211 | +
|
| 212 | +function renderTiers(tiers){ |
| 213 | + const labels=Object.keys(tiers); |
| 214 | + const data=Object.values(tiers); |
| 215 | + const bg=labels.map(l=>TIER_COLORS[l]); |
| 216 | + if(tierChart){tierChart.data.datasets[0].data=data;tierChart.update();} |
| 217 | + else{ |
| 218 | + tierChart=new Chart(document.getElementById("tierChart"),{ |
| 219 | + type:"doughnut", |
| 220 | + data:{labels,datasets:[{data,backgroundColor:bg,borderWidth:0}]}, |
| 221 | + options:{responsive:true, |
| 222 | + plugins:{legend:{labels:{color:"#c9d1d9"}}}} |
| 223 | + }); |
| 224 | + } |
| 225 | +} |
| 226 | +
|
| 227 | +async function refresh(){ |
| 228 | + try{ |
| 229 | + const r=await fetch("/api/data"); |
| 230 | + const d=await r.json(); |
| 231 | + renderKPIs(d.agents); |
| 232 | + renderTable(d.agents); |
| 233 | + renderHistory(d.history); |
| 234 | + renderTiers(d.tiers); |
| 235 | + }catch(e){console.error("refresh failed",e);} |
| 236 | +} |
| 237 | +
|
| 238 | +refresh(); |
| 239 | +setInterval(refresh,5000); |
| 240 | +</script> |
| 241 | +</body> |
| 242 | +</html>""" |
| 243 | + |
| 244 | + |
| 245 | +# --------------------------------------------------------------------------- |
| 246 | +# HTTP request handler |
| 247 | +# --------------------------------------------------------------------------- |
| 248 | + |
| 249 | +class _Handler(BaseHTTPRequestHandler): |
| 250 | + """Serves the HTML page at ``/`` and JSON data at ``/api/data``.""" |
| 251 | + |
| 252 | + def do_GET(self) -> None: # noqa: N802 |
| 253 | + if self.path == "/api/data": |
| 254 | + payload = json.dumps(get_data()).encode() |
| 255 | + self._respond(200, "application/json", payload) |
| 256 | + elif self.path in ("/", "/index.html"): |
| 257 | + self._respond(200, "text/html; charset=utf-8", _HTML_PAGE.encode()) |
| 258 | + else: |
| 259 | + self._respond(404, "text/plain", b"Not Found") |
| 260 | + |
| 261 | + def _respond(self, code: int, content_type: str, body: bytes) -> None: |
| 262 | + self.send_response(code) |
| 263 | + self.send_header("Content-Type", content_type) |
| 264 | + self.send_header("Content-Length", str(len(body))) |
| 265 | + self.send_header("Access-Control-Allow-Origin", "*") |
| 266 | + self.end_headers() |
| 267 | + self.wfile.write(body) |
| 268 | + |
| 269 | + def log_message(self, format: str, *args: Any) -> None: # noqa: A002 |
| 270 | + """Silence default request logging.""" |
| 271 | + pass |
| 272 | + |
| 273 | + |
| 274 | +# --------------------------------------------------------------------------- |
| 275 | +# Server lifecycle |
| 276 | +# --------------------------------------------------------------------------- |
| 277 | + |
| 278 | +def start_server(port: int = 8050) -> HTTPServer: |
| 279 | + """Start the dashboard server in a daemon thread and return the server.""" |
| 280 | + server = HTTPServer(("", port), _Handler) |
| 281 | + t = threading.Thread(target=server.serve_forever, daemon=True) |
| 282 | + t.start() |
| 283 | + print(f"Dashboard running at http://localhost:{port}") |
| 284 | + return server |
| 285 | + |
| 286 | + |
| 287 | +def main() -> None: |
| 288 | + parser = argparse.ArgumentParser(description="AgentMesh Trust Dashboard") |
| 289 | + parser.add_argument("--port", type=int, default=8050) |
| 290 | + args = parser.parse_args() |
| 291 | + |
| 292 | + # Seed with demo data so the page isn't blank |
| 293 | + _seed_demo_data() |
| 294 | + server = start_server(args.port) |
| 295 | + try: |
| 296 | + server.serve_forever() |
| 297 | + except KeyboardInterrupt: |
| 298 | + print("\nShutting down.") |
| 299 | + server.shutdown() |
| 300 | + |
| 301 | + |
| 302 | +def _seed_demo_data() -> None: |
| 303 | + """Populate the store with sample agents for standalone use.""" |
| 304 | + import datetime as dt |
| 305 | + import random |
| 306 | + |
| 307 | + random.seed(42) |
| 308 | + agents = { |
| 309 | + "payment-agent": {"score": 920, "protocol": "A2A", "did": "did:web:payments.mesh.io"}, |
| 310 | + "customer-service": {"score": 870, "protocol": "A2A", "did": "did:web:cs.mesh.io"}, |
| 311 | + "data-analyst": {"score": 810, "protocol": "MCP", "did": "did:web:analytics.mesh.io"}, |
| 312 | + "fraud-detector": {"score": 940, "protocol": "IATP", "did": "did:web:fraud.mesh.io"}, |
| 313 | + "inventory-manager": {"score": 720, "protocol": "MCP", "did": "did:web:inventory.mesh.io"}, |
| 314 | + "email-dispatcher": {"score": 650, "protocol": "A2A", "did": "did:web:email.mesh.io"}, |
| 315 | + "auth-gateway": {"score": 950, "protocol": "IATP", "did": "did:web:auth.mesh.io"}, |
| 316 | + "report-generator": {"score": 580, "protocol": "MCP", "did": "did:web:reports.mesh.io"}, |
| 317 | + "scheduler": {"score": 780, "protocol": "A2A", "did": "did:web:scheduler.mesh.io"}, |
| 318 | + "compliance-bot": {"score": 890, "protocol": "IATP", "did": "did:web:compliance.mesh.io"}, |
| 319 | + } |
| 320 | + |
| 321 | + now = dt.datetime.now(dt.timezone.utc) |
| 322 | + history: dict[str, list] = {} |
| 323 | + for name, info in agents.items(): |
| 324 | + pts = [] |
| 325 | + score = info["score"] |
| 326 | + for i in range(48): |
| 327 | + t = now - dt.timedelta(minutes=15 * (47 - i)) |
| 328 | + score = max(0, min(1000, score + random.randint(-15, 15))) |
| 329 | + pts.append((t.strftime("%H:%M"), score)) |
| 330 | + # Reset final score to the canonical value |
| 331 | + pts[-1] = (pts[-1][0], info["score"]) |
| 332 | + history[name] = pts |
| 333 | + |
| 334 | + update_data(agents=agents, history=history) |
| 335 | + |
| 336 | + |
| 337 | +if __name__ == "__main__": |
| 338 | + main() |
0 commit comments