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.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agentguard-hermes = agentguard_hermes.cli:main