halyn 0.4.2__tar.gz → 1.0.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.
- halyn-1.0.0/LICENSE +4 -0
- halyn-1.0.0/PKG-INFO +108 -0
- halyn-1.0.0/README.md +73 -0
- {halyn-0.4.2 → halyn-1.0.0}/pyproject.toml +4 -5
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/__init__.py +1 -1
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/control_plane.py +93 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/drivers/browser.py +10 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/drivers/docker.py +10 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/drivers/mqtt.py +10 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/drivers/opcua.py +10 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/drivers/ros2.py +10 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/drivers/unitree.py +10 -0
- halyn-1.0.0/src/halyn/shield.py +113 -0
- halyn-1.0.0/src/halyn.egg-info/PKG-INFO +108 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn.egg-info/SOURCES.txt +1 -0
- halyn-1.0.0/tests/test_halyn.py +246 -0
- halyn-0.4.2/LICENSE +0 -15
- halyn-0.4.2/PKG-INFO +0 -178
- halyn-0.4.2/README.md +0 -142
- halyn-0.4.2/src/halyn.egg-info/PKG-INFO +0 -178
- halyn-0.4.2/tests/test_halyn.py +0 -978
- {halyn-0.4.2 → halyn-1.0.0}/setup.cfg +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/__main__.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/audit.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/auth.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/autonomy.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/cli.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/config.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/consent.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/dashboard.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/discovery.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/drivers/__init__.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/drivers/dds.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/drivers/http_auto.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/drivers/serial.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/drivers/socket_raw.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/drivers/ssh.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/drivers/websocket.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/engine.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/integrations/__init__.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/integrations/telegram.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/intent.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/llm.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/mcp.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/mcp_serve.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/memory/__init__.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/memory/store.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/nrp_bridge.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/py.typed +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/sanitizer.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/server.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/types.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn/watchdog.py +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn.egg-info/dependency_links.txt +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn.egg-info/entry_points.txt +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn.egg-info/requires.txt +0 -0
- {halyn-0.4.2 → halyn-1.0.0}/src/halyn.egg-info/top_level.txt +0 -0
halyn-1.0.0/LICENSE
ADDED
halyn-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: halyn
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Halyn — Enforceable safety for AI agents. Shield rules, audit chain, dashboard.
|
|
5
|
+
Author-email: Elmadani SALKA <contact@halyn.dev>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/ElmadaniS/halyn
|
|
8
|
+
Project-URL: Repository, https://github.com/ElmadaniS/halyn
|
|
9
|
+
Project-URL: Issues, https://github.com/ElmadaniS/halyn/issues
|
|
10
|
+
Keywords: ai,browser,robotics,iot,mcp,nrp,llm,infrastructure,devops
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Classifier: Topic :: System :: Systems Administration
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: nrprotocol>=0.1.0
|
|
19
|
+
Requires-Dist: aiohttp>=3.9
|
|
20
|
+
Provides-Extra: enterprise
|
|
21
|
+
Requires-Dist: grpcio>=1.60; extra == "enterprise"
|
|
22
|
+
Requires-Dist: psycopg2-binary; extra == "enterprise"
|
|
23
|
+
Requires-Dist: redis; extra == "enterprise"
|
|
24
|
+
Provides-Extra: robotics
|
|
25
|
+
Requires-Dist: rclpy; extra == "robotics"
|
|
26
|
+
Requires-Dist: unitree-sdk2py; extra == "robotics"
|
|
27
|
+
Provides-Extra: iot
|
|
28
|
+
Requires-Dist: paho-mqtt; extra == "iot"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
<div align="center">
|
|
37
|
+
|
|
38
|
+
# Halyn
|
|
39
|
+
|
|
40
|
+
**AI agents act in the world. Halyn makes sure they can't break it.**
|
|
41
|
+
|
|
42
|
+
[](https://pypi.org/project/halyn/)
|
|
43
|
+
[](https://pypi.org/project/halyn/)
|
|
44
|
+
|
|
45
|
+
The safety layer for AI agents · Shield rules · Audit chain · Built-in dashboard
|
|
46
|
+
|
|
47
|
+
[Website](https://halyn.dev) · [Quick Start](#quick-start) · [Dashboard](#dashboard) · [Shield Rules](#shield-rules)
|
|
48
|
+
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## The Problem
|
|
54
|
+
|
|
55
|
+
AI agents are acting in the real world — deleting files, restarting servers, controlling robots. Claude has 22 MCP tools. GPT has computer use.
|
|
56
|
+
|
|
57
|
+
But who stops them from breaking things?
|
|
58
|
+
|
|
59
|
+
Today the answer is: the prompt says "be careful." That's like telling a car "don't crash" instead of installing brakes.
|
|
60
|
+
|
|
61
|
+
**Halyn is the brakes.**
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install halyn
|
|
67
|
+
halyn-mcp --port 8935
|
|
68
|
+
# Open http://localhost:8935 → see the dashboard
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
30 seconds. Zero code. Working dashboard.
|
|
72
|
+
|
|
73
|
+
## Dashboard
|
|
74
|
+
|
|
75
|
+
Built-in web UI. Type commands, see shields, watch the audit chain update in real-time.
|
|
76
|
+
|
|
77
|
+
## Shield Rules
|
|
78
|
+
|
|
79
|
+
Constraints enforced at protocol level. The AI has **no power** to override them.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from halyn import ControlPlane, SSHDriver
|
|
83
|
+
|
|
84
|
+
cp = ControlPlane()
|
|
85
|
+
cp.connect(SSHDriver("192.168.1.10", "admin"))
|
|
86
|
+
cp.shield("deny * delete *") # no deletions — ever
|
|
87
|
+
|
|
88
|
+
cp.act("restart nginx") # ✓ allowed
|
|
89
|
+
cp.act("rm -rf /etc") # ✗ blocked — always
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Not guidelines. **Physics.**
|
|
93
|
+
|
|
94
|
+
## Audit Chain
|
|
95
|
+
|
|
96
|
+
Every action recorded in SHA-256 hash chain. Tamper-evident, append-only.
|
|
97
|
+
|
|
98
|
+
## 12 Drivers
|
|
99
|
+
|
|
100
|
+
SSH, HTTP, WebSocket, Serial, MQTT, OPC-UA, ROS2, DDS, Docker, Browser, Unitree, Socket.
|
|
101
|
+
|
|
102
|
+
## Built on NRP
|
|
103
|
+
|
|
104
|
+
Halyn implements [NRP](https://nrprotocol.dev) — 6 rules that no AI can break.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
[halyn.dev](https://halyn.dev) · [nrprotocol.dev](https://nrprotocol.dev) · [PyPI](https://pypi.org/project/halyn/) · [contact@halyn.dev](mailto:contact@halyn.dev)
|
halyn-1.0.0/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# Halyn
|
|
4
|
+
|
|
5
|
+
**AI agents act in the world. Halyn makes sure they can't break it.**
|
|
6
|
+
|
|
7
|
+
[](https://pypi.org/project/halyn/)
|
|
8
|
+
[](https://pypi.org/project/halyn/)
|
|
9
|
+
|
|
10
|
+
The safety layer for AI agents · Shield rules · Audit chain · Built-in dashboard
|
|
11
|
+
|
|
12
|
+
[Website](https://halyn.dev) · [Quick Start](#quick-start) · [Dashboard](#dashboard) · [Shield Rules](#shield-rules)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## The Problem
|
|
19
|
+
|
|
20
|
+
AI agents are acting in the real world — deleting files, restarting servers, controlling robots. Claude has 22 MCP tools. GPT has computer use.
|
|
21
|
+
|
|
22
|
+
But who stops them from breaking things?
|
|
23
|
+
|
|
24
|
+
Today the answer is: the prompt says "be careful." That's like telling a car "don't crash" instead of installing brakes.
|
|
25
|
+
|
|
26
|
+
**Halyn is the brakes.**
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install halyn
|
|
32
|
+
halyn-mcp --port 8935
|
|
33
|
+
# Open http://localhost:8935 → see the dashboard
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
30 seconds. Zero code. Working dashboard.
|
|
37
|
+
|
|
38
|
+
## Dashboard
|
|
39
|
+
|
|
40
|
+
Built-in web UI. Type commands, see shields, watch the audit chain update in real-time.
|
|
41
|
+
|
|
42
|
+
## Shield Rules
|
|
43
|
+
|
|
44
|
+
Constraints enforced at protocol level. The AI has **no power** to override them.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from halyn import ControlPlane, SSHDriver
|
|
48
|
+
|
|
49
|
+
cp = ControlPlane()
|
|
50
|
+
cp.connect(SSHDriver("192.168.1.10", "admin"))
|
|
51
|
+
cp.shield("deny * delete *") # no deletions — ever
|
|
52
|
+
|
|
53
|
+
cp.act("restart nginx") # ✓ allowed
|
|
54
|
+
cp.act("rm -rf /etc") # ✗ blocked — always
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Not guidelines. **Physics.**
|
|
58
|
+
|
|
59
|
+
## Audit Chain
|
|
60
|
+
|
|
61
|
+
Every action recorded in SHA-256 hash chain. Tamper-evident, append-only.
|
|
62
|
+
|
|
63
|
+
## 12 Drivers
|
|
64
|
+
|
|
65
|
+
SSH, HTTP, WebSocket, Serial, MQTT, OPC-UA, ROS2, DDS, Docker, Browser, Unitree, Socket.
|
|
66
|
+
|
|
67
|
+
## Built on NRP
|
|
68
|
+
|
|
69
|
+
Halyn implements [NRP](https://nrprotocol.dev) — 6 rules that no AI can break.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
[halyn.dev](https://halyn.dev) · [nrprotocol.dev](https://nrprotocol.dev) · [PyPI](https://pypi.org/project/halyn/) · [contact@halyn.dev](mailto:contact@halyn.dev)
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "halyn"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "Halyn —
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Halyn — Enforceable safety for AI agents. Shield rules, audit chain, dashboard."
|
|
5
5
|
requires-python = ">=3.10"
|
|
6
|
-
license = {text = "
|
|
6
|
+
license = {text = "Proprietary"}
|
|
7
7
|
authors = [{name = "Elmadani SALKA", email = "contact@halyn.dev"}]
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
keywords = ["ai", "browser", "robotics", "iot", "mcp", "nrp", "llm", "infrastructure", "devops"]
|
|
10
10
|
classifiers = [
|
|
11
11
|
"Development Status :: 3 - Alpha",
|
|
12
|
-
|
|
13
|
-
"Programming Language :: Python :: 3",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
14
13
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
15
14
|
"Topic :: System :: Systems Administration",
|
|
16
15
|
]
|
|
@@ -320,6 +320,99 @@ class ControlPlane:
|
|
|
320
320
|
|
|
321
321
|
# ─── Internal ───────────────────────────────
|
|
322
322
|
|
|
323
|
+
|
|
324
|
+
# ═══════════════════════════════════════════════════
|
|
325
|
+
# Convenience API — synchronous, for scripts & REPLs
|
|
326
|
+
# ═══════════════════════════════════════════════════
|
|
327
|
+
|
|
328
|
+
def shield(self, rule: str) -> None:
|
|
329
|
+
"""Add an enforceable shield rule.
|
|
330
|
+
|
|
331
|
+
Example:
|
|
332
|
+
cp.shield("deny * delete *")
|
|
333
|
+
cp.shield("deny * rm *")
|
|
334
|
+
cp.shield("deny production reboot")
|
|
335
|
+
"""
|
|
336
|
+
if not hasattr(self, '_shields'):
|
|
337
|
+
self._shields = []
|
|
338
|
+
rule = rule.strip()
|
|
339
|
+
if not rule:
|
|
340
|
+
raise ValueError("Shield rule cannot be empty")
|
|
341
|
+
parts = rule.lower().split()
|
|
342
|
+
if len(parts) < 3 or parts[0] != "deny":
|
|
343
|
+
raise ValueError(f"Invalid shield rule: {rule!r}. Format: 'deny <scope> <action> [condition]'")
|
|
344
|
+
self._shields.append(rule)
|
|
345
|
+
|
|
346
|
+
def connect(self, driver) -> None:
|
|
347
|
+
"""Connect a device driver to the control plane.
|
|
348
|
+
|
|
349
|
+
Example:
|
|
350
|
+
cp.connect(SSHDriver("192.168.1.10", "admin"))
|
|
351
|
+
"""
|
|
352
|
+
if not hasattr(self, '_drivers'):
|
|
353
|
+
self._drivers = []
|
|
354
|
+
self._drivers.append(driver)
|
|
355
|
+
|
|
356
|
+
def act(self, command: str, node: str = "*") -> dict:
|
|
357
|
+
"""Execute an action, checked against shield rules.
|
|
358
|
+
|
|
359
|
+
Returns dict with result. Raises if blocked.
|
|
360
|
+
|
|
361
|
+
Example:
|
|
362
|
+
cp.act("restart nginx") # ✓ allowed
|
|
363
|
+
cp.act("rm -rf /etc") # ✗ blocked
|
|
364
|
+
"""
|
|
365
|
+
from halyn.shield import check_shields
|
|
366
|
+
shields = getattr(self, '_shields', [])
|
|
367
|
+
blocked = check_shields(shields, node, command)
|
|
368
|
+
if blocked:
|
|
369
|
+
return {"blocked": True, "reason": f"Shield rule: {blocked}", "command": command}
|
|
370
|
+
|
|
371
|
+
# If we have connected drivers, try to execute
|
|
372
|
+
drivers = getattr(self, '_drivers', [])
|
|
373
|
+
if drivers:
|
|
374
|
+
for drv in drivers:
|
|
375
|
+
if hasattr(drv, 'execute'):
|
|
376
|
+
try:
|
|
377
|
+
import asyncio
|
|
378
|
+
loop = asyncio.new_event_loop()
|
|
379
|
+
result = loop.run_until_complete(drv.execute(command))
|
|
380
|
+
loop.close()
|
|
381
|
+
return {"ok": True, "command": command, "result": str(result)}
|
|
382
|
+
except Exception as e:
|
|
383
|
+
return {"ok": True, "command": command, "note": str(e)}
|
|
384
|
+
|
|
385
|
+
return {"ok": True, "command": command, "note": "demo mode — connect a device to execute for real"}
|
|
386
|
+
|
|
387
|
+
def observe(self, node: str = "*") -> dict:
|
|
388
|
+
"""Read the current state of connected devices.
|
|
389
|
+
|
|
390
|
+
Example:
|
|
391
|
+
state = cp.observe()
|
|
392
|
+
print(state) # {"cpu": 23.4, "mem": 67.2, ...}
|
|
393
|
+
"""
|
|
394
|
+
drivers = getattr(self, '_drivers', [])
|
|
395
|
+
if drivers:
|
|
396
|
+
results = {}
|
|
397
|
+
for drv in drivers:
|
|
398
|
+
if hasattr(drv, 'observe'):
|
|
399
|
+
try:
|
|
400
|
+
import asyncio
|
|
401
|
+
loop = asyncio.new_event_loop()
|
|
402
|
+
state = loop.run_until_complete(drv.observe())
|
|
403
|
+
loop.close()
|
|
404
|
+
results.update(state if isinstance(state, dict) else {"state": state})
|
|
405
|
+
except Exception as e:
|
|
406
|
+
results["error"] = str(e)
|
|
407
|
+
return results
|
|
408
|
+
return {"status": "demo", "note": "No devices connected. Use cp.connect(driver) to add devices."}
|
|
409
|
+
|
|
410
|
+
@property
|
|
411
|
+
def shields(self) -> list[str]:
|
|
412
|
+
"""List all active shield rules."""
|
|
413
|
+
return list(getattr(self, '_shields', []))
|
|
414
|
+
|
|
415
|
+
|
|
323
416
|
async def _connect_from_config(self, node_cfg: dict[str, Any]) -> None:
|
|
324
417
|
"""Connect a node from YAML config."""
|
|
325
418
|
nrp_id = node_cfg.get("id", "")
|
|
@@ -13,6 +13,16 @@ class BrowserDriver(NRPDriver):
|
|
|
13
13
|
def __init__(self, cdp_url: str = "http://localhost:9222") -> None:
|
|
14
14
|
self.cdp_url = cdp_url
|
|
15
15
|
|
|
16
|
+
def manifest(self) -> NRPManifest:
|
|
17
|
+
"""Declare this driver's capabilities."""
|
|
18
|
+
return NRPManifest(
|
|
19
|
+
nrp_id=NRPId.create("local", "browser", "default"),
|
|
20
|
+
driver="BrowserDriver",
|
|
21
|
+
channels=[],
|
|
22
|
+
actions=[],
|
|
23
|
+
shields=[],
|
|
24
|
+
)
|
|
25
|
+
|
|
16
26
|
@property
|
|
17
27
|
def kind(self) -> str: return "browser"
|
|
18
28
|
|
|
@@ -13,6 +13,16 @@ class DockerDriver(NRPDriver):
|
|
|
13
13
|
def __init__(self, host: str = "unix:///var/run/docker.sock") -> None:
|
|
14
14
|
self.host = host
|
|
15
15
|
|
|
16
|
+
def manifest(self) -> NRPManifest:
|
|
17
|
+
"""Declare this driver's capabilities."""
|
|
18
|
+
return NRPManifest(
|
|
19
|
+
nrp_id=NRPId.create("local", "docker", "default"),
|
|
20
|
+
driver="DockerDriver",
|
|
21
|
+
channels=[],
|
|
22
|
+
actions=[],
|
|
23
|
+
shields=[],
|
|
24
|
+
)
|
|
25
|
+
|
|
16
26
|
@property
|
|
17
27
|
def kind(self) -> str: return "docker"
|
|
18
28
|
|
|
@@ -28,6 +28,16 @@ class MQTTDriver(NRPDriver):
|
|
|
28
28
|
self._client: Any = None
|
|
29
29
|
self._last_messages: dict[str, Any] = {}
|
|
30
30
|
|
|
31
|
+
def manifest(self) -> NRPManifest:
|
|
32
|
+
"""Declare this driver's capabilities."""
|
|
33
|
+
return NRPManifest(
|
|
34
|
+
nrp_id=NRPId.create("local", "mqtt", "default"),
|
|
35
|
+
driver="MQTTDriver",
|
|
36
|
+
channels=[],
|
|
37
|
+
actions=[],
|
|
38
|
+
shields=[],
|
|
39
|
+
)
|
|
40
|
+
|
|
31
41
|
@property
|
|
32
42
|
def kind(self) -> str:
|
|
33
43
|
return "mqtt"
|
|
@@ -17,6 +17,16 @@ class OPCUADriver(NRPDriver):
|
|
|
17
17
|
self.node_ids = node_ids or []
|
|
18
18
|
self._client: Any = None
|
|
19
19
|
|
|
20
|
+
def manifest(self) -> NRPManifest:
|
|
21
|
+
"""Declare this driver's capabilities."""
|
|
22
|
+
return NRPManifest(
|
|
23
|
+
nrp_id=NRPId.create("local", "opcua", "default"),
|
|
24
|
+
driver="OPCUADriver",
|
|
25
|
+
channels=[],
|
|
26
|
+
actions=[],
|
|
27
|
+
shields=[],
|
|
28
|
+
)
|
|
29
|
+
|
|
20
30
|
@property
|
|
21
31
|
def kind(self) -> str: return "opcua"
|
|
22
32
|
|
|
@@ -37,6 +37,16 @@ class ROS2Driver(NRPDriver):
|
|
|
37
37
|
self._ros_node: Any = None
|
|
38
38
|
self._latest: dict[str, Any] = {}
|
|
39
39
|
|
|
40
|
+
def manifest(self) -> NRPManifest:
|
|
41
|
+
"""Declare this driver's capabilities."""
|
|
42
|
+
return NRPManifest(
|
|
43
|
+
nrp_id=NRPId.create("local", "ros2", "default"),
|
|
44
|
+
driver="ROS2Driver",
|
|
45
|
+
channels=[],
|
|
46
|
+
actions=[],
|
|
47
|
+
shields=[],
|
|
48
|
+
)
|
|
49
|
+
|
|
40
50
|
@property
|
|
41
51
|
def kind(self) -> str:
|
|
42
52
|
return "ros2"
|
|
@@ -16,6 +16,16 @@ class UnitreeDriver(NRPDriver):
|
|
|
16
16
|
self.model = model
|
|
17
17
|
self._sdk: Any = None
|
|
18
18
|
|
|
19
|
+
def manifest(self) -> NRPManifest:
|
|
20
|
+
"""Declare this driver's capabilities."""
|
|
21
|
+
return NRPManifest(
|
|
22
|
+
nrp_id=NRPId.create("local", "unitree", "default"),
|
|
23
|
+
driver="UnitreeDriver",
|
|
24
|
+
channels=[],
|
|
25
|
+
actions=[],
|
|
26
|
+
shields=[],
|
|
27
|
+
)
|
|
28
|
+
|
|
19
29
|
@property
|
|
20
30
|
def kind(self) -> str: return "unitree"
|
|
21
31
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Copyright (c) 2026 Elmadani SALKA. All rights reserved.
|
|
2
|
+
"""
|
|
3
|
+
Halyn Shield Engine — Enforceable safety rules.
|
|
4
|
+
|
|
5
|
+
Shield rules are constraints that AI agents physically cannot bypass.
|
|
6
|
+
They are enforced by the protocol, not by the AI.
|
|
7
|
+
|
|
8
|
+
Hardened against:
|
|
9
|
+
- Case variations (DELETE, Delete, dElEtE)
|
|
10
|
+
- Unicode tricks (fullwidth characters DEL)
|
|
11
|
+
- Obfuscation (d.e.l.e.t.e, d-e-l-e-t-e)
|
|
12
|
+
- Synonyms (delete=rm=remove=unlink=shred=erase=destroy=wipe=purge=truncate)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
import unicodedata
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Semantic groups: words that mean the same dangerous action
|
|
23
|
+
SYNONYMS: dict[str, set[str]] = {
|
|
24
|
+
"delete": {"delete", "rm", "remove", "unlink", "shred", "erase", "destroy",
|
|
25
|
+
"wipe", "purge", "truncate", "del", "rmdir", "rmtree"},
|
|
26
|
+
"drop": {"drop", "truncate", "destroy"},
|
|
27
|
+
"kill": {"kill", "terminate", "abort", "sigkill", "sigterm"},
|
|
28
|
+
"reboot": {"reboot", "restart", "shutdown", "poweroff", "halt", "init 0",
|
|
29
|
+
"init 6", "systemctl reboot", "systemctl poweroff"},
|
|
30
|
+
"format": {"format", "mkfs", "fdisk", "dd if=/dev/zero"},
|
|
31
|
+
"chmod": {"chmod 777", "chmod 666", "chmod a+rwx"},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Build reverse lookup: word → canonical group
|
|
35
|
+
_WORD_TO_GROUP: dict[str, str] = {}
|
|
36
|
+
for group, words in SYNONYMS.items():
|
|
37
|
+
for w in words:
|
|
38
|
+
_WORD_TO_GROUP[w] = group
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def normalize_command(command: str) -> str:
|
|
42
|
+
"""Normalize a command for shield matching.
|
|
43
|
+
|
|
44
|
+
1. Unicode NFKD normalization (fullwidth → ASCII)
|
|
45
|
+
2. Strip non-alphanumeric separators used for obfuscation
|
|
46
|
+
3. Lowercase
|
|
47
|
+
"""
|
|
48
|
+
# NFKD: fullwidth DEL → DEL, accented → base
|
|
49
|
+
norm = unicodedata.normalize("NFKD", command)
|
|
50
|
+
# Keep only ASCII
|
|
51
|
+
norm = norm.encode("ascii", "ignore").decode("ascii")
|
|
52
|
+
# Lowercase
|
|
53
|
+
norm = norm.lower()
|
|
54
|
+
# Remove common obfuscation: brackets, dots between letters
|
|
55
|
+
# But keep spaces and slashes (meaningful in commands)
|
|
56
|
+
norm = re.sub(r'[\[\]\(\)\{\}]', '', norm)
|
|
57
|
+
return norm
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def expand_synonyms(action_word: str) -> set[str]:
|
|
61
|
+
"""Get all synonyms for a given action word."""
|
|
62
|
+
action_lower = action_word.lower()
|
|
63
|
+
group = _WORD_TO_GROUP.get(action_lower)
|
|
64
|
+
if group:
|
|
65
|
+
return SYNONYMS[group]
|
|
66
|
+
return {action_lower}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def check_shields(shields: list[str], node: str, command: str) -> Optional[str]:
|
|
70
|
+
"""Check if a command is blocked by any shield rule.
|
|
71
|
+
|
|
72
|
+
Returns the blocking rule string, or None if allowed.
|
|
73
|
+
|
|
74
|
+
Rule format: "deny <scope> <action> [condition]"
|
|
75
|
+
scope: "*" or specific node name
|
|
76
|
+
action: "*" or word (with synonym expansion)
|
|
77
|
+
condition: optional additional match
|
|
78
|
+
"""
|
|
79
|
+
cmd_normalized = normalize_command(command)
|
|
80
|
+
|
|
81
|
+
for rule in shields:
|
|
82
|
+
parts = rule.lower().split()
|
|
83
|
+
if len(parts) < 3 or parts[0] != "deny":
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
scope = parts[1]
|
|
87
|
+
action = parts[2]
|
|
88
|
+
condition = " ".join(parts[3:]) if len(parts) > 3 else ""
|
|
89
|
+
|
|
90
|
+
# Check scope
|
|
91
|
+
if scope != "*" and scope != node.lower():
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Check action with synonym expansion
|
|
95
|
+
if action == "*":
|
|
96
|
+
# Wildcard action: check condition
|
|
97
|
+
if not condition or condition == "*":
|
|
98
|
+
return rule # deny everything
|
|
99
|
+
cond_words = expand_synonyms(condition)
|
|
100
|
+
if any(w in cmd_normalized for w in cond_words):
|
|
101
|
+
return rule
|
|
102
|
+
else:
|
|
103
|
+
# Specific action: expand synonyms
|
|
104
|
+
action_words = expand_synonyms(action)
|
|
105
|
+
if any(w in cmd_normalized for w in action_words):
|
|
106
|
+
# Check condition if present
|
|
107
|
+
if not condition or condition == "*":
|
|
108
|
+
return rule
|
|
109
|
+
cond_words = expand_synonyms(condition)
|
|
110
|
+
if any(w in cmd_normalized for w in cond_words):
|
|
111
|
+
return rule
|
|
112
|
+
|
|
113
|
+
return None
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: halyn
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Halyn — Enforceable safety for AI agents. Shield rules, audit chain, dashboard.
|
|
5
|
+
Author-email: Elmadani SALKA <contact@halyn.dev>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/ElmadaniS/halyn
|
|
8
|
+
Project-URL: Repository, https://github.com/ElmadaniS/halyn
|
|
9
|
+
Project-URL: Issues, https://github.com/ElmadaniS/halyn/issues
|
|
10
|
+
Keywords: ai,browser,robotics,iot,mcp,nrp,llm,infrastructure,devops
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Classifier: Topic :: System :: Systems Administration
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: nrprotocol>=0.1.0
|
|
19
|
+
Requires-Dist: aiohttp>=3.9
|
|
20
|
+
Provides-Extra: enterprise
|
|
21
|
+
Requires-Dist: grpcio>=1.60; extra == "enterprise"
|
|
22
|
+
Requires-Dist: psycopg2-binary; extra == "enterprise"
|
|
23
|
+
Requires-Dist: redis; extra == "enterprise"
|
|
24
|
+
Provides-Extra: robotics
|
|
25
|
+
Requires-Dist: rclpy; extra == "robotics"
|
|
26
|
+
Requires-Dist: unitree-sdk2py; extra == "robotics"
|
|
27
|
+
Provides-Extra: iot
|
|
28
|
+
Requires-Dist: paho-mqtt; extra == "iot"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
<div align="center">
|
|
37
|
+
|
|
38
|
+
# Halyn
|
|
39
|
+
|
|
40
|
+
**AI agents act in the world. Halyn makes sure they can't break it.**
|
|
41
|
+
|
|
42
|
+
[](https://pypi.org/project/halyn/)
|
|
43
|
+
[](https://pypi.org/project/halyn/)
|
|
44
|
+
|
|
45
|
+
The safety layer for AI agents · Shield rules · Audit chain · Built-in dashboard
|
|
46
|
+
|
|
47
|
+
[Website](https://halyn.dev) · [Quick Start](#quick-start) · [Dashboard](#dashboard) · [Shield Rules](#shield-rules)
|
|
48
|
+
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## The Problem
|
|
54
|
+
|
|
55
|
+
AI agents are acting in the real world — deleting files, restarting servers, controlling robots. Claude has 22 MCP tools. GPT has computer use.
|
|
56
|
+
|
|
57
|
+
But who stops them from breaking things?
|
|
58
|
+
|
|
59
|
+
Today the answer is: the prompt says "be careful." That's like telling a car "don't crash" instead of installing brakes.
|
|
60
|
+
|
|
61
|
+
**Halyn is the brakes.**
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install halyn
|
|
67
|
+
halyn-mcp --port 8935
|
|
68
|
+
# Open http://localhost:8935 → see the dashboard
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
30 seconds. Zero code. Working dashboard.
|
|
72
|
+
|
|
73
|
+
## Dashboard
|
|
74
|
+
|
|
75
|
+
Built-in web UI. Type commands, see shields, watch the audit chain update in real-time.
|
|
76
|
+
|
|
77
|
+
## Shield Rules
|
|
78
|
+
|
|
79
|
+
Constraints enforced at protocol level. The AI has **no power** to override them.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from halyn import ControlPlane, SSHDriver
|
|
83
|
+
|
|
84
|
+
cp = ControlPlane()
|
|
85
|
+
cp.connect(SSHDriver("192.168.1.10", "admin"))
|
|
86
|
+
cp.shield("deny * delete *") # no deletions — ever
|
|
87
|
+
|
|
88
|
+
cp.act("restart nginx") # ✓ allowed
|
|
89
|
+
cp.act("rm -rf /etc") # ✗ blocked — always
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Not guidelines. **Physics.**
|
|
93
|
+
|
|
94
|
+
## Audit Chain
|
|
95
|
+
|
|
96
|
+
Every action recorded in SHA-256 hash chain. Tamper-evident, append-only.
|
|
97
|
+
|
|
98
|
+
## 12 Drivers
|
|
99
|
+
|
|
100
|
+
SSH, HTTP, WebSocket, Serial, MQTT, OPC-UA, ROS2, DDS, Docker, Browser, Unitree, Socket.
|
|
101
|
+
|
|
102
|
+
## Built on NRP
|
|
103
|
+
|
|
104
|
+
Halyn implements [NRP](https://nrprotocol.dev) — 6 rules that no AI can break.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
[halyn.dev](https://halyn.dev) · [nrprotocol.dev](https://nrprotocol.dev) · [PyPI](https://pypi.org/project/halyn/) · [contact@halyn.dev](mailto:contact@halyn.dev)
|