agentguard-hermes-plugin 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.
@@ -0,0 +1,3 @@
1
+ .hermes/
2
+ dist/
3
+ .pypirc
@@ -0,0 +1,11 @@
1
+ name: agentguard
2
+ description: "Forwards Hermes gateway lifecycle events to AgentGuard for real-time security monitoring. NOTE: gateway hooks only fire when running 'hermes gateway' — not in CLI mode. For CLI coverage, use the plugin (plugin.py)."
3
+ events:
4
+ - gateway:startup
5
+ - session:start
6
+ - session:end
7
+ - session:reset
8
+ - agent:start
9
+ - agent:step
10
+ - agent:end
11
+ - command:*
@@ -0,0 +1,22 @@
1
+ PYTHON := python3.12
2
+ PYPIRC := .pypirc
3
+
4
+ .PHONY: build publish install clean
5
+
6
+ ## Build sdist + wheel into dist/
7
+ build:
8
+ $(PYTHON) -m hatch build
9
+
10
+ ## Upload to PyPI using .pypirc credentials
11
+ publish: build
12
+ $(PYTHON) -m pip install twine --break-system-packages --quiet
13
+ $(PYTHON) -m twine upload --config-file $(PYPIRC) dist/*
14
+
15
+ ## Install the plugin into ~/.hermes/plugins/agentguard/ from local source
16
+ install:
17
+ $(PYTHON) -m pip install -e . --break-system-packages --quiet
18
+ agentguard-hermes install
19
+
20
+ ## Remove build artifacts
21
+ clean:
22
+ rm -rf dist/ *.egg-info agentguard_hermes.egg-info
@@ -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,84 @@
1
+ # agentguard-hermes
2
+
3
+ AgentGuard security monitoring plugin for [Hermes Agent](https://github.com/nousresearch/hermes-agent).
4
+
5
+ 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.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install agentguard-hermes-plugin
11
+ agentguard-hermes install
12
+ ```
13
+
14
+ This copies the plugin into `~/.hermes/plugins/agentguard/` where Hermes auto-loads it on startup.
15
+
16
+ ## Configuration
17
+
18
+ Set environment variables before starting Hermes:
19
+
20
+ | Variable | Default | Description |
21
+ |---|---|---|
22
+ | `AGENTGUARD_TIMEPLUS_URL` | `http://localhost:3218` | Timeplus Enterprise HTTP endpoint |
23
+ | `AGENTGUARD_USERNAME` | `proton` | Timeplus username |
24
+ | `AGENTGUARD_PASSWORD` | _(empty)_ | Timeplus password |
25
+ | `AGENTGUARD_AGENT_ID` | hostname | Identifier for this agent instance |
26
+ | `AGENTGUARD_DEPLOYMENT_ID` | `local` | Deployment environment tag |
27
+ | `AGENTGUARD_DEPLOYMENT_NAME` | `Local Dev` | Human-readable deployment name |
28
+ | `AGENTGUARD_STREAM` | `agentguard_hook_events` | Target Timeplus stream |
29
+
30
+ ### Example
31
+
32
+ ```bash
33
+ export AGENTGUARD_TIMEPLUS_URL=http://timeplus.example.com:3218
34
+ export AGENTGUARD_USERNAME=proton
35
+ export AGENTGUARD_PASSWORD=secret
36
+ export AGENTGUARD_DEPLOYMENT_ID=production
37
+ export AGENTGUARD_DEPLOYMENT_NAME="Production Hermes"
38
+
39
+ hermes
40
+ ```
41
+
42
+ ## What gets captured
43
+
44
+ Every Hermes hook event is forwarded to the `agentguard_hook_events` stream:
45
+
46
+ | Hermes hook | Sent as | Description |
47
+ |---|---|---|
48
+ | `on_session_start` | `on_session_start` | New session begins |
49
+ | `on_session_end` | `conversation_end` | Single chat turn completed |
50
+ | `on_session_finalize` | `on_session_end` | Session fully torn down (CLI exit, `/reset`) |
51
+ | `on_session_reset` | `on_session_reset` | Session rotated via `/new` |
52
+ | `pre_llm_call` | `pre_llm_call` | Before each LLM turn |
53
+ | `post_llm_call` | `post_llm_call` | After each LLM turn |
54
+ | `pre_api_request` | `pre_api_request` | Before each raw API call (token metrics) |
55
+ | `post_api_request` | `post_api_request` | After each raw API call |
56
+ | `pre_tool_call` | `pre_tool_call` | Before each tool execution |
57
+ | `post_tool_call` | `post_tool_call` | After each tool execution |
58
+
59
+ `conversation_history` is stripped from all events before ingestion — it is unbounded and contains no information not already available from the individual turn events.
60
+
61
+ ## Custom install path
62
+
63
+ ```bash
64
+ agentguard-hermes install --hermes-dir /path/to/hermes/data
65
+ ```
66
+
67
+ ## Manual installation (Makefile / Docker)
68
+
69
+ If you run Hermes via the provided Docker Compose setup in `agents/hermes/`:
70
+
71
+ ```bash
72
+ make configure # copies plugin into .hermes/plugins/agentguard/
73
+ make cli # start Hermes CLI with AgentGuard env vars pre-set
74
+ make start # start Hermes gateway + web dashboard
75
+ ```
76
+
77
+ ## Publishing to PyPI
78
+
79
+ ```bash
80
+ pip install hatch
81
+ cd agents/hermes/agentguard-plugin
82
+ hatch build # produces dist/agentguard_hermes_plugin-*.whl
83
+ hatch publish
84
+ ```
@@ -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,142 @@
1
+ """
2
+ AgentGuard gateway hook for Hermes Agent.
3
+
4
+ Forwards Hermes gateway lifecycle events to the agentguard_hook_events Timeplus
5
+ stream for real-time security monitoring.
6
+
7
+ IMPORTANT: Gateway hooks only fire when Hermes runs in gateway mode
8
+ ('hermes gateway run' / Docker). They do NOT fire in CLI mode ('hermes').
9
+ For CLI + gateway coverage, use the companion plugin (plugin.py).
10
+
11
+ Installed into ~/.hermes/hooks/agentguard/ via 'make configure'.
12
+
13
+ Events captured (from ~/.hermes/hooks/agentguard/HOOK.yaml):
14
+ gateway:startup — gateway process started
15
+ session:start — new messaging session created
16
+ session:end — session ended
17
+ session:reset — user ran /new or /reset
18
+ agent:start — agent begins processing a message
19
+ agent:step — each tool-calling loop iteration
20
+ agent:end — agent finished processing
21
+ command:* — any slash command executed
22
+
23
+ Context keys per event (from official docs):
24
+ gateway:startup → { platforms }
25
+ session:start → { platform, user_id, session_id, session_key }
26
+ session:end → { platform, user_id, session_key }
27
+ session:reset → { platform, user_id, session_key }
28
+ agent:start → { platform, user_id, session_id, message }
29
+ agent:step → { platform, user_id, session_id, iteration, tool_names }
30
+ agent:end → { platform, user_id, session_id, message, response }
31
+ command:* → { platform, user_id, command, args }
32
+
33
+ Config via environment variables:
34
+ AGENTGUARD_TIMEPLUS_URL — Timeplus HTTP endpoint (default: http://localhost:3218)
35
+ AGENTGUARD_STREAM — target stream name (default: agentguard_hook_events)
36
+ AGENTGUARD_USERNAME — Timeplus username (default: proton)
37
+ AGENTGUARD_PASSWORD — Timeplus password (default: empty)
38
+ AGENTGUARD_AGENT_ID — agent identifier (default: hostname)
39
+ AGENTGUARD_DEPLOYMENT_ID — deployment tag (default: local)
40
+ AGENTGUARD_DEPLOYMENT_NAME — human-readable deployment name (default: Local Dev)
41
+ """
42
+
43
+ import json
44
+ import os
45
+ import socket
46
+ import urllib.error
47
+ import urllib.request
48
+ from base64 import b64encode
49
+ from datetime import datetime, timezone
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Config
53
+ # ---------------------------------------------------------------------------
54
+ _TIMEPLUS_URL = os.environ.get("AGENTGUARD_TIMEPLUS_URL", "http://localhost:3218")
55
+ _STREAM = os.environ.get("AGENTGUARD_STREAM", "agentguard_hook_events")
56
+ _USERNAME = os.environ.get("AGENTGUARD_USERNAME", "proton")
57
+ _PASSWORD = os.environ.get("AGENTGUARD_PASSWORD", "")
58
+ _AGENT_ID = os.environ.get("AGENTGUARD_AGENT_ID") or socket.gethostname()
59
+ _DEPLOYMENT_ID = os.environ.get("AGENTGUARD_DEPLOYMENT_ID", "local")
60
+ _DEPLOYMENT_NAME = os.environ.get("AGENTGUARD_DEPLOYMENT_NAME", "Local Dev")
61
+
62
+ _COLUMNS = [
63
+ "hook_name", "event_time", "session_id", "run_id",
64
+ "agent_id", "deployment_id", "deployment_name", "session_key",
65
+ "tool_name", "provider", "model", "hook_decision",
66
+ "block_reason", "event_data", "agent_type",
67
+ ]
68
+
69
+ # Hermes gateway event → AgentGuard hook_name
70
+ _EVENT_MAP = {
71
+ "gateway:startup": "session_start", # gateway process start; no session yet
72
+ "session:start": "session_start",
73
+ "session:end": "session_end",
74
+ "session:reset": "session_end",
75
+ "agent:start": "user_prompt_submit",
76
+ "agent:step": "before_tool_call",
77
+ "agent:end": "llm_output",
78
+ # command:* is handled via wildcard match — mapped to "permission_denied" equivalent
79
+ }
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Ingest
83
+ # ---------------------------------------------------------------------------
84
+ def _ingest(rows: list) -> None:
85
+ url = f"{_TIMEPLUS_URL.rstrip('/')}/proton/v1/ingest/streams/{_STREAM}"
86
+ payload = json.dumps({"columns": _COLUMNS, "data": rows}).encode()
87
+ req = urllib.request.Request(url, data=payload, method="POST")
88
+ req.add_header("Content-Type", "application/json")
89
+ if _USERNAME:
90
+ token = b64encode(f"{_USERNAME}:{_PASSWORD}".encode()).decode()
91
+ req.add_header("Authorization", f"Basic {token}")
92
+ try:
93
+ with urllib.request.urlopen(req, timeout=5) as resp:
94
+ resp.read()
95
+ except urllib.error.URLError as e:
96
+ print(f"[agentguard] ingest error: {e}", flush=True)
97
+
98
+
99
+ def _row(hook_name: str, ctx: dict, tool_name: str = "", model: str = "") -> list:
100
+ return [
101
+ hook_name,
102
+ datetime.now(timezone.utc).isoformat(),
103
+ ctx.get("session_id", ""),
104
+ ctx.get("session_id", ""), # run_id mirrors session_id
105
+ _AGENT_ID,
106
+ _DEPLOYMENT_ID,
107
+ _DEPLOYMENT_NAME,
108
+ ctx.get("session_key", ""),
109
+ tool_name,
110
+ "", # provider — not exposed by gateway hooks
111
+ model,
112
+ "observe",
113
+ "",
114
+ json.dumps(ctx),
115
+ "hermes",
116
+ ]
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Hook entry point — called by Hermes for each registered event
121
+ # ---------------------------------------------------------------------------
122
+ def handle(event_type: str, context: dict) -> None:
123
+ # command:* wildcard — map all slash commands to a dedicated hook_name
124
+ if event_type.startswith("command:"):
125
+ cmd = context.get("command", event_type.replace("command:", ""))
126
+ rows = [_row("slash_command", context, tool_name=cmd)]
127
+ _ingest(rows)
128
+ return
129
+
130
+ hook_name = _EVENT_MAP.get(event_type)
131
+ if hook_name is None:
132
+ return
133
+
134
+ # agent:step carries a list of tool names — emit one row per tool so
135
+ # AgentGuard rule MVs can match on individual tool_name values.
136
+ if event_type == "agent:step":
137
+ tool_names = context.get("tool_names") or []
138
+ rows = [_row(hook_name, context, t) for t in tool_names] if tool_names else [_row(hook_name, context)]
139
+ else:
140
+ rows = [_row(hook_name, context)]
141
+
142
+ _ingest(rows)
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agentguard-hermes-plugin"
7
+ version = "0.1.0"
8
+ description = "AgentGuard security monitoring plugin for Hermes Agent"
9
+ readme = "README.md"
10
+ license = { text = "Apache-2.0" }
11
+ requires-python = ">=3.10"
12
+ keywords = ["agentguard", "hermes", "security", "ai-agent", "monitoring", "observability"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: Apache Software License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Security",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/timeplus/agentguard"
27
+ Repository = "https://github.com/timeplus/agentguard"
28
+
29
+ [project.scripts]
30
+ agentguard-hermes = "agentguard_hermes.cli:main"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["agentguard_hermes"]