halyn 0.3.3__tar.gz → 0.4.0__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 (52) hide show
  1. {halyn-0.3.3/src/halyn.egg-info → halyn-0.4.0}/PKG-INFO +1 -1
  2. {halyn-0.3.3 → halyn-0.4.0}/pyproject.toml +1 -1
  3. halyn-0.4.0/src/halyn/__init__.py +48 -0
  4. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/control_plane.py +2 -0
  5. halyn-0.4.0/src/halyn/dashboard.py +206 -0
  6. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/mcp_serve.py +10 -2
  7. {halyn-0.3.3 → halyn-0.4.0/src/halyn.egg-info}/PKG-INFO +1 -1
  8. {halyn-0.3.3 → halyn-0.4.0}/src/halyn.egg-info/SOURCES.txt +1 -0
  9. halyn-0.3.3/src/halyn/__init__.py +0 -51
  10. {halyn-0.3.3 → halyn-0.4.0}/LICENSE +0 -0
  11. {halyn-0.3.3 → halyn-0.4.0}/README.md +0 -0
  12. {halyn-0.3.3 → halyn-0.4.0}/setup.cfg +0 -0
  13. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/__main__.py +0 -0
  14. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/audit.py +0 -0
  15. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/auth.py +0 -0
  16. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/autonomy.py +0 -0
  17. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/cli.py +0 -0
  18. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/config.py +0 -0
  19. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/consent.py +0 -0
  20. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/discovery.py +0 -0
  21. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/drivers/__init__.py +0 -0
  22. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/drivers/browser.py +0 -0
  23. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/drivers/dds.py +0 -0
  24. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/drivers/docker.py +0 -0
  25. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/drivers/http_auto.py +0 -0
  26. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/drivers/mqtt.py +0 -0
  27. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/drivers/opcua.py +0 -0
  28. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/drivers/ros2.py +0 -0
  29. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/drivers/serial.py +0 -0
  30. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/drivers/socket_raw.py +0 -0
  31. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/drivers/ssh.py +0 -0
  32. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/drivers/unitree.py +0 -0
  33. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/drivers/websocket.py +0 -0
  34. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/engine.py +0 -0
  35. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/integrations/__init__.py +0 -0
  36. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/integrations/telegram.py +0 -0
  37. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/intent.py +0 -0
  38. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/llm.py +0 -0
  39. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/mcp.py +0 -0
  40. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/memory/__init__.py +0 -0
  41. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/memory/store.py +0 -0
  42. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/nrp_bridge.py +0 -0
  43. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/py.typed +0 -0
  44. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/sanitizer.py +0 -0
  45. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/server.py +0 -0
  46. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/types.py +0 -0
  47. {halyn-0.3.3 → halyn-0.4.0}/src/halyn/watchdog.py +0 -0
  48. {halyn-0.3.3 → halyn-0.4.0}/src/halyn.egg-info/dependency_links.txt +0 -0
  49. {halyn-0.3.3 → halyn-0.4.0}/src/halyn.egg-info/entry_points.txt +0 -0
  50. {halyn-0.3.3 → halyn-0.4.0}/src/halyn.egg-info/requires.txt +0 -0
  51. {halyn-0.3.3 → halyn-0.4.0}/src/halyn.egg-info/top_level.txt +0 -0
  52. {halyn-0.3.3 → halyn-0.4.0}/tests/test_halyn.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: halyn
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Halyn — NRP control plane with domain-scoped authorization.
5
5
  Author-email: Elmadani SALKA <contact@halyn.dev>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "halyn"
3
- version = "0.3.3"
3
+ version = "0.4.0"
4
4
  description = "Halyn — NRP control plane with domain-scoped authorization."
5
5
  requires-python = ">=3.10"
6
6
  license = {text = "MIT"}
@@ -0,0 +1,48 @@
1
+ # Copyright (c) 2026 Elmadani SALKA. All rights reserved.
2
+ # Licensed under the MIT License. See LICENSE file.
3
+ """Halyn — Connect any device to any AI."""
4
+
5
+ __version__ = "0.4.0"
6
+ __author__ = "Elmadani SALKA"
7
+ __license__ = "MIT"
8
+
9
+
10
+ def _try(module, cls):
11
+ try:
12
+ mod = __import__(module, fromlist=[cls])
13
+ return getattr(mod, cls)
14
+ except (ImportError, AttributeError):
15
+ return None
16
+
17
+
18
+ # Core
19
+ ControlPlane = _try("halyn.control_plane", "ControlPlane")
20
+
21
+ # Autonomy — alias Level as Autonomy for simplicity
22
+ Autonomy = _try("halyn.autonomy", "Level")
23
+ AutonomyController = _try("halyn.autonomy", "AutonomyController")
24
+
25
+ # Audit — alias AuditStore as AuditChain for public API
26
+ AuditChain = _try("halyn.audit", "AuditStore")
27
+ AuditEntry = _try("halyn.audit", "AuditEntry")
28
+
29
+ # Drivers
30
+ SSHDriver = _try("halyn.drivers.ssh", "SSHDriver")
31
+ HTTPDriver = _try("halyn.drivers.http_auto", "HTTPAutoDriver")
32
+ WebSocketDriver = _try("halyn.drivers.websocket", "WebSocketDriver")
33
+ SerialDriver = _try("halyn.drivers.serial", "SerialDriver")
34
+ MQTTDriver = _try("halyn.drivers.mqtt", "MQTTDriver")
35
+ OPCUADriver = _try("halyn.drivers.opcua", "OPCUADriver")
36
+ ROS2Driver = _try("halyn.drivers.ros2", "ROS2Driver")
37
+ DDSDriver = _try("halyn.drivers.dds", "DDSDriver")
38
+ DockerDriver = _try("halyn.drivers.docker", "DockerDriver")
39
+ BrowserDriver = _try("halyn.drivers.browser", "BrowserDriver")
40
+ UnitreeDriver = _try("halyn.drivers.unitree", "UnitreeDriver")
41
+ SocketDriver = _try("halyn.drivers.socket_raw", "SocketDriver")
42
+
43
+ __all__ = [
44
+ "ControlPlane", "Autonomy", "AutonomyController", "AuditChain", "AuditEntry",
45
+ "SSHDriver", "HTTPDriver", "WebSocketDriver", "SerialDriver",
46
+ "MQTTDriver", "OPCUADriver", "ROS2Driver", "DDSDriver",
47
+ "DockerDriver", "BrowserDriver", "UnitreeDriver", "SocketDriver",
48
+ ]
@@ -13,6 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  import asyncio
15
15
  import logging
16
+ import os
16
17
  import time
17
18
  from dataclasses import dataclass, field
18
19
  from typing import Any
@@ -53,6 +54,7 @@ class ControlPlane:
53
54
 
54
55
  # Safety & Control
55
56
  self.autonomy = AutonomyController(default_level=Level.SUPERVISED)
57
+ os.makedirs(self.config.data_dir, exist_ok=True)
56
58
  self.audit = AuditStore(f"{self.config.data_dir}/audit.db")
57
59
  self.consent = ConsentStore(f"{self.config.data_dir}/consent.db")
58
60
  self.intents = IntentStore(f"{self.config.data_dir}/intent.db")
@@ -0,0 +1,206 @@
1
+ # Copyright (c) 2026 Elmadani SALKA. MIT License.
2
+ """
3
+ Halyn Dashboard — Built-in web UI for the MCP Server.
4
+
5
+ When running halyn-mcp, opening http://localhost:8935 shows this dashboard.
6
+ Real-time device status, shield rules, audit chain — all visual.
7
+ """
8
+
9
+ DASHBOARD_HTML = '''<!DOCTYPE html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="UTF-8">
13
+ <meta name="viewport" content="width=device-width,initial-scale=1">
14
+ <title>Halyn Dashboard</title>
15
+ <style>
16
+ *{margin:0;padding:0;box-sizing:border-box}
17
+ :root{--bg:#0b0d10;--bg2:#13161b;--fg:#e1e4ea;--fg2:#8b919e;--fg3:#5c6370;--accent:#0a6e3f;--accent2:#3ddc84;--red:#ef4444;--border:#1e2330;--font:system-ui,sans-serif;--mono:'Courier New',monospace}
18
+ body{background:var(--bg);color:var(--fg);font-family:var(--font);min-height:100vh}
19
+ .top{background:var(--bg2);border-bottom:1px solid var(--border);padding:.75rem 1.5rem;display:flex;justify-content:space-between;align-items:center}
20
+ .top h1{font-size:1rem;font-weight:600;display:flex;align-items:center;gap:.5rem}
21
+ .top h1 span{color:var(--accent2)}
22
+ .top .status{font-size:.75rem;color:var(--accent2);font-family:var(--mono);display:flex;align-items:center;gap:.4rem}
23
+ .top .dot{width:8px;height:8px;border-radius:50%;background:var(--accent2);animation:pulse 2s infinite}
24
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
25
+ .grid{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--border);margin:0;height:calc(100vh - 49px)}
26
+ .panel{background:var(--bg);padding:1rem;overflow-y:auto}
27
+ .panel h2{font-size:.8rem;font-weight:600;color:var(--fg2);text-transform:uppercase;letter-spacing:1px;margin-bottom:.75rem;display:flex;align-items:center;gap:.5rem}
28
+ .panel h2 .count{background:var(--bg2);color:var(--fg3);font-size:.65rem;padding:2px 6px;border-radius:8px}
29
+
30
+ /* Chat / Command */
31
+ .chat{display:flex;flex-direction:column;height:100%}
32
+ .messages{flex:1;overflow-y:auto;padding-bottom:.5rem}
33
+ .msg{margin-bottom:.5rem;padding:.5rem .75rem;border-radius:8px;font-size:.82rem;line-height:1.5;max-width:90%}
34
+ .msg.user{background:var(--accent);color:#fff;margin-left:auto;border-bottom-right-radius:2px}
35
+ .msg.sys{background:var(--bg2);color:var(--fg2);border-bottom-left-radius:2px}
36
+ .msg.blocked{background:#2a1215;color:var(--red);border:1px solid #4a1c20}
37
+ .msg.ok{background:#0f2318;color:var(--accent2);border:1px solid #1a3a28}
38
+ .input-row{display:flex;gap:.5rem;padding-top:.5rem;border-top:1px solid var(--border)}
39
+ .input-row input{flex:1;background:var(--bg2);border:1px solid var(--border);color:var(--fg);padding:.6rem .75rem;border-radius:6px;font-size:.82rem;font-family:var(--font);outline:none}
40
+ .input-row input:focus{border-color:var(--accent)}
41
+ .input-row button{background:var(--accent);color:#fff;border:none;padding:.6rem 1rem;border-radius:6px;font-size:.82rem;cursor:pointer;font-weight:500}
42
+ .input-row button:hover{background:var(--accent2);color:#000}
43
+
44
+ /* Shields */
45
+ .shield{background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:.5rem .75rem;margin-bottom:.4rem;font-family:var(--mono);font-size:.75rem;color:var(--fg2);display:flex;align-items:center;gap:.5rem}
46
+ .shield::before{content:"🛡️";font-size:.9rem}
47
+
48
+ /* Audit */
49
+ .audit-entry{display:grid;grid-template-columns:auto 1fr auto auto;gap:.5rem;padding:.4rem 0;border-bottom:1px solid var(--border);font-size:.72rem;font-family:var(--mono);align-items:center}
50
+ .audit-entry .time{color:var(--fg3)}
51
+ .audit-entry .tool{color:var(--fg)}
52
+ .audit-entry .hash{color:var(--fg3);font-size:.65rem}
53
+ .audit-entry .badge{padding:1px 6px;border-radius:4px;font-size:.6rem;font-weight:600}
54
+ .badge.ok{background:#0f2318;color:var(--accent2)}
55
+ .badge.blocked{background:#2a1215;color:var(--red)}
56
+
57
+ /* Stats */
58
+ .stats{display:grid;grid-template-columns:repeat(4,1fr);gap:.5rem;margin-bottom:1rem}
59
+ .stat{background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:.75rem;text-align:center}
60
+ .stat .v{font-family:var(--mono);font-size:1.5rem;font-weight:700;color:var(--accent2)}
61
+ .stat .l{font-size:.65rem;color:var(--fg3);margin-top:.2rem}
62
+
63
+ @media(max-width:768px){.grid{grid-template-columns:1fr;height:auto}.panel{min-height:50vh}.stats{grid-template-columns:repeat(2,1fr)}}
64
+ </style>
65
+ </head>
66
+ <body>
67
+ <div class="top">
68
+ <h1>⬡ <span>Halyn</span> Dashboard</h1>
69
+ <div class="status"><div class="dot"></div> <span id="uptime">connecting...</span></div>
70
+ </div>
71
+ <div class="grid">
72
+ <!-- LEFT: Chat + Command -->
73
+ <div class="panel">
74
+ <div class="chat">
75
+ <div class="messages" id="msgs">
76
+ <div class="msg sys">Welcome to Halyn. Type a command or talk naturally.<br><br>Try: <code>observe all</code> · <code>shield deny * delete *</code> · <code>status</code> · <code>audit</code></div>
77
+ </div>
78
+ <div class="input-row">
79
+ <input type="text" id="cmd" placeholder="Type a command... (e.g. 'restart nginx on server-01')" autofocus>
80
+ <button onclick="send()">Send</button>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ <!-- RIGHT: Status + Shields + Audit -->
85
+ <div class="panel">
86
+ <div class="stats" id="stats">
87
+ <div class="stat"><div class="v" id="s-nodes">0</div><div class="l">Nodes</div></div>
88
+ <div class="stat"><div class="v" id="s-shields">0</div><div class="l">Shields</div></div>
89
+ <div class="stat"><div class="v" id="s-audit">0</div><div class="l">Audit</div></div>
90
+ <div class="stat"><div class="v" id="s-uptime">0s</div><div class="l">Uptime</div></div>
91
+ </div>
92
+ <h2>🛡️ Shield Rules <span class="count" id="shield-count">0</span></h2>
93
+ <div id="shields"></div>
94
+ <h2 style="margin-top:1rem">📜 Audit Chain <span class="count" id="audit-count">0</span></h2>
95
+ <div id="audit"></div>
96
+ </div>
97
+ </div>
98
+ <script>
99
+ const $ = s => document.getElementById(s);
100
+
101
+ async function mcp(name, args={}) {
102
+ const r = await fetch('/mcp', {
103
+ method: 'POST',
104
+ headers: {'Content-Type': 'application/json'},
105
+ body: JSON.stringify({jsonrpc:'2.0',id:Date.now(),method:'tools/call',params:{name,arguments:args}})
106
+ });
107
+ const d = await r.json();
108
+ return JSON.parse(d.result.content[0].text);
109
+ }
110
+
111
+ function addMsg(text, cls='sys') {
112
+ const d = document.createElement('div');
113
+ d.className = 'msg ' + cls;
114
+ d.innerHTML = text;
115
+ $('msgs').appendChild(d);
116
+ $('msgs').scrollTop = $('msgs').scrollHeight;
117
+ }
118
+
119
+ async function refresh() {
120
+ try {
121
+ const s = await mcp('halyn_status');
122
+ $('s-nodes').textContent = s.nodes;
123
+ $('s-shields').textContent = s.shields;
124
+ $('s-audit').textContent = s.audit_entries;
125
+ $('s-uptime').textContent = s.uptime_seconds + 's';
126
+ $('uptime').textContent = 'v' + s.version + ' · ' + s.uptime_seconds + 's uptime';
127
+
128
+ const sh = await mcp('halyn_shield_list');
129
+ $('shield-count').textContent = sh.count;
130
+ $('shields').innerHTML = sh.shields.map(r => '<div class="shield">' + r + '</div>').join('');
131
+
132
+ const au = await mcp('halyn_audit', {limit: 20});
133
+ $('audit-count').textContent = au.total;
134
+ $('audit').innerHTML = au.entries.reverse().map(e => {
135
+ const badge = e.result_ok ? '<span class="badge ok">OK</span>' : '<span class="badge blocked">BLOCKED</span>';
136
+ return '<div class="audit-entry"><span class="time">' + e.timestamp.split('T')[1].replace('Z','') + '</span><span class="tool">' + e.tool + '</span>' + badge + '<span class="hash">' + e.hash + '</span></div>';
137
+ }).join('');
138
+ } catch(e) {}
139
+ }
140
+
141
+ async function send() {
142
+ const input = $('cmd');
143
+ const cmd = input.value.trim();
144
+ if (!cmd) return;
145
+ input.value = '';
146
+
147
+ addMsg(cmd, 'user');
148
+
149
+ const lower = cmd.toLowerCase();
150
+
151
+ try {
152
+ if (lower === 'status' || lower === 'state') {
153
+ const s = await mcp('halyn_status');
154
+ addMsg('Nodes: ' + s.nodes + ' · Shields: ' + s.shields + ' · Audit: ' + s.audit_entries + ' · Uptime: ' + s.uptime_seconds + 's', 'ok');
155
+ } else if (lower.startsWith('shield ') || lower.startsWith('deny ') || lower.startsWith('limit ')) {
156
+ const rule = lower.startsWith('shield ') ? cmd.slice(7) : cmd;
157
+ const r = await mcp('halyn_shield_add', {rule});
158
+ if (r.error) { addMsg('Error: ' + r.error, 'blocked'); }
159
+ else { addMsg('Shield added: ' + r.added + ' (total: ' + r.total_shields + ')', 'ok'); }
160
+ } else if (lower === 'shields' || lower === 'rules') {
161
+ const r = await mcp('halyn_shield_list');
162
+ addMsg(r.shields.length ? r.shields.map(s => '🛡️ ' + s).join('<br>') : 'No shields active.', 'sys');
163
+ } else if (lower === 'audit' || lower === 'log' || lower === 'history') {
164
+ const r = await mcp('halyn_audit', {limit: 10});
165
+ const lines = r.entries.map(e => {
166
+ const s = e.result_ok ? '✓' : '✗';
167
+ return s + ' ' + e.timestamp.split('T')[1].replace('Z','') + ' ' + e.tool;
168
+ });
169
+ addMsg(lines.join('<br>') || 'No audit entries yet.', 'sys');
170
+ } else if (lower.startsWith('observe') || lower.startsWith('watch') || lower.startsWith('read')) {
171
+ const node = cmd.split(' ').slice(1).join(' ') || 'all';
172
+ const r = await mcp('halyn_observe', {node});
173
+ addMsg('<pre>' + JSON.stringify(r, null, 2) + '</pre>', 'sys');
174
+ } else if (lower === 'stop' || lower === 'emergency') {
175
+ const r = await mcp('halyn_emergency_stop');
176
+ addMsg('⚠️ ' + r.status, 'blocked');
177
+ } else if (lower === 'nodes' || lower === 'devices') {
178
+ const r = await mcp('halyn_nodes');
179
+ addMsg('<pre>' + JSON.stringify(r, null, 2) + '</pre>', 'sys');
180
+ } else if (lower === 'help') {
181
+ addMsg('Commands:<br>• <b>status</b> — system overview<br>• <b>observe [node]</b> — read device state<br>• <b>shield deny * delete *</b> — add safety rule<br>• <b>shields</b> — list active rules<br>• <b>audit</b> — view action log<br>• <b>nodes</b> — list devices<br>• <b>stop</b> — emergency stop<br>• Or type any action: <i>restart nginx on server-01</i>', 'sys');
182
+ } else {
183
+ // Treat as action on default node
184
+ const parts = cmd.match(/(.+?)\\s+on\\s+(.+)/i);
185
+ const node = parts ? parts[2] : 'default';
186
+ const command = parts ? parts[1] : cmd;
187
+ const r = await mcp('halyn_act', {node, command});
188
+ if (r.blocked) {
189
+ addMsg('🛡️ BLOCKED: ' + r.reason, 'blocked');
190
+ } else {
191
+ addMsg('✓ Executed: ' + command + (node !== 'default' ? ' on ' + node : ''), 'ok');
192
+ }
193
+ }
194
+ } catch(e) {
195
+ addMsg('Error: ' + e.message, 'blocked');
196
+ }
197
+
198
+ refresh();
199
+ }
200
+
201
+ $('cmd').addEventListener('keydown', e => { if (e.key === 'Enter') send(); });
202
+ refresh();
203
+ setInterval(refresh, 5000);
204
+ </script>
205
+ </body>
206
+ </html>'''
@@ -30,6 +30,10 @@ log = logging.getLogger("halyn.mcp_serve")
30
30
 
31
31
  try:
32
32
  from aiohttp import web
33
+ try:
34
+ from halyn.dashboard import DASHBOARD_HTML
35
+ except ImportError:
36
+ DASHBOARD_HTML = "<h1>Halyn MCP Server</h1><p>Dashboard not available.</p>"
33
37
  from aiohttp.web import middleware
34
38
  except ImportError:
35
39
  print("pip install aiohttp # required for MCP server")
@@ -41,7 +45,7 @@ except ImportError:
41
45
 
42
46
  SERVER_INFO = {
43
47
  "name": "halyn",
44
- "version": "0.3.3",
48
+ "version": "0.3.4",
45
49
  }
46
50
 
47
51
  PROTOCOL_VERSION = "2024-11-05"
@@ -323,9 +327,13 @@ def create_app(config_path: str | None = None) -> web.Application:
323
327
  async def handle_health(request: web.Request) -> web.Response:
324
328
  return web.json_response({"status": "ok", "server": "halyn-mcp", "version": SERVER_INFO["version"]})
325
329
 
330
+ async def handle_dashboard(request: web.Request) -> web.Response:
331
+ return web.Response(text=DASHBOARD_HTML, content_type="text/html")
332
+
326
333
  app.router.add_post("/mcp", handle_mcp)
327
334
  app.router.add_get("/health", handle_health)
328
- app.router.add_get("/", handle_health)
335
+ app.router.add_get("/", handle_dashboard)
336
+ app.router.add_get("/api/health", handle_health)
329
337
 
330
338
  return app
331
339
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: halyn
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Halyn — NRP control plane with domain-scoped authorization.
5
5
  Author-email: Elmadani SALKA <contact@halyn.dev>
6
6
  License: MIT
@@ -10,6 +10,7 @@ src/halyn/cli.py
10
10
  src/halyn/config.py
11
11
  src/halyn/consent.py
12
12
  src/halyn/control_plane.py
13
+ src/halyn/dashboard.py
13
14
  src/halyn/discovery.py
14
15
  src/halyn/engine.py
15
16
  src/halyn/intent.py
@@ -1,51 +0,0 @@
1
- # Copyright (c) 2026 Elmadani SALKA. All rights reserved.
2
- # Licensed under the MIT License. See LICENSE file.
3
- """Halyn — Connect any device to any AI."""
4
-
5
- __version__ = "0.3.3"
6
- __author__ = "Elmadani SALKA"
7
- __license__ = "MIT"
8
-
9
- # Core — always available
10
- try:
11
- from halyn.control_plane import ControlPlane
12
- except ImportError:
13
- ControlPlane = None
14
-
15
- try:
16
- from halyn.autonomy import Autonomy
17
- except ImportError:
18
- Autonomy = None
19
-
20
- try:
21
- from halyn.audit import AuditChain
22
- except ImportError:
23
- AuditChain = None
24
-
25
- # Drivers — import safely (some have optional deps)
26
- def _safe_import(module, cls):
27
- try:
28
- mod = __import__(f"halyn.drivers.{module}", fromlist=[cls])
29
- return getattr(mod, cls)
30
- except (ImportError, AttributeError):
31
- return None
32
-
33
- SSHDriver = _safe_import("ssh", "SSHDriver")
34
- HTTPDriver = _safe_import("http_auto", "HTTPDriver")
35
- WebSocketDriver = _safe_import("websocket", "WebSocketDriver")
36
- SerialDriver = _safe_import("serial", "SerialDriver")
37
- MQTTDriver = _safe_import("mqtt", "MQTTDriver")
38
- OPCUADriver = _safe_import("opcua", "OPCUADriver")
39
- ROS2Driver = _safe_import("ros2", "ROS2Driver")
40
- DDSDriver = _safe_import("dds", "DDSDriver")
41
- DockerDriver = _safe_import("docker", "DockerDriver")
42
- BrowserDriver = _safe_import("browser", "BrowserDriver")
43
- UnitreeDriver = _safe_import("unitree", "UnitreeDriver")
44
- SocketDriver = _safe_import("socket_raw", "SocketDriver")
45
-
46
- __all__ = [
47
- "ControlPlane", "Autonomy", "AuditChain",
48
- "SSHDriver", "HTTPDriver", "WebSocketDriver", "SerialDriver",
49
- "MQTTDriver", "OPCUADriver", "ROS2Driver", "DDSDriver",
50
- "DockerDriver", "BrowserDriver", "UnitreeDriver", "SocketDriver",
51
- ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes