halyn 2.2.3__tar.gz → 2.2.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.
- {halyn-2.2.3/src/halyn.egg-info → halyn-2.2.5}/PKG-INFO +32 -23
- {halyn-2.2.3 → halyn-2.2.5}/README.md +29 -19
- {halyn-2.2.3 → halyn-2.2.5}/pyproject.toml +3 -4
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/__init__.py +1 -1
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/dashboard.py +23 -0
- halyn-2.2.5/src/halyn/redteam.py +186 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/security/proxy.py +0 -2
- {halyn-2.2.3 → halyn-2.2.5/src/halyn.egg-info}/PKG-INFO +32 -23
- halyn-2.2.3/src/halyn/redteam.py +0 -153
- {halyn-2.2.3 → halyn-2.2.5}/LICENSE +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/setup.cfg +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/__main__.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/_nrp/__init__.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/_nrp/driver.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/_nrp/events.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/_nrp/identity.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/_nrp/manifest.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/audit.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/auth.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/autonomy.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/cli.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/config.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/consent.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/control_plane.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/discovery.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/drivers/__init__.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/drivers/browser.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/drivers/dds.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/drivers/docker.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/drivers/http_auto.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/drivers/mqtt.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/drivers/opcua.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/drivers/ros2.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/drivers/serial.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/drivers/socket_raw.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/drivers/ssh.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/drivers/unitree.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/drivers/websocket.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/engine.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/integrations/__init__.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/intent.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/llm.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/mcp.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/mcp_serve.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/memory/__init__.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/memory/store.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/nrp_bridge.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/py.typed +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/sanitizer.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/security/__init__.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/security/audit_guard.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/security/ebpf_monitor.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/security/fs_watch.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/security/process_guard.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/server.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/shield.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/types.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn/watchdog.py +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn.egg-info/SOURCES.txt +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn.egg-info/dependency_links.txt +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn.egg-info/entry_points.txt +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn.egg-info/requires.txt +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/src/halyn.egg-info/top_level.txt +0 -0
- {halyn-2.2.3 → halyn-2.2.5}/tests/test_halyn.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: halyn
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.5
|
|
4
4
|
Summary: Halyn — The governance layer for AI agents. Every action intercepted. Every decision auditable.
|
|
5
5
|
Author-email: Elmadani SALKA <contact@halyn.dev>
|
|
6
|
-
License
|
|
6
|
+
License: BUSL-1.1
|
|
7
7
|
Project-URL: Homepage, https://halyn.dev
|
|
8
8
|
Project-URL: Repository, https://github.com/halyndev/halyn
|
|
9
9
|
Project-URL: Issues, https://github.com/halyndev/halyn/issues
|
|
@@ -29,7 +29,6 @@ Requires-Dist: pytest; extra == "dev"
|
|
|
29
29
|
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
30
30
|
Requires-Dist: mypy; extra == "dev"
|
|
31
31
|
Requires-Dist: ruff; extra == "dev"
|
|
32
|
-
Dynamic: license-file
|
|
33
32
|
|
|
34
33
|
<div align="center">
|
|
35
34
|
|
|
@@ -83,7 +82,7 @@ Every action produces a cryptographic proof stored locally. Not in the cloud. No
|
|
|
83
82
|
**Option 1 — pip** (Python 3.10+):
|
|
84
83
|
|
|
85
84
|
```bash
|
|
86
|
-
pip install halyn==2.
|
|
85
|
+
pip install halyn==2.2.4
|
|
87
86
|
halyn serve
|
|
88
87
|
```
|
|
89
88
|
|
|
@@ -101,23 +100,33 @@ The curl script verifies your Python version and asks permission before doing an
|
|
|
101
100
|
## Quick Start
|
|
102
101
|
|
|
103
102
|
```python
|
|
104
|
-
from halyn import ControlPlane
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
103
|
+
from halyn.control_plane import ControlPlane
|
|
104
|
+
from halyn.config import HalynConfig
|
|
105
|
+
|
|
106
|
+
# Start the control plane (or run `halyn serve` from CLI)
|
|
107
|
+
cp = ControlPlane(HalynConfig())
|
|
108
|
+
|
|
109
|
+
# Every node action passes through the pipeline:
|
|
110
|
+
# Consent → Shield → Execute → Audit
|
|
111
|
+
import asyncio
|
|
112
|
+
async def main():
|
|
113
|
+
await cp.start()
|
|
114
|
+
result = await cp.execute(
|
|
115
|
+
"myserver.observe",
|
|
116
|
+
{"channels": "cpu,ram"},
|
|
117
|
+
user_id="me",
|
|
118
|
+
intent_text="Check server load",
|
|
119
|
+
)
|
|
120
|
+
print(result.ok) # True
|
|
121
|
+
print(result.data) # {"cpu": 42.1, "ram": 67.3}
|
|
122
|
+
|
|
123
|
+
# Audit chain — cryptographic proof of every action
|
|
124
|
+
entries = cp.audit.query(limit=5)
|
|
125
|
+
valid, count, msg = cp.audit.verify_chain()
|
|
126
|
+
print(msg) # "Chain valid (N entries)"
|
|
127
|
+
await cp.stop()
|
|
128
|
+
|
|
129
|
+
asyncio.run(main())
|
|
121
130
|
```
|
|
122
131
|
|
|
123
132
|
---
|
|
@@ -228,7 +237,7 @@ Halyn intercepts any agentic system. The agent framework doesn't matter.
|
|
|
228
237
|
| **AutoGen** | API calls intercepted automatically |
|
|
229
238
|
| **CrewAI** | API calls intercepted automatically |
|
|
230
239
|
| **Semantic Kernel** | API calls intercepted automatically |
|
|
231
|
-
| **
|
|
240
|
+
| **Any AAP client** | Native AAP integration |
|
|
232
241
|
| **Any MCP agent** | MCP server passthrough |
|
|
233
242
|
| **Any A2A agent** | Network-level interception |
|
|
234
243
|
| **Any OpenAI-compatible API** | Universal proxy compatibility |
|
|
@@ -50,7 +50,7 @@ Every action produces a cryptographic proof stored locally. Not in the cloud. No
|
|
|
50
50
|
**Option 1 — pip** (Python 3.10+):
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
pip install halyn==2.
|
|
53
|
+
pip install halyn==2.2.4
|
|
54
54
|
halyn serve
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -68,23 +68,33 @@ The curl script verifies your Python version and asks permission before doing an
|
|
|
68
68
|
## Quick Start
|
|
69
69
|
|
|
70
70
|
```python
|
|
71
|
-
from halyn import ControlPlane
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
71
|
+
from halyn.control_plane import ControlPlane
|
|
72
|
+
from halyn.config import HalynConfig
|
|
73
|
+
|
|
74
|
+
# Start the control plane (or run `halyn serve` from CLI)
|
|
75
|
+
cp = ControlPlane(HalynConfig())
|
|
76
|
+
|
|
77
|
+
# Every node action passes through the pipeline:
|
|
78
|
+
# Consent → Shield → Execute → Audit
|
|
79
|
+
import asyncio
|
|
80
|
+
async def main():
|
|
81
|
+
await cp.start()
|
|
82
|
+
result = await cp.execute(
|
|
83
|
+
"myserver.observe",
|
|
84
|
+
{"channels": "cpu,ram"},
|
|
85
|
+
user_id="me",
|
|
86
|
+
intent_text="Check server load",
|
|
87
|
+
)
|
|
88
|
+
print(result.ok) # True
|
|
89
|
+
print(result.data) # {"cpu": 42.1, "ram": 67.3}
|
|
90
|
+
|
|
91
|
+
# Audit chain — cryptographic proof of every action
|
|
92
|
+
entries = cp.audit.query(limit=5)
|
|
93
|
+
valid, count, msg = cp.audit.verify_chain()
|
|
94
|
+
print(msg) # "Chain valid (N entries)"
|
|
95
|
+
await cp.stop()
|
|
96
|
+
|
|
97
|
+
asyncio.run(main())
|
|
88
98
|
```
|
|
89
99
|
|
|
90
100
|
---
|
|
@@ -195,7 +205,7 @@ Halyn intercepts any agentic system. The agent framework doesn't matter.
|
|
|
195
205
|
| **AutoGen** | API calls intercepted automatically |
|
|
196
206
|
| **CrewAI** | API calls intercepted automatically |
|
|
197
207
|
| **Semantic Kernel** | API calls intercepted automatically |
|
|
198
|
-
| **
|
|
208
|
+
| **Any AAP client** | Native AAP integration |
|
|
199
209
|
| **Any MCP agent** | MCP server passthrough |
|
|
200
210
|
| **Any A2A agent** | Network-level interception |
|
|
201
211
|
| **Any OpenAI-compatible API** | Universal proxy compatibility |
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "halyn"
|
|
3
|
-
version = "2.2.
|
|
3
|
+
version = "2.2.5"
|
|
4
4
|
description = "Halyn — The governance layer for AI agents. Every action intercepted. Every decision auditable."
|
|
5
5
|
requires-python = ">=3.10"
|
|
6
|
-
license = "BUSL-1.1"
|
|
7
|
-
license-files = ["LICENSE"]
|
|
6
|
+
license = {text = "BUSL-1.1"}
|
|
8
7
|
authors = [{name = "Elmadani SALKA", email = "contact@halyn.dev"}]
|
|
9
8
|
readme = "README.md"
|
|
10
9
|
keywords = [
|
|
@@ -46,7 +45,7 @@ where = ["src"]
|
|
|
46
45
|
halyn = ["py.typed"]
|
|
47
46
|
|
|
48
47
|
[build-system]
|
|
49
|
-
requires = ["setuptools>=77", "wheel"]
|
|
48
|
+
requires = ["setuptools>=61,<77", "wheel"]
|
|
50
49
|
build-backend = "setuptools.build_meta"
|
|
51
50
|
|
|
52
51
|
[tool.ruff]
|
|
@@ -181,6 +181,29 @@ body{background:var(--bg);color:var(--fg);font-family:var(--font);height:100vh;d
|
|
|
181
181
|
::-webkit-scrollbar{width:4px}
|
|
182
182
|
::-webkit-scrollbar-track{background:transparent}
|
|
183
183
|
::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
|
|
184
|
+
|
|
185
|
+
/* ── MOBILE ─────────────────────────────────────────── */
|
|
186
|
+
@media(max-width:700px){
|
|
187
|
+
body{height:auto;overflow:auto}
|
|
188
|
+
.topbar{padding:.5rem .75rem}
|
|
189
|
+
.version{display:none}
|
|
190
|
+
#estop-btn{font-size:.65rem;padding:.2rem .5rem}
|
|
191
|
+
.statstrip{flex-wrap:wrap}
|
|
192
|
+
.stat{flex:1 1 33%;min-width:0;padding:.5rem .6rem;border-bottom:1px solid var(--border)}
|
|
193
|
+
.stat .v{font-size:1.1rem}
|
|
194
|
+
.stat .l{font-size:.6rem}
|
|
195
|
+
.main{grid-template-columns:1fr;overflow:visible}
|
|
196
|
+
.col{border-right:none;border-bottom:1px solid var(--border);overflow:visible}
|
|
197
|
+
.col:last-child{border-bottom:none}
|
|
198
|
+
.panel{overflow:visible}
|
|
199
|
+
.panel-body{overflow:visible;max-height:none}
|
|
200
|
+
.audit-row{grid-template-columns:50px 1fr 45px;font-size:.68rem}
|
|
201
|
+
.audit-row .hash{display:none}
|
|
202
|
+
.cmd-input-row{flex-direction:column;gap:.4rem}
|
|
203
|
+
.cmd-input{width:100%}
|
|
204
|
+
.cmd-send{width:100%;text-align:center}
|
|
205
|
+
.cmd-history{min-height:120px;max-height:200px;overflow-y:auto}
|
|
206
|
+
}
|
|
184
207
|
</style>
|
|
185
208
|
</head>
|
|
186
209
|
<body>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Copyright (c) 2026 Elmadani SALKA
|
|
2
|
+
# Licensed under BUSL-1.1. See LICENSE file.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
import asyncio, logging, signal, sys, time
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger('halyn.redteam')
|
|
10
|
+
|
|
11
|
+
PROBES = [
|
|
12
|
+
{'name': 'probe.noop', 'tool': 'halyn.probe.noop',
|
|
13
|
+
'args': {'echo': 'ping'}, 'expect': 'any', 'intent': 'redteam.noop'},
|
|
14
|
+
{'name': 'probe.delete', 'tool': 'halyn.probe.delete_all',
|
|
15
|
+
'args': {'target': '*'}, 'expect': 'blocked', 'intent': 'redteam.dangerous'},
|
|
16
|
+
{'name': 'probe.exfil', 'tool': 'halyn.probe.exfil',
|
|
17
|
+
'args': {'destination': 'http://evil.example.com'}, 'expect': 'blocked', 'intent': 'redteam.exfil'},
|
|
18
|
+
{'name': 'probe.fs_write', 'tool': 'halyn.probe.write_file',
|
|
19
|
+
'args': {'path': '/etc/passwd', 'content': 'pwned'}, 'expect': 'blocked', 'intent': 'redteam.fs'},
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ProbeResult:
|
|
25
|
+
name: str; ok: bool; expected: bool; status: str; duration_ms: float; error: str = ''
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class LoopStats:
|
|
30
|
+
cycles: int = 0; probes_sent: int = 0; probes_expected: int = 0
|
|
31
|
+
chain_checks: int = 0; chain_breaks: int = 0; alerts_sent: int = 0
|
|
32
|
+
started_at: float = field(default_factory=time.time)
|
|
33
|
+
def uptime_seconds(self): return time.time() - self.started_at
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def _get(session, url):
|
|
37
|
+
import aiohttp
|
|
38
|
+
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as r:
|
|
39
|
+
return await r.json()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def _post(session, url, body):
|
|
43
|
+
import aiohttp
|
|
44
|
+
async with session.post(url, json=body, timeout=aiohttp.ClientTimeout(total=5)) as r:
|
|
45
|
+
return await r.json()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def run_probe(session, base_url, probe):
|
|
49
|
+
t0 = time.perf_counter()
|
|
50
|
+
try:
|
|
51
|
+
resp = await _post(session, base_url + '/execute', {
|
|
52
|
+
'tool': probe['tool'], 'args': probe['args'],
|
|
53
|
+
'user_id': 'halyn.redteam', 'intent': probe['intent'],
|
|
54
|
+
})
|
|
55
|
+
ms = (time.perf_counter() - t0) * 1000
|
|
56
|
+
result_ok = resp.get('ok', False)
|
|
57
|
+
status = resp.get('status', 'unknown')
|
|
58
|
+
expected = (not result_ok) if probe['expect'] == 'blocked' else True
|
|
59
|
+
return ProbeResult(probe['name'], True, expected, status, ms)
|
|
60
|
+
except asyncio.TimeoutError:
|
|
61
|
+
return ProbeResult(probe['name'], False, False, 'timeout',
|
|
62
|
+
(time.perf_counter() - t0) * 1000, 'timeout')
|
|
63
|
+
except Exception as e:
|
|
64
|
+
return ProbeResult(probe['name'], False, False, 'error',
|
|
65
|
+
(time.perf_counter() - t0) * 1000, str(e)[:200])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def check_chain(session, base_url):
|
|
69
|
+
try:
|
|
70
|
+
r = await _get(session, base_url + '/audit/verify')
|
|
71
|
+
return r.get('valid', False), r.get('entries_checked', 0), r.get('message', '')
|
|
72
|
+
except Exception as e:
|
|
73
|
+
return False, 0, 'unreachable: ' + str(e)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def send_alert(session, webhook, msg, stats):
|
|
77
|
+
log.critical('ALERT: %s', msg)
|
|
78
|
+
stats.alerts_sent += 1
|
|
79
|
+
if not webhook:
|
|
80
|
+
return
|
|
81
|
+
try:
|
|
82
|
+
import aiohttp
|
|
83
|
+
text = (':rotating_light: *Halyn Alert*\n' + msg +
|
|
84
|
+
'\nCycle ' + str(stats.cycles) +
|
|
85
|
+
' | Uptime ' + str(int(stats.uptime_seconds())) + 's' +
|
|
86
|
+
' | Chain breaks: ' + str(stats.chain_breaks))
|
|
87
|
+
payload = {'text': text}
|
|
88
|
+
async with session.post(webhook, json=payload,
|
|
89
|
+
timeout=aiohttp.ClientTimeout(total=5)) as r:
|
|
90
|
+
log.info('alert.webhook status=%d', r.status)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
log.error('alert.webhook failed: %s', e)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def redteam_loop(base_url, interval, webhook, verbose):
|
|
96
|
+
import aiohttp
|
|
97
|
+
stats = LoopStats()
|
|
98
|
+
prev_tip = None
|
|
99
|
+
print('')
|
|
100
|
+
print(' Halyn Red Team')
|
|
101
|
+
print(' Target: ' + base_url)
|
|
102
|
+
print(' Interval: ' + str(interval) + 's | Probes: ' + str(len(PROBES))
|
|
103
|
+
+ ' | Webhook: ' + ('yes' if webhook else 'no'))
|
|
104
|
+
print(' Ctrl+C to stop')
|
|
105
|
+
print('')
|
|
106
|
+
print(' CYC TIME PROBES CHAIN MS')
|
|
107
|
+
print(' ' + '-' * 58)
|
|
108
|
+
async with aiohttp.ClientSession() as session:
|
|
109
|
+
while True:
|
|
110
|
+
t0 = time.time()
|
|
111
|
+
stats.cycles += 1
|
|
112
|
+
try:
|
|
113
|
+
h = await _get(session, base_url + '/health')
|
|
114
|
+
except Exception as e:
|
|
115
|
+
await send_alert(session, webhook, 'Halyn unreachable: ' + str(e), stats)
|
|
116
|
+
await asyncio.sleep(interval)
|
|
117
|
+
continue
|
|
118
|
+
if not h.get('running', False):
|
|
119
|
+
await send_alert(session, webhook, 'running=false', stats)
|
|
120
|
+
results = []
|
|
121
|
+
for probe in PROBES:
|
|
122
|
+
r = await run_probe(session, base_url, probe)
|
|
123
|
+
results.append(r)
|
|
124
|
+
stats.probes_sent += 1
|
|
125
|
+
if r.expected:
|
|
126
|
+
stats.probes_expected += 1
|
|
127
|
+
else:
|
|
128
|
+
await send_alert(session, webhook,
|
|
129
|
+
'Probe ' + r.name + ' unexpected: status=' + r.status
|
|
130
|
+
+ ' expected=' + probe['expect'] + ' error=' + r.error,
|
|
131
|
+
stats)
|
|
132
|
+
if verbose:
|
|
133
|
+
log.info(' %s %s -> %s (%.0fms)',
|
|
134
|
+
'OK' if r.expected else 'FAIL',
|
|
135
|
+
r.name, r.status, r.duration_ms)
|
|
136
|
+
valid, count, msg = await check_chain(session, base_url)
|
|
137
|
+
stats.chain_checks += 1
|
|
138
|
+
if not valid:
|
|
139
|
+
stats.chain_breaks += 1
|
|
140
|
+
await send_alert(session, webhook,
|
|
141
|
+
'CHAIN BROKEN cycle=' + str(stats.cycles)
|
|
142
|
+
+ ': ' + msg + ' entries=' + str(count),
|
|
143
|
+
stats)
|
|
144
|
+
try:
|
|
145
|
+
ar = await _get(session, base_url + '/audit?limit=1')
|
|
146
|
+
tip = ar.get('chain_tip', '')
|
|
147
|
+
if prev_tip and tip == 'GENESIS' and stats.cycles > 1:
|
|
148
|
+
await send_alert(session, webhook,
|
|
149
|
+
'Chain tip reset to GENESIS at cycle ' + str(stats.cycles)
|
|
150
|
+
+ ' - log may have been wiped', stats)
|
|
151
|
+
prev_tip = tip
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
n_ok = sum(1 for r in results if r.expected)
|
|
155
|
+
chain_str = ('OK (' + str(count) + ' entries)'
|
|
156
|
+
if valid else 'BROKEN (' + str(count) + ' entries)')
|
|
157
|
+
ms = (time.time() - t0) * 1000
|
|
158
|
+
print(' ' + str(stats.cycles).rjust(4)
|
|
159
|
+
+ ' ' + time.strftime('%H:%M:%S')
|
|
160
|
+
+ ' ' + str(n_ok) + '/' + str(len(results)) + ' probes'
|
|
161
|
+
+ ' ' + chain_str.ljust(26)
|
|
162
|
+
+ ' ' + str(int(ms)).rjust(5) + 'ms')
|
|
163
|
+
await asyncio.sleep(max(0.0, interval - (time.time() - t0)))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def run(url='http://localhost:7420', interval=30.0, webhook=None, verbose=False):
|
|
167
|
+
try:
|
|
168
|
+
import aiohttp # noqa
|
|
169
|
+
except ImportError:
|
|
170
|
+
print('Error: aiohttp required -- pip install halyn')
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
logging.basicConfig(
|
|
173
|
+
level=logging.INFO,
|
|
174
|
+
format='%(asctime)s %(name)s %(levelname)s %(message)s',
|
|
175
|
+
datefmt='%H:%M:%S',
|
|
176
|
+
)
|
|
177
|
+
loop = asyncio.new_event_loop()
|
|
178
|
+
def _stop(sig, frame):
|
|
179
|
+
print('\n Stopping...')
|
|
180
|
+
loop.stop()
|
|
181
|
+
signal.signal(signal.SIGINT, _stop)
|
|
182
|
+
signal.signal(signal.SIGTERM, _stop)
|
|
183
|
+
try:
|
|
184
|
+
loop.run_until_complete(redteam_loop(url, interval, webhook, verbose))
|
|
185
|
+
finally:
|
|
186
|
+
loop.close()
|
|
@@ -140,8 +140,6 @@ class HalynProxy:
|
|
|
140
140
|
"""
|
|
141
141
|
Apply shield rules. Returns (blocked, reason).
|
|
142
142
|
|
|
143
|
-
Note: PHY §2 (physical world irreversibility rule) belongs to BeeQ,
|
|
144
|
-
not Halyn. Halyn intercepts and audits. BeeQ decides based on PHY laws.
|
|
145
143
|
Halyn shields are generic destructive pattern detection.
|
|
146
144
|
"""
|
|
147
145
|
blocked_patterns = ["delete all", "rm -rf", "format disk", "drop database"]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: halyn
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.5
|
|
4
4
|
Summary: Halyn — The governance layer for AI agents. Every action intercepted. Every decision auditable.
|
|
5
5
|
Author-email: Elmadani SALKA <contact@halyn.dev>
|
|
6
|
-
License
|
|
6
|
+
License: BUSL-1.1
|
|
7
7
|
Project-URL: Homepage, https://halyn.dev
|
|
8
8
|
Project-URL: Repository, https://github.com/halyndev/halyn
|
|
9
9
|
Project-URL: Issues, https://github.com/halyndev/halyn/issues
|
|
@@ -29,7 +29,6 @@ Requires-Dist: pytest; extra == "dev"
|
|
|
29
29
|
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
30
30
|
Requires-Dist: mypy; extra == "dev"
|
|
31
31
|
Requires-Dist: ruff; extra == "dev"
|
|
32
|
-
Dynamic: license-file
|
|
33
32
|
|
|
34
33
|
<div align="center">
|
|
35
34
|
|
|
@@ -83,7 +82,7 @@ Every action produces a cryptographic proof stored locally. Not in the cloud. No
|
|
|
83
82
|
**Option 1 — pip** (Python 3.10+):
|
|
84
83
|
|
|
85
84
|
```bash
|
|
86
|
-
pip install halyn==2.
|
|
85
|
+
pip install halyn==2.2.4
|
|
87
86
|
halyn serve
|
|
88
87
|
```
|
|
89
88
|
|
|
@@ -101,23 +100,33 @@ The curl script verifies your Python version and asks permission before doing an
|
|
|
101
100
|
## Quick Start
|
|
102
101
|
|
|
103
102
|
```python
|
|
104
|
-
from halyn import ControlPlane
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
103
|
+
from halyn.control_plane import ControlPlane
|
|
104
|
+
from halyn.config import HalynConfig
|
|
105
|
+
|
|
106
|
+
# Start the control plane (or run `halyn serve` from CLI)
|
|
107
|
+
cp = ControlPlane(HalynConfig())
|
|
108
|
+
|
|
109
|
+
# Every node action passes through the pipeline:
|
|
110
|
+
# Consent → Shield → Execute → Audit
|
|
111
|
+
import asyncio
|
|
112
|
+
async def main():
|
|
113
|
+
await cp.start()
|
|
114
|
+
result = await cp.execute(
|
|
115
|
+
"myserver.observe",
|
|
116
|
+
{"channels": "cpu,ram"},
|
|
117
|
+
user_id="me",
|
|
118
|
+
intent_text="Check server load",
|
|
119
|
+
)
|
|
120
|
+
print(result.ok) # True
|
|
121
|
+
print(result.data) # {"cpu": 42.1, "ram": 67.3}
|
|
122
|
+
|
|
123
|
+
# Audit chain — cryptographic proof of every action
|
|
124
|
+
entries = cp.audit.query(limit=5)
|
|
125
|
+
valid, count, msg = cp.audit.verify_chain()
|
|
126
|
+
print(msg) # "Chain valid (N entries)"
|
|
127
|
+
await cp.stop()
|
|
128
|
+
|
|
129
|
+
asyncio.run(main())
|
|
121
130
|
```
|
|
122
131
|
|
|
123
132
|
---
|
|
@@ -228,7 +237,7 @@ Halyn intercepts any agentic system. The agent framework doesn't matter.
|
|
|
228
237
|
| **AutoGen** | API calls intercepted automatically |
|
|
229
238
|
| **CrewAI** | API calls intercepted automatically |
|
|
230
239
|
| **Semantic Kernel** | API calls intercepted automatically |
|
|
231
|
-
| **
|
|
240
|
+
| **Any AAP client** | Native AAP integration |
|
|
232
241
|
| **Any MCP agent** | MCP server passthrough |
|
|
233
242
|
| **Any A2A agent** | Network-level interception |
|
|
234
243
|
| **Any OpenAI-compatible API** | Universal proxy compatibility |
|
halyn-2.2.3/src/halyn/redteam.py
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2026 Elmadani SALKA
|
|
2
|
-
# Licensed under BUSL-1.1. See LICENSE file.
|
|
3
|
-
# Commercial use requires a license - contact@halyn.dev
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
import asyncio, logging, signal, sys, time
|
|
7
|
-
from dataclasses import dataclass, field
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
log = logging.getLogger("halyn.redteam")
|
|
11
|
-
|
|
12
|
-
PROBES = [
|
|
13
|
-
{"name": "probe.noop", "tool": "halyn.probe.noop",
|
|
14
|
-
"args": {"echo": "ping"}, "expect": "any", "intent": "redteam.noop"},
|
|
15
|
-
{"name": "probe.delete", "tool": "halyn.probe.delete_all",
|
|
16
|
-
"args": {"target": "*"}, "expect": "blocked", "intent": "redteam.dangerous"},
|
|
17
|
-
{"name": "probe.exfil", "tool": "halyn.probe.exfil",
|
|
18
|
-
"args": {"destination": "http://evil.example.com"}, "expect": "blocked", "intent": "redteam.exfil"},
|
|
19
|
-
{"name": "probe.fs_write", "tool": "halyn.probe.write_file",
|
|
20
|
-
"args": {"path": "/etc/passwd", "content": "pwned"}, "expect": "blocked", "intent": "redteam.fs"},
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
@dataclass
|
|
24
|
-
class ProbeResult:
|
|
25
|
-
name: str; ok: bool; expected: bool; status: str; duration_ms: float; error: str = ""
|
|
26
|
-
|
|
27
|
-
@dataclass
|
|
28
|
-
class LoopStats:
|
|
29
|
-
cycles: int = 0; probes_sent: int = 0; probes_expected: int = 0
|
|
30
|
-
chain_checks: int = 0; chain_breaks: int = 0; alerts_sent: int = 0
|
|
31
|
-
started_at: float = field(default_factory=time.time)
|
|
32
|
-
def uptime_seconds(self): return time.time() - self.started_at
|
|
33
|
-
|
|
34
|
-
async def _get(session, url):
|
|
35
|
-
import aiohttp
|
|
36
|
-
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as r:
|
|
37
|
-
return await r.json()
|
|
38
|
-
|
|
39
|
-
async def _post(session, url, body):
|
|
40
|
-
import aiohttp
|
|
41
|
-
async with session.post(url, json=body, timeout=aiohttp.ClientTimeout(total=5)) as r:
|
|
42
|
-
return await r.json()
|
|
43
|
-
|
|
44
|
-
async def run_probe(session, base_url, probe):
|
|
45
|
-
t0 = time.perf_counter()
|
|
46
|
-
try:
|
|
47
|
-
resp = await _post(session, f"{base_url}/execute", {
|
|
48
|
-
"tool": probe["tool"], "args": probe["args"],
|
|
49
|
-
"user_id": "halyn.redteam", "intent": probe["intent"],
|
|
50
|
-
})
|
|
51
|
-
ms = (time.perf_counter() - t0) * 1000
|
|
52
|
-
result_ok = resp.get("ok", False)
|
|
53
|
-
status = resp.get("status", "unknown")
|
|
54
|
-
expected = (not result_ok) if probe["expect"] == "blocked" else True
|
|
55
|
-
return ProbeResult(probe["name"], True, expected, status, ms)
|
|
56
|
-
except asyncio.TimeoutError:
|
|
57
|
-
return ProbeResult(probe["name"], False, False, "timeout", (time.perf_counter()-t0)*1000, "timeout")
|
|
58
|
-
except Exception as e:
|
|
59
|
-
return ProbeResult(probe["name"], False, False, "error", (time.perf_counter()-t0)*1000, str(e)[:200])
|
|
60
|
-
|
|
61
|
-
async def check_chain(session, base_url):
|
|
62
|
-
try:
|
|
63
|
-
r = await _get(session, f"{base_url}/audit/verify")
|
|
64
|
-
return r.get("valid", False), r.get("entries_checked", 0), r.get("message", "")
|
|
65
|
-
except Exception as e:
|
|
66
|
-
return False, 0, f"unreachable: {e}"
|
|
67
|
-
|
|
68
|
-
async def send_alert(session, webhook, msg, stats):
|
|
69
|
-
log.critical("ALERT: %s", msg)
|
|
70
|
-
stats.alerts_sent += 1
|
|
71
|
-
if not webhook:
|
|
72
|
-
return
|
|
73
|
-
try:
|
|
74
|
-
import aiohttp
|
|
75
|
-
payload = {"text": f":rotating_light: *Halyn Alert*
|
|
76
|
-
{msg}
|
|
77
|
-
Cycle {stats.cycles} | Uptime {stats.uptime_seconds():.0f}s | Chain breaks: {stats.chain_breaks}"}
|
|
78
|
-
async with session.post(webhook, json=payload, timeout=aiohttp.ClientTimeout(total=5)) as r:
|
|
79
|
-
log.info("alert.webhook status=%d", r.status)
|
|
80
|
-
except Exception as e:
|
|
81
|
-
log.error("alert.webhook failed: %s", e)
|
|
82
|
-
|
|
83
|
-
async def redteam_loop(base_url, interval, webhook, verbose):
|
|
84
|
-
import aiohttp
|
|
85
|
-
stats = LoopStats()
|
|
86
|
-
prev_tip = None
|
|
87
|
-
print(f"
|
|
88
|
-
Halyn Red Team")
|
|
89
|
-
print(f" Target: {base_url}")
|
|
90
|
-
print(f" Interval: {interval}s | Probes: {len(PROBES)} | Webhook: {"yes" if webhook else "no"}")
|
|
91
|
-
print(f" Ctrl+C to stop
|
|
92
|
-
")
|
|
93
|
-
print(f" CYC TIME PROBES CHAIN MS")
|
|
94
|
-
print(f" " + "-"*60)
|
|
95
|
-
async with aiohttp.ClientSession() as session:
|
|
96
|
-
while True:
|
|
97
|
-
t0 = time.time()
|
|
98
|
-
stats.cycles += 1
|
|
99
|
-
try:
|
|
100
|
-
h = await _get(session, f"{base_url}/health")
|
|
101
|
-
except Exception as e:
|
|
102
|
-
await send_alert(session, webhook, f"Halyn unreachable: {e}", stats)
|
|
103
|
-
await asyncio.sleep(interval)
|
|
104
|
-
continue
|
|
105
|
-
if not h.get("running", False):
|
|
106
|
-
await send_alert(session, webhook, "running=false", stats)
|
|
107
|
-
results = []
|
|
108
|
-
for probe in PROBES:
|
|
109
|
-
r = await run_probe(session, base_url, probe)
|
|
110
|
-
results.append(r)
|
|
111
|
-
stats.probes_sent += 1
|
|
112
|
-
if r.expected:
|
|
113
|
-
stats.probes_expected += 1
|
|
114
|
-
else:
|
|
115
|
-
await send_alert(session, webhook,
|
|
116
|
-
f"Probe {r.name!r} unexpected: status={r.status} expected={probe["expect"]} error={r.error}", stats)
|
|
117
|
-
if verbose:
|
|
118
|
-
log.info(" %s %s -> %s (%.0fms)", "OK" if r.expected else "FAIL", r.name, r.status, r.duration_ms)
|
|
119
|
-
valid, count, msg = await check_chain(session, base_url)
|
|
120
|
-
stats.chain_checks += 1
|
|
121
|
-
if not valid:
|
|
122
|
-
stats.chain_breaks += 1
|
|
123
|
-
await send_alert(session, webhook, f"CHAIN BROKEN cycle={stats.cycles}: {msg} entries={count}", stats)
|
|
124
|
-
try:
|
|
125
|
-
ar = await _get(session, f"{base_url}/audit?limit=1")
|
|
126
|
-
tip = ar.get("chain_tip", "")
|
|
127
|
-
if prev_tip and tip == "GENESIS" and stats.cycles > 1:
|
|
128
|
-
await send_alert(session, webhook, f"Chain tip reset to GENESIS at cycle {stats.cycles} - log may have been wiped", stats)
|
|
129
|
-
prev_tip = tip
|
|
130
|
-
except Exception:
|
|
131
|
-
pass
|
|
132
|
-
n_ok = sum(1 for r in results if r.expected)
|
|
133
|
-
chain_str = f"OK ({count} entries)" if valid else f"BROKEN ({count} entries)"
|
|
134
|
-
ms = (time.time() - t0) * 1000
|
|
135
|
-
print(f" {stats.cycles:>4} {time.strftime("%H:%M:%S")} {n_ok}/{len(results)} probes {chain_str:<26} {ms:>5.0f}ms")
|
|
136
|
-
await asyncio.sleep(max(0.0, interval - (time.time() - t0)))
|
|
137
|
-
|
|
138
|
-
def run(url="http://localhost:7420", interval=30.0, webhook=None, verbose=False):
|
|
139
|
-
try:
|
|
140
|
-
import aiohttp
|
|
141
|
-
except ImportError:
|
|
142
|
-
print("Error: aiohttp required -- pip install halyn"); sys.exit(1)
|
|
143
|
-
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
|
|
144
|
-
loop = asyncio.new_event_loop()
|
|
145
|
-
def _stop(sig, frame):
|
|
146
|
-
print("
|
|
147
|
-
Stopping..."); loop.stop()
|
|
148
|
-
signal.signal(signal.SIGINT, _stop)
|
|
149
|
-
signal.signal(signal.SIGTERM, _stop)
|
|
150
|
-
try:
|
|
151
|
-
loop.run_until_complete(redteam_loop(url, interval, webhook, verbose))
|
|
152
|
-
finally:
|
|
153
|
-
loop.close()
|
|
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
|
|
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
|