agentguard-hermes-plugin 0.1.0__py3-none-any.whl
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.
- agentguard_hermes/__init__.py +1 -0
- agentguard_hermes/cli.py +55 -0
- agentguard_hermes/plugin.py +142 -0
- agentguard_hermes/plugin.yaml +14 -0
- agentguard_hermes_plugin-0.1.0.dist-info/METADATA +104 -0
- agentguard_hermes_plugin-0.1.0.dist-info/RECORD +8 -0
- agentguard_hermes_plugin-0.1.0.dist-info/WHEEL +4 -0
- agentguard_hermes_plugin-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
agentguard_hermes/cli.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import importlib.resources
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
prog="agentguard-hermes",
|
|
11
|
+
description="AgentGuard security monitoring plugin for Hermes Agent",
|
|
12
|
+
)
|
|
13
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
14
|
+
|
|
15
|
+
install_cmd = sub.add_parser("install", help="Install the plugin into Hermes data directory")
|
|
16
|
+
install_cmd.add_argument(
|
|
17
|
+
"--hermes-dir",
|
|
18
|
+
default=os.path.expanduser("~/.hermes"),
|
|
19
|
+
metavar="DIR",
|
|
20
|
+
help="Hermes data directory (default: ~/.hermes)",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
args = parser.parse_args()
|
|
24
|
+
|
|
25
|
+
if args.command == "install":
|
|
26
|
+
_install(args.hermes_dir)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _install(hermes_dir: str) -> None:
|
|
30
|
+
plugin_dir = os.path.join(hermes_dir, "plugins", "agentguard")
|
|
31
|
+
os.makedirs(plugin_dir, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
pkg = importlib.resources.files("agentguard_hermes")
|
|
34
|
+
|
|
35
|
+
with importlib.resources.as_file(pkg / "plugin.yaml") as src:
|
|
36
|
+
shutil.copy2(src, os.path.join(plugin_dir, "plugin.yaml"))
|
|
37
|
+
|
|
38
|
+
with importlib.resources.as_file(pkg / "plugin.py") as src:
|
|
39
|
+
shutil.copy2(src, os.path.join(plugin_dir, "__init__.py"))
|
|
40
|
+
|
|
41
|
+
print(f"AgentGuard plugin installed → {plugin_dir}")
|
|
42
|
+
print()
|
|
43
|
+
print("Configure via environment variables before starting Hermes:")
|
|
44
|
+
print()
|
|
45
|
+
print(" AGENTGUARD_TIMEPLUS_URL (default: http://localhost:3218)")
|
|
46
|
+
print(" AGENTGUARD_USERNAME (default: proton)")
|
|
47
|
+
print(" AGENTGUARD_PASSWORD")
|
|
48
|
+
print(" AGENTGUARD_AGENT_ID (default: hostname)")
|
|
49
|
+
print(" AGENTGUARD_DEPLOYMENT_ID (default: local)")
|
|
50
|
+
print(" AGENTGUARD_DEPLOYMENT_NAME (default: Local Dev)")
|
|
51
|
+
print(" AGENTGUARD_STREAM (default: agentguard_hook_events)")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
main()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentGuard plugin for Hermes Agent.
|
|
3
|
+
|
|
4
|
+
Forwards raw Hermes plugin hook events to the AgentGuard Timeplus stream.
|
|
5
|
+
All kwargs are stored verbatim in event_data. conversation_history is dropped
|
|
6
|
+
— it only contains user/assistant turns (no system prompt) and grows
|
|
7
|
+
unboundedly. The system prompt is not exposed by any Hermes plugin hook.
|
|
8
|
+
|
|
9
|
+
Plugin hooks (CLI + gateway):
|
|
10
|
+
on_session_start session_id, model, platform
|
|
11
|
+
on_session_end session_id, completed, interrupted, model, platform
|
|
12
|
+
on_session_finalize session_id (may be None), platform
|
|
13
|
+
on_session_reset session_id, platform
|
|
14
|
+
pre_llm_call session_id, conversation_history, is_first_turn, model, platform, sender_id
|
|
15
|
+
post_llm_call session_id, conversation_history, model, platform
|
|
16
|
+
pre_tool_call tool_name, args, task_id, session_id, tool_call_id
|
|
17
|
+
post_tool_call tool_name, args, result, task_id, session_id, tool_call_id
|
|
18
|
+
pre_api_request session_id, model, provider, approx_input_tokens, message_count, ...
|
|
19
|
+
post_api_request session_id, model, provider, usage{tokens}, api_duration, ...
|
|
20
|
+
|
|
21
|
+
Hook name mapping (Hermes → AgentGuard):
|
|
22
|
+
on_session_end → "conversation_end" (fires per chat turn, not true session end)
|
|
23
|
+
on_session_finalize → "on_session_end" (true session teardown)
|
|
24
|
+
all others → unchanged
|
|
25
|
+
|
|
26
|
+
Run ID: session_id (all events in a session share the same run_id).
|
|
27
|
+
|
|
28
|
+
Install: drop plugin.yaml + __init__.py into ~/.hermes/plugins/agentguard/
|
|
29
|
+
|
|
30
|
+
Config via environment variables:
|
|
31
|
+
AGENTGUARD_TIMEPLUS_URL AGENTGUARD_STREAM AGENTGUARD_USERNAME
|
|
32
|
+
AGENTGUARD_PASSWORD AGENTGUARD_AGENT_ID AGENTGUARD_DEPLOYMENT_ID
|
|
33
|
+
AGENTGUARD_DEPLOYMENT_NAME
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import json
|
|
37
|
+
import os
|
|
38
|
+
import socket
|
|
39
|
+
import urllib.error
|
|
40
|
+
import urllib.request
|
|
41
|
+
from base64 import b64encode
|
|
42
|
+
from datetime import datetime, timezone
|
|
43
|
+
|
|
44
|
+
_TIMEPLUS_URL = os.environ.get("AGENTGUARD_TIMEPLUS_URL", "http://localhost:3218")
|
|
45
|
+
_STREAM = os.environ.get("AGENTGUARD_STREAM", "agentguard_hook_events")
|
|
46
|
+
_USERNAME = os.environ.get("AGENTGUARD_USERNAME", "proton")
|
|
47
|
+
_PASSWORD = os.environ.get("AGENTGUARD_PASSWORD", "")
|
|
48
|
+
_AGENT_ID = os.environ.get("AGENTGUARD_AGENT_ID") or socket.gethostname()
|
|
49
|
+
_DEPLOYMENT_ID = os.environ.get("AGENTGUARD_DEPLOYMENT_ID", "local")
|
|
50
|
+
_DEPLOYMENT_NAME = os.environ.get("AGENTGUARD_DEPLOYMENT_NAME", "Local Dev")
|
|
51
|
+
|
|
52
|
+
_COLUMNS = [
|
|
53
|
+
"hook_name", "event_time", "session_id", "run_id",
|
|
54
|
+
"agent_id", "deployment_id", "deployment_name", "session_key",
|
|
55
|
+
"tool_name", "provider", "model", "hook_decision",
|
|
56
|
+
"block_reason", "event_data", "agent_type",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
_SKIP_KEYS = {"conversation_history"}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _ingest(rows: list) -> None:
|
|
63
|
+
url = f"{_TIMEPLUS_URL.rstrip('/')}/proton/v1/ingest/streams/{_STREAM}"
|
|
64
|
+
payload = json.dumps({"columns": _COLUMNS, "data": rows}).encode()
|
|
65
|
+
req = urllib.request.Request(url, data=payload, method="POST")
|
|
66
|
+
req.add_header("Content-Type", "application/json")
|
|
67
|
+
if _USERNAME:
|
|
68
|
+
token = b64encode(f"{_USERNAME}:{_PASSWORD}".encode()).decode()
|
|
69
|
+
req.add_header("Authorization", f"Basic {token}")
|
|
70
|
+
try:
|
|
71
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
72
|
+
resp.read()
|
|
73
|
+
except urllib.error.URLError as e:
|
|
74
|
+
print(f"[agentguard] ingest error: {e}", flush=True)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _make_row(hook_name: str, kwargs: dict, *, session_id: str = None) -> list:
|
|
78
|
+
sid = session_id if session_id is not None else (
|
|
79
|
+
kwargs.get("session_id") or kwargs.get("task_id") or ""
|
|
80
|
+
)
|
|
81
|
+
data = {k: v for k, v in kwargs.items() if k not in _SKIP_KEYS}
|
|
82
|
+
return [
|
|
83
|
+
hook_name,
|
|
84
|
+
datetime.now(timezone.utc).isoformat(),
|
|
85
|
+
sid,
|
|
86
|
+
sid, # run_id = session_id
|
|
87
|
+
_AGENT_ID,
|
|
88
|
+
_DEPLOYMENT_ID,
|
|
89
|
+
_DEPLOYMENT_NAME,
|
|
90
|
+
"", # session_key — not available in plugin hooks
|
|
91
|
+
kwargs.get("tool_name") or "",
|
|
92
|
+
kwargs.get("provider") or "",
|
|
93
|
+
kwargs.get("model") or "",
|
|
94
|
+
"observe",
|
|
95
|
+
"",
|
|
96
|
+
json.dumps(data, default=str),
|
|
97
|
+
"hermes",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _hook(hook_name: str):
|
|
102
|
+
def _cb(**kwargs):
|
|
103
|
+
_ingest([_make_row(hook_name, kwargs)])
|
|
104
|
+
_cb.__name__ = f"_{hook_name}"
|
|
105
|
+
return _cb
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# Lifecycle hooks with non-trivial semantics
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def _on_session_end(**kwargs):
|
|
113
|
+
"""
|
|
114
|
+
Hermes fires this after every run_conversation() call (per chat turn).
|
|
115
|
+
Reported as 'conversation_end' — NOT treated as session termination by AgentGuard.
|
|
116
|
+
"""
|
|
117
|
+
_ingest([_make_row("conversation_end", kwargs)])
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _on_session_finalize(**kwargs):
|
|
121
|
+
"""
|
|
122
|
+
True session teardown (CLI exit, /new, /reset, gateway GC).
|
|
123
|
+
Reported as 'on_session_end' so AgentGuard marks the session as closed.
|
|
124
|
+
"""
|
|
125
|
+
sid = kwargs.get("session_id") or ""
|
|
126
|
+
_ingest([_make_row("on_session_end", kwargs, session_id=sid)])
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# Plugin registration — called by Hermes on plugin load
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
def register(ctx):
|
|
133
|
+
ctx.register_hook("on_session_start", _hook("on_session_start"))
|
|
134
|
+
ctx.register_hook("on_session_end", _on_session_end) # per-turn → conversation_end
|
|
135
|
+
ctx.register_hook("on_session_finalize", _on_session_finalize) # true end → on_session_end
|
|
136
|
+
ctx.register_hook("on_session_reset", _hook("on_session_reset"))
|
|
137
|
+
ctx.register_hook("pre_llm_call", _hook("pre_llm_call"))
|
|
138
|
+
ctx.register_hook("post_llm_call", _hook("post_llm_call"))
|
|
139
|
+
ctx.register_hook("pre_api_request", _hook("pre_api_request"))
|
|
140
|
+
ctx.register_hook("post_api_request", _hook("post_api_request"))
|
|
141
|
+
ctx.register_hook("pre_tool_call", _hook("pre_tool_call"))
|
|
142
|
+
ctx.register_hook("post_tool_call", _hook("post_tool_call"))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
name: agentguard
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
description: "Forwards Hermes session and tool events to AgentGuard for real-time security monitoring."
|
|
4
|
+
hooks:
|
|
5
|
+
- on_session_start
|
|
6
|
+
- on_session_end
|
|
7
|
+
- on_session_finalize
|
|
8
|
+
- on_session_reset
|
|
9
|
+
- pre_llm_call
|
|
10
|
+
- post_llm_call
|
|
11
|
+
- pre_api_request
|
|
12
|
+
- post_api_request
|
|
13
|
+
- pre_tool_call
|
|
14
|
+
- post_tool_call
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentguard-hermes-plugin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AgentGuard security monitoring plugin for Hermes Agent
|
|
5
|
+
Project-URL: Homepage, https://github.com/timeplus/agentguard
|
|
6
|
+
Project-URL: Repository, https://github.com/timeplus/agentguard
|
|
7
|
+
License: Apache-2.0
|
|
8
|
+
Keywords: agentguard,ai-agent,hermes,monitoring,observability,security
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# agentguard-hermes
|
|
22
|
+
|
|
23
|
+
AgentGuard security monitoring plugin for [Hermes Agent](https://github.com/nousresearch/hermes-agent).
|
|
24
|
+
|
|
25
|
+
Forwards Hermes session, LLM, and tool-call events to an [AgentGuard](https://github.com/timeplus/agentguard) Timeplus stream for real-time observability and security monitoring.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install agentguard-hermes-plugin
|
|
31
|
+
agentguard-hermes install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This copies the plugin into `~/.hermes/plugins/agentguard/` where Hermes auto-loads it on startup.
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
Set environment variables before starting Hermes:
|
|
39
|
+
|
|
40
|
+
| Variable | Default | Description |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `AGENTGUARD_TIMEPLUS_URL` | `http://localhost:3218` | Timeplus Enterprise HTTP endpoint |
|
|
43
|
+
| `AGENTGUARD_USERNAME` | `proton` | Timeplus username |
|
|
44
|
+
| `AGENTGUARD_PASSWORD` | _(empty)_ | Timeplus password |
|
|
45
|
+
| `AGENTGUARD_AGENT_ID` | hostname | Identifier for this agent instance |
|
|
46
|
+
| `AGENTGUARD_DEPLOYMENT_ID` | `local` | Deployment environment tag |
|
|
47
|
+
| `AGENTGUARD_DEPLOYMENT_NAME` | `Local Dev` | Human-readable deployment name |
|
|
48
|
+
| `AGENTGUARD_STREAM` | `agentguard_hook_events` | Target Timeplus stream |
|
|
49
|
+
|
|
50
|
+
### Example
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
export AGENTGUARD_TIMEPLUS_URL=http://timeplus.example.com:3218
|
|
54
|
+
export AGENTGUARD_USERNAME=proton
|
|
55
|
+
export AGENTGUARD_PASSWORD=secret
|
|
56
|
+
export AGENTGUARD_DEPLOYMENT_ID=production
|
|
57
|
+
export AGENTGUARD_DEPLOYMENT_NAME="Production Hermes"
|
|
58
|
+
|
|
59
|
+
hermes
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## What gets captured
|
|
63
|
+
|
|
64
|
+
Every Hermes hook event is forwarded to the `agentguard_hook_events` stream:
|
|
65
|
+
|
|
66
|
+
| Hermes hook | Sent as | Description |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `on_session_start` | `on_session_start` | New session begins |
|
|
69
|
+
| `on_session_end` | `conversation_end` | Single chat turn completed |
|
|
70
|
+
| `on_session_finalize` | `on_session_end` | Session fully torn down (CLI exit, `/reset`) |
|
|
71
|
+
| `on_session_reset` | `on_session_reset` | Session rotated via `/new` |
|
|
72
|
+
| `pre_llm_call` | `pre_llm_call` | Before each LLM turn |
|
|
73
|
+
| `post_llm_call` | `post_llm_call` | After each LLM turn |
|
|
74
|
+
| `pre_api_request` | `pre_api_request` | Before each raw API call (token metrics) |
|
|
75
|
+
| `post_api_request` | `post_api_request` | After each raw API call |
|
|
76
|
+
| `pre_tool_call` | `pre_tool_call` | Before each tool execution |
|
|
77
|
+
| `post_tool_call` | `post_tool_call` | After each tool execution |
|
|
78
|
+
|
|
79
|
+
`conversation_history` is stripped from all events before ingestion — it is unbounded and contains no information not already available from the individual turn events.
|
|
80
|
+
|
|
81
|
+
## Custom install path
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
agentguard-hermes install --hermes-dir /path/to/hermes/data
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Manual installation (Makefile / Docker)
|
|
88
|
+
|
|
89
|
+
If you run Hermes via the provided Docker Compose setup in `agents/hermes/`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
make configure # copies plugin into .hermes/plugins/agentguard/
|
|
93
|
+
make cli # start Hermes CLI with AgentGuard env vars pre-set
|
|
94
|
+
make start # start Hermes gateway + web dashboard
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Publishing to PyPI
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install hatch
|
|
101
|
+
cd agents/hermes/agentguard-plugin
|
|
102
|
+
hatch build # produces dist/agentguard_hermes_plugin-*.whl
|
|
103
|
+
hatch publish
|
|
104
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
agentguard_hermes/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
agentguard_hermes/cli.py,sha256=v0bcMqSprY0afhs4cOc3V3_v6wx2ZiJ6nbFpAqIuql8,1818
|
|
3
|
+
agentguard_hermes/plugin.py,sha256=sU8Tpch7aN0VMerBvuJ_gTh61OxP6gtyx3dbRva_x9o,5984
|
|
4
|
+
agentguard_hermes/plugin.yaml,sha256=lTRr22MHEf6HHiJHXgfxIB79aiqbf781-ZcuISsVXoo,341
|
|
5
|
+
agentguard_hermes_plugin-0.1.0.dist-info/METADATA,sha256=nNCFrHU2MwyM1qCfTPZoTwXZe8jzS_P2NODMVLKEk-U,3884
|
|
6
|
+
agentguard_hermes_plugin-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
agentguard_hermes_plugin-0.1.0.dist-info/entry_points.txt,sha256=jnGpaZskCqHUbTirwLgwwyrBP2Kn5hm_vLQhk-BszdI,65
|
|
8
|
+
agentguard_hermes_plugin-0.1.0.dist-info/RECORD,,
|