moss-hermes 0.1.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.
- moss_hermes-0.1.0/.gitignore +33 -0
- moss_hermes-0.1.0/LICENSE +21 -0
- moss_hermes-0.1.0/PKG-INFO +93 -0
- moss_hermes-0.1.0/README.md +60 -0
- moss_hermes-0.1.0/moss_hermes/__init__.py +81 -0
- moss_hermes-0.1.0/moss_hermes/kill_switch.py +178 -0
- moss_hermes-0.1.0/moss_hermes/signing.py +439 -0
- moss_hermes-0.1.0/moss_hermes/wrapper.py +231 -0
- moss_hermes-0.1.0/pyproject.toml +75 -0
- moss_hermes-0.1.0/tests/__init__.py +1 -0
- moss_hermes-0.1.0/tests/test_signing.py +140 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.egg
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
venv/
|
|
14
|
+
.venv/
|
|
15
|
+
env/
|
|
16
|
+
|
|
17
|
+
# IDE
|
|
18
|
+
.idea/
|
|
19
|
+
.vscode/
|
|
20
|
+
*.swp
|
|
21
|
+
*.swo
|
|
22
|
+
|
|
23
|
+
# Testing
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
.coverage
|
|
26
|
+
htmlcov/
|
|
27
|
+
|
|
28
|
+
# Type checking
|
|
29
|
+
.mypy_cache/
|
|
30
|
+
|
|
31
|
+
# OS
|
|
32
|
+
.DS_Store
|
|
33
|
+
Thumbs.db
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MOSS Computing
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: moss-hermes
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MOSS integration for Nous Research Hermes agents - cryptographic signing for AI actions
|
|
5
|
+
Project-URL: Homepage, https://mosscomputing.com
|
|
6
|
+
Project-URL: Documentation, https://docs.mosscomputing.com/hermes
|
|
7
|
+
Project-URL: Repository, https://github.com/mosscomputing/moss-hermes
|
|
8
|
+
Project-URL: Changelog, https://github.com/mosscomputing/moss-hermes/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: MOSS Computing <support@mosscomputing.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai-agents,ai-governance,cryptographic-signing,hermes,llm,ml-dsa-44,moss,nous-research,post-quantum
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Security :: Cryptography
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: moss-sdk>=0.2.0
|
|
25
|
+
Requires-Dist: requests>=2.28.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# moss-hermes
|
|
35
|
+
|
|
36
|
+
MOSS integration for [Nous Research Hermes](https://nousresearch.com) agents.
|
|
37
|
+
|
|
38
|
+
## Overview
|
|
39
|
+
|
|
40
|
+
Cryptographic signing for Hermes AI agent actions using ML-DSA-44 (NIST FIPS 204) post-quantum signatures.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install moss-hermes
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from moss_hermes import sign_tool_call
|
|
52
|
+
|
|
53
|
+
signed = sign_tool_call(
|
|
54
|
+
tool_name="send_email",
|
|
55
|
+
tool_input={"to": "user@example.com", "body": "Hello"},
|
|
56
|
+
tool_output={"status": "sent"},
|
|
57
|
+
agent_id="hermes-assistant"
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## What Gets Signed
|
|
62
|
+
|
|
63
|
+
| Function | Use Case |
|
|
64
|
+
|----------|----------|
|
|
65
|
+
| `sign_tool_call` | Tool executions |
|
|
66
|
+
| `sign_function_call` | OpenAI-compatible function calls |
|
|
67
|
+
| `sign_reasoning_chain` | Chain-of-thought reasoning |
|
|
68
|
+
| `sign_memory_retrieval` | Memory/RAG retrieval events |
|
|
69
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
| Variable | Description | Default |
|
|
73
|
+
|----------|-------------|---------|
|
|
74
|
+
| `MOSS_API_KEY` | Enterprise API key | None (local mode) |
|
|
75
|
+
| `MOSS_API_URL` | API endpoint | `https://api.mosscomputing.com` |
|
|
76
|
+
|
|
77
|
+
## Features
|
|
78
|
+
|
|
79
|
+
- **ML-DSA-44 Signatures**: Post-quantum cryptographic signing
|
|
80
|
+
- **Causal Linking**: Link actions via `parent_sig` parameter
|
|
81
|
+
- **Kill-Switch**: Monitor for agent revocation
|
|
82
|
+
- **Async Support**: All functions have async versions
|
|
83
|
+
|
|
84
|
+
## Links
|
|
85
|
+
|
|
86
|
+
- [Documentation](https://docs.mosscomputing.com/sdks/hermes)
|
|
87
|
+
- [Nous Research](https://nousresearch.com)
|
|
88
|
+
- [Hermes Models](https://huggingface.co/NousResearch)
|
|
89
|
+
- [PyPI](https://pypi.org/project/moss-hermes/)
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT - See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# moss-hermes
|
|
2
|
+
|
|
3
|
+
MOSS integration for [Nous Research Hermes](https://nousresearch.com) agents.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Cryptographic signing for Hermes AI agent actions using ML-DSA-44 (NIST FIPS 204) post-quantum signatures.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install moss-hermes
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from moss_hermes import sign_tool_call
|
|
19
|
+
|
|
20
|
+
signed = sign_tool_call(
|
|
21
|
+
tool_name="send_email",
|
|
22
|
+
tool_input={"to": "user@example.com", "body": "Hello"},
|
|
23
|
+
tool_output={"status": "sent"},
|
|
24
|
+
agent_id="hermes-assistant"
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## What Gets Signed
|
|
29
|
+
|
|
30
|
+
| Function | Use Case |
|
|
31
|
+
|----------|----------|
|
|
32
|
+
| `sign_tool_call` | Tool executions |
|
|
33
|
+
| `sign_function_call` | OpenAI-compatible function calls |
|
|
34
|
+
| `sign_reasoning_chain` | Chain-of-thought reasoning |
|
|
35
|
+
| `sign_memory_retrieval` | Memory/RAG retrieval events |
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
| Variable | Description | Default |
|
|
40
|
+
|----------|-------------|---------|
|
|
41
|
+
| `MOSS_API_KEY` | Enterprise API key | None (local mode) |
|
|
42
|
+
| `MOSS_API_URL` | API endpoint | `https://api.mosscomputing.com` |
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
- **ML-DSA-44 Signatures**: Post-quantum cryptographic signing
|
|
47
|
+
- **Causal Linking**: Link actions via `parent_sig` parameter
|
|
48
|
+
- **Kill-Switch**: Monitor for agent revocation
|
|
49
|
+
- **Async Support**: All functions have async versions
|
|
50
|
+
|
|
51
|
+
## Links
|
|
52
|
+
|
|
53
|
+
- [Documentation](https://docs.mosscomputing.com/sdks/hermes)
|
|
54
|
+
- [Nous Research](https://nousresearch.com)
|
|
55
|
+
- [Hermes Models](https://huggingface.co/NousResearch)
|
|
56
|
+
- [PyPI](https://pypi.org/project/moss-hermes/)
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT - See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MOSS Hermes Integration - Cryptographic Signing for Hermes Agent Outputs
|
|
3
|
+
|
|
4
|
+
Sign tool calls, reasoning chains, and agent actions from Nous Research's Hermes models.
|
|
5
|
+
|
|
6
|
+
Quick Start:
|
|
7
|
+
from moss_hermes import sign_tool_call, sign_reasoning_chain
|
|
8
|
+
|
|
9
|
+
# After Hermes executes a tool
|
|
10
|
+
signed = sign_tool_call(
|
|
11
|
+
tool_name="send_email",
|
|
12
|
+
tool_input={"to": "user@example.com", "body": "Hello"},
|
|
13
|
+
tool_output={"status": "sent", "message_id": "msg_123"},
|
|
14
|
+
agent_id="hermes-assistant"
|
|
15
|
+
)
|
|
16
|
+
print(f"Signature: {signed.signature}")
|
|
17
|
+
|
|
18
|
+
Enterprise Mode:
|
|
19
|
+
Set MOSS_API_KEY environment variable to enable:
|
|
20
|
+
- Policy evaluation (allow/block/hold)
|
|
21
|
+
- Evidence retention
|
|
22
|
+
- Kill-switch monitoring
|
|
23
|
+
- Registry certification
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
from .signing import (
|
|
29
|
+
sign_tool_call,
|
|
30
|
+
sign_tool_call_async,
|
|
31
|
+
sign_reasoning_chain,
|
|
32
|
+
sign_reasoning_chain_async,
|
|
33
|
+
sign_memory_retrieval,
|
|
34
|
+
sign_memory_retrieval_async,
|
|
35
|
+
sign_agent_action,
|
|
36
|
+
sign_agent_action_async,
|
|
37
|
+
sign_function_call,
|
|
38
|
+
sign_function_call_async,
|
|
39
|
+
verify_envelope,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
from .wrapper import (
|
|
43
|
+
MossHermesWrapper,
|
|
44
|
+
moss_signed,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
from .kill_switch import (
|
|
48
|
+
KillSwitchMonitor,
|
|
49
|
+
check_kill_switch,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
from moss import SignResult, VerifyResult, Envelope, enterprise_enabled
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
# Explicit signing functions
|
|
56
|
+
"sign_tool_call",
|
|
57
|
+
"sign_tool_call_async",
|
|
58
|
+
"sign_reasoning_chain",
|
|
59
|
+
"sign_reasoning_chain_async",
|
|
60
|
+
"sign_memory_retrieval",
|
|
61
|
+
"sign_memory_retrieval_async",
|
|
62
|
+
"sign_agent_action",
|
|
63
|
+
"sign_agent_action_async",
|
|
64
|
+
"sign_function_call",
|
|
65
|
+
"sign_function_call_async",
|
|
66
|
+
"verify_envelope",
|
|
67
|
+
|
|
68
|
+
# Wrapper for automatic signing
|
|
69
|
+
"MossHermesWrapper",
|
|
70
|
+
"moss_signed",
|
|
71
|
+
|
|
72
|
+
# Kill-switch
|
|
73
|
+
"KillSwitchMonitor",
|
|
74
|
+
"check_kill_switch",
|
|
75
|
+
|
|
76
|
+
# Core types
|
|
77
|
+
"SignResult",
|
|
78
|
+
"VerifyResult",
|
|
79
|
+
"Envelope",
|
|
80
|
+
"enterprise_enabled",
|
|
81
|
+
]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MOSS Hermes Kill-Switch Integration
|
|
3
|
+
|
|
4
|
+
Monitors the MOSS Kill-Switch API and terminates agent capabilities if revoked.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
from moss_hermes import KillSwitchMonitor
|
|
8
|
+
|
|
9
|
+
monitor = KillSwitchMonitor(
|
|
10
|
+
agent_id="hermes-trading-bot",
|
|
11
|
+
moss_api_key=os.environ["MOSS_API_KEY"],
|
|
12
|
+
on_revoked=lambda: sys.exit(1)
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Start monitoring in background
|
|
16
|
+
monitor.start()
|
|
17
|
+
|
|
18
|
+
# Run your agent
|
|
19
|
+
agent.run()
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import time
|
|
24
|
+
import threading
|
|
25
|
+
from typing import Callable, Optional
|
|
26
|
+
import requests
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class KillSwitchMonitor:
|
|
30
|
+
"""
|
|
31
|
+
Monitor MOSS kill-switch status and respond to revocations.
|
|
32
|
+
|
|
33
|
+
When an agent is revoked via the MOSS console or API, this monitor
|
|
34
|
+
will detect it and call the on_revoked callback.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
agent_id: str,
|
|
40
|
+
*,
|
|
41
|
+
moss_api_key: Optional[str] = None,
|
|
42
|
+
moss_api_url: str = "https://api.mosscomputing.com",
|
|
43
|
+
check_interval_seconds: int = 10,
|
|
44
|
+
on_revoked: Optional[Callable[[], None]] = None,
|
|
45
|
+
on_error: Optional[Callable[[Exception], None]] = None,
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Initialize the kill-switch monitor.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
agent_id: The agent ID to monitor
|
|
52
|
+
moss_api_key: MOSS API key (or set MOSS_API_KEY env var)
|
|
53
|
+
moss_api_url: MOSS API base URL
|
|
54
|
+
check_interval_seconds: How often to check status (default 10s)
|
|
55
|
+
on_revoked: Callback when agent is revoked
|
|
56
|
+
on_error: Callback on API errors
|
|
57
|
+
"""
|
|
58
|
+
self.agent_id = agent_id
|
|
59
|
+
self.moss_api_key = moss_api_key or os.environ.get("MOSS_API_KEY")
|
|
60
|
+
self.moss_api_url = moss_api_url.rstrip("/")
|
|
61
|
+
self.check_interval = check_interval_seconds
|
|
62
|
+
self.on_revoked = on_revoked or self._default_on_revoked
|
|
63
|
+
self.on_error = on_error
|
|
64
|
+
|
|
65
|
+
self._running = False
|
|
66
|
+
self._thread: Optional[threading.Thread] = None
|
|
67
|
+
self._revoked = False
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def is_revoked(self) -> bool:
|
|
71
|
+
"""Check if agent has been revoked."""
|
|
72
|
+
return self._revoked
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def is_running(self) -> bool:
|
|
76
|
+
"""Check if monitor is running."""
|
|
77
|
+
return self._running
|
|
78
|
+
|
|
79
|
+
def start(self):
|
|
80
|
+
"""Start the kill-switch monitor in a background thread."""
|
|
81
|
+
if self._running:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
self._running = True
|
|
85
|
+
self._thread = threading.Thread(target=self._monitor_loop, daemon=True)
|
|
86
|
+
self._thread.start()
|
|
87
|
+
|
|
88
|
+
def stop(self):
|
|
89
|
+
"""Stop the kill-switch monitor."""
|
|
90
|
+
self._running = False
|
|
91
|
+
if self._thread:
|
|
92
|
+
self._thread.join(timeout=5)
|
|
93
|
+
|
|
94
|
+
def check_status(self) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Check if agent is still active (not revoked).
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
True if active, False if revoked
|
|
100
|
+
"""
|
|
101
|
+
if not self.moss_api_key:
|
|
102
|
+
return True # No API key = assume active
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
response = requests.get(
|
|
106
|
+
f"{self.moss_api_url}/v1/agents/{self.agent_id}",
|
|
107
|
+
headers={"Authorization": f"Bearer {self.moss_api_key}"},
|
|
108
|
+
timeout=5,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if response.status_code == 404:
|
|
112
|
+
# Agent not found
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
if response.status_code == 200:
|
|
116
|
+
data = response.json()
|
|
117
|
+
status = data.get("status", "active")
|
|
118
|
+
return status == "active"
|
|
119
|
+
|
|
120
|
+
return True # On error, assume active
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
if self.on_error:
|
|
124
|
+
self.on_error(e)
|
|
125
|
+
return True # On error, assume active
|
|
126
|
+
|
|
127
|
+
def _monitor_loop(self):
|
|
128
|
+
"""Background monitoring loop."""
|
|
129
|
+
while self._running:
|
|
130
|
+
if not self.check_status():
|
|
131
|
+
self._revoked = True
|
|
132
|
+
self._running = False
|
|
133
|
+
self.on_revoked()
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
time.sleep(self.check_interval)
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _default_on_revoked():
|
|
140
|
+
"""Default revocation handler - raises exception."""
|
|
141
|
+
raise AgentRevokedException("Agent has been revoked via MOSS kill-switch")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class AgentRevokedException(Exception):
|
|
145
|
+
"""Raised when an agent is revoked via MOSS kill-switch."""
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def check_kill_switch(
|
|
150
|
+
agent_id: str,
|
|
151
|
+
*,
|
|
152
|
+
moss_api_key: Optional[str] = None,
|
|
153
|
+
moss_api_url: str = "https://api.mosscomputing.com",
|
|
154
|
+
) -> bool:
|
|
155
|
+
"""
|
|
156
|
+
One-time check if agent is revoked.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
agent_id: The agent ID to check
|
|
160
|
+
moss_api_key: MOSS API key
|
|
161
|
+
moss_api_url: MOSS API base URL
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True if agent is active, False if revoked
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
from moss_hermes import check_kill_switch
|
|
168
|
+
|
|
169
|
+
if not check_kill_switch("my-agent"):
|
|
170
|
+
print("Agent has been revoked!")
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
"""
|
|
173
|
+
monitor = KillSwitchMonitor(
|
|
174
|
+
agent_id=agent_id,
|
|
175
|
+
moss_api_key=moss_api_key,
|
|
176
|
+
moss_api_url=moss_api_url,
|
|
177
|
+
)
|
|
178
|
+
return monitor.check_status()
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MOSS Hermes Integration - Signing Functions
|
|
3
|
+
|
|
4
|
+
Provides explicit signing functions for Hermes agent outputs.
|
|
5
|
+
Compatible with Nous Research Hermes models (Hermes-2, Hermes-3, etc.)
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from moss_hermes import sign_tool_call
|
|
9
|
+
|
|
10
|
+
# After Hermes executes a tool
|
|
11
|
+
signed = sign_tool_call(
|
|
12
|
+
tool_name="execute_code",
|
|
13
|
+
tool_input={"code": "print('hello')"},
|
|
14
|
+
tool_output={"result": "hello", "exit_code": 0},
|
|
15
|
+
agent_id="hermes-coder"
|
|
16
|
+
)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
|
|
22
|
+
from moss import sign, sign_async, verify, SignResult, VerifyResult
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def sign_tool_call(
|
|
26
|
+
tool_name: str,
|
|
27
|
+
tool_input: Dict[str, Any],
|
|
28
|
+
tool_output: Any,
|
|
29
|
+
agent_id: str,
|
|
30
|
+
*,
|
|
31
|
+
parent_sig: Optional[str] = None,
|
|
32
|
+
context: Optional[Dict[str, Any]] = None,
|
|
33
|
+
) -> SignResult:
|
|
34
|
+
"""
|
|
35
|
+
Sign a Hermes tool call execution.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
tool_name: Name of the tool executed (e.g., "send_email", "execute_code")
|
|
39
|
+
tool_input: Parameters passed to the tool
|
|
40
|
+
tool_output: Result returned by the tool
|
|
41
|
+
agent_id: Identifier for the Hermes agent
|
|
42
|
+
parent_sig: Optional parent signature for causal linking
|
|
43
|
+
context: Optional context (user_id, session_id, etc.)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
SignResult with envelope and enterprise info
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
signed = sign_tool_call(
|
|
50
|
+
tool_name="web_search",
|
|
51
|
+
tool_input={"query": "latest AI news"},
|
|
52
|
+
tool_output={"results": [...]},
|
|
53
|
+
agent_id="hermes-researcher"
|
|
54
|
+
)
|
|
55
|
+
"""
|
|
56
|
+
payload = {
|
|
57
|
+
"type": "hermes_tool_call",
|
|
58
|
+
"tool_name": tool_name,
|
|
59
|
+
"tool_input": tool_input,
|
|
60
|
+
"tool_output": _serialize_output(tool_output),
|
|
61
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if parent_sig:
|
|
65
|
+
payload["parent_sig"] = parent_sig
|
|
66
|
+
|
|
67
|
+
return sign(
|
|
68
|
+
output=payload,
|
|
69
|
+
agent_id=agent_id,
|
|
70
|
+
action=f"hermes_tool:{tool_name}",
|
|
71
|
+
context=context,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def sign_tool_call_async(
|
|
76
|
+
tool_name: str,
|
|
77
|
+
tool_input: Dict[str, Any],
|
|
78
|
+
tool_output: Any,
|
|
79
|
+
agent_id: str,
|
|
80
|
+
*,
|
|
81
|
+
parent_sig: Optional[str] = None,
|
|
82
|
+
context: Optional[Dict[str, Any]] = None,
|
|
83
|
+
) -> SignResult:
|
|
84
|
+
"""Async version of sign_tool_call."""
|
|
85
|
+
payload = {
|
|
86
|
+
"type": "hermes_tool_call",
|
|
87
|
+
"tool_name": tool_name,
|
|
88
|
+
"tool_input": tool_input,
|
|
89
|
+
"tool_output": _serialize_output(tool_output),
|
|
90
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if parent_sig:
|
|
94
|
+
payload["parent_sig"] = parent_sig
|
|
95
|
+
|
|
96
|
+
return await sign_async(
|
|
97
|
+
output=payload,
|
|
98
|
+
agent_id=agent_id,
|
|
99
|
+
action=f"hermes_tool:{tool_name}",
|
|
100
|
+
context=context,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def sign_reasoning_chain(
|
|
105
|
+
reasoning_steps: List[Dict[str, Any]],
|
|
106
|
+
final_conclusion: str,
|
|
107
|
+
agent_id: str,
|
|
108
|
+
*,
|
|
109
|
+
parent_sig: Optional[str] = None,
|
|
110
|
+
context: Optional[Dict[str, Any]] = None,
|
|
111
|
+
) -> SignResult:
|
|
112
|
+
"""
|
|
113
|
+
Sign a Hermes reasoning chain (chain-of-thought).
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
reasoning_steps: List of reasoning steps with 'thought' and 'action'
|
|
117
|
+
final_conclusion: The final conclusion or answer
|
|
118
|
+
agent_id: Identifier for the Hermes agent
|
|
119
|
+
parent_sig: Optional parent signature for causal linking
|
|
120
|
+
context: Optional context
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
SignResult with envelope and enterprise info
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
signed = sign_reasoning_chain(
|
|
127
|
+
reasoning_steps=[
|
|
128
|
+
{"thought": "I need to search for...", "action": "web_search"},
|
|
129
|
+
{"thought": "Based on results...", "action": "summarize"}
|
|
130
|
+
],
|
|
131
|
+
final_conclusion="The answer is...",
|
|
132
|
+
agent_id="hermes-analyst"
|
|
133
|
+
)
|
|
134
|
+
"""
|
|
135
|
+
payload = {
|
|
136
|
+
"type": "hermes_reasoning_chain",
|
|
137
|
+
"steps": reasoning_steps,
|
|
138
|
+
"step_count": len(reasoning_steps),
|
|
139
|
+
"conclusion": final_conclusion,
|
|
140
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if parent_sig:
|
|
144
|
+
payload["parent_sig"] = parent_sig
|
|
145
|
+
|
|
146
|
+
return sign(
|
|
147
|
+
output=payload,
|
|
148
|
+
agent_id=agent_id,
|
|
149
|
+
action="hermes_reasoning",
|
|
150
|
+
context=context,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def sign_reasoning_chain_async(
|
|
155
|
+
reasoning_steps: List[Dict[str, Any]],
|
|
156
|
+
final_conclusion: str,
|
|
157
|
+
agent_id: str,
|
|
158
|
+
*,
|
|
159
|
+
parent_sig: Optional[str] = None,
|
|
160
|
+
context: Optional[Dict[str, Any]] = None,
|
|
161
|
+
) -> SignResult:
|
|
162
|
+
"""Async version of sign_reasoning_chain."""
|
|
163
|
+
payload = {
|
|
164
|
+
"type": "hermes_reasoning_chain",
|
|
165
|
+
"steps": reasoning_steps,
|
|
166
|
+
"step_count": len(reasoning_steps),
|
|
167
|
+
"conclusion": final_conclusion,
|
|
168
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if parent_sig:
|
|
172
|
+
payload["parent_sig"] = parent_sig
|
|
173
|
+
|
|
174
|
+
return await sign_async(
|
|
175
|
+
output=payload,
|
|
176
|
+
agent_id=agent_id,
|
|
177
|
+
action="hermes_reasoning",
|
|
178
|
+
context=context,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def sign_memory_retrieval(
|
|
183
|
+
query: str,
|
|
184
|
+
retrieved_memories: List[Dict[str, Any]],
|
|
185
|
+
agent_id: str,
|
|
186
|
+
*,
|
|
187
|
+
memory_type: str = "episodic",
|
|
188
|
+
context: Optional[Dict[str, Any]] = None,
|
|
189
|
+
) -> SignResult:
|
|
190
|
+
"""
|
|
191
|
+
Sign a Hermes memory retrieval event.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
query: The query used for memory retrieval
|
|
195
|
+
retrieved_memories: List of retrieved memory items
|
|
196
|
+
agent_id: Identifier for the Hermes agent
|
|
197
|
+
memory_type: Type of memory (episodic, semantic, procedural)
|
|
198
|
+
context: Optional context
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
SignResult with envelope and enterprise info
|
|
202
|
+
|
|
203
|
+
Example:
|
|
204
|
+
signed = sign_memory_retrieval(
|
|
205
|
+
query="previous conversations about project X",
|
|
206
|
+
retrieved_memories=[{"content": "...", "timestamp": "..."}],
|
|
207
|
+
agent_id="hermes-assistant",
|
|
208
|
+
memory_type="episodic"
|
|
209
|
+
)
|
|
210
|
+
"""
|
|
211
|
+
payload = {
|
|
212
|
+
"type": "hermes_memory_retrieval",
|
|
213
|
+
"query": query,
|
|
214
|
+
"memory_type": memory_type,
|
|
215
|
+
"retrieved_count": len(retrieved_memories),
|
|
216
|
+
"memories": retrieved_memories,
|
|
217
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return sign(
|
|
221
|
+
output=payload,
|
|
222
|
+
agent_id=agent_id,
|
|
223
|
+
action=f"hermes_memory:{memory_type}",
|
|
224
|
+
context=context,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
async def sign_memory_retrieval_async(
|
|
229
|
+
query: str,
|
|
230
|
+
retrieved_memories: List[Dict[str, Any]],
|
|
231
|
+
agent_id: str,
|
|
232
|
+
*,
|
|
233
|
+
memory_type: str = "episodic",
|
|
234
|
+
context: Optional[Dict[str, Any]] = None,
|
|
235
|
+
) -> SignResult:
|
|
236
|
+
"""Async version of sign_memory_retrieval."""
|
|
237
|
+
payload = {
|
|
238
|
+
"type": "hermes_memory_retrieval",
|
|
239
|
+
"query": query,
|
|
240
|
+
"memory_type": memory_type,
|
|
241
|
+
"retrieved_count": len(retrieved_memories),
|
|
242
|
+
"memories": retrieved_memories,
|
|
243
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return await sign_async(
|
|
247
|
+
output=payload,
|
|
248
|
+
agent_id=agent_id,
|
|
249
|
+
action=f"hermes_memory:{memory_type}",
|
|
250
|
+
context=context,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def sign_agent_action(
|
|
255
|
+
action_type: str,
|
|
256
|
+
action_input: Dict[str, Any],
|
|
257
|
+
action_output: Any,
|
|
258
|
+
agent_id: str,
|
|
259
|
+
*,
|
|
260
|
+
parent_sig: Optional[str] = None,
|
|
261
|
+
reasoning: Optional[str] = None,
|
|
262
|
+
context: Optional[Dict[str, Any]] = None,
|
|
263
|
+
) -> SignResult:
|
|
264
|
+
"""
|
|
265
|
+
Sign a generic Hermes agent action.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
action_type: Type of action performed
|
|
269
|
+
action_input: Input parameters for the action
|
|
270
|
+
action_output: Output of the action
|
|
271
|
+
agent_id: Identifier for the Hermes agent
|
|
272
|
+
parent_sig: Optional parent signature for causal linking
|
|
273
|
+
reasoning: Optional reasoning that led to this action
|
|
274
|
+
context: Optional context
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
SignResult with envelope and enterprise info
|
|
278
|
+
"""
|
|
279
|
+
payload = {
|
|
280
|
+
"type": "hermes_agent_action",
|
|
281
|
+
"action_type": action_type,
|
|
282
|
+
"input": action_input,
|
|
283
|
+
"output": _serialize_output(action_output),
|
|
284
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if parent_sig:
|
|
288
|
+
payload["parent_sig"] = parent_sig
|
|
289
|
+
if reasoning:
|
|
290
|
+
payload["reasoning"] = reasoning
|
|
291
|
+
|
|
292
|
+
return sign(
|
|
293
|
+
output=payload,
|
|
294
|
+
agent_id=agent_id,
|
|
295
|
+
action=f"hermes_action:{action_type}",
|
|
296
|
+
context=context,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
async def sign_agent_action_async(
|
|
301
|
+
action_type: str,
|
|
302
|
+
action_input: Dict[str, Any],
|
|
303
|
+
action_output: Any,
|
|
304
|
+
agent_id: str,
|
|
305
|
+
*,
|
|
306
|
+
parent_sig: Optional[str] = None,
|
|
307
|
+
reasoning: Optional[str] = None,
|
|
308
|
+
context: Optional[Dict[str, Any]] = None,
|
|
309
|
+
) -> SignResult:
|
|
310
|
+
"""Async version of sign_agent_action."""
|
|
311
|
+
payload = {
|
|
312
|
+
"type": "hermes_agent_action",
|
|
313
|
+
"action_type": action_type,
|
|
314
|
+
"input": action_input,
|
|
315
|
+
"output": _serialize_output(action_output),
|
|
316
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if parent_sig:
|
|
320
|
+
payload["parent_sig"] = parent_sig
|
|
321
|
+
if reasoning:
|
|
322
|
+
payload["reasoning"] = reasoning
|
|
323
|
+
|
|
324
|
+
return await sign_async(
|
|
325
|
+
output=payload,
|
|
326
|
+
agent_id=agent_id,
|
|
327
|
+
action=f"hermes_action:{action_type}",
|
|
328
|
+
context=context,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def sign_function_call(
|
|
333
|
+
function_name: str,
|
|
334
|
+
arguments: Dict[str, Any],
|
|
335
|
+
result: Any,
|
|
336
|
+
agent_id: str,
|
|
337
|
+
*,
|
|
338
|
+
parent_sig: Optional[str] = None,
|
|
339
|
+
context: Optional[Dict[str, Any]] = None,
|
|
340
|
+
) -> SignResult:
|
|
341
|
+
"""
|
|
342
|
+
Sign a Hermes function call (OpenAI-compatible function calling).
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
function_name: Name of the function called
|
|
346
|
+
arguments: Arguments passed to the function
|
|
347
|
+
result: Result of the function call
|
|
348
|
+
agent_id: Identifier for the Hermes agent
|
|
349
|
+
parent_sig: Optional parent signature for causal linking
|
|
350
|
+
context: Optional context
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
SignResult with envelope and enterprise info
|
|
354
|
+
|
|
355
|
+
Example:
|
|
356
|
+
signed = sign_function_call(
|
|
357
|
+
function_name="get_weather",
|
|
358
|
+
arguments={"city": "San Francisco"},
|
|
359
|
+
result={"temp": 65, "condition": "sunny"},
|
|
360
|
+
agent_id="hermes-weather-bot"
|
|
361
|
+
)
|
|
362
|
+
"""
|
|
363
|
+
payload = {
|
|
364
|
+
"type": "hermes_function_call",
|
|
365
|
+
"function_name": function_name,
|
|
366
|
+
"arguments": arguments,
|
|
367
|
+
"result": _serialize_output(result),
|
|
368
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if parent_sig:
|
|
372
|
+
payload["parent_sig"] = parent_sig
|
|
373
|
+
|
|
374
|
+
return sign(
|
|
375
|
+
output=payload,
|
|
376
|
+
agent_id=agent_id,
|
|
377
|
+
action=f"hermes_function:{function_name}",
|
|
378
|
+
context=context,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
async def sign_function_call_async(
|
|
383
|
+
function_name: str,
|
|
384
|
+
arguments: Dict[str, Any],
|
|
385
|
+
result: Any,
|
|
386
|
+
agent_id: str,
|
|
387
|
+
*,
|
|
388
|
+
parent_sig: Optional[str] = None,
|
|
389
|
+
context: Optional[Dict[str, Any]] = None,
|
|
390
|
+
) -> SignResult:
|
|
391
|
+
"""Async version of sign_function_call."""
|
|
392
|
+
payload = {
|
|
393
|
+
"type": "hermes_function_call",
|
|
394
|
+
"function_name": function_name,
|
|
395
|
+
"arguments": arguments,
|
|
396
|
+
"result": _serialize_output(result),
|
|
397
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if parent_sig:
|
|
401
|
+
payload["parent_sig"] = parent_sig
|
|
402
|
+
|
|
403
|
+
return await sign_async(
|
|
404
|
+
output=payload,
|
|
405
|
+
agent_id=agent_id,
|
|
406
|
+
action=f"hermes_function:{function_name}",
|
|
407
|
+
context=context,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def verify_envelope(envelope: Any, payload: Any = None) -> VerifyResult:
|
|
412
|
+
"""
|
|
413
|
+
Verify a signed envelope.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
envelope: MOSS Envelope or dict
|
|
417
|
+
payload: Original payload for hash verification (optional)
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
VerifyResult with valid=True/False and details
|
|
421
|
+
"""
|
|
422
|
+
return verify(envelope, payload)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _serialize_output(output: Any) -> Any:
|
|
426
|
+
"""Serialize output to JSON-compatible format."""
|
|
427
|
+
if output is None:
|
|
428
|
+
return None
|
|
429
|
+
if isinstance(output, (str, int, float, bool)):
|
|
430
|
+
return output
|
|
431
|
+
if isinstance(output, dict):
|
|
432
|
+
return output
|
|
433
|
+
if isinstance(output, list):
|
|
434
|
+
return output
|
|
435
|
+
if hasattr(output, "model_dump"):
|
|
436
|
+
return output.model_dump()
|
|
437
|
+
if hasattr(output, "__dict__"):
|
|
438
|
+
return output.__dict__
|
|
439
|
+
return str(output)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MOSS Hermes Wrapper - Automatic signing for Hermes agents
|
|
3
|
+
|
|
4
|
+
Provides a wrapper class and decorator for automatic signing of all tool calls.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
from moss_hermes import MossHermesWrapper
|
|
8
|
+
|
|
9
|
+
# Wrap any Hermes-compatible agent
|
|
10
|
+
agent = MossHermesWrapper(
|
|
11
|
+
agent=my_hermes_agent,
|
|
12
|
+
agent_id="hermes-assistant",
|
|
13
|
+
moss_api_key=os.environ["MOSS_API_KEY"]
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# All tool calls are now automatically signed
|
|
17
|
+
result = agent.run("Search for AI news and summarize")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Any, Callable, Dict, Optional, List
|
|
21
|
+
from functools import wraps
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
from .signing import sign_tool_call, sign_function_call, sign_agent_action
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MossHermesWrapper:
|
|
28
|
+
"""
|
|
29
|
+
Wrapper for Hermes agents that automatically signs all tool/function calls.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
from transformers import AutoModelForCausalLM
|
|
33
|
+
from moss_hermes import MossHermesWrapper
|
|
34
|
+
|
|
35
|
+
model = AutoModelForCausalLM.from_pretrained("NousResearch/Hermes-3-Llama-3.1-8B")
|
|
36
|
+
|
|
37
|
+
wrapped = MossHermesWrapper(
|
|
38
|
+
agent=model,
|
|
39
|
+
agent_id="hermes-3-assistant",
|
|
40
|
+
moss_api_key=os.environ["MOSS_API_KEY"]
|
|
41
|
+
)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
agent: Any,
|
|
47
|
+
agent_id: str,
|
|
48
|
+
*,
|
|
49
|
+
moss_api_key: Optional[str] = None,
|
|
50
|
+
auto_sign_tools: bool = True,
|
|
51
|
+
auto_sign_functions: bool = True,
|
|
52
|
+
context: Optional[Dict[str, Any]] = None,
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Initialize the MOSS Hermes wrapper.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
agent: The Hermes agent/model to wrap
|
|
59
|
+
agent_id: Identifier for this agent in MOSS
|
|
60
|
+
moss_api_key: MOSS API key (or set MOSS_API_KEY env var)
|
|
61
|
+
auto_sign_tools: Automatically sign tool calls
|
|
62
|
+
auto_sign_functions: Automatically sign function calls
|
|
63
|
+
context: Default context for all signatures
|
|
64
|
+
"""
|
|
65
|
+
self.agent = agent
|
|
66
|
+
self.agent_id = agent_id
|
|
67
|
+
self.moss_api_key = moss_api_key or os.environ.get("MOSS_API_KEY")
|
|
68
|
+
self.auto_sign_tools = auto_sign_tools
|
|
69
|
+
self.auto_sign_functions = auto_sign_functions
|
|
70
|
+
self.context = context or {}
|
|
71
|
+
self._last_signature: Optional[str] = None
|
|
72
|
+
self._signature_chain: List[str] = []
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def last_signature(self) -> Optional[str]:
|
|
76
|
+
"""Get the last signature generated."""
|
|
77
|
+
return self._last_signature
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def signature_chain(self) -> List[str]:
|
|
81
|
+
"""Get all signatures in the current chain."""
|
|
82
|
+
return self._signature_chain.copy()
|
|
83
|
+
|
|
84
|
+
def clear_chain(self):
|
|
85
|
+
"""Clear the signature chain (start fresh)."""
|
|
86
|
+
self._signature_chain = []
|
|
87
|
+
self._last_signature = None
|
|
88
|
+
|
|
89
|
+
def execute_tool(
|
|
90
|
+
self,
|
|
91
|
+
tool_name: str,
|
|
92
|
+
tool_input: Dict[str, Any],
|
|
93
|
+
tool_executor: Callable,
|
|
94
|
+
) -> Any:
|
|
95
|
+
"""
|
|
96
|
+
Execute a tool and sign the result.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
tool_name: Name of the tool
|
|
100
|
+
tool_input: Input parameters
|
|
101
|
+
tool_executor: Function that executes the tool
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Tool output (with signature attached if enterprise mode)
|
|
105
|
+
"""
|
|
106
|
+
# Execute the tool
|
|
107
|
+
output = tool_executor(tool_input)
|
|
108
|
+
|
|
109
|
+
# Sign if enabled
|
|
110
|
+
if self.auto_sign_tools:
|
|
111
|
+
signed = sign_tool_call(
|
|
112
|
+
tool_name=tool_name,
|
|
113
|
+
tool_input=tool_input,
|
|
114
|
+
tool_output=output,
|
|
115
|
+
agent_id=self.agent_id,
|
|
116
|
+
parent_sig=self._last_signature,
|
|
117
|
+
context=self.context,
|
|
118
|
+
)
|
|
119
|
+
self._last_signature = signed.signature
|
|
120
|
+
self._signature_chain.append(signed.signature)
|
|
121
|
+
|
|
122
|
+
# Check if blocked
|
|
123
|
+
if signed.blocked:
|
|
124
|
+
raise ToolBlockedError(
|
|
125
|
+
f"Tool '{tool_name}' blocked by policy: {signed.enterprise.policy.reason}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return output
|
|
129
|
+
|
|
130
|
+
def execute_function(
|
|
131
|
+
self,
|
|
132
|
+
function_name: str,
|
|
133
|
+
arguments: Dict[str, Any],
|
|
134
|
+
function_executor: Callable,
|
|
135
|
+
) -> Any:
|
|
136
|
+
"""
|
|
137
|
+
Execute a function call and sign the result.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
function_name: Name of the function
|
|
141
|
+
arguments: Function arguments
|
|
142
|
+
function_executor: Function that executes the call
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Function result (with signature attached if enterprise mode)
|
|
146
|
+
"""
|
|
147
|
+
# Execute the function
|
|
148
|
+
result = function_executor(arguments)
|
|
149
|
+
|
|
150
|
+
# Sign if enabled
|
|
151
|
+
if self.auto_sign_functions:
|
|
152
|
+
signed = sign_function_call(
|
|
153
|
+
function_name=function_name,
|
|
154
|
+
arguments=arguments,
|
|
155
|
+
result=result,
|
|
156
|
+
agent_id=self.agent_id,
|
|
157
|
+
parent_sig=self._last_signature,
|
|
158
|
+
context=self.context,
|
|
159
|
+
)
|
|
160
|
+
self._last_signature = signed.signature
|
|
161
|
+
self._signature_chain.append(signed.signature)
|
|
162
|
+
|
|
163
|
+
# Check if blocked
|
|
164
|
+
if signed.blocked:
|
|
165
|
+
raise FunctionBlockedError(
|
|
166
|
+
f"Function '{function_name}' blocked by policy: {signed.enterprise.policy.reason}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
def __getattr__(self, name: str) -> Any:
|
|
172
|
+
"""Delegate attribute access to wrapped agent."""
|
|
173
|
+
return getattr(self.agent, name)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class ToolBlockedError(Exception):
|
|
177
|
+
"""Raised when a tool call is blocked by MOSS policy."""
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class FunctionBlockedError(Exception):
|
|
182
|
+
"""Raised when a function call is blocked by MOSS policy."""
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def moss_signed(
|
|
187
|
+
agent_id: str,
|
|
188
|
+
*,
|
|
189
|
+
action_type: str = "tool_call",
|
|
190
|
+
context: Optional[Dict[str, Any]] = None,
|
|
191
|
+
):
|
|
192
|
+
"""
|
|
193
|
+
Decorator to automatically sign function/tool outputs.
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
@moss_signed(agent_id="hermes-bot", action_type="web_search")
|
|
197
|
+
def search_web(query: str) -> dict:
|
|
198
|
+
# ... perform search ...
|
|
199
|
+
return results
|
|
200
|
+
|
|
201
|
+
# Function output is automatically signed
|
|
202
|
+
results = search_web("latest AI news")
|
|
203
|
+
"""
|
|
204
|
+
def decorator(func: Callable) -> Callable:
|
|
205
|
+
@wraps(func)
|
|
206
|
+
def wrapper(*args, **kwargs):
|
|
207
|
+
# Extract input
|
|
208
|
+
tool_input = {"args": args, "kwargs": kwargs}
|
|
209
|
+
|
|
210
|
+
# Execute function
|
|
211
|
+
result = func(*args, **kwargs)
|
|
212
|
+
|
|
213
|
+
# Sign the result
|
|
214
|
+
signed = sign_agent_action(
|
|
215
|
+
action_type=action_type,
|
|
216
|
+
action_input=tool_input,
|
|
217
|
+
action_output=result,
|
|
218
|
+
agent_id=agent_id,
|
|
219
|
+
context=context,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Check if blocked
|
|
223
|
+
if signed.blocked:
|
|
224
|
+
raise ToolBlockedError(
|
|
225
|
+
f"Action blocked by policy: {signed.enterprise.policy.reason}"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return result
|
|
229
|
+
|
|
230
|
+
return wrapper
|
|
231
|
+
return decorator
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "moss-hermes"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MOSS integration for Nous Research Hermes agents - cryptographic signing for AI actions"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "MOSS Computing", email = "support@mosscomputing.com" }
|
|
13
|
+
]
|
|
14
|
+
keywords = [
|
|
15
|
+
"moss",
|
|
16
|
+
"hermes",
|
|
17
|
+
"nous-research",
|
|
18
|
+
"ai-governance",
|
|
19
|
+
"cryptographic-signing",
|
|
20
|
+
"ml-dsa-44",
|
|
21
|
+
"post-quantum",
|
|
22
|
+
"ai-agents",
|
|
23
|
+
"llm",
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 4 - Beta",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3.9",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Topic :: Security :: Cryptography",
|
|
35
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
36
|
+
]
|
|
37
|
+
requires-python = ">=3.9"
|
|
38
|
+
dependencies = [
|
|
39
|
+
"moss-sdk>=0.2.0",
|
|
40
|
+
"requests>=2.28.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
dev = [
|
|
45
|
+
"pytest>=7.0.0",
|
|
46
|
+
"pytest-asyncio>=0.21.0",
|
|
47
|
+
"black>=23.0.0",
|
|
48
|
+
"mypy>=1.0.0",
|
|
49
|
+
"ruff>=0.1.0",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://mosscomputing.com"
|
|
54
|
+
Documentation = "https://docs.mosscomputing.com/hermes"
|
|
55
|
+
Repository = "https://github.com/mosscomputing/moss-hermes"
|
|
56
|
+
Changelog = "https://github.com/mosscomputing/moss-hermes/blob/main/CHANGELOG.md"
|
|
57
|
+
|
|
58
|
+
[tool.hatch.build.targets.wheel]
|
|
59
|
+
packages = ["moss_hermes"]
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
asyncio_mode = "auto"
|
|
63
|
+
|
|
64
|
+
[tool.black]
|
|
65
|
+
line-length = 100
|
|
66
|
+
target-version = ["py39", "py310", "py311", "py312"]
|
|
67
|
+
|
|
68
|
+
[tool.ruff]
|
|
69
|
+
line-length = 100
|
|
70
|
+
select = ["E", "F", "I", "W"]
|
|
71
|
+
|
|
72
|
+
[tool.mypy]
|
|
73
|
+
python_version = "3.9"
|
|
74
|
+
warn_return_any = true
|
|
75
|
+
warn_unused_ignores = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# MOSS Hermes Tests
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Tests for MOSS Hermes signing functions."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import patch, MagicMock
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestSignToolCall:
|
|
8
|
+
"""Tests for sign_tool_call function."""
|
|
9
|
+
|
|
10
|
+
def test_sign_tool_call_basic(self):
|
|
11
|
+
"""Test basic tool call signing."""
|
|
12
|
+
with patch("moss_hermes.signing.sign") as mock_sign:
|
|
13
|
+
mock_sign.return_value = MagicMock(
|
|
14
|
+
signature="test_sig",
|
|
15
|
+
blocked=False,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from moss_hermes import sign_tool_call
|
|
19
|
+
|
|
20
|
+
result = sign_tool_call(
|
|
21
|
+
tool_name="test_tool",
|
|
22
|
+
tool_input={"key": "value"},
|
|
23
|
+
tool_output={"result": "success"},
|
|
24
|
+
agent_id="test-agent"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
assert result.signature == "test_sig"
|
|
28
|
+
assert not result.blocked
|
|
29
|
+
mock_sign.assert_called_once()
|
|
30
|
+
|
|
31
|
+
def test_sign_tool_call_with_parent(self):
|
|
32
|
+
"""Test tool call signing with parent signature."""
|
|
33
|
+
with patch("moss_hermes.signing.sign") as mock_sign:
|
|
34
|
+
mock_sign.return_value = MagicMock(
|
|
35
|
+
signature="child_sig",
|
|
36
|
+
blocked=False,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
from moss_hermes import sign_tool_call
|
|
40
|
+
|
|
41
|
+
result = sign_tool_call(
|
|
42
|
+
tool_name="child_tool",
|
|
43
|
+
tool_input={},
|
|
44
|
+
tool_output={},
|
|
45
|
+
agent_id="test-agent",
|
|
46
|
+
parent_sig="parent_sig_123"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
call_args = mock_sign.call_args
|
|
50
|
+
assert call_args[1]["output"]["parent_sig"] == "parent_sig_123"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestSignReasoningChain:
|
|
54
|
+
"""Tests for sign_reasoning_chain function."""
|
|
55
|
+
|
|
56
|
+
def test_sign_reasoning_chain(self):
|
|
57
|
+
"""Test reasoning chain signing."""
|
|
58
|
+
with patch("moss_hermes.signing.sign") as mock_sign:
|
|
59
|
+
mock_sign.return_value = MagicMock(
|
|
60
|
+
signature="reasoning_sig",
|
|
61
|
+
blocked=False,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
from moss_hermes import sign_reasoning_chain
|
|
65
|
+
|
|
66
|
+
result = sign_reasoning_chain(
|
|
67
|
+
reasoning_steps=[
|
|
68
|
+
{"thought": "Step 1", "action": "search"},
|
|
69
|
+
{"thought": "Step 2", "action": "summarize"}
|
|
70
|
+
],
|
|
71
|
+
final_conclusion="The answer is 42",
|
|
72
|
+
agent_id="test-agent"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
assert result.signature == "reasoning_sig"
|
|
76
|
+
call_args = mock_sign.call_args
|
|
77
|
+
assert call_args[1]["output"]["step_count"] == 2
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestSignFunctionCall:
|
|
81
|
+
"""Tests for sign_function_call function."""
|
|
82
|
+
|
|
83
|
+
def test_sign_function_call(self):
|
|
84
|
+
"""Test function call signing."""
|
|
85
|
+
with patch("moss_hermes.signing.sign") as mock_sign:
|
|
86
|
+
mock_sign.return_value = MagicMock(
|
|
87
|
+
signature="func_sig",
|
|
88
|
+
blocked=False,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
from moss_hermes import sign_function_call
|
|
92
|
+
|
|
93
|
+
result = sign_function_call(
|
|
94
|
+
function_name="get_weather",
|
|
95
|
+
arguments={"city": "NYC"},
|
|
96
|
+
result={"temp": 72},
|
|
97
|
+
agent_id="test-agent"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert result.signature == "func_sig"
|
|
101
|
+
call_args = mock_sign.call_args
|
|
102
|
+
assert call_args[1]["action"] == "hermes_function:get_weather"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TestKillSwitch:
|
|
106
|
+
"""Tests for kill-switch functionality."""
|
|
107
|
+
|
|
108
|
+
def test_check_kill_switch_active(self):
|
|
109
|
+
"""Test kill-switch check returns True for active agent."""
|
|
110
|
+
with patch("moss_hermes.kill_switch.requests.get") as mock_get:
|
|
111
|
+
mock_get.return_value = MagicMock(
|
|
112
|
+
status_code=200,
|
|
113
|
+
json=lambda: {"status": "active"}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
from moss_hermes import check_kill_switch
|
|
117
|
+
|
|
118
|
+
result = check_kill_switch(
|
|
119
|
+
"test-agent",
|
|
120
|
+
moss_api_key="test_key"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
assert result is True
|
|
124
|
+
|
|
125
|
+
def test_check_kill_switch_revoked(self):
|
|
126
|
+
"""Test kill-switch check returns False for revoked agent."""
|
|
127
|
+
with patch("moss_hermes.kill_switch.requests.get") as mock_get:
|
|
128
|
+
mock_get.return_value = MagicMock(
|
|
129
|
+
status_code=200,
|
|
130
|
+
json=lambda: {"status": "revoked"}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
from moss_hermes import check_kill_switch
|
|
134
|
+
|
|
135
|
+
result = check_kill_switch(
|
|
136
|
+
"test-agent",
|
|
137
|
+
moss_api_key="test_key"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
assert result is False
|